##// END OF EJS Templates
comments: fixed compare view comments.
marcink -
r1331:350c3c75 default
parent child Browse files
Show More
@@ -1,480 +1,483 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 commit controller for RhodeCode showing changes between commits
22 commit controller for RhodeCode showing changes between commits
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from collections import defaultdict
27 from collections import defaultdict
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29
29
30 from pylons import tmpl_context as c, request, response
30 from pylons import tmpl_context as c, request, response
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33
33
34 from rhodecode.lib import auth
34 from rhodecode.lib import auth
35 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib import diffs, codeblocks
36 from rhodecode.lib.auth import (
36 from rhodecode.lib.auth import (
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.base import BaseRepoController, render
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 import rhodecode.lib.helpers as h
41 import rhodecode.lib.helpers as h
42 from rhodecode.lib.utils import action_logger, jsonify
42 from rhodecode.lib.utils import action_logger, jsonify
43 from rhodecode.lib.utils2 import safe_unicode
43 from rhodecode.lib.utils2 import safe_unicode
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def _update_with_GET(params, GET):
57 def _update_with_GET(params, GET):
58 for k in ['diff1', 'diff2', 'diff']:
58 for k in ['diff1', 'diff2', 'diff']:
59 params[k] += GET.getall(k)
59 params[k] += GET.getall(k)
60
60
61
61
62 def get_ignore_ws(fid, GET):
62 def get_ignore_ws(fid, GET):
63 ig_ws_global = GET.get('ignorews')
63 ig_ws_global = GET.get('ignorews')
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 if ig_ws:
65 if ig_ws:
66 try:
66 try:
67 return int(ig_ws[0].split(':')[-1])
67 return int(ig_ws[0].split(':')[-1])
68 except Exception:
68 except Exception:
69 pass
69 pass
70 return ig_ws_global
70 return ig_ws_global
71
71
72
72
73 def _ignorews_url(GET, fileid=None):
73 def _ignorews_url(GET, fileid=None):
74 fileid = str(fileid) if fileid else None
74 fileid = str(fileid) if fileid else None
75 params = defaultdict(list)
75 params = defaultdict(list)
76 _update_with_GET(params, GET)
76 _update_with_GET(params, GET)
77 label = _('Show whitespace')
77 label = _('Show whitespace')
78 tooltiplbl = _('Show whitespace for all diffs')
78 tooltiplbl = _('Show whitespace for all diffs')
79 ig_ws = get_ignore_ws(fileid, GET)
79 ig_ws = get_ignore_ws(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
81
81
82 if ig_ws is None:
82 if ig_ws is None:
83 params['ignorews'] += [1]
83 params['ignorews'] += [1]
84 label = _('Ignore whitespace')
84 label = _('Ignore whitespace')
85 tooltiplbl = _('Ignore whitespace for all diffs')
85 tooltiplbl = _('Ignore whitespace for all diffs')
86 ctx_key = 'context'
86 ctx_key = 'context'
87 ctx_val = ln_ctx
87 ctx_val = ln_ctx
88
88
89 # if we have passed in ln_ctx pass it along to our params
89 # if we have passed in ln_ctx pass it along to our params
90 if ln_ctx:
90 if ln_ctx:
91 params[ctx_key] += [ctx_val]
91 params[ctx_key] += [ctx_val]
92
92
93 if fileid:
93 if fileid:
94 params['anchor'] = 'a_' + fileid
94 params['anchor'] = 'a_' + fileid
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96
96
97
97
98 def get_line_ctx(fid, GET):
98 def get_line_ctx(fid, GET):
99 ln_ctx_global = GET.get('context')
99 ln_ctx_global = GET.get('context')
100 if fid:
100 if fid:
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 else:
102 else:
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 if ln_ctx:
105 if ln_ctx:
106 ln_ctx = [ln_ctx]
106 ln_ctx = [ln_ctx]
107
107
108 if ln_ctx:
108 if ln_ctx:
109 retval = ln_ctx[0].split(':')[-1]
109 retval = ln_ctx[0].split(':')[-1]
110 else:
110 else:
111 retval = ln_ctx_global
111 retval = ln_ctx_global
112
112
113 try:
113 try:
114 return int(retval)
114 return int(retval)
115 except Exception:
115 except Exception:
116 return 3
116 return 3
117
117
118
118
119 def _context_url(GET, fileid=None):
119 def _context_url(GET, fileid=None):
120 """
120 """
121 Generates a url for context lines.
121 Generates a url for context lines.
122
122
123 :param fileid:
123 :param fileid:
124 """
124 """
125
125
126 fileid = str(fileid) if fileid else None
126 fileid = str(fileid) if fileid else None
127 ig_ws = get_ignore_ws(fileid, GET)
127 ig_ws = get_ignore_ws(fileid, GET)
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129
129
130 params = defaultdict(list)
130 params = defaultdict(list)
131 _update_with_GET(params, GET)
131 _update_with_GET(params, GET)
132
132
133 if ln_ctx > 0:
133 if ln_ctx > 0:
134 params['context'] += [ln_ctx]
134 params['context'] += [ln_ctx]
135
135
136 if ig_ws:
136 if ig_ws:
137 ig_ws_key = 'ignorews'
137 ig_ws_key = 'ignorews'
138 ig_ws_val = 1
138 ig_ws_val = 1
139 params[ig_ws_key] += [ig_ws_val]
139 params[ig_ws_key] += [ig_ws_val]
140
140
141 lbl = _('Increase context')
141 lbl = _('Increase context')
142 tooltiplbl = _('Increase context for all diffs')
142 tooltiplbl = _('Increase context for all diffs')
143
143
144 if fileid:
144 if fileid:
145 params['anchor'] = 'a_' + fileid
145 params['anchor'] = 'a_' + fileid
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147
147
148
148
149 class ChangesetController(BaseRepoController):
149 class ChangesetController(BaseRepoController):
150
150
151 def __before__(self):
151 def __before__(self):
152 super(ChangesetController, self).__before__()
152 super(ChangesetController, self).__before__()
153 c.affected_files_cut_off = 60
153 c.affected_files_cut_off = 60
154
154
155 def _index(self, commit_id_range, method):
155 def _index(self, commit_id_range, method):
156 c.ignorews_url = _ignorews_url
156 c.ignorews_url = _ignorews_url
157 c.context_url = _context_url
157 c.context_url = _context_url
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159
159
160 # fetch global flags of ignore ws or context lines
160 # fetch global flags of ignore ws or context lines
161 context_lcl = get_line_ctx('', request.GET)
161 context_lcl = get_line_ctx('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163
163
164 # diff_limit will cut off the whole diff if the limit is applied
164 # diff_limit will cut off the whole diff if the limit is applied
165 # otherwise it will just hide the big files from the front-end
165 # otherwise it will just hide the big files from the front-end
166 diff_limit = self.cut_off_limit_diff
166 diff_limit = self.cut_off_limit_diff
167 file_limit = self.cut_off_limit_file
167 file_limit = self.cut_off_limit_file
168
168
169 # get ranges of commit ids if preset
169 # get ranges of commit ids if preset
170 commit_range = commit_id_range.split('...')[:2]
170 commit_range = commit_id_range.split('...')[:2]
171
171
172 try:
172 try:
173 pre_load = ['affected_files', 'author', 'branch', 'date',
173 pre_load = ['affected_files', 'author', 'branch', 'date',
174 'message', 'parents']
174 'message', 'parents']
175
175
176 if len(commit_range) == 2:
176 if len(commit_range) == 2:
177 commits = c.rhodecode_repo.get_commits(
177 commits = c.rhodecode_repo.get_commits(
178 start_id=commit_range[0], end_id=commit_range[1],
178 start_id=commit_range[0], end_id=commit_range[1],
179 pre_load=pre_load)
179 pre_load=pre_load)
180 commits = list(commits)
180 commits = list(commits)
181 else:
181 else:
182 commits = [c.rhodecode_repo.get_commit(
182 commits = [c.rhodecode_repo.get_commit(
183 commit_id=commit_id_range, pre_load=pre_load)]
183 commit_id=commit_id_range, pre_load=pre_load)]
184
184
185 c.commit_ranges = commits
185 c.commit_ranges = commits
186 if not c.commit_ranges:
186 if not c.commit_ranges:
187 raise RepositoryError(
187 raise RepositoryError(
188 'The commit range returned an empty result')
188 'The commit range returned an empty result')
189 except CommitDoesNotExistError:
189 except CommitDoesNotExistError:
190 msg = _('No such commit exists for this repository')
190 msg = _('No such commit exists for this repository')
191 h.flash(msg, category='error')
191 h.flash(msg, category='error')
192 raise HTTPNotFound()
192 raise HTTPNotFound()
193 except Exception:
193 except Exception:
194 log.exception("General failure")
194 log.exception("General failure")
195 raise HTTPNotFound()
195 raise HTTPNotFound()
196
196
197 c.changes = OrderedDict()
197 c.changes = OrderedDict()
198 c.lines_added = 0
198 c.lines_added = 0
199 c.lines_deleted = 0
199 c.lines_deleted = 0
200
200
201 # auto collapse if we have more than limit
201 # auto collapse if we have more than limit
202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
204
204
205 c.commit_statuses = ChangesetStatus.STATUSES
205 c.commit_statuses = ChangesetStatus.STATUSES
206 c.inline_comments = []
206 c.inline_comments = []
207 c.files = []
207 c.files = []
208
208
209 c.statuses = []
209 c.statuses = []
210 c.comments = []
210 c.comments = []
211 if len(c.commit_ranges) == 1:
211 if len(c.commit_ranges) == 1:
212 commit = c.commit_ranges[0]
212 commit = c.commit_ranges[0]
213 c.comments = CommentsModel().get_comments(
213 c.comments = CommentsModel().get_comments(
214 c.rhodecode_db_repo.repo_id,
214 c.rhodecode_db_repo.repo_id,
215 revision=commit.raw_id)
215 revision=commit.raw_id)
216 c.statuses.append(ChangesetStatusModel().get_status(
216 c.statuses.append(ChangesetStatusModel().get_status(
217 c.rhodecode_db_repo.repo_id, commit.raw_id))
217 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 # comments from PR
218 # comments from PR
219 statuses = ChangesetStatusModel().get_statuses(
219 statuses = ChangesetStatusModel().get_statuses(
220 c.rhodecode_db_repo.repo_id, commit.raw_id,
220 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 with_revisions=True)
221 with_revisions=True)
222 prs = set(st.pull_request for st in statuses
222 prs = set(st.pull_request for st in statuses
223 if st.pull_request is not None)
223 if st.pull_request is not None)
224 # from associated statuses, check the pull requests, and
224 # from associated statuses, check the pull requests, and
225 # show comments from them
225 # show comments from them
226 for pr in prs:
226 for pr in prs:
227 c.comments.extend(pr.comments)
227 c.comments.extend(pr.comments)
228
228
229 # Iterate over ranges (default commit view is always one commit)
229 # Iterate over ranges (default commit view is always one commit)
230 for commit in c.commit_ranges:
230 for commit in c.commit_ranges:
231 c.changes[commit.raw_id] = []
231 c.changes[commit.raw_id] = []
232
232
233 commit2 = commit
233 commit2 = commit
234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
235
235
236 _diff = c.rhodecode_repo.get_diff(
236 _diff = c.rhodecode_repo.get_diff(
237 commit1, commit2,
237 commit1, commit2,
238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
239 diff_processor = diffs.DiffProcessor(
239 diff_processor = diffs.DiffProcessor(
240 _diff, format='newdiff', diff_limit=diff_limit,
240 _diff, format='newdiff', diff_limit=diff_limit,
241 file_limit=file_limit, show_full_diff=fulldiff)
241 file_limit=file_limit, show_full_diff=fulldiff)
242
242
243 commit_changes = OrderedDict()
243 commit_changes = OrderedDict()
244 if method == 'show':
244 if method == 'show':
245 _parsed = diff_processor.prepare()
245 _parsed = diff_processor.prepare()
246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
247
247
248 _parsed = diff_processor.prepare()
248 _parsed = diff_processor.prepare()
249
249
250 def _node_getter(commit):
250 def _node_getter(commit):
251 def get_node(fname):
251 def get_node(fname):
252 try:
252 try:
253 return commit.get_node(fname)
253 return commit.get_node(fname)
254 except NodeDoesNotExistError:
254 except NodeDoesNotExistError:
255 return None
255 return None
256 return get_node
256 return get_node
257
257
258 inline_comments = CommentsModel().get_inline_comments(
258 inline_comments = CommentsModel().get_inline_comments(
259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
260 c.inline_cnt = CommentsModel().get_inline_comments_count(
260 c.inline_cnt = CommentsModel().get_inline_comments_count(
261 inline_comments)
261 inline_comments)
262
262
263 diffset = codeblocks.DiffSet(
263 diffset = codeblocks.DiffSet(
264 repo_name=c.repo_name,
264 repo_name=c.repo_name,
265 source_node_getter=_node_getter(commit1),
265 source_node_getter=_node_getter(commit1),
266 target_node_getter=_node_getter(commit2),
266 target_node_getter=_node_getter(commit2),
267 comments=inline_comments
267 comments=inline_comments
268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
269 c.changes[commit.raw_id] = diffset
269 c.changes[commit.raw_id] = diffset
270 else:
270 else:
271 # downloads/raw we only need RAW diff nothing else
271 # downloads/raw we only need RAW diff nothing else
272 diff = diff_processor.as_raw()
272 diff = diff_processor.as_raw()
273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
274
274
275 # sort comments by how they were generated
275 # sort comments by how they were generated
276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
277
277
278
278
279 if len(c.commit_ranges) == 1:
279 if len(c.commit_ranges) == 1:
280 c.commit = c.commit_ranges[0]
280 c.commit = c.commit_ranges[0]
281 c.parent_tmpl = ''.join(
281 c.parent_tmpl = ''.join(
282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
283 if method == 'download':
283 if method == 'download':
284 response.content_type = 'text/plain'
284 response.content_type = 'text/plain'
285 response.content_disposition = (
285 response.content_disposition = (
286 'attachment; filename=%s.diff' % commit_id_range[:12])
286 'attachment; filename=%s.diff' % commit_id_range[:12])
287 return diff
287 return diff
288 elif method == 'patch':
288 elif method == 'patch':
289 response.content_type = 'text/plain'
289 response.content_type = 'text/plain'
290 c.diff = safe_unicode(diff)
290 c.diff = safe_unicode(diff)
291 return render('changeset/patch_changeset.mako')
291 return render('changeset/patch_changeset.mako')
292 elif method == 'raw':
292 elif method == 'raw':
293 response.content_type = 'text/plain'
293 response.content_type = 'text/plain'
294 return diff
294 return diff
295 elif method == 'show':
295 elif method == 'show':
296 if len(c.commit_ranges) == 1:
296 if len(c.commit_ranges) == 1:
297 return render('changeset/changeset.mako')
297 return render('changeset/changeset.mako')
298 else:
298 else:
299 c.ancestor = None
299 c.ancestor = None
300 c.target_repo = c.rhodecode_db_repo
300 c.target_repo = c.rhodecode_db_repo
301 return render('changeset/changeset_range.mako')
301 return render('changeset/changeset_range.mako')
302
302
303 @LoginRequired()
303 @LoginRequired()
304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
305 'repository.admin')
305 'repository.admin')
306 def index(self, revision, method='show'):
306 def index(self, revision, method='show'):
307 return self._index(revision, method=method)
307 return self._index(revision, method=method)
308
308
309 @LoginRequired()
309 @LoginRequired()
310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
311 'repository.admin')
311 'repository.admin')
312 def changeset_raw(self, revision):
312 def changeset_raw(self, revision):
313 return self._index(revision, method='raw')
313 return self._index(revision, method='raw')
314
314
315 @LoginRequired()
315 @LoginRequired()
316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
317 'repository.admin')
317 'repository.admin')
318 def changeset_patch(self, revision):
318 def changeset_patch(self, revision):
319 return self._index(revision, method='patch')
319 return self._index(revision, method='patch')
320
320
321 @LoginRequired()
321 @LoginRequired()
322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
323 'repository.admin')
323 'repository.admin')
324 def changeset_download(self, revision):
324 def changeset_download(self, revision):
325 return self._index(revision, method='download')
325 return self._index(revision, method='download')
326
326
327 @LoginRequired()
327 @LoginRequired()
328 @NotAnonymous()
328 @NotAnonymous()
329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
330 'repository.admin')
330 'repository.admin')
331 @auth.CSRFRequired()
331 @auth.CSRFRequired()
332 @jsonify
332 @jsonify
333 def comment(self, repo_name, revision):
333 def comment(self, repo_name, revision):
334 commit_id = revision
334 commit_id = revision
335 status = request.POST.get('changeset_status', None)
335 status = request.POST.get('changeset_status', None)
336 text = request.POST.get('text')
336 text = request.POST.get('text')
337 comment_type = request.POST.get('comment_type')
337 comment_type = request.POST.get('comment_type')
338 resolves_comment_id = request.POST.get('resolves_comment_id', None)
338 resolves_comment_id = request.POST.get('resolves_comment_id', None)
339
339
340 if status:
340 if status:
341 text = text or (_('Status change %(transition_icon)s %(status)s')
341 text = text or (_('Status change %(transition_icon)s %(status)s')
342 % {'transition_icon': '>',
342 % {'transition_icon': '>',
343 'status': ChangesetStatus.get_status_lbl(status)})
343 'status': ChangesetStatus.get_status_lbl(status)})
344
344
345 multi_commit_ids = filter(
345 multi_commit_ids = []
346 lambda s: s not in ['', None],
346 for _commit_id in request.POST.get('commit_ids', '').split(','):
347 request.POST.get('commit_ids', '').split(','),)
347 if _commit_id not in ['', None, EmptyCommit.raw_id]:
348 if _commit_id not in multi_commit_ids:
349 multi_commit_ids.append(_commit_id)
348
350
349 commit_ids = multi_commit_ids or [commit_id]
351 commit_ids = multi_commit_ids or [commit_id]
352
350 comment = None
353 comment = None
351 for current_id in filter(None, commit_ids):
354 for current_id in filter(None, commit_ids):
352 c.co = comment = CommentsModel().create(
355 c.co = comment = CommentsModel().create(
353 text=text,
356 text=text,
354 repo=c.rhodecode_db_repo.repo_id,
357 repo=c.rhodecode_db_repo.repo_id,
355 user=c.rhodecode_user.user_id,
358 user=c.rhodecode_user.user_id,
356 commit_id=current_id,
359 commit_id=current_id,
357 f_path=request.POST.get('f_path'),
360 f_path=request.POST.get('f_path'),
358 line_no=request.POST.get('line'),
361 line_no=request.POST.get('line'),
359 status_change=(ChangesetStatus.get_status_lbl(status)
362 status_change=(ChangesetStatus.get_status_lbl(status)
360 if status else None),
363 if status else None),
361 status_change_type=status,
364 status_change_type=status,
362 comment_type=comment_type,
365 comment_type=comment_type,
363 resolves_comment_id=resolves_comment_id
366 resolves_comment_id=resolves_comment_id
364 )
367 )
365 c.inline_comment = True if comment.line_no else False
368 c.inline_comment = True if comment.line_no else False
366
369
367 # get status if set !
370 # get status if set !
368 if status:
371 if status:
369 # if latest status was from pull request and it's closed
372 # if latest status was from pull request and it's closed
370 # disallow changing status !
373 # disallow changing status !
371 # dont_allow_on_closed_pull_request = True !
374 # dont_allow_on_closed_pull_request = True !
372
375
373 try:
376 try:
374 ChangesetStatusModel().set_status(
377 ChangesetStatusModel().set_status(
375 c.rhodecode_db_repo.repo_id,
378 c.rhodecode_db_repo.repo_id,
376 status,
379 status,
377 c.rhodecode_user.user_id,
380 c.rhodecode_user.user_id,
378 comment,
381 comment,
379 revision=current_id,
382 revision=current_id,
380 dont_allow_on_closed_pull_request=True
383 dont_allow_on_closed_pull_request=True
381 )
384 )
382 except StatusChangeOnClosedPullRequestError:
385 except StatusChangeOnClosedPullRequestError:
383 msg = _('Changing the status of a commit associated with '
386 msg = _('Changing the status of a commit associated with '
384 'a closed pull request is not allowed')
387 'a closed pull request is not allowed')
385 log.exception(msg)
388 log.exception(msg)
386 h.flash(msg, category='warning')
389 h.flash(msg, category='warning')
387 return redirect(h.url(
390 return redirect(h.url(
388 'changeset_home', repo_name=repo_name,
391 'changeset_home', repo_name=repo_name,
389 revision=current_id))
392 revision=current_id))
390
393
391 # finalize, commit and redirect
394 # finalize, commit and redirect
392 Session().commit()
395 Session().commit()
393
396
394 data = {
397 data = {
395 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
398 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
396 }
399 }
397 if comment:
400 if comment:
398 data.update(comment.get_dict())
401 data.update(comment.get_dict())
399 data.update({'rendered_text':
402 data.update({'rendered_text':
400 render('changeset/changeset_comment_block.mako')})
403 render('changeset/changeset_comment_block.mako')})
401
404
402 return data
405 return data
403
406
404 @LoginRequired()
407 @LoginRequired()
405 @NotAnonymous()
408 @NotAnonymous()
406 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
409 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
407 'repository.admin')
410 'repository.admin')
408 @auth.CSRFRequired()
411 @auth.CSRFRequired()
409 def preview_comment(self):
412 def preview_comment(self):
410 # Technically a CSRF token is not needed as no state changes with this
413 # Technically a CSRF token is not needed as no state changes with this
411 # call. However, as this is a POST is better to have it, so automated
414 # call. However, as this is a POST is better to have it, so automated
412 # tools don't flag it as potential CSRF.
415 # tools don't flag it as potential CSRF.
413 # Post is required because the payload could be bigger than the maximum
416 # Post is required because the payload could be bigger than the maximum
414 # allowed by GET.
417 # allowed by GET.
415 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
418 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
416 raise HTTPBadRequest()
419 raise HTTPBadRequest()
417 text = request.POST.get('text')
420 text = request.POST.get('text')
418 renderer = request.POST.get('renderer') or 'rst'
421 renderer = request.POST.get('renderer') or 'rst'
419 if text:
422 if text:
420 return h.render(text, renderer=renderer, mentions=True)
423 return h.render(text, renderer=renderer, mentions=True)
421 return ''
424 return ''
422
425
423 @LoginRequired()
426 @LoginRequired()
424 @NotAnonymous()
427 @NotAnonymous()
425 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
428 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
426 'repository.admin')
429 'repository.admin')
427 @auth.CSRFRequired()
430 @auth.CSRFRequired()
428 @jsonify
431 @jsonify
429 def delete_comment(self, repo_name, comment_id):
432 def delete_comment(self, repo_name, comment_id):
430 comment = ChangesetComment.get(comment_id)
433 comment = ChangesetComment.get(comment_id)
431 if not comment:
434 if not comment:
432 log.debug('Comment with id:%s not found, skipping', comment_id)
435 log.debug('Comment with id:%s not found, skipping', comment_id)
433 # comment already deleted in another call probably
436 # comment already deleted in another call probably
434 return True
437 return True
435
438
436 owner = (comment.author.user_id == c.rhodecode_user.user_id)
439 owner = (comment.author.user_id == c.rhodecode_user.user_id)
437 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
440 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
438 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
441 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
439 CommentsModel().delete(comment=comment)
442 CommentsModel().delete(comment=comment)
440 Session().commit()
443 Session().commit()
441 return True
444 return True
442 else:
445 else:
443 raise HTTPForbidden()
446 raise HTTPForbidden()
444
447
445 @LoginRequired()
448 @LoginRequired()
446 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
449 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
447 'repository.admin')
450 'repository.admin')
448 @jsonify
451 @jsonify
449 def changeset_info(self, repo_name, revision):
452 def changeset_info(self, repo_name, revision):
450 if request.is_xhr:
453 if request.is_xhr:
451 try:
454 try:
452 return c.rhodecode_repo.get_commit(commit_id=revision)
455 return c.rhodecode_repo.get_commit(commit_id=revision)
453 except CommitDoesNotExistError as e:
456 except CommitDoesNotExistError as e:
454 return EmptyCommit(message=str(e))
457 return EmptyCommit(message=str(e))
455 else:
458 else:
456 raise HTTPBadRequest()
459 raise HTTPBadRequest()
457
460
458 @LoginRequired()
461 @LoginRequired()
459 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
462 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
460 'repository.admin')
463 'repository.admin')
461 @jsonify
464 @jsonify
462 def changeset_children(self, repo_name, revision):
465 def changeset_children(self, repo_name, revision):
463 if request.is_xhr:
466 if request.is_xhr:
464 commit = c.rhodecode_repo.get_commit(commit_id=revision)
467 commit = c.rhodecode_repo.get_commit(commit_id=revision)
465 result = {"results": commit.children}
468 result = {"results": commit.children}
466 return result
469 return result
467 else:
470 else:
468 raise HTTPBadRequest()
471 raise HTTPBadRequest()
469
472
470 @LoginRequired()
473 @LoginRequired()
471 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
474 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
472 'repository.admin')
475 'repository.admin')
473 @jsonify
476 @jsonify
474 def changeset_parents(self, repo_name, revision):
477 def changeset_parents(self, repo_name, revision):
475 if request.is_xhr:
478 if request.is_xhr:
476 commit = c.rhodecode_repo.get_commit(commit_id=revision)
479 commit = c.rhodecode_repo.get_commit(commit_id=revision)
477 result = {"results": commit.parents}
480 result = {"results": commit.parents}
478 return result
481 return result
479 else:
482 else:
480 raise HTTPBadRequest()
483 raise HTTPBadRequest()
@@ -1,774 +1,796 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 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 /* Comment form for main and inline comments */
45 /* Comment form for main and inline comments */
46 (function(mod) {
46
47
47 (function(mod) {
48 if (typeof exports == "object" && typeof module == "object") {
48 if (typeof exports == "object" && typeof module == "object") // CommonJS
49 // CommonJS
49 module.exports = mod();
50 module.exports = mod();
50 else // Plain browser env
51 }
51 (this || window).CommentForm = mod();
52 else {
53 // Plain browser env
54 (this || window).CommentForm = mod();
55 }
52
56
53 })(function() {
57 })(function() {
54 "use strict";
58 "use strict";
55
59
56 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
57 if (!(this instanceof CommentForm)) {
61 if (!(this instanceof CommentForm)) {
58 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
59 }
63 }
60
64
61 // bind the element instance to our Form
65 // bind the element instance to our Form
62 $(formElement).get(0).CommentForm = this;
66 $(formElement).get(0).CommentForm = this;
63
67
64 this.withLineNo = function(selector) {
68 this.withLineNo = function(selector) {
65 var lineNo = this.lineNo;
69 var lineNo = this.lineNo;
66 if (lineNo === undefined) {
70 if (lineNo === undefined) {
67 return selector
71 return selector
68 } else {
72 } else {
69 return selector + '_' + lineNo;
73 return selector + '_' + lineNo;
70 }
74 }
71 };
75 };
72
76
73 this.commitId = commitId;
77 this.commitId = commitId;
74 this.pullRequestId = pullRequestId;
78 this.pullRequestId = pullRequestId;
75 this.lineNo = lineNo;
79 this.lineNo = lineNo;
76 this.initAutocompleteActions = initAutocompleteActions;
80 this.initAutocompleteActions = initAutocompleteActions;
77
81
78 this.previewButton = this.withLineNo('#preview-btn');
82 this.previewButton = this.withLineNo('#preview-btn');
79 this.previewContainer = this.withLineNo('#preview-container');
83 this.previewContainer = this.withLineNo('#preview-container');
80
84
81 this.previewBoxSelector = this.withLineNo('#preview-box');
85 this.previewBoxSelector = this.withLineNo('#preview-box');
82
86
83 this.editButton = this.withLineNo('#edit-btn');
87 this.editButton = this.withLineNo('#edit-btn');
84 this.editContainer = this.withLineNo('#edit-container');
88 this.editContainer = this.withLineNo('#edit-container');
85 this.cancelButton = this.withLineNo('#cancel-btn');
89 this.cancelButton = this.withLineNo('#cancel-btn');
86 this.commentType = this.withLineNo('#comment_type');
90 this.commentType = this.withLineNo('#comment_type');
87
91
88 this.resolvesId = null;
92 this.resolvesId = null;
89 this.resolvesActionId = null;
93 this.resolvesActionId = null;
90
94
91 this.cmBox = this.withLineNo('#text');
95 this.cmBox = this.withLineNo('#text');
92 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
96 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
93
97
94 this.statusChange = this.withLineNo('#change_status');
98 this.statusChange = this.withLineNo('#change_status');
95
99
96 this.submitForm = formElement;
100 this.submitForm = formElement;
97 this.submitButton = $(this.submitForm).find('input[type="submit"]');
101 this.submitButton = $(this.submitForm).find('input[type="submit"]');
98 this.submitButtonText = this.submitButton.val();
102 this.submitButtonText = this.submitButton.val();
99
103
100 this.previewUrl = pyroutes.url('changeset_comment_preview',
104 this.previewUrl = pyroutes.url('changeset_comment_preview',
101 {'repo_name': templateContext.repo_name});
105 {'repo_name': templateContext.repo_name});
102
106
103 if (resolvesCommentId){
107 if (resolvesCommentId){
104 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
108 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
105 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
109 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
106 $(this.commentType).prop('disabled', true);
110 $(this.commentType).prop('disabled', true);
107 $(this.commentType).addClass('disabled');
111 $(this.commentType).addClass('disabled');
108
112
109 // disable select
113 // disable select
110 setTimeout(function() {
114 setTimeout(function() {
111 $(self.statusChange).select2('readonly', true);
115 $(self.statusChange).select2('readonly', true);
112 }, 10);
116 }, 10);
113
117
114 var resolvedInfo = (
118 var resolvedInfo = (
115 '<li class="">' +
119 '<li class="">' +
116 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
120 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
117 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
121 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
118 '</li>'
122 '</li>'
119 ).format(resolvesCommentId, _gettext('resolve comment'));
123 ).format(resolvesCommentId, _gettext('resolve comment'));
120 $(resolvedInfo).insertAfter($(this.commentType).parent());
124 $(resolvedInfo).insertAfter($(this.commentType).parent());
121 }
125 }
122
126
123 // based on commitId, or pullRequestId decide where do we submit
127 // based on commitId, or pullRequestId decide where do we submit
124 // out data
128 // out data
125 if (this.commitId){
129 if (this.commitId){
126 this.submitUrl = pyroutes.url('changeset_comment',
130 this.submitUrl = pyroutes.url('changeset_comment',
127 {'repo_name': templateContext.repo_name,
131 {'repo_name': templateContext.repo_name,
128 'revision': this.commitId});
132 'revision': this.commitId});
129 this.selfUrl = pyroutes.url('changeset_home',
133 this.selfUrl = pyroutes.url('changeset_home',
130 {'repo_name': templateContext.repo_name,
134 {'repo_name': templateContext.repo_name,
131 'revision': this.commitId});
135 'revision': this.commitId});
132
136
133 } else if (this.pullRequestId) {
137 } else if (this.pullRequestId) {
134 this.submitUrl = pyroutes.url('pullrequest_comment',
138 this.submitUrl = pyroutes.url('pullrequest_comment',
135 {'repo_name': templateContext.repo_name,
139 {'repo_name': templateContext.repo_name,
136 'pull_request_id': this.pullRequestId});
140 'pull_request_id': this.pullRequestId});
137 this.selfUrl = pyroutes.url('pullrequest_show',
141 this.selfUrl = pyroutes.url('pullrequest_show',
138 {'repo_name': templateContext.repo_name,
142 {'repo_name': templateContext.repo_name,
139 'pull_request_id': this.pullRequestId});
143 'pull_request_id': this.pullRequestId});
140
144
141 } else {
145 } else {
142 throw new Error(
146 throw new Error(
143 'CommentForm requires pullRequestId, or commitId to be specified.')
147 'CommentForm requires pullRequestId, or commitId to be specified.')
144 }
148 }
145
149
146 // FUNCTIONS and helpers
150 // FUNCTIONS and helpers
147 var self = this;
151 var self = this;
148
152
149 this.isInline = function(){
153 this.isInline = function(){
150 return this.lineNo && this.lineNo != 'general';
154 return this.lineNo && this.lineNo != 'general';
151 };
155 };
152
156
153 this.getCmInstance = function(){
157 this.getCmInstance = function(){
154 return this.cm
158 return this.cm
155 };
159 };
156
160
157 this.setPlaceholder = function(placeholder) {
161 this.setPlaceholder = function(placeholder) {
158 var cm = this.getCmInstance();
162 var cm = this.getCmInstance();
159 if (cm){
163 if (cm){
160 cm.setOption('placeholder', placeholder);
164 cm.setOption('placeholder', placeholder);
161 }
165 }
162 };
166 };
163
167
164 this.getCommentStatus = function() {
168 this.getCommentStatus = function() {
165 return $(this.submitForm).find(this.statusChange).val();
169 return $(this.submitForm).find(this.statusChange).val();
166 };
170 };
167 this.getCommentType = function() {
171 this.getCommentType = function() {
168 return $(this.submitForm).find(this.commentType).val();
172 return $(this.submitForm).find(this.commentType).val();
169 };
173 };
170
174
171 this.getResolvesId = function() {
175 this.getResolvesId = function() {
172 return $(this.submitForm).find(this.resolvesId).val() || null;
176 return $(this.submitForm).find(this.resolvesId).val() || null;
173 };
177 };
174 this.markCommentResolved = function(resolvedCommentId){
178 this.markCommentResolved = function(resolvedCommentId){
175 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
179 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
176 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
180 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
177 };
181 };
178
182
179 this.isAllowedToSubmit = function() {
183 this.isAllowedToSubmit = function() {
180 return !$(this.submitButton).prop('disabled');
184 return !$(this.submitButton).prop('disabled');
181 };
185 };
182
186
183 this.initStatusChangeSelector = function(){
187 this.initStatusChangeSelector = function(){
184 var formatChangeStatus = function(state, escapeMarkup) {
188 var formatChangeStatus = function(state, escapeMarkup) {
185 var originalOption = state.element;
189 var originalOption = state.element;
186 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
190 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
187 '<span>' + escapeMarkup(state.text) + '</span>';
191 '<span>' + escapeMarkup(state.text) + '</span>';
188 };
192 };
189 var formatResult = function(result, container, query, escapeMarkup) {
193 var formatResult = function(result, container, query, escapeMarkup) {
190 return formatChangeStatus(result, escapeMarkup);
194 return formatChangeStatus(result, escapeMarkup);
191 };
195 };
192
196
193 var formatSelection = function(data, container, escapeMarkup) {
197 var formatSelection = function(data, container, escapeMarkup) {
194 return formatChangeStatus(data, escapeMarkup);
198 return formatChangeStatus(data, escapeMarkup);
195 };
199 };
196
200
197 $(this.submitForm).find(this.statusChange).select2({
201 $(this.submitForm).find(this.statusChange).select2({
198 placeholder: _gettext('Status Review'),
202 placeholder: _gettext('Status Review'),
199 formatResult: formatResult,
203 formatResult: formatResult,
200 formatSelection: formatSelection,
204 formatSelection: formatSelection,
201 containerCssClass: "drop-menu status_box_menu",
205 containerCssClass: "drop-menu status_box_menu",
202 dropdownCssClass: "drop-menu-dropdown",
206 dropdownCssClass: "drop-menu-dropdown",
203 dropdownAutoWidth: true,
207 dropdownAutoWidth: true,
204 minimumResultsForSearch: -1
208 minimumResultsForSearch: -1
205 });
209 });
206 $(this.submitForm).find(this.statusChange).on('change', function() {
210 $(this.submitForm).find(this.statusChange).on('change', function() {
207 var status = self.getCommentStatus();
211 var status = self.getCommentStatus();
208 if (status && !self.isInline()) {
212 if (status && !self.isInline()) {
209 $(self.submitButton).prop('disabled', false);
213 $(self.submitButton).prop('disabled', false);
210 }
214 }
211
215
212 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
216 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
213 self.setPlaceholder(placeholderText)
217 self.setPlaceholder(placeholderText)
214 })
218 })
215 };
219 };
216
220
217 // reset the comment form into it's original state
221 // reset the comment form into it's original state
218 this.resetCommentFormState = function(content) {
222 this.resetCommentFormState = function(content) {
219 content = content || '';
223 content = content || '';
220
224
221 $(this.editContainer).show();
225 $(this.editContainer).show();
222 $(this.editButton).parent().addClass('active');
226 $(this.editButton).parent().addClass('active');
223
227
224 $(this.previewContainer).hide();
228 $(this.previewContainer).hide();
225 $(this.previewButton).parent().removeClass('active');
229 $(this.previewButton).parent().removeClass('active');
226
230
227 this.setActionButtonsDisabled(true);
231 this.setActionButtonsDisabled(true);
228 self.cm.setValue(content);
232 self.cm.setValue(content);
229 self.cm.setOption("readOnly", false);
233 self.cm.setOption("readOnly", false);
230
234
231 if (this.resolvesId) {
235 if (this.resolvesId) {
232 // destroy the resolve action
236 // destroy the resolve action
233 $(this.resolvesId).parent().remove();
237 $(this.resolvesId).parent().remove();
234 }
238 }
235
239
236 $(this.statusChange).select2('readonly', false);
240 $(this.statusChange).select2('readonly', false);
237 };
241 };
238
242
243 this.globalSubmitSuccessCallback = function(){};
244
239 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
245 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
240 failHandler = failHandler || function() {};
246 failHandler = failHandler || function() {};
241 var postData = toQueryString(postData);
247 var postData = toQueryString(postData);
242 var request = $.ajax({
248 var request = $.ajax({
243 url: url,
249 url: url,
244 type: 'POST',
250 type: 'POST',
245 data: postData,
251 data: postData,
246 headers: {'X-PARTIAL-XHR': true}
252 headers: {'X-PARTIAL-XHR': true}
247 })
253 })
248 .done(function(data) {
254 .done(function(data) {
249 successHandler(data);
255 successHandler(data);
250 })
256 })
251 .fail(function(data, textStatus, errorThrown){
257 .fail(function(data, textStatus, errorThrown){
252 alert(
258 alert(
253 "Error while submitting comment.\n" +
259 "Error while submitting comment.\n" +
254 "Error code {0} ({1}).".format(data.status, data.statusText));
260 "Error code {0} ({1}).".format(data.status, data.statusText));
255 failHandler()
261 failHandler()
256 });
262 });
257 return request;
263 return request;
258 };
264 };
259
265
260 // overwrite a submitHandler, we need to do it for inline comments
266 // overwrite a submitHandler, we need to do it for inline comments
261 this.setHandleFormSubmit = function(callback) {
267 this.setHandleFormSubmit = function(callback) {
262 this.handleFormSubmit = callback;
268 this.handleFormSubmit = callback;
263 };
269 };
264
270
271 // overwrite a submitSuccessHandler
272 this.setGlobalSubmitSuccessCallback = function(callback) {
273 this.globalSubmitSuccessCallback = callback;
274 };
275
265 // default handler for for submit for main comments
276 // default handler for for submit for main comments
266 this.handleFormSubmit = function() {
277 this.handleFormSubmit = function() {
267 var text = self.cm.getValue();
278 var text = self.cm.getValue();
268 var status = self.getCommentStatus();
279 var status = self.getCommentStatus();
269 var commentType = self.getCommentType();
280 var commentType = self.getCommentType();
270 var resolvesCommentId = self.getResolvesId();
281 var resolvesCommentId = self.getResolvesId();
271
282
272 if (text === "" && !status) {
283 if (text === "" && !status) {
273 return;
284 return;
274 }
285 }
275
286
276 var excludeCancelBtn = false;
287 var excludeCancelBtn = false;
277 var submitEvent = true;
288 var submitEvent = true;
278 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
289 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
279 self.cm.setOption("readOnly", true);
290 self.cm.setOption("readOnly", true);
280
291
281 var postData = {
292 var postData = {
282 'text': text,
293 'text': text,
283 'changeset_status': status,
294 'changeset_status': status,
284 'comment_type': commentType,
295 'comment_type': commentType,
285 'csrf_token': CSRF_TOKEN
296 'csrf_token': CSRF_TOKEN
286 };
297 };
287 if (resolvesCommentId){
298 if (resolvesCommentId){
288 postData['resolves_comment_id'] = resolvesCommentId;
299 postData['resolves_comment_id'] = resolvesCommentId;
289 }
300 }
301
290 var submitSuccessCallback = function(o) {
302 var submitSuccessCallback = function(o) {
291 if (status) {
303 if (status) {
292 location.reload(true);
304 location.reload(true);
293 } else {
305 } else {
294 $('#injected_page_comments').append(o.rendered_text);
306 $('#injected_page_comments').append(o.rendered_text);
295 self.resetCommentFormState();
307 self.resetCommentFormState();
296 timeagoActivate();
308 timeagoActivate();
297
309
298 // mark visually which comment was resolved
310 // mark visually which comment was resolved
299 if (resolvesCommentId) {
311 if (resolvesCommentId) {
300 self.markCommentResolved(resolvesCommentId);
312 self.markCommentResolved(resolvesCommentId);
301 }
313 }
302 }
314 }
315
316 // run global callback on submit
317 self.globalSubmitSuccessCallback();
318
303 };
319 };
304 var submitFailCallback = function(){
320 var submitFailCallback = function(){
305 self.resetCommentFormState(text);
321 self.resetCommentFormState(text);
306 };
322 };
307 self.submitAjaxPOST(
323 self.submitAjaxPOST(
308 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
324 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
309 };
325 };
310
326
311 this.previewSuccessCallback = function(o) {
327 this.previewSuccessCallback = function(o) {
312 $(self.previewBoxSelector).html(o);
328 $(self.previewBoxSelector).html(o);
313 $(self.previewBoxSelector).removeClass('unloaded');
329 $(self.previewBoxSelector).removeClass('unloaded');
314
330
315 // swap buttons, making preview active
331 // swap buttons, making preview active
316 $(self.previewButton).parent().addClass('active');
332 $(self.previewButton).parent().addClass('active');
317 $(self.editButton).parent().removeClass('active');
333 $(self.editButton).parent().removeClass('active');
318
334
319 // unlock buttons
335 // unlock buttons
320 self.setActionButtonsDisabled(false);
336 self.setActionButtonsDisabled(false);
321 };
337 };
322
338
323 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
339 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
324 excludeCancelBtn = excludeCancelBtn || false;
340 excludeCancelBtn = excludeCancelBtn || false;
325 submitEvent = submitEvent || false;
341 submitEvent = submitEvent || false;
326
342
327 $(this.editButton).prop('disabled', state);
343 $(this.editButton).prop('disabled', state);
328 $(this.previewButton).prop('disabled', state);
344 $(this.previewButton).prop('disabled', state);
329
345
330 if (!excludeCancelBtn) {
346 if (!excludeCancelBtn) {
331 $(this.cancelButton).prop('disabled', state);
347 $(this.cancelButton).prop('disabled', state);
332 }
348 }
333
349
334 var submitState = state;
350 var submitState = state;
335 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
351 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
336 // if the value of commit review status is set, we allow
352 // if the value of commit review status is set, we allow
337 // submit button, but only on Main form, lineNo means inline
353 // submit button, but only on Main form, lineNo means inline
338 submitState = false
354 submitState = false
339 }
355 }
340 $(this.submitButton).prop('disabled', submitState);
356 $(this.submitButton).prop('disabled', submitState);
341 if (submitEvent) {
357 if (submitEvent) {
342 $(this.submitButton).val(_gettext('Submitting...'));
358 $(this.submitButton).val(_gettext('Submitting...'));
343 } else {
359 } else {
344 $(this.submitButton).val(this.submitButtonText);
360 $(this.submitButton).val(this.submitButtonText);
345 }
361 }
346
362
347 };
363 };
348
364
349 // lock preview/edit/submit buttons on load, but exclude cancel button
365 // lock preview/edit/submit buttons on load, but exclude cancel button
350 var excludeCancelBtn = true;
366 var excludeCancelBtn = true;
351 this.setActionButtonsDisabled(true, excludeCancelBtn);
367 this.setActionButtonsDisabled(true, excludeCancelBtn);
352
368
353 // anonymous users don't have access to initialized CM instance
369 // anonymous users don't have access to initialized CM instance
354 if (this.cm !== undefined){
370 if (this.cm !== undefined){
355 this.cm.on('change', function(cMirror) {
371 this.cm.on('change', function(cMirror) {
356 if (cMirror.getValue() === "") {
372 if (cMirror.getValue() === "") {
357 self.setActionButtonsDisabled(true, excludeCancelBtn)
373 self.setActionButtonsDisabled(true, excludeCancelBtn)
358 } else {
374 } else {
359 self.setActionButtonsDisabled(false, excludeCancelBtn)
375 self.setActionButtonsDisabled(false, excludeCancelBtn)
360 }
376 }
361 });
377 });
362 }
378 }
363
379
364 $(this.editButton).on('click', function(e) {
380 $(this.editButton).on('click', function(e) {
365 e.preventDefault();
381 e.preventDefault();
366
382
367 $(self.previewButton).parent().removeClass('active');
383 $(self.previewButton).parent().removeClass('active');
368 $(self.previewContainer).hide();
384 $(self.previewContainer).hide();
369
385
370 $(self.editButton).parent().addClass('active');
386 $(self.editButton).parent().addClass('active');
371 $(self.editContainer).show();
387 $(self.editContainer).show();
372
388
373 });
389 });
374
390
375 $(this.previewButton).on('click', function(e) {
391 $(this.previewButton).on('click', function(e) {
376 e.preventDefault();
392 e.preventDefault();
377 var text = self.cm.getValue();
393 var text = self.cm.getValue();
378
394
379 if (text === "") {
395 if (text === "") {
380 return;
396 return;
381 }
397 }
382
398
383 var postData = {
399 var postData = {
384 'text': text,
400 'text': text,
385 'renderer': templateContext.visual.default_renderer,
401 'renderer': templateContext.visual.default_renderer,
386 'csrf_token': CSRF_TOKEN
402 'csrf_token': CSRF_TOKEN
387 };
403 };
388
404
389 // lock ALL buttons on preview
405 // lock ALL buttons on preview
390 self.setActionButtonsDisabled(true);
406 self.setActionButtonsDisabled(true);
391
407
392 $(self.previewBoxSelector).addClass('unloaded');
408 $(self.previewBoxSelector).addClass('unloaded');
393 $(self.previewBoxSelector).html(_gettext('Loading ...'));
409 $(self.previewBoxSelector).html(_gettext('Loading ...'));
394
410
395 $(self.editContainer).hide();
411 $(self.editContainer).hide();
396 $(self.previewContainer).show();
412 $(self.previewContainer).show();
397
413
398 // by default we reset state of comment preserving the text
414 // by default we reset state of comment preserving the text
399 var previewFailCallback = function(){
415 var previewFailCallback = function(){
400 self.resetCommentFormState(text)
416 self.resetCommentFormState(text)
401 };
417 };
402 self.submitAjaxPOST(
418 self.submitAjaxPOST(
403 self.previewUrl, postData, self.previewSuccessCallback,
419 self.previewUrl, postData, self.previewSuccessCallback,
404 previewFailCallback);
420 previewFailCallback);
405
421
406 $(self.previewButton).parent().addClass('active');
422 $(self.previewButton).parent().addClass('active');
407 $(self.editButton).parent().removeClass('active');
423 $(self.editButton).parent().removeClass('active');
408 });
424 });
409
425
410 $(this.submitForm).submit(function(e) {
426 $(this.submitForm).submit(function(e) {
411 e.preventDefault();
427 e.preventDefault();
412 var allowedToSubmit = self.isAllowedToSubmit();
428 var allowedToSubmit = self.isAllowedToSubmit();
413 if (!allowedToSubmit){
429 if (!allowedToSubmit){
414 return false;
430 return false;
415 }
431 }
416 self.handleFormSubmit();
432 self.handleFormSubmit();
417 });
433 });
418
434
419 }
435 }
420
436
421 return CommentForm;
437 return CommentForm;
422 });
438 });
423
439
424 /* comments controller */
440 /* comments controller */
425 var CommentsController = function() {
441 var CommentsController = function() {
426 var mainComment = '#text';
442 var mainComment = '#text';
427 var self = this;
443 var self = this;
428
444
429 this.cancelComment = function(node) {
445 this.cancelComment = function(node) {
430 var $node = $(node);
446 var $node = $(node);
431 var $td = $node.closest('td');
447 var $td = $node.closest('td');
432 $node.closest('.comment-inline-form').remove();
448 $node.closest('.comment-inline-form').remove();
433 return false;
449 return false;
434 };
450 };
435
451
436 this.getLineNumber = function(node) {
452 this.getLineNumber = function(node) {
437 var $node = $(node);
453 var $node = $(node);
438 return $node.closest('td').attr('data-line-number');
454 return $node.closest('td').attr('data-line-number');
439 };
455 };
440
456
441 this.scrollToComment = function(node, offset, outdated) {
457 this.scrollToComment = function(node, offset, outdated) {
442 var outdated = outdated || false;
458 var outdated = outdated || false;
443 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
459 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
444
460
445 if (!node) {
461 if (!node) {
446 node = $('.comment-selected');
462 node = $('.comment-selected');
447 if (!node.length) {
463 if (!node.length) {
448 node = $('comment-current')
464 node = $('comment-current')
449 }
465 }
450 }
466 }
451 $comment = $(node).closest(klass);
467 $comment = $(node).closest(klass);
452 $comments = $(klass);
468 $comments = $(klass);
453
469
454 $('.comment-selected').removeClass('comment-selected');
470 $('.comment-selected').removeClass('comment-selected');
455
471
456 var nextIdx = $(klass).index($comment) + offset;
472 var nextIdx = $(klass).index($comment) + offset;
457 if (nextIdx >= $comments.length) {
473 if (nextIdx >= $comments.length) {
458 nextIdx = 0;
474 nextIdx = 0;
459 }
475 }
460 var $next = $(klass).eq(nextIdx);
476 var $next = $(klass).eq(nextIdx);
461 var $cb = $next.closest('.cb');
477 var $cb = $next.closest('.cb');
462 $cb.removeClass('cb-collapsed');
478 $cb.removeClass('cb-collapsed');
463
479
464 var $filediffCollapseState = $cb.closest('.filediff').prev();
480 var $filediffCollapseState = $cb.closest('.filediff').prev();
465 $filediffCollapseState.prop('checked', false);
481 $filediffCollapseState.prop('checked', false);
466 $next.addClass('comment-selected');
482 $next.addClass('comment-selected');
467 scrollToElement($next);
483 scrollToElement($next);
468 return false;
484 return false;
469 };
485 };
470
486
471 this.nextComment = function(node) {
487 this.nextComment = function(node) {
472 return self.scrollToComment(node, 1);
488 return self.scrollToComment(node, 1);
473 };
489 };
474
490
475 this.prevComment = function(node) {
491 this.prevComment = function(node) {
476 return self.scrollToComment(node, -1);
492 return self.scrollToComment(node, -1);
477 };
493 };
478
494
479 this.nextOutdatedComment = function(node) {
495 this.nextOutdatedComment = function(node) {
480 return self.scrollToComment(node, 1, true);
496 return self.scrollToComment(node, 1, true);
481 };
497 };
482
498
483 this.prevOutdatedComment = function(node) {
499 this.prevOutdatedComment = function(node) {
484 return self.scrollToComment(node, -1, true);
500 return self.scrollToComment(node, -1, true);
485 };
501 };
486
502
487 this.deleteComment = function(node) {
503 this.deleteComment = function(node) {
488 if (!confirm(_gettext('Delete this comment?'))) {
504 if (!confirm(_gettext('Delete this comment?'))) {
489 return false;
505 return false;
490 }
506 }
491 var $node = $(node);
507 var $node = $(node);
492 var $td = $node.closest('td');
508 var $td = $node.closest('td');
493 var $comment = $node.closest('.comment');
509 var $comment = $node.closest('.comment');
494 var comment_id = $comment.attr('data-comment-id');
510 var comment_id = $comment.attr('data-comment-id');
495 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
511 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
496 var postData = {
512 var postData = {
497 '_method': 'delete',
513 '_method': 'delete',
498 'csrf_token': CSRF_TOKEN
514 'csrf_token': CSRF_TOKEN
499 };
515 };
500
516
501 $comment.addClass('comment-deleting');
517 $comment.addClass('comment-deleting');
502 $comment.hide('fast');
518 $comment.hide('fast');
503
519
504 var success = function(response) {
520 var success = function(response) {
505 $comment.remove();
521 $comment.remove();
506 return false;
522 return false;
507 };
523 };
508 var failure = function(data, textStatus, xhr) {
524 var failure = function(data, textStatus, xhr) {
509 alert("error processing request: " + textStatus);
525 alert("error processing request: " + textStatus);
510 $comment.show('fast');
526 $comment.show('fast');
511 $comment.removeClass('comment-deleting');
527 $comment.removeClass('comment-deleting');
512 return false;
528 return false;
513 };
529 };
514 ajaxPOST(url, postData, success, failure);
530 ajaxPOST(url, postData, success, failure);
515 };
531 };
516
532
517 this.toggleWideMode = function (node) {
533 this.toggleWideMode = function (node) {
518 if ($('#content').hasClass('wrapper')) {
534 if ($('#content').hasClass('wrapper')) {
519 $('#content').removeClass("wrapper");
535 $('#content').removeClass("wrapper");
520 $('#content').addClass("wide-mode-wrapper");
536 $('#content').addClass("wide-mode-wrapper");
521 $(node).addClass('btn-success');
537 $(node).addClass('btn-success');
522 } else {
538 } else {
523 $('#content').removeClass("wide-mode-wrapper");
539 $('#content').removeClass("wide-mode-wrapper");
524 $('#content').addClass("wrapper");
540 $('#content').addClass("wrapper");
525 $(node).removeClass('btn-success');
541 $(node).removeClass('btn-success');
526 }
542 }
527 return false;
543 return false;
528 };
544 };
529
545
530 this.toggleComments = function(node, show) {
546 this.toggleComments = function(node, show) {
531 var $filediff = $(node).closest('.filediff');
547 var $filediff = $(node).closest('.filediff');
532 if (show === true) {
548 if (show === true) {
533 $filediff.removeClass('hide-comments');
549 $filediff.removeClass('hide-comments');
534 } else if (show === false) {
550 } else if (show === false) {
535 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
551 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
536 $filediff.addClass('hide-comments');
552 $filediff.addClass('hide-comments');
537 } else {
553 } else {
538 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
554 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
539 $filediff.toggleClass('hide-comments');
555 $filediff.toggleClass('hide-comments');
540 }
556 }
541 return false;
557 return false;
542 };
558 };
543
559
544 this.toggleLineComments = function(node) {
560 this.toggleLineComments = function(node) {
545 self.toggleComments(node, true);
561 self.toggleComments(node, true);
546 var $node = $(node);
562 var $node = $(node);
547 $node.closest('tr').toggleClass('hide-line-comments');
563 $node.closest('tr').toggleClass('hide-line-comments');
548 };
564 };
549
565
550 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
566 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
551 var pullRequestId = templateContext.pull_request_data.pull_request_id;
567 var pullRequestId = templateContext.pull_request_data.pull_request_id;
552 var commitId = templateContext.commit_data.commit_id;
568 var commitId = templateContext.commit_data.commit_id;
553
569
554 var commentForm = new CommentForm(
570 var commentForm = new CommentForm(
555 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
571 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
556 var cm = commentForm.getCmInstance();
572 var cm = commentForm.getCmInstance();
557
573
558 if (resolvesCommentId){
574 if (resolvesCommentId){
559 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
575 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
560 }
576 }
561
577
562 setTimeout(function() {
578 setTimeout(function() {
563 // callbacks
579 // callbacks
564 if (cm !== undefined) {
580 if (cm !== undefined) {
565 commentForm.setPlaceholder(placeholderText);
581 commentForm.setPlaceholder(placeholderText);
566 if (commentForm.isInline()) {
582 if (commentForm.isInline()) {
567 cm.focus();
583 cm.focus();
568 cm.refresh();
584 cm.refresh();
569 }
585 }
570 }
586 }
571 }, 10);
587 }, 10);
572
588
573 // trigger scrolldown to the resolve comment, since it might be away
589 // trigger scrolldown to the resolve comment, since it might be away
574 // from the clicked
590 // from the clicked
575 if (resolvesCommentId){
591 if (resolvesCommentId){
576 var actionNode = $(commentForm.resolvesActionId).offset();
592 var actionNode = $(commentForm.resolvesActionId).offset();
577
593
578 setTimeout(function() {
594 setTimeout(function() {
579 if (actionNode) {
595 if (actionNode) {
580 $('body, html').animate({scrollTop: actionNode.top}, 10);
596 $('body, html').animate({scrollTop: actionNode.top}, 10);
581 }
597 }
582 }, 100);
598 }, 100);
583 }
599 }
584
600
585 return commentForm;
601 return commentForm;
586 };
602 };
587
603
588 this.createGeneralComment = function(lineNo, placeholderText, resolvesCommentId){
604 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
589
605
590 var tmpl = $('#cb-comment-general-form-template').html();
606 var tmpl = $('#cb-comment-general-form-template').html();
591 tmpl = tmpl.format(null, 'general');
607 tmpl = tmpl.format(null, 'general');
592 var $form = $(tmpl);
608 var $form = $(tmpl);
593
609
594 var curForm = $('#cb-comment-general-form-placeholder').find('form');
610 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
611 var curForm = $formPlaceholder.find('form');
595 if (curForm){
612 if (curForm){
596 curForm.remove();
613 curForm.remove();
597 }
614 }
598 $('#cb-comment-general-form-placeholder').append($form);
615 $formPlaceholder.append($form);
599
616
600 var _form = $($form[0]);
617 var _form = $($form[0]);
601 var commentForm = this.createCommentForm(
618 var commentForm = this.createCommentForm(
602 _form, lineNo, placeholderText, true, resolvesCommentId);
619 _form, lineNo, placeholderText, true, resolvesCommentId);
603 commentForm.initStatusChangeSelector();
620 commentForm.initStatusChangeSelector();
621
622 return commentForm;
604 };
623 };
605
624
606 this.createComment = function(node, resolutionComment) {
625 this.createComment = function(node, resolutionComment) {
607 var resolvesCommentId = resolutionComment || null;
626 var resolvesCommentId = resolutionComment || null;
608 var $node = $(node);
627 var $node = $(node);
609 var $td = $node.closest('td');
628 var $td = $node.closest('td');
610 var $form = $td.find('.comment-inline-form');
629 var $form = $td.find('.comment-inline-form');
611
630
612 if (!$form.length) {
631 if (!$form.length) {
613
632
614 var $filediff = $node.closest('.filediff');
633 var $filediff = $node.closest('.filediff');
615 $filediff.removeClass('hide-comments');
634 $filediff.removeClass('hide-comments');
616 var f_path = $filediff.attr('data-f-path');
635 var f_path = $filediff.attr('data-f-path');
617 var lineno = self.getLineNumber(node);
636 var lineno = self.getLineNumber(node);
618 // create a new HTML from template
637 // create a new HTML from template
619 var tmpl = $('#cb-comment-inline-form-template').html();
638 var tmpl = $('#cb-comment-inline-form-template').html();
620 tmpl = tmpl.format(f_path, lineno);
639 tmpl = tmpl.format(f_path, lineno);
621 $form = $(tmpl);
640 $form = $(tmpl);
622
641
623 var $comments = $td.find('.inline-comments');
642 var $comments = $td.find('.inline-comments');
624 if (!$comments.length) {
643 if (!$comments.length) {
625 $comments = $(
644 $comments = $(
626 $('#cb-comments-inline-container-template').html());
645 $('#cb-comments-inline-container-template').html());
627 $td.append($comments);
646 $td.append($comments);
628 }
647 }
629
648
630 $td.find('.cb-comment-add-button').before($form);
649 $td.find('.cb-comment-add-button').before($form);
631
650
632 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
651 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
633 var _form = $($form[0]).find('form');
652 var _form = $($form[0]).find('form');
634
653
635 var commentForm = this.createCommentForm(
654 var commentForm = this.createCommentForm(
636 _form, lineno, placeholderText, false, resolvesCommentId);
655 _form, lineno, placeholderText, false, resolvesCommentId);
637
656
638 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
657 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
639 form: _form,
658 form: _form,
640 parent: $td[0],
659 parent: $td[0],
641 lineno: lineno,
660 lineno: lineno,
642 f_path: f_path}
661 f_path: f_path}
643 );
662 );
644
663
645 // set a CUSTOM submit handler for inline comments.
664 // set a CUSTOM submit handler for inline comments.
646 commentForm.setHandleFormSubmit(function(o) {
665 commentForm.setHandleFormSubmit(function(o) {
647 var text = commentForm.cm.getValue();
666 var text = commentForm.cm.getValue();
648 var commentType = commentForm.getCommentType();
667 var commentType = commentForm.getCommentType();
649 var resolvesCommentId = commentForm.getResolvesId();
668 var resolvesCommentId = commentForm.getResolvesId();
650
669
651 if (text === "") {
670 if (text === "") {
652 return;
671 return;
653 }
672 }
654
673
655 if (lineno === undefined) {
674 if (lineno === undefined) {
656 alert('missing line !');
675 alert('missing line !');
657 return;
676 return;
658 }
677 }
659 if (f_path === undefined) {
678 if (f_path === undefined) {
660 alert('missing file path !');
679 alert('missing file path !');
661 return;
680 return;
662 }
681 }
663
682
664 var excludeCancelBtn = false;
683 var excludeCancelBtn = false;
665 var submitEvent = true;
684 var submitEvent = true;
666 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
685 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
667 commentForm.cm.setOption("readOnly", true);
686 commentForm.cm.setOption("readOnly", true);
668 var postData = {
687 var postData = {
669 'text': text,
688 'text': text,
670 'f_path': f_path,
689 'f_path': f_path,
671 'line': lineno,
690 'line': lineno,
672 'comment_type': commentType,
691 'comment_type': commentType,
673 'csrf_token': CSRF_TOKEN
692 'csrf_token': CSRF_TOKEN
674 };
693 };
675 if (resolvesCommentId){
694 if (resolvesCommentId){
676 postData['resolves_comment_id'] = resolvesCommentId;
695 postData['resolves_comment_id'] = resolvesCommentId;
677 }
696 }
678
697
679 var submitSuccessCallback = function(json_data) {
698 var submitSuccessCallback = function(json_data) {
680 $form.remove();
699 $form.remove();
681 try {
700 try {
682 var html = json_data.rendered_text;
701 var html = json_data.rendered_text;
683 var lineno = json_data.line_no;
702 var lineno = json_data.line_no;
684 var target_id = json_data.target_id;
703 var target_id = json_data.target_id;
685
704
686 $comments.find('.cb-comment-add-button').before(html);
705 $comments.find('.cb-comment-add-button').before(html);
687
706
688 //mark visually which comment was resolved
707 //mark visually which comment was resolved
689 if (resolvesCommentId) {
708 if (resolvesCommentId) {
690 commentForm.markCommentResolved(resolvesCommentId);
709 commentForm.markCommentResolved(resolvesCommentId);
691 }
710 }
692
711
712 // run global callback on submit
713 commentForm.globalSubmitSuccessCallback();
714
693 } catch (e) {
715 } catch (e) {
694 console.error(e);
716 console.error(e);
695 }
717 }
696
718
697 // re trigger the linkification of next/prev navigation
719 // re trigger the linkification of next/prev navigation
698 linkifyComments($('.inline-comment-injected'));
720 linkifyComments($('.inline-comment-injected'));
699 timeagoActivate();
721 timeagoActivate();
700 commentForm.setActionButtonsDisabled(false);
722 commentForm.setActionButtonsDisabled(false);
701
723
702 };
724 };
703 var submitFailCallback = function(){
725 var submitFailCallback = function(){
704 commentForm.resetCommentFormState(text)
726 commentForm.resetCommentFormState(text)
705 };
727 };
706 commentForm.submitAjaxPOST(
728 commentForm.submitAjaxPOST(
707 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
729 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
708 });
730 });
709 }
731 }
710
732
711 $form.addClass('comment-inline-form-open');
733 $form.addClass('comment-inline-form-open');
712 };
734 };
713
735
714 this.createResolutionComment = function(commentId){
736 this.createResolutionComment = function(commentId){
715 // hide the trigger text
737 // hide the trigger text
716 $('#resolve-comment-{0}'.format(commentId)).hide();
738 $('#resolve-comment-{0}'.format(commentId)).hide();
717
739
718 var comment = $('#comment-'+commentId);
740 var comment = $('#comment-'+commentId);
719 var commentData = comment.data();
741 var commentData = comment.data();
720 if (commentData.commentInline) {
742 if (commentData.commentInline) {
721 this.createComment(comment, commentId)
743 this.createComment(comment, commentId)
722 } else {
744 } else {
723 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
745 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
724 }
746 }
725
747
726 return false;
748 return false;
727 };
749 };
728
750
729 this.submitResolution = function(commentId){
751 this.submitResolution = function(commentId){
730 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
752 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
731 var commentForm = form.get(0).CommentForm;
753 var commentForm = form.get(0).CommentForm;
732
754
733 var cm = commentForm.getCmInstance();
755 var cm = commentForm.getCmInstance();
734 var renderer = templateContext.visual.default_renderer;
756 var renderer = templateContext.visual.default_renderer;
735 if (renderer == 'rst'){
757 if (renderer == 'rst'){
736 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
758 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
737 } else if (renderer == 'markdown') {
759 } else if (renderer == 'markdown') {
738 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
760 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
739 } else {
761 } else {
740 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
762 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
741 }
763 }
742
764
743 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
765 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
744 form.submit();
766 form.submit();
745 return false;
767 return false;
746 };
768 };
747
769
748 this.renderInlineComments = function(file_comments) {
770 this.renderInlineComments = function(file_comments) {
749 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
771 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
750
772
751 for (var i = 0; i < file_comments.length; i++) {
773 for (var i = 0; i < file_comments.length; i++) {
752 var box = file_comments[i];
774 var box = file_comments[i];
753
775
754 var target_id = $(box).attr('target_id');
776 var target_id = $(box).attr('target_id');
755
777
756 // actually comments with line numbers
778 // actually comments with line numbers
757 var comments = box.children;
779 var comments = box.children;
758
780
759 for (var j = 0; j < comments.length; j++) {
781 for (var j = 0; j < comments.length; j++) {
760 var data = {
782 var data = {
761 'rendered_text': comments[j].outerHTML,
783 'rendered_text': comments[j].outerHTML,
762 'line_no': $(comments[j]).attr('line'),
784 'line_no': $(comments[j]).attr('line'),
763 'target_id': target_id
785 'target_id': target_id
764 };
786 };
765 }
787 }
766 }
788 }
767
789
768 // since order of injection is random, we're now re-iterating
790 // since order of injection is random, we're now re-iterating
769 // from correct order and filling in links
791 // from correct order and filling in links
770 linkifyComments($('.inline-comment-injected'));
792 linkifyComments($('.inline-comment-injected'));
771 firefoxAnchorFix();
793 firefoxAnchorFix();
772 };
794 };
773
795
774 };
796 };
@@ -1,348 +1,400 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6 <%namespace name="base" file="/base/base.mako"/>
7
7
8 <%def name="comment_block(comment, inline=False)">
8 <%def name="comment_block(comment, inline=False)">
9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
10 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
11
11
12 <div class="comment
12 <div class="comment
13 ${'comment-inline' if inline else 'comment-general'}
13 ${'comment-inline' if inline else 'comment-general'}
14 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
14 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
15 id="comment-${comment.comment_id}"
15 id="comment-${comment.comment_id}"
16 line="${comment.line_no}"
16 line="${comment.line_no}"
17 data-comment-id="${comment.comment_id}"
17 data-comment-id="${comment.comment_id}"
18 data-comment-type="${comment.comment_type}"
18 data-comment-type="${comment.comment_type}"
19 data-comment-inline=${h.json.dumps(inline)}
19 data-comment-inline=${h.json.dumps(inline)}
20 style="${'display: none;' if outdated_at_ver else ''}">
20 style="${'display: none;' if outdated_at_ver else ''}">
21
21
22 <div class="meta">
22 <div class="meta">
23 <div class="comment-type-label">
23 <div class="comment-type-label">
24 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
24 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
25 % if comment.comment_type == 'todo':
25 % if comment.comment_type == 'todo':
26 % if comment.resolved:
26 % if comment.resolved:
27 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
27 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
28 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
28 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
29 </div>
29 </div>
30 % else:
30 % else:
31 <div class="resolved tooltip" style="display: none">
31 <div class="resolved tooltip" style="display: none">
32 <span>${comment.comment_type}</span>
32 <span>${comment.comment_type}</span>
33 </div>
33 </div>
34 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
34 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
35 ${comment.comment_type}
35 ${comment.comment_type}
36 </div>
36 </div>
37 % endif
37 % endif
38 % else:
38 % else:
39 % if comment.resolved_comment:
39 % if comment.resolved_comment:
40 fix
40 fix
41 % else:
41 % else:
42 ${comment.comment_type or 'note'}
42 ${comment.comment_type or 'note'}
43 % endif
43 % endif
44 % endif
44 % endif
45 </div>
45 </div>
46 </div>
46 </div>
47
47
48 <div class="author ${'author-inline' if inline else 'author-general'}">
48 <div class="author ${'author-inline' if inline else 'author-general'}">
49 ${base.gravatar_with_user(comment.author.email, 16)}
49 ${base.gravatar_with_user(comment.author.email, 16)}
50 </div>
50 </div>
51 <div class="date">
51 <div class="date">
52 ${h.age_component(comment.modified_at, time_is_local=True)}
52 ${h.age_component(comment.modified_at, time_is_local=True)}
53 </div>
53 </div>
54 % if inline:
54 % if inline:
55 <span></span>
55 <span></span>
56 % else:
56 % else:
57 <div class="status-change">
57 <div class="status-change">
58 % if comment.pull_request:
58 % if comment.pull_request:
59 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
59 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
60 % if comment.status_change:
60 % if comment.status_change:
61 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
61 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
62 % else:
62 % else:
63 ${_('pull request #%s') % comment.pull_request.pull_request_id}
63 ${_('pull request #%s') % comment.pull_request.pull_request_id}
64 % endif
64 % endif
65 </a>
65 </a>
66 % else:
66 % else:
67 % if comment.status_change:
67 % if comment.status_change:
68 ${_('Status change on commit')}:
68 ${_('Status change on commit')}:
69 % endif
69 % endif
70 % endif
70 % endif
71 </div>
71 </div>
72 % endif
72 % endif
73
73
74 % if comment.status_change:
74 % if comment.status_change:
75 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
75 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
76 <div title="${_('Commit status')}" class="changeset-status-lbl">
76 <div title="${_('Commit status')}" class="changeset-status-lbl">
77 ${comment.status_change[0].status_lbl}
77 ${comment.status_change[0].status_lbl}
78 </div>
78 </div>
79 % endif
79 % endif
80
80
81 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
81 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
82
82
83 <div class="comment-links-block">
83 <div class="comment-links-block">
84
84
85 % if inline:
85 % if inline:
86 % if outdated_at_ver:
86 % if outdated_at_ver:
87 <div class="pr-version-inline">
87 <div class="pr-version-inline">
88 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
88 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
89 <code class="pr-version-num">
89 <code class="pr-version-num">
90 outdated ${'v{}'.format(pr_index_ver)}
90 outdated ${'v{}'.format(pr_index_ver)}
91 </code>
91 </code>
92 </a>
92 </a>
93 </div>
93 </div>
94 |
94 |
95 % endif
95 % endif
96 % else:
96 % else:
97 % if comment.pull_request_version_id and pr_index_ver:
97 % if comment.pull_request_version_id and pr_index_ver:
98 |
98 |
99 <div class="pr-version">
99 <div class="pr-version">
100 % if comment.outdated:
100 % if comment.outdated:
101 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
101 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
102 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
102 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
103 </a>
103 </a>
104 % else:
104 % else:
105 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
105 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
106 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
106 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
107 <code class="pr-version-num">
107 <code class="pr-version-num">
108 ${'v{}'.format(pr_index_ver)}
108 ${'v{}'.format(pr_index_ver)}
109 </code>
109 </code>
110 </a>
110 </a>
111 </div>
111 </div>
112 % endif
112 % endif
113 </div>
113 </div>
114 % endif
114 % endif
115 % endif
115 % endif
116
116
117 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
117 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
118 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
118 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
119 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
119 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
120 ## permissions to delete
120 ## permissions to delete
121 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
121 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
122 ## TODO: dan: add edit comment here
122 ## TODO: dan: add edit comment here
123 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
123 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
124 %else:
124 %else:
125 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
125 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
126 %endif
126 %endif
127 %else:
127 %else:
128 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
128 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
129 %endif
129 %endif
130
130
131 %if not outdated_at_ver:
131 %if not outdated_at_ver:
132 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
132 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
133 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
133 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
134 %endif
134 %endif
135
135
136 </div>
136 </div>
137 </div>
137 </div>
138 <div class="text">
138 <div class="text">
139 ${comment.render(mentions=True)|n}
139 ${comment.render(mentions=True)|n}
140 </div>
140 </div>
141
141
142 </div>
142 </div>
143 </%def>
143 </%def>
144
144
145 ## generate main comments
145 ## generate main comments
146 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
146 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
147 <div id="comments">
147 <div id="comments">
148 %for comment in c.comments:
148 %for comment in c.comments:
149 <div id="comment-tr-${comment.comment_id}">
149 <div id="comment-tr-${comment.comment_id}">
150 ## only render comments that are not from pull request, or from
150 ## only render comments that are not from pull request, or from
151 ## pull request and a status change
151 ## pull request and a status change
152 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
152 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
153 ${comment_block(comment)}
153 ${comment_block(comment)}
154 %endif
154 %endif
155 </div>
155 </div>
156 %endfor
156 %endfor
157 ## to anchor ajax comments
157 ## to anchor ajax comments
158 <div id="injected_page_comments"></div>
158 <div id="injected_page_comments"></div>
159 </div>
159 </div>
160 </%def>
160 </%def>
161
161
162
162
163 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
163 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
164
164
165 ## merge status, and merge action
165 ## merge status, and merge action
166 %if is_pull_request:
166 %if is_pull_request:
167 <div class="pull-request-merge">
167 <div class="pull-request-merge">
168 %if c.allowed_to_merge:
168 %if c.allowed_to_merge:
169 <div class="pull-request-wrap">
169 <div class="pull-request-wrap">
170 <div class="pull-right">
170 <div class="pull-right">
171 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
171 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
172 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
172 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
173 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
173 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
174 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
174 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
175 ${h.end_form()}
175 ${h.end_form()}
176 </div>
176 </div>
177 </div>
177 </div>
178 %else:
178 %else:
179 <div class="pull-request-wrap">
179 <div class="pull-request-wrap">
180 <div class="pull-right">
180 <div class="pull-right">
181 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
181 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
182 </div>
182 </div>
183 </div>
183 </div>
184 %endif
184 %endif
185 </div>
185 </div>
186 %endif
186 %endif
187
187
188 <div class="comments">
188 <div class="comments">
189 <%
189 <%
190 if is_pull_request:
190 if is_pull_request:
191 placeholder = _('Leave a comment on this Pull Request.')
191 placeholder = _('Leave a comment on this Pull Request.')
192 elif is_compare:
192 elif is_compare:
193 placeholder = _('Leave a comment on all commits in this range.')
193 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
194 else:
194 else:
195 placeholder = _('Leave a comment on this Commit.')
195 placeholder = _('Leave a comment on this Commit.')
196 %>
196 %>
197
197
198 % if c.rhodecode_user.username != h.DEFAULT_USER:
198 % if c.rhodecode_user.username != h.DEFAULT_USER:
199 <div class="js-template" id="cb-comment-general-form-template">
199 <div class="js-template" id="cb-comment-general-form-template">
200 ## template generated for injection
200 ## template generated for injection
201 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
201 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
202 </div>
202 </div>
203
203
204 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
204 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
205 ## inject form here
205 ## inject form here
206 </div>
206 </div>
207 <script type="text/javascript">
207 <script type="text/javascript">
208 var lineNo = 'general';
208 var lineNo = 'general';
209 var resolvesCommentId = null;
209 var resolvesCommentId = null;
210 Rhodecode.comments.createGeneralComment(lineNo, "${placeholder}", resolvesCommentId)
210 var generalCommentForm = Rhodecode.comments.createGeneralComment(
211 lineNo, "${placeholder}", resolvesCommentId);
212
213 // set custom success callback on rangeCommit
214 % if is_compare:
215 generalCommentForm.setHandleFormSubmit(function(o) {
216 var self = generalCommentForm;
217
218 var text = self.cm.getValue();
219 var status = self.getCommentStatus();
220 var commentType = self.getCommentType();
221
222 if (text === "" && !status) {
223 return;
224 }
225
226 // we can pick which commits we want to make the comment by
227 // selecting them via click on preview pane, this will alter the hidden inputs
228 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
229
230 var commitIds = [];
231 $('#changeset_compare_view_content .compare_select').each(function(el) {
232 var commitId = this.id.replace('row-', '');
233 if ($(this).hasClass('hl') || !cherryPicked) {
234 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
235 commitIds.push(commitId);
236 } else {
237 $("input[data-commit-id='{0}']".format(commitId)).val('')
238 }
239 });
240
241 self.setActionButtonsDisabled(true);
242 self.cm.setOption("readOnly", true);
243 var postData = {
244 'text': text,
245 'changeset_status': status,
246 'comment_type': commentType,
247 'commit_ids': commitIds,
248 'csrf_token': CSRF_TOKEN
249 };
250
251 var submitSuccessCallback = function(o) {
252 location.reload(true);
253 };
254 var submitFailCallback = function(){
255 self.resetCommentFormState(text)
256 };
257 self.submitAjaxPOST(
258 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
259 });
260 % endif
261
262
211 </script>
263 </script>
212 % else:
264 % else:
213 ## form state when not logged in
265 ## form state when not logged in
214 <div class="comment-form ac">
266 <div class="comment-form ac">
215
267
216 <div class="comment-area">
268 <div class="comment-area">
217 <div class="comment-area-header">
269 <div class="comment-area-header">
218 <ul class="nav-links clearfix">
270 <ul class="nav-links clearfix">
219 <li class="active">
271 <li class="active">
220 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
272 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
221 </li>
273 </li>
222 <li class="">
274 <li class="">
223 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
275 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
224 </li>
276 </li>
225 </ul>
277 </ul>
226 </div>
278 </div>
227
279
228 <div class="comment-area-write" style="display: block;">
280 <div class="comment-area-write" style="display: block;">
229 <div id="edit-container">
281 <div id="edit-container">
230 <div style="padding: 40px 0">
282 <div style="padding: 40px 0">
231 ${_('You need to be logged in to leave comments.')}
283 ${_('You need to be logged in to leave comments.')}
232 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
284 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
233 </div>
285 </div>
234 </div>
286 </div>
235 <div id="preview-container" class="clearfix" style="display: none;">
287 <div id="preview-container" class="clearfix" style="display: none;">
236 <div id="preview-box" class="preview-box"></div>
288 <div id="preview-box" class="preview-box"></div>
237 </div>
289 </div>
238 </div>
290 </div>
239
291
240 <div class="comment-area-footer">
292 <div class="comment-area-footer">
241 <div class="toolbar">
293 <div class="toolbar">
242 <div class="toolbar-text">
294 <div class="toolbar-text">
243 </div>
295 </div>
244 </div>
296 </div>
245 </div>
297 </div>
246 </div>
298 </div>
247
299
248 <div class="comment-footer">
300 <div class="comment-footer">
249 </div>
301 </div>
250
302
251 </div>
303 </div>
252 % endif
304 % endif
253
305
254 <script type="text/javascript">
306 <script type="text/javascript">
255 bindToggleButtons();
307 bindToggleButtons();
256 </script>
308 </script>
257 </div>
309 </div>
258 </%def>
310 </%def>
259
311
260
312
261 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
313 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
262 ## comment injected based on assumption that user is logged in
314 ## comment injected based on assumption that user is logged in
263
315
264 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
316 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
265
317
266 <div class="comment-area">
318 <div class="comment-area">
267 <div class="comment-area-header">
319 <div class="comment-area-header">
268 <ul class="nav-links clearfix">
320 <ul class="nav-links clearfix">
269 <li class="active">
321 <li class="active">
270 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
322 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
271 </li>
323 </li>
272 <li class="">
324 <li class="">
273 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
325 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
274 </li>
326 </li>
275 <li class="pull-right">
327 <li class="pull-right">
276 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
328 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
277 % for val in c.visual.comment_types:
329 % for val in c.visual.comment_types:
278 <option value="${val}">${val.upper()}</option>
330 <option value="${val}">${val.upper()}</option>
279 % endfor
331 % endfor
280 </select>
332 </select>
281 </li>
333 </li>
282 </ul>
334 </ul>
283 </div>
335 </div>
284
336
285 <div class="comment-area-write" style="display: block;">
337 <div class="comment-area-write" style="display: block;">
286 <div id="edit-container_${lineno_id}">
338 <div id="edit-container_${lineno_id}">
287 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
339 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
288 </div>
340 </div>
289 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
341 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
290 <div id="preview-box_${lineno_id}" class="preview-box"></div>
342 <div id="preview-box_${lineno_id}" class="preview-box"></div>
291 </div>
343 </div>
292 </div>
344 </div>
293
345
294 <div class="comment-area-footer">
346 <div class="comment-area-footer">
295 <div class="toolbar">
347 <div class="toolbar">
296 <div class="toolbar-text">
348 <div class="toolbar-text">
297 ${(_('Comments parsed using %s syntax with %s support.') % (
349 ${(_('Comments parsed using %s syntax with %s support.') % (
298 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
350 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
299 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
351 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
300 )
352 )
301 )|n}
353 )|n}
302 </div>
354 </div>
303 </div>
355 </div>
304 </div>
356 </div>
305 </div>
357 </div>
306
358
307 <div class="comment-footer">
359 <div class="comment-footer">
308
360
309 % if review_statuses:
361 % if review_statuses:
310 <div class="status_box">
362 <div class="status_box">
311 <select id="change_status_${lineno_id}" name="changeset_status">
363 <select id="change_status_${lineno_id}" name="changeset_status">
312 <option></option> ## Placeholder
364 <option></option> ## Placeholder
313 % for status, lbl in review_statuses:
365 % for status, lbl in review_statuses:
314 <option value="${status}" data-status="${status}">${lbl}</option>
366 <option value="${status}" data-status="${status}">${lbl}</option>
315 %if is_pull_request and change_status and status in ('approved', 'rejected'):
367 %if is_pull_request and change_status and status in ('approved', 'rejected'):
316 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
368 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
317 %endif
369 %endif
318 % endfor
370 % endfor
319 </select>
371 </select>
320 </div>
372 </div>
321 % endif
373 % endif
322
374
323 ## inject extra inputs into the form
375 ## inject extra inputs into the form
324 % if form_extras and isinstance(form_extras, (list, tuple)):
376 % if form_extras and isinstance(form_extras, (list, tuple)):
325 <div id="comment_form_extras">
377 <div id="comment_form_extras">
326 % for form_ex_el in form_extras:
378 % for form_ex_el in form_extras:
327 ${form_ex_el|n}
379 ${form_ex_el|n}
328 % endfor
380 % endfor
329 </div>
381 </div>
330 % endif
382 % endif
331
383
332 <div class="action-buttons">
384 <div class="action-buttons">
333 ## inline for has a file, and line-number together with cancel hide button.
385 ## inline for has a file, and line-number together with cancel hide button.
334 % if form_type == 'inline':
386 % if form_type == 'inline':
335 <input type="hidden" name="f_path" value="{0}">
387 <input type="hidden" name="f_path" value="{0}">
336 <input type="hidden" name="line" value="${lineno_id}">
388 <input type="hidden" name="line" value="${lineno_id}">
337 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
389 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
338 ${_('Cancel')}
390 ${_('Cancel')}
339 </button>
391 </button>
340 % endif
392 % endif
341 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
393 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
342
394
343 </div>
395 </div>
344 </div>
396 </div>
345
397
346 </form>
398 </form>
347
399
348 </%def> No newline at end of file
400 </%def>
@@ -1,377 +1,333 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.mako"/>
2 <%inherit file="/base/base.mako"/>
3 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
3 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
4
4
5 <%def name="title()">
5 <%def name="title()">
6 %if c.compare_home:
6 %if c.compare_home:
7 ${_('%s Compare') % c.repo_name}
7 ${_('%s Compare') % c.repo_name}
8 %else:
8 %else:
9 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.source_repo.repo_name, c.source_ref)} &gt; ${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}
9 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.source_repo.repo_name, c.source_ref)} &gt; ${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}
10 %endif
10 %endif
11 %if c.rhodecode_name:
11 %if c.rhodecode_name:
12 &middot; ${h.branding(c.rhodecode_name)}
12 &middot; ${h.branding(c.rhodecode_name)}
13 %endif
13 %endif
14 </%def>
14 </%def>
15
15
16 <%def name="breadcrumbs_links()">
16 <%def name="breadcrumbs_links()">
17 ${ungettext('%s commit','%s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
17 ${ungettext('%s commit','%s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
18 </%def>
18 </%def>
19
19
20 <%def name="menu_bar_nav()">
20 <%def name="menu_bar_nav()">
21 ${self.menu_items(active='repositories')}
21 ${self.menu_items(active='repositories')}
22 </%def>
22 </%def>
23
23
24 <%def name="menu_bar_subnav()">
24 <%def name="menu_bar_subnav()">
25 ${self.repo_menu(active='compare')}
25 ${self.repo_menu(active='compare')}
26 </%def>
26 </%def>
27
27
28 <%def name="main()">
28 <%def name="main()">
29 <script type="text/javascript">
29 <script type="text/javascript">
30 // set fake commitId on this commit-range page
30 // set fake commitId on this commit-range page
31 templateContext.commit_data.commit_id = "${h.EmptyCommit().raw_id}";
31 templateContext.commit_data.commit_id = "${h.EmptyCommit().raw_id}";
32 </script>
32 </script>
33
33
34 <div class="box">
34 <div class="box">
35 <div class="title">
35 <div class="title">
36 ${self.repo_page_title(c.rhodecode_db_repo)}
36 ${self.repo_page_title(c.rhodecode_db_repo)}
37 </div>
37 </div>
38
38
39 <div class="summary changeset">
39 <div class="summary changeset">
40 <div class="summary-detail">
40 <div class="summary-detail">
41 <div class="summary-detail-header">
41 <div class="summary-detail-header">
42 <span class="breadcrumbs files_location">
42 <span class="breadcrumbs files_location">
43 <h4>
43 <h4>
44 ${_('Compare Commits')}
44 ${_('Compare Commits')}
45 % if c.file_path:
45 % if c.file_path:
46 ${_('for file')} <a href="#${'a_' + h.FID('',c.file_path)}">${c.file_path}</a>
46 ${_('for file')} <a href="#${'a_' + h.FID('',c.file_path)}">${c.file_path}</a>
47 % endif
47 % endif
48
48
49 % if c.commit_ranges:
49 % if c.commit_ranges:
50 <code>
50 <code>
51 r${c.source_commit.revision}:${h.short_id(c.source_commit.raw_id)}...r${c.target_commit.revision}:${h.short_id(c.target_commit.raw_id)}
51 r${c.source_commit.revision}:${h.short_id(c.source_commit.raw_id)}...r${c.target_commit.revision}:${h.short_id(c.target_commit.raw_id)}
52 </code>
52 </code>
53 % endif
53 % endif
54 </h4>
54 </h4>
55 </span>
55 </span>
56 </div>
56 </div>
57
57
58 <div class="fieldset">
58 <div class="fieldset">
59 <div class="left-label">
59 <div class="left-label">
60 ${_('Target')}:
60 ${_('Target')}:
61 </div>
61 </div>
62 <div class="right-content">
62 <div class="right-content">
63 <div>
63 <div>
64 <div class="code-header" >
64 <div class="code-header" >
65 <div class="compare_header">
65 <div class="compare_header">
66 ## The hidden elements are replaced with a select2 widget
66 ## The hidden elements are replaced with a select2 widget
67 ${h.hidden('compare_source')}
67 ${h.hidden('compare_source')}
68 </div>
68 </div>
69 </div>
69 </div>
70 </div>
70 </div>
71 </div>
71 </div>
72 </div>
72 </div>
73
73
74 <div class="fieldset">
74 <div class="fieldset">
75 <div class="left-label">
75 <div class="left-label">
76 ${_('Source')}:
76 ${_('Source')}:
77 </div>
77 </div>
78 <div class="right-content">
78 <div class="right-content">
79 <div>
79 <div>
80 <div class="code-header" >
80 <div class="code-header" >
81 <div class="compare_header">
81 <div class="compare_header">
82 ## The hidden elements are replaced with a select2 widget
82 ## The hidden elements are replaced with a select2 widget
83 ${h.hidden('compare_target')}
83 ${h.hidden('compare_target')}
84 </div>
84 </div>
85 </div>
85 </div>
86 </div>
86 </div>
87 </div>
87 </div>
88 </div>
88 </div>
89
89
90 <div class="fieldset">
90 <div class="fieldset">
91 <div class="left-label">
91 <div class="left-label">
92 ${_('Actions')}:
92 ${_('Actions')}:
93 </div>
93 </div>
94 <div class="right-content">
94 <div class="right-content">
95 <div>
95 <div>
96 <div class="code-header" >
96 <div class="code-header" >
97 <div class="compare_header">
97 <div class="compare_header">
98
98
99 <div class="compare-buttons">
99 <div class="compare-buttons">
100 % if c.compare_home:
100 % if c.compare_home:
101 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
101 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
102
102
103 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
103 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
104 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
104 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
105 <div id="changeset_compare_view_content">
105 <div id="changeset_compare_view_content">
106 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
106 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
107 </div>
107 </div>
108
108
109 % elif c.preview_mode:
109 % elif c.preview_mode:
110 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Compare Commits')}</a>
110 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Compare Commits')}</a>
111 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
111 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
112 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
112 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
113
113
114 % else:
114 % else:
115 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
115 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
116 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}">${_('Swap')}</a>
116 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}">${_('Swap')}</a>
117
117
118 ## allow comment only if there are commits to comment on
118 ## allow comment only if there are commits to comment on
119 % if c.diffset and c.diffset.files and c.commit_ranges:
119 % if c.diffset and c.diffset.files and c.commit_ranges:
120 <a id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</a>
120 <a id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</a>
121 % else:
121 % else:
122 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
122 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
123 % endif
123 % endif
124 % endif
124 % endif
125 </div>
125 </div>
126 </div>
126 </div>
127 </div>
127 </div>
128 </div>
128 </div>
129 </div>
129 </div>
130 </div>
130 </div>
131
131
132 <%doc>
132 <%doc>
133 ##TODO(marcink): implement this and diff menus
133 ##TODO(marcink): implement this and diff menus
134 <div class="fieldset">
134 <div class="fieldset">
135 <div class="left-label">
135 <div class="left-label">
136 ${_('Diff options')}:
136 ${_('Diff options')}:
137 </div>
137 </div>
138 <div class="right-content">
138 <div class="right-content">
139 <div class="diff-actions">
139 <div class="diff-actions">
140 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision='?')}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
140 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision='?')}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
141 ${_('Raw Diff')}
141 ${_('Raw Diff')}
142 </a>
142 </a>
143 |
143 |
144 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision='?')}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
144 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision='?')}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
145 ${_('Patch Diff')}
145 ${_('Patch Diff')}
146 </a>
146 </a>
147 |
147 |
148 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision='?',diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
148 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision='?',diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
149 ${_('Download Diff')}
149 ${_('Download Diff')}
150 </a>
150 </a>
151 </div>
151 </div>
152 </div>
152 </div>
153 </div>
153 </div>
154 </%doc>
154 </%doc>
155
155
156 ## commit status form
156 ## commit status form
157 <div class="fieldset" id="compare_changeset_status" style="display: none; margin-bottom: -80px;">
157 <div class="fieldset" id="compare_changeset_status" style="display: none; margin-bottom: -80px;">
158 <div class="left-label">
158 <div class="left-label">
159 ${_('Commit status')}:
159 ${_('Commit status')}:
160 </div>
160 </div>
161 <div class="right-content">
161 <div class="right-content">
162 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
162 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
163 ## main comment form and it status
163 ## main comment form and it status
164 <%
164 <%
165 def revs(_revs):
165 def revs(_revs):
166 form_inputs = []
166 form_inputs = []
167 for cs in _revs:
167 for cs in _revs:
168 tmpl = '<input type="hidden" data-commit-id="%(cid)s" name="commit_ids" value="%(cid)s">' % {'cid': cs.raw_id}
168 tmpl = '<input type="hidden" data-commit-id="%(cid)s" name="commit_ids" value="%(cid)s">' % {'cid': cs.raw_id}
169 form_inputs.append(tmpl)
169 form_inputs.append(tmpl)
170 return form_inputs
170 return form_inputs
171 %>
171 %>
172 <div>
172 <div>
173 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision='0'*16), None, is_compare=True, form_extras=revs(c.commit_ranges))}
173 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision='0'*16), None, is_compare=True, form_extras=revs(c.commit_ranges))}
174 <script type="text/javascript">
175
176 mainCommentForm.setHandleFormSubmit(function(o) {
177 var text = mainCommentForm.cm.getValue();
178 var status = mainCommentForm.getCommentStatus();
179
180 if (text === "" && !status) {
181 return;
182 }
183
184 // we can pick which commits we want to make the comment by
185 // selecting them via click on preview pane, this will alter the hidden inputs
186 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
187
188 var commitIds = [];
189 $('#changeset_compare_view_content .compare_select').each(function(el) {
190 var commitId = this.id.replace('row-', '');
191 if ($(this).hasClass('hl') || !cherryPicked) {
192 $("input[data-commit-id='{0}']".format(commitId)).val(commitId)
193 commitIds.push(commitId);
194 } else {
195 $("input[data-commit-id='{0}']".format(commitId)).val('')
196 }
197 });
198
199 mainCommentForm.setActionButtonsDisabled(true);
200 mainCommentForm.cm.setOption("readOnly", true);
201 var postData = {
202 'text': text,
203 'changeset_status': status,
204 'commit_ids': commitIds,
205 'csrf_token': CSRF_TOKEN
206 };
207
208 var submitSuccessCallback = function(o) {
209 location.reload(true);
210 };
211 var submitFailCallback = function(){
212 mainCommentForm.resetCommentFormState(text)
213 };
214 mainCommentForm.submitAjaxPOST(
215 mainCommentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
216 });
217 </script>
218 </div>
174 </div>
219 </div>
175 </div>
220 </div>
176 </div>
221
177
222 </div> <!-- end summary-detail -->
178 </div> <!-- end summary-detail -->
223 </div> <!-- end summary -->
179 </div> <!-- end summary -->
224
180
225 ## use JS script to load it quickly before potentially large diffs render long time
181 ## use JS script to load it quickly before potentially large diffs render long time
226 ## this prevents from situation when large diffs block rendering of select2 fields
182 ## this prevents from situation when large diffs block rendering of select2 fields
227 <script type="text/javascript">
183 <script type="text/javascript">
228
184
229 var cache = {};
185 var cache = {};
230
186
231 var formatSelection = function(repoName){
187 var formatSelection = function(repoName){
232 return function(data, container, escapeMarkup) {
188 return function(data, container, escapeMarkup) {
233 var selection = data ? this.text(data) : "";
189 var selection = data ? this.text(data) : "";
234 return escapeMarkup('{0}@{1}'.format(repoName, selection));
190 return escapeMarkup('{0}@{1}'.format(repoName, selection));
235 }
191 }
236 };
192 };
237
193
238 var feedCompareData = function(query, cachedValue){
194 var feedCompareData = function(query, cachedValue){
239 var data = {results: []};
195 var data = {results: []};
240 //filter results
196 //filter results
241 $.each(cachedValue.results, function() {
197 $.each(cachedValue.results, function() {
242 var section = this.text;
198 var section = this.text;
243 var children = [];
199 var children = [];
244 $.each(this.children, function() {
200 $.each(this.children, function() {
245 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
201 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
246 children.push({
202 children.push({
247 'id': this.id,
203 'id': this.id,
248 'text': this.text,
204 'text': this.text,
249 'type': this.type
205 'type': this.type
250 })
206 })
251 }
207 }
252 });
208 });
253 data.results.push({
209 data.results.push({
254 'text': section,
210 'text': section,
255 'children': children
211 'children': children
256 })
212 })
257 });
213 });
258 //push the typed in changeset
214 //push the typed in changeset
259 data.results.push({
215 data.results.push({
260 'text': _gettext('specify commit'),
216 'text': _gettext('specify commit'),
261 'children': [{
217 'children': [{
262 'id': query.term,
218 'id': query.term,
263 'text': query.term,
219 'text': query.term,
264 'type': 'rev'
220 'type': 'rev'
265 }]
221 }]
266 });
222 });
267 query.callback(data);
223 query.callback(data);
268 };
224 };
269
225
270 var loadCompareData = function(repoName, query, cache){
226 var loadCompareData = function(repoName, query, cache){
271 $.ajax({
227 $.ajax({
272 url: pyroutes.url('repo_refs_data', {'repo_name': repoName}),
228 url: pyroutes.url('repo_refs_data', {'repo_name': repoName}),
273 data: {},
229 data: {},
274 dataType: 'json',
230 dataType: 'json',
275 type: 'GET',
231 type: 'GET',
276 success: function(data) {
232 success: function(data) {
277 cache[repoName] = data;
233 cache[repoName] = data;
278 query.callback({results: data.results});
234 query.callback({results: data.results});
279 }
235 }
280 })
236 })
281 };
237 };
282
238
283 var enable_fields = ${"false" if c.preview_mode else "true"};
239 var enable_fields = ${"false" if c.preview_mode else "true"};
284 $("#compare_source").select2({
240 $("#compare_source").select2({
285 placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}",
241 placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}",
286 containerCssClass: "drop-menu",
242 containerCssClass: "drop-menu",
287 dropdownCssClass: "drop-menu-dropdown",
243 dropdownCssClass: "drop-menu-dropdown",
288 formatSelection: formatSelection("${c.source_repo.repo_name}"),
244 formatSelection: formatSelection("${c.source_repo.repo_name}"),
289 dropdownAutoWidth: true,
245 dropdownAutoWidth: true,
290 query: function(query) {
246 query: function(query) {
291 var repoName = '${c.source_repo.repo_name}';
247 var repoName = '${c.source_repo.repo_name}';
292 var cachedValue = cache[repoName];
248 var cachedValue = cache[repoName];
293
249
294 if (cachedValue){
250 if (cachedValue){
295 feedCompareData(query, cachedValue);
251 feedCompareData(query, cachedValue);
296 }
252 }
297 else {
253 else {
298 loadCompareData(repoName, query, cache);
254 loadCompareData(repoName, query, cache);
299 }
255 }
300 }
256 }
301 }).select2("enable", enable_fields);
257 }).select2("enable", enable_fields);
302
258
303 $("#compare_target").select2({
259 $("#compare_target").select2({
304 placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}",
260 placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}",
305 dropdownAutoWidth: true,
261 dropdownAutoWidth: true,
306 containerCssClass: "drop-menu",
262 containerCssClass: "drop-menu",
307 dropdownCssClass: "drop-menu-dropdown",
263 dropdownCssClass: "drop-menu-dropdown",
308 formatSelection: formatSelection("${c.target_repo.repo_name}"),
264 formatSelection: formatSelection("${c.target_repo.repo_name}"),
309 query: function(query) {
265 query: function(query) {
310 var repoName = '${c.target_repo.repo_name}';
266 var repoName = '${c.target_repo.repo_name}';
311 var cachedValue = cache[repoName];
267 var cachedValue = cache[repoName];
312
268
313 if (cachedValue){
269 if (cachedValue){
314 feedCompareData(query, cachedValue);
270 feedCompareData(query, cachedValue);
315 }
271 }
316 else {
272 else {
317 loadCompareData(repoName, query, cache);
273 loadCompareData(repoName, query, cache);
318 }
274 }
319 }
275 }
320 }).select2("enable", enable_fields);
276 }).select2("enable", enable_fields);
321 var initial_compare_source = {id: "${c.source_ref}", type:"${c.source_ref_type}"};
277 var initial_compare_source = {id: "${c.source_ref}", type:"${c.source_ref_type}"};
322 var initial_compare_target = {id: "${c.target_ref}", type:"${c.target_ref_type}"};
278 var initial_compare_target = {id: "${c.target_ref}", type:"${c.target_ref_type}"};
323
279
324 $('#compare_revs').on('click', function(e) {
280 $('#compare_revs').on('click', function(e) {
325 var source = $('#compare_source').select2('data') || initial_compare_source;
281 var source = $('#compare_source').select2('data') || initial_compare_source;
326 var target = $('#compare_target').select2('data') || initial_compare_target;
282 var target = $('#compare_target').select2('data') || initial_compare_target;
327 if (source && target) {
283 if (source && target) {
328 var url_data = {
284 var url_data = {
329 repo_name: "${c.repo_name}",
285 repo_name: "${c.repo_name}",
330 source_ref: source.id,
286 source_ref: source.id,
331 source_ref_type: source.type,
287 source_ref_type: source.type,
332 target_ref: target.id,
288 target_ref: target.id,
333 target_ref_type: target.type
289 target_ref_type: target.type
334 };
290 };
335 window.location = pyroutes.url('compare_url', url_data);
291 window.location = pyroutes.url('compare_url', url_data);
336 }
292 }
337 });
293 });
338 $('#compare_changeset_status_toggle').on('click', function(e) {
294 $('#compare_changeset_status_toggle').on('click', function(e) {
339 $('#compare_changeset_status').toggle();
295 $('#compare_changeset_status').toggle();
340 });
296 });
341
297
342 </script>
298 </script>
343
299
344 ## table diff data
300 ## table diff data
345 <div class="table">
301 <div class="table">
346
302
347
303
348 % if not c.compare_home:
304 % if not c.compare_home:
349 <div id="changeset_compare_view_content">
305 <div id="changeset_compare_view_content">
350 <div class="pull-left">
306 <div class="pull-left">
351 <div class="btn-group">
307 <div class="btn-group">
352 <a
308 <a
353 class="btn"
309 class="btn"
354 href="#"
310 href="#"
355 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
311 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
356 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
312 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
357 </a>
313 </a>
358 <a
314 <a
359 class="btn"
315 class="btn"
360 href="#"
316 href="#"
361 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
317 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
362 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
318 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
363 </a>
319 </a>
364 </div>
320 </div>
365 </div>
321 </div>
366 <div style="padding:0 10px 10px 0px" class="pull-left"></div>
322 <div style="padding:0 10px 10px 0px" class="pull-left"></div>
367 ## commit compare generated below
323 ## commit compare generated below
368 <%include file="compare_commits.mako"/>
324 <%include file="compare_commits.mako"/>
369 ${cbdiffs.render_diffset_menu()}
325 ${cbdiffs.render_diffset_menu()}
370 ${cbdiffs.render_diffset(c.diffset)}
326 ${cbdiffs.render_diffset(c.diffset)}
371 </div>
327 </div>
372 % endif
328 % endif
373
329
374 </div>
330 </div>
375 </div>
331 </div>
376
332
377 </%def> No newline at end of file
333 </%def>
General Comments 0
You need to be logged in to leave comments. Login now