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