##// END OF EJS Templates
comments: introduce new draft comments....
milka -
r4540:25406ecd default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -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
@@ -1,60 +1,60 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
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 os
21 import os
22 from collections import OrderedDict
22 from collections import OrderedDict
23
23
24 import sys
24 import sys
25 import platform
25 import platform
26
26
27 VERSION = tuple(open(os.path.join(
27 VERSION = tuple(open(os.path.join(
28 os.path.dirname(__file__), 'VERSION')).read().split('.'))
28 os.path.dirname(__file__), 'VERSION')).read().split('.'))
29
29
30 BACKENDS = OrderedDict()
30 BACKENDS = OrderedDict()
31
31
32 BACKENDS['hg'] = 'Mercurial repository'
32 BACKENDS['hg'] = 'Mercurial repository'
33 BACKENDS['git'] = 'Git repository'
33 BACKENDS['git'] = 'Git repository'
34 BACKENDS['svn'] = 'Subversion repository'
34 BACKENDS['svn'] = 'Subversion repository'
35
35
36
36
37 CELERY_ENABLED = False
37 CELERY_ENABLED = False
38 CELERY_EAGER = False
38 CELERY_EAGER = False
39
39
40 # link to config for pyramid
40 # link to config for pyramid
41 CONFIG = {}
41 CONFIG = {}
42
42
43 # Populated with the settings dictionary from application init in
43 # Populated with the settings dictionary from application init in
44 # rhodecode.conf.environment.load_pyramid_environment
44 # rhodecode.conf.environment.load_pyramid_environment
45 PYRAMID_SETTINGS = {}
45 PYRAMID_SETTINGS = {}
46
46
47 # Linked module for extensions
47 # Linked module for extensions
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__ = 111 # 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'
55 __url__ = 'https://code.rhodecode.com'
55 __url__ = 'https://code.rhodecode.com'
56
56
57 is_windows = __platform__ in ['Windows']
57 is_windows = __platform__ in ['Windows']
58 is_unix = not is_windows
58 is_unix = not is_windows
59 is_test = False
59 is_test = False
60 disable_error_handler = False
60 disable_error_handler = False
@@ -1,1816 +1,1826 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
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 logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
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
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 # backward compat., we use for OLD PRs a plain renderer
66 # backward compat., we use for OLD PRs a plain renderer
67 c.renderer = 'plain'
67 c.renderer = 'plain'
68 return c
68 return c
69
69
70 def _get_pull_requests_list(
70 def _get_pull_requests_list(
71 self, repo_name, source, filter_type, opened_by, statuses):
71 self, repo_name, source, filter_type, opened_by, statuses):
72
72
73 draw, start, limit = self._extract_chunk(self.request)
73 draw, start, limit = self._extract_chunk(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 _render = self.request.get_partial_renderer(
75 _render = self.request.get_partial_renderer(
76 'rhodecode:templates/data_table/_dt_elements.mako')
76 'rhodecode:templates/data_table/_dt_elements.mako')
77
77
78 # pagination
78 # pagination
79
79
80 if filter_type == 'awaiting_review':
80 if filter_type == 'awaiting_review':
81 pull_requests = PullRequestModel().get_awaiting_review(
81 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
83 statuses=statuses, offset=start, length=limit,
83 statuses=statuses, offset=start, length=limit,
84 order_by=order_by, order_dir=order_dir)
84 order_by=order_by, order_dir=order_dir)
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name, search_q=search_q, source=source, statuses=statuses,
86 repo_name, search_q=search_q, source=source, statuses=statuses,
87 opened_by=opened_by)
87 opened_by=opened_by)
88 elif filter_type == 'awaiting_my_review':
88 elif filter_type == 'awaiting_my_review':
89 pull_requests = PullRequestModel().get_awaiting_my_review(
89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
92 offset=start, length=limit, order_by=order_by,
92 offset=start, length=limit, order_by=order_by,
93 order_dir=order_dir)
93 order_dir=order_dir)
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
96 statuses=statuses, opened_by=opened_by)
96 statuses=statuses, opened_by=opened_by)
97 else:
97 else:
98 pull_requests = PullRequestModel().get_all(
98 pull_requests = PullRequestModel().get_all(
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 statuses=statuses, offset=start, length=limit,
100 statuses=statuses, offset=start, length=limit,
101 order_by=order_by, order_dir=order_dir)
101 order_by=order_by, order_dir=order_dir)
102 pull_requests_total_count = PullRequestModel().count_all(
102 pull_requests_total_count = PullRequestModel().count_all(
103 repo_name, search_q=search_q, source=source, statuses=statuses,
103 repo_name, search_q=search_q, source=source, statuses=statuses,
104 opened_by=opened_by)
104 opened_by=opened_by)
105
105
106 data = []
106 data = []
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, count_only=True)
111
111
112 data.append({
112 data.append({
113 'name': _render('pullrequest_name',
113 'name': _render('pullrequest_name',
114 pr.pull_request_id, pr.pull_request_state,
114 pr.pull_request_id, pr.pull_request_state,
115 pr.work_in_progress, pr.target_repo.repo_name,
115 pr.work_in_progress, pr.target_repo.repo_name,
116 short=True),
116 short=True),
117 'name_raw': pr.pull_request_id,
117 'name_raw': pr.pull_request_id,
118 'status': _render('pullrequest_status',
118 'status': _render('pullrequest_status',
119 pr.calculated_review_status()),
119 pr.calculated_review_status()),
120 'title': _render('pullrequest_title', pr.title, pr.description),
120 'title': _render('pullrequest_title', pr.title, pr.description),
121 'description': h.escape(pr.description),
121 'description': h.escape(pr.description),
122 'updated_on': _render('pullrequest_updated_on',
122 'updated_on': _render('pullrequest_updated_on',
123 h.datetime_to_time(pr.updated_on)),
123 h.datetime_to_time(pr.updated_on)),
124 'updated_on_raw': h.datetime_to_time(pr.updated_on),
124 'updated_on_raw': h.datetime_to_time(pr.updated_on),
125 'created_on': _render('pullrequest_updated_on',
125 'created_on': _render('pullrequest_updated_on',
126 h.datetime_to_time(pr.created_on)),
126 h.datetime_to_time(pr.created_on)),
127 'created_on_raw': h.datetime_to_time(pr.created_on),
127 'created_on_raw': h.datetime_to_time(pr.created_on),
128 'state': pr.pull_request_state,
128 'state': pr.pull_request_state,
129 'author': _render('pullrequest_author',
129 'author': _render('pullrequest_author',
130 pr.author.full_contact, ),
130 pr.author.full_contact, ),
131 'author_raw': pr.author.full_name,
131 'author_raw': pr.author.full_name,
132 'comments': _render('pullrequest_comments', comments_count),
132 'comments': _render('pullrequest_comments', comments_count),
133 'comments_raw': comments_count,
133 'comments_raw': comments_count,
134 'closed': pr.is_closed(),
134 'closed': pr.is_closed(),
135 })
135 })
136
136
137 data = ({
137 data = ({
138 'draw': draw,
138 'draw': draw,
139 'data': data,
139 'data': data,
140 'recordsTotal': pull_requests_total_count,
140 'recordsTotal': pull_requests_total_count,
141 'recordsFiltered': pull_requests_total_count,
141 'recordsFiltered': pull_requests_total_count,
142 })
142 })
143 return data
143 return data
144
144
145 @LoginRequired()
145 @LoginRequired()
146 @HasRepoPermissionAnyDecorator(
146 @HasRepoPermissionAnyDecorator(
147 'repository.read', 'repository.write', 'repository.admin')
147 'repository.read', 'repository.write', 'repository.admin')
148 @view_config(
148 @view_config(
149 route_name='pullrequest_show_all', request_method='GET',
149 route_name='pullrequest_show_all', request_method='GET',
150 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
150 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
151 def pull_request_list(self):
151 def pull_request_list(self):
152 c = self.load_default_context()
152 c = self.load_default_context()
153
153
154 req_get = self.request.GET
154 req_get = self.request.GET
155 c.source = str2bool(req_get.get('source'))
155 c.source = str2bool(req_get.get('source'))
156 c.closed = str2bool(req_get.get('closed'))
156 c.closed = str2bool(req_get.get('closed'))
157 c.my = str2bool(req_get.get('my'))
157 c.my = str2bool(req_get.get('my'))
158 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
158 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
159 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
159 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
160
160
161 c.active = 'open'
161 c.active = 'open'
162 if c.my:
162 if c.my:
163 c.active = 'my'
163 c.active = 'my'
164 if c.closed:
164 if c.closed:
165 c.active = 'closed'
165 c.active = 'closed'
166 if c.awaiting_review and not c.source:
166 if c.awaiting_review and not c.source:
167 c.active = 'awaiting'
167 c.active = 'awaiting'
168 if c.source and not c.awaiting_review:
168 if c.source and not c.awaiting_review:
169 c.active = 'source'
169 c.active = 'source'
170 if c.awaiting_my_review:
170 if c.awaiting_my_review:
171 c.active = 'awaiting_my'
171 c.active = 'awaiting_my'
172
172
173 return self._get_template_context(c)
173 return self._get_template_context(c)
174
174
175 @LoginRequired()
175 @LoginRequired()
176 @HasRepoPermissionAnyDecorator(
176 @HasRepoPermissionAnyDecorator(
177 'repository.read', 'repository.write', 'repository.admin')
177 'repository.read', 'repository.write', 'repository.admin')
178 @view_config(
178 @view_config(
179 route_name='pullrequest_show_all_data', request_method='GET',
179 route_name='pullrequest_show_all_data', request_method='GET',
180 renderer='json_ext', xhr=True)
180 renderer='json_ext', xhr=True)
181 def pull_request_list_data(self):
181 def pull_request_list_data(self):
182 self.load_default_context()
182 self.load_default_context()
183
183
184 # additional filters
184 # additional filters
185 req_get = self.request.GET
185 req_get = self.request.GET
186 source = str2bool(req_get.get('source'))
186 source = str2bool(req_get.get('source'))
187 closed = str2bool(req_get.get('closed'))
187 closed = str2bool(req_get.get('closed'))
188 my = str2bool(req_get.get('my'))
188 my = str2bool(req_get.get('my'))
189 awaiting_review = str2bool(req_get.get('awaiting_review'))
189 awaiting_review = str2bool(req_get.get('awaiting_review'))
190 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
190 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
191
191
192 filter_type = 'awaiting_review' if awaiting_review \
192 filter_type = 'awaiting_review' if awaiting_review \
193 else 'awaiting_my_review' if awaiting_my_review \
193 else 'awaiting_my_review' if awaiting_my_review \
194 else None
194 else None
195
195
196 opened_by = None
196 opened_by = None
197 if my:
197 if my:
198 opened_by = [self._rhodecode_user.user_id]
198 opened_by = [self._rhodecode_user.user_id]
199
199
200 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
200 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
201 if closed:
201 if closed:
202 statuses = [PullRequest.STATUS_CLOSED]
202 statuses = [PullRequest.STATUS_CLOSED]
203
203
204 data = self._get_pull_requests_list(
204 data = self._get_pull_requests_list(
205 repo_name=self.db_repo_name, source=source,
205 repo_name=self.db_repo_name, source=source,
206 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
206 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
207
207
208 return data
208 return data
209
209
210 def _is_diff_cache_enabled(self, target_repo):
210 def _is_diff_cache_enabled(self, target_repo):
211 caching_enabled = self._get_general_setting(
211 caching_enabled = self._get_general_setting(
212 target_repo, 'rhodecode_diff_cache')
212 target_repo, 'rhodecode_diff_cache')
213 log.debug('Diff caching enabled: %s', caching_enabled)
213 log.debug('Diff caching enabled: %s', caching_enabled)
214 return caching_enabled
214 return caching_enabled
215
215
216 def _get_diffset(self, source_repo_name, source_repo,
216 def _get_diffset(self, source_repo_name, source_repo,
217 ancestor_commit,
217 ancestor_commit,
218 source_ref_id, target_ref_id,
218 source_ref_id, target_ref_id,
219 target_commit, source_commit, diff_limit, file_limit,
219 target_commit, source_commit, diff_limit, file_limit,
220 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
220 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
221
221
222 if use_ancestor:
222 if use_ancestor:
223 # we might want to not use it for versions
223 # we might want to not use it for versions
224 target_ref_id = ancestor_commit.raw_id
224 target_ref_id = ancestor_commit.raw_id
225
225
226 vcs_diff = PullRequestModel().get_diff(
226 vcs_diff = PullRequestModel().get_diff(
227 source_repo, source_ref_id, target_ref_id,
227 source_repo, source_ref_id, target_ref_id,
228 hide_whitespace_changes, diff_context)
228 hide_whitespace_changes, diff_context)
229
229
230 diff_processor = diffs.DiffProcessor(
230 diff_processor = diffs.DiffProcessor(
231 vcs_diff, format='newdiff', diff_limit=diff_limit,
231 vcs_diff, format='newdiff', diff_limit=diff_limit,
232 file_limit=file_limit, show_full_diff=fulldiff)
232 file_limit=file_limit, show_full_diff=fulldiff)
233
233
234 _parsed = diff_processor.prepare()
234 _parsed = diff_processor.prepare()
235
235
236 diffset = codeblocks.DiffSet(
236 diffset = codeblocks.DiffSet(
237 repo_name=self.db_repo_name,
237 repo_name=self.db_repo_name,
238 source_repo_name=source_repo_name,
238 source_repo_name=source_repo_name,
239 source_node_getter=codeblocks.diffset_node_getter(target_commit),
239 source_node_getter=codeblocks.diffset_node_getter(target_commit),
240 target_node_getter=codeblocks.diffset_node_getter(source_commit),
240 target_node_getter=codeblocks.diffset_node_getter(source_commit),
241 )
241 )
242 diffset = self.path_filter.render_patchset_filtered(
242 diffset = self.path_filter.render_patchset_filtered(
243 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
243 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
244
244
245 return diffset
245 return diffset
246
246
247 def _get_range_diffset(self, source_scm, source_repo,
247 def _get_range_diffset(self, source_scm, source_repo,
248 commit1, commit2, diff_limit, file_limit,
248 commit1, commit2, diff_limit, file_limit,
249 fulldiff, hide_whitespace_changes, diff_context):
249 fulldiff, hide_whitespace_changes, diff_context):
250 vcs_diff = source_scm.get_diff(
250 vcs_diff = source_scm.get_diff(
251 commit1, commit2,
251 commit1, commit2,
252 ignore_whitespace=hide_whitespace_changes,
252 ignore_whitespace=hide_whitespace_changes,
253 context=diff_context)
253 context=diff_context)
254
254
255 diff_processor = diffs.DiffProcessor(
255 diff_processor = diffs.DiffProcessor(
256 vcs_diff, format='newdiff', diff_limit=diff_limit,
256 vcs_diff, format='newdiff', diff_limit=diff_limit,
257 file_limit=file_limit, show_full_diff=fulldiff)
257 file_limit=file_limit, show_full_diff=fulldiff)
258
258
259 _parsed = diff_processor.prepare()
259 _parsed = diff_processor.prepare()
260
260
261 diffset = codeblocks.DiffSet(
261 diffset = codeblocks.DiffSet(
262 repo_name=source_repo.repo_name,
262 repo_name=source_repo.repo_name,
263 source_node_getter=codeblocks.diffset_node_getter(commit1),
263 source_node_getter=codeblocks.diffset_node_getter(commit1),
264 target_node_getter=codeblocks.diffset_node_getter(commit2))
264 target_node_getter=codeblocks.diffset_node_getter(commit2))
265
265
266 diffset = self.path_filter.render_patchset_filtered(
266 diffset = self.path_filter.render_patchset_filtered(
267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
268
268
269 return diffset
269 return diffset
270
270
271 def register_comments_vars(self, c, pull_request, versions):
271 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
272 comments_model = CommentsModel()
272 comments_model = CommentsModel()
273
273
274 # GENERAL COMMENTS with versions #
274 # GENERAL COMMENTS with versions #
275 q = comments_model._all_general_comments_of_pull_request(pull_request)
275 q = comments_model._all_general_comments_of_pull_request(pull_request)
276 q = q.order_by(ChangesetComment.comment_id.asc())
276 q = q.order_by(ChangesetComment.comment_id.asc())
277 if not include_drafts:
278 q = q.filter(ChangesetComment.draft == false())
277 general_comments = q
279 general_comments = q
278
280
279 # pick comments we want to render at current version
281 # pick comments we want to render at current version
280 c.comment_versions = comments_model.aggregate_comments(
282 c.comment_versions = comments_model.aggregate_comments(
281 general_comments, versions, c.at_version_num)
283 general_comments, versions, c.at_version_num)
282
284
283 # INLINE COMMENTS with versions #
285 # INLINE COMMENTS with versions #
284 q = comments_model._all_inline_comments_of_pull_request(pull_request)
286 q = comments_model._all_inline_comments_of_pull_request(pull_request)
285 q = q.order_by(ChangesetComment.comment_id.asc())
287 q = q.order_by(ChangesetComment.comment_id.asc())
288 if not include_drafts:
289 q = q.filter(ChangesetComment.draft == false())
286 inline_comments = q
290 inline_comments = q
287
291
288 c.inline_versions = comments_model.aggregate_comments(
292 c.inline_versions = comments_model.aggregate_comments(
289 inline_comments, versions, c.at_version_num, inline=True)
293 inline_comments, versions, c.at_version_num, inline=True)
290
294
291 # Comments inline+general
295 # Comments inline+general
292 if c.at_version:
296 if c.at_version:
293 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
297 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
294 c.comments = c.comment_versions[c.at_version_num]['display']
298 c.comments = c.comment_versions[c.at_version_num]['display']
295 else:
299 else:
296 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
300 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
297 c.comments = c.comment_versions[c.at_version_num]['until']
301 c.comments = c.comment_versions[c.at_version_num]['until']
298
302
299 return general_comments, inline_comments
303 return general_comments, inline_comments
300
304
301 @LoginRequired()
305 @LoginRequired()
302 @HasRepoPermissionAnyDecorator(
306 @HasRepoPermissionAnyDecorator(
303 'repository.read', 'repository.write', 'repository.admin')
307 'repository.read', 'repository.write', 'repository.admin')
304 @view_config(
308 @view_config(
305 route_name='pullrequest_show', request_method='GET',
309 route_name='pullrequest_show', request_method='GET',
306 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
310 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
307 def pull_request_show(self):
311 def pull_request_show(self):
308 _ = self.request.translate
312 _ = self.request.translate
309 c = self.load_default_context()
313 c = self.load_default_context()
310
314
311 pull_request = PullRequest.get_or_404(
315 pull_request = PullRequest.get_or_404(
312 self.request.matchdict['pull_request_id'])
316 self.request.matchdict['pull_request_id'])
313 pull_request_id = pull_request.pull_request_id
317 pull_request_id = pull_request.pull_request_id
314
318
315 c.state_progressing = pull_request.is_state_changing()
319 c.state_progressing = pull_request.is_state_changing()
316 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
320 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
317
321
318 _new_state = {
322 _new_state = {
319 'created': PullRequest.STATE_CREATED,
323 'created': PullRequest.STATE_CREATED,
320 }.get(self.request.GET.get('force_state'))
324 }.get(self.request.GET.get('force_state'))
321
325
322 if c.is_super_admin and _new_state:
326 if c.is_super_admin and _new_state:
323 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
327 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
324 h.flash(
328 h.flash(
325 _('Pull Request state was force changed to `{}`').format(_new_state),
329 _('Pull Request state was force changed to `{}`').format(_new_state),
326 category='success')
330 category='success')
327 Session().commit()
331 Session().commit()
328
332
329 raise HTTPFound(h.route_path(
333 raise HTTPFound(h.route_path(
330 'pullrequest_show', repo_name=self.db_repo_name,
334 'pullrequest_show', repo_name=self.db_repo_name,
331 pull_request_id=pull_request_id))
335 pull_request_id=pull_request_id))
332
336
333 version = self.request.GET.get('version')
337 version = self.request.GET.get('version')
334 from_version = self.request.GET.get('from_version') or version
338 from_version = self.request.GET.get('from_version') or version
335 merge_checks = self.request.GET.get('merge_checks')
339 merge_checks = self.request.GET.get('merge_checks')
336 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
340 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
337 force_refresh = str2bool(self.request.GET.get('force_refresh'))
341 force_refresh = str2bool(self.request.GET.get('force_refresh'))
338 c.range_diff_on = self.request.GET.get('range-diff') == "1"
342 c.range_diff_on = self.request.GET.get('range-diff') == "1"
339
343
340 # fetch global flags of ignore ws or context lines
344 # fetch global flags of ignore ws or context lines
341 diff_context = diffs.get_diff_context(self.request)
345 diff_context = diffs.get_diff_context(self.request)
342 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
346 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
343
347
344 (pull_request_latest,
348 (pull_request_latest,
345 pull_request_at_ver,
349 pull_request_at_ver,
346 pull_request_display_obj,
350 pull_request_display_obj,
347 at_version) = PullRequestModel().get_pr_version(
351 at_version) = PullRequestModel().get_pr_version(
348 pull_request_id, version=version)
352 pull_request_id, version=version)
349
353
350 pr_closed = pull_request_latest.is_closed()
354 pr_closed = pull_request_latest.is_closed()
351
355
352 if pr_closed and (version or from_version):
356 if pr_closed and (version or from_version):
353 # not allow to browse versions for closed PR
357 # not allow to browse versions for closed PR
354 raise HTTPFound(h.route_path(
358 raise HTTPFound(h.route_path(
355 'pullrequest_show', repo_name=self.db_repo_name,
359 'pullrequest_show', repo_name=self.db_repo_name,
356 pull_request_id=pull_request_id))
360 pull_request_id=pull_request_id))
357
361
358 versions = pull_request_display_obj.versions()
362 versions = pull_request_display_obj.versions()
359 # used to store per-commit range diffs
363 # used to store per-commit range diffs
360 c.changes = collections.OrderedDict()
364 c.changes = collections.OrderedDict()
361
365
362 c.at_version = at_version
366 c.at_version = at_version
363 c.at_version_num = (at_version
367 c.at_version_num = (at_version
364 if at_version and at_version != PullRequest.LATEST_VER
368 if at_version and at_version != PullRequest.LATEST_VER
365 else None)
369 else None)
366
370
367 c.at_version_index = ChangesetComment.get_index_from_version(
371 c.at_version_index = ChangesetComment.get_index_from_version(
368 c.at_version_num, versions)
372 c.at_version_num, versions)
369
373
370 (prev_pull_request_latest,
374 (prev_pull_request_latest,
371 prev_pull_request_at_ver,
375 prev_pull_request_at_ver,
372 prev_pull_request_display_obj,
376 prev_pull_request_display_obj,
373 prev_at_version) = PullRequestModel().get_pr_version(
377 prev_at_version) = PullRequestModel().get_pr_version(
374 pull_request_id, version=from_version)
378 pull_request_id, version=from_version)
375
379
376 c.from_version = prev_at_version
380 c.from_version = prev_at_version
377 c.from_version_num = (prev_at_version
381 c.from_version_num = (prev_at_version
378 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
382 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
379 else None)
383 else None)
380 c.from_version_index = ChangesetComment.get_index_from_version(
384 c.from_version_index = ChangesetComment.get_index_from_version(
381 c.from_version_num, versions)
385 c.from_version_num, versions)
382
386
383 # define if we're in COMPARE mode or VIEW at version mode
387 # define if we're in COMPARE mode or VIEW at version mode
384 compare = at_version != prev_at_version
388 compare = at_version != prev_at_version
385
389
386 # pull_requests repo_name we opened it against
390 # pull_requests repo_name we opened it against
387 # ie. target_repo must match
391 # ie. target_repo must match
388 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
392 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
389 log.warning('Mismatch between the current repo: %s, and target %s',
393 log.warning('Mismatch between the current repo: %s, and target %s',
390 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
394 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
391 raise HTTPNotFound()
395 raise HTTPNotFound()
392
396
393 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
397 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
394
398
395 c.pull_request = pull_request_display_obj
399 c.pull_request = pull_request_display_obj
396 c.renderer = pull_request_at_ver.description_renderer or c.renderer
400 c.renderer = pull_request_at_ver.description_renderer or c.renderer
397 c.pull_request_latest = pull_request_latest
401 c.pull_request_latest = pull_request_latest
398
402
399 # inject latest version
403 # inject latest version
400 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
404 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
401 c.versions = versions + [latest_ver]
405 c.versions = versions + [latest_ver]
402
406
403 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
407 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
404 c.allowed_to_change_status = False
408 c.allowed_to_change_status = False
405 c.allowed_to_update = False
409 c.allowed_to_update = False
406 c.allowed_to_merge = False
410 c.allowed_to_merge = False
407 c.allowed_to_delete = False
411 c.allowed_to_delete = False
408 c.allowed_to_comment = False
412 c.allowed_to_comment = False
409 c.allowed_to_close = False
413 c.allowed_to_close = False
410 else:
414 else:
411 can_change_status = PullRequestModel().check_user_change_status(
415 can_change_status = PullRequestModel().check_user_change_status(
412 pull_request_at_ver, self._rhodecode_user)
416 pull_request_at_ver, self._rhodecode_user)
413 c.allowed_to_change_status = can_change_status and not pr_closed
417 c.allowed_to_change_status = can_change_status and not pr_closed
414
418
415 c.allowed_to_update = PullRequestModel().check_user_update(
419 c.allowed_to_update = PullRequestModel().check_user_update(
416 pull_request_latest, self._rhodecode_user) and not pr_closed
420 pull_request_latest, self._rhodecode_user) and not pr_closed
417 c.allowed_to_merge = PullRequestModel().check_user_merge(
421 c.allowed_to_merge = PullRequestModel().check_user_merge(
418 pull_request_latest, self._rhodecode_user) and not pr_closed
422 pull_request_latest, self._rhodecode_user) and not pr_closed
419 c.allowed_to_delete = PullRequestModel().check_user_delete(
423 c.allowed_to_delete = PullRequestModel().check_user_delete(
420 pull_request_latest, self._rhodecode_user) and not pr_closed
424 pull_request_latest, self._rhodecode_user) and not pr_closed
421 c.allowed_to_comment = not pr_closed
425 c.allowed_to_comment = not pr_closed
422 c.allowed_to_close = c.allowed_to_merge and not pr_closed
426 c.allowed_to_close = c.allowed_to_merge and not pr_closed
423
427
424 c.forbid_adding_reviewers = False
428 c.forbid_adding_reviewers = False
425 c.forbid_author_to_review = False
429 c.forbid_author_to_review = False
426 c.forbid_commit_author_to_review = False
430 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')
437 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')
438 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
435 except Exception:
439 except Exception:
436 pass
440 pass
437
441
438 # check merge capabilities
442 # check merge capabilities
439 _merge_check = MergeCheck.validate(
443 _merge_check = MergeCheck.validate(
440 pull_request_latest, auth_user=self._rhodecode_user,
444 pull_request_latest, auth_user=self._rhodecode_user,
441 translator=self.request.translate,
445 translator=self.request.translate,
442 force_shadow_repo_refresh=force_refresh)
446 force_shadow_repo_refresh=force_refresh)
443
447
444 c.pr_merge_errors = _merge_check.error_details
448 c.pr_merge_errors = _merge_check.error_details
445 c.pr_merge_possible = not _merge_check.failed
449 c.pr_merge_possible = not _merge_check.failed
446 c.pr_merge_message = _merge_check.merge_msg
450 c.pr_merge_message = _merge_check.merge_msg
447 c.pr_merge_source_commit = _merge_check.source_commit
451 c.pr_merge_source_commit = _merge_check.source_commit
448 c.pr_merge_target_commit = _merge_check.target_commit
452 c.pr_merge_target_commit = _merge_check.target_commit
449
453
450 c.pr_merge_info = MergeCheck.get_merge_conditions(
454 c.pr_merge_info = MergeCheck.get_merge_conditions(
451 pull_request_latest, translator=self.request.translate)
455 pull_request_latest, translator=self.request.translate)
452
456
453 c.pull_request_review_status = _merge_check.review_status
457 c.pull_request_review_status = _merge_check.review_status
454 if merge_checks:
458 if merge_checks:
455 self.request.override_renderer = \
459 self.request.override_renderer = \
456 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
460 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
457 return self._get_template_context(c)
461 return self._get_template_context(c)
458
462
459 c.reviewers_count = pull_request.reviewers_count
463 c.reviewers_count = pull_request.reviewers_count
460 c.observers_count = pull_request.observers_count
464 c.observers_count = pull_request.observers_count
461
465
462 # reviewers and statuses
466 # reviewers and statuses
463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
467 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
464 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
468 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
465 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
469 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
466
470
467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
471 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
468 member_reviewer = h.reviewer_as_json(
472 member_reviewer = h.reviewer_as_json(
469 member, reasons=reasons, mandatory=mandatory,
473 member, reasons=reasons, mandatory=mandatory,
470 role=review_obj.role,
474 role=review_obj.role,
471 user_group=review_obj.rule_user_group_data()
475 user_group=review_obj.rule_user_group_data()
472 )
476 )
473
477
474 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
478 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
475 member_reviewer['review_status'] = current_review_status
479 member_reviewer['review_status'] = current_review_status
476 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
480 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
477 member_reviewer['allowed_to_update'] = c.allowed_to_update
481 member_reviewer['allowed_to_update'] = c.allowed_to_update
478 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
482 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
479
483
480 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
484 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
481
485
482 for observer_obj, member in pull_request_at_ver.observers():
486 for observer_obj, member in pull_request_at_ver.observers():
483 member_observer = h.reviewer_as_json(
487 member_observer = h.reviewer_as_json(
484 member, reasons=[], mandatory=False,
488 member, reasons=[], mandatory=False,
485 role=observer_obj.role,
489 role=observer_obj.role,
486 user_group=observer_obj.rule_user_group_data()
490 user_group=observer_obj.rule_user_group_data()
487 )
491 )
488 member_observer['allowed_to_update'] = c.allowed_to_update
492 member_observer['allowed_to_update'] = c.allowed_to_update
489 c.pull_request_set_observers_data_json['observers'].append(member_observer)
493 c.pull_request_set_observers_data_json['observers'].append(member_observer)
490
494
491 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
495 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
492
496
493 general_comments, inline_comments = \
497 general_comments, inline_comments = \
494 self.register_comments_vars(c, pull_request_latest, versions)
498 self.register_comments_vars(c, pull_request_latest, versions)
495
499
496 # TODOs
500 # TODOs
497 c.unresolved_comments = CommentsModel() \
501 c.unresolved_comments = CommentsModel() \
498 .get_pull_request_unresolved_todos(pull_request_latest)
502 .get_pull_request_unresolved_todos(pull_request_latest)
499 c.resolved_comments = CommentsModel() \
503 c.resolved_comments = CommentsModel() \
500 .get_pull_request_resolved_todos(pull_request_latest)
504 .get_pull_request_resolved_todos(pull_request_latest)
501
505
502 # if we use version, then do not show later comments
506 # if we use version, then do not show later comments
503 # than current version
507 # than current version
504 display_inline_comments = collections.defaultdict(
508 display_inline_comments = collections.defaultdict(
505 lambda: collections.defaultdict(list))
509 lambda: collections.defaultdict(list))
506 for co in inline_comments:
510 for co in inline_comments:
507 if c.at_version_num:
511 if c.at_version_num:
508 # pick comments that are at least UPTO given version, so we
512 # pick comments that are at least UPTO given version, so we
509 # don't render comments for higher version
513 # don't render comments for higher version
510 should_render = co.pull_request_version_id and \
514 should_render = co.pull_request_version_id and \
511 co.pull_request_version_id <= c.at_version_num
515 co.pull_request_version_id <= c.at_version_num
512 else:
516 else:
513 # showing all, for 'latest'
517 # showing all, for 'latest'
514 should_render = True
518 should_render = True
515
519
516 if should_render:
520 if should_render:
517 display_inline_comments[co.f_path][co.line_no].append(co)
521 display_inline_comments[co.f_path][co.line_no].append(co)
518
522
519 # load diff data into template context, if we use compare mode then
523 # load diff data into template context, if we use compare mode then
520 # diff is calculated based on changes between versions of PR
524 # diff is calculated based on changes between versions of PR
521
525
522 source_repo = pull_request_at_ver.source_repo
526 source_repo = pull_request_at_ver.source_repo
523 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
527 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
524
528
525 target_repo = pull_request_at_ver.target_repo
529 target_repo = pull_request_at_ver.target_repo
526 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
530 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
527
531
528 if compare:
532 if compare:
529 # in compare switch the diff base to latest commit from prev version
533 # in compare switch the diff base to latest commit from prev version
530 target_ref_id = prev_pull_request_display_obj.revisions[0]
534 target_ref_id = prev_pull_request_display_obj.revisions[0]
531
535
532 # despite opening commits for bookmarks/branches/tags, we always
536 # despite opening commits for bookmarks/branches/tags, we always
533 # convert this to rev to prevent changes after bookmark or branch change
537 # convert this to rev to prevent changes after bookmark or branch change
534 c.source_ref_type = 'rev'
538 c.source_ref_type = 'rev'
535 c.source_ref = source_ref_id
539 c.source_ref = source_ref_id
536
540
537 c.target_ref_type = 'rev'
541 c.target_ref_type = 'rev'
538 c.target_ref = target_ref_id
542 c.target_ref = target_ref_id
539
543
540 c.source_repo = source_repo
544 c.source_repo = source_repo
541 c.target_repo = target_repo
545 c.target_repo = target_repo
542
546
543 c.commit_ranges = []
547 c.commit_ranges = []
544 source_commit = EmptyCommit()
548 source_commit = EmptyCommit()
545 target_commit = EmptyCommit()
549 target_commit = EmptyCommit()
546 c.missing_requirements = False
550 c.missing_requirements = False
547
551
548 source_scm = source_repo.scm_instance()
552 source_scm = source_repo.scm_instance()
549 target_scm = target_repo.scm_instance()
553 target_scm = target_repo.scm_instance()
550
554
551 shadow_scm = None
555 shadow_scm = None
552 try:
556 try:
553 shadow_scm = pull_request_latest.get_shadow_repo()
557 shadow_scm = pull_request_latest.get_shadow_repo()
554 except Exception:
558 except Exception:
555 log.debug('Failed to get shadow repo', exc_info=True)
559 log.debug('Failed to get shadow repo', exc_info=True)
556 # try first the existing source_repo, and then shadow
560 # try first the existing source_repo, and then shadow
557 # repo if we can obtain one
561 # repo if we can obtain one
558 commits_source_repo = source_scm
562 commits_source_repo = source_scm
559 if shadow_scm:
563 if shadow_scm:
560 commits_source_repo = shadow_scm
564 commits_source_repo = shadow_scm
561
565
562 c.commits_source_repo = commits_source_repo
566 c.commits_source_repo = commits_source_repo
563 c.ancestor = None # set it to None, to hide it from PR view
567 c.ancestor = None # set it to None, to hide it from PR view
564
568
565 # empty version means latest, so we keep this to prevent
569 # empty version means latest, so we keep this to prevent
566 # double caching
570 # double caching
567 version_normalized = version or PullRequest.LATEST_VER
571 version_normalized = version or PullRequest.LATEST_VER
568 from_version_normalized = from_version or PullRequest.LATEST_VER
572 from_version_normalized = from_version or PullRequest.LATEST_VER
569
573
570 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
574 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
571 cache_file_path = diff_cache_exist(
575 cache_file_path = diff_cache_exist(
572 cache_path, 'pull_request', pull_request_id, version_normalized,
576 cache_path, 'pull_request', pull_request_id, version_normalized,
573 from_version_normalized, source_ref_id, target_ref_id,
577 from_version_normalized, source_ref_id, target_ref_id,
574 hide_whitespace_changes, diff_context, c.fulldiff)
578 hide_whitespace_changes, diff_context, c.fulldiff)
575
579
576 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
580 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
577 force_recache = self.get_recache_flag()
581 force_recache = self.get_recache_flag()
578
582
579 cached_diff = None
583 cached_diff = None
580 if caching_enabled:
584 if caching_enabled:
581 cached_diff = load_cached_diff(cache_file_path)
585 cached_diff = load_cached_diff(cache_file_path)
582
586
583 has_proper_commit_cache = (
587 has_proper_commit_cache = (
584 cached_diff and cached_diff.get('commits')
588 cached_diff and cached_diff.get('commits')
585 and len(cached_diff.get('commits', [])) == 5
589 and len(cached_diff.get('commits', [])) == 5
586 and cached_diff.get('commits')[0]
590 and cached_diff.get('commits')[0]
587 and cached_diff.get('commits')[3])
591 and cached_diff.get('commits')[3])
588
592
589 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
593 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
590 diff_commit_cache = \
594 diff_commit_cache = \
591 (ancestor_commit, commit_cache, missing_requirements,
595 (ancestor_commit, commit_cache, missing_requirements,
592 source_commit, target_commit) = cached_diff['commits']
596 source_commit, target_commit) = cached_diff['commits']
593 else:
597 else:
594 # NOTE(marcink): we reach potentially unreachable errors when a PR has
598 # NOTE(marcink): we reach potentially unreachable errors when a PR has
595 # merge errors resulting in potentially hidden commits in the shadow repo.
599 # merge errors resulting in potentially hidden commits in the shadow repo.
596 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
600 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
597 and _merge_check.merge_response
601 and _merge_check.merge_response
598 maybe_unreachable = maybe_unreachable \
602 maybe_unreachable = maybe_unreachable \
599 and _merge_check.merge_response.metadata.get('unresolved_files')
603 and _merge_check.merge_response.metadata.get('unresolved_files')
600 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
604 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
601 diff_commit_cache = \
605 diff_commit_cache = \
602 (ancestor_commit, commit_cache, missing_requirements,
606 (ancestor_commit, commit_cache, missing_requirements,
603 source_commit, target_commit) = self.get_commits(
607 source_commit, target_commit) = self.get_commits(
604 commits_source_repo,
608 commits_source_repo,
605 pull_request_at_ver,
609 pull_request_at_ver,
606 source_commit,
610 source_commit,
607 source_ref_id,
611 source_ref_id,
608 source_scm,
612 source_scm,
609 target_commit,
613 target_commit,
610 target_ref_id,
614 target_ref_id,
611 target_scm,
615 target_scm,
612 maybe_unreachable=maybe_unreachable)
616 maybe_unreachable=maybe_unreachable)
613
617
614 # register our commit range
618 # register our commit range
615 for comm in commit_cache.values():
619 for comm in commit_cache.values():
616 c.commit_ranges.append(comm)
620 c.commit_ranges.append(comm)
617
621
618 c.missing_requirements = missing_requirements
622 c.missing_requirements = missing_requirements
619 c.ancestor_commit = ancestor_commit
623 c.ancestor_commit = ancestor_commit
620 c.statuses = source_repo.statuses(
624 c.statuses = source_repo.statuses(
621 [x.raw_id for x in c.commit_ranges])
625 [x.raw_id for x in c.commit_ranges])
622
626
623 # auto collapse if we have more than limit
627 # auto collapse if we have more than limit
624 collapse_limit = diffs.DiffProcessor._collapse_commits_over
628 collapse_limit = diffs.DiffProcessor._collapse_commits_over
625 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
629 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
626 c.compare_mode = compare
630 c.compare_mode = compare
627
631
628 # diff_limit is the old behavior, will cut off the whole diff
632 # diff_limit is the old behavior, will cut off the whole diff
629 # if the limit is applied otherwise will just hide the
633 # if the limit is applied otherwise will just hide the
630 # big files from the front-end
634 # big files from the front-end
631 diff_limit = c.visual.cut_off_limit_diff
635 diff_limit = c.visual.cut_off_limit_diff
632 file_limit = c.visual.cut_off_limit_file
636 file_limit = c.visual.cut_off_limit_file
633
637
634 c.missing_commits = False
638 c.missing_commits = False
635 if (c.missing_requirements
639 if (c.missing_requirements
636 or isinstance(source_commit, EmptyCommit)
640 or isinstance(source_commit, EmptyCommit)
637 or source_commit == target_commit):
641 or source_commit == target_commit):
638
642
639 c.missing_commits = True
643 c.missing_commits = True
640 else:
644 else:
641 c.inline_comments = display_inline_comments
645 c.inline_comments = display_inline_comments
642
646
643 use_ancestor = True
647 use_ancestor = True
644 if from_version_normalized != version_normalized:
648 if from_version_normalized != version_normalized:
645 use_ancestor = False
649 use_ancestor = False
646
650
647 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
651 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
648 if not force_recache and has_proper_diff_cache:
652 if not force_recache and has_proper_diff_cache:
649 c.diffset = cached_diff['diff']
653 c.diffset = cached_diff['diff']
650 else:
654 else:
651 try:
655 try:
652 c.diffset = self._get_diffset(
656 c.diffset = self._get_diffset(
653 c.source_repo.repo_name, commits_source_repo,
657 c.source_repo.repo_name, commits_source_repo,
654 c.ancestor_commit,
658 c.ancestor_commit,
655 source_ref_id, target_ref_id,
659 source_ref_id, target_ref_id,
656 target_commit, source_commit,
660 target_commit, source_commit,
657 diff_limit, file_limit, c.fulldiff,
661 diff_limit, file_limit, c.fulldiff,
658 hide_whitespace_changes, diff_context,
662 hide_whitespace_changes, diff_context,
659 use_ancestor=use_ancestor
663 use_ancestor=use_ancestor
660 )
664 )
661
665
662 # save cached diff
666 # save cached diff
663 if caching_enabled:
667 if caching_enabled:
664 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
668 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
665 except CommitDoesNotExistError:
669 except CommitDoesNotExistError:
666 log.exception('Failed to generate diffset')
670 log.exception('Failed to generate diffset')
667 c.missing_commits = True
671 c.missing_commits = True
668
672
669 if not c.missing_commits:
673 if not c.missing_commits:
670
674
671 c.limited_diff = c.diffset.limited_diff
675 c.limited_diff = c.diffset.limited_diff
672
676
673 # calculate removed files that are bound to comments
677 # calculate removed files that are bound to comments
674 comment_deleted_files = [
678 comment_deleted_files = [
675 fname for fname in display_inline_comments
679 fname for fname in display_inline_comments
676 if fname not in c.diffset.file_stats]
680 if fname not in c.diffset.file_stats]
677
681
678 c.deleted_files_comments = collections.defaultdict(dict)
682 c.deleted_files_comments = collections.defaultdict(dict)
679 for fname, per_line_comments in display_inline_comments.items():
683 for fname, per_line_comments in display_inline_comments.items():
680 if fname in comment_deleted_files:
684 if fname in comment_deleted_files:
681 c.deleted_files_comments[fname]['stats'] = 0
685 c.deleted_files_comments[fname]['stats'] = 0
682 c.deleted_files_comments[fname]['comments'] = list()
686 c.deleted_files_comments[fname]['comments'] = list()
683 for lno, comments in per_line_comments.items():
687 for lno, comments in per_line_comments.items():
684 c.deleted_files_comments[fname]['comments'].extend(comments)
688 c.deleted_files_comments[fname]['comments'].extend(comments)
685
689
686 # maybe calculate the range diff
690 # maybe calculate the range diff
687 if c.range_diff_on:
691 if c.range_diff_on:
688 # TODO(marcink): set whitespace/context
692 # TODO(marcink): set whitespace/context
689 context_lcl = 3
693 context_lcl = 3
690 ign_whitespace_lcl = False
694 ign_whitespace_lcl = False
691
695
692 for commit in c.commit_ranges:
696 for commit in c.commit_ranges:
693 commit2 = commit
697 commit2 = commit
694 commit1 = commit.first_parent
698 commit1 = commit.first_parent
695
699
696 range_diff_cache_file_path = diff_cache_exist(
700 range_diff_cache_file_path = diff_cache_exist(
697 cache_path, 'diff', commit.raw_id,
701 cache_path, 'diff', commit.raw_id,
698 ign_whitespace_lcl, context_lcl, c.fulldiff)
702 ign_whitespace_lcl, context_lcl, c.fulldiff)
699
703
700 cached_diff = None
704 cached_diff = None
701 if caching_enabled:
705 if caching_enabled:
702 cached_diff = load_cached_diff(range_diff_cache_file_path)
706 cached_diff = load_cached_diff(range_diff_cache_file_path)
703
707
704 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
708 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
705 if not force_recache and has_proper_diff_cache:
709 if not force_recache and has_proper_diff_cache:
706 diffset = cached_diff['diff']
710 diffset = cached_diff['diff']
707 else:
711 else:
708 diffset = self._get_range_diffset(
712 diffset = self._get_range_diffset(
709 commits_source_repo, source_repo,
713 commits_source_repo, source_repo,
710 commit1, commit2, diff_limit, file_limit,
714 commit1, commit2, diff_limit, file_limit,
711 c.fulldiff, ign_whitespace_lcl, context_lcl
715 c.fulldiff, ign_whitespace_lcl, context_lcl
712 )
716 )
713
717
714 # save cached diff
718 # save cached diff
715 if caching_enabled:
719 if caching_enabled:
716 cache_diff(range_diff_cache_file_path, diffset, None)
720 cache_diff(range_diff_cache_file_path, diffset, None)
717
721
718 c.changes[commit.raw_id] = diffset
722 c.changes[commit.raw_id] = diffset
719
723
720 # this is a hack to properly display links, when creating PR, the
724 # this is a hack to properly display links, when creating PR, the
721 # compare view and others uses different notation, and
725 # compare view and others uses different notation, and
722 # compare_commits.mako renders links based on the target_repo.
726 # compare_commits.mako renders links based on the target_repo.
723 # We need to swap that here to generate it properly on the html side
727 # We need to swap that here to generate it properly on the html side
724 c.target_repo = c.source_repo
728 c.target_repo = c.source_repo
725
729
726 c.commit_statuses = ChangesetStatus.STATUSES
730 c.commit_statuses = ChangesetStatus.STATUSES
727
731
728 c.show_version_changes = not pr_closed
732 c.show_version_changes = not pr_closed
729 if c.show_version_changes:
733 if c.show_version_changes:
730 cur_obj = pull_request_at_ver
734 cur_obj = pull_request_at_ver
731 prev_obj = prev_pull_request_at_ver
735 prev_obj = prev_pull_request_at_ver
732
736
733 old_commit_ids = prev_obj.revisions
737 old_commit_ids = prev_obj.revisions
734 new_commit_ids = cur_obj.revisions
738 new_commit_ids = cur_obj.revisions
735 commit_changes = PullRequestModel()._calculate_commit_id_changes(
739 commit_changes = PullRequestModel()._calculate_commit_id_changes(
736 old_commit_ids, new_commit_ids)
740 old_commit_ids, new_commit_ids)
737 c.commit_changes_summary = commit_changes
741 c.commit_changes_summary = commit_changes
738
742
739 # calculate the diff for commits between versions
743 # calculate the diff for commits between versions
740 c.commit_changes = []
744 c.commit_changes = []
741
745
742 def mark(cs, fw):
746 def mark(cs, fw):
743 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
747 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
744
748
745 for c_type, raw_id in mark(commit_changes.added, 'a') \
749 for c_type, raw_id in mark(commit_changes.added, 'a') \
746 + mark(commit_changes.removed, 'r') \
750 + mark(commit_changes.removed, 'r') \
747 + mark(commit_changes.common, 'c'):
751 + mark(commit_changes.common, 'c'):
748
752
749 if raw_id in commit_cache:
753 if raw_id in commit_cache:
750 commit = commit_cache[raw_id]
754 commit = commit_cache[raw_id]
751 else:
755 else:
752 try:
756 try:
753 commit = commits_source_repo.get_commit(raw_id)
757 commit = commits_source_repo.get_commit(raw_id)
754 except CommitDoesNotExistError:
758 except CommitDoesNotExistError:
755 # in case we fail extracting still use "dummy" commit
759 # in case we fail extracting still use "dummy" commit
756 # for display in commit diff
760 # for display in commit diff
757 commit = h.AttributeDict(
761 commit = h.AttributeDict(
758 {'raw_id': raw_id,
762 {'raw_id': raw_id,
759 'message': 'EMPTY or MISSING COMMIT'})
763 'message': 'EMPTY or MISSING COMMIT'})
760 c.commit_changes.append([c_type, commit])
764 c.commit_changes.append([c_type, commit])
761
765
762 # current user review statuses for each version
766 # current user review statuses for each version
763 c.review_versions = {}
767 c.review_versions = {}
764 is_reviewer = PullRequestModel().is_user_reviewer(
768 is_reviewer = PullRequestModel().is_user_reviewer(
765 pull_request, self._rhodecode_user)
769 pull_request, self._rhodecode_user)
766 if is_reviewer:
770 if is_reviewer:
767 for co in general_comments:
771 for co in general_comments:
768 if co.author.user_id == self._rhodecode_user.user_id:
772 if co.author.user_id == self._rhodecode_user.user_id:
769 status = co.status_change
773 status = co.status_change
770 if status:
774 if status:
771 _ver_pr = status[0].comment.pull_request_version_id
775 _ver_pr = status[0].comment.pull_request_version_id
772 c.review_versions[_ver_pr] = status[0]
776 c.review_versions[_ver_pr] = status[0]
773
777
774 return self._get_template_context(c)
778 return self._get_template_context(c)
775
779
776 def get_commits(
780 def get_commits(
777 self, commits_source_repo, pull_request_at_ver, source_commit,
781 self, commits_source_repo, pull_request_at_ver, source_commit,
778 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
782 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
779 maybe_unreachable=False):
783 maybe_unreachable=False):
780
784
781 commit_cache = collections.OrderedDict()
785 commit_cache = collections.OrderedDict()
782 missing_requirements = False
786 missing_requirements = False
783
787
784 try:
788 try:
785 pre_load = ["author", "date", "message", "branch", "parents"]
789 pre_load = ["author", "date", "message", "branch", "parents"]
786
790
787 pull_request_commits = pull_request_at_ver.revisions
791 pull_request_commits = pull_request_at_ver.revisions
788 log.debug('Loading %s commits from %s',
792 log.debug('Loading %s commits from %s',
789 len(pull_request_commits), commits_source_repo)
793 len(pull_request_commits), commits_source_repo)
790
794
791 for rev in pull_request_commits:
795 for rev in pull_request_commits:
792 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
796 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
793 maybe_unreachable=maybe_unreachable)
797 maybe_unreachable=maybe_unreachable)
794 commit_cache[comm.raw_id] = comm
798 commit_cache[comm.raw_id] = comm
795
799
796 # Order here matters, we first need to get target, and then
800 # Order here matters, we first need to get target, and then
797 # the source
801 # the source
798 target_commit = commits_source_repo.get_commit(
802 target_commit = commits_source_repo.get_commit(
799 commit_id=safe_str(target_ref_id))
803 commit_id=safe_str(target_ref_id))
800
804
801 source_commit = commits_source_repo.get_commit(
805 source_commit = commits_source_repo.get_commit(
802 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
806 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
803 except CommitDoesNotExistError:
807 except CommitDoesNotExistError:
804 log.warning('Failed to get commit from `{}` repo'.format(
808 log.warning('Failed to get commit from `{}` repo'.format(
805 commits_source_repo), exc_info=True)
809 commits_source_repo), exc_info=True)
806 except RepositoryRequirementError:
810 except RepositoryRequirementError:
807 log.warning('Failed to get all required data from repo', exc_info=True)
811 log.warning('Failed to get all required data from repo', exc_info=True)
808 missing_requirements = True
812 missing_requirements = True
809
813
810 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
814 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
811
815
812 try:
816 try:
813 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
817 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
814 except Exception:
818 except Exception:
815 ancestor_commit = None
819 ancestor_commit = None
816
820
817 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
821 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
818
822
819 def assure_not_empty_repo(self):
823 def assure_not_empty_repo(self):
820 _ = self.request.translate
824 _ = self.request.translate
821
825
822 try:
826 try:
823 self.db_repo.scm_instance().get_commit()
827 self.db_repo.scm_instance().get_commit()
824 except EmptyRepositoryError:
828 except EmptyRepositoryError:
825 h.flash(h.literal(_('There are no commits yet')),
829 h.flash(h.literal(_('There are no commits yet')),
826 category='warning')
830 category='warning')
827 raise HTTPFound(
831 raise HTTPFound(
828 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
832 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
829
833
830 @LoginRequired()
834 @LoginRequired()
831 @NotAnonymous()
835 @NotAnonymous()
832 @HasRepoPermissionAnyDecorator(
836 @HasRepoPermissionAnyDecorator(
833 'repository.read', 'repository.write', 'repository.admin')
837 'repository.read', 'repository.write', 'repository.admin')
834 @view_config(
838 @view_config(
835 route_name='pullrequest_new', request_method='GET',
839 route_name='pullrequest_new', request_method='GET',
836 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
840 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
837 def pull_request_new(self):
841 def pull_request_new(self):
838 _ = self.request.translate
842 _ = self.request.translate
839 c = self.load_default_context()
843 c = self.load_default_context()
840
844
841 self.assure_not_empty_repo()
845 self.assure_not_empty_repo()
842 source_repo = self.db_repo
846 source_repo = self.db_repo
843
847
844 commit_id = self.request.GET.get('commit')
848 commit_id = self.request.GET.get('commit')
845 branch_ref = self.request.GET.get('branch')
849 branch_ref = self.request.GET.get('branch')
846 bookmark_ref = self.request.GET.get('bookmark')
850 bookmark_ref = self.request.GET.get('bookmark')
847
851
848 try:
852 try:
849 source_repo_data = PullRequestModel().generate_repo_data(
853 source_repo_data = PullRequestModel().generate_repo_data(
850 source_repo, commit_id=commit_id,
854 source_repo, commit_id=commit_id,
851 branch=branch_ref, bookmark=bookmark_ref,
855 branch=branch_ref, bookmark=bookmark_ref,
852 translator=self.request.translate)
856 translator=self.request.translate)
853 except CommitDoesNotExistError as e:
857 except CommitDoesNotExistError as e:
854 log.exception(e)
858 log.exception(e)
855 h.flash(_('Commit does not exist'), 'error')
859 h.flash(_('Commit does not exist'), 'error')
856 raise HTTPFound(
860 raise HTTPFound(
857 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
861 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
858
862
859 default_target_repo = source_repo
863 default_target_repo = source_repo
860
864
861 if source_repo.parent and c.has_origin_repo_read_perm:
865 if source_repo.parent and c.has_origin_repo_read_perm:
862 parent_vcs_obj = source_repo.parent.scm_instance()
866 parent_vcs_obj = source_repo.parent.scm_instance()
863 if parent_vcs_obj and not parent_vcs_obj.is_empty():
867 if parent_vcs_obj and not parent_vcs_obj.is_empty():
864 # change default if we have a parent repo
868 # change default if we have a parent repo
865 default_target_repo = source_repo.parent
869 default_target_repo = source_repo.parent
866
870
867 target_repo_data = PullRequestModel().generate_repo_data(
871 target_repo_data = PullRequestModel().generate_repo_data(
868 default_target_repo, translator=self.request.translate)
872 default_target_repo, translator=self.request.translate)
869
873
870 selected_source_ref = source_repo_data['refs']['selected_ref']
874 selected_source_ref = source_repo_data['refs']['selected_ref']
871 title_source_ref = ''
875 title_source_ref = ''
872 if selected_source_ref:
876 if selected_source_ref:
873 title_source_ref = selected_source_ref.split(':', 2)[1]
877 title_source_ref = selected_source_ref.split(':', 2)[1]
874 c.default_title = PullRequestModel().generate_pullrequest_title(
878 c.default_title = PullRequestModel().generate_pullrequest_title(
875 source=source_repo.repo_name,
879 source=source_repo.repo_name,
876 source_ref=title_source_ref,
880 source_ref=title_source_ref,
877 target=default_target_repo.repo_name
881 target=default_target_repo.repo_name
878 )
882 )
879
883
880 c.default_repo_data = {
884 c.default_repo_data = {
881 'source_repo_name': source_repo.repo_name,
885 'source_repo_name': source_repo.repo_name,
882 'source_refs_json': json.dumps(source_repo_data),
886 'source_refs_json': json.dumps(source_repo_data),
883 'target_repo_name': default_target_repo.repo_name,
887 'target_repo_name': default_target_repo.repo_name,
884 'target_refs_json': json.dumps(target_repo_data),
888 'target_refs_json': json.dumps(target_repo_data),
885 }
889 }
886 c.default_source_ref = selected_source_ref
890 c.default_source_ref = selected_source_ref
887
891
888 return self._get_template_context(c)
892 return self._get_template_context(c)
889
893
890 @LoginRequired()
894 @LoginRequired()
891 @NotAnonymous()
895 @NotAnonymous()
892 @HasRepoPermissionAnyDecorator(
896 @HasRepoPermissionAnyDecorator(
893 'repository.read', 'repository.write', 'repository.admin')
897 'repository.read', 'repository.write', 'repository.admin')
894 @view_config(
898 @view_config(
895 route_name='pullrequest_repo_refs', request_method='GET',
899 route_name='pullrequest_repo_refs', request_method='GET',
896 renderer='json_ext', xhr=True)
900 renderer='json_ext', xhr=True)
897 def pull_request_repo_refs(self):
901 def pull_request_repo_refs(self):
898 self.load_default_context()
902 self.load_default_context()
899 target_repo_name = self.request.matchdict['target_repo_name']
903 target_repo_name = self.request.matchdict['target_repo_name']
900 repo = Repository.get_by_repo_name(target_repo_name)
904 repo = Repository.get_by_repo_name(target_repo_name)
901 if not repo:
905 if not repo:
902 raise HTTPNotFound()
906 raise HTTPNotFound()
903
907
904 target_perm = HasRepoPermissionAny(
908 target_perm = HasRepoPermissionAny(
905 'repository.read', 'repository.write', 'repository.admin')(
909 'repository.read', 'repository.write', 'repository.admin')(
906 target_repo_name)
910 target_repo_name)
907 if not target_perm:
911 if not target_perm:
908 raise HTTPNotFound()
912 raise HTTPNotFound()
909
913
910 return PullRequestModel().generate_repo_data(
914 return PullRequestModel().generate_repo_data(
911 repo, translator=self.request.translate)
915 repo, translator=self.request.translate)
912
916
913 @LoginRequired()
917 @LoginRequired()
914 @NotAnonymous()
918 @NotAnonymous()
915 @HasRepoPermissionAnyDecorator(
919 @HasRepoPermissionAnyDecorator(
916 'repository.read', 'repository.write', 'repository.admin')
920 'repository.read', 'repository.write', 'repository.admin')
917 @view_config(
921 @view_config(
918 route_name='pullrequest_repo_targets', request_method='GET',
922 route_name='pullrequest_repo_targets', request_method='GET',
919 renderer='json_ext', xhr=True)
923 renderer='json_ext', xhr=True)
920 def pullrequest_repo_targets(self):
924 def pullrequest_repo_targets(self):
921 _ = self.request.translate
925 _ = self.request.translate
922 filter_query = self.request.GET.get('query')
926 filter_query = self.request.GET.get('query')
923
927
924 # get the parents
928 # get the parents
925 parent_target_repos = []
929 parent_target_repos = []
926 if self.db_repo.parent:
930 if self.db_repo.parent:
927 parents_query = Repository.query() \
931 parents_query = Repository.query() \
928 .order_by(func.length(Repository.repo_name)) \
932 .order_by(func.length(Repository.repo_name)) \
929 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
933 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
930
934
931 if filter_query:
935 if filter_query:
932 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
936 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
933 parents_query = parents_query.filter(
937 parents_query = parents_query.filter(
934 Repository.repo_name.ilike(ilike_expression))
938 Repository.repo_name.ilike(ilike_expression))
935 parents = parents_query.limit(20).all()
939 parents = parents_query.limit(20).all()
936
940
937 for parent in parents:
941 for parent in parents:
938 parent_vcs_obj = parent.scm_instance()
942 parent_vcs_obj = parent.scm_instance()
939 if parent_vcs_obj and not parent_vcs_obj.is_empty():
943 if parent_vcs_obj and not parent_vcs_obj.is_empty():
940 parent_target_repos.append(parent)
944 parent_target_repos.append(parent)
941
945
942 # get other forks, and repo itself
946 # get other forks, and repo itself
943 query = Repository.query() \
947 query = Repository.query() \
944 .order_by(func.length(Repository.repo_name)) \
948 .order_by(func.length(Repository.repo_name)) \
945 .filter(
949 .filter(
946 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
950 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
947 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
951 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
948 ) \
952 ) \
949 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
953 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
950
954
951 if filter_query:
955 if filter_query:
952 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
956 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
953 query = query.filter(Repository.repo_name.ilike(ilike_expression))
957 query = query.filter(Repository.repo_name.ilike(ilike_expression))
954
958
955 limit = max(20 - len(parent_target_repos), 5) # not less then 5
959 limit = max(20 - len(parent_target_repos), 5) # not less then 5
956 target_repos = query.limit(limit).all()
960 target_repos = query.limit(limit).all()
957
961
958 all_target_repos = target_repos + parent_target_repos
962 all_target_repos = target_repos + parent_target_repos
959
963
960 repos = []
964 repos = []
961 # This checks permissions to the repositories
965 # This checks permissions to the repositories
962 for obj in ScmModel().get_repos(all_target_repos):
966 for obj in ScmModel().get_repos(all_target_repos):
963 repos.append({
967 repos.append({
964 'id': obj['name'],
968 'id': obj['name'],
965 'text': obj['name'],
969 'text': obj['name'],
966 'type': 'repo',
970 'type': 'repo',
967 'repo_id': obj['dbrepo']['repo_id'],
971 'repo_id': obj['dbrepo']['repo_id'],
968 'repo_type': obj['dbrepo']['repo_type'],
972 'repo_type': obj['dbrepo']['repo_type'],
969 'private': obj['dbrepo']['private'],
973 'private': obj['dbrepo']['private'],
970
974
971 })
975 })
972
976
973 data = {
977 data = {
974 'more': False,
978 'more': False,
975 'results': [{
979 'results': [{
976 'text': _('Repositories'),
980 'text': _('Repositories'),
977 'children': repos
981 'children': repos
978 }] if repos else []
982 }] if repos else []
979 }
983 }
980 return data
984 return data
981
985
982 def _get_existing_ids(self, post_data):
986 def _get_existing_ids(self, post_data):
983 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
987 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
984
988
985 @LoginRequired()
989 @LoginRequired()
986 @NotAnonymous()
990 @NotAnonymous()
987 @HasRepoPermissionAnyDecorator(
991 @HasRepoPermissionAnyDecorator(
988 'repository.read', 'repository.write', 'repository.admin')
992 'repository.read', 'repository.write', 'repository.admin')
989 @view_config(
993 @view_config(
990 route_name='pullrequest_comments', request_method='POST',
994 route_name='pullrequest_comments', request_method='POST',
991 renderer='string_html', xhr=True)
995 renderer='string_html', xhr=True)
992 def pullrequest_comments(self):
996 def pullrequest_comments(self):
993 self.load_default_context()
997 self.load_default_context()
994
998
995 pull_request = PullRequest.get_or_404(
999 pull_request = PullRequest.get_or_404(
996 self.request.matchdict['pull_request_id'])
1000 self.request.matchdict['pull_request_id'])
997 pull_request_id = pull_request.pull_request_id
1001 pull_request_id = pull_request.pull_request_id
998 version = self.request.GET.get('version')
1002 version = self.request.GET.get('version')
999
1003
1000 _render = self.request.get_partial_renderer(
1004 _render = self.request.get_partial_renderer(
1001 'rhodecode:templates/base/sidebar.mako')
1005 'rhodecode:templates/base/sidebar.mako')
1002 c = _render.get_call_context()
1006 c = _render.get_call_context()
1003
1007
1004 (pull_request_latest,
1008 (pull_request_latest,
1005 pull_request_at_ver,
1009 pull_request_at_ver,
1006 pull_request_display_obj,
1010 pull_request_display_obj,
1007 at_version) = PullRequestModel().get_pr_version(
1011 at_version) = PullRequestModel().get_pr_version(
1008 pull_request_id, version=version)
1012 pull_request_id, version=version)
1009 versions = pull_request_display_obj.versions()
1013 versions = pull_request_display_obj.versions()
1010 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1014 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1011 c.versions = versions + [latest_ver]
1015 c.versions = versions + [latest_ver]
1012
1016
1013 c.at_version = at_version
1017 c.at_version = at_version
1014 c.at_version_num = (at_version
1018 c.at_version_num = (at_version
1015 if at_version and at_version != PullRequest.LATEST_VER
1019 if at_version and at_version != PullRequest.LATEST_VER
1016 else None)
1020 else None)
1017
1021
1018 self.register_comments_vars(c, pull_request_latest, versions)
1022 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1019 all_comments = c.inline_comments_flat + c.comments
1023 all_comments = c.inline_comments_flat + c.comments
1020
1024
1021 existing_ids = self._get_existing_ids(self.request.POST)
1025 existing_ids = self._get_existing_ids(self.request.POST)
1022 return _render('comments_table', all_comments, len(all_comments),
1026 return _render('comments_table', all_comments, len(all_comments),
1023 existing_ids=existing_ids)
1027 existing_ids=existing_ids)
1024
1028
1025 @LoginRequired()
1029 @LoginRequired()
1026 @NotAnonymous()
1030 @NotAnonymous()
1027 @HasRepoPermissionAnyDecorator(
1031 @HasRepoPermissionAnyDecorator(
1028 'repository.read', 'repository.write', 'repository.admin')
1032 'repository.read', 'repository.write', 'repository.admin')
1029 @view_config(
1033 @view_config(
1030 route_name='pullrequest_todos', request_method='POST',
1034 route_name='pullrequest_todos', request_method='POST',
1031 renderer='string_html', xhr=True)
1035 renderer='string_html', xhr=True)
1032 def pullrequest_todos(self):
1036 def pullrequest_todos(self):
1033 self.load_default_context()
1037 self.load_default_context()
1034
1038
1035 pull_request = PullRequest.get_or_404(
1039 pull_request = PullRequest.get_or_404(
1036 self.request.matchdict['pull_request_id'])
1040 self.request.matchdict['pull_request_id'])
1037 pull_request_id = pull_request.pull_request_id
1041 pull_request_id = pull_request.pull_request_id
1038 version = self.request.GET.get('version')
1042 version = self.request.GET.get('version')
1039
1043
1040 _render = self.request.get_partial_renderer(
1044 _render = self.request.get_partial_renderer(
1041 'rhodecode:templates/base/sidebar.mako')
1045 'rhodecode:templates/base/sidebar.mako')
1042 c = _render.get_call_context()
1046 c = _render.get_call_context()
1043 (pull_request_latest,
1047 (pull_request_latest,
1044 pull_request_at_ver,
1048 pull_request_at_ver,
1045 pull_request_display_obj,
1049 pull_request_display_obj,
1046 at_version) = PullRequestModel().get_pr_version(
1050 at_version) = PullRequestModel().get_pr_version(
1047 pull_request_id, version=version)
1051 pull_request_id, version=version)
1048 versions = pull_request_display_obj.versions()
1052 versions = pull_request_display_obj.versions()
1049 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1053 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1050 c.versions = versions + [latest_ver]
1054 c.versions = versions + [latest_ver]
1051
1055
1052 c.at_version = at_version
1056 c.at_version = at_version
1053 c.at_version_num = (at_version
1057 c.at_version_num = (at_version
1054 if at_version and at_version != PullRequest.LATEST_VER
1058 if at_version and at_version != PullRequest.LATEST_VER
1055 else None)
1059 else None)
1056
1060
1057 c.unresolved_comments = CommentsModel() \
1061 c.unresolved_comments = CommentsModel() \
1058 .get_pull_request_unresolved_todos(pull_request)
1062 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1059 c.resolved_comments = CommentsModel() \
1063 c.resolved_comments = CommentsModel() \
1060 .get_pull_request_resolved_todos(pull_request)
1064 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1061
1065
1062 all_comments = c.unresolved_comments + c.resolved_comments
1066 all_comments = c.unresolved_comments + c.resolved_comments
1063 existing_ids = self._get_existing_ids(self.request.POST)
1067 existing_ids = self._get_existing_ids(self.request.POST)
1064 return _render('comments_table', all_comments, len(c.unresolved_comments),
1068 return _render('comments_table', all_comments, len(c.unresolved_comments),
1065 todo_comments=True, existing_ids=existing_ids)
1069 todo_comments=True, existing_ids=existing_ids)
1066
1070
1067 @LoginRequired()
1071 @LoginRequired()
1068 @NotAnonymous()
1072 @NotAnonymous()
1069 @HasRepoPermissionAnyDecorator(
1073 @HasRepoPermissionAnyDecorator(
1070 'repository.read', 'repository.write', 'repository.admin')
1074 'repository.read', 'repository.write', 'repository.admin')
1071 @CSRFRequired()
1075 @CSRFRequired()
1072 @view_config(
1076 @view_config(
1073 route_name='pullrequest_create', request_method='POST',
1077 route_name='pullrequest_create', request_method='POST',
1074 renderer=None)
1078 renderer=None)
1075 def pull_request_create(self):
1079 def pull_request_create(self):
1076 _ = self.request.translate
1080 _ = self.request.translate
1077 self.assure_not_empty_repo()
1081 self.assure_not_empty_repo()
1078 self.load_default_context()
1082 self.load_default_context()
1079
1083
1080 controls = peppercorn.parse(self.request.POST.items())
1084 controls = peppercorn.parse(self.request.POST.items())
1081
1085
1082 try:
1086 try:
1083 form = PullRequestForm(
1087 form = PullRequestForm(
1084 self.request.translate, self.db_repo.repo_id)()
1088 self.request.translate, self.db_repo.repo_id)()
1085 _form = form.to_python(controls)
1089 _form = form.to_python(controls)
1086 except formencode.Invalid as errors:
1090 except formencode.Invalid as errors:
1087 if errors.error_dict.get('revisions'):
1091 if errors.error_dict.get('revisions'):
1088 msg = 'Revisions: %s' % errors.error_dict['revisions']
1092 msg = 'Revisions: %s' % errors.error_dict['revisions']
1089 elif errors.error_dict.get('pullrequest_title'):
1093 elif errors.error_dict.get('pullrequest_title'):
1090 msg = errors.error_dict.get('pullrequest_title')
1094 msg = errors.error_dict.get('pullrequest_title')
1091 else:
1095 else:
1092 msg = _('Error creating pull request: {}').format(errors)
1096 msg = _('Error creating pull request: {}').format(errors)
1093 log.exception(msg)
1097 log.exception(msg)
1094 h.flash(msg, 'error')
1098 h.flash(msg, 'error')
1095
1099
1096 # would rather just go back to form ...
1100 # would rather just go back to form ...
1097 raise HTTPFound(
1101 raise HTTPFound(
1098 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1102 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1099
1103
1100 source_repo = _form['source_repo']
1104 source_repo = _form['source_repo']
1101 source_ref = _form['source_ref']
1105 source_ref = _form['source_ref']
1102 target_repo = _form['target_repo']
1106 target_repo = _form['target_repo']
1103 target_ref = _form['target_ref']
1107 target_ref = _form['target_ref']
1104 commit_ids = _form['revisions'][::-1]
1108 commit_ids = _form['revisions'][::-1]
1105 common_ancestor_id = _form['common_ancestor']
1109 common_ancestor_id = _form['common_ancestor']
1106
1110
1107 # find the ancestor for this pr
1111 # find the ancestor for this pr
1108 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1112 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1109 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1113 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1110
1114
1111 if not (source_db_repo or target_db_repo):
1115 if not (source_db_repo or target_db_repo):
1112 h.flash(_('source_repo or target repo not found'), category='error')
1116 h.flash(_('source_repo or target repo not found'), category='error')
1113 raise HTTPFound(
1117 raise HTTPFound(
1114 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1118 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1115
1119
1116 # re-check permissions again here
1120 # re-check permissions again here
1117 # source_repo we must have read permissions
1121 # source_repo we must have read permissions
1118
1122
1119 source_perm = HasRepoPermissionAny(
1123 source_perm = HasRepoPermissionAny(
1120 'repository.read', 'repository.write', 'repository.admin')(
1124 'repository.read', 'repository.write', 'repository.admin')(
1121 source_db_repo.repo_name)
1125 source_db_repo.repo_name)
1122 if not source_perm:
1126 if not source_perm:
1123 msg = _('Not Enough permissions to source repo `{}`.'.format(
1127 msg = _('Not Enough permissions to source repo `{}`.'.format(
1124 source_db_repo.repo_name))
1128 source_db_repo.repo_name))
1125 h.flash(msg, category='error')
1129 h.flash(msg, category='error')
1126 # copy the args back to redirect
1130 # copy the args back to redirect
1127 org_query = self.request.GET.mixed()
1131 org_query = self.request.GET.mixed()
1128 raise HTTPFound(
1132 raise HTTPFound(
1129 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1133 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1130 _query=org_query))
1134 _query=org_query))
1131
1135
1132 # target repo we must have read permissions, and also later on
1136 # target repo we must have read permissions, and also later on
1133 # we want to check branch permissions here
1137 # we want to check branch permissions here
1134 target_perm = HasRepoPermissionAny(
1138 target_perm = HasRepoPermissionAny(
1135 'repository.read', 'repository.write', 'repository.admin')(
1139 'repository.read', 'repository.write', 'repository.admin')(
1136 target_db_repo.repo_name)
1140 target_db_repo.repo_name)
1137 if not target_perm:
1141 if not target_perm:
1138 msg = _('Not Enough permissions to target repo `{}`.'.format(
1142 msg = _('Not Enough permissions to target repo `{}`.'.format(
1139 target_db_repo.repo_name))
1143 target_db_repo.repo_name))
1140 h.flash(msg, category='error')
1144 h.flash(msg, category='error')
1141 # copy the args back to redirect
1145 # copy the args back to redirect
1142 org_query = self.request.GET.mixed()
1146 org_query = self.request.GET.mixed()
1143 raise HTTPFound(
1147 raise HTTPFound(
1144 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1148 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1145 _query=org_query))
1149 _query=org_query))
1146
1150
1147 source_scm = source_db_repo.scm_instance()
1151 source_scm = source_db_repo.scm_instance()
1148 target_scm = target_db_repo.scm_instance()
1152 target_scm = target_db_repo.scm_instance()
1149
1153
1150 source_ref_obj = unicode_to_reference(source_ref)
1154 source_ref_obj = unicode_to_reference(source_ref)
1151 target_ref_obj = unicode_to_reference(target_ref)
1155 target_ref_obj = unicode_to_reference(target_ref)
1152
1156
1153 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1157 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1154 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1158 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1155
1159
1156 ancestor = source_scm.get_common_ancestor(
1160 ancestor = source_scm.get_common_ancestor(
1157 source_commit.raw_id, target_commit.raw_id, target_scm)
1161 source_commit.raw_id, target_commit.raw_id, target_scm)
1158
1162
1159 # recalculate target ref based on ancestor
1163 # recalculate target ref based on ancestor
1160 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1164 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1161
1165
1162 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1166 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1163 PullRequestModel().get_reviewer_functions()
1167 PullRequestModel().get_reviewer_functions()
1164
1168
1165 # recalculate reviewers logic, to make sure we can validate this
1169 # recalculate reviewers logic, to make sure we can validate this
1166 reviewer_rules = get_default_reviewers_data(
1170 reviewer_rules = get_default_reviewers_data(
1167 self._rhodecode_db_user,
1171 self._rhodecode_db_user,
1168 source_db_repo,
1172 source_db_repo,
1169 source_ref_obj,
1173 source_ref_obj,
1170 target_db_repo,
1174 target_db_repo,
1171 target_ref_obj,
1175 target_ref_obj,
1172 include_diff_info=False)
1176 include_diff_info=False)
1173
1177
1174 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1178 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1175 observers = validate_observers(_form['observer_members'], reviewer_rules)
1179 observers = validate_observers(_form['observer_members'], reviewer_rules)
1176
1180
1177 pullrequest_title = _form['pullrequest_title']
1181 pullrequest_title = _form['pullrequest_title']
1178 title_source_ref = source_ref_obj.name
1182 title_source_ref = source_ref_obj.name
1179 if not pullrequest_title:
1183 if not pullrequest_title:
1180 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1184 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1181 source=source_repo,
1185 source=source_repo,
1182 source_ref=title_source_ref,
1186 source_ref=title_source_ref,
1183 target=target_repo
1187 target=target_repo
1184 )
1188 )
1185
1189
1186 description = _form['pullrequest_desc']
1190 description = _form['pullrequest_desc']
1187 description_renderer = _form['description_renderer']
1191 description_renderer = _form['description_renderer']
1188
1192
1189 try:
1193 try:
1190 pull_request = PullRequestModel().create(
1194 pull_request = PullRequestModel().create(
1191 created_by=self._rhodecode_user.user_id,
1195 created_by=self._rhodecode_user.user_id,
1192 source_repo=source_repo,
1196 source_repo=source_repo,
1193 source_ref=source_ref,
1197 source_ref=source_ref,
1194 target_repo=target_repo,
1198 target_repo=target_repo,
1195 target_ref=target_ref,
1199 target_ref=target_ref,
1196 revisions=commit_ids,
1200 revisions=commit_ids,
1197 common_ancestor_id=common_ancestor_id,
1201 common_ancestor_id=common_ancestor_id,
1198 reviewers=reviewers,
1202 reviewers=reviewers,
1199 observers=observers,
1203 observers=observers,
1200 title=pullrequest_title,
1204 title=pullrequest_title,
1201 description=description,
1205 description=description,
1202 description_renderer=description_renderer,
1206 description_renderer=description_renderer,
1203 reviewer_data=reviewer_rules,
1207 reviewer_data=reviewer_rules,
1204 auth_user=self._rhodecode_user
1208 auth_user=self._rhodecode_user
1205 )
1209 )
1206 Session().commit()
1210 Session().commit()
1207
1211
1208 h.flash(_('Successfully opened new pull request'),
1212 h.flash(_('Successfully opened new pull request'),
1209 category='success')
1213 category='success')
1210 except Exception:
1214 except Exception:
1211 msg = _('Error occurred during creation of this pull request.')
1215 msg = _('Error occurred during creation of this pull request.')
1212 log.exception(msg)
1216 log.exception(msg)
1213 h.flash(msg, category='error')
1217 h.flash(msg, category='error')
1214
1218
1215 # copy the args back to redirect
1219 # copy the args back to redirect
1216 org_query = self.request.GET.mixed()
1220 org_query = self.request.GET.mixed()
1217 raise HTTPFound(
1221 raise HTTPFound(
1218 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1222 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1219 _query=org_query))
1223 _query=org_query))
1220
1224
1221 raise HTTPFound(
1225 raise HTTPFound(
1222 h.route_path('pullrequest_show', repo_name=target_repo,
1226 h.route_path('pullrequest_show', repo_name=target_repo,
1223 pull_request_id=pull_request.pull_request_id))
1227 pull_request_id=pull_request.pull_request_id))
1224
1228
1225 @LoginRequired()
1229 @LoginRequired()
1226 @NotAnonymous()
1230 @NotAnonymous()
1227 @HasRepoPermissionAnyDecorator(
1231 @HasRepoPermissionAnyDecorator(
1228 'repository.read', 'repository.write', 'repository.admin')
1232 'repository.read', 'repository.write', 'repository.admin')
1229 @CSRFRequired()
1233 @CSRFRequired()
1230 @view_config(
1234 @view_config(
1231 route_name='pullrequest_update', request_method='POST',
1235 route_name='pullrequest_update', request_method='POST',
1232 renderer='json_ext')
1236 renderer='json_ext')
1233 def pull_request_update(self):
1237 def pull_request_update(self):
1234 pull_request = PullRequest.get_or_404(
1238 pull_request = PullRequest.get_or_404(
1235 self.request.matchdict['pull_request_id'])
1239 self.request.matchdict['pull_request_id'])
1236 _ = self.request.translate
1240 _ = self.request.translate
1237
1241
1238 c = self.load_default_context()
1242 c = self.load_default_context()
1239 redirect_url = None
1243 redirect_url = None
1240
1244
1241 if pull_request.is_closed():
1245 if pull_request.is_closed():
1242 log.debug('update: forbidden because pull request is closed')
1246 log.debug('update: forbidden because pull request is closed')
1243 msg = _(u'Cannot update closed pull requests.')
1247 msg = _(u'Cannot update closed pull requests.')
1244 h.flash(msg, category='error')
1248 h.flash(msg, category='error')
1245 return {'response': True,
1249 return {'response': True,
1246 'redirect_url': redirect_url}
1250 'redirect_url': redirect_url}
1247
1251
1248 is_state_changing = pull_request.is_state_changing()
1252 is_state_changing = pull_request.is_state_changing()
1249 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1253 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1250
1254
1251 # only owner or admin can update it
1255 # only owner or admin can update it
1252 allowed_to_update = PullRequestModel().check_user_update(
1256 allowed_to_update = PullRequestModel().check_user_update(
1253 pull_request, self._rhodecode_user)
1257 pull_request, self._rhodecode_user)
1254
1258
1255 if allowed_to_update:
1259 if allowed_to_update:
1256 controls = peppercorn.parse(self.request.POST.items())
1260 controls = peppercorn.parse(self.request.POST.items())
1257 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1261 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1258
1262
1259 if 'review_members' in controls:
1263 if 'review_members' in controls:
1260 self._update_reviewers(
1264 self._update_reviewers(
1261 c,
1265 c,
1262 pull_request, controls['review_members'],
1266 pull_request, controls['review_members'],
1263 pull_request.reviewer_data,
1267 pull_request.reviewer_data,
1264 PullRequestReviewers.ROLE_REVIEWER)
1268 PullRequestReviewers.ROLE_REVIEWER)
1265 elif 'observer_members' in controls:
1269 elif 'observer_members' in controls:
1266 self._update_reviewers(
1270 self._update_reviewers(
1267 c,
1271 c,
1268 pull_request, controls['observer_members'],
1272 pull_request, controls['observer_members'],
1269 pull_request.reviewer_data,
1273 pull_request.reviewer_data,
1270 PullRequestReviewers.ROLE_OBSERVER)
1274 PullRequestReviewers.ROLE_OBSERVER)
1271 elif str2bool(self.request.POST.get('update_commits', 'false')):
1275 elif str2bool(self.request.POST.get('update_commits', 'false')):
1272 if is_state_changing:
1276 if is_state_changing:
1273 log.debug('commits update: forbidden because pull request is in state %s',
1277 log.debug('commits update: forbidden because pull request is in state %s',
1274 pull_request.pull_request_state)
1278 pull_request.pull_request_state)
1275 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1279 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1276 u'Current state is: `{}`').format(
1280 u'Current state is: `{}`').format(
1277 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1281 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1278 h.flash(msg, category='error')
1282 h.flash(msg, category='error')
1279 return {'response': True,
1283 return {'response': True,
1280 'redirect_url': redirect_url}
1284 'redirect_url': redirect_url}
1281
1285
1282 self._update_commits(c, pull_request)
1286 self._update_commits(c, pull_request)
1283 if force_refresh:
1287 if force_refresh:
1284 redirect_url = h.route_path(
1288 redirect_url = h.route_path(
1285 'pullrequest_show', repo_name=self.db_repo_name,
1289 'pullrequest_show', repo_name=self.db_repo_name,
1286 pull_request_id=pull_request.pull_request_id,
1290 pull_request_id=pull_request.pull_request_id,
1287 _query={"force_refresh": 1})
1291 _query={"force_refresh": 1})
1288 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1292 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1289 self._edit_pull_request(pull_request)
1293 self._edit_pull_request(pull_request)
1290 else:
1294 else:
1291 log.error('Unhandled update data.')
1295 log.error('Unhandled update data.')
1292 raise HTTPBadRequest()
1296 raise HTTPBadRequest()
1293
1297
1294 return {'response': True,
1298 return {'response': True,
1295 'redirect_url': redirect_url}
1299 'redirect_url': redirect_url}
1296 raise HTTPForbidden()
1300 raise HTTPForbidden()
1297
1301
1298 def _edit_pull_request(self, pull_request):
1302 def _edit_pull_request(self, pull_request):
1299 """
1303 """
1300 Edit title and description
1304 Edit title and description
1301 """
1305 """
1302 _ = self.request.translate
1306 _ = self.request.translate
1303
1307
1304 try:
1308 try:
1305 PullRequestModel().edit(
1309 PullRequestModel().edit(
1306 pull_request,
1310 pull_request,
1307 self.request.POST.get('title'),
1311 self.request.POST.get('title'),
1308 self.request.POST.get('description'),
1312 self.request.POST.get('description'),
1309 self.request.POST.get('description_renderer'),
1313 self.request.POST.get('description_renderer'),
1310 self._rhodecode_user)
1314 self._rhodecode_user)
1311 except ValueError:
1315 except ValueError:
1312 msg = _(u'Cannot update closed pull requests.')
1316 msg = _(u'Cannot update closed pull requests.')
1313 h.flash(msg, category='error')
1317 h.flash(msg, category='error')
1314 return
1318 return
1315 else:
1319 else:
1316 Session().commit()
1320 Session().commit()
1317
1321
1318 msg = _(u'Pull request title & description updated.')
1322 msg = _(u'Pull request title & description updated.')
1319 h.flash(msg, category='success')
1323 h.flash(msg, category='success')
1320 return
1324 return
1321
1325
1322 def _update_commits(self, c, pull_request):
1326 def _update_commits(self, c, pull_request):
1323 _ = self.request.translate
1327 _ = self.request.translate
1324
1328
1325 with pull_request.set_state(PullRequest.STATE_UPDATING):
1329 with pull_request.set_state(PullRequest.STATE_UPDATING):
1326 resp = PullRequestModel().update_commits(
1330 resp = PullRequestModel().update_commits(
1327 pull_request, self._rhodecode_db_user)
1331 pull_request, self._rhodecode_db_user)
1328
1332
1329 if resp.executed:
1333 if resp.executed:
1330
1334
1331 if resp.target_changed and resp.source_changed:
1335 if resp.target_changed and resp.source_changed:
1332 changed = 'target and source repositories'
1336 changed = 'target and source repositories'
1333 elif resp.target_changed and not resp.source_changed:
1337 elif resp.target_changed and not resp.source_changed:
1334 changed = 'target repository'
1338 changed = 'target repository'
1335 elif not resp.target_changed and resp.source_changed:
1339 elif not resp.target_changed and resp.source_changed:
1336 changed = 'source repository'
1340 changed = 'source repository'
1337 else:
1341 else:
1338 changed = 'nothing'
1342 changed = 'nothing'
1339
1343
1340 msg = _(u'Pull request updated to "{source_commit_id}" with '
1344 msg = _(u'Pull request updated to "{source_commit_id}" with '
1341 u'{count_added} added, {count_removed} removed commits. '
1345 u'{count_added} added, {count_removed} removed commits. '
1342 u'Source of changes: {change_source}.')
1346 u'Source of changes: {change_source}.')
1343 msg = msg.format(
1347 msg = msg.format(
1344 source_commit_id=pull_request.source_ref_parts.commit_id,
1348 source_commit_id=pull_request.source_ref_parts.commit_id,
1345 count_added=len(resp.changes.added),
1349 count_added=len(resp.changes.added),
1346 count_removed=len(resp.changes.removed),
1350 count_removed=len(resp.changes.removed),
1347 change_source=changed)
1351 change_source=changed)
1348 h.flash(msg, category='success')
1352 h.flash(msg, category='success')
1349 channelstream.pr_update_channelstream_push(
1353 channelstream.pr_update_channelstream_push(
1350 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1354 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1351 else:
1355 else:
1352 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1356 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1353 warning_reasons = [
1357 warning_reasons = [
1354 UpdateFailureReason.NO_CHANGE,
1358 UpdateFailureReason.NO_CHANGE,
1355 UpdateFailureReason.WRONG_REF_TYPE,
1359 UpdateFailureReason.WRONG_REF_TYPE,
1356 ]
1360 ]
1357 category = 'warning' if resp.reason in warning_reasons else 'error'
1361 category = 'warning' if resp.reason in warning_reasons else 'error'
1358 h.flash(msg, category=category)
1362 h.flash(msg, category=category)
1359
1363
1360 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1364 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1361 _ = self.request.translate
1365 _ = self.request.translate
1362
1366
1363 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1367 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1364 PullRequestModel().get_reviewer_functions()
1368 PullRequestModel().get_reviewer_functions()
1365
1369
1366 if role == PullRequestReviewers.ROLE_REVIEWER:
1370 if role == PullRequestReviewers.ROLE_REVIEWER:
1367 try:
1371 try:
1368 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1372 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1369 except ValueError as e:
1373 except ValueError as e:
1370 log.error('Reviewers Validation: {}'.format(e))
1374 log.error('Reviewers Validation: {}'.format(e))
1371 h.flash(e, category='error')
1375 h.flash(e, category='error')
1372 return
1376 return
1373
1377
1374 old_calculated_status = pull_request.calculated_review_status()
1378 old_calculated_status = pull_request.calculated_review_status()
1375 PullRequestModel().update_reviewers(
1379 PullRequestModel().update_reviewers(
1376 pull_request, reviewers, self._rhodecode_db_user)
1380 pull_request, reviewers, self._rhodecode_db_user)
1377
1381
1378 Session().commit()
1382 Session().commit()
1379
1383
1380 msg = _('Pull request reviewers updated.')
1384 msg = _('Pull request reviewers updated.')
1381 h.flash(msg, category='success')
1385 h.flash(msg, category='success')
1382 channelstream.pr_update_channelstream_push(
1386 channelstream.pr_update_channelstream_push(
1383 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1387 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1384
1388
1385 # trigger status changed if change in reviewers changes the status
1389 # trigger status changed if change in reviewers changes the status
1386 calculated_status = pull_request.calculated_review_status()
1390 calculated_status = pull_request.calculated_review_status()
1387 if old_calculated_status != calculated_status:
1391 if old_calculated_status != calculated_status:
1388 PullRequestModel().trigger_pull_request_hook(
1392 PullRequestModel().trigger_pull_request_hook(
1389 pull_request, self._rhodecode_user, 'review_status_change',
1393 pull_request, self._rhodecode_user, 'review_status_change',
1390 data={'status': calculated_status})
1394 data={'status': calculated_status})
1391
1395
1392 elif role == PullRequestReviewers.ROLE_OBSERVER:
1396 elif role == PullRequestReviewers.ROLE_OBSERVER:
1393 try:
1397 try:
1394 observers = validate_observers(review_members, reviewer_rules)
1398 observers = validate_observers(review_members, reviewer_rules)
1395 except ValueError as e:
1399 except ValueError as e:
1396 log.error('Observers Validation: {}'.format(e))
1400 log.error('Observers Validation: {}'.format(e))
1397 h.flash(e, category='error')
1401 h.flash(e, category='error')
1398 return
1402 return
1399
1403
1400 PullRequestModel().update_observers(
1404 PullRequestModel().update_observers(
1401 pull_request, observers, self._rhodecode_db_user)
1405 pull_request, observers, self._rhodecode_db_user)
1402
1406
1403 Session().commit()
1407 Session().commit()
1404 msg = _('Pull request observers updated.')
1408 msg = _('Pull request observers updated.')
1405 h.flash(msg, category='success')
1409 h.flash(msg, category='success')
1406 channelstream.pr_update_channelstream_push(
1410 channelstream.pr_update_channelstream_push(
1407 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1411 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1408
1412
1409 @LoginRequired()
1413 @LoginRequired()
1410 @NotAnonymous()
1414 @NotAnonymous()
1411 @HasRepoPermissionAnyDecorator(
1415 @HasRepoPermissionAnyDecorator(
1412 'repository.read', 'repository.write', 'repository.admin')
1416 'repository.read', 'repository.write', 'repository.admin')
1413 @CSRFRequired()
1417 @CSRFRequired()
1414 @view_config(
1418 @view_config(
1415 route_name='pullrequest_merge', request_method='POST',
1419 route_name='pullrequest_merge', request_method='POST',
1416 renderer='json_ext')
1420 renderer='json_ext')
1417 def pull_request_merge(self):
1421 def pull_request_merge(self):
1418 """
1422 """
1419 Merge will perform a server-side merge of the specified
1423 Merge will perform a server-side merge of the specified
1420 pull request, if the pull request is approved and mergeable.
1424 pull request, if the pull request is approved and mergeable.
1421 After successful merging, the pull request is automatically
1425 After successful merging, the pull request is automatically
1422 closed, with a relevant comment.
1426 closed, with a relevant comment.
1423 """
1427 """
1424 pull_request = PullRequest.get_or_404(
1428 pull_request = PullRequest.get_or_404(
1425 self.request.matchdict['pull_request_id'])
1429 self.request.matchdict['pull_request_id'])
1426 _ = self.request.translate
1430 _ = self.request.translate
1427
1431
1428 if pull_request.is_state_changing():
1432 if pull_request.is_state_changing():
1429 log.debug('show: forbidden because pull request is in state %s',
1433 log.debug('show: forbidden because pull request is in state %s',
1430 pull_request.pull_request_state)
1434 pull_request.pull_request_state)
1431 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1435 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1432 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1436 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1433 pull_request.pull_request_state)
1437 pull_request.pull_request_state)
1434 h.flash(msg, category='error')
1438 h.flash(msg, category='error')
1435 raise HTTPFound(
1439 raise HTTPFound(
1436 h.route_path('pullrequest_show',
1440 h.route_path('pullrequest_show',
1437 repo_name=pull_request.target_repo.repo_name,
1441 repo_name=pull_request.target_repo.repo_name,
1438 pull_request_id=pull_request.pull_request_id))
1442 pull_request_id=pull_request.pull_request_id))
1439
1443
1440 self.load_default_context()
1444 self.load_default_context()
1441
1445
1442 with pull_request.set_state(PullRequest.STATE_UPDATING):
1446 with pull_request.set_state(PullRequest.STATE_UPDATING):
1443 check = MergeCheck.validate(
1447 check = MergeCheck.validate(
1444 pull_request, auth_user=self._rhodecode_user,
1448 pull_request, auth_user=self._rhodecode_user,
1445 translator=self.request.translate)
1449 translator=self.request.translate)
1446 merge_possible = not check.failed
1450 merge_possible = not check.failed
1447
1451
1448 for err_type, error_msg in check.errors:
1452 for err_type, error_msg in check.errors:
1449 h.flash(error_msg, category=err_type)
1453 h.flash(error_msg, category=err_type)
1450
1454
1451 if merge_possible:
1455 if merge_possible:
1452 log.debug("Pre-conditions checked, trying to merge.")
1456 log.debug("Pre-conditions checked, trying to merge.")
1453 extras = vcs_operation_context(
1457 extras = vcs_operation_context(
1454 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1458 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1455 username=self._rhodecode_db_user.username, action='push',
1459 username=self._rhodecode_db_user.username, action='push',
1456 scm=pull_request.target_repo.repo_type)
1460 scm=pull_request.target_repo.repo_type)
1457 with pull_request.set_state(PullRequest.STATE_UPDATING):
1461 with pull_request.set_state(PullRequest.STATE_UPDATING):
1458 self._merge_pull_request(
1462 self._merge_pull_request(
1459 pull_request, self._rhodecode_db_user, extras)
1463 pull_request, self._rhodecode_db_user, extras)
1460 else:
1464 else:
1461 log.debug("Pre-conditions failed, NOT merging.")
1465 log.debug("Pre-conditions failed, NOT merging.")
1462
1466
1463 raise HTTPFound(
1467 raise HTTPFound(
1464 h.route_path('pullrequest_show',
1468 h.route_path('pullrequest_show',
1465 repo_name=pull_request.target_repo.repo_name,
1469 repo_name=pull_request.target_repo.repo_name,
1466 pull_request_id=pull_request.pull_request_id))
1470 pull_request_id=pull_request.pull_request_id))
1467
1471
1468 def _merge_pull_request(self, pull_request, user, extras):
1472 def _merge_pull_request(self, pull_request, user, extras):
1469 _ = self.request.translate
1473 _ = self.request.translate
1470 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1474 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1471
1475
1472 if merge_resp.executed:
1476 if merge_resp.executed:
1473 log.debug("The merge was successful, closing the pull request.")
1477 log.debug("The merge was successful, closing the pull request.")
1474 PullRequestModel().close_pull_request(
1478 PullRequestModel().close_pull_request(
1475 pull_request.pull_request_id, user)
1479 pull_request.pull_request_id, user)
1476 Session().commit()
1480 Session().commit()
1477 msg = _('Pull request was successfully merged and closed.')
1481 msg = _('Pull request was successfully merged and closed.')
1478 h.flash(msg, category='success')
1482 h.flash(msg, category='success')
1479 else:
1483 else:
1480 log.debug(
1484 log.debug(
1481 "The merge was not successful. Merge response: %s", merge_resp)
1485 "The merge was not successful. Merge response: %s", merge_resp)
1482 msg = merge_resp.merge_status_message
1486 msg = merge_resp.merge_status_message
1483 h.flash(msg, category='error')
1487 h.flash(msg, category='error')
1484
1488
1485 @LoginRequired()
1489 @LoginRequired()
1486 @NotAnonymous()
1490 @NotAnonymous()
1487 @HasRepoPermissionAnyDecorator(
1491 @HasRepoPermissionAnyDecorator(
1488 'repository.read', 'repository.write', 'repository.admin')
1492 'repository.read', 'repository.write', 'repository.admin')
1489 @CSRFRequired()
1493 @CSRFRequired()
1490 @view_config(
1494 @view_config(
1491 route_name='pullrequest_delete', request_method='POST',
1495 route_name='pullrequest_delete', request_method='POST',
1492 renderer='json_ext')
1496 renderer='json_ext')
1493 def pull_request_delete(self):
1497 def pull_request_delete(self):
1494 _ = self.request.translate
1498 _ = self.request.translate
1495
1499
1496 pull_request = PullRequest.get_or_404(
1500 pull_request = PullRequest.get_or_404(
1497 self.request.matchdict['pull_request_id'])
1501 self.request.matchdict['pull_request_id'])
1498 self.load_default_context()
1502 self.load_default_context()
1499
1503
1500 pr_closed = pull_request.is_closed()
1504 pr_closed = pull_request.is_closed()
1501 allowed_to_delete = PullRequestModel().check_user_delete(
1505 allowed_to_delete = PullRequestModel().check_user_delete(
1502 pull_request, self._rhodecode_user) and not pr_closed
1506 pull_request, self._rhodecode_user) and not pr_closed
1503
1507
1504 # only owner can delete it !
1508 # only owner can delete it !
1505 if allowed_to_delete:
1509 if allowed_to_delete:
1506 PullRequestModel().delete(pull_request, self._rhodecode_user)
1510 PullRequestModel().delete(pull_request, self._rhodecode_user)
1507 Session().commit()
1511 Session().commit()
1508 h.flash(_('Successfully deleted pull request'),
1512 h.flash(_('Successfully deleted pull request'),
1509 category='success')
1513 category='success')
1510 raise HTTPFound(h.route_path('pullrequest_show_all',
1514 raise HTTPFound(h.route_path('pullrequest_show_all',
1511 repo_name=self.db_repo_name))
1515 repo_name=self.db_repo_name))
1512
1516
1513 log.warning('user %s tried to delete pull request without access',
1517 log.warning('user %s tried to delete pull request without access',
1514 self._rhodecode_user)
1518 self._rhodecode_user)
1515 raise HTTPNotFound()
1519 raise HTTPNotFound()
1516
1520
1517 @LoginRequired()
1521 @LoginRequired()
1518 @NotAnonymous()
1522 @NotAnonymous()
1519 @HasRepoPermissionAnyDecorator(
1523 @HasRepoPermissionAnyDecorator(
1520 'repository.read', 'repository.write', 'repository.admin')
1524 'repository.read', 'repository.write', 'repository.admin')
1521 @CSRFRequired()
1525 @CSRFRequired()
1522 @view_config(
1526 @view_config(
1523 route_name='pullrequest_comment_create', request_method='POST',
1527 route_name='pullrequest_comment_create', request_method='POST',
1524 renderer='json_ext')
1528 renderer='json_ext')
1525 def pull_request_comment_create(self):
1529 def pull_request_comment_create(self):
1526 _ = self.request.translate
1530 _ = self.request.translate
1527
1531
1528 pull_request = PullRequest.get_or_404(
1532 pull_request = PullRequest.get_or_404(
1529 self.request.matchdict['pull_request_id'])
1533 self.request.matchdict['pull_request_id'])
1530 pull_request_id = pull_request.pull_request_id
1534 pull_request_id = pull_request.pull_request_id
1531
1535
1532 if pull_request.is_closed():
1536 if pull_request.is_closed():
1533 log.debug('comment: forbidden because pull request is closed')
1537 log.debug('comment: forbidden because pull request is closed')
1534 raise HTTPForbidden()
1538 raise HTTPForbidden()
1535
1539
1536 allowed_to_comment = PullRequestModel().check_user_comment(
1540 allowed_to_comment = PullRequestModel().check_user_comment(
1537 pull_request, self._rhodecode_user)
1541 pull_request, self._rhodecode_user)
1538 if not allowed_to_comment:
1542 if not allowed_to_comment:
1539 log.debug('comment: forbidden because pull request is from forbidden repo')
1543 log.debug('comment: forbidden because pull request is from forbidden repo')
1540 raise HTTPForbidden()
1544 raise HTTPForbidden()
1541
1545
1542 c = self.load_default_context()
1546 c = self.load_default_context()
1543
1547
1544 status = self.request.POST.get('changeset_status', None)
1548 status = self.request.POST.get('changeset_status', None)
1545 text = self.request.POST.get('text')
1549 text = self.request.POST.get('text')
1546 comment_type = self.request.POST.get('comment_type')
1550 comment_type = self.request.POST.get('comment_type')
1551 is_draft = str2bool(self.request.POST.get('draft'))
1547 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1552 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1548 close_pull_request = self.request.POST.get('close_pull_request')
1553 close_pull_request = self.request.POST.get('close_pull_request')
1549
1554
1550 # the logic here should work like following, if we submit close
1555 # the logic here should work like following, if we submit close
1551 # pr comment, use `close_pull_request_with_comment` function
1556 # pr comment, use `close_pull_request_with_comment` function
1552 # else handle regular comment logic
1557 # else handle regular comment logic
1553
1558
1554 if close_pull_request:
1559 if close_pull_request:
1555 # only owner or admin or person with write permissions
1560 # only owner or admin or person with write permissions
1556 allowed_to_close = PullRequestModel().check_user_update(
1561 allowed_to_close = PullRequestModel().check_user_update(
1557 pull_request, self._rhodecode_user)
1562 pull_request, self._rhodecode_user)
1558 if not allowed_to_close:
1563 if not allowed_to_close:
1559 log.debug('comment: forbidden because not allowed to close '
1564 log.debug('comment: forbidden because not allowed to close '
1560 'pull request %s', pull_request_id)
1565 'pull request %s', pull_request_id)
1561 raise HTTPForbidden()
1566 raise HTTPForbidden()
1562
1567
1563 # This also triggers `review_status_change`
1568 # This also triggers `review_status_change`
1564 comment, status = PullRequestModel().close_pull_request_with_comment(
1569 comment, status = PullRequestModel().close_pull_request_with_comment(
1565 pull_request, self._rhodecode_user, self.db_repo, message=text,
1570 pull_request, self._rhodecode_user, self.db_repo, message=text,
1566 auth_user=self._rhodecode_user)
1571 auth_user=self._rhodecode_user)
1567 Session().flush()
1572 Session().flush()
1568 is_inline = comment.is_inline
1573 is_inline = comment.is_inline
1569
1574
1570 PullRequestModel().trigger_pull_request_hook(
1575 PullRequestModel().trigger_pull_request_hook(
1571 pull_request, self._rhodecode_user, 'comment',
1576 pull_request, self._rhodecode_user, 'comment',
1572 data={'comment': comment})
1577 data={'comment': comment})
1573
1578
1574 else:
1579 else:
1575 # regular comment case, could be inline, or one with status.
1580 # regular comment case, could be inline, or one with status.
1576 # for that one we check also permissions
1581 # for that one we check also permissions
1577
1582 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1578 allowed_to_change_status = PullRequestModel().check_user_change_status(
1583 allowed_to_change_status = PullRequestModel().check_user_change_status(
1579 pull_request, self._rhodecode_user)
1584 pull_request, self._rhodecode_user) and not is_draft
1580
1585
1581 if status and allowed_to_change_status:
1586 if status and allowed_to_change_status:
1582 message = (_('Status change %(transition_icon)s %(status)s')
1587 message = (_('Status change %(transition_icon)s %(status)s')
1583 % {'transition_icon': '>',
1588 % {'transition_icon': '>',
1584 'status': ChangesetStatus.get_status_lbl(status)})
1589 'status': ChangesetStatus.get_status_lbl(status)})
1585 text = text or message
1590 text = text or message
1586
1591
1587 comment = CommentsModel().create(
1592 comment = CommentsModel().create(
1588 text=text,
1593 text=text,
1589 repo=self.db_repo.repo_id,
1594 repo=self.db_repo.repo_id,
1590 user=self._rhodecode_user.user_id,
1595 user=self._rhodecode_user.user_id,
1591 pull_request=pull_request,
1596 pull_request=pull_request,
1592 f_path=self.request.POST.get('f_path'),
1597 f_path=self.request.POST.get('f_path'),
1593 line_no=self.request.POST.get('line'),
1598 line_no=self.request.POST.get('line'),
1594 status_change=(ChangesetStatus.get_status_lbl(status)
1599 status_change=(ChangesetStatus.get_status_lbl(status)
1595 if status and allowed_to_change_status else None),
1600 if status and allowed_to_change_status else None),
1596 status_change_type=(status
1601 status_change_type=(status
1597 if status and allowed_to_change_status else None),
1602 if status and allowed_to_change_status else None),
1598 comment_type=comment_type,
1603 comment_type=comment_type,
1604 is_draft=is_draft,
1599 resolves_comment_id=resolves_comment_id,
1605 resolves_comment_id=resolves_comment_id,
1600 auth_user=self._rhodecode_user
1606 auth_user=self._rhodecode_user,
1607 send_email=not is_draft, # skip notification for draft comments
1601 )
1608 )
1602 is_inline = comment.is_inline
1609 is_inline = comment.is_inline
1603
1610
1604 if allowed_to_change_status:
1611 if allowed_to_change_status:
1605 # calculate old status before we change it
1612 # calculate old status before we change it
1606 old_calculated_status = pull_request.calculated_review_status()
1613 old_calculated_status = pull_request.calculated_review_status()
1607
1614
1608 # get status if set !
1615 # get status if set !
1609 if status:
1616 if status:
1610 ChangesetStatusModel().set_status(
1617 ChangesetStatusModel().set_status(
1611 self.db_repo.repo_id,
1618 self.db_repo.repo_id,
1612 status,
1619 status,
1613 self._rhodecode_user.user_id,
1620 self._rhodecode_user.user_id,
1614 comment,
1621 comment,
1615 pull_request=pull_request
1622 pull_request=pull_request
1616 )
1623 )
1617
1624
1618 Session().flush()
1625 Session().flush()
1619 # this is somehow required to get access to some relationship
1626 # this is somehow required to get access to some relationship
1620 # loaded on comment
1627 # loaded on comment
1621 Session().refresh(comment)
1628 Session().refresh(comment)
1622
1629
1623 PullRequestModel().trigger_pull_request_hook(
1630 PullRequestModel().trigger_pull_request_hook(
1624 pull_request, self._rhodecode_user, 'comment',
1631 pull_request, self._rhodecode_user, 'comment',
1625 data={'comment': comment})
1632 data={'comment': comment})
1626
1633
1627 # we now calculate the status of pull request, and based on that
1634 # we now calculate the status of pull request, and based on that
1628 # calculation we set the commits status
1635 # calculation we set the commits status
1629 calculated_status = pull_request.calculated_review_status()
1636 calculated_status = pull_request.calculated_review_status()
1630 if old_calculated_status != calculated_status:
1637 if old_calculated_status != calculated_status:
1631 PullRequestModel().trigger_pull_request_hook(
1638 PullRequestModel().trigger_pull_request_hook(
1632 pull_request, self._rhodecode_user, 'review_status_change',
1639 pull_request, self._rhodecode_user, 'review_status_change',
1633 data={'status': calculated_status})
1640 data={'status': calculated_status})
1634
1641
1635 Session().commit()
1642 Session().commit()
1636
1643
1637 data = {
1644 data = {
1638 'target_id': h.safeid(h.safe_unicode(
1645 'target_id': h.safeid(h.safe_unicode(
1639 self.request.POST.get('f_path'))),
1646 self.request.POST.get('f_path'))),
1640 }
1647 }
1648
1641 if comment:
1649 if comment:
1642 c.co = comment
1650 c.co = comment
1643 c.at_version_num = None
1651 c.at_version_num = None
1644 rendered_comment = render(
1652 rendered_comment = render(
1645 'rhodecode:templates/changeset/changeset_comment_block.mako',
1653 'rhodecode:templates/changeset/changeset_comment_block.mako',
1646 self._get_template_context(c), self.request)
1654 self._get_template_context(c), self.request)
1647
1655
1648 data.update(comment.get_dict())
1656 data.update(comment.get_dict())
1649 data.update({'rendered_text': rendered_comment})
1657 data.update({'rendered_text': rendered_comment})
1650
1658
1651 comment_broadcast_channel = channelstream.comment_channel(
1659 # skip channelstream for draft comments
1652 self.db_repo_name, pull_request_obj=pull_request)
1660 if not is_draft:
1661 comment_broadcast_channel = channelstream.comment_channel(
1662 self.db_repo_name, pull_request_obj=pull_request)
1653
1663
1654 comment_data = data
1664 comment_data = data
1655 comment_type = 'inline' if is_inline else 'general'
1665 comment_type = 'inline' if is_inline else 'general'
1656 channelstream.comment_channelstream_push(
1666 channelstream.comment_channelstream_push(
1657 self.request, comment_broadcast_channel, self._rhodecode_user,
1667 self.request, comment_broadcast_channel, self._rhodecode_user,
1658 _('posted a new {} comment').format(comment_type),
1668 _('posted a new {} comment').format(comment_type),
1659 comment_data=comment_data)
1669 comment_data=comment_data)
1660
1670
1661 return data
1671 return data
1662
1672
1663 @LoginRequired()
1673 @LoginRequired()
1664 @NotAnonymous()
1674 @NotAnonymous()
1665 @HasRepoPermissionAnyDecorator(
1675 @HasRepoPermissionAnyDecorator(
1666 'repository.read', 'repository.write', 'repository.admin')
1676 'repository.read', 'repository.write', 'repository.admin')
1667 @CSRFRequired()
1677 @CSRFRequired()
1668 @view_config(
1678 @view_config(
1669 route_name='pullrequest_comment_delete', request_method='POST',
1679 route_name='pullrequest_comment_delete', request_method='POST',
1670 renderer='json_ext')
1680 renderer='json_ext')
1671 def pull_request_comment_delete(self):
1681 def pull_request_comment_delete(self):
1672 pull_request = PullRequest.get_or_404(
1682 pull_request = PullRequest.get_or_404(
1673 self.request.matchdict['pull_request_id'])
1683 self.request.matchdict['pull_request_id'])
1674
1684
1675 comment = ChangesetComment.get_or_404(
1685 comment = ChangesetComment.get_or_404(
1676 self.request.matchdict['comment_id'])
1686 self.request.matchdict['comment_id'])
1677 comment_id = comment.comment_id
1687 comment_id = comment.comment_id
1678
1688
1679 if comment.immutable:
1689 if comment.immutable:
1680 # don't allow deleting comments that are immutable
1690 # don't allow deleting comments that are immutable
1681 raise HTTPForbidden()
1691 raise HTTPForbidden()
1682
1692
1683 if pull_request.is_closed():
1693 if pull_request.is_closed():
1684 log.debug('comment: forbidden because pull request is closed')
1694 log.debug('comment: forbidden because pull request is closed')
1685 raise HTTPForbidden()
1695 raise HTTPForbidden()
1686
1696
1687 if not comment:
1697 if not comment:
1688 log.debug('Comment with id:%s not found, skipping', comment_id)
1698 log.debug('Comment with id:%s not found, skipping', comment_id)
1689 # comment already deleted in another call probably
1699 # comment already deleted in another call probably
1690 return True
1700 return True
1691
1701
1692 if comment.pull_request.is_closed():
1702 if comment.pull_request.is_closed():
1693 # don't allow deleting comments on closed pull request
1703 # don't allow deleting comments on closed pull request
1694 raise HTTPForbidden()
1704 raise HTTPForbidden()
1695
1705
1696 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1706 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1697 super_admin = h.HasPermissionAny('hg.admin')()
1707 super_admin = h.HasPermissionAny('hg.admin')()
1698 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1708 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1699 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1709 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1700 comment_repo_admin = is_repo_admin and is_repo_comment
1710 comment_repo_admin = is_repo_admin and is_repo_comment
1701
1711
1702 if super_admin or comment_owner or comment_repo_admin:
1712 if super_admin or comment_owner or comment_repo_admin:
1703 old_calculated_status = comment.pull_request.calculated_review_status()
1713 old_calculated_status = comment.pull_request.calculated_review_status()
1704 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1714 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1705 Session().commit()
1715 Session().commit()
1706 calculated_status = comment.pull_request.calculated_review_status()
1716 calculated_status = comment.pull_request.calculated_review_status()
1707 if old_calculated_status != calculated_status:
1717 if old_calculated_status != calculated_status:
1708 PullRequestModel().trigger_pull_request_hook(
1718 PullRequestModel().trigger_pull_request_hook(
1709 comment.pull_request, self._rhodecode_user, 'review_status_change',
1719 comment.pull_request, self._rhodecode_user, 'review_status_change',
1710 data={'status': calculated_status})
1720 data={'status': calculated_status})
1711 return True
1721 return True
1712 else:
1722 else:
1713 log.warning('No permissions for user %s to delete comment_id: %s',
1723 log.warning('No permissions for user %s to delete comment_id: %s',
1714 self._rhodecode_db_user, comment_id)
1724 self._rhodecode_db_user, comment_id)
1715 raise HTTPNotFound()
1725 raise HTTPNotFound()
1716
1726
1717 @LoginRequired()
1727 @LoginRequired()
1718 @NotAnonymous()
1728 @NotAnonymous()
1719 @HasRepoPermissionAnyDecorator(
1729 @HasRepoPermissionAnyDecorator(
1720 'repository.read', 'repository.write', 'repository.admin')
1730 'repository.read', 'repository.write', 'repository.admin')
1721 @CSRFRequired()
1731 @CSRFRequired()
1722 @view_config(
1732 @view_config(
1723 route_name='pullrequest_comment_edit', request_method='POST',
1733 route_name='pullrequest_comment_edit', request_method='POST',
1724 renderer='json_ext')
1734 renderer='json_ext')
1725 def pull_request_comment_edit(self):
1735 def pull_request_comment_edit(self):
1726 self.load_default_context()
1736 self.load_default_context()
1727
1737
1728 pull_request = PullRequest.get_or_404(
1738 pull_request = PullRequest.get_or_404(
1729 self.request.matchdict['pull_request_id']
1739 self.request.matchdict['pull_request_id']
1730 )
1740 )
1731 comment = ChangesetComment.get_or_404(
1741 comment = ChangesetComment.get_or_404(
1732 self.request.matchdict['comment_id']
1742 self.request.matchdict['comment_id']
1733 )
1743 )
1734 comment_id = comment.comment_id
1744 comment_id = comment.comment_id
1735
1745
1736 if comment.immutable:
1746 if comment.immutable:
1737 # don't allow deleting comments that are immutable
1747 # don't allow deleting comments that are immutable
1738 raise HTTPForbidden()
1748 raise HTTPForbidden()
1739
1749
1740 if pull_request.is_closed():
1750 if pull_request.is_closed():
1741 log.debug('comment: forbidden because pull request is closed')
1751 log.debug('comment: forbidden because pull request is closed')
1742 raise HTTPForbidden()
1752 raise HTTPForbidden()
1743
1753
1744 if not comment:
1754 if not comment:
1745 log.debug('Comment with id:%s not found, skipping', comment_id)
1755 log.debug('Comment with id:%s not found, skipping', comment_id)
1746 # comment already deleted in another call probably
1756 # comment already deleted in another call probably
1747 return True
1757 return True
1748
1758
1749 if comment.pull_request.is_closed():
1759 if comment.pull_request.is_closed():
1750 # don't allow deleting comments on closed pull request
1760 # don't allow deleting comments on closed pull request
1751 raise HTTPForbidden()
1761 raise HTTPForbidden()
1752
1762
1753 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1763 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1754 super_admin = h.HasPermissionAny('hg.admin')()
1764 super_admin = h.HasPermissionAny('hg.admin')()
1755 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1765 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1756 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1766 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1757 comment_repo_admin = is_repo_admin and is_repo_comment
1767 comment_repo_admin = is_repo_admin and is_repo_comment
1758
1768
1759 if super_admin or comment_owner or comment_repo_admin:
1769 if super_admin or comment_owner or comment_repo_admin:
1760 text = self.request.POST.get('text')
1770 text = self.request.POST.get('text')
1761 version = self.request.POST.get('version')
1771 version = self.request.POST.get('version')
1762 if text == comment.text:
1772 if text == comment.text:
1763 log.warning(
1773 log.warning(
1764 'Comment(PR): '
1774 'Comment(PR): '
1765 'Trying to create new version '
1775 'Trying to create new version '
1766 'with the same comment body {}'.format(
1776 'with the same comment body {}'.format(
1767 comment_id,
1777 comment_id,
1768 )
1778 )
1769 )
1779 )
1770 raise HTTPNotFound()
1780 raise HTTPNotFound()
1771
1781
1772 if version.isdigit():
1782 if version.isdigit():
1773 version = int(version)
1783 version = int(version)
1774 else:
1784 else:
1775 log.warning(
1785 log.warning(
1776 'Comment(PR): Wrong version type {} {} '
1786 'Comment(PR): Wrong version type {} {} '
1777 'for comment {}'.format(
1787 'for comment {}'.format(
1778 version,
1788 version,
1779 type(version),
1789 type(version),
1780 comment_id,
1790 comment_id,
1781 )
1791 )
1782 )
1792 )
1783 raise HTTPNotFound()
1793 raise HTTPNotFound()
1784
1794
1785 try:
1795 try:
1786 comment_history = CommentsModel().edit(
1796 comment_history = CommentsModel().edit(
1787 comment_id=comment_id,
1797 comment_id=comment_id,
1788 text=text,
1798 text=text,
1789 auth_user=self._rhodecode_user,
1799 auth_user=self._rhodecode_user,
1790 version=version,
1800 version=version,
1791 )
1801 )
1792 except CommentVersionMismatch:
1802 except CommentVersionMismatch:
1793 raise HTTPConflict()
1803 raise HTTPConflict()
1794
1804
1795 if not comment_history:
1805 if not comment_history:
1796 raise HTTPNotFound()
1806 raise HTTPNotFound()
1797
1807
1798 Session().commit()
1808 Session().commit()
1799
1809
1800 PullRequestModel().trigger_pull_request_hook(
1810 PullRequestModel().trigger_pull_request_hook(
1801 pull_request, self._rhodecode_user, 'comment_edit',
1811 pull_request, self._rhodecode_user, 'comment_edit',
1802 data={'comment': comment})
1812 data={'comment': comment})
1803
1813
1804 return {
1814 return {
1805 'comment_history_id': comment_history.comment_history_id,
1815 'comment_history_id': comment_history.comment_history_id,
1806 'comment_id': comment.comment_id,
1816 'comment_id': comment.comment_id,
1807 'comment_version': comment_history.version,
1817 'comment_version': comment_history.version,
1808 'comment_author_username': comment_history.author.username,
1818 'comment_author_username': comment_history.author.username,
1809 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1819 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1810 'comment_created_on': h.age_component(comment_history.created_on,
1820 'comment_created_on': h.age_component(comment_history.created_on,
1811 time_is_local=True),
1821 time_is_local=True),
1812 }
1822 }
1813 else:
1823 else:
1814 log.warning('No permissions for user %s to edit comment_id: %s',
1824 log.warning('No permissions for user %s to edit comment_id: %s',
1815 self._rhodecode_db_user, comment_id)
1825 self._rhodecode_db_user, comment_id)
1816 raise HTTPNotFound()
1826 raise HTTPNotFound()
@@ -1,821 +1,843 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
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 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24 import datetime
24 import datetime
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28 import collections
28 import collections
29
29
30 from pyramid.threadlocal import get_current_registry, get_current_request
30 from pyramid.threadlocal import get_current_registry, get_current_request
31 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.expression import null
32 from sqlalchemy.sql.functions import coalesce
32 from sqlalchemy.sql.functions import coalesce
33
33
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 from rhodecode.lib import audit_logger
35 from rhodecode.lib import audit_logger
36 from rhodecode.lib.exceptions import CommentVersionMismatch
36 from rhodecode.lib.exceptions import CommentVersionMismatch
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,
40 ChangesetComment,
41 ChangesetComment,
41 User,
42 User,
42 Notification,
43 Notification,
43 PullRequest,
44 PullRequest,
44 AttributeDict,
45 AttributeDict,
45 ChangesetCommentHistory,
46 ChangesetCommentHistory,
46 )
47 )
47 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.meta import Session
49 from rhodecode.model.meta import Session
49 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.validation_schema.schemas import comment_schema
52 from rhodecode.model.validation_schema.schemas import comment_schema
52
53
53
54
54 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
55
56
56
57
57 class CommentsModel(BaseModel):
58 class CommentsModel(BaseModel):
58
59
59 cls = ChangesetComment
60 cls = ChangesetComment
60
61
61 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_AFTER = 3
63 DIFF_CONTEXT_AFTER = 3
63
64
64 def __get_commit_comment(self, changeset_comment):
65 def __get_commit_comment(self, changeset_comment):
65 return self._get_instance(ChangesetComment, changeset_comment)
66 return self._get_instance(ChangesetComment, changeset_comment)
66
67
67 def __get_pull_request(self, pull_request):
68 def __get_pull_request(self, pull_request):
68 return self._get_instance(PullRequest, pull_request)
69 return self._get_instance(PullRequest, pull_request)
69
70
70 def _extract_mentions(self, s):
71 def _extract_mentions(self, s):
71 user_objects = []
72 user_objects = []
72 for username in extract_mentioned_users(s):
73 for username in extract_mentioned_users(s):
73 user_obj = User.get_by_username(username, case_insensitive=True)
74 user_obj = User.get_by_username(username, case_insensitive=True)
74 if user_obj:
75 if user_obj:
75 user_objects.append(user_obj)
76 user_objects.append(user_obj)
76 return user_objects
77 return user_objects
77
78
78 def _get_renderer(self, global_renderer='rst', request=None):
79 def _get_renderer(self, global_renderer='rst', request=None):
79 request = request or get_current_request()
80 request = request or get_current_request()
80
81
81 try:
82 try:
82 global_renderer = request.call_context.visual.default_renderer
83 global_renderer = request.call_context.visual.default_renderer
83 except AttributeError:
84 except AttributeError:
84 log.debug("Renderer not set, falling back "
85 log.debug("Renderer not set, falling back "
85 "to default renderer '%s'", global_renderer)
86 "to default renderer '%s'", global_renderer)
86 except Exception:
87 except Exception:
87 log.error(traceback.format_exc())
88 log.error(traceback.format_exc())
88 return global_renderer
89 return global_renderer
89
90
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 # group by versions, and count until, and display objects
92 # group by versions, and count until, and display objects
92
93
93 comment_groups = collections.defaultdict(list)
94 comment_groups = collections.defaultdict(list)
94 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95
96
96 def yield_comments(pos):
97 def yield_comments(pos):
97 for co in comment_groups[pos]:
98 for co in comment_groups[pos]:
98 yield co
99 yield co
99
100
100 comment_versions = collections.defaultdict(
101 comment_versions = collections.defaultdict(
101 lambda: collections.defaultdict(list))
102 lambda: collections.defaultdict(list))
102 prev_prvid = -1
103 prev_prvid = -1
103 # fake last entry with None, to aggregate on "latest" version which
104 # fake last entry with None, to aggregate on "latest" version which
104 # doesn't have an pull_request_version_id
105 # doesn't have an pull_request_version_id
105 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 prvid = ver.pull_request_version_id
107 prvid = ver.pull_request_version_id
107 if prev_prvid == -1:
108 if prev_prvid == -1:
108 prev_prvid = prvid
109 prev_prvid = prvid
109
110
110 for co in yield_comments(prvid):
111 for co in yield_comments(prvid):
111 comment_versions[prvid]['at'].append(co)
112 comment_versions[prvid]['at'].append(co)
112
113
113 # save until
114 # save until
114 current = comment_versions[prvid]['at']
115 current = comment_versions[prvid]['at']
115 prev_until = comment_versions[prev_prvid]['until']
116 prev_until = comment_versions[prev_prvid]['until']
116 cur_until = prev_until + current
117 cur_until = prev_until + current
117 comment_versions[prvid]['until'].extend(cur_until)
118 comment_versions[prvid]['until'].extend(cur_until)
118
119
119 # save outdated
120 # save outdated
120 if inline:
121 if inline:
121 outdated = [x for x in cur_until
122 outdated = [x for x in cur_until
122 if x.outdated_at_version(show_version)]
123 if x.outdated_at_version(show_version)]
123 else:
124 else:
124 outdated = [x for x in cur_until
125 outdated = [x for x in cur_until
125 if x.older_than_version(show_version)]
126 if x.older_than_version(show_version)]
126 display = [x for x in cur_until if x not in outdated]
127 display = [x for x in cur_until if x not in outdated]
127
128
128 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['display'] = display
130 comment_versions[prvid]['display'] = display
130
131
131 prev_prvid = prvid
132 prev_prvid = prvid
132
133
133 return comment_versions
134 return comment_versions
134
135
135 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 qry = Session().query(ChangesetComment) \
137 qry = Session().query(ChangesetComment) \
137 .filter(ChangesetComment.repo == repo)
138 .filter(ChangesetComment.repo == repo)
138
139
139 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141
142
142 if user:
143 if user:
143 user = self._get_user(user)
144 user = self._get_user(user)
144 if user:
145 if user:
145 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146
147
147 if commit_id:
148 if commit_id:
148 qry = qry.filter(ChangesetComment.revision == commit_id)
149 qry = qry.filter(ChangesetComment.revision == commit_id)
149
150
150 qry = qry.order_by(ChangesetComment.created_on)
151 qry = qry.order_by(ChangesetComment.created_on)
151 return qry.all()
152 return qry.all()
152
153
153 def get_repository_unresolved_todos(self, repo):
154 def get_repository_unresolved_todos(self, repo):
154 todos = Session().query(ChangesetComment) \
155 todos = Session().query(ChangesetComment) \
155 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.comment_type
158 .filter(ChangesetComment.comment_type
158 == ChangesetComment.COMMENT_TYPE_TODO)
159 == ChangesetComment.COMMENT_TYPE_TODO)
159 todos = todos.all()
160 todos = todos.all()
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) \
167 .filter(ChangesetComment.resolved_by == None) \
168 .filter(ChangesetComment.resolved_by == None) \
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, '') !=
174 ChangesetComment.COMMENT_OUTDATED)
178 ChangesetComment.COMMENT_OUTDATED)
175
179
176 todos = todos.all()
180 todos = todos.all()
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) \
184 .filter(ChangesetComment.resolved_by != None) \
188 .filter(ChangesetComment.resolved_by != None) \
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, '') !=
191 ChangesetComment.COMMENT_OUTDATED)
198 ChangesetComment.COMMENT_OUTDATED)
192
199
193 todos = todos.all()
200 todos = todos.all()
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_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
198
205
199 todos = Session().query(ChangesetComment) \
206 todos = Session().query(ChangesetComment) \
200 .filter(ChangesetComment.revision == commit_id) \
207 .filter(ChangesetComment.revision == commit_id) \
201 .filter(ChangesetComment.resolved_by == None) \
208 .filter(ChangesetComment.resolved_by == None) \
202 .filter(ChangesetComment.comment_type
209 .filter(ChangesetComment.comment_type
203 == ChangesetComment.COMMENT_TYPE_TODO)
210 == ChangesetComment.COMMENT_TYPE_TODO)
204
211
212 if not include_drafts:
213 todos = todos.filter(ChangesetComment.draft == false())
214
205 if not show_outdated:
215 if not show_outdated:
206 todos = todos.filter(
216 todos = todos.filter(
207 coalesce(ChangesetComment.display_state, '') !=
217 coalesce(ChangesetComment.display_state, '') !=
208 ChangesetComment.COMMENT_OUTDATED)
218 ChangesetComment.COMMENT_OUTDATED)
209
219
210 todos = todos.all()
220 todos = todos.all()
211
221
212 return todos
222 return todos
213
223
214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
224 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
215
225
216 todos = Session().query(ChangesetComment) \
226 todos = Session().query(ChangesetComment) \
217 .filter(ChangesetComment.revision == commit_id) \
227 .filter(ChangesetComment.revision == commit_id) \
218 .filter(ChangesetComment.resolved_by != None) \
228 .filter(ChangesetComment.resolved_by != None) \
219 .filter(ChangesetComment.comment_type
229 .filter(ChangesetComment.comment_type
220 == ChangesetComment.COMMENT_TYPE_TODO)
230 == ChangesetComment.COMMENT_TYPE_TODO)
221
231
232 if not include_drafts:
233 todos = todos.filter(ChangesetComment.draft == false())
234
222 if not show_outdated:
235 if not show_outdated:
223 todos = todos.filter(
236 todos = todos.filter(
224 coalesce(ChangesetComment.display_state, '') !=
237 coalesce(ChangesetComment.display_state, '') !=
225 ChangesetComment.COMMENT_OUTDATED)
238 ChangesetComment.COMMENT_OUTDATED)
226
239
227 todos = todos.all()
240 todos = todos.all()
228
241
229 return todos
242 return todos
230
243
231 def get_commit_inline_comments(self, commit_id):
244 def get_commit_inline_comments(self, commit_id, include_drafts=True):
232 inline_comments = Session().query(ChangesetComment) \
245 inline_comments = Session().query(ChangesetComment) \
233 .filter(ChangesetComment.line_no != None) \
246 .filter(ChangesetComment.line_no != None) \
234 .filter(ChangesetComment.f_path != None) \
247 .filter(ChangesetComment.f_path != None) \
235 .filter(ChangesetComment.revision == commit_id)
248 .filter(ChangesetComment.revision == commit_id)
249
250 if not include_drafts:
251 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
252
236 inline_comments = inline_comments.all()
253 inline_comments = inline_comments.all()
237 return inline_comments
254 return inline_comments
238
255
239 def _log_audit_action(self, action, action_data, auth_user, comment):
256 def _log_audit_action(self, action, action_data, auth_user, comment):
240 audit_logger.store(
257 audit_logger.store(
241 action=action,
258 action=action,
242 action_data=action_data,
259 action_data=action_data,
243 user=auth_user,
260 user=auth_user,
244 repo=comment.repo)
261 repo=comment.repo)
245
262
246 def create(self, text, repo, user, commit_id=None, pull_request=None,
263 def create(self, text, repo, user, commit_id=None, pull_request=None,
247 f_path=None, line_no=None, status_change=None,
264 f_path=None, line_no=None, status_change=None,
248 status_change_type=None, comment_type=None,
265 status_change_type=None, comment_type=None, is_draft=False,
249 resolves_comment_id=None, closing_pr=False, send_email=True,
266 resolves_comment_id=None, closing_pr=False, send_email=True,
250 renderer=None, auth_user=None, extra_recipients=None):
267 renderer=None, auth_user=None, extra_recipients=None):
251 """
268 """
252 Creates new comment for commit or pull request.
269 Creates new comment for commit or pull request.
253 IF status_change is not none this comment is associated with a
270 IF status_change is not none this comment is associated with a
254 status change of commit or commit associated with pull request
271 status change of commit or commit associated with pull request
255
272
256 :param text:
273 :param text:
257 :param repo:
274 :param repo:
258 :param user:
275 :param user:
259 :param commit_id:
276 :param commit_id:
260 :param pull_request:
277 :param pull_request:
261 :param f_path:
278 :param f_path:
262 :param line_no:
279 :param line_no:
263 :param status_change: Label for status change
280 :param status_change: Label for status change
264 :param comment_type: Type of comment
281 :param comment_type: Type of comment
282 :param is_draft: is comment a draft only
265 :param resolves_comment_id: id of comment which this one will resolve
283 :param resolves_comment_id: id of comment which this one will resolve
266 :param status_change_type: type of status change
284 :param status_change_type: type of status change
267 :param closing_pr:
285 :param closing_pr:
268 :param send_email:
286 :param send_email:
269 :param renderer: pick renderer for this comment
287 :param renderer: pick renderer for this comment
270 :param auth_user: current authenticated user calling this method
288 :param auth_user: current authenticated user calling this method
271 :param extra_recipients: list of extra users to be added to recipients
289 :param extra_recipients: list of extra users to be added to recipients
272 """
290 """
273
291
274 if not text:
292 if not text:
275 log.warning('Missing text for comment, skipping...')
293 log.warning('Missing text for comment, skipping...')
276 return
294 return
277 request = get_current_request()
295 request = get_current_request()
278 _ = request.translate
296 _ = request.translate
279
297
280 if not renderer:
298 if not renderer:
281 renderer = self._get_renderer(request=request)
299 renderer = self._get_renderer(request=request)
282
300
283 repo = self._get_repo(repo)
301 repo = self._get_repo(repo)
284 user = self._get_user(user)
302 user = self._get_user(user)
285 auth_user = auth_user or user
303 auth_user = auth_user or user
286
304
287 schema = comment_schema.CommentSchema()
305 schema = comment_schema.CommentSchema()
288 validated_kwargs = schema.deserialize(dict(
306 validated_kwargs = schema.deserialize(dict(
289 comment_body=text,
307 comment_body=text,
290 comment_type=comment_type,
308 comment_type=comment_type,
309 is_draft=is_draft,
291 comment_file=f_path,
310 comment_file=f_path,
292 comment_line=line_no,
311 comment_line=line_no,
293 renderer_type=renderer,
312 renderer_type=renderer,
294 status_change=status_change_type,
313 status_change=status_change_type,
295 resolves_comment_id=resolves_comment_id,
314 resolves_comment_id=resolves_comment_id,
296 repo=repo.repo_id,
315 repo=repo.repo_id,
297 user=user.user_id,
316 user=user.user_id,
298 ))
317 ))
318 is_draft = validated_kwargs['is_draft']
299
319
300 comment = ChangesetComment()
320 comment = ChangesetComment()
301 comment.renderer = validated_kwargs['renderer_type']
321 comment.renderer = validated_kwargs['renderer_type']
302 comment.text = validated_kwargs['comment_body']
322 comment.text = validated_kwargs['comment_body']
303 comment.f_path = validated_kwargs['comment_file']
323 comment.f_path = validated_kwargs['comment_file']
304 comment.line_no = validated_kwargs['comment_line']
324 comment.line_no = validated_kwargs['comment_line']
305 comment.comment_type = validated_kwargs['comment_type']
325 comment.comment_type = validated_kwargs['comment_type']
326 comment.draft = is_draft
306
327
307 comment.repo = repo
328 comment.repo = repo
308 comment.author = user
329 comment.author = user
309 resolved_comment = self.__get_commit_comment(
330 resolved_comment = self.__get_commit_comment(
310 validated_kwargs['resolves_comment_id'])
331 validated_kwargs['resolves_comment_id'])
311 # check if the comment actually belongs to this PR
332 # check if the comment actually belongs to this PR
312 if resolved_comment and resolved_comment.pull_request and \
333 if resolved_comment and resolved_comment.pull_request and \
313 resolved_comment.pull_request != pull_request:
334 resolved_comment.pull_request != pull_request:
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
335 log.warning('Comment tried to resolved unrelated todo comment: %s',
315 resolved_comment)
336 resolved_comment)
316 # comment not bound to this pull request, forbid
337 # comment not bound to this pull request, forbid
317 resolved_comment = None
338 resolved_comment = None
318
339
319 elif resolved_comment and resolved_comment.repo and \
340 elif resolved_comment and resolved_comment.repo and \
320 resolved_comment.repo != repo:
341 resolved_comment.repo != repo:
321 log.warning('Comment tried to resolved unrelated todo comment: %s',
342 log.warning('Comment tried to resolved unrelated todo comment: %s',
322 resolved_comment)
343 resolved_comment)
323 # comment not bound to this repo, forbid
344 # comment not bound to this repo, forbid
324 resolved_comment = None
345 resolved_comment = None
325
346
326 comment.resolved_comment = resolved_comment
347 comment.resolved_comment = resolved_comment
327
348
328 pull_request_id = pull_request
349 pull_request_id = pull_request
329
350
330 commit_obj = None
351 commit_obj = None
331 pull_request_obj = None
352 pull_request_obj = None
332
353
333 if commit_id:
354 if commit_id:
334 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
355 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
335 # do a lookup, so we don't pass something bad here
356 # do a lookup, so we don't pass something bad here
336 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
357 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
337 comment.revision = commit_obj.raw_id
358 comment.revision = commit_obj.raw_id
338
359
339 elif pull_request_id:
360 elif pull_request_id:
340 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
361 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
341 pull_request_obj = self.__get_pull_request(pull_request_id)
362 pull_request_obj = self.__get_pull_request(pull_request_id)
342 comment.pull_request = pull_request_obj
363 comment.pull_request = pull_request_obj
343 else:
364 else:
344 raise Exception('Please specify commit or pull_request_id')
365 raise Exception('Please specify commit or pull_request_id')
345
366
346 Session().add(comment)
367 Session().add(comment)
347 Session().flush()
368 Session().flush()
348 kwargs = {
369 kwargs = {
349 'user': user,
370 'user': user,
350 'renderer_type': renderer,
371 'renderer_type': renderer,
351 'repo_name': repo.repo_name,
372 'repo_name': repo.repo_name,
352 'status_change': status_change,
373 'status_change': status_change,
353 'status_change_type': status_change_type,
374 'status_change_type': status_change_type,
354 'comment_body': text,
375 'comment_body': text,
355 'comment_file': f_path,
376 'comment_file': f_path,
356 'comment_line': line_no,
377 'comment_line': line_no,
357 'comment_type': comment_type or 'note',
378 'comment_type': comment_type or 'note',
358 'comment_id': comment.comment_id
379 'comment_id': comment.comment_id
359 }
380 }
360
381
361 if commit_obj:
382 if commit_obj:
362 recipients = ChangesetComment.get_users(
383 recipients = ChangesetComment.get_users(
363 revision=commit_obj.raw_id)
384 revision=commit_obj.raw_id)
364 # add commit author if it's in RhodeCode system
385 # add commit author if it's in RhodeCode system
365 cs_author = User.get_from_cs_author(commit_obj.author)
386 cs_author = User.get_from_cs_author(commit_obj.author)
366 if not cs_author:
387 if not cs_author:
367 # use repo owner if we cannot extract the author correctly
388 # use repo owner if we cannot extract the author correctly
368 cs_author = repo.user
389 cs_author = repo.user
369 recipients += [cs_author]
390 recipients += [cs_author]
370
391
371 commit_comment_url = self.get_url(comment, request=request)
392 commit_comment_url = self.get_url(comment, request=request)
372 commit_comment_reply_url = self.get_url(
393 commit_comment_reply_url = self.get_url(
373 comment, request=request,
394 comment, request=request,
374 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
395 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
375
396
376 target_repo_url = h.link_to(
397 target_repo_url = h.link_to(
377 repo.repo_name,
398 repo.repo_name,
378 h.route_url('repo_summary', repo_name=repo.repo_name))
399 h.route_url('repo_summary', repo_name=repo.repo_name))
379
400
380 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
401 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
381 commit_id=commit_id)
402 commit_id=commit_id)
382
403
383 # commit specifics
404 # commit specifics
384 kwargs.update({
405 kwargs.update({
385 'commit': commit_obj,
406 'commit': commit_obj,
386 'commit_message': commit_obj.message,
407 'commit_message': commit_obj.message,
387 'commit_target_repo_url': target_repo_url,
408 'commit_target_repo_url': target_repo_url,
388 'commit_comment_url': commit_comment_url,
409 'commit_comment_url': commit_comment_url,
389 'commit_comment_reply_url': commit_comment_reply_url,
410 'commit_comment_reply_url': commit_comment_reply_url,
390 'commit_url': commit_url,
411 'commit_url': commit_url,
391 'thread_ids': [commit_url, commit_comment_url],
412 'thread_ids': [commit_url, commit_comment_url],
392 })
413 })
393
414
394 elif pull_request_obj:
415 elif pull_request_obj:
395 # get the current participants of this pull request
416 # get the current participants of this pull request
396 recipients = ChangesetComment.get_users(
417 recipients = ChangesetComment.get_users(
397 pull_request_id=pull_request_obj.pull_request_id)
418 pull_request_id=pull_request_obj.pull_request_id)
398 # add pull request author
419 # add pull request author
399 recipients += [pull_request_obj.author]
420 recipients += [pull_request_obj.author]
400
421
401 # add the reviewers to notification
422 # add the reviewers to notification
402 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
423 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
403
424
404 pr_target_repo = pull_request_obj.target_repo
425 pr_target_repo = pull_request_obj.target_repo
405 pr_source_repo = pull_request_obj.source_repo
426 pr_source_repo = pull_request_obj.source_repo
406
427
407 pr_comment_url = self.get_url(comment, request=request)
428 pr_comment_url = self.get_url(comment, request=request)
408 pr_comment_reply_url = self.get_url(
429 pr_comment_reply_url = self.get_url(
409 comment, request=request,
430 comment, request=request,
410 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
431 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
411
432
412 pr_url = h.route_url(
433 pr_url = h.route_url(
413 'pullrequest_show',
434 'pullrequest_show',
414 repo_name=pr_target_repo.repo_name,
435 repo_name=pr_target_repo.repo_name,
415 pull_request_id=pull_request_obj.pull_request_id, )
436 pull_request_id=pull_request_obj.pull_request_id, )
416
437
417 # set some variables for email notification
438 # set some variables for email notification
418 pr_target_repo_url = h.route_url(
439 pr_target_repo_url = h.route_url(
419 'repo_summary', repo_name=pr_target_repo.repo_name)
440 'repo_summary', repo_name=pr_target_repo.repo_name)
420
441
421 pr_source_repo_url = h.route_url(
442 pr_source_repo_url = h.route_url(
422 'repo_summary', repo_name=pr_source_repo.repo_name)
443 'repo_summary', repo_name=pr_source_repo.repo_name)
423
444
424 # pull request specifics
445 # pull request specifics
425 kwargs.update({
446 kwargs.update({
426 'pull_request': pull_request_obj,
447 'pull_request': pull_request_obj,
427 'pr_id': pull_request_obj.pull_request_id,
448 'pr_id': pull_request_obj.pull_request_id,
428 'pull_request_url': pr_url,
449 'pull_request_url': pr_url,
429 'pull_request_target_repo': pr_target_repo,
450 'pull_request_target_repo': pr_target_repo,
430 'pull_request_target_repo_url': pr_target_repo_url,
451 'pull_request_target_repo_url': pr_target_repo_url,
431 'pull_request_source_repo': pr_source_repo,
452 'pull_request_source_repo': pr_source_repo,
432 'pull_request_source_repo_url': pr_source_repo_url,
453 'pull_request_source_repo_url': pr_source_repo_url,
433 'pr_comment_url': pr_comment_url,
454 'pr_comment_url': pr_comment_url,
434 'pr_comment_reply_url': pr_comment_reply_url,
455 'pr_comment_reply_url': pr_comment_reply_url,
435 'pr_closing': closing_pr,
456 'pr_closing': closing_pr,
436 'thread_ids': [pr_url, pr_comment_url],
457 'thread_ids': [pr_url, pr_comment_url],
437 })
458 })
438
459
439 if send_email:
460 if send_email:
440 recipients += [self._get_user(u) for u in (extra_recipients or [])]
461 recipients += [self._get_user(u) for u in (extra_recipients or [])]
441 # pre-generate the subject for notification itself
462 # pre-generate the subject for notification itself
442 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
463 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
443 notification_type, **kwargs)
464 notification_type, **kwargs)
444
465
445 mention_recipients = set(
466 mention_recipients = set(
446 self._extract_mentions(text)).difference(recipients)
467 self._extract_mentions(text)).difference(recipients)
447
468
448 # create notification objects, and emails
469 # create notification objects, and emails
449 NotificationModel().create(
470 NotificationModel().create(
450 created_by=user,
471 created_by=user,
451 notification_subject=subject,
472 notification_subject=subject,
452 notification_body=body_plaintext,
473 notification_body=body_plaintext,
453 notification_type=notification_type,
474 notification_type=notification_type,
454 recipients=recipients,
475 recipients=recipients,
455 mention_recipients=mention_recipients,
476 mention_recipients=mention_recipients,
456 email_kwargs=kwargs,
477 email_kwargs=kwargs,
457 )
478 )
458
479
459 Session().flush()
480 Session().flush()
460 if comment.pull_request:
481 if comment.pull_request:
461 action = 'repo.pull_request.comment.create'
482 action = 'repo.pull_request.comment.create'
462 else:
483 else:
463 action = 'repo.commit.comment.create'
484 action = 'repo.commit.comment.create'
464
485
465 comment_data = comment.get_api_data()
486 if not is_draft:
487 comment_data = comment.get_api_data()
466
488
467 self._log_audit_action(
489 self._log_audit_action(
468 action, {'data': comment_data}, auth_user, comment)
490 action, {'data': comment_data}, auth_user, comment)
469
491
470 return comment
492 return comment
471
493
472 def edit(self, comment_id, text, auth_user, version):
494 def edit(self, comment_id, text, auth_user, version):
473 """
495 """
474 Change existing comment for commit or pull request.
496 Change existing comment for commit or pull request.
475
497
476 :param comment_id:
498 :param comment_id:
477 :param text:
499 :param text:
478 :param auth_user: current authenticated user calling this method
500 :param auth_user: current authenticated user calling this method
479 :param version: last comment version
501 :param version: last comment version
480 """
502 """
481 if not text:
503 if not text:
482 log.warning('Missing text for comment, skipping...')
504 log.warning('Missing text for comment, skipping...')
483 return
505 return
484
506
485 comment = ChangesetComment.get(comment_id)
507 comment = ChangesetComment.get(comment_id)
486 old_comment_text = comment.text
508 old_comment_text = comment.text
487 comment.text = text
509 comment.text = text
488 comment.modified_at = datetime.datetime.now()
510 comment.modified_at = datetime.datetime.now()
489 version = safe_int(version)
511 version = safe_int(version)
490
512
491 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
513 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
492 # would return 3 here
514 # would return 3 here
493 comment_version = ChangesetCommentHistory.get_version(comment_id)
515 comment_version = ChangesetCommentHistory.get_version(comment_id)
494
516
495 if isinstance(version, (int, long)) and (comment_version - version) != 1:
517 if isinstance(version, (int, long)) and (comment_version - version) != 1:
496 log.warning(
518 log.warning(
497 'Version mismatch comment_version {} submitted {}, skipping'.format(
519 'Version mismatch comment_version {} submitted {}, skipping'.format(
498 comment_version-1, # -1 since note above
520 comment_version-1, # -1 since note above
499 version
521 version
500 )
522 )
501 )
523 )
502 raise CommentVersionMismatch()
524 raise CommentVersionMismatch()
503
525
504 comment_history = ChangesetCommentHistory()
526 comment_history = ChangesetCommentHistory()
505 comment_history.comment_id = comment_id
527 comment_history.comment_id = comment_id
506 comment_history.version = comment_version
528 comment_history.version = comment_version
507 comment_history.created_by_user_id = auth_user.user_id
529 comment_history.created_by_user_id = auth_user.user_id
508 comment_history.text = old_comment_text
530 comment_history.text = old_comment_text
509 # TODO add email notification
531 # TODO add email notification
510 Session().add(comment_history)
532 Session().add(comment_history)
511 Session().add(comment)
533 Session().add(comment)
512 Session().flush()
534 Session().flush()
513
535
514 if comment.pull_request:
536 if comment.pull_request:
515 action = 'repo.pull_request.comment.edit'
537 action = 'repo.pull_request.comment.edit'
516 else:
538 else:
517 action = 'repo.commit.comment.edit'
539 action = 'repo.commit.comment.edit'
518
540
519 comment_data = comment.get_api_data()
541 comment_data = comment.get_api_data()
520 comment_data['old_comment_text'] = old_comment_text
542 comment_data['old_comment_text'] = old_comment_text
521 self._log_audit_action(
543 self._log_audit_action(
522 action, {'data': comment_data}, auth_user, comment)
544 action, {'data': comment_data}, auth_user, comment)
523
545
524 return comment_history
546 return comment_history
525
547
526 def delete(self, comment, auth_user):
548 def delete(self, comment, auth_user):
527 """
549 """
528 Deletes given comment
550 Deletes given comment
529 """
551 """
530 comment = self.__get_commit_comment(comment)
552 comment = self.__get_commit_comment(comment)
531 old_data = comment.get_api_data()
553 old_data = comment.get_api_data()
532 Session().delete(comment)
554 Session().delete(comment)
533
555
534 if comment.pull_request:
556 if comment.pull_request:
535 action = 'repo.pull_request.comment.delete'
557 action = 'repo.pull_request.comment.delete'
536 else:
558 else:
537 action = 'repo.commit.comment.delete'
559 action = 'repo.commit.comment.delete'
538
560
539 self._log_audit_action(
561 self._log_audit_action(
540 action, {'old_data': old_data}, auth_user, comment)
562 action, {'old_data': old_data}, auth_user, comment)
541
563
542 return comment
564 return comment
543
565
544 def get_all_comments(self, repo_id, revision=None, pull_request=None, count_only=False):
566 def get_all_comments(self, repo_id, revision=None, pull_request=None, count_only=False):
545 q = ChangesetComment.query()\
567 q = ChangesetComment.query()\
546 .filter(ChangesetComment.repo_id == repo_id)
568 .filter(ChangesetComment.repo_id == repo_id)
547 if revision:
569 if revision:
548 q = q.filter(ChangesetComment.revision == revision)
570 q = q.filter(ChangesetComment.revision == revision)
549 elif pull_request:
571 elif pull_request:
550 pull_request = self.__get_pull_request(pull_request)
572 pull_request = self.__get_pull_request(pull_request)
551 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
573 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
552 else:
574 else:
553 raise Exception('Please specify commit or pull_request')
575 raise Exception('Please specify commit or pull_request')
554 q = q.order_by(ChangesetComment.created_on)
576 q = q.order_by(ChangesetComment.created_on)
555 if count_only:
577 if count_only:
556 return q.count()
578 return q.count()
557
579
558 return q.all()
580 return q.all()
559
581
560 def get_url(self, comment, request=None, permalink=False, anchor=None):
582 def get_url(self, comment, request=None, permalink=False, anchor=None):
561 if not request:
583 if not request:
562 request = get_current_request()
584 request = get_current_request()
563
585
564 comment = self.__get_commit_comment(comment)
586 comment = self.__get_commit_comment(comment)
565 if anchor is None:
587 if anchor is None:
566 anchor = 'comment-{}'.format(comment.comment_id)
588 anchor = 'comment-{}'.format(comment.comment_id)
567
589
568 if comment.pull_request:
590 if comment.pull_request:
569 pull_request = comment.pull_request
591 pull_request = comment.pull_request
570 if permalink:
592 if permalink:
571 return request.route_url(
593 return request.route_url(
572 'pull_requests_global',
594 'pull_requests_global',
573 pull_request_id=pull_request.pull_request_id,
595 pull_request_id=pull_request.pull_request_id,
574 _anchor=anchor)
596 _anchor=anchor)
575 else:
597 else:
576 return request.route_url(
598 return request.route_url(
577 'pullrequest_show',
599 'pullrequest_show',
578 repo_name=safe_str(pull_request.target_repo.repo_name),
600 repo_name=safe_str(pull_request.target_repo.repo_name),
579 pull_request_id=pull_request.pull_request_id,
601 pull_request_id=pull_request.pull_request_id,
580 _anchor=anchor)
602 _anchor=anchor)
581
603
582 else:
604 else:
583 repo = comment.repo
605 repo = comment.repo
584 commit_id = comment.revision
606 commit_id = comment.revision
585
607
586 if permalink:
608 if permalink:
587 return request.route_url(
609 return request.route_url(
588 'repo_commit', repo_name=safe_str(repo.repo_id),
610 'repo_commit', repo_name=safe_str(repo.repo_id),
589 commit_id=commit_id,
611 commit_id=commit_id,
590 _anchor=anchor)
612 _anchor=anchor)
591
613
592 else:
614 else:
593 return request.route_url(
615 return request.route_url(
594 'repo_commit', repo_name=safe_str(repo.repo_name),
616 'repo_commit', repo_name=safe_str(repo.repo_name),
595 commit_id=commit_id,
617 commit_id=commit_id,
596 _anchor=anchor)
618 _anchor=anchor)
597
619
598 def get_comments(self, repo_id, revision=None, pull_request=None):
620 def get_comments(self, repo_id, revision=None, pull_request=None):
599 """
621 """
600 Gets main comments based on revision or pull_request_id
622 Gets main comments based on revision or pull_request_id
601
623
602 :param repo_id:
624 :param repo_id:
603 :param revision:
625 :param revision:
604 :param pull_request:
626 :param pull_request:
605 """
627 """
606
628
607 q = ChangesetComment.query()\
629 q = ChangesetComment.query()\
608 .filter(ChangesetComment.repo_id == repo_id)\
630 .filter(ChangesetComment.repo_id == repo_id)\
609 .filter(ChangesetComment.line_no == None)\
631 .filter(ChangesetComment.line_no == None)\
610 .filter(ChangesetComment.f_path == None)
632 .filter(ChangesetComment.f_path == None)
611 if revision:
633 if revision:
612 q = q.filter(ChangesetComment.revision == revision)
634 q = q.filter(ChangesetComment.revision == revision)
613 elif pull_request:
635 elif pull_request:
614 pull_request = self.__get_pull_request(pull_request)
636 pull_request = self.__get_pull_request(pull_request)
615 q = q.filter(ChangesetComment.pull_request == pull_request)
637 q = q.filter(ChangesetComment.pull_request == pull_request)
616 else:
638 else:
617 raise Exception('Please specify commit or pull_request')
639 raise Exception('Please specify commit or pull_request')
618 q = q.order_by(ChangesetComment.created_on)
640 q = q.order_by(ChangesetComment.created_on)
619 return q.all()
641 return q.all()
620
642
621 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
643 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
622 q = self._get_inline_comments_query(repo_id, revision, pull_request)
644 q = self._get_inline_comments_query(repo_id, revision, pull_request)
623 return self._group_comments_by_path_and_line_number(q)
645 return self._group_comments_by_path_and_line_number(q)
624
646
625 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
647 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
626 version=None):
648 version=None):
627 inline_comms = []
649 inline_comms = []
628 for fname, per_line_comments in inline_comments.iteritems():
650 for fname, per_line_comments in inline_comments.iteritems():
629 for lno, comments in per_line_comments.iteritems():
651 for lno, comments in per_line_comments.iteritems():
630 for comm in comments:
652 for comm in comments:
631 if not comm.outdated_at_version(version) and skip_outdated:
653 if not comm.outdated_at_version(version) and skip_outdated:
632 inline_comms.append(comm)
654 inline_comms.append(comm)
633
655
634 return inline_comms
656 return inline_comms
635
657
636 def get_outdated_comments(self, repo_id, pull_request):
658 def get_outdated_comments(self, repo_id, pull_request):
637 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
659 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
638 # of a pull request.
660 # of a pull request.
639 q = self._all_inline_comments_of_pull_request(pull_request)
661 q = self._all_inline_comments_of_pull_request(pull_request)
640 q = q.filter(
662 q = q.filter(
641 ChangesetComment.display_state ==
663 ChangesetComment.display_state ==
642 ChangesetComment.COMMENT_OUTDATED
664 ChangesetComment.COMMENT_OUTDATED
643 ).order_by(ChangesetComment.comment_id.asc())
665 ).order_by(ChangesetComment.comment_id.asc())
644
666
645 return self._group_comments_by_path_and_line_number(q)
667 return self._group_comments_by_path_and_line_number(q)
646
668
647 def _get_inline_comments_query(self, repo_id, revision, pull_request):
669 def _get_inline_comments_query(self, repo_id, revision, pull_request):
648 # TODO: johbo: Split this into two methods: One for PR and one for
670 # TODO: johbo: Split this into two methods: One for PR and one for
649 # commit.
671 # commit.
650 if revision:
672 if revision:
651 q = Session().query(ChangesetComment).filter(
673 q = Session().query(ChangesetComment).filter(
652 ChangesetComment.repo_id == repo_id,
674 ChangesetComment.repo_id == repo_id,
653 ChangesetComment.line_no != null(),
675 ChangesetComment.line_no != null(),
654 ChangesetComment.f_path != null(),
676 ChangesetComment.f_path != null(),
655 ChangesetComment.revision == revision)
677 ChangesetComment.revision == revision)
656
678
657 elif pull_request:
679 elif pull_request:
658 pull_request = self.__get_pull_request(pull_request)
680 pull_request = self.__get_pull_request(pull_request)
659 if not CommentsModel.use_outdated_comments(pull_request):
681 if not CommentsModel.use_outdated_comments(pull_request):
660 q = self._visible_inline_comments_of_pull_request(pull_request)
682 q = self._visible_inline_comments_of_pull_request(pull_request)
661 else:
683 else:
662 q = self._all_inline_comments_of_pull_request(pull_request)
684 q = self._all_inline_comments_of_pull_request(pull_request)
663
685
664 else:
686 else:
665 raise Exception('Please specify commit or pull_request_id')
687 raise Exception('Please specify commit or pull_request_id')
666 q = q.order_by(ChangesetComment.comment_id.asc())
688 q = q.order_by(ChangesetComment.comment_id.asc())
667 return q
689 return q
668
690
669 def _group_comments_by_path_and_line_number(self, q):
691 def _group_comments_by_path_and_line_number(self, q):
670 comments = q.all()
692 comments = q.all()
671 paths = collections.defaultdict(lambda: collections.defaultdict(list))
693 paths = collections.defaultdict(lambda: collections.defaultdict(list))
672 for co in comments:
694 for co in comments:
673 paths[co.f_path][co.line_no].append(co)
695 paths[co.f_path][co.line_no].append(co)
674 return paths
696 return paths
675
697
676 @classmethod
698 @classmethod
677 def needed_extra_diff_context(cls):
699 def needed_extra_diff_context(cls):
678 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
700 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
679
701
680 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
702 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
681 if not CommentsModel.use_outdated_comments(pull_request):
703 if not CommentsModel.use_outdated_comments(pull_request):
682 return
704 return
683
705
684 comments = self._visible_inline_comments_of_pull_request(pull_request)
706 comments = self._visible_inline_comments_of_pull_request(pull_request)
685 comments_to_outdate = comments.all()
707 comments_to_outdate = comments.all()
686
708
687 for comment in comments_to_outdate:
709 for comment in comments_to_outdate:
688 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
710 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
689
711
690 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
712 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
691 diff_line = _parse_comment_line_number(comment.line_no)
713 diff_line = _parse_comment_line_number(comment.line_no)
692
714
693 try:
715 try:
694 old_context = old_diff_proc.get_context_of_line(
716 old_context = old_diff_proc.get_context_of_line(
695 path=comment.f_path, diff_line=diff_line)
717 path=comment.f_path, diff_line=diff_line)
696 new_context = new_diff_proc.get_context_of_line(
718 new_context = new_diff_proc.get_context_of_line(
697 path=comment.f_path, diff_line=diff_line)
719 path=comment.f_path, diff_line=diff_line)
698 except (diffs.LineNotInDiffException,
720 except (diffs.LineNotInDiffException,
699 diffs.FileNotInDiffException):
721 diffs.FileNotInDiffException):
700 comment.display_state = ChangesetComment.COMMENT_OUTDATED
722 comment.display_state = ChangesetComment.COMMENT_OUTDATED
701 return
723 return
702
724
703 if old_context == new_context:
725 if old_context == new_context:
704 return
726 return
705
727
706 if self._should_relocate_diff_line(diff_line):
728 if self._should_relocate_diff_line(diff_line):
707 new_diff_lines = new_diff_proc.find_context(
729 new_diff_lines = new_diff_proc.find_context(
708 path=comment.f_path, context=old_context,
730 path=comment.f_path, context=old_context,
709 offset=self.DIFF_CONTEXT_BEFORE)
731 offset=self.DIFF_CONTEXT_BEFORE)
710 if not new_diff_lines:
732 if not new_diff_lines:
711 comment.display_state = ChangesetComment.COMMENT_OUTDATED
733 comment.display_state = ChangesetComment.COMMENT_OUTDATED
712 else:
734 else:
713 new_diff_line = self._choose_closest_diff_line(
735 new_diff_line = self._choose_closest_diff_line(
714 diff_line, new_diff_lines)
736 diff_line, new_diff_lines)
715 comment.line_no = _diff_to_comment_line_number(new_diff_line)
737 comment.line_no = _diff_to_comment_line_number(new_diff_line)
716 else:
738 else:
717 comment.display_state = ChangesetComment.COMMENT_OUTDATED
739 comment.display_state = ChangesetComment.COMMENT_OUTDATED
718
740
719 def _should_relocate_diff_line(self, diff_line):
741 def _should_relocate_diff_line(self, diff_line):
720 """
742 """
721 Checks if relocation shall be tried for the given `diff_line`.
743 Checks if relocation shall be tried for the given `diff_line`.
722
744
723 If a comment points into the first lines, then we can have a situation
745 If a comment points into the first lines, then we can have a situation
724 that after an update another line has been added on top. In this case
746 that after an update another line has been added on top. In this case
725 we would find the context still and move the comment around. This
747 we would find the context still and move the comment around. This
726 would be wrong.
748 would be wrong.
727 """
749 """
728 should_relocate = (
750 should_relocate = (
729 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
751 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
730 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
752 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
731 return should_relocate
753 return should_relocate
732
754
733 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
755 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
734 candidate = new_diff_lines[0]
756 candidate = new_diff_lines[0]
735 best_delta = _diff_line_delta(diff_line, candidate)
757 best_delta = _diff_line_delta(diff_line, candidate)
736 for new_diff_line in new_diff_lines[1:]:
758 for new_diff_line in new_diff_lines[1:]:
737 delta = _diff_line_delta(diff_line, new_diff_line)
759 delta = _diff_line_delta(diff_line, new_diff_line)
738 if delta < best_delta:
760 if delta < best_delta:
739 candidate = new_diff_line
761 candidate = new_diff_line
740 best_delta = delta
762 best_delta = delta
741 return candidate
763 return candidate
742
764
743 def _visible_inline_comments_of_pull_request(self, pull_request):
765 def _visible_inline_comments_of_pull_request(self, pull_request):
744 comments = self._all_inline_comments_of_pull_request(pull_request)
766 comments = self._all_inline_comments_of_pull_request(pull_request)
745 comments = comments.filter(
767 comments = comments.filter(
746 coalesce(ChangesetComment.display_state, '') !=
768 coalesce(ChangesetComment.display_state, '') !=
747 ChangesetComment.COMMENT_OUTDATED)
769 ChangesetComment.COMMENT_OUTDATED)
748 return comments
770 return comments
749
771
750 def _all_inline_comments_of_pull_request(self, pull_request):
772 def _all_inline_comments_of_pull_request(self, pull_request):
751 comments = Session().query(ChangesetComment)\
773 comments = Session().query(ChangesetComment)\
752 .filter(ChangesetComment.line_no != None)\
774 .filter(ChangesetComment.line_no != None)\
753 .filter(ChangesetComment.f_path != None)\
775 .filter(ChangesetComment.f_path != None)\
754 .filter(ChangesetComment.pull_request == pull_request)
776 .filter(ChangesetComment.pull_request == pull_request)
755 return comments
777 return comments
756
778
757 def _all_general_comments_of_pull_request(self, pull_request):
779 def _all_general_comments_of_pull_request(self, pull_request):
758 comments = Session().query(ChangesetComment)\
780 comments = Session().query(ChangesetComment)\
759 .filter(ChangesetComment.line_no == None)\
781 .filter(ChangesetComment.line_no == None)\
760 .filter(ChangesetComment.f_path == None)\
782 .filter(ChangesetComment.f_path == None)\
761 .filter(ChangesetComment.pull_request == pull_request)
783 .filter(ChangesetComment.pull_request == pull_request)
762
784
763 return comments
785 return comments
764
786
765 @staticmethod
787 @staticmethod
766 def use_outdated_comments(pull_request):
788 def use_outdated_comments(pull_request):
767 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
789 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
768 settings = settings_model.get_general_settings()
790 settings = settings_model.get_general_settings()
769 return settings.get('rhodecode_use_outdated_comments', False)
791 return settings.get('rhodecode_use_outdated_comments', False)
770
792
771 def trigger_commit_comment_hook(self, repo, user, action, data=None):
793 def trigger_commit_comment_hook(self, repo, user, action, data=None):
772 repo = self._get_repo(repo)
794 repo = self._get_repo(repo)
773 target_scm = repo.scm_instance()
795 target_scm = repo.scm_instance()
774 if action == 'create':
796 if action == 'create':
775 trigger_hook = hooks_utils.trigger_comment_commit_hooks
797 trigger_hook = hooks_utils.trigger_comment_commit_hooks
776 elif action == 'edit':
798 elif action == 'edit':
777 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
799 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
778 else:
800 else:
779 return
801 return
780
802
781 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
803 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
782 repo, action, trigger_hook)
804 repo, action, trigger_hook)
783 trigger_hook(
805 trigger_hook(
784 username=user.username,
806 username=user.username,
785 repo_name=repo.repo_name,
807 repo_name=repo.repo_name,
786 repo_type=target_scm.alias,
808 repo_type=target_scm.alias,
787 repo=repo,
809 repo=repo,
788 data=data)
810 data=data)
789
811
790
812
791 def _parse_comment_line_number(line_no):
813 def _parse_comment_line_number(line_no):
792 """
814 """
793 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
815 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
794 """
816 """
795 old_line = None
817 old_line = None
796 new_line = None
818 new_line = None
797 if line_no.startswith('o'):
819 if line_no.startswith('o'):
798 old_line = int(line_no[1:])
820 old_line = int(line_no[1:])
799 elif line_no.startswith('n'):
821 elif line_no.startswith('n'):
800 new_line = int(line_no[1:])
822 new_line = int(line_no[1:])
801 else:
823 else:
802 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
824 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
803 return diffs.DiffLineNumber(old_line, new_line)
825 return diffs.DiffLineNumber(old_line, new_line)
804
826
805
827
806 def _diff_to_comment_line_number(diff_line):
828 def _diff_to_comment_line_number(diff_line):
807 if diff_line.new is not None:
829 if diff_line.new is not None:
808 return u'n{}'.format(diff_line.new)
830 return u'n{}'.format(diff_line.new)
809 elif diff_line.old is not None:
831 elif diff_line.old is not None:
810 return u'o{}'.format(diff_line.old)
832 return u'o{}'.format(diff_line.old)
811 return u''
833 return u''
812
834
813
835
814 def _diff_line_delta(a, b):
836 def _diff_line_delta(a, b):
815 if None not in (a.new, b.new):
837 if None not in (a.new, b.new):
816 return abs(a.new - b.new)
838 return abs(a.new - b.new)
817 elif None not in (a.old, b.old):
839 elif None not in (a.old, b.old):
818 return abs(a.old - b.old)
840 return abs(a.old - b.old)
819 else:
841 else:
820 raise ValueError(
842 raise ValueError(
821 "Cannot compute delta between {} and {}".format(a, b))
843 "Cannot compute delta between {} and {}".format(a, b))
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,74 +1,74 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2017-2020 RhodeCode GmbH
3 # Copyright (C) 2017-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
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 os
21 import os
22
22
23 import colander
23 import colander
24
24
25 from rhodecode.translation import _
25 from rhodecode.translation import _
26 from rhodecode.model.validation_schema import preparers
26 from rhodecode.model.validation_schema import preparers
27 from rhodecode.model.validation_schema import types
27 from rhodecode.model.validation_schema import types
28
28
29
29
30 @colander.deferred
30 @colander.deferred
31 def deferred_lifetime_validator(node, kw):
31 def deferred_lifetime_validator(node, kw):
32 options = kw.get('lifetime_options', [])
32 options = kw.get('lifetime_options', [])
33 return colander.All(
33 return colander.All(
34 colander.Range(min=-1, max=60 * 24 * 30 * 12),
34 colander.Range(min=-1, max=60 * 24 * 30 * 12),
35 colander.OneOf([x for x in options]))
35 colander.OneOf([x for x in options]))
36
36
37
37
38 def unique_gist_validator(node, value):
38 def unique_gist_validator(node, value):
39 from rhodecode.model.db import Gist
39 from rhodecode.model.db import Gist
40 existing = Gist.get_by_access_id(value)
40 existing = Gist.get_by_access_id(value)
41 if existing:
41 if existing:
42 msg = _(u'Gist with name {} already exists').format(value)
42 msg = _(u'Gist with name {} already exists').format(value)
43 raise colander.Invalid(node, msg)
43 raise colander.Invalid(node, msg)
44
44
45
45
46 def filename_validator(node, value):
46 def filename_validator(node, value):
47 if value != os.path.basename(value):
47 if value != os.path.basename(value):
48 msg = _(u'Filename {} cannot be inside a directory').format(value)
48 msg = _(u'Filename {} cannot be inside a directory').format(value)
49 raise colander.Invalid(node, msg)
49 raise colander.Invalid(node, msg)
50
50
51
51
52 comment_types = ['note', 'todo']
52 comment_types = ['note', 'todo']
53
53
54
54
55 class CommentSchema(colander.MappingSchema):
55 class CommentSchema(colander.MappingSchema):
56 from rhodecode.model.db import ChangesetComment, ChangesetStatus
56 from rhodecode.model.db import ChangesetComment, ChangesetStatus
57
57
58 comment_body = colander.SchemaNode(colander.String())
58 comment_body = colander.SchemaNode(colander.String())
59 comment_type = colander.SchemaNode(
59 comment_type = colander.SchemaNode(
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(
67 colander.String(), missing=None,
67 colander.String(), missing=None,
68 validator=colander.OneOf([x[0] for x in ChangesetStatus.STATUSES]))
68 validator=colander.OneOf([x[0] for x in ChangesetStatus.STATUSES]))
69 renderer_type = colander.SchemaNode(colander.String())
69 renderer_type = colander.SchemaNode(colander.String())
70
70
71 resolves_comment_id = colander.SchemaNode(colander.Integer(), missing=None)
71 resolves_comment_id = colander.SchemaNode(colander.Integer(), missing=None)
72
72
73 user = colander.SchemaNode(types.StrOrIntType())
73 user = colander.SchemaNode(types.StrOrIntType())
74 repo = colander.SchemaNode(types.StrOrIntType())
74 repo = colander.SchemaNode(types.StrOrIntType())
@@ -1,540 +1,600 b''
1
1
2
2
3 //BUTTONS
3 //BUTTONS
4 button,
4 button,
5 .btn,
5 .btn,
6 input[type="button"] {
6 input[type="button"] {
7 -webkit-appearance: none;
7 -webkit-appearance: none;
8 display: inline-block;
8 display: inline-block;
9 margin: 0 @padding/3 0 0;
9 margin: 0 @padding/3 0 0;
10 padding: @button-padding;
10 padding: @button-padding;
11 text-align: center;
11 text-align: center;
12 font-size: @basefontsize;
12 font-size: @basefontsize;
13 line-height: 1em;
13 line-height: 1em;
14 font-family: @text-light;
14 font-family: @text-light;
15 text-decoration: none;
15 text-decoration: none;
16 text-shadow: none;
16 text-shadow: none;
17 color: @grey2;
17 color: @grey2;
18 background-color: white;
18 background-color: white;
19 background-image: none;
19 background-image: none;
20 border: none;
20 border: none;
21 .border ( @border-thickness-buttons, @grey5 );
21 .border ( @border-thickness-buttons, @grey5 );
22 .border-radius (@border-radius);
22 .border-radius (@border-radius);
23 cursor: pointer;
23 cursor: pointer;
24 white-space: nowrap;
24 white-space: nowrap;
25 -webkit-transition: background .3s,color .3s;
25 -webkit-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
29 box-shadow: @button-shadow;
29 box-shadow: @button-shadow;
30 -webkit-box-shadow: @button-shadow;
30 -webkit-box-shadow: @button-shadow;
31
31
32
32
33
33
34 a {
34 a {
35 display: block;
35 display: block;
36 margin: 0;
36 margin: 0;
37 padding: 0;
37 padding: 0;
38 color: inherit;
38 color: inherit;
39 text-decoration: none;
39 text-decoration: none;
40
40
41 &:hover {
41 &:hover {
42 text-decoration: none;
42 text-decoration: none;
43 }
43 }
44 }
44 }
45
45
46 &:focus,
46 &:focus,
47 &:active {
47 &:active {
48 outline:none;
48 outline:none;
49 }
49 }
50
50
51 &:hover {
51 &:hover {
52 color: @rcdarkblue;
52 color: @rcdarkblue;
53 background-color: @grey6;
53 background-color: @grey6;
54
54
55 }
55 }
56
56
57 &.btn-active {
57 &.btn-active {
58 color: @rcdarkblue;
58 color: @rcdarkblue;
59 background-color: @grey6;
59 background-color: @grey6;
60 }
60 }
61
61
62 .icon-remove {
62 .icon-remove {
63 display: none;
63 display: none;
64 }
64 }
65
65
66 //disabled buttons
66 //disabled buttons
67 //last; overrides any other styles
67 //last; overrides any other styles
68 &:disabled {
68 &:disabled {
69 opacity: .7;
69 opacity: .7;
70 cursor: auto;
70 cursor: auto;
71 background-color: white;
71 background-color: white;
72 color: @grey4;
72 color: @grey4;
73 text-shadow: none;
73 text-shadow: none;
74 }
74 }
75
75
76 &.no-margin {
76 &.no-margin {
77 margin: 0 0 0 0;
77 margin: 0 0 0 0;
78 }
78 }
79
79
80
80
81
81
82 }
82 }
83
83
84
84
85 .btn-default {
85 .btn-default {
86 border: @border-thickness solid @grey5;
86 border: @border-thickness solid @grey5;
87 background-image: none;
87 background-image: none;
88 color: @grey2;
88 color: @grey2;
89
89
90 a {
90 a {
91 color: @grey2;
91 color: @grey2;
92 }
92 }
93
93
94 &:hover,
94 &:hover,
95 &.active {
95 &.active {
96 color: @rcdarkblue;
96 color: @rcdarkblue;
97 background-color: @white;
97 background-color: @white;
98 .border ( @border-thickness, @grey4 );
98 .border ( @border-thickness, @grey4 );
99
99
100 a {
100 a {
101 color: @grey2;
101 color: @grey2;
102 }
102 }
103 }
103 }
104 &:disabled {
104 &:disabled {
105 .border ( @border-thickness-buttons, @grey5 );
105 .border ( @border-thickness-buttons, @grey5 );
106 background-color: transparent;
106 background-color: transparent;
107 }
107 }
108 &.btn-active {
108 &.btn-active {
109 color: @rcdarkblue;
109 color: @rcdarkblue;
110 background-color: @white;
110 background-color: @white;
111 .border ( @border-thickness, @rcdarkblue );
111 .border ( @border-thickness, @rcdarkblue );
112 }
112 }
113 }
113 }
114
114
115 .btn-primary,
115 .btn-primary,
116 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
116 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
117 .btn-success {
117 .btn-success {
118 .border ( @border-thickness, @rcblue );
118 .border ( @border-thickness, @rcblue );
119 background-color: @rcblue;
119 background-color: @rcblue;
120 color: white;
120 color: white;
121
121
122 a {
122 a {
123 color: white;
123 color: white;
124 }
124 }
125
125
126 &:hover,
126 &:hover,
127 &.active {
127 &.active {
128 .border ( @border-thickness, @rcdarkblue );
128 .border ( @border-thickness, @rcdarkblue );
129 color: white;
129 color: white;
130 background-color: @rcdarkblue;
130 background-color: @rcdarkblue;
131
131
132 a {
132 a {
133 color: white;
133 color: white;
134 }
134 }
135 }
135 }
136 &:disabled {
136 &:disabled {
137 background-color: @rcblue;
137 background-color: @rcblue;
138 }
138 }
139 }
139 }
140
140
141 .btn-secondary {
141 .btn-secondary {
142 &:extend(.btn-default);
142 &:extend(.btn-default);
143
143
144 background-color: white;
144 background-color: white;
145
145
146 &:focus {
146 &:focus {
147 outline: 0;
147 outline: 0;
148 }
148 }
149
149
150 &:hover {
150 &:hover {
151 &:extend(.btn-default:hover);
151 &:extend(.btn-default:hover);
152 }
152 }
153
153
154 &.btn-link {
154 &.btn-link {
155 &:extend(.btn-link);
155 &:extend(.btn-link);
156 color: @rcblue;
156 color: @rcblue;
157 }
157 }
158
158
159 &:disabled {
159 &:disabled {
160 color: @rcblue;
160 color: @rcblue;
161 background-color: white;
161 background-color: white;
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,
169 .form .action_button.btn-x {
168 .form .action_button.btn-x {
170 .border ( @border-thickness, @alert2 );
169 .border ( @border-thickness, @alert2 );
171 background-color: white;
170 background-color: white;
172 color: @alert2;
171 color: @alert2;
173
172
174 a {
173 a {
175 color: @alert2;
174 color: @alert2;
176 }
175 }
177
176
178 &:hover,
177 &:hover,
179 &.active {
178 &.active {
180 .border ( @border-thickness, @alert2 );
179 .border ( @border-thickness, @alert2 );
181 color: white;
180 color: white;
182 background-color: @alert2;
181 background-color: @alert2;
183
182
184 a {
183 a {
185 color: white;
184 color: white;
186 }
185 }
187 }
186 }
188
187
189 i {
188 i {
190 display:none;
189 display:none;
191 }
190 }
192
191
193 &:disabled {
192 &:disabled {
194 background-color: white;
193 background-color: white;
195 color: @alert2;
194 color: @alert2;
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;
202 color: @alert1;
231 color: @alert1;
203
232
204 }
233 }
205
234
206 .btn-rejected-status {
235 .btn-rejected-status {
207 .border ( @border-thickness, @alert2 );
236 .border ( @border-thickness, @alert2 );
208 background-color: white;
237 background-color: white;
209 color: @alert2;
238 color: @alert2;
210 }
239 }
211
240
212 .btn-sm,
241 .btn-sm,
213 .btn-mini,
242 .btn-mini,
214 .field-sm .btn {
243 .field-sm .btn {
215 padding: @padding/3;
244 padding: @padding/3;
216 }
245 }
217
246
218 .btn-xs {
247 .btn-xs {
219 padding: @padding/4;
248 padding: @padding/4;
220 }
249 }
221
250
222 .btn-lg {
251 .btn-lg {
223 padding: @padding * 1.2;
252 padding: @padding * 1.2;
224 }
253 }
225
254
226 .btn-group {
255 .btn-group {
227 display: inline-block;
256 display: inline-block;
228 .btn {
257 .btn {
229 float: left;
258 float: left;
230 margin: 0 0 0 0;
259 margin: 0 0 0 0;
231 // first item
260 // first item
232 &:first-of-type:not(:last-of-type) {
261 &:first-of-type:not(:last-of-type) {
233 border-radius: @border-radius 0 0 @border-radius;
262 border-radius: @border-radius 0 0 @border-radius;
234
263
235 }
264 }
236 // middle elements
265 // middle elements
237 &:not(:first-of-type):not(:last-of-type) {
266 &:not(:first-of-type):not(:last-of-type) {
238 border-radius: 0;
267 border-radius: 0;
239 border-left-width: 0;
268 border-left-width: 0;
240 border-right-width: 0;
269 border-right-width: 0;
241 }
270 }
242 // last item
271 // last item
243 &:last-of-type:not(:first-of-type) {
272 &:last-of-type:not(:first-of-type) {
244 border-radius: 0 @border-radius @border-radius 0;
273 border-radius: 0 @border-radius @border-radius 0;
245 }
274 }
246
275
247 &:only-child {
276 &:only-child {
248 border-radius: @border-radius;
277 border-radius: @border-radius;
249 }
278 }
250 }
279 }
251
280
252 }
281 }
253
282
254
283
255 .btn-group-actions {
284 .btn-group-actions {
256 position: relative;
285 position: relative;
257 z-index: 50;
286 z-index: 50;
258
287
259 &:not(.open) .btn-action-switcher-container {
288 &:not(.open) .btn-action-switcher-container {
260 display: none;
289 display: none;
261 }
290 }
262
291
263 .btn-more-option {
292 .btn-more-option {
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 }
296 }
268 }
297 }
269
298
270
299
271 .btn-action-switcher-container {
300 .btn-action-switcher-container {
272 position: absolute;
301 position: absolute;
273 top: 100%;
302 top: 100%;
274
303
275 &.left-align {
304 &.left-align {
276 left: 0;
305 left: 0;
277 }
306 }
278 &.right-align {
307 &.right-align {
279 right: 0;
308 right: 0;
280 }
309 }
281
310
282 }
311 }
283
312
284 .btn-action-switcher {
313 .btn-action-switcher {
285 display: block;
314 display: block;
286 position: relative;
315 position: relative;
287 z-index: 300;
316 z-index: 300;
288 max-width: 600px;
317 max-width: 600px;
289 margin-top: 4px;
318 margin-top: 4px;
290 margin-bottom: 24px;
319 margin-bottom: 24px;
291 font-size: 14px;
320 font-size: 14px;
292 font-weight: 400;
321 font-weight: 400;
293 padding: 8px 0;
322 padding: 8px 0;
294 background-color: #fff;
323 background-color: #fff;
295 border: 1px solid @grey4;
324 border: 1px solid @grey4;
296 border-radius: 3px;
325 border-radius: 3px;
297 box-shadow: @dropdown-shadow;
326 box-shadow: @dropdown-shadow;
298 overflow: auto;
327 overflow: auto;
299
328
300 li {
329 li {
301 display: block;
330 display: block;
302 text-align: left;
331 text-align: left;
303 list-style: none;
332 list-style: none;
304 padding: 5px 10px;
333 padding: 5px 10px;
305 }
334 }
306
335
307 li .action-help-block {
336 li .action-help-block {
308 font-size: 10px;
337 font-size: 10px;
309 line-height: normal;
338 line-height: normal;
310 color: @grey4;
339 color: @grey4;
311 }
340 }
312
341
313 }
342 }
314
343
315 .btn-link {
344 .btn-link {
316 background: transparent;
345 background: transparent;
317 border: none;
346 border: none;
318 padding: 0;
347 padding: 0;
319 color: @rcblue;
348 color: @rcblue;
320
349
321 &:hover {
350 &:hover {
322 background: transparent;
351 background: transparent;
323 border: none;
352 border: none;
324 color: @rcdarkblue;
353 color: @rcdarkblue;
325 }
354 }
326
355
327 //disabled buttons
356 //disabled buttons
328 //last; overrides any other styles
357 //last; overrides any other styles
329 &:disabled {
358 &:disabled {
330 opacity: .7;
359 opacity: .7;
331 cursor: auto;
360 cursor: auto;
332 background-color: white;
361 background-color: white;
333 color: @grey4;
362 color: @grey4;
334 text-shadow: none;
363 text-shadow: none;
335 }
364 }
336
365
337 // TODO: johbo: Check if we can avoid this, indicates that the structure
366 // TODO: johbo: Check if we can avoid this, indicates that the structure
338 // is not yet good.
367 // is not yet good.
339 // lisa: The button CSS reflects the button HTML; both need a cleanup.
368 // lisa: The button CSS reflects the button HTML; both need a cleanup.
340 &.btn-danger {
369 &.btn-danger {
341 color: @alert2;
370 color: @alert2;
342
371
343 &:hover {
372 &:hover {
344 color: darken(@alert2,30%);
373 color: darken(@alert2,30%);
345 }
374 }
346
375
347 &:disabled {
376 &:disabled {
348 color: @alert2;
377 color: @alert2;
349 }
378 }
350 }
379 }
351 }
380 }
352
381
353 .btn-social {
382 .btn-social {
354 &:extend(.btn-default);
383 &:extend(.btn-default);
355 margin: 5px 5px 5px 0px;
384 margin: 5px 5px 5px 0px;
356 min-width: 160px;
385 min-width: 160px;
357 }
386 }
358
387
359 // TODO: johbo: check these exceptions
388 // TODO: johbo: check these exceptions
360
389
361 .links {
390 .links {
362
391
363 .btn + .btn {
392 .btn + .btn {
364 margin-top: @padding;
393 margin-top: @padding;
365 }
394 }
366 }
395 }
367
396
368
397
369 .action_button {
398 .action_button {
370 display:inline;
399 display:inline;
371 margin: 0;
400 margin: 0;
372 padding: 0 1em 0 0;
401 padding: 0 1em 0 0;
373 font-size: inherit;
402 font-size: inherit;
374 color: @rcblue;
403 color: @rcblue;
375 border: none;
404 border: none;
376 border-radius: 0;
405 border-radius: 0;
377 background-color: transparent;
406 background-color: transparent;
378
407
379 &.last-item {
408 &.last-item {
380 border: none;
409 border: none;
381 padding: 0 0 0 0;
410 padding: 0 0 0 0;
382 }
411 }
383
412
384 &:last-child {
413 &:last-child {
385 border: none;
414 border: none;
386 padding: 0 0 0 0;
415 padding: 0 0 0 0;
387 }
416 }
388
417
389 &:hover {
418 &:hover {
390 color: @rcdarkblue;
419 color: @rcdarkblue;
391 background-color: transparent;
420 background-color: transparent;
392 border: none;
421 border: none;
393 }
422 }
394 .noselect
423 .noselect
395 }
424 }
396
425
397 .grid_delete {
426 .grid_delete {
398 .action_button {
427 .action_button {
399 border: none;
428 border: none;
400 }
429 }
401 }
430 }
402
431
403
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
404 // 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
405 input[type="submit"] {
465 input[type="submit"] {
406 &:extend(.btn-primary);
466 &:extend(.btn-primary);
407
467
408 &:focus {
468 &:focus {
409 outline: 0;
469 outline: 0;
410 }
470 }
411
471
412 &:hover {
472 &:hover {
413 &:extend(.btn-primary:hover);
473 &:extend(.btn-primary:hover);
414 }
474 }
415
475
416 &.btn-link {
476 &.btn-link {
417 &:extend(.btn-link);
477 &:extend(.btn-link);
418 color: @rcblue;
478 color: @rcblue;
419
479
420 &:disabled {
480 &:disabled {
421 color: @rcblue;
481 color: @rcblue;
422 background-color: transparent;
482 background-color: transparent;
423 }
483 }
424 }
484 }
425
485
426 &:disabled {
486 &:disabled {
427 .border ( @border-thickness-buttons, @rcblue );
487 .border ( @border-thickness-buttons, @rcblue );
428 background-color: @rcblue;
488 background-color: @rcblue;
429 color: white;
489 color: white;
430 opacity: 0.5;
490 opacity: 0.5;
431 }
491 }
432 }
492 }
433
493
434 input[type="reset"] {
494 input[type="reset"] {
435 &:extend(.btn-default);
495 &:extend(.btn-default);
436
496
437 // TODO: johbo: Check if this tweak can be avoided.
497 // TODO: johbo: Check if this tweak can be avoided.
438 background: transparent;
498 background: transparent;
439
499
440 &:focus {
500 &:focus {
441 outline: 0;
501 outline: 0;
442 }
502 }
443
503
444 &:hover {
504 &:hover {
445 &:extend(.btn-default:hover);
505 &:extend(.btn-default:hover);
446 }
506 }
447
507
448 &.btn-link {
508 &.btn-link {
449 &:extend(.btn-link);
509 &:extend(.btn-link);
450 color: @rcblue;
510 color: @rcblue;
451
511
452 &:disabled {
512 &:disabled {
453 border: none;
513 border: none;
454 }
514 }
455 }
515 }
456
516
457 &:disabled {
517 &:disabled {
458 .border ( @border-thickness-buttons, @rcblue );
518 .border ( @border-thickness-buttons, @rcblue );
459 background-color: white;
519 background-color: white;
460 color: @rcblue;
520 color: @rcblue;
461 }
521 }
462 }
522 }
463
523
464 input[type="submit"],
524 input[type="submit"],
465 input[type="reset"] {
525 input[type="reset"] {
466 &.btn-danger {
526 &.btn-danger {
467 &:extend(.btn-danger);
527 &:extend(.btn-danger);
468
528
469 &:focus {
529 &:focus {
470 outline: 0;
530 outline: 0;
471 }
531 }
472
532
473 &:hover {
533 &:hover {
474 &:extend(.btn-danger:hover);
534 &:extend(.btn-danger:hover);
475 }
535 }
476
536
477 &.btn-link {
537 &.btn-link {
478 &:extend(.btn-link);
538 &:extend(.btn-link);
479 color: @alert2;
539 color: @alert2;
480
540
481 &:hover {
541 &:hover {
482 color: darken(@alert2,30%);
542 color: darken(@alert2,30%);
483 }
543 }
484 }
544 }
485
545
486 &:disabled {
546 &:disabled {
487 color: @alert2;
547 color: @alert2;
488 background-color: white;
548 background-color: white;
489 }
549 }
490 }
550 }
491 &.btn-danger-action {
551 &.btn-danger-action {
492 .border ( @border-thickness, @alert2 );
552 .border ( @border-thickness, @alert2 );
493 background-color: @alert2;
553 background-color: @alert2;
494 color: white;
554 color: white;
495
555
496 a {
556 a {
497 color: white;
557 color: white;
498 }
558 }
499
559
500 &:hover {
560 &:hover {
501 background-color: darken(@alert2,20%);
561 background-color: darken(@alert2,20%);
502 }
562 }
503
563
504 &.active {
564 &.active {
505 .border ( @border-thickness, @alert2 );
565 .border ( @border-thickness, @alert2 );
506 color: white;
566 color: white;
507 background-color: @alert2;
567 background-color: @alert2;
508
568
509 a {
569 a {
510 color: white;
570 color: white;
511 }
571 }
512 }
572 }
513
573
514 &:disabled {
574 &:disabled {
515 background-color: white;
575 background-color: white;
516 color: @alert2;
576 color: @alert2;
517 }
577 }
518 }
578 }
519 }
579 }
520
580
521
581
522 .button-links {
582 .button-links {
523 float: left;
583 float: left;
524 display: inline;
584 display: inline;
525 margin: 0;
585 margin: 0;
526 padding-left: 0;
586 padding-left: 0;
527 list-style: none;
587 list-style: none;
528 text-align: right;
588 text-align: right;
529
589
530 li {
590 li {
531
591
532
592
533 }
593 }
534
594
535 li.active {
595 li.active {
536 background-color: @grey6;
596 background-color: @grey6;
537 .border ( @border-thickness, @grey4 );
597 .border ( @border-thickness, @grey4 );
538 }
598 }
539
599
540 }
600 }
@@ -1,635 +1,642 b''
1 // comments.less
1 // comments.less
2 // For use in RhodeCode applications;
2 // For use in RhodeCode applications;
3 // see style guide documentation for guidelines.
3 // see style guide documentation for guidelines.
4
4
5
5
6 // Comments
6 // Comments
7 @comment-outdated-opacity: 0.6;
7 @comment-outdated-opacity: 0.6;
8
8
9 .comments {
9 .comments {
10 width: 100%;
10 width: 100%;
11 }
11 }
12
12
13 .comments-heading {
13 .comments-heading {
14 margin-bottom: -1px;
14 margin-bottom: -1px;
15 background: @grey6;
15 background: @grey6;
16 display: block;
16 display: block;
17 padding: 10px 0px;
17 padding: 10px 0px;
18 font-size: 18px
18 font-size: 18px
19 }
19 }
20
20
21 #comment-tr-show {
21 #comment-tr-show {
22 padding: 5px 0;
22 padding: 5px 0;
23 }
23 }
24
24
25 tr.inline-comments div {
25 tr.inline-comments div {
26 max-width: 100%;
26 max-width: 100%;
27
27
28 p {
28 p {
29 white-space: normal;
29 white-space: normal;
30 }
30 }
31
31
32 code, pre, .code, dd {
32 code, pre, .code, dd {
33 overflow-x: auto;
33 overflow-x: auto;
34 width: 1062px;
34 width: 1062px;
35 }
35 }
36
36
37 dd {
37 dd {
38 width: auto;
38 width: auto;
39 }
39 }
40 }
40 }
41
41
42 #injected_page_comments {
42 #injected_page_comments {
43 .comment-previous-link,
43 .comment-previous-link,
44 .comment-next-link,
44 .comment-next-link,
45 .comment-links-divider {
45 .comment-links-divider {
46 display: none;
46 display: none;
47 }
47 }
48 }
48 }
49
49
50 .add-comment {
50 .add-comment {
51 margin-bottom: 10px;
51 margin-bottom: 10px;
52 }
52 }
53 .hide-comment-button .add-comment {
53 .hide-comment-button .add-comment {
54 display: none;
54 display: none;
55 }
55 }
56
56
57 .comment-bubble {
57 .comment-bubble {
58 color: @grey4;
58 color: @grey4;
59 margin-top: 4px;
59 margin-top: 4px;
60 margin-right: 30px;
60 margin-right: 30px;
61 visibility: hidden;
61 visibility: hidden;
62 }
62 }
63
63
64 .comment-draft {
65 float: left;
66 margin-right: 10px;
67 font-weight: 600;
68 color: @alert3;
69 }
70
64 .comment-label {
71 .comment-label {
65 float: left;
72 float: left;
66
73
67 padding: 0.4em 0.4em;
74 padding: 0.4em 0.4em;
68 margin: 2px 4px 0px 0px;
75 margin: 2px 4px 0px 0px;
69 display: inline-block;
76 display: inline-block;
70 min-height: 0;
77 min-height: 0;
71
78
72 text-align: center;
79 text-align: center;
73 font-size: 10px;
80 font-size: 10px;
74 line-height: .8em;
81 line-height: .8em;
75
82
76 font-family: @text-italic;
83 font-family: @text-italic;
77 font-style: italic;
84 font-style: italic;
78 background: #fff none;
85 background: #fff none;
79 color: @grey3;
86 color: @grey3;
80 border: 1px solid @grey4;
87 border: 1px solid @grey4;
81 white-space: nowrap;
88 white-space: nowrap;
82
89
83 text-transform: uppercase;
90 text-transform: uppercase;
84 min-width: 50px;
91 min-width: 50px;
85 border-radius: 4px;
92 border-radius: 4px;
86
93
87 &.todo {
94 &.todo {
88 color: @color5;
95 color: @color5;
89 font-style: italic;
96 font-style: italic;
90 font-weight: @text-bold-italic-weight;
97 font-weight: @text-bold-italic-weight;
91 font-family: @text-bold-italic;
98 font-family: @text-bold-italic;
92 }
99 }
93
100
94 .resolve {
101 .resolve {
95 cursor: pointer;
102 cursor: pointer;
96 text-decoration: underline;
103 text-decoration: underline;
97 }
104 }
98
105
99 .resolved {
106 .resolved {
100 text-decoration: line-through;
107 text-decoration: line-through;
101 color: @color1;
108 color: @color1;
102 }
109 }
103 .resolved a {
110 .resolved a {
104 text-decoration: line-through;
111 text-decoration: line-through;
105 color: @color1;
112 color: @color1;
106 }
113 }
107 .resolve-text {
114 .resolve-text {
108 color: @color1;
115 color: @color1;
109 margin: 2px 8px;
116 margin: 2px 8px;
110 font-family: @text-italic;
117 font-family: @text-italic;
111 font-style: italic;
118 font-style: italic;
112 }
119 }
113 }
120 }
114
121
115 .has-spacer-after {
122 .has-spacer-after {
116 &:after {
123 &:after {
117 content: ' | ';
124 content: ' | ';
118 color: @grey5;
125 color: @grey5;
119 }
126 }
120 }
127 }
121
128
122 .has-spacer-before {
129 .has-spacer-before {
123 &:before {
130 &:before {
124 content: ' | ';
131 content: ' | ';
125 color: @grey5;
132 color: @grey5;
126 }
133 }
127 }
134 }
128
135
129 .comment {
136 .comment {
130
137
131 &.comment-general {
138 &.comment-general {
132 border: 1px solid @grey5;
139 border: 1px solid @grey5;
133 padding: 5px 5px 5px 5px;
140 padding: 5px 5px 5px 5px;
134 }
141 }
135
142
136 margin: @padding 0;
143 margin: @padding 0;
137 padding: 4px 0 0 0;
144 padding: 4px 0 0 0;
138 line-height: 1em;
145 line-height: 1em;
139
146
140 .rc-user {
147 .rc-user {
141 min-width: 0;
148 min-width: 0;
142 margin: 0px .5em 0 0;
149 margin: 0px .5em 0 0;
143
150
144 .user {
151 .user {
145 display: inline;
152 display: inline;
146 }
153 }
147 }
154 }
148
155
149 .meta {
156 .meta {
150 position: relative;
157 position: relative;
151 width: 100%;
158 width: 100%;
152 border-bottom: 1px solid @grey5;
159 border-bottom: 1px solid @grey5;
153 margin: -5px 0px;
160 margin: -5px 0px;
154 line-height: 24px;
161 line-height: 24px;
155
162
156 &:hover .permalink {
163 &:hover .permalink {
157 visibility: visible;
164 visibility: visible;
158 color: @rcblue;
165 color: @rcblue;
159 }
166 }
160 }
167 }
161
168
162 .author,
169 .author,
163 .date {
170 .date {
164 display: inline;
171 display: inline;
165
172
166 &:after {
173 &:after {
167 content: ' | ';
174 content: ' | ';
168 color: @grey5;
175 color: @grey5;
169 }
176 }
170 }
177 }
171
178
172 .author-general img {
179 .author-general img {
173 top: 3px;
180 top: 3px;
174 }
181 }
175 .author-inline img {
182 .author-inline img {
176 top: 3px;
183 top: 3px;
177 }
184 }
178
185
179 .status-change,
186 .status-change,
180 .permalink,
187 .permalink,
181 .changeset-status-lbl {
188 .changeset-status-lbl {
182 display: inline;
189 display: inline;
183 }
190 }
184
191
185 .permalink {
192 .permalink {
186 visibility: hidden;
193 visibility: hidden;
187 }
194 }
188
195
189 .comment-links-divider {
196 .comment-links-divider {
190 display: inline;
197 display: inline;
191 }
198 }
192
199
193 .comment-links-block {
200 .comment-links-block {
194 float:right;
201 float:right;
195 text-align: right;
202 text-align: right;
196 min-width: 85px;
203 min-width: 85px;
197
204
198 [class^="icon-"]:before,
205 [class^="icon-"]:before,
199 [class*=" icon-"]:before {
206 [class*=" icon-"]:before {
200 margin-left: 0;
207 margin-left: 0;
201 margin-right: 0;
208 margin-right: 0;
202 }
209 }
203 }
210 }
204
211
205 .comment-previous-link {
212 .comment-previous-link {
206 display: inline-block;
213 display: inline-block;
207
214
208 .arrow_comment_link{
215 .arrow_comment_link{
209 cursor: pointer;
216 cursor: pointer;
210 i {
217 i {
211 font-size:10px;
218 font-size:10px;
212 }
219 }
213 }
220 }
214 .arrow_comment_link.disabled {
221 .arrow_comment_link.disabled {
215 cursor: default;
222 cursor: default;
216 color: @grey5;
223 color: @grey5;
217 }
224 }
218 }
225 }
219
226
220 .comment-next-link {
227 .comment-next-link {
221 display: inline-block;
228 display: inline-block;
222
229
223 .arrow_comment_link{
230 .arrow_comment_link{
224 cursor: pointer;
231 cursor: pointer;
225 i {
232 i {
226 font-size:10px;
233 font-size:10px;
227 }
234 }
228 }
235 }
229 .arrow_comment_link.disabled {
236 .arrow_comment_link.disabled {
230 cursor: default;
237 cursor: default;
231 color: @grey5;
238 color: @grey5;
232 }
239 }
233 }
240 }
234
241
235 .delete-comment {
242 .delete-comment {
236 display: inline-block;
243 display: inline-block;
237 color: @rcblue;
244 color: @rcblue;
238
245
239 &:hover {
246 &:hover {
240 cursor: pointer;
247 cursor: pointer;
241 }
248 }
242 }
249 }
243
250
244 .text {
251 .text {
245 clear: both;
252 clear: both;
246 .border-radius(@border-radius);
253 .border-radius(@border-radius);
247 .box-sizing(border-box);
254 .box-sizing(border-box);
248
255
249 .markdown-block p,
256 .markdown-block p,
250 .rst-block p {
257 .rst-block p {
251 margin: .5em 0 !important;
258 margin: .5em 0 !important;
252 // TODO: lisa: This is needed because of other rst !important rules :[
259 // TODO: lisa: This is needed because of other rst !important rules :[
253 }
260 }
254 }
261 }
255
262
256 .pr-version {
263 .pr-version {
257 display: inline-block;
264 display: inline-block;
258 }
265 }
259 .pr-version-inline {
266 .pr-version-inline {
260 display: inline-block;
267 display: inline-block;
261 }
268 }
262 .pr-version-num {
269 .pr-version-num {
263 font-size: 10px;
270 font-size: 10px;
264 }
271 }
265 }
272 }
266
273
267 @comment-padding: 5px;
274 @comment-padding: 5px;
268
275
269 .general-comments {
276 .general-comments {
270 .comment-outdated {
277 .comment-outdated {
271 opacity: @comment-outdated-opacity;
278 opacity: @comment-outdated-opacity;
272 }
279 }
273 }
280 }
274
281
275 .inline-comments {
282 .inline-comments {
276 border-radius: @border-radius;
283 border-radius: @border-radius;
277 .comment {
284 .comment {
278 margin: 0;
285 margin: 0;
279 border-radius: @border-radius;
286 border-radius: @border-radius;
280 }
287 }
281 .comment-outdated {
288 .comment-outdated {
282 opacity: @comment-outdated-opacity;
289 opacity: @comment-outdated-opacity;
283 }
290 }
284
291
285 .comment-inline {
292 .comment-inline {
286 background: white;
293 background: white;
287 padding: @comment-padding @comment-padding;
294 padding: @comment-padding @comment-padding;
288 border: @comment-padding solid @grey6;
295 border: @comment-padding solid @grey6;
289
296
290 .text {
297 .text {
291 border: none;
298 border: none;
292 }
299 }
293 .meta {
300 .meta {
294 border-bottom: 1px solid @grey6;
301 border-bottom: 1px solid @grey6;
295 margin: -5px 0px;
302 margin: -5px 0px;
296 line-height: 24px;
303 line-height: 24px;
297 }
304 }
298 }
305 }
299 .comment-selected {
306 .comment-selected {
300 border-left: 6px solid @comment-highlight-color;
307 border-left: 6px solid @comment-highlight-color;
301 }
308 }
302 .comment-inline-form {
309 .comment-inline-form {
303 padding: @comment-padding;
310 padding: @comment-padding;
304 display: none;
311 display: none;
305 }
312 }
306 .cb-comment-add-button {
313 .cb-comment-add-button {
307 margin: @comment-padding;
314 margin: @comment-padding;
308 }
315 }
309 /* hide add comment button when form is open */
316 /* hide add comment button when form is open */
310 .comment-inline-form-open ~ .cb-comment-add-button {
317 .comment-inline-form-open ~ .cb-comment-add-button {
311 display: none;
318 display: none;
312 }
319 }
313 .comment-inline-form-open {
320 .comment-inline-form-open {
314 display: block;
321 display: block;
315 }
322 }
316 /* hide add comment button when form but no comments */
323 /* hide add comment button when form but no comments */
317 .comment-inline-form:first-child + .cb-comment-add-button {
324 .comment-inline-form:first-child + .cb-comment-add-button {
318 display: none;
325 display: none;
319 }
326 }
320 /* hide add comment button when no comments or form */
327 /* hide add comment button when no comments or form */
321 .cb-comment-add-button:first-child {
328 .cb-comment-add-button:first-child {
322 display: none;
329 display: none;
323 }
330 }
324 /* hide add comment button when only comment is being deleted */
331 /* hide add comment button when only comment is being deleted */
325 .comment-deleting:first-child + .cb-comment-add-button {
332 .comment-deleting:first-child + .cb-comment-add-button {
326 display: none;
333 display: none;
327 }
334 }
328 }
335 }
329
336
330
337
331 .show-outdated-comments {
338 .show-outdated-comments {
332 display: inline;
339 display: inline;
333 color: @rcblue;
340 color: @rcblue;
334 }
341 }
335
342
336 // Comment Form
343 // Comment Form
337 div.comment-form {
344 div.comment-form {
338 margin-top: 20px;
345 margin-top: 20px;
339 }
346 }
340
347
341 .comment-form strong {
348 .comment-form strong {
342 display: block;
349 display: block;
343 margin-bottom: 15px;
350 margin-bottom: 15px;
344 }
351 }
345
352
346 .comment-form textarea {
353 .comment-form textarea {
347 width: 100%;
354 width: 100%;
348 height: 100px;
355 height: 100px;
349 font-family: @text-monospace;
356 font-family: @text-monospace;
350 }
357 }
351
358
352 form.comment-form {
359 form.comment-form {
353 margin-top: 10px;
360 margin-top: 10px;
354 margin-left: 10px;
361 margin-left: 10px;
355 }
362 }
356
363
357 .comment-inline-form .comment-block-ta,
364 .comment-inline-form .comment-block-ta,
358 .comment-form .comment-block-ta,
365 .comment-form .comment-block-ta,
359 .comment-form .preview-box {
366 .comment-form .preview-box {
360 .border-radius(@border-radius);
367 .border-radius(@border-radius);
361 .box-sizing(border-box);
368 .box-sizing(border-box);
362 background-color: white;
369 background-color: white;
363 }
370 }
364
371
365 .comment-form-submit {
372 .comment-form-submit {
366 margin-top: 5px;
373 margin-top: 5px;
367 margin-left: 525px;
374 margin-left: 525px;
368 }
375 }
369
376
370 .file-comments {
377 .file-comments {
371 display: none;
378 display: none;
372 }
379 }
373
380
374 .comment-form .preview-box.unloaded,
381 .comment-form .preview-box.unloaded,
375 .comment-inline-form .preview-box.unloaded {
382 .comment-inline-form .preview-box.unloaded {
376 height: 50px;
383 height: 50px;
377 text-align: center;
384 text-align: center;
378 padding: 20px;
385 padding: 20px;
379 background-color: white;
386 background-color: white;
380 }
387 }
381
388
382 .comment-footer {
389 .comment-footer {
383 position: relative;
390 position: relative;
384 width: 100%;
391 width: 100%;
385 min-height: 42px;
392 min-height: 42px;
386
393
387 .status_box,
394 .status_box,
388 .cancel-button {
395 .cancel-button {
389 float: left;
396 float: left;
390 display: inline-block;
397 display: inline-block;
391 }
398 }
392
399
393 .status_box {
400 .status_box {
394 margin-left: 10px;
401 margin-left: 10px;
395 }
402 }
396
403
397 .action-buttons {
404 .action-buttons {
398 float: left;
405 float: left;
399 display: inline-block;
406 display: inline-block;
400 }
407 }
401
408
402 .action-buttons-extra {
409 .action-buttons-extra {
403 display: inline-block;
410 display: inline-block;
404 }
411 }
405 }
412 }
406
413
407 .comment-form {
414 .comment-form {
408
415
409 .comment {
416 .comment {
410 margin-left: 10px;
417 margin-left: 10px;
411 }
418 }
412
419
413 .comment-help {
420 .comment-help {
414 color: @grey4;
421 color: @grey4;
415 padding: 5px 0 5px 0;
422 padding: 5px 0 5px 0;
416 }
423 }
417
424
418 .comment-title {
425 .comment-title {
419 padding: 5px 0 5px 0;
426 padding: 5px 0 5px 0;
420 }
427 }
421
428
422 .comment-button {
429 .comment-button {
423 display: inline-block;
430 display: inline-block;
424 }
431 }
425
432
426 .comment-button-input {
433 .comment-button-input {
427 margin-right: 0;
434 margin-right: 0;
428 }
435 }
429
436
430 .comment-footer {
437 .comment-footer {
431 margin-bottom: 50px;
438 margin-bottom: 50px;
432 margin-top: 10px;
439 margin-top: 10px;
433 }
440 }
434 }
441 }
435
442
436
443
437 .comment-form-login {
444 .comment-form-login {
438 .comment-help {
445 .comment-help {
439 padding: 0.7em; //same as the button
446 padding: 0.7em; //same as the button
440 }
447 }
441
448
442 div.clearfix {
449 div.clearfix {
443 clear: both;
450 clear: both;
444 width: 100%;
451 width: 100%;
445 display: block;
452 display: block;
446 }
453 }
447 }
454 }
448
455
449 .comment-version-select {
456 .comment-version-select {
450 margin: 0px;
457 margin: 0px;
451 border-radius: inherit;
458 border-radius: inherit;
452 border-color: @grey6;
459 border-color: @grey6;
453 height: 20px;
460 height: 20px;
454 }
461 }
455
462
456 .comment-type {
463 .comment-type {
457 margin: 0px;
464 margin: 0px;
458 border-radius: inherit;
465 border-radius: inherit;
459 border-color: @grey6;
466 border-color: @grey6;
460 }
467 }
461
468
462 .preview-box {
469 .preview-box {
463 min-height: 105px;
470 min-height: 105px;
464 margin-bottom: 15px;
471 margin-bottom: 15px;
465 background-color: white;
472 background-color: white;
466 .border-radius(@border-radius);
473 .border-radius(@border-radius);
467 .box-sizing(border-box);
474 .box-sizing(border-box);
468 }
475 }
469
476
470 .add-another-button {
477 .add-another-button {
471 margin-left: 10px;
478 margin-left: 10px;
472 margin-top: 10px;
479 margin-top: 10px;
473 margin-bottom: 10px;
480 margin-bottom: 10px;
474 }
481 }
475
482
476 .comment .buttons {
483 .comment .buttons {
477 float: right;
484 float: right;
478 margin: -1px 0px 0px 0px;
485 margin: -1px 0px 0px 0px;
479 }
486 }
480
487
481 // Inline Comment Form
488 // Inline Comment Form
482 .injected_diff .comment-inline-form,
489 .injected_diff .comment-inline-form,
483 .comment-inline-form {
490 .comment-inline-form {
484 background-color: white;
491 background-color: white;
485 margin-top: 10px;
492 margin-top: 10px;
486 margin-bottom: 20px;
493 margin-bottom: 20px;
487 }
494 }
488
495
489 .inline-form {
496 .inline-form {
490 padding: 10px 7px;
497 padding: 10px 7px;
491 }
498 }
492
499
493 .inline-form div {
500 .inline-form div {
494 max-width: 100%;
501 max-width: 100%;
495 }
502 }
496
503
497 .overlay {
504 .overlay {
498 display: none;
505 display: none;
499 position: absolute;
506 position: absolute;
500 width: 100%;
507 width: 100%;
501 text-align: center;
508 text-align: center;
502 vertical-align: middle;
509 vertical-align: middle;
503 font-size: 16px;
510 font-size: 16px;
504 background: none repeat scroll 0 0 white;
511 background: none repeat scroll 0 0 white;
505
512
506 &.submitting {
513 &.submitting {
507 display: block;
514 display: block;
508 opacity: 0.5;
515 opacity: 0.5;
509 z-index: 100;
516 z-index: 100;
510 }
517 }
511 }
518 }
512 .comment-inline-form .overlay.submitting .overlay-text {
519 .comment-inline-form .overlay.submitting .overlay-text {
513 margin-top: 5%;
520 margin-top: 5%;
514 }
521 }
515
522
516 .comment-inline-form .clearfix,
523 .comment-inline-form .clearfix,
517 .comment-form .clearfix {
524 .comment-form .clearfix {
518 .border-radius(@border-radius);
525 .border-radius(@border-radius);
519 margin: 0px;
526 margin: 0px;
520 }
527 }
521
528
522 .comment-inline-form .comment-footer {
529 .comment-inline-form .comment-footer {
523 margin: 10px 0px 0px 0px;
530 margin: 10px 0px 0px 0px;
524 }
531 }
525
532
526 .hide-inline-form-button {
533 .hide-inline-form-button {
527 margin-left: 5px;
534 margin-left: 5px;
528 }
535 }
529 .comment-button .hide-inline-form {
536 .comment-button .hide-inline-form {
530 background: white;
537 background: white;
531 }
538 }
532
539
533 .comment-area {
540 .comment-area {
534 padding: 6px 8px;
541 padding: 6px 8px;
535 border: 1px solid @grey5;
542 border: 1px solid @grey5;
536 .border-radius(@border-radius);
543 .border-radius(@border-radius);
537
544
538 .resolve-action {
545 .resolve-action {
539 padding: 1px 0px 0px 6px;
546 padding: 1px 0px 0px 6px;
540 }
547 }
541
548
542 }
549 }
543
550
544 comment-area-text {
551 comment-area-text {
545 color: @grey3;
552 color: @grey3;
546 }
553 }
547
554
548 .comment-area-header {
555 .comment-area-header {
549 height: 35px;
556 height: 35px;
550 }
557 }
551
558
552 .comment-area-header .nav-links {
559 .comment-area-header .nav-links {
553 display: flex;
560 display: flex;
554 flex-flow: row wrap;
561 flex-flow: row wrap;
555 -webkit-flex-flow: row wrap;
562 -webkit-flex-flow: row wrap;
556 width: 100%;
563 width: 100%;
557 }
564 }
558
565
559 .comment-area-footer {
566 .comment-area-footer {
560 min-height: 30px;
567 min-height: 30px;
561 }
568 }
562
569
563 .comment-footer .toolbar {
570 .comment-footer .toolbar {
564
571
565 }
572 }
566
573
567 .comment-attachment-uploader {
574 .comment-attachment-uploader {
568 border: 1px dashed white;
575 border: 1px dashed white;
569 border-radius: @border-radius;
576 border-radius: @border-radius;
570 margin-top: -10px;
577 margin-top: -10px;
571 line-height: 30px;
578 line-height: 30px;
572 &.dz-drag-hover {
579 &.dz-drag-hover {
573 border-color: @grey3;
580 border-color: @grey3;
574 }
581 }
575
582
576 .dz-error-message {
583 .dz-error-message {
577 padding-top: 0;
584 padding-top: 0;
578 }
585 }
579 }
586 }
580
587
581 .comment-attachment-text {
588 .comment-attachment-text {
582 clear: both;
589 clear: both;
583 font-size: 11px;
590 font-size: 11px;
584 color: #8F8F8F;
591 color: #8F8F8F;
585 width: 100%;
592 width: 100%;
586 .pick-attachment {
593 .pick-attachment {
587 color: #8F8F8F;
594 color: #8F8F8F;
588 }
595 }
589 .pick-attachment:hover {
596 .pick-attachment:hover {
590 color: @rcblue;
597 color: @rcblue;
591 }
598 }
592 }
599 }
593
600
594 .nav-links {
601 .nav-links {
595 padding: 0;
602 padding: 0;
596 margin: 0;
603 margin: 0;
597 list-style: none;
604 list-style: none;
598 height: auto;
605 height: auto;
599 border-bottom: 1px solid @grey5;
606 border-bottom: 1px solid @grey5;
600 }
607 }
601 .nav-links li {
608 .nav-links li {
602 display: inline-block;
609 display: inline-block;
603 list-style-type: none;
610 list-style-type: none;
604 }
611 }
605
612
606 .nav-links li a.disabled {
613 .nav-links li a.disabled {
607 cursor: not-allowed;
614 cursor: not-allowed;
608 }
615 }
609
616
610 .nav-links li.active a {
617 .nav-links li.active a {
611 border-bottom: 2px solid @rcblue;
618 border-bottom: 2px solid @rcblue;
612 color: #000;
619 color: #000;
613 font-weight: 600;
620 font-weight: 600;
614 }
621 }
615 .nav-links li a {
622 .nav-links li a {
616 display: inline-block;
623 display: inline-block;
617 padding: 0px 10px 5px 10px;
624 padding: 0px 10px 5px 10px;
618 margin-bottom: -1px;
625 margin-bottom: -1px;
619 font-size: 14px;
626 font-size: 14px;
620 line-height: 28px;
627 line-height: 28px;
621 color: #8f8f8f;
628 color: #8f8f8f;
622 border-bottom: 2px solid transparent;
629 border-bottom: 2px solid transparent;
623 }
630 }
624
631
625 .toolbar-text {
632 .toolbar-text {
626 float: right;
633 float: right;
627 font-size: 11px;
634 font-size: 11px;
628 color: @grey4;
635 color: @grey4;
629 text-align: right;
636 text-align: right;
630
637
631 a {
638 a {
632 color: @grey4;
639 color: @grey4;
633 }
640 }
634 }
641 }
635
642
@@ -1,843 +1,850 b''
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 /**
19 /**
20 * Code Mirror
20 * Code Mirror
21 */
21 */
22 // global code-mirror logger;, to enable run
22 // global code-mirror logger;, to enable run
23 // Logger.get('CodeMirror').setLevel(Logger.DEBUG)
23 // Logger.get('CodeMirror').setLevel(Logger.DEBUG)
24
24
25 cmLog = Logger.get('CodeMirror');
25 cmLog = Logger.get('CodeMirror');
26 cmLog.setLevel(Logger.OFF);
26 cmLog.setLevel(Logger.OFF);
27
27
28
28
29 //global cache for inline forms
29 //global cache for inline forms
30 var userHintsCache = {};
30 var userHintsCache = {};
31
31
32 // global timer, used to cancel async loading
32 // global timer, used to cancel async loading
33 var CodeMirrorLoadUserHintTimer;
33 var CodeMirrorLoadUserHintTimer;
34
34
35 var escapeRegExChars = function(value) {
35 var escapeRegExChars = function(value) {
36 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
36 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
37 };
37 };
38
38
39 /**
39 /**
40 * Load hints from external source returns an array of objects in a format
40 * Load hints from external source returns an array of objects in a format
41 * that hinting lib requires
41 * that hinting lib requires
42 * @returns {Array}
42 * @returns {Array}
43 */
43 */
44 var CodeMirrorLoadUserHints = function(query, triggerHints) {
44 var CodeMirrorLoadUserHints = function(query, triggerHints) {
45 cmLog.debug('Loading mentions users via AJAX');
45 cmLog.debug('Loading mentions users via AJAX');
46 var _users = [];
46 var _users = [];
47 $.ajax({
47 $.ajax({
48 type: 'GET',
48 type: 'GET',
49 data: {query: query},
49 data: {query: query},
50 url: pyroutes.url('user_autocomplete_data'),
50 url: pyroutes.url('user_autocomplete_data'),
51 headers: {'X-PARTIAL-XHR': true},
51 headers: {'X-PARTIAL-XHR': true},
52 async: true
52 async: true
53 })
53 })
54 .done(function(data) {
54 .done(function(data) {
55 var tmpl = '<img class="gravatar" src="{0}"/>{1}';
55 var tmpl = '<img class="gravatar" src="{0}"/>{1}';
56 $.each(data.suggestions, function(i) {
56 $.each(data.suggestions, function(i) {
57 var userObj = data.suggestions[i];
57 var userObj = data.suggestions[i];
58
58
59 if (userObj.username !== "default") {
59 if (userObj.username !== "default") {
60 _users.push({
60 _users.push({
61 text: userObj.username + " ",
61 text: userObj.username + " ",
62 org_text: userObj.username,
62 org_text: userObj.username,
63 displayText: userObj.value_display, // search that field
63 displayText: userObj.value_display, // search that field
64 // internal caches
64 // internal caches
65 _icon_link: userObj.icon_link,
65 _icon_link: userObj.icon_link,
66 _text: userObj.value_display,
66 _text: userObj.value_display,
67
67
68 render: function(elt, data, completion) {
68 render: function(elt, data, completion) {
69 var el = document.createElement('div');
69 var el = document.createElement('div');
70 el.className = "CodeMirror-hint-entry";
70 el.className = "CodeMirror-hint-entry";
71 el.innerHTML = tmpl.format(
71 el.innerHTML = tmpl.format(
72 completion._icon_link, completion._text);
72 completion._icon_link, completion._text);
73 elt.appendChild(el);
73 elt.appendChild(el);
74 }
74 }
75 });
75 });
76 }
76 }
77 });
77 });
78 cmLog.debug('Mention users loaded');
78 cmLog.debug('Mention users loaded');
79 // set to global cache
79 // set to global cache
80 userHintsCache[query] = _users;
80 userHintsCache[query] = _users;
81 triggerHints(userHintsCache[query]);
81 triggerHints(userHintsCache[query]);
82 })
82 })
83 .fail(function(data, textStatus, xhr) {
83 .fail(function(data, textStatus, xhr) {
84 alert("error processing request. \n" +
84 alert("error processing request. \n" +
85 "Error code {0} ({1}).".format(data.status, data.statusText));
85 "Error code {0} ({1}).".format(data.status, data.statusText));
86 });
86 });
87 };
87 };
88
88
89 /**
89 /**
90 * filters the results based on the current context
90 * filters the results based on the current context
91 * @param users
91 * @param users
92 * @param context
92 * @param context
93 * @returns {Array}
93 * @returns {Array}
94 */
94 */
95 var CodeMirrorFilterUsers = function(users, context) {
95 var CodeMirrorFilterUsers = function(users, context) {
96 var MAX_LIMIT = 10;
96 var MAX_LIMIT = 10;
97 var filtered_users = [];
97 var filtered_users = [];
98 var curWord = context.string;
98 var curWord = context.string;
99
99
100 cmLog.debug('Filtering users based on query:', curWord);
100 cmLog.debug('Filtering users based on query:', curWord);
101 $.each(users, function(i) {
101 $.each(users, function(i) {
102 var match = users[i];
102 var match = users[i];
103 var searchText = match.displayText;
103 var searchText = match.displayText;
104
104
105 if (!curWord ||
105 if (!curWord ||
106 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
106 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
107 // reset state
107 // reset state
108 match._text = match.displayText;
108 match._text = match.displayText;
109 if (curWord) {
109 if (curWord) {
110 // do highlighting
110 // do highlighting
111 var pattern = '(' + escapeRegExChars(curWord) + ')';
111 var pattern = '(' + escapeRegExChars(curWord) + ')';
112 match._text = searchText.replace(
112 match._text = searchText.replace(
113 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
113 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
114 }
114 }
115
115
116 filtered_users.push(match);
116 filtered_users.push(match);
117 }
117 }
118 // to not return to many results, use limit of filtered results
118 // to not return to many results, use limit of filtered results
119 if (filtered_users.length > MAX_LIMIT) {
119 if (filtered_users.length > MAX_LIMIT) {
120 return false;
120 return false;
121 }
121 }
122 });
122 });
123
123
124 return filtered_users;
124 return filtered_users;
125 };
125 };
126
126
127 var CodeMirrorMentionHint = function(editor, callback, options) {
127 var CodeMirrorMentionHint = function(editor, callback, options) {
128 var cur = editor.getCursor();
128 var cur = editor.getCursor();
129 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
129 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
130
130
131 // match on @ +1char
131 // match on @ +1char
132 var tokenMatch = new RegExp(
132 var tokenMatch = new RegExp(
133 '(^@| @)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*)$').exec(curLine);
133 '(^@| @)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*)$').exec(curLine);
134
134
135 var tokenStr = '';
135 var tokenStr = '';
136 if (tokenMatch !== null && tokenMatch.length > 0){
136 if (tokenMatch !== null && tokenMatch.length > 0){
137 tokenStr = tokenMatch[0].strip();
137 tokenStr = tokenMatch[0].strip();
138 } else {
138 } else {
139 // skip if we didn't match our token
139 // skip if we didn't match our token
140 return;
140 return;
141 }
141 }
142
142
143 var context = {
143 var context = {
144 start: (cur.ch - tokenStr.length) + 1,
144 start: (cur.ch - tokenStr.length) + 1,
145 end: cur.ch,
145 end: cur.ch,
146 string: tokenStr.slice(1),
146 string: tokenStr.slice(1),
147 type: null
147 type: null
148 };
148 };
149
149
150 // case when we put the @sign in fron of a string,
150 // case when we put the @sign in fron of a string,
151 // eg <@ we put it here>sometext then we need to prepend to text
151 // eg <@ we put it here>sometext then we need to prepend to text
152 if (context.end > cur.ch) {
152 if (context.end > cur.ch) {
153 context.start = context.start + 1; // we add to the @ sign
153 context.start = context.start + 1; // we add to the @ sign
154 context.end = cur.ch; // don't eat front part just append
154 context.end = cur.ch; // don't eat front part just append
155 context.string = context.string.slice(1, cur.ch - context.start);
155 context.string = context.string.slice(1, cur.ch - context.start);
156 }
156 }
157
157
158 cmLog.debug('Mention context', context);
158 cmLog.debug('Mention context', context);
159
159
160 var triggerHints = function(userHints){
160 var triggerHints = function(userHints){
161 return callback({
161 return callback({
162 list: CodeMirrorFilterUsers(userHints, context),
162 list: CodeMirrorFilterUsers(userHints, context),
163 from: CodeMirror.Pos(cur.line, context.start),
163 from: CodeMirror.Pos(cur.line, context.start),
164 to: CodeMirror.Pos(cur.line, context.end)
164 to: CodeMirror.Pos(cur.line, context.end)
165 });
165 });
166 };
166 };
167
167
168 var queryBasedHintsCache = undefined;
168 var queryBasedHintsCache = undefined;
169 // if we have something in the cache, try to fetch the query based cache
169 // if we have something in the cache, try to fetch the query based cache
170 if (userHintsCache !== {}){
170 if (userHintsCache !== {}){
171 queryBasedHintsCache = userHintsCache[context.string];
171 queryBasedHintsCache = userHintsCache[context.string];
172 }
172 }
173
173
174 if (queryBasedHintsCache !== undefined) {
174 if (queryBasedHintsCache !== undefined) {
175 cmLog.debug('Users loaded from cache');
175 cmLog.debug('Users loaded from cache');
176 triggerHints(queryBasedHintsCache);
176 triggerHints(queryBasedHintsCache);
177 } else {
177 } else {
178 // this takes care for async loading, and then displaying results
178 // this takes care for async loading, and then displaying results
179 // and also propagates the userHintsCache
179 // and also propagates the userHintsCache
180 window.clearTimeout(CodeMirrorLoadUserHintTimer);
180 window.clearTimeout(CodeMirrorLoadUserHintTimer);
181 CodeMirrorLoadUserHintTimer = setTimeout(function() {
181 CodeMirrorLoadUserHintTimer = setTimeout(function() {
182 CodeMirrorLoadUserHints(context.string, triggerHints);
182 CodeMirrorLoadUserHints(context.string, triggerHints);
183 }, 300);
183 }, 300);
184 }
184 }
185 };
185 };
186
186
187 var CodeMirrorCompleteAfter = function(cm, pred) {
187 var CodeMirrorCompleteAfter = function(cm, pred) {
188 var options = {
188 var options = {
189 completeSingle: false,
189 completeSingle: false,
190 async: true,
190 async: true,
191 closeOnUnfocus: true
191 closeOnUnfocus: true
192 };
192 };
193 var cur = cm.getCursor();
193 var cur = cm.getCursor();
194 setTimeout(function() {
194 setTimeout(function() {
195 if (!cm.state.completionActive) {
195 if (!cm.state.completionActive) {
196 cmLog.debug('Trigger mentions hinting');
196 cmLog.debug('Trigger mentions hinting');
197 CodeMirror.showHint(cm, CodeMirror.hint.mentions, options);
197 CodeMirror.showHint(cm, CodeMirror.hint.mentions, options);
198 }
198 }
199 }, 100);
199 }, 100);
200
200
201 // tell CodeMirror we didn't handle the key
201 // tell CodeMirror we didn't handle the key
202 // trick to trigger on a char but still complete it
202 // trick to trigger on a char but still complete it
203 return CodeMirror.Pass;
203 return CodeMirror.Pass;
204 };
204 };
205
205
206 var initCodeMirror = function(textAreadId, resetUrl, focus, options) {
206 var initCodeMirror = function(textAreadId, resetUrl, focus, options) {
207 if (textAreadId.substr(0,1) === "#"){
207 if (textAreadId.substr(0,1) === "#"){
208 var ta = $(textAreadId).get(0);
208 var ta = $(textAreadId).get(0);
209 }else {
209 }else {
210 var ta = $('#' + textAreadId).get(0);
210 var ta = $('#' + textAreadId).get(0);
211 }
211 }
212
212
213 if (focus === undefined) {
213 if (focus === undefined) {
214 focus = true;
214 focus = true;
215 }
215 }
216
216
217 // default options
217 // default options
218 var codeMirrorOptions = {
218 var codeMirrorOptions = {
219 mode: "null",
219 mode: "null",
220 lineNumbers: true,
220 lineNumbers: true,
221 indentUnit: 4,
221 indentUnit: 4,
222 autofocus: focus
222 autofocus: focus
223 };
223 };
224
224
225 if (options !== undefined) {
225 if (options !== undefined) {
226 // extend with custom options
226 // extend with custom options
227 codeMirrorOptions = $.extend(true, codeMirrorOptions, options);
227 codeMirrorOptions = $.extend(true, codeMirrorOptions, options);
228 }
228 }
229
229
230 var myCodeMirror = CodeMirror.fromTextArea(ta, codeMirrorOptions);
230 var myCodeMirror = CodeMirror.fromTextArea(ta, codeMirrorOptions);
231
231
232 $('#reset').on('click', function(e) {
232 $('#reset').on('click', function(e) {
233 window.location = resetUrl;
233 window.location = resetUrl;
234 });
234 });
235
235
236 return myCodeMirror;
236 return myCodeMirror;
237 };
237 };
238
238
239
239
240 var initMarkupCodeMirror = function(textAreadId, focus, options) {
240 var initMarkupCodeMirror = function(textAreadId, focus, options) {
241 var initialHeight = 100;
241 var initialHeight = 100;
242
242
243 var ta = $(textAreadId).get(0);
243 var ta = $(textAreadId).get(0);
244 if (focus === undefined) {
244 if (focus === undefined) {
245 focus = true;
245 focus = true;
246 }
246 }
247
247
248 // default options
248 // default options
249 var codeMirrorOptions = {
249 var codeMirrorOptions = {
250 lineNumbers: false,
250 lineNumbers: false,
251 indentUnit: 4,
251 indentUnit: 4,
252 viewportMargin: 30,
252 viewportMargin: 30,
253 // this is a trick to trigger some logic behind codemirror placeholder
253 // this is a trick to trigger some logic behind codemirror placeholder
254 // it influences styling and behaviour.
254 // it influences styling and behaviour.
255 placeholder: " ",
255 placeholder: " ",
256 lineWrapping: true,
256 lineWrapping: true,
257 autofocus: focus
257 autofocus: focus
258 };
258 };
259
259
260 if (options !== undefined) {
260 if (options !== undefined) {
261 // extend with custom options
261 // extend with custom options
262 codeMirrorOptions = $.extend(true, codeMirrorOptions, options);
262 codeMirrorOptions = $.extend(true, codeMirrorOptions, options);
263 }
263 }
264
264
265 var cm = CodeMirror.fromTextArea(ta, codeMirrorOptions);
265 var cm = CodeMirror.fromTextArea(ta, codeMirrorOptions);
266 cm.setSize(null, initialHeight);
266 cm.setSize(null, initialHeight);
267 cm.setOption("mode", DEFAULT_RENDERER);
267 cm.setOption("mode", DEFAULT_RENDERER);
268 CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode
268 CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode
269 cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER);
269 cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER);
270
270
271 // start listening on changes to make auto-expanded editor
271 // start listening on changes to make auto-expanded editor
272 cm.on("change", function (instance, changeObj) {
272 cm.on("change", function (instance, changeObj) {
273 var height = initialHeight;
273 var height = initialHeight;
274 var lines = instance.lineCount();
274 var lines = instance.lineCount();
275 if (lines > 6 && lines < 20) {
275 if (lines > 6 && lines < 20) {
276 height = "auto";
276 height = "auto";
277 } else if (lines >= 20) {
277 } else if (lines >= 20) {
278 height = 20 * 15;
278 height = 20 * 15;
279 }
279 }
280 instance.setSize(null, height);
280 instance.setSize(null, height);
281
281
282 // detect if the change was trigger by auto desc, or user input
282 // detect if the change was trigger by auto desc, or user input
283 var changeOrigin = changeObj.origin;
283 var changeOrigin = changeObj.origin;
284
284
285 if (changeOrigin === "setValue") {
285 if (changeOrigin === "setValue") {
286 cmLog.debug('Change triggered by setValue');
286 cmLog.debug('Change triggered by setValue');
287 }
287 }
288 else {
288 else {
289 cmLog.debug('user triggered change !');
289 cmLog.debug('user triggered change !');
290 // set special marker to indicate user has created an input.
290 // set special marker to indicate user has created an input.
291 instance._userDefinedValue = true;
291 instance._userDefinedValue = true;
292 }
292 }
293
293
294 });
294 });
295
295
296 return cm;
296 return cm;
297 };
297 };
298
298
299
299
300 var initCommentBoxCodeMirror = function(CommentForm, textAreaId, triggerActions){
300 var initCommentBoxCodeMirror = function(CommentForm, textAreaId, triggerActions){
301 var initialHeight = 100;
301 var initialHeight = 100;
302
302
303 if (typeof userHintsCache === "undefined") {
303 if (typeof userHintsCache === "undefined") {
304 userHintsCache = {};
304 userHintsCache = {};
305 cmLog.debug('Init empty cache for mentions');
305 cmLog.debug('Init empty cache for mentions');
306 }
306 }
307 if (!$(textAreaId).get(0)) {
307 if (!$(textAreaId).get(0)) {
308 cmLog.debug('Element for textarea not found', textAreaId);
308 cmLog.debug('Element for textarea not found', textAreaId);
309 return;
309 return;
310 }
310 }
311 /**
311 /**
312 * Filter action based on typed in text
312 * Filter action based on typed in text
313 * @param actions
313 * @param actions
314 * @param context
314 * @param context
315 * @returns {Array}
315 * @returns {Array}
316 */
316 */
317
317
318 var filterActions = function(actions, context){
318 var filterActions = function(actions, context){
319
319
320 var MAX_LIMIT = 10;
320 var MAX_LIMIT = 10;
321 var filtered_actions = [];
321 var filtered_actions = [];
322 var curWord = context.string;
322 var curWord = context.string;
323
323
324 cmLog.debug('Filtering actions based on query:', curWord);
324 cmLog.debug('Filtering actions based on query:', curWord);
325 $.each(actions, function(i) {
325 $.each(actions, function(i) {
326 var match = actions[i];
326 var match = actions[i];
327 var searchText = match.searchText;
327 var searchText = match.searchText;
328
328
329 if (!curWord ||
329 if (!curWord ||
330 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
330 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
331 // reset state
331 // reset state
332 match._text = match.displayText;
332 match._text = match.displayText;
333 if (curWord) {
333 if (curWord) {
334 // do highlighting
334 // do highlighting
335 var pattern = '(' + escapeRegExChars(curWord) + ')';
335 var pattern = '(' + escapeRegExChars(curWord) + ')';
336 match._text = searchText.replace(
336 match._text = searchText.replace(
337 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
337 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
338 }
338 }
339
339
340 filtered_actions.push(match);
340 filtered_actions.push(match);
341 }
341 }
342 // to not return to many results, use limit of filtered results
342 // to not return to many results, use limit of filtered results
343 if (filtered_actions.length > MAX_LIMIT) {
343 if (filtered_actions.length > MAX_LIMIT) {
344 return false;
344 return false;
345 }
345 }
346 });
346 });
347
347
348 return filtered_actions;
348 return filtered_actions;
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
356 var completeActions = function(actions){
361 var completeActions = function(actions){
357
362
358 var registeredActions = [];
363 var registeredActions = [];
359 var allActions = [
364 var allActions = [
360 {
365 {
361 text: "approve",
366 text: "approve",
362 searchText: "status approved",
367 searchText: "status approved",
363 displayText: _gettext('Set status to Approved'),
368 displayText: _gettext('Set status to Approved'),
364 hint: function(CodeMirror, data, completion) {
369 hint: function(CodeMirror, data, completion) {
365 CodeMirror.replaceRange("", completion.from || data.from,
370 CodeMirror.replaceRange("", completion.from || data.from,
366 completion.to || data.to, "complete");
371 completion.to || data.to, "complete");
367 $(CommentForm.statusChange).select2("val", 'approved').trigger('change');
372 $(CommentForm.statusChange).select2("val", 'approved').trigger('change');
368 },
373 },
369 render: function(elt, data, completion) {
374 render: function(elt, data, completion) {
370 var el = document.createElement('i');
375 var el = document.createElement('i');
371
376
372 el.className = "icon-circle review-status-approved";
377 el.className = "icon-circle review-status-approved";
373 elt.appendChild(el);
378 elt.appendChild(el);
374
379
375 el = document.createElement('span');
380 el = document.createElement('span');
376 el.innerHTML = completion.displayText;
381 el.innerHTML = completion.displayText;
377 elt.appendChild(el);
382 elt.appendChild(el);
378 }
383 }
379 },
384 },
380 {
385 {
381 text: "reject",
386 text: "reject",
382 searchText: "status rejected",
387 searchText: "status rejected",
383 displayText: _gettext('Set status to Rejected'),
388 displayText: _gettext('Set status to Rejected'),
384 hint: function(CodeMirror, data, completion) {
389 hint: function(CodeMirror, data, completion) {
385 CodeMirror.replaceRange("", completion.from || data.from,
390 CodeMirror.replaceRange("", completion.from || data.from,
386 completion.to || data.to, "complete");
391 completion.to || data.to, "complete");
387 $(CommentForm.statusChange).select2("val", 'rejected').trigger('change');
392 $(CommentForm.statusChange).select2("val", 'rejected').trigger('change');
388 },
393 },
389 render: function(elt, data, completion) {
394 render: function(elt, data, completion) {
390 var el = document.createElement('i');
395 var el = document.createElement('i');
391 el.className = "icon-circle review-status-rejected";
396 el.className = "icon-circle review-status-rejected";
392 elt.appendChild(el);
397 elt.appendChild(el);
393
398
394 el = document.createElement('span');
399 el = document.createElement('span');
395 el.innerHTML = completion.displayText;
400 el.innerHTML = completion.displayText;
396 elt.appendChild(el);
401 elt.appendChild(el);
397 }
402 }
398 },
403 },
399 {
404 {
400 text: "as_todo",
405 text: "as_todo",
401 searchText: "todo comment",
406 searchText: "todo comment",
402 displayText: _gettext('TODO comment'),
407 displayText: _gettext('TODO comment'),
403 hint: function(CodeMirror, data, completion) {
408 hint: function(CodeMirror, data, completion) {
404 CodeMirror.replaceRange("", completion.from || data.from,
409 CodeMirror.replaceRange("", completion.from || data.from,
405 completion.to || data.to, "complete");
410 completion.to || data.to, "complete");
406
411
407 $(CommentForm.commentType).val('todo');
412 $(CommentForm.commentType).val('todo');
408 },
413 },
409 render: function(elt, data, completion) {
414 render: function(elt, data, completion) {
410 var el = document.createElement('div');
415 var el = document.createElement('div');
411 el.className = "pull-left";
416 el.className = "pull-left";
412 elt.appendChild(el);
417 elt.appendChild(el);
413
418
414 el = document.createElement('span');
419 el = document.createElement('span');
415 el.innerHTML = completion.displayText;
420 el.innerHTML = completion.displayText;
416 elt.appendChild(el);
421 elt.appendChild(el);
417 }
422 }
418 },
423 },
419 {
424 {
420 text: "as_note",
425 text: "as_note",
421 searchText: "note comment",
426 searchText: "note comment",
422 displayText: _gettext('Note Comment'),
427 displayText: _gettext('Note Comment'),
423 hint: function(CodeMirror, data, completion) {
428 hint: function(CodeMirror, data, completion) {
424 CodeMirror.replaceRange("", completion.from || data.from,
429 CodeMirror.replaceRange("", completion.from || data.from,
425 completion.to || data.to, "complete");
430 completion.to || data.to, "complete");
426
431
427 $(CommentForm.commentType).val('note');
432 $(CommentForm.commentType).val('note');
428 },
433 },
429 render: function(elt, data, completion) {
434 render: function(elt, data, completion) {
430 var el = document.createElement('div');
435 var el = document.createElement('div');
431 el.className = "pull-left";
436 el.className = "pull-left";
432 elt.appendChild(el);
437 elt.appendChild(el);
433
438
434 el = document.createElement('span');
439 el = document.createElement('span');
435 el.innerHTML = completion.displayText;
440 el.innerHTML = completion.displayText;
436 elt.appendChild(el);
441 elt.appendChild(el);
437 }
442 }
438 }
443 }
439 ];
444 ];
440
445
441 $.each(allActions, function(index, value){
446 $.each(allActions, function(index, value){
442 var actionData = allActions[index];
447 var actionData = allActions[index];
443 if (actions.indexOf(actionData['text']) != -1) {
448 if (actions.indexOf(actionData['text']) != -1) {
444 registeredActions.push(actionData);
449 registeredActions.push(actionData);
445 }
450 }
446 });
451 });
447
452
448 return function(cm, pred) {
453 return function(cm, pred) {
449 var cur = cm.getCursor();
454 var cur = cm.getCursor();
450 var options = {
455 var options = {
451 closeOnUnfocus: true,
456 closeOnUnfocus: true,
452 registeredActions: registeredActions
457 registeredActions: registeredActions
453 };
458 };
454 setTimeout(function() {
459 setTimeout(function() {
455 if (!cm.state.completionActive) {
460 if (!cm.state.completionActive) {
456 cmLog.debug('Trigger actions hinting');
461 cmLog.debug('Trigger actions hinting');
457 CodeMirror.showHint(cm, CodeMirror.hint.actions, options);
462 CodeMirror.showHint(cm, CodeMirror.hint.actions, options);
458 }
463 }
459 }, 100);
464 }, 100);
460
465
461 // tell CodeMirror we didn't handle the key
466 // tell CodeMirror we didn't handle the key
462 // trick to trigger on a char but still complete it
467 // trick to trigger on a char but still complete it
463 return CodeMirror.Pass;
468 return CodeMirror.Pass;
464 }
469 }
465 };
470 };
466
471
467 var extraKeys = {
472 var extraKeys = {
468 "'@'": CodeMirrorCompleteAfter,
473 "'@'": CodeMirrorCompleteAfter,
469 Tab: function(cm) {
474 Tab: function(cm) {
470 // space indent instead of TABS
475 // space indent instead of TABS
471 var spaces = new Array(cm.getOption("indentUnit") + 1).join(" ");
476 var spaces = new Array(cm.getOption("indentUnit") + 1).join(" ");
472 cm.replaceSelection(spaces);
477 cm.replaceSelection(spaces);
473 }
478 }
474 };
479 };
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) {
484 // register triggerActions for this instance
491 // register triggerActions for this instance
485 extraKeys["'/'"] = completeActions(triggerActions);
492 extraKeys["'/'"] = completeActions(triggerActions);
486 }
493 }
487
494
488 var cm = CodeMirror.fromTextArea($(textAreaId).get(0), {
495 var cm = CodeMirror.fromTextArea($(textAreaId).get(0), {
489 lineNumbers: false,
496 lineNumbers: false,
490 indentUnit: 4,
497 indentUnit: 4,
491 viewportMargin: 30,
498 viewportMargin: 30,
492 // this is a trick to trigger some logic behind codemirror placeholder
499 // this is a trick to trigger some logic behind codemirror placeholder
493 // it influences styling and behaviour.
500 // it influences styling and behaviour.
494 placeholder: " ",
501 placeholder: " ",
495 extraKeys: extraKeys,
502 extraKeys: extraKeys,
496 lineWrapping: true
503 lineWrapping: true
497 });
504 });
498
505
499 cm.setSize(null, initialHeight);
506 cm.setSize(null, initialHeight);
500 cm.setOption("mode", DEFAULT_RENDERER);
507 cm.setOption("mode", DEFAULT_RENDERER);
501 CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode
508 CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode
502 cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER);
509 cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER);
503
510
504 // start listening on changes to make auto-expanded editor
511 // start listening on changes to make auto-expanded editor
505 cm.on("change", function (self) {
512 cm.on("change", function (self) {
506 var height = initialHeight;
513 var height = initialHeight;
507 var lines = self.lineCount();
514 var lines = self.lineCount();
508 if (lines > 6 && lines < 20) {
515 if (lines > 6 && lines < 20) {
509 height = "auto";
516 height = "auto";
510 } else if (lines >= 20) {
517 } else if (lines >= 20) {
511 height = 20 * 15;
518 height = 20 * 15;
512 }
519 }
513 self.setSize(null, height);
520 self.setSize(null, height);
514 });
521 });
515
522
516 var actionHint = function(editor, options) {
523 var actionHint = function(editor, options) {
517
524
518 var cur = editor.getCursor();
525 var cur = editor.getCursor();
519 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
526 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
520
527
521 // match only on /+1 character minimum
528 // match only on /+1 character minimum
522 var tokenMatch = new RegExp('(^/\|/\)([a-zA-Z]*)$').exec(curLine);
529 var tokenMatch = new RegExp('(^/\|/\)([a-zA-Z]*)$').exec(curLine);
523
530
524 var tokenStr = '';
531 var tokenStr = '';
525 if (tokenMatch !== null && tokenMatch.length > 0){
532 if (tokenMatch !== null && tokenMatch.length > 0){
526 tokenStr = tokenMatch[2].strip();
533 tokenStr = tokenMatch[2].strip();
527 }
534 }
528
535
529 var context = {
536 var context = {
530 start: (cur.ch - tokenStr.length) - 1,
537 start: (cur.ch - tokenStr.length) - 1,
531 end: cur.ch,
538 end: cur.ch,
532 string: tokenStr,
539 string: tokenStr,
533 type: null
540 type: null
534 };
541 };
535
542
536 return {
543 return {
537 list: filterActions(options.registeredActions, context),
544 list: filterActions(options.registeredActions, context),
538 from: CodeMirror.Pos(cur.line, context.start),
545 from: CodeMirror.Pos(cur.line, context.start),
539 to: CodeMirror.Pos(cur.line, context.end)
546 to: CodeMirror.Pos(cur.line, context.end)
540 };
547 };
541
548
542 };
549 };
543 CodeMirror.registerHelper("hint", "mentions", CodeMirrorMentionHint);
550 CodeMirror.registerHelper("hint", "mentions", CodeMirrorMentionHint);
544 CodeMirror.registerHelper("hint", "actions", actionHint);
551 CodeMirror.registerHelper("hint", "actions", actionHint);
545 return cm;
552 return cm;
546 };
553 };
547
554
548 var setCodeMirrorMode = function(codeMirrorInstance, mode) {
555 var setCodeMirrorMode = function(codeMirrorInstance, mode) {
549 CodeMirror.autoLoadMode(codeMirrorInstance, mode);
556 CodeMirror.autoLoadMode(codeMirrorInstance, mode);
550 codeMirrorInstance.setOption("mode", mode);
557 codeMirrorInstance.setOption("mode", mode);
551 };
558 };
552
559
553 var setCodeMirrorLineWrap = function(codeMirrorInstance, line_wrap) {
560 var setCodeMirrorLineWrap = function(codeMirrorInstance, line_wrap) {
554 codeMirrorInstance.setOption("lineWrapping", line_wrap);
561 codeMirrorInstance.setOption("lineWrapping", line_wrap);
555 };
562 };
556
563
557 var setCodeMirrorModeFromSelect = function(
564 var setCodeMirrorModeFromSelect = function(
558 targetSelect, targetFileInput, codeMirrorInstance, callback){
565 targetSelect, targetFileInput, codeMirrorInstance, callback){
559
566
560 $(targetSelect).on('change', function(e) {
567 $(targetSelect).on('change', function(e) {
561 cmLog.debug('codemirror select2 mode change event !');
568 cmLog.debug('codemirror select2 mode change event !');
562 var selected = e.currentTarget;
569 var selected = e.currentTarget;
563 var node = selected.options[selected.selectedIndex];
570 var node = selected.options[selected.selectedIndex];
564 var mimetype = node.value;
571 var mimetype = node.value;
565 cmLog.debug('picked mimetype', mimetype);
572 cmLog.debug('picked mimetype', mimetype);
566 var new_mode = $(node).attr('mode');
573 var new_mode = $(node).attr('mode');
567 setCodeMirrorMode(codeMirrorInstance, new_mode);
574 setCodeMirrorMode(codeMirrorInstance, new_mode);
568 cmLog.debug('set new mode', new_mode);
575 cmLog.debug('set new mode', new_mode);
569
576
570 //propose filename from picked mode
577 //propose filename from picked mode
571 cmLog.debug('setting mimetype', mimetype);
578 cmLog.debug('setting mimetype', mimetype);
572 var proposed_ext = getExtFromMimeType(mimetype);
579 var proposed_ext = getExtFromMimeType(mimetype);
573 cmLog.debug('file input', $(targetFileInput).val());
580 cmLog.debug('file input', $(targetFileInput).val());
574 var file_data = getFilenameAndExt($(targetFileInput).val());
581 var file_data = getFilenameAndExt($(targetFileInput).val());
575 var filename = file_data.filename || 'filename1';
582 var filename = file_data.filename || 'filename1';
576 $(targetFileInput).val(filename + proposed_ext);
583 $(targetFileInput).val(filename + proposed_ext);
577 cmLog.debug('proposed file', filename + proposed_ext);
584 cmLog.debug('proposed file', filename + proposed_ext);
578
585
579
586
580 if (typeof(callback) === 'function') {
587 if (typeof(callback) === 'function') {
581 try {
588 try {
582 cmLog.debug('running callback', callback);
589 cmLog.debug('running callback', callback);
583 callback(filename, mimetype, new_mode);
590 callback(filename, mimetype, new_mode);
584 } catch (err) {
591 } catch (err) {
585 console.log('failed to run callback', callback, err);
592 console.log('failed to run callback', callback, err);
586 }
593 }
587 }
594 }
588 cmLog.debug('finish iteration...');
595 cmLog.debug('finish iteration...');
589 });
596 });
590 };
597 };
591
598
592 var setCodeMirrorModeFromInput = function(
599 var setCodeMirrorModeFromInput = function(
593 targetSelect, targetFileInput, codeMirrorInstance, callback) {
600 targetSelect, targetFileInput, codeMirrorInstance, callback) {
594
601
595 // on type the new filename set mode
602 // on type the new filename set mode
596 $(targetFileInput).on('keyup', function(e) {
603 $(targetFileInput).on('keyup', function(e) {
597 var file_data = getFilenameAndExt(this.value);
604 var file_data = getFilenameAndExt(this.value);
598 if (file_data.ext === null) {
605 if (file_data.ext === null) {
599 return;
606 return;
600 }
607 }
601
608
602 var mimetypes = getMimeTypeFromExt(file_data.ext, true);
609 var mimetypes = getMimeTypeFromExt(file_data.ext, true);
603 cmLog.debug('mimetype from file', file_data, mimetypes);
610 cmLog.debug('mimetype from file', file_data, mimetypes);
604 var detected_mode;
611 var detected_mode;
605 var detected_option;
612 var detected_option;
606 for (var i in mimetypes) {
613 for (var i in mimetypes) {
607 var mt = mimetypes[i];
614 var mt = mimetypes[i];
608 if (!detected_mode) {
615 if (!detected_mode) {
609 detected_mode = detectCodeMirrorMode(this.value, mt);
616 detected_mode = detectCodeMirrorMode(this.value, mt);
610 }
617 }
611
618
612 if (!detected_option) {
619 if (!detected_option) {
613 cmLog.debug('#mimetype option[value="{0}"]'.format(mt));
620 cmLog.debug('#mimetype option[value="{0}"]'.format(mt));
614 if ($(targetSelect).find('option[value="{0}"]'.format(mt)).length) {
621 if ($(targetSelect).find('option[value="{0}"]'.format(mt)).length) {
615 detected_option = mt;
622 detected_option = mt;
616 }
623 }
617 }
624 }
618 }
625 }
619
626
620 cmLog.debug('detected mode', detected_mode);
627 cmLog.debug('detected mode', detected_mode);
621 cmLog.debug('detected option', detected_option);
628 cmLog.debug('detected option', detected_option);
622 if (detected_mode && detected_option){
629 if (detected_mode && detected_option){
623
630
624 $(targetSelect).select2("val", detected_option);
631 $(targetSelect).select2("val", detected_option);
625 setCodeMirrorMode(codeMirrorInstance, detected_mode);
632 setCodeMirrorMode(codeMirrorInstance, detected_mode);
626
633
627 if(typeof(callback) === 'function'){
634 if(typeof(callback) === 'function'){
628 try{
635 try{
629 cmLog.debug('running callback', callback);
636 cmLog.debug('running callback', callback);
630 var filename = file_data.filename + "." + file_data.ext;
637 var filename = file_data.filename + "." + file_data.ext;
631 callback(filename, detected_option, detected_mode);
638 callback(filename, detected_option, detected_mode);
632 }catch (err){
639 }catch (err){
633 console.log('failed to run callback', callback, err);
640 console.log('failed to run callback', callback, err);
634 }
641 }
635 }
642 }
636 }
643 }
637
644
638 });
645 });
639 };
646 };
640
647
641 var fillCodeMirrorOptions = function(targetSelect) {
648 var fillCodeMirrorOptions = function(targetSelect) {
642 //inject new modes, based on codeMirrors modeInfo object
649 //inject new modes, based on codeMirrors modeInfo object
643 var modes_select = $(targetSelect);
650 var modes_select = $(targetSelect);
644 for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
651 for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
645 var m = CodeMirror.modeInfo[i];
652 var m = CodeMirror.modeInfo[i];
646 var opt = new Option(m.name, m.mime);
653 var opt = new Option(m.name, m.mime);
647 $(opt).attr('mode', m.mode);
654 $(opt).attr('mode', m.mode);
648 modes_select.append(opt);
655 modes_select.append(opt);
649 }
656 }
650 };
657 };
651
658
652
659
653 /* markup form */
660 /* markup form */
654 (function(mod) {
661 (function(mod) {
655
662
656 if (typeof exports == "object" && typeof module == "object") {
663 if (typeof exports == "object" && typeof module == "object") {
657 // CommonJS
664 // CommonJS
658 module.exports = mod();
665 module.exports = mod();
659 }
666 }
660 else {
667 else {
661 // Plain browser env
668 // Plain browser env
662 (this || window).MarkupForm = mod();
669 (this || window).MarkupForm = mod();
663 }
670 }
664
671
665 })(function() {
672 })(function() {
666 "use strict";
673 "use strict";
667
674
668 function MarkupForm(textareaId) {
675 function MarkupForm(textareaId) {
669 if (!(this instanceof MarkupForm)) {
676 if (!(this instanceof MarkupForm)) {
670 return new MarkupForm(textareaId);
677 return new MarkupForm(textareaId);
671 }
678 }
672
679
673 // bind the element instance to our Form
680 // bind the element instance to our Form
674 $('#' + textareaId).get(0).MarkupForm = this;
681 $('#' + textareaId).get(0).MarkupForm = this;
675
682
676 this.withSelectorId = function(selector) {
683 this.withSelectorId = function(selector) {
677 var selectorId = textareaId;
684 var selectorId = textareaId;
678 return selector + '_' + selectorId;
685 return selector + '_' + selectorId;
679 };
686 };
680
687
681 this.previewButton = this.withSelectorId('#preview-btn');
688 this.previewButton = this.withSelectorId('#preview-btn');
682 this.previewContainer = this.withSelectorId('#preview-container');
689 this.previewContainer = this.withSelectorId('#preview-container');
683
690
684 this.previewBoxSelector = this.withSelectorId('#preview-box');
691 this.previewBoxSelector = this.withSelectorId('#preview-box');
685
692
686 this.editButton = this.withSelectorId('#edit-btn');
693 this.editButton = this.withSelectorId('#edit-btn');
687 this.editContainer = this.withSelectorId('#edit-container');
694 this.editContainer = this.withSelectorId('#edit-container');
688
695
689 this.cmBox = textareaId;
696 this.cmBox = textareaId;
690 this.cm = initMarkupCodeMirror('#' + textareaId);
697 this.cm = initMarkupCodeMirror('#' + textareaId);
691
698
692 this.previewUrl = pyroutes.url('markup_preview');
699 this.previewUrl = pyroutes.url('markup_preview');
693
700
694 // FUNCTIONS and helpers
701 // FUNCTIONS and helpers
695 var self = this;
702 var self = this;
696
703
697 this.getCmInstance = function(){
704 this.getCmInstance = function(){
698 return this.cm
705 return this.cm
699 };
706 };
700
707
701 this.setPlaceholder = function(placeholder) {
708 this.setPlaceholder = function(placeholder) {
702 var cm = this.getCmInstance();
709 var cm = this.getCmInstance();
703 if (cm){
710 if (cm){
704 cm.setOption('placeholder', placeholder);
711 cm.setOption('placeholder', placeholder);
705 }
712 }
706 };
713 };
707
714
708 this.initStatusChangeSelector = function(){
715 this.initStatusChangeSelector = function(){
709 var formatChangeStatus = function(state, escapeMarkup) {
716 var formatChangeStatus = function(state, escapeMarkup) {
710 var originalOption = state.element;
717 var originalOption = state.element;
711 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
718 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
712 return tmpl
719 return tmpl
713 };
720 };
714 var formatResult = function(result, container, query, escapeMarkup) {
721 var formatResult = function(result, container, query, escapeMarkup) {
715 return formatChangeStatus(result, escapeMarkup);
722 return formatChangeStatus(result, escapeMarkup);
716 };
723 };
717
724
718 var formatSelection = function(data, container, escapeMarkup) {
725 var formatSelection = function(data, container, escapeMarkup) {
719 return formatChangeStatus(data, escapeMarkup);
726 return formatChangeStatus(data, escapeMarkup);
720 };
727 };
721
728
722 $(this.submitForm).find(this.statusChange).select2({
729 $(this.submitForm).find(this.statusChange).select2({
723 placeholder: _gettext('Status Review'),
730 placeholder: _gettext('Status Review'),
724 formatResult: formatResult,
731 formatResult: formatResult,
725 formatSelection: formatSelection,
732 formatSelection: formatSelection,
726 containerCssClass: "drop-menu status_box_menu",
733 containerCssClass: "drop-menu status_box_menu",
727 dropdownCssClass: "drop-menu-dropdown",
734 dropdownCssClass: "drop-menu-dropdown",
728 dropdownAutoWidth: true,
735 dropdownAutoWidth: true,
729 minimumResultsForSearch: -1
736 minimumResultsForSearch: -1
730 });
737 });
731 $(this.submitForm).find(this.statusChange).on('change', function() {
738 $(this.submitForm).find(this.statusChange).on('change', function() {
732 var status = self.getCommentStatus();
739 var status = self.getCommentStatus();
733
740
734 if (status && !self.isInline()) {
741 if (status && !self.isInline()) {
735 $(self.submitButton).prop('disabled', false);
742 $(self.submitButton).prop('disabled', false);
736 }
743 }
737
744
738 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
745 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
739 self.setPlaceholder(placeholderText)
746 self.setPlaceholder(placeholderText)
740 })
747 })
741 };
748 };
742
749
743 // reset the text area into it's original state
750 // reset the text area into it's original state
744 this.resetMarkupFormState = function(content) {
751 this.resetMarkupFormState = function(content) {
745 content = content || '';
752 content = content || '';
746
753
747 $(this.editContainer).show();
754 $(this.editContainer).show();
748 $(this.editButton).parent().addClass('active');
755 $(this.editButton).parent().addClass('active');
749
756
750 $(this.previewContainer).hide();
757 $(this.previewContainer).hide();
751 $(this.previewButton).parent().removeClass('active');
758 $(this.previewButton).parent().removeClass('active');
752
759
753 this.setActionButtonsDisabled(true);
760 this.setActionButtonsDisabled(true);
754 self.cm.setValue(content);
761 self.cm.setValue(content);
755 self.cm.setOption("readOnly", false);
762 self.cm.setOption("readOnly", false);
756 };
763 };
757
764
758 this.previewSuccessCallback = function(o) {
765 this.previewSuccessCallback = function(o) {
759 $(self.previewBoxSelector).html(o);
766 $(self.previewBoxSelector).html(o);
760 $(self.previewBoxSelector).removeClass('unloaded');
767 $(self.previewBoxSelector).removeClass('unloaded');
761
768
762 // swap buttons, making preview active
769 // swap buttons, making preview active
763 $(self.previewButton).parent().addClass('active');
770 $(self.previewButton).parent().addClass('active');
764 $(self.editButton).parent().removeClass('active');
771 $(self.editButton).parent().removeClass('active');
765
772
766 // unlock buttons
773 // unlock buttons
767 self.setActionButtonsDisabled(false);
774 self.setActionButtonsDisabled(false);
768 };
775 };
769
776
770 this.setActionButtonsDisabled = function(state) {
777 this.setActionButtonsDisabled = function(state) {
771 $(this.editButton).prop('disabled', state);
778 $(this.editButton).prop('disabled', state);
772 $(this.previewButton).prop('disabled', state);
779 $(this.previewButton).prop('disabled', state);
773 };
780 };
774
781
775 // lock preview/edit/submit buttons on load, but exclude cancel button
782 // lock preview/edit/submit buttons on load, but exclude cancel button
776 var excludeCancelBtn = true;
783 var excludeCancelBtn = true;
777 this.setActionButtonsDisabled(true);
784 this.setActionButtonsDisabled(true);
778
785
779 // anonymous users don't have access to initialized CM instance
786 // anonymous users don't have access to initialized CM instance
780 if (this.cm !== undefined){
787 if (this.cm !== undefined){
781 this.cm.on('change', function(cMirror) {
788 this.cm.on('change', function(cMirror) {
782 if (cMirror.getValue() === "") {
789 if (cMirror.getValue() === "") {
783 self.setActionButtonsDisabled(true)
790 self.setActionButtonsDisabled(true)
784 } else {
791 } else {
785 self.setActionButtonsDisabled(false)
792 self.setActionButtonsDisabled(false)
786 }
793 }
787 });
794 });
788 }
795 }
789
796
790 $(this.editButton).on('click', function(e) {
797 $(this.editButton).on('click', function(e) {
791 e.preventDefault();
798 e.preventDefault();
792
799
793 $(self.previewButton).parent().removeClass('active');
800 $(self.previewButton).parent().removeClass('active');
794 $(self.previewContainer).hide();
801 $(self.previewContainer).hide();
795
802
796 $(self.editButton).parent().addClass('active');
803 $(self.editButton).parent().addClass('active');
797 $(self.editContainer).show();
804 $(self.editContainer).show();
798
805
799 });
806 });
800
807
801 $(this.previewButton).on('click', function(e) {
808 $(this.previewButton).on('click', function(e) {
802 e.preventDefault();
809 e.preventDefault();
803 var text = self.cm.getValue();
810 var text = self.cm.getValue();
804
811
805 if (text === "") {
812 if (text === "") {
806 return;
813 return;
807 }
814 }
808
815
809 var postData = {
816 var postData = {
810 'text': text,
817 'text': text,
811 'renderer': templateContext.visual.default_renderer,
818 'renderer': templateContext.visual.default_renderer,
812 'csrf_token': CSRF_TOKEN
819 'csrf_token': CSRF_TOKEN
813 };
820 };
814
821
815 // lock ALL buttons on preview
822 // lock ALL buttons on preview
816 self.setActionButtonsDisabled(true);
823 self.setActionButtonsDisabled(true);
817
824
818 $(self.previewBoxSelector).addClass('unloaded');
825 $(self.previewBoxSelector).addClass('unloaded');
819 $(self.previewBoxSelector).html(_gettext('Loading ...'));
826 $(self.previewBoxSelector).html(_gettext('Loading ...'));
820
827
821 $(self.editContainer).hide();
828 $(self.editContainer).hide();
822 $(self.previewContainer).show();
829 $(self.previewContainer).show();
823
830
824 // by default we reset state of comment preserving the text
831 // by default we reset state of comment preserving the text
825 var previewFailCallback = function(data){
832 var previewFailCallback = function(data){
826 alert(
833 alert(
827 "Error while submitting preview.\n" +
834 "Error while submitting preview.\n" +
828 "Error code {0} ({1}).".format(data.status, data.statusText)
835 "Error code {0} ({1}).".format(data.status, data.statusText)
829 );
836 );
830 self.resetMarkupFormState(text)
837 self.resetMarkupFormState(text)
831 };
838 };
832 _submitAjaxPOST(
839 _submitAjaxPOST(
833 self.previewUrl, postData, self.previewSuccessCallback,
840 self.previewUrl, postData, self.previewSuccessCallback,
834 previewFailCallback);
841 previewFailCallback);
835
842
836 $(self.previewButton).parent().addClass('active');
843 $(self.previewButton).parent().addClass('active');
837 $(self.editButton).parent().removeClass('active');
844 $(self.editButton).parent().removeClass('active');
838 });
845 });
839
846
840 }
847 }
841
848
842 return MarkupForm;
849 return MarkupForm;
843 });
850 });
@@ -1,1298 +1,1349 b''
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 var linkifyComments = function(comments) {
28 var linkifyComments = function(comments) {
29 var firstCommentId = null;
29 var firstCommentId = null;
30 if (comments) {
30 if (comments) {
31 firstCommentId = $(comments[0]).data('comment-id');
31 firstCommentId = $(comments[0]).data('comment-id');
32 }
32 }
33
33
34 if (firstCommentId){
34 if (firstCommentId){
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 }
36 }
37 };
37 };
38
38
39 var bindToggleButtons = function() {
39 var bindToggleButtons = function() {
40 $('.comment-toggle').on('click', function() {
40 $('.comment-toggle').on('click', function() {
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 });
42 });
43 };
43 };
44
44
45
45
46
46
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 failHandler = failHandler || function() {};
48 failHandler = failHandler || function() {};
49 postData = toQueryString(postData);
49 postData = toQueryString(postData);
50 var request = $.ajax({
50 var request = $.ajax({
51 url: url,
51 url: url,
52 type: 'POST',
52 type: 'POST',
53 data: postData,
53 data: postData,
54 headers: {'X-PARTIAL-XHR': true}
54 headers: {'X-PARTIAL-XHR': true}
55 })
55 })
56 .done(function (data) {
56 .done(function (data) {
57 successHandler(data);
57 successHandler(data);
58 })
58 })
59 .fail(function (data, textStatus, errorThrown) {
59 .fail(function (data, textStatus, errorThrown) {
60 failHandler(data, textStatus, errorThrown)
60 failHandler(data, textStatus, errorThrown)
61 });
61 });
62 return request;
62 return request;
63 };
63 };
64
64
65
65
66
66
67
67
68 /* Comment form for main and inline comments */
68 /* Comment form for main and inline comments */
69 (function(mod) {
69 (function(mod) {
70
70
71 if (typeof exports == "object" && typeof module == "object") {
71 if (typeof exports == "object" && typeof module == "object") {
72 // CommonJS
72 // CommonJS
73 module.exports = mod();
73 module.exports = mod();
74 }
74 }
75 else {
75 else {
76 // Plain browser env
76 // Plain browser env
77 (this || window).CommentForm = mod();
77 (this || window).CommentForm = mod();
78 }
78 }
79
79
80 })(function() {
80 })(function() {
81 "use strict";
81 "use strict";
82
82
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
84
84
85 if (!(this instanceof CommentForm)) {
85 if (!(this instanceof CommentForm)) {
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
87 }
87 }
88
88
89 // bind the element instance to our Form
89 // bind the element instance to our Form
90 $(formElement).get(0).CommentForm = this;
90 $(formElement).get(0).CommentForm = this;
91
91
92 this.withLineNo = function(selector) {
92 this.withLineNo = function(selector) {
93 var lineNo = this.lineNo;
93 var lineNo = this.lineNo;
94 if (lineNo === undefined) {
94 if (lineNo === undefined) {
95 return selector
95 return selector
96 } else {
96 } else {
97 return selector + '_' + lineNo;
97 return selector + '_' + lineNo;
98 }
98 }
99 };
99 };
100
100
101 this.commitId = commitId;
101 this.commitId = commitId;
102 this.pullRequestId = pullRequestId;
102 this.pullRequestId = pullRequestId;
103 this.lineNo = lineNo;
103 this.lineNo = lineNo;
104 this.initAutocompleteActions = initAutocompleteActions;
104 this.initAutocompleteActions = initAutocompleteActions;
105
105
106 this.previewButton = this.withLineNo('#preview-btn');
106 this.previewButton = this.withLineNo('#preview-btn');
107 this.previewContainer = this.withLineNo('#preview-container');
107 this.previewContainer = this.withLineNo('#preview-container');
108
108
109 this.previewBoxSelector = this.withLineNo('#preview-box');
109 this.previewBoxSelector = this.withLineNo('#preview-box');
110
110
111 this.editButton = this.withLineNo('#edit-btn');
111 this.editButton = this.withLineNo('#edit-btn');
112 this.editContainer = this.withLineNo('#edit-container');
112 this.editContainer = this.withLineNo('#edit-container');
113 this.cancelButton = this.withLineNo('#cancel-btn');
113 this.cancelButton = this.withLineNo('#cancel-btn');
114 this.commentType = this.withLineNo('#comment_type');
114 this.commentType = this.withLineNo('#comment_type');
115
115
116 this.resolvesId = null;
116 this.resolvesId = null;
117 this.resolvesActionId = null;
117 this.resolvesActionId = null;
118
118
119 this.closesPr = '#close_pull_request';
119 this.closesPr = '#close_pull_request';
120
120
121 this.cmBox = this.withLineNo('#text');
121 this.cmBox = this.withLineNo('#text');
122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
123
123
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 =
140 '';
144 '';
141 $(editInfo).insertBefore($(this.editButton).parent());
145 $(editInfo).insertBefore($(this.editButton).parent());
142 }
146 }
143
147
144 if (resolvesCommentId){
148 if (resolvesCommentId){
145 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
146 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
147 $(this.commentType).prop('disabled', true);
151 $(this.commentType).prop('disabled', true);
148 $(this.commentType).addClass('disabled');
152 $(this.commentType).addClass('disabled');
149
153
150 // disable select
154 // disable select
151 setTimeout(function() {
155 setTimeout(function() {
152 $(self.statusChange).select2('readonly', true);
156 $(self.statusChange).select2('readonly', true);
153 }, 10);
157 }, 10);
154
158
155 var resolvedInfo = (
159 var resolvedInfo = (
156 '<li class="resolve-action">' +
160 '<li class="resolve-action">' +
157 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
158 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
159 '</li>'
163 '</li>'
160 ).format(resolvesCommentId, _gettext('resolve comment'));
164 ).format(resolvesCommentId, _gettext('resolve comment'));
161 $(resolvedInfo).insertAfter($(this.commentType).parent());
165 $(resolvedInfo).insertAfter($(this.commentType).parent());
162 }
166 }
163
167
164 // based on commitId, or pullRequestId decide where do we submit
168 // based on commitId, or pullRequestId decide where do we submit
165 // out data
169 // out data
166 if (this.commitId){
170 if (this.commitId){
167 var pyurl = 'repo_commit_comment_create';
171 var pyurl = 'repo_commit_comment_create';
168 if(edit){
172 if(edit){
169 pyurl = 'repo_commit_comment_edit';
173 pyurl = 'repo_commit_comment_edit';
170 }
174 }
171 this.submitUrl = pyroutes.url(pyurl,
175 this.submitUrl = pyroutes.url(pyurl,
172 {'repo_name': templateContext.repo_name,
176 {'repo_name': templateContext.repo_name,
173 'commit_id': this.commitId,
177 'commit_id': this.commitId,
174 'comment_id': comment_id});
178 'comment_id': comment_id});
175 this.selfUrl = pyroutes.url('repo_commit',
179 this.selfUrl = pyroutes.url('repo_commit',
176 {'repo_name': templateContext.repo_name,
180 {'repo_name': templateContext.repo_name,
177 'commit_id': this.commitId});
181 'commit_id': this.commitId});
178
182
179 } else if (this.pullRequestId) {
183 } else if (this.pullRequestId) {
180 var pyurl = 'pullrequest_comment_create';
184 var pyurl = 'pullrequest_comment_create';
181 if(edit){
185 if(edit){
182 pyurl = 'pullrequest_comment_edit';
186 pyurl = 'pullrequest_comment_edit';
183 }
187 }
184 this.submitUrl = pyroutes.url(pyurl,
188 this.submitUrl = pyroutes.url(pyurl,
185 {'repo_name': templateContext.repo_name,
189 {'repo_name': templateContext.repo_name,
186 'pull_request_id': this.pullRequestId,
190 'pull_request_id': this.pullRequestId,
187 'comment_id': comment_id});
191 'comment_id': comment_id});
188 this.selfUrl = pyroutes.url('pullrequest_show',
192 this.selfUrl = pyroutes.url('pullrequest_show',
189 {'repo_name': templateContext.repo_name,
193 {'repo_name': templateContext.repo_name,
190 'pull_request_id': this.pullRequestId});
194 'pull_request_id': this.pullRequestId});
191
195
192 } else {
196 } else {
193 throw new Error(
197 throw new Error(
194 'CommentForm requires pullRequestId, or commitId to be specified.')
198 'CommentForm requires pullRequestId, or commitId to be specified.')
195 }
199 }
196
200
197 // FUNCTIONS and helpers
201 // FUNCTIONS and helpers
198 var self = this;
202 var self = this;
199
203
200 this.isInline = function(){
204 this.isInline = function(){
201 return this.lineNo && this.lineNo != 'general';
205 return this.lineNo && this.lineNo != 'general';
202 };
206 };
203
207
204 this.getCmInstance = function(){
208 this.getCmInstance = function(){
205 return this.cm
209 return this.cm
206 };
210 };
207
211
208 this.setPlaceholder = function(placeholder) {
212 this.setPlaceholder = function(placeholder) {
209 var cm = this.getCmInstance();
213 var cm = this.getCmInstance();
210 if (cm){
214 if (cm){
211 cm.setOption('placeholder', placeholder);
215 cm.setOption('placeholder', placeholder);
212 }
216 }
213 };
217 };
214
218
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 };
225
236
226 this.getClosePr = function() {
237 this.getClosePr = function() {
227 return $(this.submitForm).find(this.closesPr).val() || null;
238 return $(this.submitForm).find(this.closesPr).val() || null;
228 };
239 };
229
240
230 this.markCommentResolved = function(resolvedCommentId){
241 this.markCommentResolved = function(resolvedCommentId){
231 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
232 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
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(){
240 var formatChangeStatus = function(state, escapeMarkup) {
253 var formatChangeStatus = function(state, escapeMarkup) {
241 var originalOption = state.element;
254 var originalOption = state.element;
242 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
243 return tmpl
256 return tmpl
244 };
257 };
245 var formatResult = function(result, container, query, escapeMarkup) {
258 var formatResult = function(result, container, query, escapeMarkup) {
246 return formatChangeStatus(result, escapeMarkup);
259 return formatChangeStatus(result, escapeMarkup);
247 };
260 };
248
261
249 var formatSelection = function(data, container, escapeMarkup) {
262 var formatSelection = function(data, container, escapeMarkup) {
250 return formatChangeStatus(data, escapeMarkup);
263 return formatChangeStatus(data, escapeMarkup);
251 };
264 };
252
265
253 $(this.submitForm).find(this.statusChange).select2({
266 $(this.submitForm).find(this.statusChange).select2({
254 placeholder: _gettext('Status Review'),
267 placeholder: _gettext('Status Review'),
255 formatResult: formatResult,
268 formatResult: formatResult,
256 formatSelection: formatSelection,
269 formatSelection: formatSelection,
257 containerCssClass: "drop-menu status_box_menu",
270 containerCssClass: "drop-menu status_box_menu",
258 dropdownCssClass: "drop-menu-dropdown",
271 dropdownCssClass: "drop-menu-dropdown",
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);
270 self.setPlaceholder(placeholderText)
285 self.setPlaceholder(placeholderText)
271 })
286 })
272 };
287 };
273
288
274 // reset the comment form into it's original state
289 // reset the comment form into it's original state
275 this.resetCommentFormState = function(content) {
290 this.resetCommentFormState = function(content) {
276 content = content || '';
291 content = content || '';
277
292
278 $(this.editContainer).show();
293 $(this.editContainer).show();
279 $(this.editButton).parent().addClass('active');
294 $(this.editButton).parent().addClass('active');
280
295
281 $(this.previewContainer).hide();
296 $(this.previewContainer).hide();
282 $(this.previewButton).parent().removeClass('active');
297 $(this.previewButton).parent().removeClass('active');
283
298
284 this.setActionButtonsDisabled(true);
299 this.setActionButtonsDisabled(true);
285 self.cm.setValue(content);
300 self.cm.setValue(content);
286 self.cm.setOption("readOnly", false);
301 self.cm.setOption("readOnly", false);
287
302
288 if (this.resolvesId) {
303 if (this.resolvesId) {
289 // destroy the resolve action
304 // destroy the resolve action
290 $(this.resolvesId).parent().remove();
305 $(this.resolvesId).parent().remove();
291 }
306 }
292 // reset closingPR flag
307 // reset closingPR flag
293 $('.close-pr-input').remove();
308 $('.close-pr-input').remove();
294
309
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
305 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
306 return _submitAjaxPOST(url, postData, successHandler, failHandler);
321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
307 };
322 };
308
323
309 // overwrite a submitHandler, we need to do it for inline comments
324 // overwrite a submitHandler, we need to do it for inline comments
310 this.setHandleFormSubmit = function(callback) {
325 this.setHandleFormSubmit = function(callback) {
311 this.handleFormSubmit = callback;
326 this.handleFormSubmit = callback;
312 };
327 };
313
328
314 // overwrite a submitSuccessHandler
329 // overwrite a submitSuccessHandler
315 this.setGlobalSubmitSuccessCallback = function(callback) {
330 this.setGlobalSubmitSuccessCallback = function(callback) {
316 this.globalSubmitSuccessCallback = callback;
331 this.globalSubmitSuccessCallback = callback;
317 };
332 };
318
333
319 // default handler for for submit for main comments
334 // default handler for for submit for main comments
320 this.handleFormSubmit = function() {
335 this.handleFormSubmit = function() {
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
327 if (text === "" && !status) {
343 if (text === "" && !status) {
328 return;
344 return;
329 }
345 }
330
346
331 var excludeCancelBtn = false;
347 var excludeCancelBtn = false;
332 var submitEvent = true;
348 var submitEvent = true;
333 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
334 self.cm.setOption("readOnly", true);
350 self.cm.setOption("readOnly", true);
335
351
336 var postData = {
352 var postData = {
337 'text': text,
353 'text': text,
338 'changeset_status': status,
354 'changeset_status': status,
339 'comment_type': commentType,
355 'comment_type': commentType,
340 'csrf_token': CSRF_TOKEN
356 'csrf_token': CSRF_TOKEN
341 };
357 };
342
358
343 if (resolvesCommentId) {
359 if (resolvesCommentId) {
344 postData['resolves_comment_id'] = resolvesCommentId;
360 postData['resolves_comment_id'] = resolvesCommentId;
345 }
361 }
346
362
347 if (closePullRequest) {
363 if (closePullRequest) {
348 postData['close_pull_request'] = true;
364 postData['close_pull_request'] = true;
349 }
365 }
350
366
351 var submitSuccessCallback = function(o) {
367 var submitSuccessCallback = function(o) {
352 // reload page if we change status for single commit.
368 // reload page if we change status for single commit.
353 if (status && self.commitId) {
369 if (status && self.commitId) {
354 location.reload(true);
370 location.reload(true);
355 } else {
371 } else {
356 $('#injected_page_comments').append(o.rendered_text);
372 $('#injected_page_comments').append(o.rendered_text);
357 self.resetCommentFormState();
373 self.resetCommentFormState();
358 timeagoActivate();
374 timeagoActivate();
359 tooltipActivate();
375 tooltipActivate();
360
376
361 // mark visually which comment was resolved
377 // mark visually which comment was resolved
362 if (resolvesCommentId) {
378 if (resolvesCommentId) {
363 self.markCommentResolved(resolvesCommentId);
379 self.markCommentResolved(resolvesCommentId);
364 }
380 }
365 }
381 }
366
382
367 // run global callback on submit
383 // run global callback on submit
368 self.globalSubmitSuccessCallback();
384 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
369
385
370 };
386 };
371 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
387 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
372 var prefix = "Error while submitting comment.\n"
388 var prefix = "Error while submitting comment.\n"
373 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
389 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
374 ajaxErrorSwal(message);
390 ajaxErrorSwal(message);
375 self.resetCommentFormState(text);
391 self.resetCommentFormState(text);
376 };
392 };
377 self.submitAjaxPOST(
393 self.submitAjaxPOST(
378 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
394 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
379 };
395 };
380
396
381 this.previewSuccessCallback = function(o) {
397 this.previewSuccessCallback = function(o) {
382 $(self.previewBoxSelector).html(o);
398 $(self.previewBoxSelector).html(o);
383 $(self.previewBoxSelector).removeClass('unloaded');
399 $(self.previewBoxSelector).removeClass('unloaded');
384
400
385 // swap buttons, making preview active
401 // swap buttons, making preview active
386 $(self.previewButton).parent().addClass('active');
402 $(self.previewButton).parent().addClass('active');
387 $(self.editButton).parent().removeClass('active');
403 $(self.editButton).parent().removeClass('active');
388
404
389 // unlock buttons
405 // unlock buttons
390 self.setActionButtonsDisabled(false);
406 self.setActionButtonsDisabled(false);
391 };
407 };
392
408
393 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
409 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
394 excludeCancelBtn = excludeCancelBtn || false;
410 excludeCancelBtn = excludeCancelBtn || false;
395 submitEvent = submitEvent || false;
411 submitEvent = submitEvent || false;
396
412
397 $(this.editButton).prop('disabled', state);
413 $(this.editButton).prop('disabled', state);
398 $(this.previewButton).prop('disabled', state);
414 $(this.previewButton).prop('disabled', state);
399
415
400 if (!excludeCancelBtn) {
416 if (!excludeCancelBtn) {
401 $(this.cancelButton).prop('disabled', state);
417 $(this.cancelButton).prop('disabled', state);
402 }
418 }
403
419
404 var submitState = state;
420 var submitState = state;
405 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
421 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
406 // if the value of commit review status is set, we allow
422 // if the value of commit review status is set, we allow
407 // submit button, but only on Main form, isInline means inline
423 // submit button, but only on Main form, isInline means inline
408 submitState = false
424 submitState = false
409 }
425 }
410
426
411 $(this.submitButton).prop('disabled', submitState);
427 $(this.submitButton).prop('disabled', submitState);
428 $(this.submitDraftButton).prop('disabled', submitState);
429
412 if (submitEvent) {
430 if (submitEvent) {
413 $(this.submitButton).val(_gettext('Submitting...'));
431 var isDraft = self.getDraftState();
432
433 if (isDraft) {
434 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
435 } else {
436 $(this.submitButton).val(_gettext('Submitting...'));
437 }
438
414 } else {
439 } else {
415 $(this.submitButton).val(this.submitButtonText);
440 $(this.submitButton).val(this.submitButtonText);
441 $(this.submitDraftButton).val(this.submitDraftButtonText);
416 }
442 }
417
443
418 };
444 };
419
445
420 // lock preview/edit/submit buttons on load, but exclude cancel button
446 // lock preview/edit/submit buttons on load, but exclude cancel button
421 var excludeCancelBtn = true;
447 var excludeCancelBtn = true;
422 this.setActionButtonsDisabled(true, excludeCancelBtn);
448 this.setActionButtonsDisabled(true, excludeCancelBtn);
423
449
424 // anonymous users don't have access to initialized CM instance
450 // anonymous users don't have access to initialized CM instance
425 if (this.cm !== undefined){
451 if (this.cm !== undefined){
426 this.cm.on('change', function(cMirror) {
452 this.cm.on('change', function(cMirror) {
427 if (cMirror.getValue() === "") {
453 if (cMirror.getValue() === "") {
428 self.setActionButtonsDisabled(true, excludeCancelBtn)
454 self.setActionButtonsDisabled(true, excludeCancelBtn)
429 } else {
455 } else {
430 self.setActionButtonsDisabled(false, excludeCancelBtn)
456 self.setActionButtonsDisabled(false, excludeCancelBtn)
431 }
457 }
432 });
458 });
433 }
459 }
434
460
435 $(this.editButton).on('click', function(e) {
461 $(this.editButton).on('click', function(e) {
436 e.preventDefault();
462 e.preventDefault();
437
463
438 $(self.previewButton).parent().removeClass('active');
464 $(self.previewButton).parent().removeClass('active');
439 $(self.previewContainer).hide();
465 $(self.previewContainer).hide();
440
466
441 $(self.editButton).parent().addClass('active');
467 $(self.editButton).parent().addClass('active');
442 $(self.editContainer).show();
468 $(self.editContainer).show();
443
469
444 });
470 });
445
471
446 $(this.previewButton).on('click', function(e) {
472 $(this.previewButton).on('click', function(e) {
447 e.preventDefault();
473 e.preventDefault();
448 var text = self.cm.getValue();
474 var text = self.cm.getValue();
449
475
450 if (text === "") {
476 if (text === "") {
451 return;
477 return;
452 }
478 }
453
479
454 var postData = {
480 var postData = {
455 'text': text,
481 'text': text,
456 'renderer': templateContext.visual.default_renderer,
482 'renderer': templateContext.visual.default_renderer,
457 'csrf_token': CSRF_TOKEN
483 'csrf_token': CSRF_TOKEN
458 };
484 };
459
485
460 // lock ALL buttons on preview
486 // lock ALL buttons on preview
461 self.setActionButtonsDisabled(true);
487 self.setActionButtonsDisabled(true);
462
488
463 $(self.previewBoxSelector).addClass('unloaded');
489 $(self.previewBoxSelector).addClass('unloaded');
464 $(self.previewBoxSelector).html(_gettext('Loading ...'));
490 $(self.previewBoxSelector).html(_gettext('Loading ...'));
465
491
466 $(self.editContainer).hide();
492 $(self.editContainer).hide();
467 $(self.previewContainer).show();
493 $(self.previewContainer).show();
468
494
469 // by default we reset state of comment preserving the text
495 // by default we reset state of comment preserving the text
470 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
496 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
471 var prefix = "Error while preview of comment.\n"
497 var prefix = "Error while preview of comment.\n"
472 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
498 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
473 ajaxErrorSwal(message);
499 ajaxErrorSwal(message);
474
500
475 self.resetCommentFormState(text)
501 self.resetCommentFormState(text)
476 };
502 };
477 self.submitAjaxPOST(
503 self.submitAjaxPOST(
478 self.previewUrl, postData, self.previewSuccessCallback,
504 self.previewUrl, postData, self.previewSuccessCallback,
479 previewFailCallback);
505 previewFailCallback);
480
506
481 $(self.previewButton).parent().addClass('active');
507 $(self.previewButton).parent().addClass('active');
482 $(self.editButton).parent().removeClass('active');
508 $(self.editButton).parent().removeClass('active');
483 });
509 });
484
510
485 $(this.submitForm).submit(function(e) {
511 $(this.submitForm).submit(function(e) {
486 e.preventDefault();
512 e.preventDefault();
487 var allowedToSubmit = self.isAllowedToSubmit();
513 var allowedToSubmit = self.isAllowedToSubmit();
488 if (!allowedToSubmit){
514 if (!allowedToSubmit){
489 return false;
515 return false;
490 }
516 }
517
491 self.handleFormSubmit();
518 self.handleFormSubmit();
492 });
519 });
493
520
494 }
521 }
495
522
496 return CommentForm;
523 return CommentForm;
497 });
524 });
498
525
499 /* selector for comment versions */
526 /* selector for comment versions */
500 var initVersionSelector = function(selector, initialData) {
527 var initVersionSelector = function(selector, initialData) {
501
528
502 var formatResult = function(result, container, query, escapeMarkup) {
529 var formatResult = function(result, container, query, escapeMarkup) {
503
530
504 return renderTemplate('commentVersion', {
531 return renderTemplate('commentVersion', {
505 show_disabled: true,
532 show_disabled: true,
506 version: result.comment_version,
533 version: result.comment_version,
507 user_name: result.comment_author_username,
534 user_name: result.comment_author_username,
508 gravatar_url: result.comment_author_gravatar,
535 gravatar_url: result.comment_author_gravatar,
509 size: 16,
536 size: 16,
510 timeago_component: result.comment_created_on,
537 timeago_component: result.comment_created_on,
511 })
538 })
512 };
539 };
513
540
514 $(selector).select2({
541 $(selector).select2({
515 placeholder: "Edited",
542 placeholder: "Edited",
516 containerCssClass: "drop-menu-comment-history",
543 containerCssClass: "drop-menu-comment-history",
517 dropdownCssClass: "drop-menu-dropdown",
544 dropdownCssClass: "drop-menu-dropdown",
518 dropdownAutoWidth: true,
545 dropdownAutoWidth: true,
519 minimumResultsForSearch: -1,
546 minimumResultsForSearch: -1,
520 data: initialData,
547 data: initialData,
521 formatResult: formatResult,
548 formatResult: formatResult,
522 });
549 });
523
550
524 $(selector).on('select2-selecting', function (e) {
551 $(selector).on('select2-selecting', function (e) {
525 // hide the mast as we later do preventDefault()
552 // hide the mast as we later do preventDefault()
526 $("#select2-drop-mask").click();
553 $("#select2-drop-mask").click();
527 e.preventDefault();
554 e.preventDefault();
528 e.choice.action();
555 e.choice.action();
529 });
556 });
530
557
531 $(selector).on("select2-open", function() {
558 $(selector).on("select2-open", function() {
532 timeagoActivate();
559 timeagoActivate();
533 });
560 });
534 };
561 };
535
562
536 /* comments controller */
563 /* comments controller */
537 var CommentsController = function() {
564 var CommentsController = function() {
538 var mainComment = '#text';
565 var mainComment = '#text';
539 var self = this;
566 var self = this;
540
567
541 this.cancelComment = function (node) {
568 this.cancelComment = function (node) {
542 var $node = $(node);
569 var $node = $(node);
543 var edit = $(this).attr('edit');
570 var edit = $(this).attr('edit');
544 if (edit) {
571 if (edit) {
545 var $general_comments = null;
572 var $general_comments = null;
546 var $inline_comments = $node.closest('div.inline-comments');
573 var $inline_comments = $node.closest('div.inline-comments');
547 if (!$inline_comments.length) {
574 if (!$inline_comments.length) {
548 $general_comments = $('#comments');
575 $general_comments = $('#comments');
549 var $comment = $general_comments.parent().find('div.comment:hidden');
576 var $comment = $general_comments.parent().find('div.comment:hidden');
550 // show hidden general comment form
577 // show hidden general comment form
551 $('#cb-comment-general-form-placeholder').show();
578 $('#cb-comment-general-form-placeholder').show();
552 } else {
579 } else {
553 var $comment = $inline_comments.find('div.comment:hidden');
580 var $comment = $inline_comments.find('div.comment:hidden');
554 }
581 }
555 $comment.show();
582 $comment.show();
556 }
583 }
557 $node.closest('.comment-inline-form').remove();
584 $node.closest('.comment-inline-form').remove();
558 return false;
585 return false;
559 };
586 };
560
587
561 this.showVersion = function (comment_id, comment_history_id) {
588 this.showVersion = function (comment_id, comment_history_id) {
562
589
563 var historyViewUrl = pyroutes.url(
590 var historyViewUrl = pyroutes.url(
564 'repo_commit_comment_history_view',
591 'repo_commit_comment_history_view',
565 {
592 {
566 'repo_name': templateContext.repo_name,
593 'repo_name': templateContext.repo_name,
567 'commit_id': comment_id,
594 'commit_id': comment_id,
568 'comment_history_id': comment_history_id,
595 'comment_history_id': comment_history_id,
569 }
596 }
570 );
597 );
571 successRenderCommit = function (data) {
598 successRenderCommit = function (data) {
572 SwalNoAnimation.fire({
599 SwalNoAnimation.fire({
573 html: data,
600 html: data,
574 title: '',
601 title: '',
575 });
602 });
576 };
603 };
577 failRenderCommit = function () {
604 failRenderCommit = function () {
578 SwalNoAnimation.fire({
605 SwalNoAnimation.fire({
579 html: 'Error while loading comment history',
606 html: 'Error while loading comment history',
580 title: '',
607 title: '',
581 });
608 });
582 };
609 };
583 _submitAjaxPOST(
610 _submitAjaxPOST(
584 historyViewUrl, {'csrf_token': CSRF_TOKEN},
611 historyViewUrl, {'csrf_token': CSRF_TOKEN},
585 successRenderCommit,
612 successRenderCommit,
586 failRenderCommit
613 failRenderCommit
587 );
614 );
588 };
615 };
589
616
590 this.getLineNumber = function(node) {
617 this.getLineNumber = function(node) {
591 var $node = $(node);
618 var $node = $(node);
592 var lineNo = $node.closest('td').attr('data-line-no');
619 var lineNo = $node.closest('td').attr('data-line-no');
593 if (lineNo === undefined && $node.data('commentInline')){
620 if (lineNo === undefined && $node.data('commentInline')){
594 lineNo = $node.data('commentLineNo')
621 lineNo = $node.data('commentLineNo')
595 }
622 }
596
623
597 return lineNo
624 return lineNo
598 };
625 };
599
626
600 this.scrollToComment = function(node, offset, outdated) {
627 this.scrollToComment = function(node, offset, outdated) {
601 if (offset === undefined) {
628 if (offset === undefined) {
602 offset = 0;
629 offset = 0;
603 }
630 }
604 var outdated = outdated || false;
631 var outdated = outdated || false;
605 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
632 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
606
633
607 if (!node) {
634 if (!node) {
608 node = $('.comment-selected');
635 node = $('.comment-selected');
609 if (!node.length) {
636 if (!node.length) {
610 node = $('comment-current')
637 node = $('comment-current')
611 }
638 }
612 }
639 }
613
640
614 $wrapper = $(node).closest('div.comment');
641 $wrapper = $(node).closest('div.comment');
615
642
616 // show hidden comment when referenced.
643 // show hidden comment when referenced.
617 if (!$wrapper.is(':visible')){
644 if (!$wrapper.is(':visible')){
618 $wrapper.show();
645 $wrapper.show();
619 }
646 }
620
647
621 $comment = $(node).closest(klass);
648 $comment = $(node).closest(klass);
622 $comments = $(klass);
649 $comments = $(klass);
623
650
624 $('.comment-selected').removeClass('comment-selected');
651 $('.comment-selected').removeClass('comment-selected');
625
652
626 var nextIdx = $(klass).index($comment) + offset;
653 var nextIdx = $(klass).index($comment) + offset;
627 if (nextIdx >= $comments.length) {
654 if (nextIdx >= $comments.length) {
628 nextIdx = 0;
655 nextIdx = 0;
629 }
656 }
630 var $next = $(klass).eq(nextIdx);
657 var $next = $(klass).eq(nextIdx);
631
658
632 var $cb = $next.closest('.cb');
659 var $cb = $next.closest('.cb');
633 $cb.removeClass('cb-collapsed');
660 $cb.removeClass('cb-collapsed');
634
661
635 var $filediffCollapseState = $cb.closest('.filediff').prev();
662 var $filediffCollapseState = $cb.closest('.filediff').prev();
636 $filediffCollapseState.prop('checked', false);
663 $filediffCollapseState.prop('checked', false);
637 $next.addClass('comment-selected');
664 $next.addClass('comment-selected');
638 scrollToElement($next);
665 scrollToElement($next);
639 return false;
666 return false;
640 };
667 };
641
668
642 this.nextComment = function(node) {
669 this.nextComment = function(node) {
643 return self.scrollToComment(node, 1);
670 return self.scrollToComment(node, 1);
644 };
671 };
645
672
646 this.prevComment = function(node) {
673 this.prevComment = function(node) {
647 return self.scrollToComment(node, -1);
674 return self.scrollToComment(node, -1);
648 };
675 };
649
676
650 this.nextOutdatedComment = function(node) {
677 this.nextOutdatedComment = function(node) {
651 return self.scrollToComment(node, 1, true);
678 return self.scrollToComment(node, 1, true);
652 };
679 };
653
680
654 this.prevOutdatedComment = function(node) {
681 this.prevOutdatedComment = function(node) {
655 return self.scrollToComment(node, -1, true);
682 return self.scrollToComment(node, -1, true);
656 };
683 };
657
684
658 this._deleteComment = function(node) {
685 this._deleteComment = function(node) {
659 var $node = $(node);
686 var $node = $(node);
660 var $td = $node.closest('td');
687 var $td = $node.closest('td');
661 var $comment = $node.closest('.comment');
688 var $comment = $node.closest('.comment');
662 var comment_id = $comment.attr('data-comment-id');
689 var comment_id = $($comment).data('commentId');
690 var isDraft = $($comment).data('commentDraft');
663 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
691 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
664 var postData = {
692 var postData = {
665 'csrf_token': CSRF_TOKEN
693 'csrf_token': CSRF_TOKEN
666 };
694 };
667
695
668 $comment.addClass('comment-deleting');
696 $comment.addClass('comment-deleting');
669 $comment.hide('fast');
697 $comment.hide('fast');
670
698
671 var success = function(response) {
699 var success = function(response) {
672 $comment.remove();
700 $comment.remove();
673
701
674 if (window.updateSticky !== undefined) {
702 if (window.updateSticky !== undefined) {
675 // potentially our comments change the active window size, so we
703 // potentially our comments change the active window size, so we
676 // notify sticky elements
704 // notify sticky elements
677 updateSticky()
705 updateSticky()
678 }
706 }
679
707
680 if (window.refreshAllComments !== undefined) {
708 if (window.refreshAllComments !== undefined && !isDraft) {
681 // if we have this handler, run it, and refresh all comments boxes
709 // if we have this handler, run it, and refresh all comments boxes
682 refreshAllComments()
710 refreshAllComments()
683 }
711 }
684 return false;
712 return false;
685 };
713 };
686
714
687 var failure = function(jqXHR, textStatus, errorThrown) {
715 var failure = function(jqXHR, textStatus, errorThrown) {
688 var prefix = "Error while deleting this comment.\n"
716 var prefix = "Error while deleting this comment.\n"
689 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
717 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
690 ajaxErrorSwal(message);
718 ajaxErrorSwal(message);
691
719
692 $comment.show('fast');
720 $comment.show('fast');
693 $comment.removeClass('comment-deleting');
721 $comment.removeClass('comment-deleting');
694 return false;
722 return false;
695 };
723 };
696 ajaxPOST(url, postData, success, failure);
724 ajaxPOST(url, postData, success, failure);
697
725
698
726
699
727
700 }
728 }
701
729
702 this.deleteComment = function(node) {
730 this.deleteComment = function(node) {
703 var $comment = $(node).closest('.comment');
731 var $comment = $(node).closest('.comment');
704 var comment_id = $comment.attr('data-comment-id');
732 var comment_id = $comment.attr('data-comment-id');
705
733
706 SwalNoAnimation.fire({
734 SwalNoAnimation.fire({
707 title: 'Delete this comment?',
735 title: 'Delete this comment?',
708 icon: 'warning',
736 icon: 'warning',
709 showCancelButton: true,
737 showCancelButton: true,
710 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
738 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
711
739
712 }).then(function(result) {
740 }).then(function(result) {
713 if (result.value) {
741 if (result.value) {
714 self._deleteComment(node);
742 self._deleteComment(node);
715 }
743 }
716 })
744 })
717 };
745 };
718
746
747 this._finalizeDrafts = function(commentIds) {
748 window.finalizeDrafts(commentIds)
749 }
750
751 this.finalizeDrafts = function(commentIds) {
752
753 SwalNoAnimation.fire({
754 title: _ngettext('Submit {0} draft comment', 'Submit {0} draft comments', commentIds.length).format(commentIds.length),
755 icon: 'warning',
756 showCancelButton: true,
757 confirmButtonText: _gettext('Yes, finalize drafts'),
758
759 }).then(function(result) {
760 if (result.value) {
761 self._finalizeDrafts(commentIds);
762 }
763 })
764 };
765
719 this.toggleWideMode = function (node) {
766 this.toggleWideMode = function (node) {
720 if ($('#content').hasClass('wrapper')) {
767 if ($('#content').hasClass('wrapper')) {
721 $('#content').removeClass("wrapper");
768 $('#content').removeClass("wrapper");
722 $('#content').addClass("wide-mode-wrapper");
769 $('#content').addClass("wide-mode-wrapper");
723 $(node).addClass('btn-success');
770 $(node).addClass('btn-success');
724 return true
771 return true
725 } else {
772 } else {
726 $('#content').removeClass("wide-mode-wrapper");
773 $('#content').removeClass("wide-mode-wrapper");
727 $('#content').addClass("wrapper");
774 $('#content').addClass("wrapper");
728 $(node).removeClass('btn-success');
775 $(node).removeClass('btn-success');
729 return false
776 return false
730 }
777 }
731
778
732 };
779 };
733
780
734 this.toggleComments = function(node, show) {
781 this.toggleComments = function(node, show) {
735 var $filediff = $(node).closest('.filediff');
782 var $filediff = $(node).closest('.filediff');
736 if (show === true) {
783 if (show === true) {
737 $filediff.removeClass('hide-comments');
784 $filediff.removeClass('hide-comments');
738 } else if (show === false) {
785 } else if (show === false) {
739 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
786 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
740 $filediff.addClass('hide-comments');
787 $filediff.addClass('hide-comments');
741 } else {
788 } else {
742 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
789 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
743 $filediff.toggleClass('hide-comments');
790 $filediff.toggleClass('hide-comments');
744 }
791 }
745
792
746 // since we change the height of the diff container that has anchor points for upper
793 // since we change the height of the diff container that has anchor points for upper
747 // sticky header, we need to tell it to re-calculate those
794 // sticky header, we need to tell it to re-calculate those
748 if (window.updateSticky !== undefined) {
795 if (window.updateSticky !== undefined) {
749 // potentially our comments change the active window size, so we
796 // potentially our comments change the active window size, so we
750 // notify sticky elements
797 // notify sticky elements
751 updateSticky()
798 updateSticky()
752 }
799 }
753
800
754 return false;
801 return false;
755 };
802 };
756
803
757 this.toggleLineComments = function(node) {
804 this.toggleLineComments = function(node) {
758 self.toggleComments(node, true);
805 self.toggleComments(node, true);
759 var $node = $(node);
806 var $node = $(node);
760 // mark outdated comments as visible before the toggle;
807 // mark outdated comments as visible before the toggle;
761 $(node.closest('tr')).find('.comment-outdated').show();
808 $(node.closest('tr')).find('.comment-outdated').show();
762 $node.closest('tr').toggleClass('hide-line-comments');
809 $node.closest('tr').toggleClass('hide-line-comments');
763 };
810 };
764
811
765 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
812 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
766 var pullRequestId = templateContext.pull_request_data.pull_request_id;
813 var pullRequestId = templateContext.pull_request_data.pull_request_id;
767 var commitId = templateContext.commit_data.commit_id;
814 var commitId = templateContext.commit_data.commit_id;
768
815
769 var commentForm = new CommentForm(
816 var commentForm = new CommentForm(
770 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
817 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
771 var cm = commentForm.getCmInstance();
818 var cm = commentForm.getCmInstance();
772
819
773 if (resolvesCommentId){
820 if (resolvesCommentId){
774 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
821 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
775 }
822 }
776
823
777 setTimeout(function() {
824 setTimeout(function() {
778 // callbacks
825 // callbacks
779 if (cm !== undefined) {
826 if (cm !== undefined) {
780 commentForm.setPlaceholder(placeholderText);
827 commentForm.setPlaceholder(placeholderText);
781 if (commentForm.isInline()) {
828 if (commentForm.isInline()) {
782 cm.focus();
829 cm.focus();
783 cm.refresh();
830 cm.refresh();
784 }
831 }
785 }
832 }
786 }, 10);
833 }, 10);
787
834
788 // trigger scrolldown to the resolve comment, since it might be away
835 // trigger scrolldown to the resolve comment, since it might be away
789 // from the clicked
836 // from the clicked
790 if (resolvesCommentId){
837 if (resolvesCommentId){
791 var actionNode = $(commentForm.resolvesActionId).offset();
838 var actionNode = $(commentForm.resolvesActionId).offset();
792
839
793 setTimeout(function() {
840 setTimeout(function() {
794 if (actionNode) {
841 if (actionNode) {
795 $('body, html').animate({scrollTop: actionNode.top}, 10);
842 $('body, html').animate({scrollTop: actionNode.top}, 10);
796 }
843 }
797 }, 100);
844 }, 100);
798 }
845 }
799
846
800 // add dropzone support
847 // add dropzone support
801 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
848 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
802 var renderer = templateContext.visual.default_renderer;
849 var renderer = templateContext.visual.default_renderer;
803 if (renderer == 'rst') {
850 if (renderer == 'rst') {
804 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
851 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
805 if (isRendered){
852 if (isRendered){
806 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
853 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
807 }
854 }
808 } else if (renderer == 'markdown') {
855 } else if (renderer == 'markdown') {
809 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
856 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
810 if (isRendered){
857 if (isRendered){
811 attachmentUrl = '!' + attachmentUrl;
858 attachmentUrl = '!' + attachmentUrl;
812 }
859 }
813 } else {
860 } else {
814 var attachmentUrl = '{}'.format(attachmentStoreUrl);
861 var attachmentUrl = '{}'.format(attachmentStoreUrl);
815 }
862 }
816 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
863 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
817
864
818 return false;
865 return false;
819 };
866 };
820
867
821 //see: https://www.dropzonejs.com/#configuration
868 //see: https://www.dropzonejs.com/#configuration
822 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
869 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
823 {'repo_name': templateContext.repo_name,
870 {'repo_name': templateContext.repo_name,
824 'commit_id': templateContext.commit_data.commit_id})
871 'commit_id': templateContext.commit_data.commit_id})
825
872
826 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
873 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
827 if (previewTmpl !== undefined){
874 if (previewTmpl !== undefined){
828 var selectLink = $(formElement).find('.pick-attachment').get(0);
875 var selectLink = $(formElement).find('.pick-attachment').get(0);
829 $(formElement).find('.comment-attachment-uploader').dropzone({
876 $(formElement).find('.comment-attachment-uploader').dropzone({
830 url: storeUrl,
877 url: storeUrl,
831 headers: {"X-CSRF-Token": CSRF_TOKEN},
878 headers: {"X-CSRF-Token": CSRF_TOKEN},
832 paramName: function () {
879 paramName: function () {
833 return "attachment"
880 return "attachment"
834 }, // The name that will be used to transfer the file
881 }, // The name that will be used to transfer the file
835 clickable: selectLink,
882 clickable: selectLink,
836 parallelUploads: 1,
883 parallelUploads: 1,
837 maxFiles: 10,
884 maxFiles: 10,
838 maxFilesize: templateContext.attachment_store.max_file_size_mb,
885 maxFilesize: templateContext.attachment_store.max_file_size_mb,
839 uploadMultiple: false,
886 uploadMultiple: false,
840 autoProcessQueue: true, // if false queue will not be processed automatically.
887 autoProcessQueue: true, // if false queue will not be processed automatically.
841 createImageThumbnails: false,
888 createImageThumbnails: false,
842 previewTemplate: previewTmpl.innerHTML,
889 previewTemplate: previewTmpl.innerHTML,
843
890
844 accept: function (file, done) {
891 accept: function (file, done) {
845 done();
892 done();
846 },
893 },
847 init: function () {
894 init: function () {
848
895
849 this.on("sending", function (file, xhr, formData) {
896 this.on("sending", function (file, xhr, formData) {
850 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
897 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
851 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
898 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
852 });
899 });
853
900
854 this.on("success", function (file, response) {
901 this.on("success", function (file, response) {
855 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
902 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
856 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
903 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
857
904
858 var isRendered = false;
905 var isRendered = false;
859 var ext = file.name.split('.').pop();
906 var ext = file.name.split('.').pop();
860 var imageExts = templateContext.attachment_store.image_ext;
907 var imageExts = templateContext.attachment_store.image_ext;
861 if (imageExts.indexOf(ext) !== -1){
908 if (imageExts.indexOf(ext) !== -1){
862 isRendered = true;
909 isRendered = true;
863 }
910 }
864
911
865 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
912 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
866 });
913 });
867
914
868 this.on("error", function (file, errorMessage, xhr) {
915 this.on("error", function (file, errorMessage, xhr) {
869 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
916 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
870
917
871 var error = null;
918 var error = null;
872
919
873 if (xhr !== undefined){
920 if (xhr !== undefined){
874 var httpStatus = xhr.status + " " + xhr.statusText;
921 var httpStatus = xhr.status + " " + xhr.statusText;
875 if (xhr !== undefined && xhr.status >= 500) {
922 if (xhr !== undefined && xhr.status >= 500) {
876 error = httpStatus;
923 error = httpStatus;
877 }
924 }
878 }
925 }
879
926
880 if (error === null) {
927 if (error === null) {
881 error = errorMessage.error || errorMessage || httpStatus;
928 error = errorMessage.error || errorMessage || httpStatus;
882 }
929 }
883 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
930 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
884
931
885 });
932 });
886 }
933 }
887 });
934 });
888 }
935 }
889 return commentForm;
936 return commentForm;
890 };
937 };
891
938
892 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
939 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
893
940
894 var tmpl = $('#cb-comment-general-form-template').html();
941 var tmpl = $('#cb-comment-general-form-template').html();
895 tmpl = tmpl.format(null, 'general');
942 tmpl = tmpl.format(null, 'general');
896 var $form = $(tmpl);
943 var $form = $(tmpl);
897
944
898 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
945 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
899 var curForm = $formPlaceholder.find('form');
946 var curForm = $formPlaceholder.find('form');
900 if (curForm){
947 if (curForm){
901 curForm.remove();
948 curForm.remove();
902 }
949 }
903 $formPlaceholder.append($form);
950 $formPlaceholder.append($form);
904
951
905 var _form = $($form[0]);
952 var _form = $($form[0]);
906 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
953 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
907 var edit = false;
954 var edit = false;
908 var comment_id = null;
955 var comment_id = null;
909 var commentForm = this.createCommentForm(
956 var commentForm = this.createCommentForm(
910 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
957 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
911 commentForm.initStatusChangeSelector();
958 commentForm.initStatusChangeSelector();
912
959
913 return commentForm;
960 return commentForm;
914 };
961 };
915
962
916 this.editComment = function(node) {
963 this.editComment = function(node) {
917 var $node = $(node);
964 var $node = $(node);
918 var $comment = $(node).closest('.comment');
965 var $comment = $(node).closest('.comment');
919 var comment_id = $comment.attr('data-comment-id');
966 var comment_id = $($comment).data('commentId');
967 var isDraft = $($comment).data('commentDraft');
920 var $form = null
968 var $form = null
921
969
922 var $comments = $node.closest('div.inline-comments');
970 var $comments = $node.closest('div.inline-comments');
923 var $general_comments = null;
971 var $general_comments = null;
924 var lineno = null;
972 var lineno = null;
925
973
926 if($comments.length){
974 if($comments.length){
927 // inline comments setup
975 // inline comments setup
928 $form = $comments.find('.comment-inline-form');
976 $form = $comments.find('.comment-inline-form');
929 lineno = self.getLineNumber(node)
977 lineno = self.getLineNumber(node)
930 }
978 }
931 else{
979 else{
932 // general comments setup
980 // general comments setup
933 $comments = $('#comments');
981 $comments = $('#comments');
934 $form = $comments.find('.comment-inline-form');
982 $form = $comments.find('.comment-inline-form');
935 lineno = $comment[0].id
983 lineno = $comment[0].id
936 $('#cb-comment-general-form-placeholder').hide();
984 $('#cb-comment-general-form-placeholder').hide();
937 }
985 }
938
986
939 this.edit = true;
987 this.edit = true;
940
988
941 if (!$form.length) {
989 if (!$form.length) {
942
990
943 var $filediff = $node.closest('.filediff');
991 var $filediff = $node.closest('.filediff');
944 $filediff.removeClass('hide-comments');
992 $filediff.removeClass('hide-comments');
945 var f_path = $filediff.attr('data-f-path');
993 var f_path = $filediff.attr('data-f-path');
946
994
947 // create a new HTML from template
995 // create a new HTML from template
948
996
949 var tmpl = $('#cb-comment-inline-form-template').html();
997 var tmpl = $('#cb-comment-inline-form-template').html();
950 tmpl = tmpl.format(escapeHtml(f_path), lineno);
998 tmpl = tmpl.format(escapeHtml(f_path), lineno);
951 $form = $(tmpl);
999 $form = $(tmpl);
952 $comment.after($form)
1000 $comment.after($form)
953
1001
954 var _form = $($form[0]).find('form');
1002 var _form = $($form[0]).find('form');
955 var autocompleteActions = ['as_note',];
1003 var autocompleteActions = ['as_note',];
956 var commentForm = this.createCommentForm(
1004 var commentForm = this.createCommentForm(
957 _form, lineno, '', autocompleteActions, resolvesCommentId,
1005 _form, lineno, '', autocompleteActions, resolvesCommentId,
958 this.edit, comment_id);
1006 this.edit, comment_id);
959 var old_comment_text_binary = $comment.attr('data-comment-text');
1007 var old_comment_text_binary = $comment.attr('data-comment-text');
960 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1008 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
961 commentForm.cm.setValue(old_comment_text);
1009 commentForm.cm.setValue(old_comment_text);
962 $comment.hide();
1010 $comment.hide();
963
1011
964 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
1012 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
965 form: _form,
1013 form: _form,
966 parent: $comments,
1014 parent: $comments,
967 lineno: lineno,
1015 lineno: lineno,
968 f_path: f_path}
1016 f_path: f_path}
969 );
1017 );
970
1018
971 // set a CUSTOM submit handler for inline comments.
1019 // set a CUSTOM submit handler for inline comments.
972 commentForm.setHandleFormSubmit(function(o) {
1020 commentForm.setHandleFormSubmit(function(o) {
973 var text = commentForm.cm.getValue();
1021 var text = commentForm.cm.getValue();
974 var commentType = commentForm.getCommentType();
1022 var commentType = commentForm.getCommentType();
975
1023
976 if (text === "") {
1024 if (text === "") {
977 return;
1025 return;
978 }
1026 }
979
1027
980 if (old_comment_text == text) {
1028 if (old_comment_text == text) {
981 SwalNoAnimation.fire({
1029 SwalNoAnimation.fire({
982 title: 'Unable to edit comment',
1030 title: 'Unable to edit comment',
983 html: _gettext('Comment body was not changed.'),
1031 html: _gettext('Comment body was not changed.'),
984 });
1032 });
985 return;
1033 return;
986 }
1034 }
987 var excludeCancelBtn = false;
1035 var excludeCancelBtn = false;
988 var submitEvent = true;
1036 var submitEvent = true;
989 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1037 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
990 commentForm.cm.setOption("readOnly", true);
1038 commentForm.cm.setOption("readOnly", true);
991
1039
992 // Read last version known
1040 // Read last version known
993 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1041 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
994 var version = versionSelector.data('lastVersion');
1042 var version = versionSelector.data('lastVersion');
995
1043
996 if (!version) {
1044 if (!version) {
997 version = 0;
1045 version = 0;
998 }
1046 }
999
1047
1000 var postData = {
1048 var postData = {
1001 'text': text,
1049 'text': text,
1002 'f_path': f_path,
1050 'f_path': f_path,
1003 'line': lineno,
1051 'line': lineno,
1004 'comment_type': commentType,
1052 'comment_type': commentType,
1053 'draft': isDraft,
1005 'version': version,
1054 'version': version,
1006 'csrf_token': CSRF_TOKEN
1055 'csrf_token': CSRF_TOKEN
1007 };
1056 };
1008
1057
1009 var submitSuccessCallback = function(json_data) {
1058 var submitSuccessCallback = function(json_data) {
1010 $form.remove();
1059 $form.remove();
1011 $comment.show();
1060 $comment.show();
1012 var postData = {
1061 var postData = {
1013 'text': text,
1062 'text': text,
1014 'renderer': $comment.attr('data-comment-renderer'),
1063 'renderer': $comment.attr('data-comment-renderer'),
1015 'csrf_token': CSRF_TOKEN
1064 'csrf_token': CSRF_TOKEN
1016 };
1065 };
1017
1066
1018 /* Inject new edited version selector */
1067 /* Inject new edited version selector */
1019 var updateCommentVersionDropDown = function () {
1068 var updateCommentVersionDropDown = function () {
1020 var versionSelectId = '#comment_versions_'+comment_id;
1069 var versionSelectId = '#comment_versions_'+comment_id;
1021 var preLoadVersionData = [
1070 var preLoadVersionData = [
1022 {
1071 {
1023 id: json_data['comment_version'],
1072 id: json_data['comment_version'],
1024 text: "v{0}".format(json_data['comment_version']),
1073 text: "v{0}".format(json_data['comment_version']),
1025 action: function () {
1074 action: function () {
1026 Rhodecode.comments.showVersion(
1075 Rhodecode.comments.showVersion(
1027 json_data['comment_id'],
1076 json_data['comment_id'],
1028 json_data['comment_history_id']
1077 json_data['comment_history_id']
1029 )
1078 )
1030 },
1079 },
1031 comment_version: json_data['comment_version'],
1080 comment_version: json_data['comment_version'],
1032 comment_author_username: json_data['comment_author_username'],
1081 comment_author_username: json_data['comment_author_username'],
1033 comment_author_gravatar: json_data['comment_author_gravatar'],
1082 comment_author_gravatar: json_data['comment_author_gravatar'],
1034 comment_created_on: json_data['comment_created_on'],
1083 comment_created_on: json_data['comment_created_on'],
1035 },
1084 },
1036 ]
1085 ]
1037
1086
1038
1087
1039 if ($(versionSelectId).data('select2')) {
1088 if ($(versionSelectId).data('select2')) {
1040 var oldData = $(versionSelectId).data('select2').opts.data.results;
1089 var oldData = $(versionSelectId).data('select2').opts.data.results;
1041 $(versionSelectId).select2("destroy");
1090 $(versionSelectId).select2("destroy");
1042 preLoadVersionData = oldData.concat(preLoadVersionData)
1091 preLoadVersionData = oldData.concat(preLoadVersionData)
1043 }
1092 }
1044
1093
1045 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1094 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1046
1095
1047 $comment.attr('data-comment-text', utf8ToB64(text));
1096 $comment.attr('data-comment-text', utf8ToB64(text));
1048
1097
1049 var versionSelector = $('#comment_versions_'+comment_id);
1098 var versionSelector = $('#comment_versions_'+comment_id);
1050
1099
1051 // set lastVersion so we know our last edit version
1100 // set lastVersion so we know our last edit version
1052 versionSelector.data('lastVersion', json_data['comment_version'])
1101 versionSelector.data('lastVersion', json_data['comment_version'])
1053 versionSelector.parent().show();
1102 versionSelector.parent().show();
1054 }
1103 }
1055 updateCommentVersionDropDown();
1104 updateCommentVersionDropDown();
1056
1105
1057 // by default we reset state of comment preserving the text
1106 // by default we reset state of comment preserving the text
1058 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1107 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1059 var prefix = "Error while editing this comment.\n"
1108 var prefix = "Error while editing this comment.\n"
1060 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1109 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1061 ajaxErrorSwal(message);
1110 ajaxErrorSwal(message);
1062 };
1111 };
1063
1112
1064 var successRenderCommit = function(o){
1113 var successRenderCommit = function(o){
1065 $comment.show();
1114 $comment.show();
1066 $comment[0].lastElementChild.innerHTML = o;
1115 $comment[0].lastElementChild.innerHTML = o;
1067 };
1116 };
1068
1117
1069 var previewUrl = pyroutes.url(
1118 var previewUrl = pyroutes.url(
1070 'repo_commit_comment_preview',
1119 'repo_commit_comment_preview',
1071 {'repo_name': templateContext.repo_name,
1120 {'repo_name': templateContext.repo_name,
1072 'commit_id': templateContext.commit_data.commit_id});
1121 'commit_id': templateContext.commit_data.commit_id});
1073
1122
1074 _submitAjaxPOST(
1123 _submitAjaxPOST(
1075 previewUrl, postData, successRenderCommit,
1124 previewUrl, postData, successRenderCommit,
1076 failRenderCommit
1125 failRenderCommit
1077 );
1126 );
1078
1127
1079 try {
1128 try {
1080 var html = json_data.rendered_text;
1129 var html = json_data.rendered_text;
1081 var lineno = json_data.line_no;
1130 var lineno = json_data.line_no;
1082 var target_id = json_data.target_id;
1131 var target_id = json_data.target_id;
1083
1132
1084 $comments.find('.cb-comment-add-button').before(html);
1133 $comments.find('.cb-comment-add-button').before(html);
1085
1134
1086 // run global callback on submit
1135 // run global callback on submit
1087 commentForm.globalSubmitSuccessCallback();
1136 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1088
1137
1089 } catch (e) {
1138 } catch (e) {
1090 console.error(e);
1139 console.error(e);
1091 }
1140 }
1092
1141
1093 // re trigger the linkification of next/prev navigation
1142 // re trigger the linkification of next/prev navigation
1094 linkifyComments($('.inline-comment-injected'));
1143 linkifyComments($('.inline-comment-injected'));
1095 timeagoActivate();
1144 timeagoActivate();
1096 tooltipActivate();
1145 tooltipActivate();
1097
1146
1098 if (window.updateSticky !== undefined) {
1147 if (window.updateSticky !== undefined) {
1099 // potentially our comments change the active window size, so we
1148 // potentially our comments change the active window size, so we
1100 // notify sticky elements
1149 // notify sticky elements
1101 updateSticky()
1150 updateSticky()
1102 }
1151 }
1103
1152
1104 if (window.refreshAllComments !== undefined) {
1153 if (window.refreshAllComments !== undefined && !isDraft) {
1105 // if we have this handler, run it, and refresh all comments boxes
1154 // if we have this handler, run it, and refresh all comments boxes
1106 refreshAllComments()
1155 refreshAllComments()
1107 }
1156 }
1108
1157
1109 commentForm.setActionButtonsDisabled(false);
1158 commentForm.setActionButtonsDisabled(false);
1110
1159
1111 };
1160 };
1112
1161
1113 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1162 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1114 var prefix = "Error while editing comment.\n"
1163 var prefix = "Error while editing comment.\n"
1115 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1164 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1116 if (jqXHR.status == 409){
1165 if (jqXHR.status == 409){
1117 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1166 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1118 ajaxErrorSwal(message, 'Comment version mismatch.');
1167 ajaxErrorSwal(message, 'Comment version mismatch.');
1119 } else {
1168 } else {
1120 ajaxErrorSwal(message);
1169 ajaxErrorSwal(message);
1121 }
1170 }
1122
1171
1123 commentForm.resetCommentFormState(text)
1172 commentForm.resetCommentFormState(text)
1124 };
1173 };
1125 commentForm.submitAjaxPOST(
1174 commentForm.submitAjaxPOST(
1126 commentForm.submitUrl, postData,
1175 commentForm.submitUrl, postData,
1127 submitSuccessCallback,
1176 submitSuccessCallback,
1128 submitFailCallback);
1177 submitFailCallback);
1129 });
1178 });
1130 }
1179 }
1131
1180
1132 $form.addClass('comment-inline-form-open');
1181 $form.addClass('comment-inline-form-open');
1133 };
1182 };
1134
1183
1135 this.createComment = function(node, resolutionComment) {
1184 this.createComment = function(node, resolutionComment) {
1136 var resolvesCommentId = resolutionComment || null;
1185 var resolvesCommentId = resolutionComment || null;
1137 var $node = $(node);
1186 var $node = $(node);
1138 var $td = $node.closest('td');
1187 var $td = $node.closest('td');
1139 var $form = $td.find('.comment-inline-form');
1188 var $form = $td.find('.comment-inline-form');
1140 this.edit = false;
1189 this.edit = false;
1141
1190
1142 if (!$form.length) {
1191 if (!$form.length) {
1143
1192
1144 var $filediff = $node.closest('.filediff');
1193 var $filediff = $node.closest('.filediff');
1145 $filediff.removeClass('hide-comments');
1194 $filediff.removeClass('hide-comments');
1146 var f_path = $filediff.attr('data-f-path');
1195 var f_path = $filediff.attr('data-f-path');
1147 var lineno = self.getLineNumber(node);
1196 var lineno = self.getLineNumber(node);
1148 // create a new HTML from template
1197 // create a new HTML from template
1149 var tmpl = $('#cb-comment-inline-form-template').html();
1198 var tmpl = $('#cb-comment-inline-form-template').html();
1150 tmpl = tmpl.format(escapeHtml(f_path), lineno);
1199 tmpl = tmpl.format(escapeHtml(f_path), lineno);
1151 $form = $(tmpl);
1200 $form = $(tmpl);
1152
1201
1153 var $comments = $td.find('.inline-comments');
1202 var $comments = $td.find('.inline-comments');
1154 if (!$comments.length) {
1203 if (!$comments.length) {
1155 $comments = $(
1204 $comments = $(
1156 $('#cb-comments-inline-container-template').html());
1205 $('#cb-comments-inline-container-template').html());
1157 $td.append($comments);
1206 $td.append($comments);
1158 }
1207 }
1159
1208
1160 $td.find('.cb-comment-add-button').before($form);
1209 $td.find('.cb-comment-add-button').before($form);
1161
1210
1162 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
1211 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
1163 var _form = $($form[0]).find('form');
1212 var _form = $($form[0]).find('form');
1164 var autocompleteActions = ['as_note', 'as_todo'];
1213 var autocompleteActions = ['as_note', 'as_todo'];
1165 var comment_id=null;
1214 var comment_id=null;
1166 var commentForm = this.createCommentForm(
1215 var commentForm = this.createCommentForm(
1167 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
1216 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
1168
1217
1169 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
1218 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
1170 form: _form,
1219 form: _form,
1171 parent: $td[0],
1220 parent: $td[0],
1172 lineno: lineno,
1221 lineno: lineno,
1173 f_path: f_path}
1222 f_path: f_path}
1174 );
1223 );
1175
1224
1176 // set a CUSTOM submit handler for inline comments.
1225 // set a CUSTOM submit handler for inline comments.
1177 commentForm.setHandleFormSubmit(function(o) {
1226 commentForm.setHandleFormSubmit(function(o) {
1178 var text = commentForm.cm.getValue();
1227 var text = commentForm.cm.getValue();
1179 var commentType = commentForm.getCommentType();
1228 var commentType = commentForm.getCommentType();
1180 var resolvesCommentId = commentForm.getResolvesId();
1229 var resolvesCommentId = commentForm.getResolvesId();
1230 var isDraft = commentForm.getDraftState();
1181
1231
1182 if (text === "") {
1232 if (text === "") {
1183 return;
1233 return;
1184 }
1234 }
1185
1235
1186 if (lineno === undefined) {
1236 if (lineno === undefined) {
1187 alert('missing line !');
1237 alert('missing line !');
1188 return;
1238 return;
1189 }
1239 }
1190 if (f_path === undefined) {
1240 if (f_path === undefined) {
1191 alert('missing file path !');
1241 alert('missing file path !');
1192 return;
1242 return;
1193 }
1243 }
1194
1244
1195 var excludeCancelBtn = false;
1245 var excludeCancelBtn = false;
1196 var submitEvent = true;
1246 var submitEvent = true;
1197 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1247 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1198 commentForm.cm.setOption("readOnly", true);
1248 commentForm.cm.setOption("readOnly", true);
1199 var postData = {
1249 var postData = {
1200 'text': text,
1250 'text': text,
1201 'f_path': f_path,
1251 'f_path': f_path,
1202 'line': lineno,
1252 'line': lineno,
1203 'comment_type': commentType,
1253 'comment_type': commentType,
1254 'draft': isDraft,
1204 'csrf_token': CSRF_TOKEN
1255 'csrf_token': CSRF_TOKEN
1205 };
1256 };
1206 if (resolvesCommentId){
1257 if (resolvesCommentId){
1207 postData['resolves_comment_id'] = resolvesCommentId;
1258 postData['resolves_comment_id'] = resolvesCommentId;
1208 }
1259 }
1209
1260
1210 var submitSuccessCallback = function(json_data) {
1261 var submitSuccessCallback = function(json_data) {
1211 $form.remove();
1262 $form.remove();
1212 try {
1263 try {
1213 var html = json_data.rendered_text;
1264 var html = json_data.rendered_text;
1214 var lineno = json_data.line_no;
1265 var lineno = json_data.line_no;
1215 var target_id = json_data.target_id;
1266 var target_id = json_data.target_id;
1216
1267
1217 $comments.find('.cb-comment-add-button').before(html);
1268 $comments.find('.cb-comment-add-button').before(html);
1218
1269
1219 //mark visually which comment was resolved
1270 //mark visually which comment was resolved
1220 if (resolvesCommentId) {
1271 if (resolvesCommentId) {
1221 commentForm.markCommentResolved(resolvesCommentId);
1272 commentForm.markCommentResolved(resolvesCommentId);
1222 }
1273 }
1223
1274
1224 // run global callback on submit
1275 // run global callback on submit
1225 commentForm.globalSubmitSuccessCallback();
1276 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1226
1277
1227 } catch (e) {
1278 } catch (e) {
1228 console.error(e);
1279 console.error(e);
1229 }
1280 }
1230
1281
1231 // re trigger the linkification of next/prev navigation
1282 // re trigger the linkification of next/prev navigation
1232 linkifyComments($('.inline-comment-injected'));
1283 linkifyComments($('.inline-comment-injected'));
1233 timeagoActivate();
1284 timeagoActivate();
1234 tooltipActivate();
1285 tooltipActivate();
1235
1286
1236 if (window.updateSticky !== undefined) {
1287 if (window.updateSticky !== undefined) {
1237 // potentially our comments change the active window size, so we
1288 // potentially our comments change the active window size, so we
1238 // notify sticky elements
1289 // notify sticky elements
1239 updateSticky()
1290 updateSticky()
1240 }
1291 }
1241
1292
1242 if (window.refreshAllComments !== undefined) {
1293 if (window.refreshAllComments !== undefined && !isDraft) {
1243 // if we have this handler, run it, and refresh all comments boxes
1294 // if we have this handler, run it, and refresh all comments boxes
1244 refreshAllComments()
1295 refreshAllComments()
1245 }
1296 }
1246
1297
1247 commentForm.setActionButtonsDisabled(false);
1298 commentForm.setActionButtonsDisabled(false);
1248
1299
1249 };
1300 };
1250 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1301 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1251 var prefix = "Error while submitting comment.\n"
1302 var prefix = "Error while submitting comment.\n"
1252 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1303 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1253 ajaxErrorSwal(message);
1304 ajaxErrorSwal(message);
1254 commentForm.resetCommentFormState(text)
1305 commentForm.resetCommentFormState(text)
1255 };
1306 };
1256 commentForm.submitAjaxPOST(
1307 commentForm.submitAjaxPOST(
1257 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1308 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1258 });
1309 });
1259 }
1310 }
1260
1311
1261 $form.addClass('comment-inline-form-open');
1312 $form.addClass('comment-inline-form-open');
1262 };
1313 };
1263
1314
1264 this.createResolutionComment = function(commentId){
1315 this.createResolutionComment = function(commentId){
1265 // hide the trigger text
1316 // hide the trigger text
1266 $('#resolve-comment-{0}'.format(commentId)).hide();
1317 $('#resolve-comment-{0}'.format(commentId)).hide();
1267
1318
1268 var comment = $('#comment-'+commentId);
1319 var comment = $('#comment-'+commentId);
1269 var commentData = comment.data();
1320 var commentData = comment.data();
1270 if (commentData.commentInline) {
1321 if (commentData.commentInline) {
1271 this.createComment(comment, commentId)
1322 this.createComment(comment, commentId)
1272 } else {
1323 } else {
1273 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
1324 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
1274 }
1325 }
1275
1326
1276 return false;
1327 return false;
1277 };
1328 };
1278
1329
1279 this.submitResolution = function(commentId){
1330 this.submitResolution = function(commentId){
1280 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1331 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1281 var commentForm = form.get(0).CommentForm;
1332 var commentForm = form.get(0).CommentForm;
1282
1333
1283 var cm = commentForm.getCmInstance();
1334 var cm = commentForm.getCmInstance();
1284 var renderer = templateContext.visual.default_renderer;
1335 var renderer = templateContext.visual.default_renderer;
1285 if (renderer == 'rst'){
1336 if (renderer == 'rst'){
1286 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1337 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1287 } else if (renderer == 'markdown') {
1338 } else if (renderer == 'markdown') {
1288 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1339 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1289 } else {
1340 } else {
1290 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1341 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1291 }
1342 }
1292
1343
1293 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1344 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1294 form.submit();
1345 form.submit();
1295 return false;
1346 return false;
1296 };
1347 };
1297
1348
1298 };
1349 };
@@ -1,1254 +1,1262 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%!
3 <%!
4 from rhodecode.lib import html_filters
4 from rhodecode.lib import html_filters
5 %>
5 %>
6
6
7 <%inherit file="root.mako"/>
7 <%inherit file="root.mako"/>
8
8
9 <%include file="/ejs_templates/templates.html"/>
9 <%include file="/ejs_templates/templates.html"/>
10
10
11 <div class="outerwrapper">
11 <div class="outerwrapper">
12 <!-- HEADER -->
12 <!-- HEADER -->
13 <div class="header">
13 <div class="header">
14 <div id="header-inner" class="wrapper">
14 <div id="header-inner" class="wrapper">
15 <div id="logo">
15 <div id="logo">
16 <div class="logo-wrapper">
16 <div class="logo-wrapper">
17 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
17 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
18 </div>
18 </div>
19 % if c.rhodecode_name:
19 % if c.rhodecode_name:
20 <div class="branding">
20 <div class="branding">
21 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
21 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
22 </div>
22 </div>
23 % endif
23 % endif
24 </div>
24 </div>
25 <!-- MENU BAR NAV -->
25 <!-- MENU BAR NAV -->
26 ${self.menu_bar_nav()}
26 ${self.menu_bar_nav()}
27 <!-- END MENU BAR NAV -->
27 <!-- END MENU BAR NAV -->
28 </div>
28 </div>
29 </div>
29 </div>
30 ${self.menu_bar_subnav()}
30 ${self.menu_bar_subnav()}
31 <!-- END HEADER -->
31 <!-- END HEADER -->
32
32
33 <!-- CONTENT -->
33 <!-- CONTENT -->
34 <div id="content" class="wrapper">
34 <div id="content" class="wrapper">
35
35
36 <rhodecode-toast id="notifications"></rhodecode-toast>
36 <rhodecode-toast id="notifications"></rhodecode-toast>
37
37
38 <div class="main">
38 <div class="main">
39 ${next.main()}
39 ${next.main()}
40 </div>
40 </div>
41
41
42 </div>
42 </div>
43 <!-- END CONTENT -->
43 <!-- END CONTENT -->
44
44
45 </div>
45 </div>
46
46
47 <!-- FOOTER -->
47 <!-- FOOTER -->
48 <div id="footer">
48 <div id="footer">
49 <div id="footer-inner" class="title wrapper">
49 <div id="footer-inner" class="title wrapper">
50 <div>
50 <div>
51 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
51 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
52
52
53 <p class="footer-link-right">
53 <p class="footer-link-right">
54 <a class="grey-link-action" href="${h.route_path('home', _query={'showrcid': 1})}">
54 <a class="grey-link-action" href="${h.route_path('home', _query={'showrcid': 1})}">
55 RhodeCode
55 RhodeCode
56 % if c.visual.show_version:
56 % if c.visual.show_version:
57 ${c.rhodecode_version}
57 ${c.rhodecode_version}
58 % endif
58 % endif
59 ${c.rhodecode_edition}
59 ${c.rhodecode_edition}
60 </a> |
60 </a> |
61
61
62 % if c.visual.rhodecode_support_url:
62 % if c.visual.rhodecode_support_url:
63 <a class="grey-link-action" href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a> |
63 <a class="grey-link-action" href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a> |
64 <a class="grey-link-action" href="https://docs.rhodecode.com" target="_blank">${_('Documentation')}</a>
64 <a class="grey-link-action" href="https://docs.rhodecode.com" target="_blank">${_('Documentation')}</a>
65 % endif
65 % endif
66
66
67 </p>
67 </p>
68
68
69 <p class="server-instance" style="display:${sid}">
69 <p class="server-instance" style="display:${sid}">
70 ## display hidden instance ID if specially defined
70 ## display hidden instance ID if specially defined
71 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
71 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
72 % if c.rhodecode_instanceid:
72 % if c.rhodecode_instanceid:
73 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
73 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
74 % endif
74 % endif
75 </p>
75 </p>
76 </div>
76 </div>
77 </div>
77 </div>
78 </div>
78 </div>
79
79
80 <!-- END FOOTER -->
80 <!-- END FOOTER -->
81
81
82 ### MAKO DEFS ###
82 ### MAKO DEFS ###
83
83
84 <%def name="menu_bar_subnav()">
84 <%def name="menu_bar_subnav()">
85 </%def>
85 </%def>
86
86
87 <%def name="breadcrumbs(class_='breadcrumbs')">
87 <%def name="breadcrumbs(class_='breadcrumbs')">
88 <div class="${class_}">
88 <div class="${class_}">
89 ${self.breadcrumbs_links()}
89 ${self.breadcrumbs_links()}
90 </div>
90 </div>
91 </%def>
91 </%def>
92
92
93 <%def name="admin_menu(active=None)">
93 <%def name="admin_menu(active=None)">
94
94
95 <div id="context-bar">
95 <div id="context-bar">
96 <div class="wrapper">
96 <div class="wrapper">
97 <div class="title">
97 <div class="title">
98 <div class="title-content">
98 <div class="title-content">
99 <div class="title-main">
99 <div class="title-main">
100 % if c.is_super_admin:
100 % if c.is_super_admin:
101 ${_('Super-admin Panel')}
101 ${_('Super-admin Panel')}
102 % else:
102 % else:
103 ${_('Delegated Admin Panel')}
103 ${_('Delegated Admin Panel')}
104 % endif
104 % endif
105 </div>
105 </div>
106 </div>
106 </div>
107 </div>
107 </div>
108
108
109 <ul id="context-pages" class="navigation horizontal-list">
109 <ul id="context-pages" class="navigation horizontal-list">
110
110
111 ## super-admin case
111 ## super-admin case
112 % if c.is_super_admin:
112 % if c.is_super_admin:
113 <li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
113 <li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
114 <li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
114 <li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
115 <li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
115 <li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
116 <li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
116 <li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
117 <li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
117 <li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
118 <li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
118 <li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
119 <li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
119 <li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
120 <li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
120 <li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
121 <li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
121 <li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
122 <li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
122 <li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
123
123
124 ## delegated admin
124 ## delegated admin
125 % elif c.is_delegated_admin:
125 % elif c.is_delegated_admin:
126 <%
126 <%
127 repositories=c.auth_user.repositories_admin or c.can_create_repo
127 repositories=c.auth_user.repositories_admin or c.can_create_repo
128 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
128 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
129 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
129 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
130 %>
130 %>
131
131
132 %if repositories:
132 %if repositories:
133 <li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
133 <li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
134 %endif
134 %endif
135 %if repository_groups:
135 %if repository_groups:
136 <li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
136 <li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
137 %endif
137 %endif
138 %if user_groups:
138 %if user_groups:
139 <li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
139 <li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
140 %endif
140 %endif
141 % endif
141 % endif
142 </ul>
142 </ul>
143
143
144 </div>
144 </div>
145 <div class="clear"></div>
145 <div class="clear"></div>
146 </div>
146 </div>
147 </%def>
147 </%def>
148
148
149 <%def name="dt_info_panel(elements)">
149 <%def name="dt_info_panel(elements)">
150 <dl class="dl-horizontal">
150 <dl class="dl-horizontal">
151 %for dt, dd, title, show_items in elements:
151 %for dt, dd, title, show_items in elements:
152 <dt>${dt}:</dt>
152 <dt>${dt}:</dt>
153 <dd title="${h.tooltip(title)}">
153 <dd title="${h.tooltip(title)}">
154 %if callable(dd):
154 %if callable(dd):
155 ## allow lazy evaluation of elements
155 ## allow lazy evaluation of elements
156 ${dd()}
156 ${dd()}
157 %else:
157 %else:
158 ${dd}
158 ${dd}
159 %endif
159 %endif
160 %if show_items:
160 %if show_items:
161 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
161 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
162 %endif
162 %endif
163 </dd>
163 </dd>
164
164
165 %if show_items:
165 %if show_items:
166 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
166 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
167 %for item in show_items:
167 %for item in show_items:
168 <dt></dt>
168 <dt></dt>
169 <dd>${item}</dd>
169 <dd>${item}</dd>
170 %endfor
170 %endfor
171 </div>
171 </div>
172 %endif
172 %endif
173
173
174 %endfor
174 %endfor
175 </dl>
175 </dl>
176 </%def>
176 </%def>
177
177
178 <%def name="tr_info_entry(element)">
178 <%def name="tr_info_entry(element)">
179 <% key, val, title, show_items = element %>
179 <% key, val, title, show_items = element %>
180
180
181 <tr>
181 <tr>
182 <td style="vertical-align: top">${key}</td>
182 <td style="vertical-align: top">${key}</td>
183 <td title="${h.tooltip(title)}">
183 <td title="${h.tooltip(title)}">
184 %if callable(val):
184 %if callable(val):
185 ## allow lazy evaluation of elements
185 ## allow lazy evaluation of elements
186 ${val()}
186 ${val()}
187 %else:
187 %else:
188 ${val}
188 ${val}
189 %endif
189 %endif
190 %if show_items:
190 %if show_items:
191 <div class="collapsable-content" data-toggle="item-${h.md5_safe(val)[:6]}-details" style="display: none">
191 <div class="collapsable-content" data-toggle="item-${h.md5_safe(val)[:6]}-details" style="display: none">
192 % for item in show_items:
192 % for item in show_items:
193 <dt></dt>
193 <dt></dt>
194 <dd>${item}</dd>
194 <dd>${item}</dd>
195 % endfor
195 % endfor
196 </div>
196 </div>
197 %endif
197 %endif
198 </td>
198 </td>
199 <td style="vertical-align: top">
199 <td style="vertical-align: top">
200 %if show_items:
200 %if show_items:
201 <span class="btn-collapse" data-toggle="item-${h.md5_safe(val)[:6]}-details">${_('Show More')} </span>
201 <span class="btn-collapse" data-toggle="item-${h.md5_safe(val)[:6]}-details">${_('Show More')} </span>
202 %endif
202 %endif
203 </td>
203 </td>
204 </tr>
204 </tr>
205
205
206 </%def>
206 </%def>
207
207
208 <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None, extra_class=None)">
208 <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None, extra_class=None)">
209 <%
209 <%
210 if size > 16:
210 if size > 16:
211 gravatar_class = ['gravatar','gravatar-large']
211 gravatar_class = ['gravatar','gravatar-large']
212 else:
212 else:
213 gravatar_class = ['gravatar']
213 gravatar_class = ['gravatar']
214
214
215 data_hovercard_url = ''
215 data_hovercard_url = ''
216 data_hovercard_alt = tooltip_alt.replace('<', '&lt;').replace('>', '&gt;') if tooltip_alt else ''
216 data_hovercard_alt = tooltip_alt.replace('<', '&lt;').replace('>', '&gt;') if tooltip_alt else ''
217
217
218 if tooltip:
218 if tooltip:
219 gravatar_class += ['tooltip-hovercard']
219 gravatar_class += ['tooltip-hovercard']
220 if extra_class:
220 if extra_class:
221 gravatar_class += extra_class
221 gravatar_class += extra_class
222 if tooltip and user:
222 if tooltip and user:
223 if user.username == h.DEFAULT_USER:
223 if user.username == h.DEFAULT_USER:
224 gravatar_class.pop(-1)
224 gravatar_class.pop(-1)
225 else:
225 else:
226 data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', ''))
226 data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', ''))
227 gravatar_class = ' '.join(gravatar_class)
227 gravatar_class = ' '.join(gravatar_class)
228
228
229 %>
229 %>
230 <%doc>
230 <%doc>
231 TODO: johbo: For now we serve double size images to make it smooth
231 TODO: johbo: For now we serve double size images to make it smooth
232 for retina. This is how it worked until now. Should be replaced
232 for retina. This is how it worked until now. Should be replaced
233 with a better solution at some point.
233 with a better solution at some point.
234 </%doc>
234 </%doc>
235
235
236 <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2)}" />
236 <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2)}" />
237 </%def>
237 </%def>
238
238
239
239
240 <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False, _class='rc-user')">
240 <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False, _class='rc-user')">
241 <%
241 <%
242 email = h.email_or_none(contact)
242 email = h.email_or_none(contact)
243 rc_user = h.discover_user(contact)
243 rc_user = h.discover_user(contact)
244 %>
244 %>
245
245
246 <div class="${_class}">
246 <div class="${_class}">
247 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
247 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
248 <span class="${('user user-disabled' if show_disabled else 'user')}">
248 <span class="${('user user-disabled' if show_disabled else 'user')}">
249 ${h.link_to_user(rc_user or contact)}
249 ${h.link_to_user(rc_user or contact)}
250 </span>
250 </span>
251 </div>
251 </div>
252 </%def>
252 </%def>
253
253
254
254
255 <%def name="user_group_icon(user_group=None, size=16, tooltip=False)">
255 <%def name="user_group_icon(user_group=None, size=16, tooltip=False)">
256 <%
256 <%
257 if (size > 16):
257 if (size > 16):
258 gravatar_class = 'icon-user-group-alt'
258 gravatar_class = 'icon-user-group-alt'
259 else:
259 else:
260 gravatar_class = 'icon-user-group-alt'
260 gravatar_class = 'icon-user-group-alt'
261
261
262 if tooltip:
262 if tooltip:
263 gravatar_class += ' tooltip-hovercard'
263 gravatar_class += ' tooltip-hovercard'
264
264
265 data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id)
265 data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id)
266 %>
266 %>
267 <%doc>
267 <%doc>
268 TODO: johbo: For now we serve double size images to make it smooth
268 TODO: johbo: For now we serve double size images to make it smooth
269 for retina. This is how it worked until now. Should be replaced
269 for retina. This is how it worked until now. Should be replaced
270 with a better solution at some point.
270 with a better solution at some point.
271 </%doc>
271 </%doc>
272
272
273 <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i>
273 <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i>
274 </%def>
274 </%def>
275
275
276 <%def name="repo_page_title(repo_instance)">
276 <%def name="repo_page_title(repo_instance)">
277 <div class="title-content repo-title">
277 <div class="title-content repo-title">
278
278
279 <div class="title-main">
279 <div class="title-main">
280 ## SVN/HG/GIT icons
280 ## SVN/HG/GIT icons
281 %if h.is_hg(repo_instance):
281 %if h.is_hg(repo_instance):
282 <i class="icon-hg"></i>
282 <i class="icon-hg"></i>
283 %endif
283 %endif
284 %if h.is_git(repo_instance):
284 %if h.is_git(repo_instance):
285 <i class="icon-git"></i>
285 <i class="icon-git"></i>
286 %endif
286 %endif
287 %if h.is_svn(repo_instance):
287 %if h.is_svn(repo_instance):
288 <i class="icon-svn"></i>
288 <i class="icon-svn"></i>
289 %endif
289 %endif
290
290
291 ## public/private
291 ## public/private
292 %if repo_instance.private:
292 %if repo_instance.private:
293 <i class="icon-repo-private"></i>
293 <i class="icon-repo-private"></i>
294 %else:
294 %else:
295 <i class="icon-repo-public"></i>
295 <i class="icon-repo-public"></i>
296 %endif
296 %endif
297
297
298 ## repo name with group name
298 ## repo name with group name
299 ${h.breadcrumb_repo_link(repo_instance)}
299 ${h.breadcrumb_repo_link(repo_instance)}
300
300
301 ## Context Actions
301 ## Context Actions
302 <div class="pull-right">
302 <div class="pull-right">
303 %if c.rhodecode_user.username != h.DEFAULT_USER:
303 %if c.rhodecode_user.username != h.DEFAULT_USER:
304 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
304 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
305
305
306 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
306 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
307 % if c.repository_is_user_following:
307 % if c.repository_is_user_following:
308 <i class="icon-eye-off"></i>${_('Unwatch')}
308 <i class="icon-eye-off"></i>${_('Unwatch')}
309 % else:
309 % else:
310 <i class="icon-eye"></i>${_('Watch')}
310 <i class="icon-eye"></i>${_('Watch')}
311 % endif
311 % endif
312
312
313 </a>
313 </a>
314 %else:
314 %else:
315 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
315 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
316 %endif
316 %endif
317 </div>
317 </div>
318
318
319 </div>
319 </div>
320
320
321 ## FORKED
321 ## FORKED
322 %if repo_instance.fork:
322 %if repo_instance.fork:
323 <p class="discreet">
323 <p class="discreet">
324 <i class="icon-code-fork"></i> ${_('Fork of')}
324 <i class="icon-code-fork"></i> ${_('Fork of')}
325 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
325 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
326 </p>
326 </p>
327 %endif
327 %endif
328
328
329 ## IMPORTED FROM REMOTE
329 ## IMPORTED FROM REMOTE
330 %if repo_instance.clone_uri:
330 %if repo_instance.clone_uri:
331 <p class="discreet">
331 <p class="discreet">
332 <i class="icon-code-fork"></i> ${_('Clone from')}
332 <i class="icon-code-fork"></i> ${_('Clone from')}
333 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
333 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
334 </p>
334 </p>
335 %endif
335 %endif
336
336
337 ## LOCKING STATUS
337 ## LOCKING STATUS
338 %if repo_instance.locked[0]:
338 %if repo_instance.locked[0]:
339 <p class="locking_locked discreet">
339 <p class="locking_locked discreet">
340 <i class="icon-repo-lock"></i>
340 <i class="icon-repo-lock"></i>
341 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
341 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
342 </p>
342 </p>
343 %elif repo_instance.enable_locking:
343 %elif repo_instance.enable_locking:
344 <p class="locking_unlocked discreet">
344 <p class="locking_unlocked discreet">
345 <i class="icon-repo-unlock"></i>
345 <i class="icon-repo-unlock"></i>
346 ${_('Repository not locked. Pull repository to lock it.')}
346 ${_('Repository not locked. Pull repository to lock it.')}
347 </p>
347 </p>
348 %endif
348 %endif
349
349
350 </div>
350 </div>
351 </%def>
351 </%def>
352
352
353 <%def name="repo_menu(active=None)">
353 <%def name="repo_menu(active=None)">
354 <%
354 <%
355 ## determine if we have "any" option available
355 ## determine if we have "any" option available
356 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
356 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
357 has_actions = can_lock
357 has_actions = can_lock
358
358
359 %>
359 %>
360 % if c.rhodecode_db_repo.archived:
360 % if c.rhodecode_db_repo.archived:
361 <div class="alert alert-warning text-center">
361 <div class="alert alert-warning text-center">
362 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
362 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
363 </div>
363 </div>
364 % endif
364 % endif
365
365
366 <!--- REPO CONTEXT BAR -->
366 <!--- REPO CONTEXT BAR -->
367 <div id="context-bar">
367 <div id="context-bar">
368 <div class="wrapper">
368 <div class="wrapper">
369
369
370 <div class="title">
370 <div class="title">
371 ${self.repo_page_title(c.rhodecode_db_repo)}
371 ${self.repo_page_title(c.rhodecode_db_repo)}
372 </div>
372 </div>
373
373
374 <ul id="context-pages" class="navigation horizontal-list">
374 <ul id="context-pages" class="navigation horizontal-list">
375 <li class="${h.is_active('summary', active)}"><a class="menulink" href="${h.route_path('repo_summary_explicit', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
375 <li class="${h.is_active('summary', active)}"><a class="menulink" href="${h.route_path('repo_summary_explicit', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
376 <li class="${h.is_active('commits', active)}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
376 <li class="${h.is_active('commits', active)}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
377 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.repo_files_by_ref_url(c.repo_name, c.rhodecode_db_repo.repo_type, f_path='', ref_name=c.rhodecode_db_repo.landing_ref_name, commit_id='tip', query={'at':c.rhodecode_db_repo.landing_ref_name})}"><div class="menulabel">${_('Files')}</div></a></li>
377 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.repo_files_by_ref_url(c.repo_name, c.rhodecode_db_repo.repo_type, f_path='', ref_name=c.rhodecode_db_repo.landing_ref_name, commit_id='tip', query={'at':c.rhodecode_db_repo.landing_ref_name})}"><div class="menulabel">${_('Files')}</div></a></li>
378 <li class="${h.is_active('compare', active)}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
378 <li class="${h.is_active('compare', active)}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
379
379
380 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
380 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
381 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
381 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
382 <li class="${h.is_active('showpullrequest', active)}">
382 <li class="${h.is_active('showpullrequest', active)}">
383 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
383 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
384 <div class="menulabel">
384 <div class="menulabel">
385 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
385 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
386 </div>
386 </div>
387 </a>
387 </a>
388 </li>
388 </li>
389 %endif
389 %endif
390
390
391 <li class="${h.is_active('artifacts', active)}">
391 <li class="${h.is_active('artifacts', active)}">
392 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
392 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
393 <div class="menulabel">
393 <div class="menulabel">
394 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
394 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
395 </div>
395 </div>
396 </a>
396 </a>
397 </li>
397 </li>
398
398
399 %if not c.rhodecode_db_repo.archived and h.HasRepoPermissionAll('repository.admin')(c.repo_name):
399 %if not c.rhodecode_db_repo.archived and h.HasRepoPermissionAll('repository.admin')(c.repo_name):
400 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
400 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
401 %endif
401 %endif
402
402
403 <li class="${h.is_active('options', active)}">
403 <li class="${h.is_active('options', active)}">
404 % if has_actions:
404 % if has_actions:
405 <a class="menulink dropdown">
405 <a class="menulink dropdown">
406 <div class="menulabel">${_('Options')}<div class="show_more"></div></div>
406 <div class="menulabel">${_('Options')}<div class="show_more"></div></div>
407 </a>
407 </a>
408 <ul class="submenu">
408 <ul class="submenu">
409 %if can_lock:
409 %if can_lock:
410 %if c.rhodecode_db_repo.locked[0]:
410 %if c.rhodecode_db_repo.locked[0]:
411 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock Repository')}</a></li>
411 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock Repository')}</a></li>
412 %else:
412 %else:
413 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock Repository')}</a></li>
413 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock Repository')}</a></li>
414 %endif
414 %endif
415 %endif
415 %endif
416 </ul>
416 </ul>
417 % endif
417 % endif
418 </li>
418 </li>
419
419
420 </ul>
420 </ul>
421 </div>
421 </div>
422 <div class="clear"></div>
422 <div class="clear"></div>
423 </div>
423 </div>
424
424
425 <!--- REPO END CONTEXT BAR -->
425 <!--- REPO END CONTEXT BAR -->
426
426
427 </%def>
427 </%def>
428
428
429 <%def name="repo_group_page_title(repo_group_instance)">
429 <%def name="repo_group_page_title(repo_group_instance)">
430 <div class="title-content">
430 <div class="title-content">
431 <div class="title-main">
431 <div class="title-main">
432 ## Repository Group icon
432 ## Repository Group icon
433 <i class="icon-repo-group"></i>
433 <i class="icon-repo-group"></i>
434
434
435 ## repo name with group name
435 ## repo name with group name
436 ${h.breadcrumb_repo_group_link(repo_group_instance)}
436 ${h.breadcrumb_repo_group_link(repo_group_instance)}
437 </div>
437 </div>
438
438
439 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
439 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
440 <div class="repo-group-desc discreet">
440 <div class="repo-group-desc discreet">
441 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
441 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
442 </div>
442 </div>
443
443
444 </div>
444 </div>
445 </%def>
445 </%def>
446
446
447
447
448 <%def name="repo_group_menu(active=None)">
448 <%def name="repo_group_menu(active=None)">
449 <%
449 <%
450 gr_name = c.repo_group.group_name if c.repo_group else None
450 gr_name = c.repo_group.group_name if c.repo_group else None
451 # create repositories with write permission on group is set to true
451 # create repositories with write permission on group is set to true
452 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
452 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
453
453
454 %>
454 %>
455
455
456
456
457 <!--- REPO GROUP CONTEXT BAR -->
457 <!--- REPO GROUP CONTEXT BAR -->
458 <div id="context-bar">
458 <div id="context-bar">
459 <div class="wrapper">
459 <div class="wrapper">
460 <div class="title">
460 <div class="title">
461 ${self.repo_group_page_title(c.repo_group)}
461 ${self.repo_group_page_title(c.repo_group)}
462 </div>
462 </div>
463
463
464 <ul id="context-pages" class="navigation horizontal-list">
464 <ul id="context-pages" class="navigation horizontal-list">
465 <li class="${h.is_active('home', active)}">
465 <li class="${h.is_active('home', active)}">
466 <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a>
466 <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a>
467 </li>
467 </li>
468 % if c.is_super_admin or group_admin:
468 % if c.is_super_admin or group_admin:
469 <li class="${h.is_active('settings', active)}">
469 <li class="${h.is_active('settings', active)}">
470 <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
470 <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
471 </li>
471 </li>
472 % endif
472 % endif
473
473
474 </ul>
474 </ul>
475 </div>
475 </div>
476 <div class="clear"></div>
476 <div class="clear"></div>
477 </div>
477 </div>
478
478
479 <!--- REPO GROUP CONTEXT BAR -->
479 <!--- REPO GROUP CONTEXT BAR -->
480
480
481 </%def>
481 </%def>
482
482
483
483
484 <%def name="usermenu(active=False)">
484 <%def name="usermenu(active=False)">
485 <%
485 <%
486 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
486 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
487
487
488 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
488 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
489 # create repositories with write permission on group is set to true
489 # create repositories with write permission on group is set to true
490
490
491 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
491 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
492 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
492 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
493 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
493 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
494 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
494 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
495
495
496 can_create_repos = c.is_super_admin or c.can_create_repo
496 can_create_repos = c.is_super_admin or c.can_create_repo
497 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
497 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
498
498
499 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
499 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
500 can_create_repo_groups_in_group = c.is_super_admin or group_admin
500 can_create_repo_groups_in_group = c.is_super_admin or group_admin
501 %>
501 %>
502
502
503 % if not_anonymous:
503 % if not_anonymous:
504 <%
504 <%
505 default_target_group = dict()
505 default_target_group = dict()
506 if c.rhodecode_user.personal_repo_group:
506 if c.rhodecode_user.personal_repo_group:
507 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
507 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
508 %>
508 %>
509
509
510 ## create action
510 ## create action
511 <li>
511 <li>
512 <a href="#create-actions" onclick="return false;" class="menulink childs">
512 <a href="#create-actions" onclick="return false;" class="menulink childs">
513 <i class="icon-plus-circled"></i>
513 <i class="icon-plus-circled"></i>
514 </a>
514 </a>
515
515
516 <div class="action-menu submenu">
516 <div class="action-menu submenu">
517
517
518 <ol>
518 <ol>
519 ## scope of within a repository
519 ## scope of within a repository
520 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
520 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
521 <li class="submenu-title">${_('This Repository')}</li>
521 <li class="submenu-title">${_('This Repository')}</li>
522 <li>
522 <li>
523 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
523 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
524 </li>
524 </li>
525 % if can_fork:
525 % if can_fork:
526 <li>
526 <li>
527 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
527 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
528 </li>
528 </li>
529 % endif
529 % endif
530 % endif
530 % endif
531
531
532 ## scope of within repository groups
532 ## scope of within repository groups
533 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
533 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
534 <li class="submenu-title">${_('This Repository Group')}</li>
534 <li class="submenu-title">${_('This Repository Group')}</li>
535
535
536 % if can_create_repos_in_group:
536 % if can_create_repos_in_group:
537 <li>
537 <li>
538 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository')}</a>
538 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository')}</a>
539 </li>
539 </li>
540 % endif
540 % endif
541
541
542 % if can_create_repo_groups_in_group:
542 % if can_create_repo_groups_in_group:
543 <li>
543 <li>
544 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_(u'New Repository Group')}</a>
544 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_(u'New Repository Group')}</a>
545 </li>
545 </li>
546 % endif
546 % endif
547 % endif
547 % endif
548
548
549 ## personal group
549 ## personal group
550 % if c.rhodecode_user.personal_repo_group:
550 % if c.rhodecode_user.personal_repo_group:
551 <li class="submenu-title">Personal Group</li>
551 <li class="submenu-title">Personal Group</li>
552
552
553 <li>
553 <li>
554 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
554 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
555 </li>
555 </li>
556
556
557 <li>
557 <li>
558 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
558 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
559 </li>
559 </li>
560 % endif
560 % endif
561
561
562 ## Global actions
562 ## Global actions
563 <li class="submenu-title">RhodeCode</li>
563 <li class="submenu-title">RhodeCode</li>
564 % if can_create_repos:
564 % if can_create_repos:
565 <li>
565 <li>
566 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
566 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
567 </li>
567 </li>
568 % endif
568 % endif
569
569
570 % if can_create_repo_groups:
570 % if can_create_repo_groups:
571 <li>
571 <li>
572 <a href="${h.route_path('repo_group_new')}" >${_(u'New Repository Group')}</a>
572 <a href="${h.route_path('repo_group_new')}" >${_(u'New Repository Group')}</a>
573 </li>
573 </li>
574 % endif
574 % endif
575
575
576 <li>
576 <li>
577 <a href="${h.route_path('gists_new')}">${_(u'New Gist')}</a>
577 <a href="${h.route_path('gists_new')}">${_(u'New Gist')}</a>
578 </li>
578 </li>
579
579
580 </ol>
580 </ol>
581
581
582 </div>
582 </div>
583 </li>
583 </li>
584
584
585 ## notifications
585 ## notifications
586 <li>
586 <li>
587 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
587 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
588 ${c.unread_notifications}
588 ${c.unread_notifications}
589 </a>
589 </a>
590 </li>
590 </li>
591 % endif
591 % endif
592
592
593 ## USER MENU
593 ## USER MENU
594 <li id="quick_login_li" class="${'active' if active else ''}">
594 <li id="quick_login_li" class="${'active' if active else ''}">
595 % if c.rhodecode_user.username == h.DEFAULT_USER:
595 % if c.rhodecode_user.username == h.DEFAULT_USER:
596 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
596 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
597 ${gravatar(c.rhodecode_user.email, 20)}
597 ${gravatar(c.rhodecode_user.email, 20)}
598 <span class="user">
598 <span class="user">
599 <span>${_('Sign in')}</span>
599 <span>${_('Sign in')}</span>
600 </span>
600 </span>
601 </a>
601 </a>
602 % else:
602 % else:
603 ## logged in user
603 ## logged in user
604 <a id="quick_login_link" class="menulink childs">
604 <a id="quick_login_link" class="menulink childs">
605 ${gravatar(c.rhodecode_user.email, 20)}
605 ${gravatar(c.rhodecode_user.email, 20)}
606 <span class="user">
606 <span class="user">
607 <span class="menu_link_user">${c.rhodecode_user.username}</span>
607 <span class="menu_link_user">${c.rhodecode_user.username}</span>
608 <div class="show_more"></div>
608 <div class="show_more"></div>
609 </span>
609 </span>
610 </a>
610 </a>
611 ## subnav with menu for logged in user
611 ## subnav with menu for logged in user
612 <div class="user-menu submenu">
612 <div class="user-menu submenu">
613 <div id="quick_login">
613 <div id="quick_login">
614 %if c.rhodecode_user.username != h.DEFAULT_USER:
614 %if c.rhodecode_user.username != h.DEFAULT_USER:
615 <div class="">
615 <div class="">
616 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
616 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
617 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
617 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
618 <div class="email">${c.rhodecode_user.email}</div>
618 <div class="email">${c.rhodecode_user.email}</div>
619 </div>
619 </div>
620 <div class="">
620 <div class="">
621 <ol class="links">
621 <ol class="links">
622 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
622 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
623 % if c.rhodecode_user.personal_repo_group:
623 % if c.rhodecode_user.personal_repo_group:
624 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
624 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
625 % endif
625 % endif
626 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
626 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
627
627
628 % if c.debug_style:
628 % if c.debug_style:
629 <li>
629 <li>
630 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
630 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
631 <div class="menulabel">${_('[Style]')}</div>
631 <div class="menulabel">${_('[Style]')}</div>
632 </a>
632 </a>
633 </li>
633 </li>
634 % endif
634 % endif
635
635
636 ## bookmark-items
636 ## bookmark-items
637 <li class="bookmark-items">
637 <li class="bookmark-items">
638 ${_('Bookmarks')}
638 ${_('Bookmarks')}
639 <div class="pull-right">
639 <div class="pull-right">
640 <a href="${h.route_path('my_account_bookmarks')}">
640 <a href="${h.route_path('my_account_bookmarks')}">
641
641
642 <i class="icon-cog"></i>
642 <i class="icon-cog"></i>
643 </a>
643 </a>
644 </div>
644 </div>
645 </li>
645 </li>
646 % if not c.bookmark_items:
646 % if not c.bookmark_items:
647 <li>
647 <li>
648 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
648 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
649 </li>
649 </li>
650 % endif
650 % endif
651 % for item in c.bookmark_items:
651 % for item in c.bookmark_items:
652 <li>
652 <li>
653 % if item.repository:
653 % if item.repository:
654 <div>
654 <div>
655 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
655 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
656 <code>${item.position}</code>
656 <code>${item.position}</code>
657 % if item.repository.repo_type == 'hg':
657 % if item.repository.repo_type == 'hg':
658 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
658 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
659 % elif item.repository.repo_type == 'git':
659 % elif item.repository.repo_type == 'git':
660 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
660 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
661 % elif item.repository.repo_type == 'svn':
661 % elif item.repository.repo_type == 'svn':
662 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
662 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
663 % endif
663 % endif
664 ${(item.title or h.shorter(item.repository.repo_name, 30))}
664 ${(item.title or h.shorter(item.repository.repo_name, 30))}
665 </a>
665 </a>
666 </div>
666 </div>
667 % elif item.repository_group:
667 % elif item.repository_group:
668 <div>
668 <div>
669 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
669 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
670 <code>${item.position}</code>
670 <code>${item.position}</code>
671 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
671 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
672 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
672 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
673 </a>
673 </a>
674 </div>
674 </div>
675 % else:
675 % else:
676 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
676 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
677 <code>${item.position}</code>
677 <code>${item.position}</code>
678 ${item.title}
678 ${item.title}
679 </a>
679 </a>
680 % endif
680 % endif
681 </li>
681 </li>
682 % endfor
682 % endfor
683
683
684 <li class="logout">
684 <li class="logout">
685 ${h.secure_form(h.route_path('logout'), request=request)}
685 ${h.secure_form(h.route_path('logout'), request=request)}
686 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
686 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
687 ${h.end_form()}
687 ${h.end_form()}
688 </li>
688 </li>
689 </ol>
689 </ol>
690 </div>
690 </div>
691 %endif
691 %endif
692 </div>
692 </div>
693 </div>
693 </div>
694
694
695 % endif
695 % endif
696 </li>
696 </li>
697 </%def>
697 </%def>
698
698
699 <%def name="menu_items(active=None)">
699 <%def name="menu_items(active=None)">
700 <%
700 <%
701 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
701 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
702 notice_display = 'none' if len(notice_messages) == 0 else ''
702 notice_display = 'none' if len(notice_messages) == 0 else ''
703 %>
703 %>
704
704
705 <ul id="quick" class="main_nav navigation horizontal-list">
705 <ul id="quick" class="main_nav navigation horizontal-list">
706 ## notice box for important system messages
706 ## notice box for important system messages
707 <li style="display: ${notice_display}">
707 <li style="display: ${notice_display}">
708 <a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false">
708 <a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false">
709 <div class="menulabel-notice ${notice_level}" >
709 <div class="menulabel-notice ${notice_level}" >
710 ${len(notice_messages)}
710 ${len(notice_messages)}
711 </div>
711 </div>
712 </a>
712 </a>
713 </li>
713 </li>
714 <div class="notice-messages-container" style="display: none">
714 <div class="notice-messages-container" style="display: none">
715 <div class="notice-messages">
715 <div class="notice-messages">
716 <table class="rctable">
716 <table class="rctable">
717 % for notice in notice_messages:
717 % for notice in notice_messages:
718 <tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}">
718 <tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}">
719 <td style="vertical-align: text-top; width: 20px">
719 <td style="vertical-align: text-top; width: 20px">
720 <i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i>
720 <i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i>
721 </td>
721 </td>
722 <td>
722 <td>
723 <span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span>
723 <span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span>
724 ${notice['subject']}
724 ${notice['subject']}
725
725
726 <div id="notice-${notice['msg_id']}" style="display: none">
726 <div id="notice-${notice['msg_id']}" style="display: none">
727 ${h.render(notice['body'], renderer='markdown')}
727 ${h.render(notice['body'], renderer='markdown')}
728 </div>
728 </div>
729 </td>
729 </td>
730 <td style="vertical-align: text-top; width: 35px;">
730 <td style="vertical-align: text-top; width: 35px;">
731 <a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false">
731 <a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false">
732 <i class="icon-remove icon-filled-red"></i>
732 <i class="icon-remove icon-filled-red"></i>
733 </a>
733 </a>
734 </td>
734 </td>
735 </tr>
735 </tr>
736
736
737 % endfor
737 % endfor
738 </table>
738 </table>
739 </div>
739 </div>
740 </div>
740 </div>
741 ## Main filter
741 ## Main filter
742 <li>
742 <li>
743 <div class="menulabel main_filter_box">
743 <div class="menulabel main_filter_box">
744 <div class="main_filter_input_box">
744 <div class="main_filter_input_box">
745 <ul class="searchItems">
745 <ul class="searchItems">
746
746
747 <li class="searchTag searchTagIcon">
747 <li class="searchTag searchTagIcon">
748 <i class="icon-search"></i>
748 <i class="icon-search"></i>
749 </li>
749 </li>
750
750
751 % if c.template_context['search_context']['repo_id']:
751 % if c.template_context['search_context']['repo_id']:
752 <li class="searchTag searchTagFilter searchTagHidable" >
752 <li class="searchTag searchTagFilter searchTagHidable" >
753 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
753 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
754 <span class="tag">
754 <span class="tag">
755 This repo
755 This repo
756 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
756 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
757 </span>
757 </span>
758 ##</a>
758 ##</a>
759 </li>
759 </li>
760 % elif c.template_context['search_context']['repo_group_id']:
760 % elif c.template_context['search_context']['repo_group_id']:
761 <li class="searchTag searchTagFilter searchTagHidable">
761 <li class="searchTag searchTagFilter searchTagHidable">
762 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
762 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
763 <span class="tag">
763 <span class="tag">
764 This group
764 This group
765 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
765 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
766 </span>
766 </span>
767 ##</a>
767 ##</a>
768 </li>
768 </li>
769 % endif
769 % endif
770
770
771 <li class="searchTagInput">
771 <li class="searchTagInput">
772 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
772 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
773 </li>
773 </li>
774 <li class="searchTag searchTagHelp">
774 <li class="searchTag searchTagHelp">
775 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
775 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
776 </li>
776 </li>
777 </ul>
777 </ul>
778 </div>
778 </div>
779 </div>
779 </div>
780
780
781 <div id="main_filter_help" style="display: none">
781 <div id="main_filter_help" style="display: none">
782 - Use '/' key to quickly access this field.
782 - Use '/' key to quickly access this field.
783
783
784 - Enter a name of repository, or repository group for quick search.
784 - Enter a name of repository, or repository group for quick search.
785
785
786 - Prefix query to allow special search:
786 - Prefix query to allow special search:
787
787
788 <strong>user:</strong>admin, to search for usernames, always global
788 <strong>user:</strong>admin, to search for usernames, always global
789
789
790 <strong>user_group:</strong>devops, to search for user groups, always global
790 <strong>user_group:</strong>devops, to search for user groups, always global
791
791
792 <strong>pr:</strong>303, to search for pull request number, title, or description, always global
792 <strong>pr:</strong>303, to search for pull request number, title, or description, always global
793
793
794 <strong>commit:</strong>efced4, to search for commits, scoped to repositories or groups
794 <strong>commit:</strong>efced4, to search for commits, scoped to repositories or groups
795
795
796 <strong>file:</strong>models.py, to search for file paths, scoped to repositories or groups
796 <strong>file:</strong>models.py, to search for file paths, scoped to repositories or groups
797
797
798 % if c.template_context['search_context']['repo_id']:
798 % if c.template_context['search_context']['repo_id']:
799 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
799 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
800 % elif c.template_context['search_context']['repo_group_id']:
800 % elif c.template_context['search_context']['repo_group_id']:
801 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
801 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
802 % else:
802 % else:
803 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
803 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
804 % endif
804 % endif
805 </div>
805 </div>
806 </li>
806 </li>
807
807
808 ## ROOT MENU
808 ## ROOT MENU
809 <li class="${h.is_active('home', active)}">
809 <li class="${h.is_active('home', active)}">
810 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
810 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
811 <div class="menulabel">${_('Home')}</div>
811 <div class="menulabel">${_('Home')}</div>
812 </a>
812 </a>
813 </li>
813 </li>
814
814
815 %if c.rhodecode_user.username != h.DEFAULT_USER:
815 %if c.rhodecode_user.username != h.DEFAULT_USER:
816 <li class="${h.is_active('journal', active)}">
816 <li class="${h.is_active('journal', active)}">
817 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
817 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
818 <div class="menulabel">${_('Journal')}</div>
818 <div class="menulabel">${_('Journal')}</div>
819 </a>
819 </a>
820 </li>
820 </li>
821 %else:
821 %else:
822 <li class="${h.is_active('journal', active)}">
822 <li class="${h.is_active('journal', active)}">
823 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
823 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
824 <div class="menulabel">${_('Public journal')}</div>
824 <div class="menulabel">${_('Public journal')}</div>
825 </a>
825 </a>
826 </li>
826 </li>
827 %endif
827 %endif
828
828
829 <li class="${h.is_active('gists', active)}">
829 <li class="${h.is_active('gists', active)}">
830 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
830 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
831 <div class="menulabel">${_('Gists')}</div>
831 <div class="menulabel">${_('Gists')}</div>
832 </a>
832 </a>
833 </li>
833 </li>
834
834
835 % if c.is_super_admin or c.is_delegated_admin:
835 % if c.is_super_admin or c.is_delegated_admin:
836 <li class="${h.is_active('admin', active)}">
836 <li class="${h.is_active('admin', active)}">
837 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
837 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
838 <div class="menulabel">${_('Admin')} </div>
838 <div class="menulabel">${_('Admin')} </div>
839 </a>
839 </a>
840 </li>
840 </li>
841 % endif
841 % endif
842
842
843 ## render extra user menu
843 ## render extra user menu
844 ${usermenu(active=(active=='my_account'))}
844 ${usermenu(active=(active=='my_account'))}
845
845
846 </ul>
846 </ul>
847
847
848 <script type="text/javascript">
848 <script type="text/javascript">
849 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
849 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
850
850
851 var formatRepoResult = function(result, container, query, escapeMarkup) {
851 var formatRepoResult = function(result, container, query, escapeMarkup) {
852 return function(data, escapeMarkup) {
852 return function(data, escapeMarkup) {
853 if (!data.repo_id){
853 if (!data.repo_id){
854 return data.text; // optgroup text Repositories
854 return data.text; // optgroup text Repositories
855 }
855 }
856
856
857 var tmpl = '';
857 var tmpl = '';
858 var repoType = data['repo_type'];
858 var repoType = data['repo_type'];
859 var repoName = data['text'];
859 var repoName = data['text'];
860
860
861 if(data && data.type == 'repo'){
861 if(data && data.type == 'repo'){
862 if(repoType === 'hg'){
862 if(repoType === 'hg'){
863 tmpl += '<i class="icon-hg"></i> ';
863 tmpl += '<i class="icon-hg"></i> ';
864 }
864 }
865 else if(repoType === 'git'){
865 else if(repoType === 'git'){
866 tmpl += '<i class="icon-git"></i> ';
866 tmpl += '<i class="icon-git"></i> ';
867 }
867 }
868 else if(repoType === 'svn'){
868 else if(repoType === 'svn'){
869 tmpl += '<i class="icon-svn"></i> ';
869 tmpl += '<i class="icon-svn"></i> ';
870 }
870 }
871 if(data['private']){
871 if(data['private']){
872 tmpl += '<i class="icon-lock" ></i> ';
872 tmpl += '<i class="icon-lock" ></i> ';
873 }
873 }
874 else if(visualShowPublicIcon){
874 else if(visualShowPublicIcon){
875 tmpl += '<i class="icon-unlock-alt"></i> ';
875 tmpl += '<i class="icon-unlock-alt"></i> ';
876 }
876 }
877 }
877 }
878 tmpl += escapeMarkup(repoName);
878 tmpl += escapeMarkup(repoName);
879 return tmpl;
879 return tmpl;
880
880
881 }(result, escapeMarkup);
881 }(result, escapeMarkup);
882 };
882 };
883
883
884 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
884 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
885 return function(data, escapeMarkup) {
885 return function(data, escapeMarkup) {
886 if (!data.repo_group_id){
886 if (!data.repo_group_id){
887 return data.text; // optgroup text Repositories
887 return data.text; // optgroup text Repositories
888 }
888 }
889
889
890 var tmpl = '';
890 var tmpl = '';
891 var repoGroupName = data['text'];
891 var repoGroupName = data['text'];
892
892
893 if(data){
893 if(data){
894
894
895 tmpl += '<i class="icon-repo-group"></i> ';
895 tmpl += '<i class="icon-repo-group"></i> ';
896
896
897 }
897 }
898 tmpl += escapeMarkup(repoGroupName);
898 tmpl += escapeMarkup(repoGroupName);
899 return tmpl;
899 return tmpl;
900
900
901 }(result, escapeMarkup);
901 }(result, escapeMarkup);
902 };
902 };
903
903
904 var escapeRegExChars = function (value) {
904 var escapeRegExChars = function (value) {
905 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
905 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
906 };
906 };
907
907
908 var getRepoIcon = function(repo_type) {
908 var getRepoIcon = function(repo_type) {
909 if (repo_type === 'hg') {
909 if (repo_type === 'hg') {
910 return '<i class="icon-hg"></i> ';
910 return '<i class="icon-hg"></i> ';
911 }
911 }
912 else if (repo_type === 'git') {
912 else if (repo_type === 'git') {
913 return '<i class="icon-git"></i> ';
913 return '<i class="icon-git"></i> ';
914 }
914 }
915 else if (repo_type === 'svn') {
915 else if (repo_type === 'svn') {
916 return '<i class="icon-svn"></i> ';
916 return '<i class="icon-svn"></i> ';
917 }
917 }
918 return ''
918 return ''
919 };
919 };
920
920
921 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
921 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
922
922
923 if (value.split(':').length === 2) {
923 if (value.split(':').length === 2) {
924 value = value.split(':')[1]
924 value = value.split(':')[1]
925 }
925 }
926
926
927 var searchType = data['type'];
927 var searchType = data['type'];
928 var searchSubType = data['subtype'];
928 var searchSubType = data['subtype'];
929 var valueDisplay = data['value_display'];
929 var valueDisplay = data['value_display'];
930 var valueIcon = data['value_icon'];
930 var valueIcon = data['value_icon'];
931
931
932 var pattern = '(' + escapeRegExChars(value) + ')';
932 var pattern = '(' + escapeRegExChars(value) + ')';
933
933
934 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
934 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
935
935
936 // highlight match
936 // highlight match
937 if (searchType != 'text') {
937 if (searchType != 'text') {
938 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
938 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
939 }
939 }
940
940
941 var icon = '';
941 var icon = '';
942
942
943 if (searchType === 'hint') {
943 if (searchType === 'hint') {
944 icon += '<i class="icon-repo-group"></i> ';
944 icon += '<i class="icon-repo-group"></i> ';
945 }
945 }
946 // full text search/hints
946 // full text search/hints
947 else if (searchType === 'search') {
947 else if (searchType === 'search') {
948 if (valueIcon === undefined) {
948 if (valueIcon === undefined) {
949 icon += '<i class="icon-more"></i> ';
949 icon += '<i class="icon-more"></i> ';
950 } else {
950 } else {
951 icon += valueIcon + ' ';
951 icon += valueIcon + ' ';
952 }
952 }
953
953
954 if (searchSubType !== undefined && searchSubType == 'repo') {
954 if (searchSubType !== undefined && searchSubType == 'repo') {
955 valueDisplay += '<div class="pull-right tag">repository</div>';
955 valueDisplay += '<div class="pull-right tag">repository</div>';
956 }
956 }
957 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
957 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
958 valueDisplay += '<div class="pull-right tag">repo group</div>';
958 valueDisplay += '<div class="pull-right tag">repo group</div>';
959 }
959 }
960 }
960 }
961 // repository
961 // repository
962 else if (searchType === 'repo') {
962 else if (searchType === 'repo') {
963
963
964 var repoIcon = getRepoIcon(data['repo_type']);
964 var repoIcon = getRepoIcon(data['repo_type']);
965 icon += repoIcon;
965 icon += repoIcon;
966
966
967 if (data['private']) {
967 if (data['private']) {
968 icon += '<i class="icon-lock" ></i> ';
968 icon += '<i class="icon-lock" ></i> ';
969 }
969 }
970 else if (visualShowPublicIcon) {
970 else if (visualShowPublicIcon) {
971 icon += '<i class="icon-unlock-alt"></i> ';
971 icon += '<i class="icon-unlock-alt"></i> ';
972 }
972 }
973 }
973 }
974 // repository groups
974 // repository groups
975 else if (searchType === 'repo_group') {
975 else if (searchType === 'repo_group') {
976 icon += '<i class="icon-repo-group"></i> ';
976 icon += '<i class="icon-repo-group"></i> ';
977 }
977 }
978 // user group
978 // user group
979 else if (searchType === 'user_group') {
979 else if (searchType === 'user_group') {
980 icon += '<i class="icon-group"></i> ';
980 icon += '<i class="icon-group"></i> ';
981 }
981 }
982 // user
982 // user
983 else if (searchType === 'user') {
983 else if (searchType === 'user') {
984 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
984 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
985 }
985 }
986 // pull request
986 // pull request
987 else if (searchType === 'pull_request') {
987 else if (searchType === 'pull_request') {
988 icon += '<i class="icon-merge"></i> ';
988 icon += '<i class="icon-merge"></i> ';
989 }
989 }
990 // commit
990 // commit
991 else if (searchType === 'commit') {
991 else if (searchType === 'commit') {
992 var repo_data = data['repo_data'];
992 var repo_data = data['repo_data'];
993 var repoIcon = getRepoIcon(repo_data['repository_type']);
993 var repoIcon = getRepoIcon(repo_data['repository_type']);
994 if (repoIcon) {
994 if (repoIcon) {
995 icon += repoIcon;
995 icon += repoIcon;
996 } else {
996 } else {
997 icon += '<i class="icon-tag"></i>';
997 icon += '<i class="icon-tag"></i>';
998 }
998 }
999 }
999 }
1000 // file
1000 // file
1001 else if (searchType === 'file') {
1001 else if (searchType === 'file') {
1002 var repo_data = data['repo_data'];
1002 var repo_data = data['repo_data'];
1003 var repoIcon = getRepoIcon(repo_data['repository_type']);
1003 var repoIcon = getRepoIcon(repo_data['repository_type']);
1004 if (repoIcon) {
1004 if (repoIcon) {
1005 icon += repoIcon;
1005 icon += repoIcon;
1006 } else {
1006 } else {
1007 icon += '<i class="icon-tag"></i>';
1007 icon += '<i class="icon-tag"></i>';
1008 }
1008 }
1009 }
1009 }
1010 // generic text
1010 // generic text
1011 else if (searchType === 'text') {
1011 else if (searchType === 'text') {
1012 icon = '';
1012 icon = '';
1013 }
1013 }
1014
1014
1015 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
1015 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
1016 return tmpl.format(icon, valueDisplay);
1016 return tmpl.format(icon, valueDisplay);
1017 };
1017 };
1018
1018
1019 var handleSelect = function(element, suggestion) {
1019 var handleSelect = function(element, suggestion) {
1020 if (suggestion.type === "hint") {
1020 if (suggestion.type === "hint") {
1021 // we skip action
1021 // we skip action
1022 $('#main_filter').focus();
1022 $('#main_filter').focus();
1023 }
1023 }
1024 else if (suggestion.type === "text") {
1024 else if (suggestion.type === "text") {
1025 // we skip action
1025 // we skip action
1026 $('#main_filter').focus();
1026 $('#main_filter').focus();
1027
1027
1028 } else {
1028 } else {
1029 window.location = suggestion['url'];
1029 window.location = suggestion['url'];
1030 }
1030 }
1031 };
1031 };
1032
1032
1033 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
1033 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
1034 if (queryLowerCase.split(':').length === 2) {
1034 if (queryLowerCase.split(':').length === 2) {
1035 queryLowerCase = queryLowerCase.split(':')[1]
1035 queryLowerCase = queryLowerCase.split(':')[1]
1036 }
1036 }
1037 if (suggestion.type === "text") {
1037 if (suggestion.type === "text") {
1038 // special case we don't want to "skip" display for
1038 // special case we don't want to "skip" display for
1039 return true
1039 return true
1040 }
1040 }
1041 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
1041 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
1042 };
1042 };
1043
1043
1044 var cleanContext = {
1044 var cleanContext = {
1045 repo_view_type: null,
1045 repo_view_type: null,
1046
1046
1047 repo_id: null,
1047 repo_id: null,
1048 repo_name: "",
1048 repo_name: "",
1049
1049
1050 repo_group_id: null,
1050 repo_group_id: null,
1051 repo_group_name: null
1051 repo_group_name: null
1052 };
1052 };
1053 var removeGoToFilter = function () {
1053 var removeGoToFilter = function () {
1054 $('.searchTagHidable').hide();
1054 $('.searchTagHidable').hide();
1055 $('#main_filter').autocomplete(
1055 $('#main_filter').autocomplete(
1056 'setOptions', {params:{search_context: cleanContext}});
1056 'setOptions', {params:{search_context: cleanContext}});
1057 };
1057 };
1058
1058
1059 $('#main_filter').autocomplete({
1059 $('#main_filter').autocomplete({
1060 serviceUrl: pyroutes.url('goto_switcher_data'),
1060 serviceUrl: pyroutes.url('goto_switcher_data'),
1061 params: {
1061 params: {
1062 "search_context": templateContext.search_context
1062 "search_context": templateContext.search_context
1063 },
1063 },
1064 minChars:2,
1064 minChars:2,
1065 maxHeight:400,
1065 maxHeight:400,
1066 deferRequestBy: 300, //miliseconds
1066 deferRequestBy: 300, //miliseconds
1067 tabDisabled: true,
1067 tabDisabled: true,
1068 autoSelectFirst: false,
1068 autoSelectFirst: false,
1069 containerClass: 'autocomplete-qfilter-suggestions',
1069 containerClass: 'autocomplete-qfilter-suggestions',
1070 formatResult: autocompleteMainFilterFormatResult,
1070 formatResult: autocompleteMainFilterFormatResult,
1071 lookupFilter: autocompleteMainFilterResult,
1071 lookupFilter: autocompleteMainFilterResult,
1072 onSelect: function (element, suggestion) {
1072 onSelect: function (element, suggestion) {
1073 handleSelect(element, suggestion);
1073 handleSelect(element, suggestion);
1074 return false;
1074 return false;
1075 },
1075 },
1076 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
1076 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
1077 if (jqXHR !== 'abort') {
1077 if (jqXHR !== 'abort') {
1078 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
1078 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
1079 SwalNoAnimation.fire({
1079 SwalNoAnimation.fire({
1080 icon: 'error',
1080 icon: 'error',
1081 title: _gettext('Error during search operation'),
1081 title: _gettext('Error during search operation'),
1082 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
1082 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
1083 }).then(function(result) {
1083 }).then(function(result) {
1084 window.location.reload();
1084 window.location.reload();
1085 })
1085 })
1086 }
1086 }
1087 },
1087 },
1088 onSearchStart: function (params) {
1088 onSearchStart: function (params) {
1089 $('.searchTag.searchTagIcon').html('<i class="icon-spin animate-spin"></i>')
1089 $('.searchTag.searchTagIcon').html('<i class="icon-spin animate-spin"></i>')
1090 },
1090 },
1091 onSearchComplete: function (query, suggestions) {
1091 onSearchComplete: function (query, suggestions) {
1092 $('.searchTag.searchTagIcon').html('<i class="icon-search"></i>')
1092 $('.searchTag.searchTagIcon').html('<i class="icon-search"></i>')
1093 },
1093 },
1094 });
1094 });
1095
1095
1096 showMainFilterBox = function () {
1096 showMainFilterBox = function () {
1097 $('#main_filter_help').toggle();
1097 $('#main_filter_help').toggle();
1098 };
1098 };
1099
1099
1100 $('#main_filter').on('keydown.autocomplete', function (e) {
1100 $('#main_filter').on('keydown.autocomplete', function (e) {
1101
1101
1102 var BACKSPACE = 8;
1102 var BACKSPACE = 8;
1103 var el = $(e.currentTarget);
1103 var el = $(e.currentTarget);
1104 if(e.which === BACKSPACE){
1104 if(e.which === BACKSPACE){
1105 var inputVal = el.val();
1105 var inputVal = el.val();
1106 if (inputVal === ""){
1106 if (inputVal === ""){
1107 removeGoToFilter()
1107 removeGoToFilter()
1108 }
1108 }
1109 }
1109 }
1110 });
1110 });
1111
1111
1112 var dismissNotice = function(noticeId) {
1112 var dismissNotice = function(noticeId) {
1113
1113
1114 var url = pyroutes.url('user_notice_dismiss',
1114 var url = pyroutes.url('user_notice_dismiss',
1115 {"user_id": templateContext.rhodecode_user.user_id});
1115 {"user_id": templateContext.rhodecode_user.user_id});
1116
1116
1117 var postData = {
1117 var postData = {
1118 'csrf_token': CSRF_TOKEN,
1118 'csrf_token': CSRF_TOKEN,
1119 'notice_id': noticeId,
1119 'notice_id': noticeId,
1120 };
1120 };
1121
1121
1122 var success = function(response) {
1122 var success = function(response) {
1123 $('#notice-message-' + noticeId).remove();
1123 $('#notice-message-' + noticeId).remove();
1124 return false;
1124 return false;
1125 };
1125 };
1126 var failure = function(data, textStatus, xhr) {
1126 var failure = function(data, textStatus, xhr) {
1127 alert("error processing request: " + textStatus);
1127 alert("error processing request: " + textStatus);
1128 return false;
1128 return false;
1129 };
1129 };
1130 ajaxPOST(url, postData, success, failure);
1130 ajaxPOST(url, postData, success, failure);
1131 }
1131 }
1132
1132
1133 var hideLicenseWarning = function () {
1133 var hideLicenseWarning = function () {
1134 var fingerprint = templateContext.session_attrs.license_fingerprint;
1134 var fingerprint = templateContext.session_attrs.license_fingerprint;
1135 storeUserSessionAttr('rc_user_session_attr.hide_license_warning', fingerprint);
1135 storeUserSessionAttr('rc_user_session_attr.hide_license_warning', fingerprint);
1136 $('#notifications').hide();
1136 $('#notifications').hide();
1137 }
1137 }
1138
1138
1139 var hideLicenseError = function () {
1139 var hideLicenseError = function () {
1140 var fingerprint = templateContext.session_attrs.license_fingerprint;
1140 var fingerprint = templateContext.session_attrs.license_fingerprint;
1141 storeUserSessionAttr('rc_user_session_attr.hide_license_error', fingerprint);
1141 storeUserSessionAttr('rc_user_session_attr.hide_license_error', fingerprint);
1142 $('#notifications').hide();
1142 $('#notifications').hide();
1143 }
1143 }
1144
1144
1145 </script>
1145 </script>
1146 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1146 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1147 </%def>
1147 </%def>
1148
1148
1149 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
1149 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
1150 <div class="modal-dialog">
1150 <div class="modal-dialog">
1151 <div class="modal-content">
1151 <div class="modal-content">
1152 <div class="modal-header">
1152 <div class="modal-header">
1153 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
1153 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
1154 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
1154 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
1155 </div>
1155 </div>
1156 <div class="modal-body">
1156 <div class="modal-body">
1157 <div class="block-left">
1157 <div class="block-left">
1158 <table class="keyboard-mappings">
1158 <table class="keyboard-mappings">
1159 <tbody>
1159 <tbody>
1160 <tr>
1160 <tr>
1161 <th></th>
1161 <th></th>
1162 <th>${_('Site-wide shortcuts')}</th>
1162 <th>${_('Site-wide shortcuts')}</th>
1163 </tr>
1163 </tr>
1164 <%
1164 <%
1165 elems = [
1165 elems = [
1166 ('/', 'Use quick search box'),
1166 ('/', 'Use quick search box'),
1167 ('g h', 'Goto home page'),
1167 ('g h', 'Goto home page'),
1168 ('g g', 'Goto my private gists page'),
1168 ('g g', 'Goto my private gists page'),
1169 ('g G', 'Goto my public gists page'),
1169 ('g G', 'Goto my public gists page'),
1170 ('g 0-9', 'Goto bookmarked items from 0-9'),
1170 ('g 0-9', 'Goto bookmarked items from 0-9'),
1171 ('n r', 'New repository page'),
1171 ('n r', 'New repository page'),
1172 ('n g', 'New gist page'),
1172 ('n g', 'New gist page'),
1173 ]
1173 ]
1174 %>
1174 %>
1175 %for key, desc in elems:
1175 %for key, desc in elems:
1176 <tr>
1176 <tr>
1177 <td class="keys">
1177 <td class="keys">
1178 <span class="key tag">${key}</span>
1178 <span class="key tag">${key}</span>
1179 </td>
1179 </td>
1180 <td>${desc}</td>
1180 <td>${desc}</td>
1181 </tr>
1181 </tr>
1182 %endfor
1182 %endfor
1183 </tbody>
1183 </tbody>
1184 </table>
1184 </table>
1185 </div>
1185 </div>
1186 <div class="block-left">
1186 <div class="block-left">
1187 <table class="keyboard-mappings">
1187 <table class="keyboard-mappings">
1188 <tbody>
1188 <tbody>
1189 <tr>
1189 <tr>
1190 <th></th>
1190 <th></th>
1191 <th>${_('Repositories')}</th>
1191 <th>${_('Repositories')}</th>
1192 </tr>
1192 </tr>
1193 <%
1193 <%
1194 elems = [
1194 elems = [
1195 ('g s', 'Goto summary page'),
1195 ('g s', 'Goto summary page'),
1196 ('g c', 'Goto changelog page'),
1196 ('g c', 'Goto changelog page'),
1197 ('g f', 'Goto files page'),
1197 ('g f', 'Goto files page'),
1198 ('g F', 'Goto files page with file search activated'),
1198 ('g F', 'Goto files page with file search activated'),
1199 ('g p', 'Goto pull requests page'),
1199 ('g p', 'Goto pull requests page'),
1200 ('g o', 'Goto repository settings'),
1200 ('g o', 'Goto repository settings'),
1201 ('g O', 'Goto repository access permissions settings'),
1201 ('g O', 'Goto repository access permissions settings'),
1202 ('t s', 'Toggle sidebar on some pages'),
1202 ('t s', 'Toggle sidebar on some pages'),
1203 ]
1203 ]
1204 %>
1204 %>
1205 %for key, desc in elems:
1205 %for key, desc in elems:
1206 <tr>
1206 <tr>
1207 <td class="keys">
1207 <td class="keys">
1208 <span class="key tag">${key}</span>
1208 <span class="key tag">${key}</span>
1209 </td>
1209 </td>
1210 <td>${desc}</td>
1210 <td>${desc}</td>
1211 </tr>
1211 </tr>
1212 %endfor
1212 %endfor
1213 </tbody>
1213 </tbody>
1214 </table>
1214 </table>
1215 </div>
1215 </div>
1216 </div>
1216 </div>
1217 <div class="modal-footer">
1217 <div class="modal-footer">
1218 </div>
1218 </div>
1219 </div><!-- /.modal-content -->
1219 </div><!-- /.modal-content -->
1220 </div><!-- /.modal-dialog -->
1220 </div><!-- /.modal-dialog -->
1221 </div><!-- /.modal -->
1221 </div><!-- /.modal -->
1222
1222
1223
1223
1224 <script type="text/javascript">
1224 <script type="text/javascript">
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;
1231 var sidebarEnabled = $('aside.right-sidebar').get(0);
1239 var sidebarEnabled = $('aside.right-sidebar').get(0);
1232
1240
1233 if (sidebarState === 'expanded') {
1241 if (sidebarState === 'expanded') {
1234 expanded = true
1242 expanded = true
1235 } else if (sidebarState === 'collapsed') {
1243 } else if (sidebarState === 'collapsed') {
1236 expanded = false
1244 expanded = false
1237 }
1245 }
1238 if (sidebarEnabled) {
1246 if (sidebarEnabled) {
1239 // show sidebar since it's hidden on load
1247 // show sidebar since it's hidden on load
1240 $('.right-sidebar').show();
1248 $('.right-sidebar').show();
1241
1249
1242 // init based on set initial class, or if defined user session attrs
1250 // init based on set initial class, or if defined user session attrs
1243 if (expanded) {
1251 if (expanded) {
1244 window.expandSidebar();
1252 window.expandSidebar();
1245 window.updateStickyHeader();
1253 window.updateStickyHeader();
1246
1254
1247 } else {
1255 } else {
1248 window.collapseSidebar();
1256 window.collapseSidebar();
1249 window.updateStickyHeader();
1257 window.updateStickyHeader();
1250 }
1258 }
1251 }
1259 }
1252 })()
1260 })()
1253
1261
1254 </script>
1262 </script>
@@ -1,147 +1,151 b''
1 ## snippet for sidebar elements
1 ## snippet for sidebar elements
2 ## usage:
2 ## usage:
3 ## <%namespace name="sidebar" file="/base/sidebar.mako"/>
3 ## <%namespace name="sidebar" file="/base/sidebar.mako"/>
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, 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'
11 def sorter(entry):
11 def sorter(entry):
12 user_id = entry.author.user_id
12 user_id = entry.author.user_id
13 resolved = '1' if entry.resolved else '0'
13 resolved = '1' if entry.resolved else '0'
14 if user_id == c.rhodecode_user.user_id:
14 if user_id == c.rhodecode_user.user_id:
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 else:
18 else:
19 cls_ = 'comments-content-table'
19 cls_ = 'comments-content-table'
20 def sorter(entry):
20 def sorter(entry):
21 user_id = entry.author.user_id
21 user_id = entry.author.user_id
22 return '{}'.format(str(entry.comment_id).zfill(10000))
22 return '{}'.format(str(entry.comment_id).zfill(10000))
23
23
24 existing_ids = existing_ids or []
24 existing_ids = existing_ids or []
25
25
26 %>
26 %>
27
27
28 <table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}">
28 <table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}">
29
29
30 % for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))):
30 % for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))):
31 <%
31 <%
32 display = ''
32 display = ''
33 _cls = ''
33 _cls = ''
34 ## Extra precaution to not show drafts in the sidebar for todo/comments
35 if comment_obj.draft:
36 continue
34 %>
37 %>
35
38
39
36 <%
40 <%
37 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
41 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
38 prev_comment_ver_index = 0
42 prev_comment_ver_index = 0
39 if loop_obj.previous:
43 if loop_obj.previous:
40 prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', []))
44 prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', []))
41
45
42 ver_info = None
46 ver_info = None
43 if getattr(c, 'versions', []):
47 if getattr(c, 'versions', []):
44 ver_info = c.versions[comment_ver_index-1] if comment_ver_index else None
48 ver_info = c.versions[comment_ver_index-1] if comment_ver_index else None
45 %>
49 %>
46 <% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %>
50 <% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %>
47 <% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %>
51 <% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %>
48 <%
52 <%
49 if (prev_comment_ver_index > comment_ver_index):
53 if (prev_comment_ver_index > comment_ver_index):
50 comments_ver_divider = comment_ver_index
54 comments_ver_divider = comment_ver_index
51 else:
55 else:
52 comments_ver_divider = None
56 comments_ver_divider = None
53 %>
57 %>
54
58
55 % if todo_comments:
59 % if todo_comments:
56 % if comment_obj.resolved:
60 % if comment_obj.resolved:
57 <% _cls = 'resolved-todo' %>
61 <% _cls = 'resolved-todo' %>
58 <% display = 'none' %>
62 <% display = 'none' %>
59 % endif
63 % endif
60 % else:
64 % else:
61 ## SKIP TODOs we display them in other area
65 ## SKIP TODOs we display them in other area
62 % if comment_obj.is_todo:
66 % if comment_obj.is_todo:
63 <% display = 'none' %>
67 <% display = 'none' %>
64 % endif
68 % endif
65 ## Skip outdated comments
69 ## Skip outdated comments
66 % if comment_obj.outdated:
70 % if comment_obj.outdated:
67 <% display = 'none' %>
71 <% display = 'none' %>
68 <% _cls = 'hidden-comment' %>
72 <% _cls = 'hidden-comment' %>
69 % endif
73 % endif
70 % endif
74 % endif
71
75
72 % if not todo_comments and comments_ver_divider:
76 % if not todo_comments and comments_ver_divider:
73 <tr class="old-comments-marker">
77 <tr class="old-comments-marker">
74 <td colspan="3">
78 <td colspan="3">
75 % if ver_info:
79 % if ver_info:
76 <code>v${comments_ver_divider} ${h.age_component(ver_info.created_on, time_is_local=True, tooltip=False)}</code>
80 <code>v${comments_ver_divider} ${h.age_component(ver_info.created_on, time_is_local=True, tooltip=False)}</code>
77 % else:
81 % else:
78 <code>v${comments_ver_divider}</code>
82 <code>v${comments_ver_divider}</code>
79 % endif
83 % endif
80 </td>
84 </td>
81 </tr>
85 </tr>
82
86
83 % endif
87 % endif
84
88
85 <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
89 <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
86 <td class="td-todo-number">
90 <td class="td-todo-number">
87 <%
91 <%
88 version_info = ''
92 version_info = ''
89 if is_pr:
93 if is_pr:
90 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
94 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
91 %>
95 %>
92 ## new comments, since refresh
96 ## new comments, since refresh
93 % if existing_ids and comment_obj.comment_id not in existing_ids:
97 % if existing_ids and comment_obj.comment_id not in existing_ids:
94 <div class="tooltip" style="position: absolute; left: 8px; color: #682668" title="New comment">
98 <div class="tooltip" style="position: absolute; left: 8px; color: #682668" title="New comment">
95 !
99 !
96 </div>
100 </div>
97 % endif
101 % endif
98
102
99 <%
103 <%
100 data = h.json.dumps({
104 data = h.json.dumps({
101 'comment_id': comment_obj.comment_id,
105 'comment_id': comment_obj.comment_id,
102 'version_info': version_info,
106 'version_info': version_info,
103 'file_name': comment_obj.f_path,
107 'file_name': comment_obj.f_path,
104 'line_no': comment_obj.line_no,
108 'line_no': comment_obj.line_no,
105 'outdated': comment_obj.outdated,
109 'outdated': comment_obj.outdated,
106 'inline': comment_obj.is_inline,
110 'inline': comment_obj.is_inline,
107 'is_todo': comment_obj.is_todo,
111 'is_todo': comment_obj.is_todo,
108 'created_on': h.format_date(comment_obj.created_on),
112 'created_on': h.format_date(comment_obj.created_on),
109 'datetime': '{}{}'.format(comment_obj.created_on, h.get_timezone(comment_obj.created_on, time_is_local=True)),
113 'datetime': '{}{}'.format(comment_obj.created_on, h.get_timezone(comment_obj.created_on, time_is_local=True)),
110 'review_status': (comment_obj.review_status or '')
114 'review_status': (comment_obj.review_status or '')
111 })
115 })
112
116
113 if comment_obj.outdated:
117 if comment_obj.outdated:
114 icon = 'icon-comment-toggle'
118 icon = 'icon-comment-toggle'
115 elif comment_obj.is_inline:
119 elif comment_obj.is_inline:
116 icon = 'icon-code'
120 icon = 'icon-code'
117 else:
121 else:
118 icon = 'icon-comment'
122 icon = 'icon-comment'
119 %>
123 %>
120
124
121 <i id="commentHovercard${comment_obj.comment_id}"
125 <i id="commentHovercard${comment_obj.comment_id}"
122 class="${icon} tooltip-hovercard"
126 class="${icon} tooltip-hovercard"
123 data-hovercard-url="javascript:sidebarComment(${comment_obj.comment_id})"
127 data-hovercard-url="javascript:sidebarComment(${comment_obj.comment_id})"
124 data-comment-json-b64='${h.b64(data)}'>
128 data-comment-json-b64='${h.b64(data)}'>
125 </i>
129 </i>
126
130
127 </td>
131 </td>
128
132
129 <td class="td-todo-gravatar">
133 <td class="td-todo-gravatar">
130 ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
134 ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
131 </td>
135 </td>
132 <td class="todo-comment-text-wrapper">
136 <td class="todo-comment-text-wrapper">
133 <div class="todo-comment-text ${('todo-resolved' if comment_obj.resolved else '')}">
137 <div class="todo-comment-text ${('todo-resolved' if comment_obj.resolved else '')}">
134 <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
138 <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
135 href="#comment-${comment_obj.comment_id}"
139 href="#comment-${comment_obj.comment_id}"
136 onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
140 onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
137
141
138 ${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}
142 ${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}
139 </a>
143 </a>
140 </div>
144 </div>
141 </td>
145 </td>
142 </tr>
146 </tr>
143 % endfor
147 % endfor
144
148
145 </table>
149 </table>
146
150
147 </%def> No newline at end of file
151 </%def>
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now