##// END OF EJS Templates
commit-page: show unresolved TODOs on commit page below comments.
marcink -
r1385:96c503a2 default
parent child Browse files
Show More
@@ -1,483 +1,486 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 c.unresolved_comments = []
211 if len(c.commit_ranges) == 1:
212 if len(c.commit_ranges) == 1:
212 commit = c.commit_ranges[0]
213 commit = c.commit_ranges[0]
213 c.comments = CommentsModel().get_comments(
214 c.comments = CommentsModel().get_comments(
214 c.rhodecode_db_repo.repo_id,
215 c.rhodecode_db_repo.repo_id,
215 revision=commit.raw_id)
216 revision=commit.raw_id)
216 c.statuses.append(ChangesetStatusModel().get_status(
217 c.statuses.append(ChangesetStatusModel().get_status(
217 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 # comments from PR
219 # comments from PR
219 statuses = ChangesetStatusModel().get_statuses(
220 statuses = ChangesetStatusModel().get_statuses(
220 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 with_revisions=True)
222 with_revisions=True)
222 prs = set(st.pull_request for st in statuses
223 prs = set(st.pull_request for st in statuses
223 if st.pull_request is not None)
224 if st.pull_request is not None)
224 # from associated statuses, check the pull requests, and
225 # from associated statuses, check the pull requests, and
225 # show comments from them
226 # show comments from them
226 for pr in prs:
227 for pr in prs:
227 c.comments.extend(pr.comments)
228 c.comments.extend(pr.comments)
228
229
230 c.unresolved_comments = CommentsModel()\
231 .get_commit_unresolved_todos(commit.raw_id)
232
229 # Iterate over ranges (default commit view is always one commit)
233 # Iterate over ranges (default commit view is always one commit)
230 for commit in c.commit_ranges:
234 for commit in c.commit_ranges:
231 c.changes[commit.raw_id] = []
235 c.changes[commit.raw_id] = []
232
236
233 commit2 = commit
237 commit2 = commit
234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
238 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
235
239
236 _diff = c.rhodecode_repo.get_diff(
240 _diff = c.rhodecode_repo.get_diff(
237 commit1, commit2,
241 commit1, commit2,
238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
242 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
239 diff_processor = diffs.DiffProcessor(
243 diff_processor = diffs.DiffProcessor(
240 _diff, format='newdiff', diff_limit=diff_limit,
244 _diff, format='newdiff', diff_limit=diff_limit,
241 file_limit=file_limit, show_full_diff=fulldiff)
245 file_limit=file_limit, show_full_diff=fulldiff)
242
246
243 commit_changes = OrderedDict()
247 commit_changes = OrderedDict()
244 if method == 'show':
248 if method == 'show':
245 _parsed = diff_processor.prepare()
249 _parsed = diff_processor.prepare()
246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
250 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
247
251
248 _parsed = diff_processor.prepare()
252 _parsed = diff_processor.prepare()
249
253
250 def _node_getter(commit):
254 def _node_getter(commit):
251 def get_node(fname):
255 def get_node(fname):
252 try:
256 try:
253 return commit.get_node(fname)
257 return commit.get_node(fname)
254 except NodeDoesNotExistError:
258 except NodeDoesNotExistError:
255 return None
259 return None
256 return get_node
260 return get_node
257
261
258 inline_comments = CommentsModel().get_inline_comments(
262 inline_comments = CommentsModel().get_inline_comments(
259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
263 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
260 c.inline_cnt = CommentsModel().get_inline_comments_count(
264 c.inline_cnt = CommentsModel().get_inline_comments_count(
261 inline_comments)
265 inline_comments)
262
266
263 diffset = codeblocks.DiffSet(
267 diffset = codeblocks.DiffSet(
264 repo_name=c.repo_name,
268 repo_name=c.repo_name,
265 source_node_getter=_node_getter(commit1),
269 source_node_getter=_node_getter(commit1),
266 target_node_getter=_node_getter(commit2),
270 target_node_getter=_node_getter(commit2),
267 comments=inline_comments
271 comments=inline_comments
268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
272 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
269 c.changes[commit.raw_id] = diffset
273 c.changes[commit.raw_id] = diffset
270 else:
274 else:
271 # downloads/raw we only need RAW diff nothing else
275 # downloads/raw we only need RAW diff nothing else
272 diff = diff_processor.as_raw()
276 diff = diff_processor.as_raw()
273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
277 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
274
278
275 # sort comments by how they were generated
279 # sort comments by how they were generated
276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
280 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
277
281
278
279 if len(c.commit_ranges) == 1:
282 if len(c.commit_ranges) == 1:
280 c.commit = c.commit_ranges[0]
283 c.commit = c.commit_ranges[0]
281 c.parent_tmpl = ''.join(
284 c.parent_tmpl = ''.join(
282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
285 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
283 if method == 'download':
286 if method == 'download':
284 response.content_type = 'text/plain'
287 response.content_type = 'text/plain'
285 response.content_disposition = (
288 response.content_disposition = (
286 'attachment; filename=%s.diff' % commit_id_range[:12])
289 'attachment; filename=%s.diff' % commit_id_range[:12])
287 return diff
290 return diff
288 elif method == 'patch':
291 elif method == 'patch':
289 response.content_type = 'text/plain'
292 response.content_type = 'text/plain'
290 c.diff = safe_unicode(diff)
293 c.diff = safe_unicode(diff)
291 return render('changeset/patch_changeset.mako')
294 return render('changeset/patch_changeset.mako')
292 elif method == 'raw':
295 elif method == 'raw':
293 response.content_type = 'text/plain'
296 response.content_type = 'text/plain'
294 return diff
297 return diff
295 elif method == 'show':
298 elif method == 'show':
296 if len(c.commit_ranges) == 1:
299 if len(c.commit_ranges) == 1:
297 return render('changeset/changeset.mako')
300 return render('changeset/changeset.mako')
298 else:
301 else:
299 c.ancestor = None
302 c.ancestor = None
300 c.target_repo = c.rhodecode_db_repo
303 c.target_repo = c.rhodecode_db_repo
301 return render('changeset/changeset_range.mako')
304 return render('changeset/changeset_range.mako')
302
305
303 @LoginRequired()
306 @LoginRequired()
304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
305 'repository.admin')
308 'repository.admin')
306 def index(self, revision, method='show'):
309 def index(self, revision, method='show'):
307 return self._index(revision, method=method)
310 return self._index(revision, method=method)
308
311
309 @LoginRequired()
312 @LoginRequired()
310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
311 'repository.admin')
314 'repository.admin')
312 def changeset_raw(self, revision):
315 def changeset_raw(self, revision):
313 return self._index(revision, method='raw')
316 return self._index(revision, method='raw')
314
317
315 @LoginRequired()
318 @LoginRequired()
316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
317 'repository.admin')
320 'repository.admin')
318 def changeset_patch(self, revision):
321 def changeset_patch(self, revision):
319 return self._index(revision, method='patch')
322 return self._index(revision, method='patch')
320
323
321 @LoginRequired()
324 @LoginRequired()
322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
325 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
323 'repository.admin')
326 'repository.admin')
324 def changeset_download(self, revision):
327 def changeset_download(self, revision):
325 return self._index(revision, method='download')
328 return self._index(revision, method='download')
326
329
327 @LoginRequired()
330 @LoginRequired()
328 @NotAnonymous()
331 @NotAnonymous()
329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
332 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
330 'repository.admin')
333 'repository.admin')
331 @auth.CSRFRequired()
334 @auth.CSRFRequired()
332 @jsonify
335 @jsonify
333 def comment(self, repo_name, revision):
336 def comment(self, repo_name, revision):
334 commit_id = revision
337 commit_id = revision
335 status = request.POST.get('changeset_status', None)
338 status = request.POST.get('changeset_status', None)
336 text = request.POST.get('text')
339 text = request.POST.get('text')
337 comment_type = request.POST.get('comment_type')
340 comment_type = request.POST.get('comment_type')
338 resolves_comment_id = request.POST.get('resolves_comment_id', None)
341 resolves_comment_id = request.POST.get('resolves_comment_id', None)
339
342
340 if status:
343 if status:
341 text = text or (_('Status change %(transition_icon)s %(status)s')
344 text = text or (_('Status change %(transition_icon)s %(status)s')
342 % {'transition_icon': '>',
345 % {'transition_icon': '>',
343 'status': ChangesetStatus.get_status_lbl(status)})
346 'status': ChangesetStatus.get_status_lbl(status)})
344
347
345 multi_commit_ids = []
348 multi_commit_ids = []
346 for _commit_id in request.POST.get('commit_ids', '').split(','):
349 for _commit_id in request.POST.get('commit_ids', '').split(','):
347 if _commit_id not in ['', None, EmptyCommit.raw_id]:
350 if _commit_id not in ['', None, EmptyCommit.raw_id]:
348 if _commit_id not in multi_commit_ids:
351 if _commit_id not in multi_commit_ids:
349 multi_commit_ids.append(_commit_id)
352 multi_commit_ids.append(_commit_id)
350
353
351 commit_ids = multi_commit_ids or [commit_id]
354 commit_ids = multi_commit_ids or [commit_id]
352
355
353 comment = None
356 comment = None
354 for current_id in filter(None, commit_ids):
357 for current_id in filter(None, commit_ids):
355 c.co = comment = CommentsModel().create(
358 c.co = comment = CommentsModel().create(
356 text=text,
359 text=text,
357 repo=c.rhodecode_db_repo.repo_id,
360 repo=c.rhodecode_db_repo.repo_id,
358 user=c.rhodecode_user.user_id,
361 user=c.rhodecode_user.user_id,
359 commit_id=current_id,
362 commit_id=current_id,
360 f_path=request.POST.get('f_path'),
363 f_path=request.POST.get('f_path'),
361 line_no=request.POST.get('line'),
364 line_no=request.POST.get('line'),
362 status_change=(ChangesetStatus.get_status_lbl(status)
365 status_change=(ChangesetStatus.get_status_lbl(status)
363 if status else None),
366 if status else None),
364 status_change_type=status,
367 status_change_type=status,
365 comment_type=comment_type,
368 comment_type=comment_type,
366 resolves_comment_id=resolves_comment_id
369 resolves_comment_id=resolves_comment_id
367 )
370 )
368 c.inline_comment = True if comment.line_no else False
371 c.inline_comment = True if comment.line_no else False
369
372
370 # get status if set !
373 # get status if set !
371 if status:
374 if status:
372 # if latest status was from pull request and it's closed
375 # if latest status was from pull request and it's closed
373 # disallow changing status !
376 # disallow changing status !
374 # dont_allow_on_closed_pull_request = True !
377 # dont_allow_on_closed_pull_request = True !
375
378
376 try:
379 try:
377 ChangesetStatusModel().set_status(
380 ChangesetStatusModel().set_status(
378 c.rhodecode_db_repo.repo_id,
381 c.rhodecode_db_repo.repo_id,
379 status,
382 status,
380 c.rhodecode_user.user_id,
383 c.rhodecode_user.user_id,
381 comment,
384 comment,
382 revision=current_id,
385 revision=current_id,
383 dont_allow_on_closed_pull_request=True
386 dont_allow_on_closed_pull_request=True
384 )
387 )
385 except StatusChangeOnClosedPullRequestError:
388 except StatusChangeOnClosedPullRequestError:
386 msg = _('Changing the status of a commit associated with '
389 msg = _('Changing the status of a commit associated with '
387 'a closed pull request is not allowed')
390 'a closed pull request is not allowed')
388 log.exception(msg)
391 log.exception(msg)
389 h.flash(msg, category='warning')
392 h.flash(msg, category='warning')
390 return redirect(h.url(
393 return redirect(h.url(
391 'changeset_home', repo_name=repo_name,
394 'changeset_home', repo_name=repo_name,
392 revision=current_id))
395 revision=current_id))
393
396
394 # finalize, commit and redirect
397 # finalize, commit and redirect
395 Session().commit()
398 Session().commit()
396
399
397 data = {
400 data = {
398 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
401 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
399 }
402 }
400 if comment:
403 if comment:
401 data.update(comment.get_dict())
404 data.update(comment.get_dict())
402 data.update({'rendered_text':
405 data.update({'rendered_text':
403 render('changeset/changeset_comment_block.mako')})
406 render('changeset/changeset_comment_block.mako')})
404
407
405 return data
408 return data
406
409
407 @LoginRequired()
410 @LoginRequired()
408 @NotAnonymous()
411 @NotAnonymous()
409 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
412 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
410 'repository.admin')
413 'repository.admin')
411 @auth.CSRFRequired()
414 @auth.CSRFRequired()
412 def preview_comment(self):
415 def preview_comment(self):
413 # Technically a CSRF token is not needed as no state changes with this
416 # Technically a CSRF token is not needed as no state changes with this
414 # call. However, as this is a POST is better to have it, so automated
417 # call. However, as this is a POST is better to have it, so automated
415 # tools don't flag it as potential CSRF.
418 # tools don't flag it as potential CSRF.
416 # Post is required because the payload could be bigger than the maximum
419 # Post is required because the payload could be bigger than the maximum
417 # allowed by GET.
420 # allowed by GET.
418 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
421 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
419 raise HTTPBadRequest()
422 raise HTTPBadRequest()
420 text = request.POST.get('text')
423 text = request.POST.get('text')
421 renderer = request.POST.get('renderer') or 'rst'
424 renderer = request.POST.get('renderer') or 'rst'
422 if text:
425 if text:
423 return h.render(text, renderer=renderer, mentions=True)
426 return h.render(text, renderer=renderer, mentions=True)
424 return ''
427 return ''
425
428
426 @LoginRequired()
429 @LoginRequired()
427 @NotAnonymous()
430 @NotAnonymous()
428 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
431 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
429 'repository.admin')
432 'repository.admin')
430 @auth.CSRFRequired()
433 @auth.CSRFRequired()
431 @jsonify
434 @jsonify
432 def delete_comment(self, repo_name, comment_id):
435 def delete_comment(self, repo_name, comment_id):
433 comment = ChangesetComment.get(comment_id)
436 comment = ChangesetComment.get(comment_id)
434 if not comment:
437 if not comment:
435 log.debug('Comment with id:%s not found, skipping', comment_id)
438 log.debug('Comment with id:%s not found, skipping', comment_id)
436 # comment already deleted in another call probably
439 # comment already deleted in another call probably
437 return True
440 return True
438
441
439 owner = (comment.author.user_id == c.rhodecode_user.user_id)
442 owner = (comment.author.user_id == c.rhodecode_user.user_id)
440 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
443 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
441 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
444 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
442 CommentsModel().delete(comment=comment)
445 CommentsModel().delete(comment=comment)
443 Session().commit()
446 Session().commit()
444 return True
447 return True
445 else:
448 else:
446 raise HTTPForbidden()
449 raise HTTPForbidden()
447
450
448 @LoginRequired()
451 @LoginRequired()
449 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
452 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
450 'repository.admin')
453 'repository.admin')
451 @jsonify
454 @jsonify
452 def changeset_info(self, repo_name, revision):
455 def changeset_info(self, repo_name, revision):
453 if request.is_xhr:
456 if request.is_xhr:
454 try:
457 try:
455 return c.rhodecode_repo.get_commit(commit_id=revision)
458 return c.rhodecode_repo.get_commit(commit_id=revision)
456 except CommitDoesNotExistError as e:
459 except CommitDoesNotExistError as e:
457 return EmptyCommit(message=str(e))
460 return EmptyCommit(message=str(e))
458 else:
461 else:
459 raise HTTPBadRequest()
462 raise HTTPBadRequest()
460
463
461 @LoginRequired()
464 @LoginRequired()
462 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
465 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
463 'repository.admin')
466 'repository.admin')
464 @jsonify
467 @jsonify
465 def changeset_children(self, repo_name, revision):
468 def changeset_children(self, repo_name, revision):
466 if request.is_xhr:
469 if request.is_xhr:
467 commit = c.rhodecode_repo.get_commit(commit_id=revision)
470 commit = c.rhodecode_repo.get_commit(commit_id=revision)
468 result = {"results": commit.children}
471 result = {"results": commit.children}
469 return result
472 return result
470 else:
473 else:
471 raise HTTPBadRequest()
474 raise HTTPBadRequest()
472
475
473 @LoginRequired()
476 @LoginRequired()
474 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
477 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
475 'repository.admin')
478 'repository.admin')
476 @jsonify
479 @jsonify
477 def changeset_parents(self, repo_name, revision):
480 def changeset_parents(self, repo_name, revision):
478 if request.is_xhr:
481 if request.is_xhr:
479 commit = c.rhodecode_repo.get_commit(commit_id=revision)
482 commit = c.rhodecode_repo.get_commit(commit_id=revision)
480 result = {"results": commit.parents}
483 result = {"results": commit.parents}
481 return result
484 return result
482 else:
485 else:
483 raise HTTPBadRequest()
486 raise HTTPBadRequest()
@@ -1,617 +1,634 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-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 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from datetime import datetime
29 from datetime import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry
32 from pyramid.threadlocal import get_current_registry
33 from sqlalchemy.sql.expression import null
33 from sqlalchemy.sql.expression import null
34 from sqlalchemy.sql.functions import coalesce
34 from sqlalchemy.sql.functions import coalesce
35
35
36 from rhodecode.lib import helpers as h, diffs
36 from rhodecode.lib import helpers as h, diffs
37 from rhodecode.lib.channelstream import channelstream_request
37 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.utils import action_logger
38 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils2 import extract_mentioned_users
39 from rhodecode.lib.utils2 import extract_mentioned_users
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
43 from rhodecode.model.notification import NotificationModel
43 from rhodecode.model.notification import NotificationModel
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import VcsSettingsModel
45 from rhodecode.model.settings import VcsSettingsModel
46 from rhodecode.model.notification import EmailNotificationModel
46 from rhodecode.model.notification import EmailNotificationModel
47 from rhodecode.model.validation_schema.schemas import comment_schema
47 from rhodecode.model.validation_schema.schemas import comment_schema
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class CommentsModel(BaseModel):
53 class CommentsModel(BaseModel):
54
54
55 cls = ChangesetComment
55 cls = ChangesetComment
56
56
57 DIFF_CONTEXT_BEFORE = 3
57 DIFF_CONTEXT_BEFORE = 3
58 DIFF_CONTEXT_AFTER = 3
58 DIFF_CONTEXT_AFTER = 3
59
59
60 def __get_commit_comment(self, changeset_comment):
60 def __get_commit_comment(self, changeset_comment):
61 return self._get_instance(ChangesetComment, changeset_comment)
61 return self._get_instance(ChangesetComment, changeset_comment)
62
62
63 def __get_pull_request(self, pull_request):
63 def __get_pull_request(self, pull_request):
64 return self._get_instance(PullRequest, pull_request)
64 return self._get_instance(PullRequest, pull_request)
65
65
66 def _extract_mentions(self, s):
66 def _extract_mentions(self, s):
67 user_objects = []
67 user_objects = []
68 for username in extract_mentioned_users(s):
68 for username in extract_mentioned_users(s):
69 user_obj = User.get_by_username(username, case_insensitive=True)
69 user_obj = User.get_by_username(username, case_insensitive=True)
70 if user_obj:
70 if user_obj:
71 user_objects.append(user_obj)
71 user_objects.append(user_obj)
72 return user_objects
72 return user_objects
73
73
74 def _get_renderer(self, global_renderer='rst'):
74 def _get_renderer(self, global_renderer='rst'):
75 try:
75 try:
76 # try reading from visual context
76 # try reading from visual context
77 from pylons import tmpl_context
77 from pylons import tmpl_context
78 global_renderer = tmpl_context.visual.default_renderer
78 global_renderer = tmpl_context.visual.default_renderer
79 except AttributeError:
79 except AttributeError:
80 log.debug("Renderer not set, falling back "
80 log.debug("Renderer not set, falling back "
81 "to default renderer '%s'", global_renderer)
81 "to default renderer '%s'", global_renderer)
82 except Exception:
82 except Exception:
83 log.error(traceback.format_exc())
83 log.error(traceback.format_exc())
84 return global_renderer
84 return global_renderer
85
85
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
87 # group by versions, and count until, and display objects
87 # group by versions, and count until, and display objects
88
88
89 comment_groups = collections.defaultdict(list)
89 comment_groups = collections.defaultdict(list)
90 [comment_groups[
90 [comment_groups[
91 _co.pull_request_version_id].append(_co) for _co in comments]
91 _co.pull_request_version_id].append(_co) for _co in comments]
92
92
93 def yield_comments(pos):
93 def yield_comments(pos):
94 for co in comment_groups[pos]:
94 for co in comment_groups[pos]:
95 yield co
95 yield co
96
96
97 comment_versions = collections.defaultdict(
97 comment_versions = collections.defaultdict(
98 lambda: collections.defaultdict(list))
98 lambda: collections.defaultdict(list))
99 prev_prvid = -1
99 prev_prvid = -1
100 # fake last entry with None, to aggregate on "latest" version which
100 # fake last entry with None, to aggregate on "latest" version which
101 # doesn't have an pull_request_version_id
101 # doesn't have an pull_request_version_id
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
103 prvid = ver.pull_request_version_id
103 prvid = ver.pull_request_version_id
104 if prev_prvid == -1:
104 if prev_prvid == -1:
105 prev_prvid = prvid
105 prev_prvid = prvid
106
106
107 for co in yield_comments(prvid):
107 for co in yield_comments(prvid):
108 comment_versions[prvid]['at'].append(co)
108 comment_versions[prvid]['at'].append(co)
109
109
110 # save until
110 # save until
111 current = comment_versions[prvid]['at']
111 current = comment_versions[prvid]['at']
112 prev_until = comment_versions[prev_prvid]['until']
112 prev_until = comment_versions[prev_prvid]['until']
113 cur_until = prev_until + current
113 cur_until = prev_until + current
114 comment_versions[prvid]['until'].extend(cur_until)
114 comment_versions[prvid]['until'].extend(cur_until)
115
115
116 # save outdated
116 # save outdated
117 if inline:
117 if inline:
118 outdated = [x for x in cur_until
118 outdated = [x for x in cur_until
119 if x.outdated_at_version(show_version)]
119 if x.outdated_at_version(show_version)]
120 else:
120 else:
121 outdated = [x for x in cur_until
121 outdated = [x for x in cur_until
122 if x.older_than_version(show_version)]
122 if x.older_than_version(show_version)]
123 display = [x for x in cur_until if x not in outdated]
123 display = [x for x in cur_until if x not in outdated]
124
124
125 comment_versions[prvid]['outdated'] = outdated
125 comment_versions[prvid]['outdated'] = outdated
126 comment_versions[prvid]['display'] = display
126 comment_versions[prvid]['display'] = display
127
127
128 prev_prvid = prvid
128 prev_prvid = prvid
129
129
130 return comment_versions
130 return comment_versions
131
131
132 def get_unresolved_todos(self, pull_request, show_outdated=True):
132 def get_unresolved_todos(self, pull_request, show_outdated=True):
133
133
134 todos = Session().query(ChangesetComment) \
134 todos = Session().query(ChangesetComment) \
135 .filter(ChangesetComment.pull_request == pull_request) \
135 .filter(ChangesetComment.pull_request == pull_request) \
136 .filter(ChangesetComment.resolved_by == None) \
136 .filter(ChangesetComment.resolved_by == None) \
137 .filter(ChangesetComment.comment_type
137 .filter(ChangesetComment.comment_type
138 == ChangesetComment.COMMENT_TYPE_TODO)
138 == ChangesetComment.COMMENT_TYPE_TODO)
139
139
140 if not show_outdated:
140 if not show_outdated:
141 todos = todos.filter(
141 todos = todos.filter(
142 coalesce(ChangesetComment.display_state, '') !=
142 coalesce(ChangesetComment.display_state, '') !=
143 ChangesetComment.COMMENT_OUTDATED)
143 ChangesetComment.COMMENT_OUTDATED)
144
144
145 todos = todos.all()
145 todos = todos.all()
146
146
147 return todos
147 return todos
148
148
149 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
150
151 todos = Session().query(ChangesetComment) \
152 .filter(ChangesetComment.revision == commit_id) \
153 .filter(ChangesetComment.resolved_by == None) \
154 .filter(ChangesetComment.comment_type
155 == ChangesetComment.COMMENT_TYPE_TODO)
156
157 if not show_outdated:
158 todos = todos.filter(
159 coalesce(ChangesetComment.display_state, '') !=
160 ChangesetComment.COMMENT_OUTDATED)
161
162 todos = todos.all()
163
164 return todos
165
149 def create(self, text, repo, user, commit_id=None, pull_request=None,
166 def create(self, text, repo, user, commit_id=None, pull_request=None,
150 f_path=None, line_no=None, status_change=None,
167 f_path=None, line_no=None, status_change=None,
151 status_change_type=None, comment_type=None,
168 status_change_type=None, comment_type=None,
152 resolves_comment_id=None, closing_pr=False, send_email=True,
169 resolves_comment_id=None, closing_pr=False, send_email=True,
153 renderer=None):
170 renderer=None):
154 """
171 """
155 Creates new comment for commit or pull request.
172 Creates new comment for commit or pull request.
156 IF status_change is not none this comment is associated with a
173 IF status_change is not none this comment is associated with a
157 status change of commit or commit associated with pull request
174 status change of commit or commit associated with pull request
158
175
159 :param text:
176 :param text:
160 :param repo:
177 :param repo:
161 :param user:
178 :param user:
162 :param commit_id:
179 :param commit_id:
163 :param pull_request:
180 :param pull_request:
164 :param f_path:
181 :param f_path:
165 :param line_no:
182 :param line_no:
166 :param status_change: Label for status change
183 :param status_change: Label for status change
167 :param comment_type: Type of comment
184 :param comment_type: Type of comment
168 :param status_change_type: type of status change
185 :param status_change_type: type of status change
169 :param closing_pr:
186 :param closing_pr:
170 :param send_email:
187 :param send_email:
171 :param renderer: pick renderer for this comment
188 :param renderer: pick renderer for this comment
172 """
189 """
173 if not text:
190 if not text:
174 log.warning('Missing text for comment, skipping...')
191 log.warning('Missing text for comment, skipping...')
175 return
192 return
176
193
177 if not renderer:
194 if not renderer:
178 renderer = self._get_renderer()
195 renderer = self._get_renderer()
179
196
180 repo = self._get_repo(repo)
197 repo = self._get_repo(repo)
181 user = self._get_user(user)
198 user = self._get_user(user)
182
199
183 schema = comment_schema.CommentSchema()
200 schema = comment_schema.CommentSchema()
184 validated_kwargs = schema.deserialize(dict(
201 validated_kwargs = schema.deserialize(dict(
185 comment_body=text,
202 comment_body=text,
186 comment_type=comment_type,
203 comment_type=comment_type,
187 comment_file=f_path,
204 comment_file=f_path,
188 comment_line=line_no,
205 comment_line=line_no,
189 renderer_type=renderer,
206 renderer_type=renderer,
190 status_change=status_change_type,
207 status_change=status_change_type,
191 resolves_comment_id=resolves_comment_id,
208 resolves_comment_id=resolves_comment_id,
192 repo=repo.repo_id,
209 repo=repo.repo_id,
193 user=user.user_id,
210 user=user.user_id,
194 ))
211 ))
195
212
196 comment = ChangesetComment()
213 comment = ChangesetComment()
197 comment.renderer = validated_kwargs['renderer_type']
214 comment.renderer = validated_kwargs['renderer_type']
198 comment.text = validated_kwargs['comment_body']
215 comment.text = validated_kwargs['comment_body']
199 comment.f_path = validated_kwargs['comment_file']
216 comment.f_path = validated_kwargs['comment_file']
200 comment.line_no = validated_kwargs['comment_line']
217 comment.line_no = validated_kwargs['comment_line']
201 comment.comment_type = validated_kwargs['comment_type']
218 comment.comment_type = validated_kwargs['comment_type']
202
219
203 comment.repo = repo
220 comment.repo = repo
204 comment.author = user
221 comment.author = user
205 comment.resolved_comment = self.__get_commit_comment(
222 comment.resolved_comment = self.__get_commit_comment(
206 validated_kwargs['resolves_comment_id'])
223 validated_kwargs['resolves_comment_id'])
207
224
208 pull_request_id = pull_request
225 pull_request_id = pull_request
209
226
210 commit_obj = None
227 commit_obj = None
211 pull_request_obj = None
228 pull_request_obj = None
212
229
213 if commit_id:
230 if commit_id:
214 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
231 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
215 # do a lookup, so we don't pass something bad here
232 # do a lookup, so we don't pass something bad here
216 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
233 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
217 comment.revision = commit_obj.raw_id
234 comment.revision = commit_obj.raw_id
218
235
219 elif pull_request_id:
236 elif pull_request_id:
220 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
237 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
221 pull_request_obj = self.__get_pull_request(pull_request_id)
238 pull_request_obj = self.__get_pull_request(pull_request_id)
222 comment.pull_request = pull_request_obj
239 comment.pull_request = pull_request_obj
223 else:
240 else:
224 raise Exception('Please specify commit or pull_request_id')
241 raise Exception('Please specify commit or pull_request_id')
225
242
226 Session().add(comment)
243 Session().add(comment)
227 Session().flush()
244 Session().flush()
228 kwargs = {
245 kwargs = {
229 'user': user,
246 'user': user,
230 'renderer_type': renderer,
247 'renderer_type': renderer,
231 'repo_name': repo.repo_name,
248 'repo_name': repo.repo_name,
232 'status_change': status_change,
249 'status_change': status_change,
233 'status_change_type': status_change_type,
250 'status_change_type': status_change_type,
234 'comment_body': text,
251 'comment_body': text,
235 'comment_file': f_path,
252 'comment_file': f_path,
236 'comment_line': line_no,
253 'comment_line': line_no,
237 }
254 }
238
255
239 if commit_obj:
256 if commit_obj:
240 recipients = ChangesetComment.get_users(
257 recipients = ChangesetComment.get_users(
241 revision=commit_obj.raw_id)
258 revision=commit_obj.raw_id)
242 # add commit author if it's in RhodeCode system
259 # add commit author if it's in RhodeCode system
243 cs_author = User.get_from_cs_author(commit_obj.author)
260 cs_author = User.get_from_cs_author(commit_obj.author)
244 if not cs_author:
261 if not cs_author:
245 # use repo owner if we cannot extract the author correctly
262 # use repo owner if we cannot extract the author correctly
246 cs_author = repo.user
263 cs_author = repo.user
247 recipients += [cs_author]
264 recipients += [cs_author]
248
265
249 commit_comment_url = self.get_url(comment)
266 commit_comment_url = self.get_url(comment)
250
267
251 target_repo_url = h.link_to(
268 target_repo_url = h.link_to(
252 repo.repo_name,
269 repo.repo_name,
253 h.url('summary_home',
270 h.url('summary_home',
254 repo_name=repo.repo_name, qualified=True))
271 repo_name=repo.repo_name, qualified=True))
255
272
256 # commit specifics
273 # commit specifics
257 kwargs.update({
274 kwargs.update({
258 'commit': commit_obj,
275 'commit': commit_obj,
259 'commit_message': commit_obj.message,
276 'commit_message': commit_obj.message,
260 'commit_target_repo': target_repo_url,
277 'commit_target_repo': target_repo_url,
261 'commit_comment_url': commit_comment_url,
278 'commit_comment_url': commit_comment_url,
262 })
279 })
263
280
264 elif pull_request_obj:
281 elif pull_request_obj:
265 # get the current participants of this pull request
282 # get the current participants of this pull request
266 recipients = ChangesetComment.get_users(
283 recipients = ChangesetComment.get_users(
267 pull_request_id=pull_request_obj.pull_request_id)
284 pull_request_id=pull_request_obj.pull_request_id)
268 # add pull request author
285 # add pull request author
269 recipients += [pull_request_obj.author]
286 recipients += [pull_request_obj.author]
270
287
271 # add the reviewers to notification
288 # add the reviewers to notification
272 recipients += [x.user for x in pull_request_obj.reviewers]
289 recipients += [x.user for x in pull_request_obj.reviewers]
273
290
274 pr_target_repo = pull_request_obj.target_repo
291 pr_target_repo = pull_request_obj.target_repo
275 pr_source_repo = pull_request_obj.source_repo
292 pr_source_repo = pull_request_obj.source_repo
276
293
277 pr_comment_url = h.url(
294 pr_comment_url = h.url(
278 'pullrequest_show',
295 'pullrequest_show',
279 repo_name=pr_target_repo.repo_name,
296 repo_name=pr_target_repo.repo_name,
280 pull_request_id=pull_request_obj.pull_request_id,
297 pull_request_id=pull_request_obj.pull_request_id,
281 anchor='comment-%s' % comment.comment_id,
298 anchor='comment-%s' % comment.comment_id,
282 qualified=True,)
299 qualified=True,)
283
300
284 # set some variables for email notification
301 # set some variables for email notification
285 pr_target_repo_url = h.url(
302 pr_target_repo_url = h.url(
286 'summary_home', repo_name=pr_target_repo.repo_name,
303 'summary_home', repo_name=pr_target_repo.repo_name,
287 qualified=True)
304 qualified=True)
288
305
289 pr_source_repo_url = h.url(
306 pr_source_repo_url = h.url(
290 'summary_home', repo_name=pr_source_repo.repo_name,
307 'summary_home', repo_name=pr_source_repo.repo_name,
291 qualified=True)
308 qualified=True)
292
309
293 # pull request specifics
310 # pull request specifics
294 kwargs.update({
311 kwargs.update({
295 'pull_request': pull_request_obj,
312 'pull_request': pull_request_obj,
296 'pr_id': pull_request_obj.pull_request_id,
313 'pr_id': pull_request_obj.pull_request_id,
297 'pr_target_repo': pr_target_repo,
314 'pr_target_repo': pr_target_repo,
298 'pr_target_repo_url': pr_target_repo_url,
315 'pr_target_repo_url': pr_target_repo_url,
299 'pr_source_repo': pr_source_repo,
316 'pr_source_repo': pr_source_repo,
300 'pr_source_repo_url': pr_source_repo_url,
317 'pr_source_repo_url': pr_source_repo_url,
301 'pr_comment_url': pr_comment_url,
318 'pr_comment_url': pr_comment_url,
302 'pr_closing': closing_pr,
319 'pr_closing': closing_pr,
303 })
320 })
304 if send_email:
321 if send_email:
305 # pre-generate the subject for notification itself
322 # pre-generate the subject for notification itself
306 (subject,
323 (subject,
307 _h, _e, # we don't care about those
324 _h, _e, # we don't care about those
308 body_plaintext) = EmailNotificationModel().render_email(
325 body_plaintext) = EmailNotificationModel().render_email(
309 notification_type, **kwargs)
326 notification_type, **kwargs)
310
327
311 mention_recipients = set(
328 mention_recipients = set(
312 self._extract_mentions(text)).difference(recipients)
329 self._extract_mentions(text)).difference(recipients)
313
330
314 # create notification objects, and emails
331 # create notification objects, and emails
315 NotificationModel().create(
332 NotificationModel().create(
316 created_by=user,
333 created_by=user,
317 notification_subject=subject,
334 notification_subject=subject,
318 notification_body=body_plaintext,
335 notification_body=body_plaintext,
319 notification_type=notification_type,
336 notification_type=notification_type,
320 recipients=recipients,
337 recipients=recipients,
321 mention_recipients=mention_recipients,
338 mention_recipients=mention_recipients,
322 email_kwargs=kwargs,
339 email_kwargs=kwargs,
323 )
340 )
324
341
325 action = (
342 action = (
326 'user_commented_pull_request:{}'.format(
343 'user_commented_pull_request:{}'.format(
327 comment.pull_request.pull_request_id)
344 comment.pull_request.pull_request_id)
328 if comment.pull_request
345 if comment.pull_request
329 else 'user_commented_revision:{}'.format(comment.revision)
346 else 'user_commented_revision:{}'.format(comment.revision)
330 )
347 )
331 action_logger(user, action, comment.repo)
348 action_logger(user, action, comment.repo)
332
349
333 registry = get_current_registry()
350 registry = get_current_registry()
334 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
351 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
335 channelstream_config = rhodecode_plugins.get('channelstream', {})
352 channelstream_config = rhodecode_plugins.get('channelstream', {})
336 msg_url = ''
353 msg_url = ''
337 if commit_obj:
354 if commit_obj:
338 msg_url = commit_comment_url
355 msg_url = commit_comment_url
339 repo_name = repo.repo_name
356 repo_name = repo.repo_name
340 elif pull_request_obj:
357 elif pull_request_obj:
341 msg_url = pr_comment_url
358 msg_url = pr_comment_url
342 repo_name = pr_target_repo.repo_name
359 repo_name = pr_target_repo.repo_name
343
360
344 if channelstream_config.get('enabled'):
361 if channelstream_config.get('enabled'):
345 message = '<strong>{}</strong> {} - ' \
362 message = '<strong>{}</strong> {} - ' \
346 '<a onclick="window.location=\'{}\';' \
363 '<a onclick="window.location=\'{}\';' \
347 'window.location.reload()">' \
364 'window.location.reload()">' \
348 '<strong>{}</strong></a>'
365 '<strong>{}</strong></a>'
349 message = message.format(
366 message = message.format(
350 user.username, _('made a comment'), msg_url,
367 user.username, _('made a comment'), msg_url,
351 _('Show it now'))
368 _('Show it now'))
352 channel = '/repo${}$/pr/{}'.format(
369 channel = '/repo${}$/pr/{}'.format(
353 repo_name,
370 repo_name,
354 pull_request_id
371 pull_request_id
355 )
372 )
356 payload = {
373 payload = {
357 'type': 'message',
374 'type': 'message',
358 'timestamp': datetime.utcnow(),
375 'timestamp': datetime.utcnow(),
359 'user': 'system',
376 'user': 'system',
360 'exclude_users': [user.username],
377 'exclude_users': [user.username],
361 'channel': channel,
378 'channel': channel,
362 'message': {
379 'message': {
363 'message': message,
380 'message': message,
364 'level': 'info',
381 'level': 'info',
365 'topic': '/notifications'
382 'topic': '/notifications'
366 }
383 }
367 }
384 }
368 channelstream_request(channelstream_config, [payload],
385 channelstream_request(channelstream_config, [payload],
369 '/message', raise_exc=False)
386 '/message', raise_exc=False)
370
387
371 return comment
388 return comment
372
389
373 def delete(self, comment):
390 def delete(self, comment):
374 """
391 """
375 Deletes given comment
392 Deletes given comment
376
393
377 :param comment_id:
394 :param comment_id:
378 """
395 """
379 comment = self.__get_commit_comment(comment)
396 comment = self.__get_commit_comment(comment)
380 Session().delete(comment)
397 Session().delete(comment)
381
398
382 return comment
399 return comment
383
400
384 def get_all_comments(self, repo_id, revision=None, pull_request=None):
401 def get_all_comments(self, repo_id, revision=None, pull_request=None):
385 q = ChangesetComment.query()\
402 q = ChangesetComment.query()\
386 .filter(ChangesetComment.repo_id == repo_id)
403 .filter(ChangesetComment.repo_id == repo_id)
387 if revision:
404 if revision:
388 q = q.filter(ChangesetComment.revision == revision)
405 q = q.filter(ChangesetComment.revision == revision)
389 elif pull_request:
406 elif pull_request:
390 pull_request = self.__get_pull_request(pull_request)
407 pull_request = self.__get_pull_request(pull_request)
391 q = q.filter(ChangesetComment.pull_request == pull_request)
408 q = q.filter(ChangesetComment.pull_request == pull_request)
392 else:
409 else:
393 raise Exception('Please specify commit or pull_request')
410 raise Exception('Please specify commit or pull_request')
394 q = q.order_by(ChangesetComment.created_on)
411 q = q.order_by(ChangesetComment.created_on)
395 return q.all()
412 return q.all()
396
413
397 def get_url(self, comment):
414 def get_url(self, comment):
398 comment = self.__get_commit_comment(comment)
415 comment = self.__get_commit_comment(comment)
399 if comment.pull_request:
416 if comment.pull_request:
400 return h.url(
417 return h.url(
401 'pullrequest_show',
418 'pullrequest_show',
402 repo_name=comment.pull_request.target_repo.repo_name,
419 repo_name=comment.pull_request.target_repo.repo_name,
403 pull_request_id=comment.pull_request.pull_request_id,
420 pull_request_id=comment.pull_request.pull_request_id,
404 anchor='comment-%s' % comment.comment_id,
421 anchor='comment-%s' % comment.comment_id,
405 qualified=True,)
422 qualified=True,)
406 else:
423 else:
407 return h.url(
424 return h.url(
408 'changeset_home',
425 'changeset_home',
409 repo_name=comment.repo.repo_name,
426 repo_name=comment.repo.repo_name,
410 revision=comment.revision,
427 revision=comment.revision,
411 anchor='comment-%s' % comment.comment_id,
428 anchor='comment-%s' % comment.comment_id,
412 qualified=True,)
429 qualified=True,)
413
430
414 def get_comments(self, repo_id, revision=None, pull_request=None):
431 def get_comments(self, repo_id, revision=None, pull_request=None):
415 """
432 """
416 Gets main comments based on revision or pull_request_id
433 Gets main comments based on revision or pull_request_id
417
434
418 :param repo_id:
435 :param repo_id:
419 :param revision:
436 :param revision:
420 :param pull_request:
437 :param pull_request:
421 """
438 """
422
439
423 q = ChangesetComment.query()\
440 q = ChangesetComment.query()\
424 .filter(ChangesetComment.repo_id == repo_id)\
441 .filter(ChangesetComment.repo_id == repo_id)\
425 .filter(ChangesetComment.line_no == None)\
442 .filter(ChangesetComment.line_no == None)\
426 .filter(ChangesetComment.f_path == None)
443 .filter(ChangesetComment.f_path == None)
427 if revision:
444 if revision:
428 q = q.filter(ChangesetComment.revision == revision)
445 q = q.filter(ChangesetComment.revision == revision)
429 elif pull_request:
446 elif pull_request:
430 pull_request = self.__get_pull_request(pull_request)
447 pull_request = self.__get_pull_request(pull_request)
431 q = q.filter(ChangesetComment.pull_request == pull_request)
448 q = q.filter(ChangesetComment.pull_request == pull_request)
432 else:
449 else:
433 raise Exception('Please specify commit or pull_request')
450 raise Exception('Please specify commit or pull_request')
434 q = q.order_by(ChangesetComment.created_on)
451 q = q.order_by(ChangesetComment.created_on)
435 return q.all()
452 return q.all()
436
453
437 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
454 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
438 q = self._get_inline_comments_query(repo_id, revision, pull_request)
455 q = self._get_inline_comments_query(repo_id, revision, pull_request)
439 return self._group_comments_by_path_and_line_number(q)
456 return self._group_comments_by_path_and_line_number(q)
440
457
441 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
458 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
442 version=None):
459 version=None):
443 inline_cnt = 0
460 inline_cnt = 0
444 for fname, per_line_comments in inline_comments.iteritems():
461 for fname, per_line_comments in inline_comments.iteritems():
445 for lno, comments in per_line_comments.iteritems():
462 for lno, comments in per_line_comments.iteritems():
446 for comm in comments:
463 for comm in comments:
447 if not comm.outdated_at_version(version) and skip_outdated:
464 if not comm.outdated_at_version(version) and skip_outdated:
448 inline_cnt += 1
465 inline_cnt += 1
449
466
450 return inline_cnt
467 return inline_cnt
451
468
452 def get_outdated_comments(self, repo_id, pull_request):
469 def get_outdated_comments(self, repo_id, pull_request):
453 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
470 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
454 # of a pull request.
471 # of a pull request.
455 q = self._all_inline_comments_of_pull_request(pull_request)
472 q = self._all_inline_comments_of_pull_request(pull_request)
456 q = q.filter(
473 q = q.filter(
457 ChangesetComment.display_state ==
474 ChangesetComment.display_state ==
458 ChangesetComment.COMMENT_OUTDATED
475 ChangesetComment.COMMENT_OUTDATED
459 ).order_by(ChangesetComment.comment_id.asc())
476 ).order_by(ChangesetComment.comment_id.asc())
460
477
461 return self._group_comments_by_path_and_line_number(q)
478 return self._group_comments_by_path_and_line_number(q)
462
479
463 def _get_inline_comments_query(self, repo_id, revision, pull_request):
480 def _get_inline_comments_query(self, repo_id, revision, pull_request):
464 # TODO: johbo: Split this into two methods: One for PR and one for
481 # TODO: johbo: Split this into two methods: One for PR and one for
465 # commit.
482 # commit.
466 if revision:
483 if revision:
467 q = Session().query(ChangesetComment).filter(
484 q = Session().query(ChangesetComment).filter(
468 ChangesetComment.repo_id == repo_id,
485 ChangesetComment.repo_id == repo_id,
469 ChangesetComment.line_no != null(),
486 ChangesetComment.line_no != null(),
470 ChangesetComment.f_path != null(),
487 ChangesetComment.f_path != null(),
471 ChangesetComment.revision == revision)
488 ChangesetComment.revision == revision)
472
489
473 elif pull_request:
490 elif pull_request:
474 pull_request = self.__get_pull_request(pull_request)
491 pull_request = self.__get_pull_request(pull_request)
475 if not CommentsModel.use_outdated_comments(pull_request):
492 if not CommentsModel.use_outdated_comments(pull_request):
476 q = self._visible_inline_comments_of_pull_request(pull_request)
493 q = self._visible_inline_comments_of_pull_request(pull_request)
477 else:
494 else:
478 q = self._all_inline_comments_of_pull_request(pull_request)
495 q = self._all_inline_comments_of_pull_request(pull_request)
479
496
480 else:
497 else:
481 raise Exception('Please specify commit or pull_request_id')
498 raise Exception('Please specify commit or pull_request_id')
482 q = q.order_by(ChangesetComment.comment_id.asc())
499 q = q.order_by(ChangesetComment.comment_id.asc())
483 return q
500 return q
484
501
485 def _group_comments_by_path_and_line_number(self, q):
502 def _group_comments_by_path_and_line_number(self, q):
486 comments = q.all()
503 comments = q.all()
487 paths = collections.defaultdict(lambda: collections.defaultdict(list))
504 paths = collections.defaultdict(lambda: collections.defaultdict(list))
488 for co in comments:
505 for co in comments:
489 paths[co.f_path][co.line_no].append(co)
506 paths[co.f_path][co.line_no].append(co)
490 return paths
507 return paths
491
508
492 @classmethod
509 @classmethod
493 def needed_extra_diff_context(cls):
510 def needed_extra_diff_context(cls):
494 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
511 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
495
512
496 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
513 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
497 if not CommentsModel.use_outdated_comments(pull_request):
514 if not CommentsModel.use_outdated_comments(pull_request):
498 return
515 return
499
516
500 comments = self._visible_inline_comments_of_pull_request(pull_request)
517 comments = self._visible_inline_comments_of_pull_request(pull_request)
501 comments_to_outdate = comments.all()
518 comments_to_outdate = comments.all()
502
519
503 for comment in comments_to_outdate:
520 for comment in comments_to_outdate:
504 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
521 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
505
522
506 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
523 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
507 diff_line = _parse_comment_line_number(comment.line_no)
524 diff_line = _parse_comment_line_number(comment.line_no)
508
525
509 try:
526 try:
510 old_context = old_diff_proc.get_context_of_line(
527 old_context = old_diff_proc.get_context_of_line(
511 path=comment.f_path, diff_line=diff_line)
528 path=comment.f_path, diff_line=diff_line)
512 new_context = new_diff_proc.get_context_of_line(
529 new_context = new_diff_proc.get_context_of_line(
513 path=comment.f_path, diff_line=diff_line)
530 path=comment.f_path, diff_line=diff_line)
514 except (diffs.LineNotInDiffException,
531 except (diffs.LineNotInDiffException,
515 diffs.FileNotInDiffException):
532 diffs.FileNotInDiffException):
516 comment.display_state = ChangesetComment.COMMENT_OUTDATED
533 comment.display_state = ChangesetComment.COMMENT_OUTDATED
517 return
534 return
518
535
519 if old_context == new_context:
536 if old_context == new_context:
520 return
537 return
521
538
522 if self._should_relocate_diff_line(diff_line):
539 if self._should_relocate_diff_line(diff_line):
523 new_diff_lines = new_diff_proc.find_context(
540 new_diff_lines = new_diff_proc.find_context(
524 path=comment.f_path, context=old_context,
541 path=comment.f_path, context=old_context,
525 offset=self.DIFF_CONTEXT_BEFORE)
542 offset=self.DIFF_CONTEXT_BEFORE)
526 if not new_diff_lines:
543 if not new_diff_lines:
527 comment.display_state = ChangesetComment.COMMENT_OUTDATED
544 comment.display_state = ChangesetComment.COMMENT_OUTDATED
528 else:
545 else:
529 new_diff_line = self._choose_closest_diff_line(
546 new_diff_line = self._choose_closest_diff_line(
530 diff_line, new_diff_lines)
547 diff_line, new_diff_lines)
531 comment.line_no = _diff_to_comment_line_number(new_diff_line)
548 comment.line_no = _diff_to_comment_line_number(new_diff_line)
532 else:
549 else:
533 comment.display_state = ChangesetComment.COMMENT_OUTDATED
550 comment.display_state = ChangesetComment.COMMENT_OUTDATED
534
551
535 def _should_relocate_diff_line(self, diff_line):
552 def _should_relocate_diff_line(self, diff_line):
536 """
553 """
537 Checks if relocation shall be tried for the given `diff_line`.
554 Checks if relocation shall be tried for the given `diff_line`.
538
555
539 If a comment points into the first lines, then we can have a situation
556 If a comment points into the first lines, then we can have a situation
540 that after an update another line has been added on top. In this case
557 that after an update another line has been added on top. In this case
541 we would find the context still and move the comment around. This
558 we would find the context still and move the comment around. This
542 would be wrong.
559 would be wrong.
543 """
560 """
544 should_relocate = (
561 should_relocate = (
545 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
562 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
546 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
563 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
547 return should_relocate
564 return should_relocate
548
565
549 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
566 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
550 candidate = new_diff_lines[0]
567 candidate = new_diff_lines[0]
551 best_delta = _diff_line_delta(diff_line, candidate)
568 best_delta = _diff_line_delta(diff_line, candidate)
552 for new_diff_line in new_diff_lines[1:]:
569 for new_diff_line in new_diff_lines[1:]:
553 delta = _diff_line_delta(diff_line, new_diff_line)
570 delta = _diff_line_delta(diff_line, new_diff_line)
554 if delta < best_delta:
571 if delta < best_delta:
555 candidate = new_diff_line
572 candidate = new_diff_line
556 best_delta = delta
573 best_delta = delta
557 return candidate
574 return candidate
558
575
559 def _visible_inline_comments_of_pull_request(self, pull_request):
576 def _visible_inline_comments_of_pull_request(self, pull_request):
560 comments = self._all_inline_comments_of_pull_request(pull_request)
577 comments = self._all_inline_comments_of_pull_request(pull_request)
561 comments = comments.filter(
578 comments = comments.filter(
562 coalesce(ChangesetComment.display_state, '') !=
579 coalesce(ChangesetComment.display_state, '') !=
563 ChangesetComment.COMMENT_OUTDATED)
580 ChangesetComment.COMMENT_OUTDATED)
564 return comments
581 return comments
565
582
566 def _all_inline_comments_of_pull_request(self, pull_request):
583 def _all_inline_comments_of_pull_request(self, pull_request):
567 comments = Session().query(ChangesetComment)\
584 comments = Session().query(ChangesetComment)\
568 .filter(ChangesetComment.line_no != None)\
585 .filter(ChangesetComment.line_no != None)\
569 .filter(ChangesetComment.f_path != None)\
586 .filter(ChangesetComment.f_path != None)\
570 .filter(ChangesetComment.pull_request == pull_request)
587 .filter(ChangesetComment.pull_request == pull_request)
571 return comments
588 return comments
572
589
573 def _all_general_comments_of_pull_request(self, pull_request):
590 def _all_general_comments_of_pull_request(self, pull_request):
574 comments = Session().query(ChangesetComment)\
591 comments = Session().query(ChangesetComment)\
575 .filter(ChangesetComment.line_no == None)\
592 .filter(ChangesetComment.line_no == None)\
576 .filter(ChangesetComment.f_path == None)\
593 .filter(ChangesetComment.f_path == None)\
577 .filter(ChangesetComment.pull_request == pull_request)
594 .filter(ChangesetComment.pull_request == pull_request)
578 return comments
595 return comments
579
596
580 @staticmethod
597 @staticmethod
581 def use_outdated_comments(pull_request):
598 def use_outdated_comments(pull_request):
582 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
599 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
583 settings = settings_model.get_general_settings()
600 settings = settings_model.get_general_settings()
584 return settings.get('rhodecode_use_outdated_comments', False)
601 return settings.get('rhodecode_use_outdated_comments', False)
585
602
586
603
587 def _parse_comment_line_number(line_no):
604 def _parse_comment_line_number(line_no):
588 """
605 """
589 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
606 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
590 """
607 """
591 old_line = None
608 old_line = None
592 new_line = None
609 new_line = None
593 if line_no.startswith('o'):
610 if line_no.startswith('o'):
594 old_line = int(line_no[1:])
611 old_line = int(line_no[1:])
595 elif line_no.startswith('n'):
612 elif line_no.startswith('n'):
596 new_line = int(line_no[1:])
613 new_line = int(line_no[1:])
597 else:
614 else:
598 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
615 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
599 return diffs.DiffLineNumber(old_line, new_line)
616 return diffs.DiffLineNumber(old_line, new_line)
600
617
601
618
602 def _diff_to_comment_line_number(diff_line):
619 def _diff_to_comment_line_number(diff_line):
603 if diff_line.new is not None:
620 if diff_line.new is not None:
604 return u'n{}'.format(diff_line.new)
621 return u'n{}'.format(diff_line.new)
605 elif diff_line.old is not None:
622 elif diff_line.old is not None:
606 return u'o{}'.format(diff_line.old)
623 return u'o{}'.format(diff_line.old)
607 return u''
624 return u''
608
625
609
626
610 def _diff_line_delta(a, b):
627 def _diff_line_delta(a, b):
611 if None not in (a.new, b.new):
628 if None not in (a.new, b.new):
612 return abs(a.new - b.new)
629 return abs(a.new - b.new)
613 elif None not in (a.old, b.old):
630 elif None not in (a.old, b.old):
614 return abs(a.old - b.old)
631 return abs(a.old - b.old)
615 else:
632 else:
616 raise ValueError(
633 raise ValueError(
617 "Cannot compute delta between {} and {}".format(a, b))
634 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,480 +1,483 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 /**
19 /**
20 RhodeCode JS Files
20 RhodeCode JS Files
21 **/
21 **/
22
22
23 if (typeof console == "undefined" || typeof console.log == "undefined"){
23 if (typeof console == "undefined" || typeof console.log == "undefined"){
24 console = { log: function() {} }
24 console = { log: function() {} }
25 }
25 }
26
26
27 // TODO: move the following function to submodules
27 // TODO: move the following function to submodules
28
28
29 /**
29 /**
30 * show more
30 * show more
31 */
31 */
32 var show_more_event = function(){
32 var show_more_event = function(){
33 $('table .show_more').click(function(e) {
33 $('table .show_more').click(function(e) {
34 var cid = e.target.id.substring(1);
34 var cid = e.target.id.substring(1);
35 var button = $(this);
35 var button = $(this);
36 if (button.hasClass('open')) {
36 if (button.hasClass('open')) {
37 $('#'+cid).hide();
37 $('#'+cid).hide();
38 button.removeClass('open');
38 button.removeClass('open');
39 } else {
39 } else {
40 $('#'+cid).show();
40 $('#'+cid).show();
41 button.addClass('open one');
41 button.addClass('open one');
42 }
42 }
43 });
43 });
44 };
44 };
45
45
46 var compare_radio_buttons = function(repo_name, compare_ref_type){
46 var compare_radio_buttons = function(repo_name, compare_ref_type){
47 $('#compare_action').on('click', function(e){
47 $('#compare_action').on('click', function(e){
48 e.preventDefault();
48 e.preventDefault();
49
49
50 var source = $('input[name=compare_source]:checked').val();
50 var source = $('input[name=compare_source]:checked').val();
51 var target = $('input[name=compare_target]:checked').val();
51 var target = $('input[name=compare_target]:checked').val();
52 if(source && target){
52 if(source && target){
53 var url_data = {
53 var url_data = {
54 repo_name: repo_name,
54 repo_name: repo_name,
55 source_ref: source,
55 source_ref: source,
56 source_ref_type: compare_ref_type,
56 source_ref_type: compare_ref_type,
57 target_ref: target,
57 target_ref: target,
58 target_ref_type: compare_ref_type,
58 target_ref_type: compare_ref_type,
59 merge: 1
59 merge: 1
60 };
60 };
61 window.location = pyroutes.url('compare_url', url_data);
61 window.location = pyroutes.url('compare_url', url_data);
62 }
62 }
63 });
63 });
64 $('.compare-radio-button').on('click', function(e){
64 $('.compare-radio-button').on('click', function(e){
65 var source = $('input[name=compare_source]:checked').val();
65 var source = $('input[name=compare_source]:checked').val();
66 var target = $('input[name=compare_target]:checked').val();
66 var target = $('input[name=compare_target]:checked').val();
67 if(source && target){
67 if(source && target){
68 $('#compare_action').removeAttr("disabled");
68 $('#compare_action').removeAttr("disabled");
69 $('#compare_action').removeClass("disabled");
69 $('#compare_action').removeClass("disabled");
70 }
70 }
71 })
71 })
72 };
72 };
73
73
74 var showRepoSize = function(target, repo_name, commit_id, callback) {
74 var showRepoSize = function(target, repo_name, commit_id, callback) {
75 var container = $('#' + target);
75 var container = $('#' + target);
76 var url = pyroutes.url('repo_stats',
76 var url = pyroutes.url('repo_stats',
77 {"repo_name": repo_name, "commit_id": commit_id});
77 {"repo_name": repo_name, "commit_id": commit_id});
78
78
79 if (!container.hasClass('loaded')) {
79 if (!container.hasClass('loaded')) {
80 $.ajax({url: url})
80 $.ajax({url: url})
81 .complete(function (data) {
81 .complete(function (data) {
82 var responseJSON = data.responseJSON;
82 var responseJSON = data.responseJSON;
83 container.addClass('loaded');
83 container.addClass('loaded');
84 container.html(responseJSON.size);
84 container.html(responseJSON.size);
85 callback(responseJSON.code_stats)
85 callback(responseJSON.code_stats)
86 })
86 })
87 .fail(function (data) {
87 .fail(function (data) {
88 console.log('failed to load repo stats');
88 console.log('failed to load repo stats');
89 });
89 });
90 }
90 }
91
91
92 };
92 };
93
93
94 var showRepoStats = function(target, data){
94 var showRepoStats = function(target, data){
95 var container = $('#' + target);
95 var container = $('#' + target);
96
96
97 if (container.hasClass('loaded')) {
97 if (container.hasClass('loaded')) {
98 return
98 return
99 }
99 }
100
100
101 var total = 0;
101 var total = 0;
102 var no_data = true;
102 var no_data = true;
103 var tbl = document.createElement('table');
103 var tbl = document.createElement('table');
104 tbl.setAttribute('class', 'trending_language_tbl');
104 tbl.setAttribute('class', 'trending_language_tbl');
105
105
106 $.each(data, function(key, val){
106 $.each(data, function(key, val){
107 total += val.count;
107 total += val.count;
108 });
108 });
109
109
110 var sortedStats = [];
110 var sortedStats = [];
111 for (var obj in data){
111 for (var obj in data){
112 sortedStats.push([obj, data[obj]])
112 sortedStats.push([obj, data[obj]])
113 }
113 }
114 var sortedData = sortedStats.sort(function (a, b) {
114 var sortedData = sortedStats.sort(function (a, b) {
115 return b[1].count - a[1].count
115 return b[1].count - a[1].count
116 });
116 });
117 var cnt = 0;
117 var cnt = 0;
118 $.each(sortedData, function(idx, val){
118 $.each(sortedData, function(idx, val){
119 cnt += 1;
119 cnt += 1;
120 no_data = false;
120 no_data = false;
121
121
122 var hide = cnt > 2;
122 var hide = cnt > 2;
123 var tr = document.createElement('tr');
123 var tr = document.createElement('tr');
124 if (hide) {
124 if (hide) {
125 tr.setAttribute('style', 'display:none');
125 tr.setAttribute('style', 'display:none');
126 tr.setAttribute('class', 'stats_hidden');
126 tr.setAttribute('class', 'stats_hidden');
127 }
127 }
128
128
129 var key = val[0];
129 var key = val[0];
130 var obj = {"desc": val[1].desc, "count": val[1].count};
130 var obj = {"desc": val[1].desc, "count": val[1].count};
131
131
132 var percentage = Math.round((obj.count / total * 100), 2);
132 var percentage = Math.round((obj.count / total * 100), 2);
133
133
134 var td1 = document.createElement('td');
134 var td1 = document.createElement('td');
135 td1.width = 300;
135 td1.width = 300;
136 var trending_language_label = document.createElement('div');
136 var trending_language_label = document.createElement('div');
137 trending_language_label.innerHTML = obj.desc + " (.{0})".format(key);
137 trending_language_label.innerHTML = obj.desc + " (.{0})".format(key);
138 td1.appendChild(trending_language_label);
138 td1.appendChild(trending_language_label);
139
139
140 var td2 = document.createElement('td');
140 var td2 = document.createElement('td');
141 var trending_language = document.createElement('div');
141 var trending_language = document.createElement('div');
142 var nr_files = obj.count +" "+ _ngettext('file', 'files', obj.count);
142 var nr_files = obj.count +" "+ _ngettext('file', 'files', obj.count);
143
143
144 trending_language.title = key + " " + nr_files;
144 trending_language.title = key + " " + nr_files;
145
145
146 trending_language.innerHTML = "<span>" + percentage + "% " + nr_files
146 trending_language.innerHTML = "<span>" + percentage + "% " + nr_files
147 + "</span><b>" + percentage + "% " + nr_files + "</b>";
147 + "</span><b>" + percentage + "% " + nr_files + "</b>";
148
148
149 trending_language.setAttribute("class", 'trending_language');
149 trending_language.setAttribute("class", 'trending_language');
150 $('b', trending_language)[0].style.width = percentage + "%";
150 $('b', trending_language)[0].style.width = percentage + "%";
151 td2.appendChild(trending_language);
151 td2.appendChild(trending_language);
152
152
153 tr.appendChild(td1);
153 tr.appendChild(td1);
154 tr.appendChild(td2);
154 tr.appendChild(td2);
155 tbl.appendChild(tr);
155 tbl.appendChild(tr);
156 if (cnt == 3) {
156 if (cnt == 3) {
157 var show_more = document.createElement('tr');
157 var show_more = document.createElement('tr');
158 var td = document.createElement('td');
158 var td = document.createElement('td');
159 lnk = document.createElement('a');
159 lnk = document.createElement('a');
160
160
161 lnk.href = '#';
161 lnk.href = '#';
162 lnk.innerHTML = _gettext('Show more');
162 lnk.innerHTML = _gettext('Show more');
163 lnk.id = 'code_stats_show_more';
163 lnk.id = 'code_stats_show_more';
164 td.appendChild(lnk);
164 td.appendChild(lnk);
165
165
166 show_more.appendChild(td);
166 show_more.appendChild(td);
167 show_more.appendChild(document.createElement('td'));
167 show_more.appendChild(document.createElement('td'));
168 tbl.appendChild(show_more);
168 tbl.appendChild(show_more);
169 }
169 }
170 });
170 });
171
171
172 $(container).html(tbl);
172 $(container).html(tbl);
173 $(container).addClass('loaded');
173 $(container).addClass('loaded');
174
174
175 $('#code_stats_show_more').on('click', function (e) {
175 $('#code_stats_show_more').on('click', function (e) {
176 e.preventDefault();
176 e.preventDefault();
177 $('.stats_hidden').each(function (idx) {
177 $('.stats_hidden').each(function (idx) {
178 $(this).css("display", "");
178 $(this).css("display", "");
179 });
179 });
180 $('#code_stats_show_more').hide();
180 $('#code_stats_show_more').hide();
181 });
181 });
182
182
183 };
183 };
184
184
185 // returns a node from given html;
185 // returns a node from given html;
186 var fromHTML = function(html){
186 var fromHTML = function(html){
187 var _html = document.createElement('element');
187 var _html = document.createElement('element');
188 _html.innerHTML = html;
188 _html.innerHTML = html;
189 return _html;
189 return _html;
190 };
190 };
191
191
192 // Toggle Collapsable Content
192 // Toggle Collapsable Content
193 function collapsableContent() {
193 function collapsableContent() {
194
194
195 $('.collapsable-content').not('.no-hide').hide();
195 $('.collapsable-content').not('.no-hide').hide();
196
196
197 $('.btn-collapse').unbind(); //in case we've been here before
197 $('.btn-collapse').unbind(); //in case we've been here before
198 $('.btn-collapse').click(function() {
198 $('.btn-collapse').click(function() {
199 var button = $(this);
199 var button = $(this);
200 var togglename = $(this).data("toggle");
200 var togglename = $(this).data("toggle");
201 $('.collapsable-content[data-toggle='+togglename+']').toggle();
201 $('.collapsable-content[data-toggle='+togglename+']').toggle();
202 if ($(this).html()=="Show Less")
202 if ($(this).html()=="Show Less")
203 $(this).html("Show More");
203 $(this).html("Show More");
204 else
204 else
205 $(this).html("Show Less");
205 $(this).html("Show Less");
206 });
206 });
207 };
207 };
208
208
209 var timeagoActivate = function() {
209 var timeagoActivate = function() {
210 $("time.timeago").timeago();
210 $("time.timeago").timeago();
211 };
211 };
212
212
213 // Formatting values in a Select2 dropdown of commit references
213 // Formatting values in a Select2 dropdown of commit references
214 var formatSelect2SelectionRefs = function(commit_ref){
214 var formatSelect2SelectionRefs = function(commit_ref){
215 var tmpl = '';
215 var tmpl = '';
216 if (!commit_ref.text || commit_ref.type === 'sha'){
216 if (!commit_ref.text || commit_ref.type === 'sha'){
217 return commit_ref.text;
217 return commit_ref.text;
218 }
218 }
219 if (commit_ref.type === 'branch'){
219 if (commit_ref.type === 'branch'){
220 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
220 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
221 } else if (commit_ref.type === 'tag'){
221 } else if (commit_ref.type === 'tag'){
222 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
222 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
223 } else if (commit_ref.type === 'book'){
223 } else if (commit_ref.type === 'book'){
224 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
224 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
225 }
225 }
226 return tmpl.concat(commit_ref.text);
226 return tmpl.concat(commit_ref.text);
227 };
227 };
228
228
229 // takes a given html element and scrolls it down offset pixels
229 // takes a given html element and scrolls it down offset pixels
230 function offsetScroll(element, offset) {
230 function offsetScroll(element, offset) {
231 setTimeout(function() {
231 setTimeout(function() {
232 var location = element.offset().top;
232 var location = element.offset().top;
233 // some browsers use body, some use html
233 // some browsers use body, some use html
234 $('html, body').animate({ scrollTop: (location - offset) });
234 $('html, body').animate({ scrollTop: (location - offset) });
235 }, 100);
235 }, 100);
236 }
236 }
237
237
238 // scroll an element `percent`% from the top of page in `time` ms
238 // scroll an element `percent`% from the top of page in `time` ms
239 function scrollToElement(element, percent, time) {
239 function scrollToElement(element, percent, time) {
240 percent = (percent === undefined ? 25 : percent);
240 percent = (percent === undefined ? 25 : percent);
241 time = (time === undefined ? 100 : time);
241 time = (time === undefined ? 100 : time);
242
242
243 var $element = $(element);
243 var $element = $(element);
244 if ($element.length == 0) {
245 throw('Cannot scroll to {0}'.format(element))
246 }
244 var elOffset = $element.offset().top;
247 var elOffset = $element.offset().top;
245 var elHeight = $element.height();
248 var elHeight = $element.height();
246 var windowHeight = $(window).height();
249 var windowHeight = $(window).height();
247 var offset = elOffset;
250 var offset = elOffset;
248 if (elHeight < windowHeight) {
251 if (elHeight < windowHeight) {
249 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
252 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
250 }
253 }
251 setTimeout(function() {
254 setTimeout(function() {
252 $('html, body').animate({ scrollTop: offset});
255 $('html, body').animate({ scrollTop: offset});
253 }, time);
256 }, time);
254 }
257 }
255
258
256 /**
259 /**
257 * global hooks after DOM is loaded
260 * global hooks after DOM is loaded
258 */
261 */
259 $(document).ready(function() {
262 $(document).ready(function() {
260 firefoxAnchorFix();
263 firefoxAnchorFix();
261
264
262 $('.navigation a.menulink').on('click', function(e){
265 $('.navigation a.menulink').on('click', function(e){
263 var menuitem = $(this).parent('li');
266 var menuitem = $(this).parent('li');
264 if (menuitem.hasClass('open')) {
267 if (menuitem.hasClass('open')) {
265 menuitem.removeClass('open');
268 menuitem.removeClass('open');
266 } else {
269 } else {
267 menuitem.addClass('open');
270 menuitem.addClass('open');
268 $(document).on('click', function(event) {
271 $(document).on('click', function(event) {
269 if (!$(event.target).closest(menuitem).length) {
272 if (!$(event.target).closest(menuitem).length) {
270 menuitem.removeClass('open');
273 menuitem.removeClass('open');
271 }
274 }
272 });
275 });
273 }
276 }
274 });
277 });
275 $('.compare_view_files').on(
278 $('.compare_view_files').on(
276 'mouseenter mouseleave', 'tr.line .lineno a',function(event) {
279 'mouseenter mouseleave', 'tr.line .lineno a',function(event) {
277 if (event.type === "mouseenter") {
280 if (event.type === "mouseenter") {
278 $(this).parents('tr.line').addClass('hover');
281 $(this).parents('tr.line').addClass('hover');
279 } else {
282 } else {
280 $(this).parents('tr.line').removeClass('hover');
283 $(this).parents('tr.line').removeClass('hover');
281 }
284 }
282 });
285 });
283
286
284 $('.compare_view_files').on(
287 $('.compare_view_files').on(
285 'mouseenter mouseleave', 'tr.line .add-comment-line a',function(event){
288 'mouseenter mouseleave', 'tr.line .add-comment-line a',function(event){
286 if (event.type === "mouseenter") {
289 if (event.type === "mouseenter") {
287 $(this).parents('tr.line').addClass('commenting');
290 $(this).parents('tr.line').addClass('commenting');
288 } else {
291 } else {
289 $(this).parents('tr.line').removeClass('commenting');
292 $(this).parents('tr.line').removeClass('commenting');
290 }
293 }
291 });
294 });
292
295
293 $('body').on( /* TODO: replace the $('.compare_view_files').on('click') below
296 $('body').on( /* TODO: replace the $('.compare_view_files').on('click') below
294 when new diffs are integrated */
297 when new diffs are integrated */
295 'click', '.cb-lineno a', function(event) {
298 'click', '.cb-lineno a', function(event) {
296
299
297 if ($(this).attr('data-line-no') !== ""){
300 if ($(this).attr('data-line-no') !== ""){
298 $('.cb-line-selected').removeClass('cb-line-selected');
301 $('.cb-line-selected').removeClass('cb-line-selected');
299 var td = $(this).parent();
302 var td = $(this).parent();
300 td.addClass('cb-line-selected'); // line number td
303 td.addClass('cb-line-selected'); // line number td
301 td.prev().addClass('cb-line-selected'); // line data td
304 td.prev().addClass('cb-line-selected'); // line data td
302 td.next().addClass('cb-line-selected'); // line content td
305 td.next().addClass('cb-line-selected'); // line content td
303
306
304 // Replace URL without jumping to it if browser supports.
307 // Replace URL without jumping to it if browser supports.
305 // Default otherwise
308 // Default otherwise
306 if (history.pushState) {
309 if (history.pushState) {
307 var new_location = location.href.rstrip('#');
310 var new_location = location.href.rstrip('#');
308 if (location.hash) {
311 if (location.hash) {
309 new_location = new_location.replace(location.hash, "");
312 new_location = new_location.replace(location.hash, "");
310 }
313 }
311
314
312 // Make new anchor url
315 // Make new anchor url
313 new_location = new_location + $(this).attr('href');
316 new_location = new_location + $(this).attr('href');
314 history.pushState(true, document.title, new_location);
317 history.pushState(true, document.title, new_location);
315
318
316 return false;
319 return false;
317 }
320 }
318 }
321 }
319 });
322 });
320
323
321 $('.compare_view_files').on( /* TODO: replace this with .cb function above
324 $('.compare_view_files').on( /* TODO: replace this with .cb function above
322 when new diffs are integrated */
325 when new diffs are integrated */
323 'click', 'tr.line .lineno a',function(event) {
326 'click', 'tr.line .lineno a',function(event) {
324 if ($(this).text() != ""){
327 if ($(this).text() != ""){
325 $('tr.line').removeClass('selected');
328 $('tr.line').removeClass('selected');
326 $(this).parents("tr.line").addClass('selected');
329 $(this).parents("tr.line").addClass('selected');
327
330
328 // Replace URL without jumping to it if browser supports.
331 // Replace URL without jumping to it if browser supports.
329 // Default otherwise
332 // Default otherwise
330 if (history.pushState) {
333 if (history.pushState) {
331 var new_location = location.href;
334 var new_location = location.href;
332 if (location.hash){
335 if (location.hash){
333 new_location = new_location.replace(location.hash, "");
336 new_location = new_location.replace(location.hash, "");
334 }
337 }
335
338
336 // Make new anchor url
339 // Make new anchor url
337 var new_location = new_location+$(this).attr('href');
340 var new_location = new_location+$(this).attr('href');
338 history.pushState(true, document.title, new_location);
341 history.pushState(true, document.title, new_location);
339
342
340 return false;
343 return false;
341 }
344 }
342 }
345 }
343 });
346 });
344
347
345 $('.compare_view_files').on(
348 $('.compare_view_files').on(
346 'click', 'tr.line .add-comment-line a',function(event) {
349 'click', 'tr.line .add-comment-line a',function(event) {
347 var tr = $(event.currentTarget).parents('tr.line')[0];
350 var tr = $(event.currentTarget).parents('tr.line')[0];
348 injectInlineForm(tr);
351 injectInlineForm(tr);
349 return false;
352 return false;
350 });
353 });
351
354
352 $('.collapse_file').on('click', function(e) {
355 $('.collapse_file').on('click', function(e) {
353 e.stopPropagation();
356 e.stopPropagation();
354 if ($(e.target).is('a')) { return; }
357 if ($(e.target).is('a')) { return; }
355 var node = $(e.delegateTarget).first();
358 var node = $(e.delegateTarget).first();
356 var icon = $($(node.children().first()).children().first());
359 var icon = $($(node.children().first()).children().first());
357 var id = node.attr('fid');
360 var id = node.attr('fid');
358 var target = $('#'+id);
361 var target = $('#'+id);
359 var tr = $('#tr_'+id);
362 var tr = $('#tr_'+id);
360 var diff = $('#diff_'+id);
363 var diff = $('#diff_'+id);
361 if(node.hasClass('expand_file')){
364 if(node.hasClass('expand_file')){
362 node.removeClass('expand_file');
365 node.removeClass('expand_file');
363 icon.removeClass('expand_file_icon');
366 icon.removeClass('expand_file_icon');
364 node.addClass('collapse_file');
367 node.addClass('collapse_file');
365 icon.addClass('collapse_file_icon');
368 icon.addClass('collapse_file_icon');
366 diff.show();
369 diff.show();
367 tr.show();
370 tr.show();
368 target.show();
371 target.show();
369 } else {
372 } else {
370 node.removeClass('collapse_file');
373 node.removeClass('collapse_file');
371 icon.removeClass('collapse_file_icon');
374 icon.removeClass('collapse_file_icon');
372 node.addClass('expand_file');
375 node.addClass('expand_file');
373 icon.addClass('expand_file_icon');
376 icon.addClass('expand_file_icon');
374 diff.hide();
377 diff.hide();
375 tr.hide();
378 tr.hide();
376 target.hide();
379 target.hide();
377 }
380 }
378 });
381 });
379
382
380 $('#expand_all_files').click(function() {
383 $('#expand_all_files').click(function() {
381 $('.expand_file').each(function() {
384 $('.expand_file').each(function() {
382 var node = $(this);
385 var node = $(this);
383 var icon = $($(node.children().first()).children().first());
386 var icon = $($(node.children().first()).children().first());
384 var id = $(this).attr('fid');
387 var id = $(this).attr('fid');
385 var target = $('#'+id);
388 var target = $('#'+id);
386 var tr = $('#tr_'+id);
389 var tr = $('#tr_'+id);
387 var diff = $('#diff_'+id);
390 var diff = $('#diff_'+id);
388 node.removeClass('expand_file');
391 node.removeClass('expand_file');
389 icon.removeClass('expand_file_icon');
392 icon.removeClass('expand_file_icon');
390 node.addClass('collapse_file');
393 node.addClass('collapse_file');
391 icon.addClass('collapse_file_icon');
394 icon.addClass('collapse_file_icon');
392 diff.show();
395 diff.show();
393 tr.show();
396 tr.show();
394 target.show();
397 target.show();
395 });
398 });
396 });
399 });
397
400
398 $('#collapse_all_files').click(function() {
401 $('#collapse_all_files').click(function() {
399 $('.collapse_file').each(function() {
402 $('.collapse_file').each(function() {
400 var node = $(this);
403 var node = $(this);
401 var icon = $($(node.children().first()).children().first());
404 var icon = $($(node.children().first()).children().first());
402 var id = $(this).attr('fid');
405 var id = $(this).attr('fid');
403 var target = $('#'+id);
406 var target = $('#'+id);
404 var tr = $('#tr_'+id);
407 var tr = $('#tr_'+id);
405 var diff = $('#diff_'+id);
408 var diff = $('#diff_'+id);
406 node.removeClass('collapse_file');
409 node.removeClass('collapse_file');
407 icon.removeClass('collapse_file_icon');
410 icon.removeClass('collapse_file_icon');
408 node.addClass('expand_file');
411 node.addClass('expand_file');
409 icon.addClass('expand_file_icon');
412 icon.addClass('expand_file_icon');
410 diff.hide();
413 diff.hide();
411 tr.hide();
414 tr.hide();
412 target.hide();
415 target.hide();
413 });
416 });
414 });
417 });
415
418
416 // Mouse over behavior for comments and line selection
419 // Mouse over behavior for comments and line selection
417
420
418 // Select the line that comes from the url anchor
421 // Select the line that comes from the url anchor
419 // At the time of development, Chrome didn't seem to support jquery's :target
422 // At the time of development, Chrome didn't seem to support jquery's :target
420 // element, so I had to scroll manually
423 // element, so I had to scroll manually
421
424
422 if (location.hash) {
425 if (location.hash) {
423 var result = splitDelimitedHash(location.hash);
426 var result = splitDelimitedHash(location.hash);
424 var loc = result.loc;
427 var loc = result.loc;
425 if (loc.length > 1) {
428 if (loc.length > 1) {
426
429
427 var highlightable_line_tds = [];
430 var highlightable_line_tds = [];
428
431
429 // source code line format
432 // source code line format
430 var page_highlights = loc.substring(
433 var page_highlights = loc.substring(
431 loc.indexOf('#') + 1).split('L');
434 loc.indexOf('#') + 1).split('L');
432
435
433 if (page_highlights.length > 1) {
436 if (page_highlights.length > 1) {
434 var highlight_ranges = page_highlights[1].split(",");
437 var highlight_ranges = page_highlights[1].split(",");
435 var h_lines = [];
438 var h_lines = [];
436 for (var pos in highlight_ranges) {
439 for (var pos in highlight_ranges) {
437 var _range = highlight_ranges[pos].split('-');
440 var _range = highlight_ranges[pos].split('-');
438 if (_range.length === 2) {
441 if (_range.length === 2) {
439 var start = parseInt(_range[0]);
442 var start = parseInt(_range[0]);
440 var end = parseInt(_range[1]);
443 var end = parseInt(_range[1]);
441 if (start < end) {
444 if (start < end) {
442 for (var i = start; i <= end; i++) {
445 for (var i = start; i <= end; i++) {
443 h_lines.push(i);
446 h_lines.push(i);
444 }
447 }
445 }
448 }
446 }
449 }
447 else {
450 else {
448 h_lines.push(parseInt(highlight_ranges[pos]));
451 h_lines.push(parseInt(highlight_ranges[pos]));
449 }
452 }
450 }
453 }
451 for (pos in h_lines) {
454 for (pos in h_lines) {
452 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
455 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
453 if (line_td.length) {
456 if (line_td.length) {
454 highlightable_line_tds.push(line_td);
457 highlightable_line_tds.push(line_td);
455 }
458 }
456 }
459 }
457 }
460 }
458
461
459 // now check a direct id reference (diff page)
462 // now check a direct id reference (diff page)
460 if ($(loc).length && $(loc).hasClass('cb-lineno')) {
463 if ($(loc).length && $(loc).hasClass('cb-lineno')) {
461 highlightable_line_tds.push($(loc));
464 highlightable_line_tds.push($(loc));
462 }
465 }
463 $.each(highlightable_line_tds, function (i, $td) {
466 $.each(highlightable_line_tds, function (i, $td) {
464 $td.addClass('cb-line-selected'); // line number td
467 $td.addClass('cb-line-selected'); // line number td
465 $td.prev().addClass('cb-line-selected'); // line data
468 $td.prev().addClass('cb-line-selected'); // line data
466 $td.next().addClass('cb-line-selected'); // line content
469 $td.next().addClass('cb-line-selected'); // line content
467 });
470 });
468
471
469 if (highlightable_line_tds.length) {
472 if (highlightable_line_tds.length) {
470 var $first_line_td = highlightable_line_tds[0];
473 var $first_line_td = highlightable_line_tds[0];
471 scrollToElement($first_line_td);
474 scrollToElement($first_line_td);
472 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
475 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
473 td: $first_line_td,
476 td: $first_line_td,
474 remainder: result.remainder
477 remainder: result.remainder
475 });
478 });
476 }
479 }
477 }
480 }
478 }
481 }
479 collapsableContent();
482 collapsableContent();
480 });
483 });
@@ -1,809 +1,811 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 (function(mod) {
47
47
48 if (typeof exports == "object" && typeof module == "object") {
48 if (typeof exports == "object" && typeof module == "object") {
49 // CommonJS
49 // CommonJS
50 module.exports = mod();
50 module.exports = mod();
51 }
51 }
52 else {
52 else {
53 // Plain browser env
53 // Plain browser env
54 (this || window).CommentForm = mod();
54 (this || window).CommentForm = mod();
55 }
55 }
56
56
57 })(function() {
57 })(function() {
58 "use strict";
58 "use strict";
59
59
60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
61 if (!(this instanceof CommentForm)) {
61 if (!(this instanceof CommentForm)) {
62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
63 }
63 }
64
64
65 // bind the element instance to our Form
65 // bind the element instance to our Form
66 $(formElement).get(0).CommentForm = this;
66 $(formElement).get(0).CommentForm = this;
67
67
68 this.withLineNo = function(selector) {
68 this.withLineNo = function(selector) {
69 var lineNo = this.lineNo;
69 var lineNo = this.lineNo;
70 if (lineNo === undefined) {
70 if (lineNo === undefined) {
71 return selector
71 return selector
72 } else {
72 } else {
73 return selector + '_' + lineNo;
73 return selector + '_' + lineNo;
74 }
74 }
75 };
75 };
76
76
77 this.commitId = commitId;
77 this.commitId = commitId;
78 this.pullRequestId = pullRequestId;
78 this.pullRequestId = pullRequestId;
79 this.lineNo = lineNo;
79 this.lineNo = lineNo;
80 this.initAutocompleteActions = initAutocompleteActions;
80 this.initAutocompleteActions = initAutocompleteActions;
81
81
82 this.previewButton = this.withLineNo('#preview-btn');
82 this.previewButton = this.withLineNo('#preview-btn');
83 this.previewContainer = this.withLineNo('#preview-container');
83 this.previewContainer = this.withLineNo('#preview-container');
84
84
85 this.previewBoxSelector = this.withLineNo('#preview-box');
85 this.previewBoxSelector = this.withLineNo('#preview-box');
86
86
87 this.editButton = this.withLineNo('#edit-btn');
87 this.editButton = this.withLineNo('#edit-btn');
88 this.editContainer = this.withLineNo('#edit-container');
88 this.editContainer = this.withLineNo('#edit-container');
89 this.cancelButton = this.withLineNo('#cancel-btn');
89 this.cancelButton = this.withLineNo('#cancel-btn');
90 this.commentType = this.withLineNo('#comment_type');
90 this.commentType = this.withLineNo('#comment_type');
91
91
92 this.resolvesId = null;
92 this.resolvesId = null;
93 this.resolvesActionId = null;
93 this.resolvesActionId = null;
94
94
95 this.cmBox = this.withLineNo('#text');
95 this.cmBox = this.withLineNo('#text');
96 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
96 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
97
97
98 this.statusChange = this.withLineNo('#change_status');
98 this.statusChange = this.withLineNo('#change_status');
99
99
100 this.submitForm = formElement;
100 this.submitForm = formElement;
101 this.submitButton = $(this.submitForm).find('input[type="submit"]');
101 this.submitButton = $(this.submitForm).find('input[type="submit"]');
102 this.submitButtonText = this.submitButton.val();
102 this.submitButtonText = this.submitButton.val();
103
103
104 this.previewUrl = pyroutes.url('changeset_comment_preview',
104 this.previewUrl = pyroutes.url('changeset_comment_preview',
105 {'repo_name': templateContext.repo_name});
105 {'repo_name': templateContext.repo_name});
106
106
107 if (resolvesCommentId){
107 if (resolvesCommentId){
108 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
108 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
109 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
109 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
110 $(this.commentType).prop('disabled', true);
110 $(this.commentType).prop('disabled', true);
111 $(this.commentType).addClass('disabled');
111 $(this.commentType).addClass('disabled');
112
112
113 // disable select
113 // disable select
114 setTimeout(function() {
114 setTimeout(function() {
115 $(self.statusChange).select2('readonly', true);
115 $(self.statusChange).select2('readonly', true);
116 }, 10);
116 }, 10);
117
117
118 var resolvedInfo = (
118 var resolvedInfo = (
119 '<li class="resolve-action">' +
119 '<li class="resolve-action">' +
120 '<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}">' +
121 '<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>' +
122 '</li>'
122 '</li>'
123 ).format(resolvesCommentId, _gettext('resolve comment'));
123 ).format(resolvesCommentId, _gettext('resolve comment'));
124 $(resolvedInfo).insertAfter($(this.commentType).parent());
124 $(resolvedInfo).insertAfter($(this.commentType).parent());
125 }
125 }
126
126
127 // based on commitId, or pullRequestId decide where do we submit
127 // based on commitId, or pullRequestId decide where do we submit
128 // out data
128 // out data
129 if (this.commitId){
129 if (this.commitId){
130 this.submitUrl = pyroutes.url('changeset_comment',
130 this.submitUrl = pyroutes.url('changeset_comment',
131 {'repo_name': templateContext.repo_name,
131 {'repo_name': templateContext.repo_name,
132 'revision': this.commitId});
132 'revision': this.commitId});
133 this.selfUrl = pyroutes.url('changeset_home',
133 this.selfUrl = pyroutes.url('changeset_home',
134 {'repo_name': templateContext.repo_name,
134 {'repo_name': templateContext.repo_name,
135 'revision': this.commitId});
135 'revision': this.commitId});
136
136
137 } else if (this.pullRequestId) {
137 } else if (this.pullRequestId) {
138 this.submitUrl = pyroutes.url('pullrequest_comment',
138 this.submitUrl = pyroutes.url('pullrequest_comment',
139 {'repo_name': templateContext.repo_name,
139 {'repo_name': templateContext.repo_name,
140 'pull_request_id': this.pullRequestId});
140 'pull_request_id': this.pullRequestId});
141 this.selfUrl = pyroutes.url('pullrequest_show',
141 this.selfUrl = pyroutes.url('pullrequest_show',
142 {'repo_name': templateContext.repo_name,
142 {'repo_name': templateContext.repo_name,
143 'pull_request_id': this.pullRequestId});
143 'pull_request_id': this.pullRequestId});
144
144
145 } else {
145 } else {
146 throw new Error(
146 throw new Error(
147 'CommentForm requires pullRequestId, or commitId to be specified.')
147 'CommentForm requires pullRequestId, or commitId to be specified.')
148 }
148 }
149
149
150 // FUNCTIONS and helpers
150 // FUNCTIONS and helpers
151 var self = this;
151 var self = this;
152
152
153 this.isInline = function(){
153 this.isInline = function(){
154 return this.lineNo && this.lineNo != 'general';
154 return this.lineNo && this.lineNo != 'general';
155 };
155 };
156
156
157 this.getCmInstance = function(){
157 this.getCmInstance = function(){
158 return this.cm
158 return this.cm
159 };
159 };
160
160
161 this.setPlaceholder = function(placeholder) {
161 this.setPlaceholder = function(placeholder) {
162 var cm = this.getCmInstance();
162 var cm = this.getCmInstance();
163 if (cm){
163 if (cm){
164 cm.setOption('placeholder', placeholder);
164 cm.setOption('placeholder', placeholder);
165 }
165 }
166 };
166 };
167
167
168 this.getCommentStatus = function() {
168 this.getCommentStatus = function() {
169 return $(this.submitForm).find(this.statusChange).val();
169 return $(this.submitForm).find(this.statusChange).val();
170 };
170 };
171 this.getCommentType = function() {
171 this.getCommentType = function() {
172 return $(this.submitForm).find(this.commentType).val();
172 return $(this.submitForm).find(this.commentType).val();
173 };
173 };
174
174
175 this.getResolvesId = function() {
175 this.getResolvesId = function() {
176 return $(this.submitForm).find(this.resolvesId).val() || null;
176 return $(this.submitForm).find(this.resolvesId).val() || null;
177 };
177 };
178 this.markCommentResolved = function(resolvedCommentId){
178 this.markCommentResolved = function(resolvedCommentId){
179 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
179 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
180 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
180 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
181 };
181 };
182
182
183 this.isAllowedToSubmit = function() {
183 this.isAllowedToSubmit = function() {
184 return !$(this.submitButton).prop('disabled');
184 return !$(this.submitButton).prop('disabled');
185 };
185 };
186
186
187 this.initStatusChangeSelector = function(){
187 this.initStatusChangeSelector = function(){
188 var formatChangeStatus = function(state, escapeMarkup) {
188 var formatChangeStatus = function(state, escapeMarkup) {
189 var originalOption = state.element;
189 var originalOption = state.element;
190 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
190 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
191 '<span>' + escapeMarkup(state.text) + '</span>';
191 '<span>' + escapeMarkup(state.text) + '</span>';
192 };
192 };
193 var formatResult = function(result, container, query, escapeMarkup) {
193 var formatResult = function(result, container, query, escapeMarkup) {
194 return formatChangeStatus(result, escapeMarkup);
194 return formatChangeStatus(result, escapeMarkup);
195 };
195 };
196
196
197 var formatSelection = function(data, container, escapeMarkup) {
197 var formatSelection = function(data, container, escapeMarkup) {
198 return formatChangeStatus(data, escapeMarkup);
198 return formatChangeStatus(data, escapeMarkup);
199 };
199 };
200
200
201 $(this.submitForm).find(this.statusChange).select2({
201 $(this.submitForm).find(this.statusChange).select2({
202 placeholder: _gettext('Status Review'),
202 placeholder: _gettext('Status Review'),
203 formatResult: formatResult,
203 formatResult: formatResult,
204 formatSelection: formatSelection,
204 formatSelection: formatSelection,
205 containerCssClass: "drop-menu status_box_menu",
205 containerCssClass: "drop-menu status_box_menu",
206 dropdownCssClass: "drop-menu-dropdown",
206 dropdownCssClass: "drop-menu-dropdown",
207 dropdownAutoWidth: true,
207 dropdownAutoWidth: true,
208 minimumResultsForSearch: -1
208 minimumResultsForSearch: -1
209 });
209 });
210 $(this.submitForm).find(this.statusChange).on('change', function() {
210 $(this.submitForm).find(this.statusChange).on('change', function() {
211 var status = self.getCommentStatus();
211 var status = self.getCommentStatus();
212 if (status && !self.isInline()) {
212 if (status && !self.isInline()) {
213 $(self.submitButton).prop('disabled', false);
213 $(self.submitButton).prop('disabled', false);
214 }
214 }
215
215
216 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);
217 self.setPlaceholder(placeholderText)
217 self.setPlaceholder(placeholderText)
218 })
218 })
219 };
219 };
220
220
221 // reset the comment form into it's original state
221 // reset the comment form into it's original state
222 this.resetCommentFormState = function(content) {
222 this.resetCommentFormState = function(content) {
223 content = content || '';
223 content = content || '';
224
224
225 $(this.editContainer).show();
225 $(this.editContainer).show();
226 $(this.editButton).parent().addClass('active');
226 $(this.editButton).parent().addClass('active');
227
227
228 $(this.previewContainer).hide();
228 $(this.previewContainer).hide();
229 $(this.previewButton).parent().removeClass('active');
229 $(this.previewButton).parent().removeClass('active');
230
230
231 this.setActionButtonsDisabled(true);
231 this.setActionButtonsDisabled(true);
232 self.cm.setValue(content);
232 self.cm.setValue(content);
233 self.cm.setOption("readOnly", false);
233 self.cm.setOption("readOnly", false);
234
234
235 if (this.resolvesId) {
235 if (this.resolvesId) {
236 // destroy the resolve action
236 // destroy the resolve action
237 $(this.resolvesId).parent().remove();
237 $(this.resolvesId).parent().remove();
238 }
238 }
239
239
240 $(this.statusChange).select2('readonly', false);
240 $(this.statusChange).select2('readonly', false);
241 };
241 };
242
242
243 this.globalSubmitSuccessCallback = function(){
243 this.globalSubmitSuccessCallback = function(){
244 // default behaviour is to call GLOBAL hook, if it's registered.
244 // default behaviour is to call GLOBAL hook, if it's registered.
245 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
245 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
246 commentFormGlobalSubmitSuccessCallback()
246 commentFormGlobalSubmitSuccessCallback()
247 }
247 }
248 };
248 };
249
249
250 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
250 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
251 failHandler = failHandler || function() {};
251 failHandler = failHandler || function() {};
252 var postData = toQueryString(postData);
252 var postData = toQueryString(postData);
253 var request = $.ajax({
253 var request = $.ajax({
254 url: url,
254 url: url,
255 type: 'POST',
255 type: 'POST',
256 data: postData,
256 data: postData,
257 headers: {'X-PARTIAL-XHR': true}
257 headers: {'X-PARTIAL-XHR': true}
258 })
258 })
259 .done(function(data) {
259 .done(function(data) {
260 successHandler(data);
260 successHandler(data);
261 })
261 })
262 .fail(function(data, textStatus, errorThrown){
262 .fail(function(data, textStatus, errorThrown){
263 alert(
263 alert(
264 "Error while submitting comment.\n" +
264 "Error while submitting comment.\n" +
265 "Error code {0} ({1}).".format(data.status, data.statusText));
265 "Error code {0} ({1}).".format(data.status, data.statusText));
266 failHandler()
266 failHandler()
267 });
267 });
268 return request;
268 return request;
269 };
269 };
270
270
271 // overwrite a submitHandler, we need to do it for inline comments
271 // overwrite a submitHandler, we need to do it for inline comments
272 this.setHandleFormSubmit = function(callback) {
272 this.setHandleFormSubmit = function(callback) {
273 this.handleFormSubmit = callback;
273 this.handleFormSubmit = callback;
274 };
274 };
275
275
276 // overwrite a submitSuccessHandler
276 // overwrite a submitSuccessHandler
277 this.setGlobalSubmitSuccessCallback = function(callback) {
277 this.setGlobalSubmitSuccessCallback = function(callback) {
278 this.globalSubmitSuccessCallback = callback;
278 this.globalSubmitSuccessCallback = callback;
279 };
279 };
280
280
281 // default handler for for submit for main comments
281 // default handler for for submit for main comments
282 this.handleFormSubmit = function() {
282 this.handleFormSubmit = function() {
283 var text = self.cm.getValue();
283 var text = self.cm.getValue();
284 var status = self.getCommentStatus();
284 var status = self.getCommentStatus();
285 var commentType = self.getCommentType();
285 var commentType = self.getCommentType();
286 var resolvesCommentId = self.getResolvesId();
286 var resolvesCommentId = self.getResolvesId();
287
287
288 if (text === "" && !status) {
288 if (text === "" && !status) {
289 return;
289 return;
290 }
290 }
291
291
292 var excludeCancelBtn = false;
292 var excludeCancelBtn = false;
293 var submitEvent = true;
293 var submitEvent = true;
294 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
294 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
295 self.cm.setOption("readOnly", true);
295 self.cm.setOption("readOnly", true);
296
296
297 var postData = {
297 var postData = {
298 'text': text,
298 'text': text,
299 'changeset_status': status,
299 'changeset_status': status,
300 'comment_type': commentType,
300 'comment_type': commentType,
301 'csrf_token': CSRF_TOKEN
301 'csrf_token': CSRF_TOKEN
302 };
302 };
303 if (resolvesCommentId){
303 if (resolvesCommentId){
304 postData['resolves_comment_id'] = resolvesCommentId;
304 postData['resolves_comment_id'] = resolvesCommentId;
305 }
305 }
306
306
307 var submitSuccessCallback = function(o) {
307 var submitSuccessCallback = function(o) {
308 // reload page if we change status for single commit.
308 // reload page if we change status for single commit.
309 if (status && self.commitId) {
309 if (status && self.commitId) {
310 location.reload(true);
310 location.reload(true);
311 } else {
311 } else {
312 $('#injected_page_comments').append(o.rendered_text);
312 $('#injected_page_comments').append(o.rendered_text);
313 self.resetCommentFormState();
313 self.resetCommentFormState();
314 timeagoActivate();
314 timeagoActivate();
315
315
316 // mark visually which comment was resolved
316 // mark visually which comment was resolved
317 if (resolvesCommentId) {
317 if (resolvesCommentId) {
318 self.markCommentResolved(resolvesCommentId);
318 self.markCommentResolved(resolvesCommentId);
319 }
319 }
320 }
320 }
321
321
322 // run global callback on submit
322 // run global callback on submit
323 self.globalSubmitSuccessCallback();
323 self.globalSubmitSuccessCallback();
324
324
325 };
325 };
326 var submitFailCallback = function(){
326 var submitFailCallback = function(){
327 self.resetCommentFormState(text);
327 self.resetCommentFormState(text);
328 };
328 };
329 self.submitAjaxPOST(
329 self.submitAjaxPOST(
330 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
330 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
331 };
331 };
332
332
333 this.previewSuccessCallback = function(o) {
333 this.previewSuccessCallback = function(o) {
334 $(self.previewBoxSelector).html(o);
334 $(self.previewBoxSelector).html(o);
335 $(self.previewBoxSelector).removeClass('unloaded');
335 $(self.previewBoxSelector).removeClass('unloaded');
336
336
337 // swap buttons, making preview active
337 // swap buttons, making preview active
338 $(self.previewButton).parent().addClass('active');
338 $(self.previewButton).parent().addClass('active');
339 $(self.editButton).parent().removeClass('active');
339 $(self.editButton).parent().removeClass('active');
340
340
341 // unlock buttons
341 // unlock buttons
342 self.setActionButtonsDisabled(false);
342 self.setActionButtonsDisabled(false);
343 };
343 };
344
344
345 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
345 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
346 excludeCancelBtn = excludeCancelBtn || false;
346 excludeCancelBtn = excludeCancelBtn || false;
347 submitEvent = submitEvent || false;
347 submitEvent = submitEvent || false;
348
348
349 $(this.editButton).prop('disabled', state);
349 $(this.editButton).prop('disabled', state);
350 $(this.previewButton).prop('disabled', state);
350 $(this.previewButton).prop('disabled', state);
351
351
352 if (!excludeCancelBtn) {
352 if (!excludeCancelBtn) {
353 $(this.cancelButton).prop('disabled', state);
353 $(this.cancelButton).prop('disabled', state);
354 }
354 }
355
355
356 var submitState = state;
356 var submitState = state;
357 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
357 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
358 // if the value of commit review status is set, we allow
358 // if the value of commit review status is set, we allow
359 // submit button, but only on Main form, lineNo means inline
359 // submit button, but only on Main form, lineNo means inline
360 submitState = false
360 submitState = false
361 }
361 }
362 $(this.submitButton).prop('disabled', submitState);
362 $(this.submitButton).prop('disabled', submitState);
363 if (submitEvent) {
363 if (submitEvent) {
364 $(this.submitButton).val(_gettext('Submitting...'));
364 $(this.submitButton).val(_gettext('Submitting...'));
365 } else {
365 } else {
366 $(this.submitButton).val(this.submitButtonText);
366 $(this.submitButton).val(this.submitButtonText);
367 }
367 }
368
368
369 };
369 };
370
370
371 // lock preview/edit/submit buttons on load, but exclude cancel button
371 // lock preview/edit/submit buttons on load, but exclude cancel button
372 var excludeCancelBtn = true;
372 var excludeCancelBtn = true;
373 this.setActionButtonsDisabled(true, excludeCancelBtn);
373 this.setActionButtonsDisabled(true, excludeCancelBtn);
374
374
375 // anonymous users don't have access to initialized CM instance
375 // anonymous users don't have access to initialized CM instance
376 if (this.cm !== undefined){
376 if (this.cm !== undefined){
377 this.cm.on('change', function(cMirror) {
377 this.cm.on('change', function(cMirror) {
378 if (cMirror.getValue() === "") {
378 if (cMirror.getValue() === "") {
379 self.setActionButtonsDisabled(true, excludeCancelBtn)
379 self.setActionButtonsDisabled(true, excludeCancelBtn)
380 } else {
380 } else {
381 self.setActionButtonsDisabled(false, excludeCancelBtn)
381 self.setActionButtonsDisabled(false, excludeCancelBtn)
382 }
382 }
383 });
383 });
384 }
384 }
385
385
386 $(this.editButton).on('click', function(e) {
386 $(this.editButton).on('click', function(e) {
387 e.preventDefault();
387 e.preventDefault();
388
388
389 $(self.previewButton).parent().removeClass('active');
389 $(self.previewButton).parent().removeClass('active');
390 $(self.previewContainer).hide();
390 $(self.previewContainer).hide();
391
391
392 $(self.editButton).parent().addClass('active');
392 $(self.editButton).parent().addClass('active');
393 $(self.editContainer).show();
393 $(self.editContainer).show();
394
394
395 });
395 });
396
396
397 $(this.previewButton).on('click', function(e) {
397 $(this.previewButton).on('click', function(e) {
398 e.preventDefault();
398 e.preventDefault();
399 var text = self.cm.getValue();
399 var text = self.cm.getValue();
400
400
401 if (text === "") {
401 if (text === "") {
402 return;
402 return;
403 }
403 }
404
404
405 var postData = {
405 var postData = {
406 'text': text,
406 'text': text,
407 'renderer': templateContext.visual.default_renderer,
407 'renderer': templateContext.visual.default_renderer,
408 'csrf_token': CSRF_TOKEN
408 'csrf_token': CSRF_TOKEN
409 };
409 };
410
410
411 // lock ALL buttons on preview
411 // lock ALL buttons on preview
412 self.setActionButtonsDisabled(true);
412 self.setActionButtonsDisabled(true);
413
413
414 $(self.previewBoxSelector).addClass('unloaded');
414 $(self.previewBoxSelector).addClass('unloaded');
415 $(self.previewBoxSelector).html(_gettext('Loading ...'));
415 $(self.previewBoxSelector).html(_gettext('Loading ...'));
416
416
417 $(self.editContainer).hide();
417 $(self.editContainer).hide();
418 $(self.previewContainer).show();
418 $(self.previewContainer).show();
419
419
420 // by default we reset state of comment preserving the text
420 // by default we reset state of comment preserving the text
421 var previewFailCallback = function(){
421 var previewFailCallback = function(){
422 self.resetCommentFormState(text)
422 self.resetCommentFormState(text)
423 };
423 };
424 self.submitAjaxPOST(
424 self.submitAjaxPOST(
425 self.previewUrl, postData, self.previewSuccessCallback,
425 self.previewUrl, postData, self.previewSuccessCallback,
426 previewFailCallback);
426 previewFailCallback);
427
427
428 $(self.previewButton).parent().addClass('active');
428 $(self.previewButton).parent().addClass('active');
429 $(self.editButton).parent().removeClass('active');
429 $(self.editButton).parent().removeClass('active');
430 });
430 });
431
431
432 $(this.submitForm).submit(function(e) {
432 $(this.submitForm).submit(function(e) {
433 e.preventDefault();
433 e.preventDefault();
434 var allowedToSubmit = self.isAllowedToSubmit();
434 var allowedToSubmit = self.isAllowedToSubmit();
435 if (!allowedToSubmit){
435 if (!allowedToSubmit){
436 return false;
436 return false;
437 }
437 }
438 self.handleFormSubmit();
438 self.handleFormSubmit();
439 });
439 });
440
440
441 }
441 }
442
442
443 return CommentForm;
443 return CommentForm;
444 });
444 });
445
445
446 /* comments controller */
446 /* comments controller */
447 var CommentsController = function() {
447 var CommentsController = function() {
448 var mainComment = '#text';
448 var mainComment = '#text';
449 var self = this;
449 var self = this;
450
450
451 this.cancelComment = function(node) {
451 this.cancelComment = function(node) {
452 var $node = $(node);
452 var $node = $(node);
453 var $td = $node.closest('td');
453 var $td = $node.closest('td');
454 $node.closest('.comment-inline-form').remove();
454 $node.closest('.comment-inline-form').remove();
455 return false;
455 return false;
456 };
456 };
457
457
458 this.getLineNumber = function(node) {
458 this.getLineNumber = function(node) {
459 var $node = $(node);
459 var $node = $(node);
460 return $node.closest('td').attr('data-line-number');
460 return $node.closest('td').attr('data-line-number');
461 };
461 };
462
462
463 this.scrollToComment = function(node, offset, outdated) {
463 this.scrollToComment = function(node, offset, outdated) {
464 var offset = offset || 1;
464 var outdated = outdated || false;
465 var outdated = outdated || false;
465 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
466 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
466
467
467 if (!node) {
468 if (!node) {
468 node = $('.comment-selected');
469 node = $('.comment-selected');
469 if (!node.length) {
470 if (!node.length) {
470 node = $('comment-current')
471 node = $('comment-current')
471 }
472 }
472 }
473 }
473 $wrapper = $(node).closest('div.comment');
474 $wrapper = $(node).closest('div.comment');
474 $comment = $(node).closest(klass);
475 $comment = $(node).closest(klass);
475 $comments = $(klass);
476 $comments = $(klass);
476
477
477 // show hidden comment when referenced.
478 // show hidden comment when referenced.
478 if (!$wrapper.is(':visible')){
479 if (!$wrapper.is(':visible')){
479 $wrapper.show();
480 $wrapper.show();
480 }
481 }
481
482
482 $('.comment-selected').removeClass('comment-selected');
483 $('.comment-selected').removeClass('comment-selected');
483
484
484 var nextIdx = $(klass).index($comment) + offset;
485 var nextIdx = $(klass).index($comment) + offset;
485 if (nextIdx >= $comments.length) {
486 if (nextIdx >= $comments.length) {
486 nextIdx = 0;
487 nextIdx = 0;
487 }
488 }
488 var $next = $(klass).eq(nextIdx);
489 var $next = $(klass).eq(nextIdx);
490
489 var $cb = $next.closest('.cb');
491 var $cb = $next.closest('.cb');
490 $cb.removeClass('cb-collapsed');
492 $cb.removeClass('cb-collapsed');
491
493
492 var $filediffCollapseState = $cb.closest('.filediff').prev();
494 var $filediffCollapseState = $cb.closest('.filediff').prev();
493 $filediffCollapseState.prop('checked', false);
495 $filediffCollapseState.prop('checked', false);
494 $next.addClass('comment-selected');
496 $next.addClass('comment-selected');
495 scrollToElement($next);
497 scrollToElement($next);
496 return false;
498 return false;
497 };
499 };
498
500
499 this.nextComment = function(node) {
501 this.nextComment = function(node) {
500 return self.scrollToComment(node, 1);
502 return self.scrollToComment(node, 1);
501 };
503 };
502
504
503 this.prevComment = function(node) {
505 this.prevComment = function(node) {
504 return self.scrollToComment(node, -1);
506 return self.scrollToComment(node, -1);
505 };
507 };
506
508
507 this.nextOutdatedComment = function(node) {
509 this.nextOutdatedComment = function(node) {
508 return self.scrollToComment(node, 1, true);
510 return self.scrollToComment(node, 1, true);
509 };
511 };
510
512
511 this.prevOutdatedComment = function(node) {
513 this.prevOutdatedComment = function(node) {
512 return self.scrollToComment(node, -1, true);
514 return self.scrollToComment(node, -1, true);
513 };
515 };
514
516
515 this.deleteComment = function(node) {
517 this.deleteComment = function(node) {
516 if (!confirm(_gettext('Delete this comment?'))) {
518 if (!confirm(_gettext('Delete this comment?'))) {
517 return false;
519 return false;
518 }
520 }
519 var $node = $(node);
521 var $node = $(node);
520 var $td = $node.closest('td');
522 var $td = $node.closest('td');
521 var $comment = $node.closest('.comment');
523 var $comment = $node.closest('.comment');
522 var comment_id = $comment.attr('data-comment-id');
524 var comment_id = $comment.attr('data-comment-id');
523 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
525 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
524 var postData = {
526 var postData = {
525 '_method': 'delete',
527 '_method': 'delete',
526 'csrf_token': CSRF_TOKEN
528 'csrf_token': CSRF_TOKEN
527 };
529 };
528
530
529 $comment.addClass('comment-deleting');
531 $comment.addClass('comment-deleting');
530 $comment.hide('fast');
532 $comment.hide('fast');
531
533
532 var success = function(response) {
534 var success = function(response) {
533 $comment.remove();
535 $comment.remove();
534 return false;
536 return false;
535 };
537 };
536 var failure = function(data, textStatus, xhr) {
538 var failure = function(data, textStatus, xhr) {
537 alert("error processing request: " + textStatus);
539 alert("error processing request: " + textStatus);
538 $comment.show('fast');
540 $comment.show('fast');
539 $comment.removeClass('comment-deleting');
541 $comment.removeClass('comment-deleting');
540 return false;
542 return false;
541 };
543 };
542 ajaxPOST(url, postData, success, failure);
544 ajaxPOST(url, postData, success, failure);
543 };
545 };
544
546
545 this.toggleWideMode = function (node) {
547 this.toggleWideMode = function (node) {
546 if ($('#content').hasClass('wrapper')) {
548 if ($('#content').hasClass('wrapper')) {
547 $('#content').removeClass("wrapper");
549 $('#content').removeClass("wrapper");
548 $('#content').addClass("wide-mode-wrapper");
550 $('#content').addClass("wide-mode-wrapper");
549 $(node).addClass('btn-success');
551 $(node).addClass('btn-success');
550 } else {
552 } else {
551 $('#content').removeClass("wide-mode-wrapper");
553 $('#content').removeClass("wide-mode-wrapper");
552 $('#content').addClass("wrapper");
554 $('#content').addClass("wrapper");
553 $(node).removeClass('btn-success');
555 $(node).removeClass('btn-success');
554 }
556 }
555 return false;
557 return false;
556 };
558 };
557
559
558 this.toggleComments = function(node, show) {
560 this.toggleComments = function(node, show) {
559 var $filediff = $(node).closest('.filediff');
561 var $filediff = $(node).closest('.filediff');
560 if (show === true) {
562 if (show === true) {
561 $filediff.removeClass('hide-comments');
563 $filediff.removeClass('hide-comments');
562 } else if (show === false) {
564 } else if (show === false) {
563 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
565 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
564 $filediff.addClass('hide-comments');
566 $filediff.addClass('hide-comments');
565 } else {
567 } else {
566 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
568 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
567 $filediff.toggleClass('hide-comments');
569 $filediff.toggleClass('hide-comments');
568 }
570 }
569 return false;
571 return false;
570 };
572 };
571
573
572 this.toggleLineComments = function(node) {
574 this.toggleLineComments = function(node) {
573 self.toggleComments(node, true);
575 self.toggleComments(node, true);
574 var $node = $(node);
576 var $node = $(node);
575 $node.closest('tr').toggleClass('hide-line-comments');
577 $node.closest('tr').toggleClass('hide-line-comments');
576 };
578 };
577
579
578 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
580 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
579 var pullRequestId = templateContext.pull_request_data.pull_request_id;
581 var pullRequestId = templateContext.pull_request_data.pull_request_id;
580 var commitId = templateContext.commit_data.commit_id;
582 var commitId = templateContext.commit_data.commit_id;
581
583
582 var commentForm = new CommentForm(
584 var commentForm = new CommentForm(
583 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
585 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
584 var cm = commentForm.getCmInstance();
586 var cm = commentForm.getCmInstance();
585
587
586 if (resolvesCommentId){
588 if (resolvesCommentId){
587 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
589 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
588 }
590 }
589
591
590 setTimeout(function() {
592 setTimeout(function() {
591 // callbacks
593 // callbacks
592 if (cm !== undefined) {
594 if (cm !== undefined) {
593 commentForm.setPlaceholder(placeholderText);
595 commentForm.setPlaceholder(placeholderText);
594 if (commentForm.isInline()) {
596 if (commentForm.isInline()) {
595 cm.focus();
597 cm.focus();
596 cm.refresh();
598 cm.refresh();
597 }
599 }
598 }
600 }
599 }, 10);
601 }, 10);
600
602
601 // trigger scrolldown to the resolve comment, since it might be away
603 // trigger scrolldown to the resolve comment, since it might be away
602 // from the clicked
604 // from the clicked
603 if (resolvesCommentId){
605 if (resolvesCommentId){
604 var actionNode = $(commentForm.resolvesActionId).offset();
606 var actionNode = $(commentForm.resolvesActionId).offset();
605
607
606 setTimeout(function() {
608 setTimeout(function() {
607 if (actionNode) {
609 if (actionNode) {
608 $('body, html').animate({scrollTop: actionNode.top}, 10);
610 $('body, html').animate({scrollTop: actionNode.top}, 10);
609 }
611 }
610 }, 100);
612 }, 100);
611 }
613 }
612
614
613 return commentForm;
615 return commentForm;
614 };
616 };
615
617
616 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
618 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
617
619
618 var tmpl = $('#cb-comment-general-form-template').html();
620 var tmpl = $('#cb-comment-general-form-template').html();
619 tmpl = tmpl.format(null, 'general');
621 tmpl = tmpl.format(null, 'general');
620 var $form = $(tmpl);
622 var $form = $(tmpl);
621
623
622 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
624 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
623 var curForm = $formPlaceholder.find('form');
625 var curForm = $formPlaceholder.find('form');
624 if (curForm){
626 if (curForm){
625 curForm.remove();
627 curForm.remove();
626 }
628 }
627 $formPlaceholder.append($form);
629 $formPlaceholder.append($form);
628
630
629 var _form = $($form[0]);
631 var _form = $($form[0]);
630 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
632 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
631 var commentForm = this.createCommentForm(
633 var commentForm = this.createCommentForm(
632 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
634 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
633 commentForm.initStatusChangeSelector();
635 commentForm.initStatusChangeSelector();
634
636
635 return commentForm;
637 return commentForm;
636 };
638 };
637
639
638 this.createComment = function(node, resolutionComment) {
640 this.createComment = function(node, resolutionComment) {
639 var resolvesCommentId = resolutionComment || null;
641 var resolvesCommentId = resolutionComment || null;
640 var $node = $(node);
642 var $node = $(node);
641 var $td = $node.closest('td');
643 var $td = $node.closest('td');
642 var $form = $td.find('.comment-inline-form');
644 var $form = $td.find('.comment-inline-form');
643
645
644 if (!$form.length) {
646 if (!$form.length) {
645
647
646 var $filediff = $node.closest('.filediff');
648 var $filediff = $node.closest('.filediff');
647 $filediff.removeClass('hide-comments');
649 $filediff.removeClass('hide-comments');
648 var f_path = $filediff.attr('data-f-path');
650 var f_path = $filediff.attr('data-f-path');
649 var lineno = self.getLineNumber(node);
651 var lineno = self.getLineNumber(node);
650 // create a new HTML from template
652 // create a new HTML from template
651 var tmpl = $('#cb-comment-inline-form-template').html();
653 var tmpl = $('#cb-comment-inline-form-template').html();
652 tmpl = tmpl.format(f_path, lineno);
654 tmpl = tmpl.format(f_path, lineno);
653 $form = $(tmpl);
655 $form = $(tmpl);
654
656
655 var $comments = $td.find('.inline-comments');
657 var $comments = $td.find('.inline-comments');
656 if (!$comments.length) {
658 if (!$comments.length) {
657 $comments = $(
659 $comments = $(
658 $('#cb-comments-inline-container-template').html());
660 $('#cb-comments-inline-container-template').html());
659 $td.append($comments);
661 $td.append($comments);
660 }
662 }
661
663
662 $td.find('.cb-comment-add-button').before($form);
664 $td.find('.cb-comment-add-button').before($form);
663
665
664 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
666 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
665 var _form = $($form[0]).find('form');
667 var _form = $($form[0]).find('form');
666 var autocompleteActions = ['as_note', 'as_todo'];
668 var autocompleteActions = ['as_note', 'as_todo'];
667 var commentForm = this.createCommentForm(
669 var commentForm = this.createCommentForm(
668 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
670 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
669
671
670 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
672 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
671 form: _form,
673 form: _form,
672 parent: $td[0],
674 parent: $td[0],
673 lineno: lineno,
675 lineno: lineno,
674 f_path: f_path}
676 f_path: f_path}
675 );
677 );
676
678
677 // set a CUSTOM submit handler for inline comments.
679 // set a CUSTOM submit handler for inline comments.
678 commentForm.setHandleFormSubmit(function(o) {
680 commentForm.setHandleFormSubmit(function(o) {
679 var text = commentForm.cm.getValue();
681 var text = commentForm.cm.getValue();
680 var commentType = commentForm.getCommentType();
682 var commentType = commentForm.getCommentType();
681 var resolvesCommentId = commentForm.getResolvesId();
683 var resolvesCommentId = commentForm.getResolvesId();
682
684
683 if (text === "") {
685 if (text === "") {
684 return;
686 return;
685 }
687 }
686
688
687 if (lineno === undefined) {
689 if (lineno === undefined) {
688 alert('missing line !');
690 alert('missing line !');
689 return;
691 return;
690 }
692 }
691 if (f_path === undefined) {
693 if (f_path === undefined) {
692 alert('missing file path !');
694 alert('missing file path !');
693 return;
695 return;
694 }
696 }
695
697
696 var excludeCancelBtn = false;
698 var excludeCancelBtn = false;
697 var submitEvent = true;
699 var submitEvent = true;
698 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
700 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
699 commentForm.cm.setOption("readOnly", true);
701 commentForm.cm.setOption("readOnly", true);
700 var postData = {
702 var postData = {
701 'text': text,
703 'text': text,
702 'f_path': f_path,
704 'f_path': f_path,
703 'line': lineno,
705 'line': lineno,
704 'comment_type': commentType,
706 'comment_type': commentType,
705 'csrf_token': CSRF_TOKEN
707 'csrf_token': CSRF_TOKEN
706 };
708 };
707 if (resolvesCommentId){
709 if (resolvesCommentId){
708 postData['resolves_comment_id'] = resolvesCommentId;
710 postData['resolves_comment_id'] = resolvesCommentId;
709 }
711 }
710
712
711 var submitSuccessCallback = function(json_data) {
713 var submitSuccessCallback = function(json_data) {
712 $form.remove();
714 $form.remove();
713 try {
715 try {
714 var html = json_data.rendered_text;
716 var html = json_data.rendered_text;
715 var lineno = json_data.line_no;
717 var lineno = json_data.line_no;
716 var target_id = json_data.target_id;
718 var target_id = json_data.target_id;
717
719
718 $comments.find('.cb-comment-add-button').before(html);
720 $comments.find('.cb-comment-add-button').before(html);
719
721
720 //mark visually which comment was resolved
722 //mark visually which comment was resolved
721 if (resolvesCommentId) {
723 if (resolvesCommentId) {
722 commentForm.markCommentResolved(resolvesCommentId);
724 commentForm.markCommentResolved(resolvesCommentId);
723 }
725 }
724
726
725 // run global callback on submit
727 // run global callback on submit
726 commentForm.globalSubmitSuccessCallback();
728 commentForm.globalSubmitSuccessCallback();
727
729
728 } catch (e) {
730 } catch (e) {
729 console.error(e);
731 console.error(e);
730 }
732 }
731
733
732 // re trigger the linkification of next/prev navigation
734 // re trigger the linkification of next/prev navigation
733 linkifyComments($('.inline-comment-injected'));
735 linkifyComments($('.inline-comment-injected'));
734 timeagoActivate();
736 timeagoActivate();
735 commentForm.setActionButtonsDisabled(false);
737 commentForm.setActionButtonsDisabled(false);
736
738
737 };
739 };
738 var submitFailCallback = function(){
740 var submitFailCallback = function(){
739 commentForm.resetCommentFormState(text)
741 commentForm.resetCommentFormState(text)
740 };
742 };
741 commentForm.submitAjaxPOST(
743 commentForm.submitAjaxPOST(
742 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
744 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
743 });
745 });
744 }
746 }
745
747
746 $form.addClass('comment-inline-form-open');
748 $form.addClass('comment-inline-form-open');
747 };
749 };
748
750
749 this.createResolutionComment = function(commentId){
751 this.createResolutionComment = function(commentId){
750 // hide the trigger text
752 // hide the trigger text
751 $('#resolve-comment-{0}'.format(commentId)).hide();
753 $('#resolve-comment-{0}'.format(commentId)).hide();
752
754
753 var comment = $('#comment-'+commentId);
755 var comment = $('#comment-'+commentId);
754 var commentData = comment.data();
756 var commentData = comment.data();
755 if (commentData.commentInline) {
757 if (commentData.commentInline) {
756 this.createComment(comment, commentId)
758 this.createComment(comment, commentId)
757 } else {
759 } else {
758 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
760 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
759 }
761 }
760
762
761 return false;
763 return false;
762 };
764 };
763
765
764 this.submitResolution = function(commentId){
766 this.submitResolution = function(commentId){
765 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
767 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
766 var commentForm = form.get(0).CommentForm;
768 var commentForm = form.get(0).CommentForm;
767
769
768 var cm = commentForm.getCmInstance();
770 var cm = commentForm.getCmInstance();
769 var renderer = templateContext.visual.default_renderer;
771 var renderer = templateContext.visual.default_renderer;
770 if (renderer == 'rst'){
772 if (renderer == 'rst'){
771 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
773 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
772 } else if (renderer == 'markdown') {
774 } else if (renderer == 'markdown') {
773 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
775 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
774 } else {
776 } else {
775 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
777 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
776 }
778 }
777
779
778 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
780 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
779 form.submit();
781 form.submit();
780 return false;
782 return false;
781 };
783 };
782
784
783 this.renderInlineComments = function(file_comments) {
785 this.renderInlineComments = function(file_comments) {
784 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
786 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
785
787
786 for (var i = 0; i < file_comments.length; i++) {
788 for (var i = 0; i < file_comments.length; i++) {
787 var box = file_comments[i];
789 var box = file_comments[i];
788
790
789 var target_id = $(box).attr('target_id');
791 var target_id = $(box).attr('target_id');
790
792
791 // actually comments with line numbers
793 // actually comments with line numbers
792 var comments = box.children;
794 var comments = box.children;
793
795
794 for (var j = 0; j < comments.length; j++) {
796 for (var j = 0; j < comments.length; j++) {
795 var data = {
797 var data = {
796 'rendered_text': comments[j].outerHTML,
798 'rendered_text': comments[j].outerHTML,
797 'line_no': $(comments[j]).attr('line'),
799 'line_no': $(comments[j]).attr('line'),
798 'target_id': target_id
800 'target_id': target_id
799 };
801 };
800 }
802 }
801 }
803 }
802
804
803 // since order of injection is random, we're now re-iterating
805 // since order of injection is random, we're now re-iterating
804 // from correct order and filling in links
806 // from correct order and filling in links
805 linkifyComments($('.inline-comment-injected'));
807 linkifyComments($('.inline-comment-injected'));
806 firefoxAnchorFix();
808 firefoxAnchorFix();
807 };
809 };
808
810
809 };
811 };
@@ -1,314 +1,331 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%inherit file="/base/base.mako"/>
3 <%inherit file="/base/base.mako"/>
4 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
4 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
5
5
6 <%def name="title()">
6 <%def name="title()">
7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
8 %if c.rhodecode_name:
8 %if c.rhodecode_name:
9 &middot; ${h.branding(c.rhodecode_name)}
9 &middot; ${h.branding(c.rhodecode_name)}
10 %endif
10 %endif
11 </%def>
11 </%def>
12
12
13 <%def name="menu_bar_nav()">
13 <%def name="menu_bar_nav()">
14 ${self.menu_items(active='repositories')}
14 ${self.menu_items(active='repositories')}
15 </%def>
15 </%def>
16
16
17 <%def name="menu_bar_subnav()">
17 <%def name="menu_bar_subnav()">
18 ${self.repo_menu(active='changelog')}
18 ${self.repo_menu(active='changelog')}
19 </%def>
19 </%def>
20
20
21 <%def name="main()">
21 <%def name="main()">
22 <script>
22 <script>
23 // TODO: marcink switch this to pyroutes
23 // TODO: marcink switch this to pyroutes
24 AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
24 AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
26 </script>
26 </script>
27 <div class="box">
27 <div class="box">
28 <div class="title">
28 <div class="title">
29 ${self.repo_page_title(c.rhodecode_db_repo)}
29 ${self.repo_page_title(c.rhodecode_db_repo)}
30 </div>
30 </div>
31
31
32 <div id="changeset_compare_view_content" class="summary changeset">
32 <div id="changeset_compare_view_content" class="summary changeset">
33 <div class="summary-detail">
33 <div class="summary-detail">
34 <div class="summary-detail-header">
34 <div class="summary-detail-header">
35 <span class="breadcrumbs files_location">
35 <span class="breadcrumbs files_location">
36 <h4>${_('Commit')}
36 <h4>${_('Commit')}
37 <code>
37 <code>
38 ${h.show_id(c.commit)}
38 ${h.show_id(c.commit)}
39 </code>
39 </code>
40 </h4>
40 </h4>
41 </span>
41 </span>
42 <span id="parent_link">
42 <span id="parent_link">
43 <a href="#" title="${_('Parent Commit')}">${_('Parent')}</a>
43 <a href="#" title="${_('Parent Commit')}">${_('Parent')}</a>
44 </span>
44 </span>
45 |
45 |
46 <span id="child_link">
46 <span id="child_link">
47 <a href="#" title="${_('Child Commit')}">${_('Child')}</a>
47 <a href="#" title="${_('Child Commit')}">${_('Child')}</a>
48 </span>
48 </span>
49 </div>
49 </div>
50
50
51 <div class="fieldset">
51 <div class="fieldset">
52 <div class="left-label">
52 <div class="left-label">
53 ${_('Description')}:
53 ${_('Description')}:
54 </div>
54 </div>
55 <div class="right-content">
55 <div class="right-content">
56 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
56 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
57 <div id="message_expand" style="display:none;">
57 <div id="message_expand" style="display:none;">
58 ${_('Expand')}
58 ${_('Expand')}
59 </div>
59 </div>
60 </div>
60 </div>
61 </div>
61 </div>
62
62
63 %if c.statuses:
63 %if c.statuses:
64 <div class="fieldset">
64 <div class="fieldset">
65 <div class="left-label">
65 <div class="left-label">
66 ${_('Commit status')}:
66 ${_('Commit status')}:
67 </div>
67 </div>
68 <div class="right-content">
68 <div class="right-content">
69 <div class="changeset-status-ico">
69 <div class="changeset-status-ico">
70 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
70 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
71 </div>
71 </div>
72 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
72 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
73 </div>
73 </div>
74 </div>
74 </div>
75 %endif
75 %endif
76
76
77 <div class="fieldset">
77 <div class="fieldset">
78 <div class="left-label">
78 <div class="left-label">
79 ${_('References')}:
79 ${_('References')}:
80 </div>
80 </div>
81 <div class="right-content">
81 <div class="right-content">
82 <div class="tags">
82 <div class="tags">
83
83
84 %if c.commit.merge:
84 %if c.commit.merge:
85 <span class="mergetag tag">
85 <span class="mergetag tag">
86 <i class="icon-merge"></i>${_('merge')}
86 <i class="icon-merge"></i>${_('merge')}
87 </span>
87 </span>
88 %endif
88 %endif
89
89
90 %if h.is_hg(c.rhodecode_repo):
90 %if h.is_hg(c.rhodecode_repo):
91 %for book in c.commit.bookmarks:
91 %for book in c.commit.bookmarks:
92 <span class="booktag tag" title="${_('Bookmark %s') % book}">
92 <span class="booktag tag" title="${_('Bookmark %s') % book}">
93 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
93 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
94 </span>
94 </span>
95 %endfor
95 %endfor
96 %endif
96 %endif
97
97
98 %for tag in c.commit.tags:
98 %for tag in c.commit.tags:
99 <span class="tagtag tag" title="${_('Tag %s') % tag}">
99 <span class="tagtag tag" title="${_('Tag %s') % tag}">
100 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-tag"></i>${tag}</a>
100 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-tag"></i>${tag}</a>
101 </span>
101 </span>
102 %endfor
102 %endfor
103
103
104 %if c.commit.branch:
104 %if c.commit.branch:
105 <span class="branchtag tag" title="${_('Branch %s') % c.commit.branch}">
105 <span class="branchtag tag" title="${_('Branch %s') % c.commit.branch}">
106 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
106 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
107 </span>
107 </span>
108 %endif
108 %endif
109 </div>
109 </div>
110 </div>
110 </div>
111 </div>
111 </div>
112
112
113 <div class="fieldset">
113 <div class="fieldset">
114 <div class="left-label">
114 <div class="left-label">
115 ${_('Diff options')}:
115 ${_('Diff options')}:
116 </div>
116 </div>
117 <div class="right-content">
117 <div class="right-content">
118 <div class="diff-actions">
118 <div class="diff-actions">
119 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
119 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
120 ${_('Raw Diff')}
120 ${_('Raw Diff')}
121 </a>
121 </a>
122 |
122 |
123 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
123 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
124 ${_('Patch Diff')}
124 ${_('Patch Diff')}
125 </a>
125 </a>
126 |
126 |
127 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.commit.raw_id,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
127 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.commit.raw_id,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
128 ${_('Download Diff')}
128 ${_('Download Diff')}
129 </a>
129 </a>
130 |
130 |
131 ${c.ignorews_url(request.GET)}
131 ${c.ignorews_url(request.GET)}
132 |
132 |
133 ${c.context_url(request.GET)}
133 ${c.context_url(request.GET)}
134 </div>
134 </div>
135 </div>
135 </div>
136 </div>
136 </div>
137
137
138 <div class="fieldset">
138 <div class="fieldset">
139 <div class="left-label">
139 <div class="left-label">
140 ${_('Comments')}:
140 ${_('Comments')}:
141 </div>
141 </div>
142 <div class="right-content">
142 <div class="right-content">
143 <div class="comments-number">
143 <div class="comments-number">
144 %if c.comments:
144 %if c.comments:
145 <a href="#comments">${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
145 <a href="#comments">${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
146 %else:
146 %else:
147 ${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
147 ${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
148 %endif
148 %endif
149 %if c.inline_cnt:
149 %if c.inline_cnt:
150 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
150 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
151 %else:
151 %else:
152 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
152 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
153 %endif
153 %endif
154 </div>
154 </div>
155 </div>
155 </div>
156 </div>
156 </div>
157
157
158 <div class="fieldset">
159 <div class="left-label">
160 ${_('Unresolved TODOs')}:
161 </div>
162 <div class="right-content">
163 <div class="comments-number">
164 % if c.unresolved_comments:
165 % for co in c.unresolved_comments:
166 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))"> #${co.comment_id}</a>${'' if loop.last else ','}
167 % endfor
168 % else:
169 ${_('There are no unresolved TODOs')}
170 % endif
171 </div>
172 </div>
173 </div>
174
158 </div> <!-- end summary-detail -->
175 </div> <!-- end summary-detail -->
159
176
160 <div id="commit-stats" class="sidebar-right">
177 <div id="commit-stats" class="sidebar-right">
161 <div class="summary-detail-header">
178 <div class="summary-detail-header">
162 <h4 class="item">
179 <h4 class="item">
163 ${_('Author')}
180 ${_('Author')}
164 </h4>
181 </h4>
165 </div>
182 </div>
166 <div class="sidebar-right-content">
183 <div class="sidebar-right-content">
167 ${self.gravatar_with_user(c.commit.author)}
184 ${self.gravatar_with_user(c.commit.author)}
168 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
185 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
169 </div>
186 </div>
170 </div><!-- end sidebar -->
187 </div><!-- end sidebar -->
171 </div> <!-- end summary -->
188 </div> <!-- end summary -->
172 <div class="cs_files">
189 <div class="cs_files">
173 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
190 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
174 ${cbdiffs.render_diffset_menu()}
191 ${cbdiffs.render_diffset_menu()}
175 ${cbdiffs.render_diffset(
192 ${cbdiffs.render_diffset(
176 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True)}
193 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True)}
177 </div>
194 </div>
178
195
179 ## template for inline comment form
196 ## template for inline comment form
180 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
197 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
181
198
182 ## render comments
199 ## render comments
183 ${comment.generate_comments(c.comments)}
200 ${comment.generate_comments(c.comments)}
184
201
185 ## main comment form and it status
202 ## main comment form and it status
186 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.commit.raw_id),
203 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.commit.raw_id),
187 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
204 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
188 </div>
205 </div>
189
206
190 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
207 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
191 <script type="text/javascript">
208 <script type="text/javascript">
192
209
193 $(document).ready(function() {
210 $(document).ready(function() {
194
211
195 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
212 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
196 if($('#trimmed_message_box').height() === boxmax){
213 if($('#trimmed_message_box').height() === boxmax){
197 $('#message_expand').show();
214 $('#message_expand').show();
198 }
215 }
199
216
200 $('#message_expand').on('click', function(e){
217 $('#message_expand').on('click', function(e){
201 $('#trimmed_message_box').css('max-height', 'none');
218 $('#trimmed_message_box').css('max-height', 'none');
202 $(this).hide();
219 $(this).hide();
203 });
220 });
204
221
205 $('.show-inline-comments').on('click', function(e){
222 $('.show-inline-comments').on('click', function(e){
206 var boxid = $(this).attr('data-comment-id');
223 var boxid = $(this).attr('data-comment-id');
207 var button = $(this);
224 var button = $(this);
208
225
209 if(button.hasClass("comments-visible")) {
226 if(button.hasClass("comments-visible")) {
210 $('#{0} .inline-comments'.format(boxid)).each(function(index){
227 $('#{0} .inline-comments'.format(boxid)).each(function(index){
211 $(this).hide();
228 $(this).hide();
212 });
229 });
213 button.removeClass("comments-visible");
230 button.removeClass("comments-visible");
214 } else {
231 } else {
215 $('#{0} .inline-comments'.format(boxid)).each(function(index){
232 $('#{0} .inline-comments'.format(boxid)).each(function(index){
216 $(this).show();
233 $(this).show();
217 });
234 });
218 button.addClass("comments-visible");
235 button.addClass("comments-visible");
219 }
236 }
220 });
237 });
221
238
222
239
223 // next links
240 // next links
224 $('#child_link').on('click', function(e){
241 $('#child_link').on('click', function(e){
225 // fetch via ajax what is going to be the next link, if we have
242 // fetch via ajax what is going to be the next link, if we have
226 // >1 links show them to user to choose
243 // >1 links show them to user to choose
227 if(!$('#child_link').hasClass('disabled')){
244 if(!$('#child_link').hasClass('disabled')){
228 $.ajax({
245 $.ajax({
229 url: '${h.url('changeset_children',repo_name=c.repo_name, revision=c.commit.raw_id)}',
246 url: '${h.url('changeset_children',repo_name=c.repo_name, revision=c.commit.raw_id)}',
230 success: function(data) {
247 success: function(data) {
231 if(data.results.length === 0){
248 if(data.results.length === 0){
232 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
249 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
233 }
250 }
234 if(data.results.length === 1){
251 if(data.results.length === 1){
235 var commit = data.results[0];
252 var commit = data.results[0];
236 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
253 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
237 }
254 }
238 else if(data.results.length === 2){
255 else if(data.results.length === 2){
239 $('#child_link').addClass('disabled');
256 $('#child_link').addClass('disabled');
240 $('#child_link').addClass('double');
257 $('#child_link').addClass('double');
241 var _html = '';
258 var _html = '';
242 _html +='<a title="__title__" href="__url__">__rev__</a> '
259 _html +='<a title="__title__" href="__url__">__rev__</a> '
243 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
260 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
244 .replace('__title__', data.results[0].message)
261 .replace('__title__', data.results[0].message)
245 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
262 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
246 _html +=' | ';
263 _html +=' | ';
247 _html +='<a title="__title__" href="__url__">__rev__</a> '
264 _html +='<a title="__title__" href="__url__">__rev__</a> '
248 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
265 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
249 .replace('__title__', data.results[1].message)
266 .replace('__title__', data.results[1].message)
250 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
267 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
251 $('#child_link').html(_html);
268 $('#child_link').html(_html);
252 }
269 }
253 }
270 }
254 });
271 });
255 e.preventDefault();
272 e.preventDefault();
256 }
273 }
257 });
274 });
258
275
259 // prev links
276 // prev links
260 $('#parent_link').on('click', function(e){
277 $('#parent_link').on('click', function(e){
261 // fetch via ajax what is going to be the next link, if we have
278 // fetch via ajax what is going to be the next link, if we have
262 // >1 links show them to user to choose
279 // >1 links show them to user to choose
263 if(!$('#parent_link').hasClass('disabled')){
280 if(!$('#parent_link').hasClass('disabled')){
264 $.ajax({
281 $.ajax({
265 url: '${h.url("changeset_parents",repo_name=c.repo_name, revision=c.commit.raw_id)}',
282 url: '${h.url("changeset_parents",repo_name=c.repo_name, revision=c.commit.raw_id)}',
266 success: function(data) {
283 success: function(data) {
267 if(data.results.length === 0){
284 if(data.results.length === 0){
268 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
285 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
269 }
286 }
270 if(data.results.length === 1){
287 if(data.results.length === 1){
271 var commit = data.results[0];
288 var commit = data.results[0];
272 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
289 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
273 }
290 }
274 else if(data.results.length === 2){
291 else if(data.results.length === 2){
275 $('#parent_link').addClass('disabled');
292 $('#parent_link').addClass('disabled');
276 $('#parent_link').addClass('double');
293 $('#parent_link').addClass('double');
277 var _html = '';
294 var _html = '';
278 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
295 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
279 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
296 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
280 .replace('__title__', data.results[0].message)
297 .replace('__title__', data.results[0].message)
281 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
298 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
282 _html +=' | ';
299 _html +=' | ';
283 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
300 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
284 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
301 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
285 .replace('__title__', data.results[1].message)
302 .replace('__title__', data.results[1].message)
286 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
303 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
287 $('#parent_link').html(_html);
304 $('#parent_link').html(_html);
288 }
305 }
289 }
306 }
290 });
307 });
291 e.preventDefault();
308 e.preventDefault();
292 }
309 }
293 });
310 });
294
311
295 if (location.hash) {
312 if (location.hash) {
296 var result = splitDelimitedHash(location.hash);
313 var result = splitDelimitedHash(location.hash);
297 var line = $('html').find(result.loc);
314 var line = $('html').find(result.loc);
298 if (line.length > 0){
315 if (line.length > 0){
299 offsetScroll(line, 70);
316 offsetScroll(line, 70);
300 }
317 }
301 }
318 }
302
319
303 // browse tree @ revision
320 // browse tree @ revision
304 $('#files_link').on('click', function(e){
321 $('#files_link').on('click', function(e){
305 window.location = '${h.url('files_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path='')}';
322 window.location = '${h.url('files_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path='')}';
306 e.preventDefault();
323 e.preventDefault();
307 });
324 });
308
325
309 // inject comments into their proper positions
326 // inject comments into their proper positions
310 var file_comments = $('.inline-comment-placeholder');
327 var file_comments = $('.inline-comment-placeholder');
311 })
328 })
312 </script>
329 </script>
313
330
314 </%def>
331 </%def>
General Comments 0
You need to be logged in to leave comments. Login now