##// END OF EJS Templates
comments: add ability to resolve todos from the side-bar.
milka -
r4633:bc11fd7f stable
parent child Browse files
Show More
@@ -1,852 +1,857 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24 import datetime
24 import datetime
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28 import collections
28 import collections
29
29
30 from pyramid.threadlocal import get_current_registry, get_current_request
30 from pyramid.threadlocal import get_current_registry, get_current_request
31 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.expression import null
32 from sqlalchemy.sql.functions import coalesce
32 from sqlalchemy.sql.functions import coalesce
33
33
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 from rhodecode.lib import audit_logger
35 from rhodecode.lib import audit_logger
36 from rhodecode.lib.exceptions import CommentVersionMismatch
36 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 from rhodecode.model import BaseModel
38 from rhodecode.model import BaseModel
39 from rhodecode.model.db import (
39 from rhodecode.model.db import (
40 false, true,
40 false, true,
41 ChangesetComment,
41 ChangesetComment,
42 User,
42 User,
43 Notification,
43 Notification,
44 PullRequest,
44 PullRequest,
45 AttributeDict,
45 AttributeDict,
46 ChangesetCommentHistory,
46 ChangesetCommentHistory,
47 )
47 )
48 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.notification import NotificationModel
49 from rhodecode.model.meta import Session
49 from rhodecode.model.meta import Session
50 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.settings import VcsSettingsModel
51 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.notification import EmailNotificationModel
52 from rhodecode.model.validation_schema.schemas import comment_schema
52 from rhodecode.model.validation_schema.schemas import comment_schema
53
53
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class CommentsModel(BaseModel):
58 class CommentsModel(BaseModel):
59
59
60 cls = ChangesetComment
60 cls = ChangesetComment
61
61
62 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_BEFORE = 3
63 DIFF_CONTEXT_AFTER = 3
63 DIFF_CONTEXT_AFTER = 3
64
64
65 def __get_commit_comment(self, changeset_comment):
65 def __get_commit_comment(self, changeset_comment):
66 return self._get_instance(ChangesetComment, changeset_comment)
66 return self._get_instance(ChangesetComment, changeset_comment)
67
67
68 def __get_pull_request(self, pull_request):
68 def __get_pull_request(self, pull_request):
69 return self._get_instance(PullRequest, pull_request)
69 return self._get_instance(PullRequest, pull_request)
70
70
71 def _extract_mentions(self, s):
71 def _extract_mentions(self, s):
72 user_objects = []
72 user_objects = []
73 for username in extract_mentioned_users(s):
73 for username in extract_mentioned_users(s):
74 user_obj = User.get_by_username(username, case_insensitive=True)
74 user_obj = User.get_by_username(username, case_insensitive=True)
75 if user_obj:
75 if user_obj:
76 user_objects.append(user_obj)
76 user_objects.append(user_obj)
77 return user_objects
77 return user_objects
78
78
79 def _get_renderer(self, global_renderer='rst', request=None):
79 def _get_renderer(self, global_renderer='rst', request=None):
80 request = request or get_current_request()
80 request = request or get_current_request()
81
81
82 try:
82 try:
83 global_renderer = request.call_context.visual.default_renderer
83 global_renderer = request.call_context.visual.default_renderer
84 except AttributeError:
84 except AttributeError:
85 log.debug("Renderer not set, falling back "
85 log.debug("Renderer not set, falling back "
86 "to default renderer '%s'", global_renderer)
86 "to default renderer '%s'", global_renderer)
87 except Exception:
87 except Exception:
88 log.error(traceback.format_exc())
88 log.error(traceback.format_exc())
89 return global_renderer
89 return global_renderer
90
90
91 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 def aggregate_comments(self, comments, versions, show_version, inline=False):
92 # group by versions, and count until, and display objects
92 # group by versions, and count until, and display objects
93
93
94 comment_groups = collections.defaultdict(list)
94 comment_groups = collections.defaultdict(list)
95 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
96
96
97 def yield_comments(pos):
97 def yield_comments(pos):
98 for co in comment_groups[pos]:
98 for co in comment_groups[pos]:
99 yield co
99 yield co
100
100
101 comment_versions = collections.defaultdict(
101 comment_versions = collections.defaultdict(
102 lambda: collections.defaultdict(list))
102 lambda: collections.defaultdict(list))
103 prev_prvid = -1
103 prev_prvid = -1
104 # fake last entry with None, to aggregate on "latest" version which
104 # fake last entry with None, to aggregate on "latest" version which
105 # doesn't have an pull_request_version_id
105 # doesn't have an pull_request_version_id
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
107 prvid = ver.pull_request_version_id
107 prvid = ver.pull_request_version_id
108 if prev_prvid == -1:
108 if prev_prvid == -1:
109 prev_prvid = prvid
109 prev_prvid = prvid
110
110
111 for co in yield_comments(prvid):
111 for co in yield_comments(prvid):
112 comment_versions[prvid]['at'].append(co)
112 comment_versions[prvid]['at'].append(co)
113
113
114 # save until
114 # save until
115 current = comment_versions[prvid]['at']
115 current = comment_versions[prvid]['at']
116 prev_until = comment_versions[prev_prvid]['until']
116 prev_until = comment_versions[prev_prvid]['until']
117 cur_until = prev_until + current
117 cur_until = prev_until + current
118 comment_versions[prvid]['until'].extend(cur_until)
118 comment_versions[prvid]['until'].extend(cur_until)
119
119
120 # save outdated
120 # save outdated
121 if inline:
121 if inline:
122 outdated = [x for x in cur_until
122 outdated = [x for x in cur_until
123 if x.outdated_at_version(show_version)]
123 if x.outdated_at_version(show_version)]
124 else:
124 else:
125 outdated = [x for x in cur_until
125 outdated = [x for x in cur_until
126 if x.older_than_version(show_version)]
126 if x.older_than_version(show_version)]
127 display = [x for x in cur_until if x not in outdated]
127 display = [x for x in cur_until if x not in outdated]
128
128
129 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['outdated'] = outdated
130 comment_versions[prvid]['display'] = display
130 comment_versions[prvid]['display'] = display
131
131
132 prev_prvid = prvid
132 prev_prvid = prvid
133
133
134 return comment_versions
134 return comment_versions
135
135
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
137 qry = Session().query(ChangesetComment) \
137 qry = Session().query(ChangesetComment) \
138 .filter(ChangesetComment.repo == repo)
138 .filter(ChangesetComment.repo == repo)
139
139
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
142
142
143 if user:
143 if user:
144 user = self._get_user(user)
144 user = self._get_user(user)
145 if user:
145 if user:
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
147
147
148 if commit_id:
148 if commit_id:
149 qry = qry.filter(ChangesetComment.revision == commit_id)
149 qry = qry.filter(ChangesetComment.revision == commit_id)
150
150
151 qry = qry.order_by(ChangesetComment.created_on)
151 qry = qry.order_by(ChangesetComment.created_on)
152 return qry.all()
152 return qry.all()
153
153
154 def get_repository_unresolved_todos(self, repo):
154 def get_repository_unresolved_todos(self, repo):
155 todos = Session().query(ChangesetComment) \
155 todos = Session().query(ChangesetComment) \
156 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.repo == repo) \
157 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.resolved_by == None) \
158 .filter(ChangesetComment.comment_type
158 .filter(ChangesetComment.comment_type
159 == ChangesetComment.COMMENT_TYPE_TODO)
159 == ChangesetComment.COMMENT_TYPE_TODO)
160 todos = todos.all()
160 todos = todos.all()
161
161
162 return todos
162 return todos
163
163
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
165
165
166 todos = Session().query(ChangesetComment) \
166 todos = Session().query(ChangesetComment) \
167 .filter(ChangesetComment.pull_request == pull_request) \
167 .filter(ChangesetComment.pull_request == pull_request) \
168 .filter(ChangesetComment.resolved_by == None) \
168 .filter(ChangesetComment.resolved_by == None) \
169 .filter(ChangesetComment.comment_type
169 .filter(ChangesetComment.comment_type
170 == ChangesetComment.COMMENT_TYPE_TODO)
170 == ChangesetComment.COMMENT_TYPE_TODO)
171
171
172 if not include_drafts:
172 if not include_drafts:
173 todos = todos.filter(ChangesetComment.draft == false())
173 todos = todos.filter(ChangesetComment.draft == false())
174
174
175 if not show_outdated:
175 if not show_outdated:
176 todos = todos.filter(
176 todos = todos.filter(
177 coalesce(ChangesetComment.display_state, '') !=
177 coalesce(ChangesetComment.display_state, '') !=
178 ChangesetComment.COMMENT_OUTDATED)
178 ChangesetComment.COMMENT_OUTDATED)
179
179
180 todos = todos.all()
180 todos = todos.all()
181
181
182 return todos
182 return todos
183
183
184 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
184 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
185
185
186 todos = Session().query(ChangesetComment) \
186 todos = Session().query(ChangesetComment) \
187 .filter(ChangesetComment.pull_request == pull_request) \
187 .filter(ChangesetComment.pull_request == pull_request) \
188 .filter(ChangesetComment.resolved_by != None) \
188 .filter(ChangesetComment.resolved_by != None) \
189 .filter(ChangesetComment.comment_type
189 .filter(ChangesetComment.comment_type
190 == ChangesetComment.COMMENT_TYPE_TODO)
190 == ChangesetComment.COMMENT_TYPE_TODO)
191
191
192 if not include_drafts:
192 if not include_drafts:
193 todos = todos.filter(ChangesetComment.draft == false())
193 todos = todos.filter(ChangesetComment.draft == false())
194
194
195 if not show_outdated:
195 if not show_outdated:
196 todos = todos.filter(
196 todos = todos.filter(
197 coalesce(ChangesetComment.display_state, '') !=
197 coalesce(ChangesetComment.display_state, '') !=
198 ChangesetComment.COMMENT_OUTDATED)
198 ChangesetComment.COMMENT_OUTDATED)
199
199
200 todos = todos.all()
200 todos = todos.all()
201
201
202 return todos
202 return todos
203
203
204 def get_pull_request_drafts(self, user_id, pull_request):
204 def get_pull_request_drafts(self, user_id, pull_request):
205 drafts = Session().query(ChangesetComment) \
205 drafts = Session().query(ChangesetComment) \
206 .filter(ChangesetComment.pull_request == pull_request) \
206 .filter(ChangesetComment.pull_request == pull_request) \
207 .filter(ChangesetComment.user_id == user_id) \
207 .filter(ChangesetComment.user_id == user_id) \
208 .filter(ChangesetComment.draft == true())
208 .filter(ChangesetComment.draft == true())
209 return drafts.all()
209 return drafts.all()
210
210
211 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
211 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
212
212
213 todos = Session().query(ChangesetComment) \
213 todos = Session().query(ChangesetComment) \
214 .filter(ChangesetComment.revision == commit_id) \
214 .filter(ChangesetComment.revision == commit_id) \
215 .filter(ChangesetComment.resolved_by == None) \
215 .filter(ChangesetComment.resolved_by == None) \
216 .filter(ChangesetComment.comment_type
216 .filter(ChangesetComment.comment_type
217 == ChangesetComment.COMMENT_TYPE_TODO)
217 == ChangesetComment.COMMENT_TYPE_TODO)
218
218
219 if not include_drafts:
219 if not include_drafts:
220 todos = todos.filter(ChangesetComment.draft == false())
220 todos = todos.filter(ChangesetComment.draft == false())
221
221
222 if not show_outdated:
222 if not show_outdated:
223 todos = todos.filter(
223 todos = todos.filter(
224 coalesce(ChangesetComment.display_state, '') !=
224 coalesce(ChangesetComment.display_state, '') !=
225 ChangesetComment.COMMENT_OUTDATED)
225 ChangesetComment.COMMENT_OUTDATED)
226
226
227 todos = todos.all()
227 todos = todos.all()
228
228
229 return todos
229 return todos
230
230
231 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
231 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
232
232
233 todos = Session().query(ChangesetComment) \
233 todos = Session().query(ChangesetComment) \
234 .filter(ChangesetComment.revision == commit_id) \
234 .filter(ChangesetComment.revision == commit_id) \
235 .filter(ChangesetComment.resolved_by != None) \
235 .filter(ChangesetComment.resolved_by != None) \
236 .filter(ChangesetComment.comment_type
236 .filter(ChangesetComment.comment_type
237 == ChangesetComment.COMMENT_TYPE_TODO)
237 == ChangesetComment.COMMENT_TYPE_TODO)
238
238
239 if not include_drafts:
239 if not include_drafts:
240 todos = todos.filter(ChangesetComment.draft == false())
240 todos = todos.filter(ChangesetComment.draft == false())
241
241
242 if not show_outdated:
242 if not show_outdated:
243 todos = todos.filter(
243 todos = todos.filter(
244 coalesce(ChangesetComment.display_state, '') !=
244 coalesce(ChangesetComment.display_state, '') !=
245 ChangesetComment.COMMENT_OUTDATED)
245 ChangesetComment.COMMENT_OUTDATED)
246
246
247 todos = todos.all()
247 todos = todos.all()
248
248
249 return todos
249 return todos
250
250
251 def get_commit_inline_comments(self, commit_id, include_drafts=True):
251 def get_commit_inline_comments(self, commit_id, include_drafts=True):
252 inline_comments = Session().query(ChangesetComment) \
252 inline_comments = Session().query(ChangesetComment) \
253 .filter(ChangesetComment.line_no != None) \
253 .filter(ChangesetComment.line_no != None) \
254 .filter(ChangesetComment.f_path != None) \
254 .filter(ChangesetComment.f_path != None) \
255 .filter(ChangesetComment.revision == commit_id)
255 .filter(ChangesetComment.revision == commit_id)
256
256
257 if not include_drafts:
257 if not include_drafts:
258 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
258 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
259
259
260 inline_comments = inline_comments.all()
260 inline_comments = inline_comments.all()
261 return inline_comments
261 return inline_comments
262
262
263 def _log_audit_action(self, action, action_data, auth_user, comment):
263 def _log_audit_action(self, action, action_data, auth_user, comment):
264 audit_logger.store(
264 audit_logger.store(
265 action=action,
265 action=action,
266 action_data=action_data,
266 action_data=action_data,
267 user=auth_user,
267 user=auth_user,
268 repo=comment.repo)
268 repo=comment.repo)
269
269
270 def create(self, text, repo, user, commit_id=None, pull_request=None,
270 def create(self, text, repo, user, commit_id=None, pull_request=None,
271 f_path=None, line_no=None, status_change=None,
271 f_path=None, line_no=None, status_change=None,
272 status_change_type=None, comment_type=None, is_draft=False,
272 status_change_type=None, comment_type=None, is_draft=False,
273 resolves_comment_id=None, closing_pr=False, send_email=True,
273 resolves_comment_id=None, closing_pr=False, send_email=True,
274 renderer=None, auth_user=None, extra_recipients=None):
274 renderer=None, auth_user=None, extra_recipients=None):
275 """
275 """
276 Creates new comment for commit or pull request.
276 Creates new comment for commit or pull request.
277 IF status_change is not none this comment is associated with a
277 IF status_change is not none this comment is associated with a
278 status change of commit or commit associated with pull request
278 status change of commit or commit associated with pull request
279
279
280 :param text:
280 :param text:
281 :param repo:
281 :param repo:
282 :param user:
282 :param user:
283 :param commit_id:
283 :param commit_id:
284 :param pull_request:
284 :param pull_request:
285 :param f_path:
285 :param f_path:
286 :param line_no:
286 :param line_no:
287 :param status_change: Label for status change
287 :param status_change: Label for status change
288 :param comment_type: Type of comment
288 :param comment_type: Type of comment
289 :param is_draft: is comment a draft only
289 :param is_draft: is comment a draft only
290 :param resolves_comment_id: id of comment which this one will resolve
290 :param resolves_comment_id: id of comment which this one will resolve
291 :param status_change_type: type of status change
291 :param status_change_type: type of status change
292 :param closing_pr:
292 :param closing_pr:
293 :param send_email:
293 :param send_email:
294 :param renderer: pick renderer for this comment
294 :param renderer: pick renderer for this comment
295 :param auth_user: current authenticated user calling this method
295 :param auth_user: current authenticated user calling this method
296 :param extra_recipients: list of extra users to be added to recipients
296 :param extra_recipients: list of extra users to be added to recipients
297 """
297 """
298
298
299 if not text:
299 if not text:
300 log.warning('Missing text for comment, skipping...')
300 log.warning('Missing text for comment, skipping...')
301 return
301 return
302 request = get_current_request()
302 request = get_current_request()
303 _ = request.translate
303 _ = request.translate
304
304
305 if not renderer:
305 if not renderer:
306 renderer = self._get_renderer(request=request)
306 renderer = self._get_renderer(request=request)
307
307
308 repo = self._get_repo(repo)
308 repo = self._get_repo(repo)
309 user = self._get_user(user)
309 user = self._get_user(user)
310 auth_user = auth_user or user
310 auth_user = auth_user or user
311
311
312 schema = comment_schema.CommentSchema()
312 schema = comment_schema.CommentSchema()
313 validated_kwargs = schema.deserialize(dict(
313 validated_kwargs = schema.deserialize(dict(
314 comment_body=text,
314 comment_body=text,
315 comment_type=comment_type,
315 comment_type=comment_type,
316 is_draft=is_draft,
316 is_draft=is_draft,
317 comment_file=f_path,
317 comment_file=f_path,
318 comment_line=line_no,
318 comment_line=line_no,
319 renderer_type=renderer,
319 renderer_type=renderer,
320 status_change=status_change_type,
320 status_change=status_change_type,
321 resolves_comment_id=resolves_comment_id,
321 resolves_comment_id=resolves_comment_id,
322 repo=repo.repo_id,
322 repo=repo.repo_id,
323 user=user.user_id,
323 user=user.user_id,
324 ))
324 ))
325 is_draft = validated_kwargs['is_draft']
325 is_draft = validated_kwargs['is_draft']
326
326
327 comment = ChangesetComment()
327 comment = ChangesetComment()
328 comment.renderer = validated_kwargs['renderer_type']
328 comment.renderer = validated_kwargs['renderer_type']
329 comment.text = validated_kwargs['comment_body']
329 comment.text = validated_kwargs['comment_body']
330 comment.f_path = validated_kwargs['comment_file']
330 comment.f_path = validated_kwargs['comment_file']
331 comment.line_no = validated_kwargs['comment_line']
331 comment.line_no = validated_kwargs['comment_line']
332 comment.comment_type = validated_kwargs['comment_type']
332 comment.comment_type = validated_kwargs['comment_type']
333 comment.draft = is_draft
333 comment.draft = is_draft
334
334
335 comment.repo = repo
335 comment.repo = repo
336 comment.author = user
336 comment.author = user
337 resolved_comment = self.__get_commit_comment(
337 resolved_comment = self.__get_commit_comment(
338 validated_kwargs['resolves_comment_id'])
338 validated_kwargs['resolves_comment_id'])
339
339 # check if the comment actually belongs to this PR
340 # check if the comment actually belongs to this PR
340 if resolved_comment and resolved_comment.pull_request and \
341 if resolved_comment and resolved_comment.pull_request and \
341 resolved_comment.pull_request != pull_request:
342 resolved_comment.pull_request != pull_request:
342 log.warning('Comment tried to resolved unrelated todo comment: %s',
343 log.warning('Comment tried to resolved unrelated todo comment: %s',
343 resolved_comment)
344 resolved_comment)
344 # comment not bound to this pull request, forbid
345 # comment not bound to this pull request, forbid
345 resolved_comment = None
346 resolved_comment = None
346
347
347 elif resolved_comment and resolved_comment.repo and \
348 elif resolved_comment and resolved_comment.repo and \
348 resolved_comment.repo != repo:
349 resolved_comment.repo != repo:
349 log.warning('Comment tried to resolved unrelated todo comment: %s',
350 log.warning('Comment tried to resolved unrelated todo comment: %s',
350 resolved_comment)
351 resolved_comment)
351 # comment not bound to this repo, forbid
352 # comment not bound to this repo, forbid
352 resolved_comment = None
353 resolved_comment = None
353
354
355 if resolved_comment and resolved_comment.resolved_by:
356 # if this comment is already resolved, don't mark it again!
357 resolved_comment = None
358
354 comment.resolved_comment = resolved_comment
359 comment.resolved_comment = resolved_comment
355
360
356 pull_request_id = pull_request
361 pull_request_id = pull_request
357
362
358 commit_obj = None
363 commit_obj = None
359 pull_request_obj = None
364 pull_request_obj = None
360
365
361 if commit_id:
366 if commit_id:
362 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
367 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
363 # do a lookup, so we don't pass something bad here
368 # do a lookup, so we don't pass something bad here
364 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
369 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
365 comment.revision = commit_obj.raw_id
370 comment.revision = commit_obj.raw_id
366
371
367 elif pull_request_id:
372 elif pull_request_id:
368 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
373 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
369 pull_request_obj = self.__get_pull_request(pull_request_id)
374 pull_request_obj = self.__get_pull_request(pull_request_id)
370 comment.pull_request = pull_request_obj
375 comment.pull_request = pull_request_obj
371 else:
376 else:
372 raise Exception('Please specify commit or pull_request_id')
377 raise Exception('Please specify commit or pull_request_id')
373
378
374 Session().add(comment)
379 Session().add(comment)
375 Session().flush()
380 Session().flush()
376 kwargs = {
381 kwargs = {
377 'user': user,
382 'user': user,
378 'renderer_type': renderer,
383 'renderer_type': renderer,
379 'repo_name': repo.repo_name,
384 'repo_name': repo.repo_name,
380 'status_change': status_change,
385 'status_change': status_change,
381 'status_change_type': status_change_type,
386 'status_change_type': status_change_type,
382 'comment_body': text,
387 'comment_body': text,
383 'comment_file': f_path,
388 'comment_file': f_path,
384 'comment_line': line_no,
389 'comment_line': line_no,
385 'comment_type': comment_type or 'note',
390 'comment_type': comment_type or 'note',
386 'comment_id': comment.comment_id
391 'comment_id': comment.comment_id
387 }
392 }
388
393
389 if commit_obj:
394 if commit_obj:
390 recipients = ChangesetComment.get_users(
395 recipients = ChangesetComment.get_users(
391 revision=commit_obj.raw_id)
396 revision=commit_obj.raw_id)
392 # add commit author if it's in RhodeCode system
397 # add commit author if it's in RhodeCode system
393 cs_author = User.get_from_cs_author(commit_obj.author)
398 cs_author = User.get_from_cs_author(commit_obj.author)
394 if not cs_author:
399 if not cs_author:
395 # use repo owner if we cannot extract the author correctly
400 # use repo owner if we cannot extract the author correctly
396 cs_author = repo.user
401 cs_author = repo.user
397 recipients += [cs_author]
402 recipients += [cs_author]
398
403
399 commit_comment_url = self.get_url(comment, request=request)
404 commit_comment_url = self.get_url(comment, request=request)
400 commit_comment_reply_url = self.get_url(
405 commit_comment_reply_url = self.get_url(
401 comment, request=request,
406 comment, request=request,
402 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
407 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
403
408
404 target_repo_url = h.link_to(
409 target_repo_url = h.link_to(
405 repo.repo_name,
410 repo.repo_name,
406 h.route_url('repo_summary', repo_name=repo.repo_name))
411 h.route_url('repo_summary', repo_name=repo.repo_name))
407
412
408 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
413 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
409 commit_id=commit_id)
414 commit_id=commit_id)
410
415
411 # commit specifics
416 # commit specifics
412 kwargs.update({
417 kwargs.update({
413 'commit': commit_obj,
418 'commit': commit_obj,
414 'commit_message': commit_obj.message,
419 'commit_message': commit_obj.message,
415 'commit_target_repo_url': target_repo_url,
420 'commit_target_repo_url': target_repo_url,
416 'commit_comment_url': commit_comment_url,
421 'commit_comment_url': commit_comment_url,
417 'commit_comment_reply_url': commit_comment_reply_url,
422 'commit_comment_reply_url': commit_comment_reply_url,
418 'commit_url': commit_url,
423 'commit_url': commit_url,
419 'thread_ids': [commit_url, commit_comment_url],
424 'thread_ids': [commit_url, commit_comment_url],
420 })
425 })
421
426
422 elif pull_request_obj:
427 elif pull_request_obj:
423 # get the current participants of this pull request
428 # get the current participants of this pull request
424 recipients = ChangesetComment.get_users(
429 recipients = ChangesetComment.get_users(
425 pull_request_id=pull_request_obj.pull_request_id)
430 pull_request_id=pull_request_obj.pull_request_id)
426 # add pull request author
431 # add pull request author
427 recipients += [pull_request_obj.author]
432 recipients += [pull_request_obj.author]
428
433
429 # add the reviewers to notification
434 # add the reviewers to notification
430 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
435 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
431
436
432 pr_target_repo = pull_request_obj.target_repo
437 pr_target_repo = pull_request_obj.target_repo
433 pr_source_repo = pull_request_obj.source_repo
438 pr_source_repo = pull_request_obj.source_repo
434
439
435 pr_comment_url = self.get_url(comment, request=request)
440 pr_comment_url = self.get_url(comment, request=request)
436 pr_comment_reply_url = self.get_url(
441 pr_comment_reply_url = self.get_url(
437 comment, request=request,
442 comment, request=request,
438 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
443 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
439
444
440 pr_url = h.route_url(
445 pr_url = h.route_url(
441 'pullrequest_show',
446 'pullrequest_show',
442 repo_name=pr_target_repo.repo_name,
447 repo_name=pr_target_repo.repo_name,
443 pull_request_id=pull_request_obj.pull_request_id, )
448 pull_request_id=pull_request_obj.pull_request_id, )
444
449
445 # set some variables for email notification
450 # set some variables for email notification
446 pr_target_repo_url = h.route_url(
451 pr_target_repo_url = h.route_url(
447 'repo_summary', repo_name=pr_target_repo.repo_name)
452 'repo_summary', repo_name=pr_target_repo.repo_name)
448
453
449 pr_source_repo_url = h.route_url(
454 pr_source_repo_url = h.route_url(
450 'repo_summary', repo_name=pr_source_repo.repo_name)
455 'repo_summary', repo_name=pr_source_repo.repo_name)
451
456
452 # pull request specifics
457 # pull request specifics
453 kwargs.update({
458 kwargs.update({
454 'pull_request': pull_request_obj,
459 'pull_request': pull_request_obj,
455 'pr_id': pull_request_obj.pull_request_id,
460 'pr_id': pull_request_obj.pull_request_id,
456 'pull_request_url': pr_url,
461 'pull_request_url': pr_url,
457 'pull_request_target_repo': pr_target_repo,
462 'pull_request_target_repo': pr_target_repo,
458 'pull_request_target_repo_url': pr_target_repo_url,
463 'pull_request_target_repo_url': pr_target_repo_url,
459 'pull_request_source_repo': pr_source_repo,
464 'pull_request_source_repo': pr_source_repo,
460 'pull_request_source_repo_url': pr_source_repo_url,
465 'pull_request_source_repo_url': pr_source_repo_url,
461 'pr_comment_url': pr_comment_url,
466 'pr_comment_url': pr_comment_url,
462 'pr_comment_reply_url': pr_comment_reply_url,
467 'pr_comment_reply_url': pr_comment_reply_url,
463 'pr_closing': closing_pr,
468 'pr_closing': closing_pr,
464 'thread_ids': [pr_url, pr_comment_url],
469 'thread_ids': [pr_url, pr_comment_url],
465 })
470 })
466
471
467 if send_email:
472 if send_email:
468 recipients += [self._get_user(u) for u in (extra_recipients or [])]
473 recipients += [self._get_user(u) for u in (extra_recipients or [])]
469
474
470 mention_recipients = set(
475 mention_recipients = set(
471 self._extract_mentions(text)).difference(recipients)
476 self._extract_mentions(text)).difference(recipients)
472
477
473 # create notification objects, and emails
478 # create notification objects, and emails
474 NotificationModel().create(
479 NotificationModel().create(
475 created_by=user,
480 created_by=user,
476 notification_subject='', # Filled in based on the notification_type
481 notification_subject='', # Filled in based on the notification_type
477 notification_body='', # Filled in based on the notification_type
482 notification_body='', # Filled in based on the notification_type
478 notification_type=notification_type,
483 notification_type=notification_type,
479 recipients=recipients,
484 recipients=recipients,
480 mention_recipients=mention_recipients,
485 mention_recipients=mention_recipients,
481 email_kwargs=kwargs,
486 email_kwargs=kwargs,
482 )
487 )
483
488
484 Session().flush()
489 Session().flush()
485 if comment.pull_request:
490 if comment.pull_request:
486 action = 'repo.pull_request.comment.create'
491 action = 'repo.pull_request.comment.create'
487 else:
492 else:
488 action = 'repo.commit.comment.create'
493 action = 'repo.commit.comment.create'
489
494
490 if not is_draft:
495 if not is_draft:
491 comment_data = comment.get_api_data()
496 comment_data = comment.get_api_data()
492
497
493 self._log_audit_action(
498 self._log_audit_action(
494 action, {'data': comment_data}, auth_user, comment)
499 action, {'data': comment_data}, auth_user, comment)
495
500
496 return comment
501 return comment
497
502
498 def edit(self, comment_id, text, auth_user, version):
503 def edit(self, comment_id, text, auth_user, version):
499 """
504 """
500 Change existing comment for commit or pull request.
505 Change existing comment for commit or pull request.
501
506
502 :param comment_id:
507 :param comment_id:
503 :param text:
508 :param text:
504 :param auth_user: current authenticated user calling this method
509 :param auth_user: current authenticated user calling this method
505 :param version: last comment version
510 :param version: last comment version
506 """
511 """
507 if not text:
512 if not text:
508 log.warning('Missing text for comment, skipping...')
513 log.warning('Missing text for comment, skipping...')
509 return
514 return
510
515
511 comment = ChangesetComment.get(comment_id)
516 comment = ChangesetComment.get(comment_id)
512 old_comment_text = comment.text
517 old_comment_text = comment.text
513 comment.text = text
518 comment.text = text
514 comment.modified_at = datetime.datetime.now()
519 comment.modified_at = datetime.datetime.now()
515 version = safe_int(version)
520 version = safe_int(version)
516
521
517 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
522 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
518 # would return 3 here
523 # would return 3 here
519 comment_version = ChangesetCommentHistory.get_version(comment_id)
524 comment_version = ChangesetCommentHistory.get_version(comment_id)
520
525
521 if isinstance(version, (int, long)) and (comment_version - version) != 1:
526 if isinstance(version, (int, long)) and (comment_version - version) != 1:
522 log.warning(
527 log.warning(
523 'Version mismatch comment_version {} submitted {}, skipping'.format(
528 'Version mismatch comment_version {} submitted {}, skipping'.format(
524 comment_version-1, # -1 since note above
529 comment_version-1, # -1 since note above
525 version
530 version
526 )
531 )
527 )
532 )
528 raise CommentVersionMismatch()
533 raise CommentVersionMismatch()
529
534
530 comment_history = ChangesetCommentHistory()
535 comment_history = ChangesetCommentHistory()
531 comment_history.comment_id = comment_id
536 comment_history.comment_id = comment_id
532 comment_history.version = comment_version
537 comment_history.version = comment_version
533 comment_history.created_by_user_id = auth_user.user_id
538 comment_history.created_by_user_id = auth_user.user_id
534 comment_history.text = old_comment_text
539 comment_history.text = old_comment_text
535 # TODO add email notification
540 # TODO add email notification
536 Session().add(comment_history)
541 Session().add(comment_history)
537 Session().add(comment)
542 Session().add(comment)
538 Session().flush()
543 Session().flush()
539
544
540 if comment.pull_request:
545 if comment.pull_request:
541 action = 'repo.pull_request.comment.edit'
546 action = 'repo.pull_request.comment.edit'
542 else:
547 else:
543 action = 'repo.commit.comment.edit'
548 action = 'repo.commit.comment.edit'
544
549
545 comment_data = comment.get_api_data()
550 comment_data = comment.get_api_data()
546 comment_data['old_comment_text'] = old_comment_text
551 comment_data['old_comment_text'] = old_comment_text
547 self._log_audit_action(
552 self._log_audit_action(
548 action, {'data': comment_data}, auth_user, comment)
553 action, {'data': comment_data}, auth_user, comment)
549
554
550 return comment_history
555 return comment_history
551
556
552 def delete(self, comment, auth_user):
557 def delete(self, comment, auth_user):
553 """
558 """
554 Deletes given comment
559 Deletes given comment
555 """
560 """
556 comment = self.__get_commit_comment(comment)
561 comment = self.__get_commit_comment(comment)
557 old_data = comment.get_api_data()
562 old_data = comment.get_api_data()
558 Session().delete(comment)
563 Session().delete(comment)
559
564
560 if comment.pull_request:
565 if comment.pull_request:
561 action = 'repo.pull_request.comment.delete'
566 action = 'repo.pull_request.comment.delete'
562 else:
567 else:
563 action = 'repo.commit.comment.delete'
568 action = 'repo.commit.comment.delete'
564
569
565 self._log_audit_action(
570 self._log_audit_action(
566 action, {'old_data': old_data}, auth_user, comment)
571 action, {'old_data': old_data}, auth_user, comment)
567
572
568 return comment
573 return comment
569
574
570 def get_all_comments(self, repo_id, revision=None, pull_request=None,
575 def get_all_comments(self, repo_id, revision=None, pull_request=None,
571 include_drafts=True, count_only=False):
576 include_drafts=True, count_only=False):
572 q = ChangesetComment.query()\
577 q = ChangesetComment.query()\
573 .filter(ChangesetComment.repo_id == repo_id)
578 .filter(ChangesetComment.repo_id == repo_id)
574 if revision:
579 if revision:
575 q = q.filter(ChangesetComment.revision == revision)
580 q = q.filter(ChangesetComment.revision == revision)
576 elif pull_request:
581 elif pull_request:
577 pull_request = self.__get_pull_request(pull_request)
582 pull_request = self.__get_pull_request(pull_request)
578 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
583 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
579 else:
584 else:
580 raise Exception('Please specify commit or pull_request')
585 raise Exception('Please specify commit or pull_request')
581 if not include_drafts:
586 if not include_drafts:
582 q = q.filter(ChangesetComment.draft == false())
587 q = q.filter(ChangesetComment.draft == false())
583 q = q.order_by(ChangesetComment.created_on)
588 q = q.order_by(ChangesetComment.created_on)
584 if count_only:
589 if count_only:
585 return q.count()
590 return q.count()
586
591
587 return q.all()
592 return q.all()
588
593
589 def get_url(self, comment, request=None, permalink=False, anchor=None):
594 def get_url(self, comment, request=None, permalink=False, anchor=None):
590 if not request:
595 if not request:
591 request = get_current_request()
596 request = get_current_request()
592
597
593 comment = self.__get_commit_comment(comment)
598 comment = self.__get_commit_comment(comment)
594 if anchor is None:
599 if anchor is None:
595 anchor = 'comment-{}'.format(comment.comment_id)
600 anchor = 'comment-{}'.format(comment.comment_id)
596
601
597 if comment.pull_request:
602 if comment.pull_request:
598 pull_request = comment.pull_request
603 pull_request = comment.pull_request
599 if permalink:
604 if permalink:
600 return request.route_url(
605 return request.route_url(
601 'pull_requests_global',
606 'pull_requests_global',
602 pull_request_id=pull_request.pull_request_id,
607 pull_request_id=pull_request.pull_request_id,
603 _anchor=anchor)
608 _anchor=anchor)
604 else:
609 else:
605 return request.route_url(
610 return request.route_url(
606 'pullrequest_show',
611 'pullrequest_show',
607 repo_name=safe_str(pull_request.target_repo.repo_name),
612 repo_name=safe_str(pull_request.target_repo.repo_name),
608 pull_request_id=pull_request.pull_request_id,
613 pull_request_id=pull_request.pull_request_id,
609 _anchor=anchor)
614 _anchor=anchor)
610
615
611 else:
616 else:
612 repo = comment.repo
617 repo = comment.repo
613 commit_id = comment.revision
618 commit_id = comment.revision
614
619
615 if permalink:
620 if permalink:
616 return request.route_url(
621 return request.route_url(
617 'repo_commit', repo_name=safe_str(repo.repo_id),
622 'repo_commit', repo_name=safe_str(repo.repo_id),
618 commit_id=commit_id,
623 commit_id=commit_id,
619 _anchor=anchor)
624 _anchor=anchor)
620
625
621 else:
626 else:
622 return request.route_url(
627 return request.route_url(
623 'repo_commit', repo_name=safe_str(repo.repo_name),
628 'repo_commit', repo_name=safe_str(repo.repo_name),
624 commit_id=commit_id,
629 commit_id=commit_id,
625 _anchor=anchor)
630 _anchor=anchor)
626
631
627 def get_comments(self, repo_id, revision=None, pull_request=None):
632 def get_comments(self, repo_id, revision=None, pull_request=None):
628 """
633 """
629 Gets main comments based on revision or pull_request_id
634 Gets main comments based on revision or pull_request_id
630
635
631 :param repo_id:
636 :param repo_id:
632 :param revision:
637 :param revision:
633 :param pull_request:
638 :param pull_request:
634 """
639 """
635
640
636 q = ChangesetComment.query()\
641 q = ChangesetComment.query()\
637 .filter(ChangesetComment.repo_id == repo_id)\
642 .filter(ChangesetComment.repo_id == repo_id)\
638 .filter(ChangesetComment.line_no == None)\
643 .filter(ChangesetComment.line_no == None)\
639 .filter(ChangesetComment.f_path == None)
644 .filter(ChangesetComment.f_path == None)
640 if revision:
645 if revision:
641 q = q.filter(ChangesetComment.revision == revision)
646 q = q.filter(ChangesetComment.revision == revision)
642 elif pull_request:
647 elif pull_request:
643 pull_request = self.__get_pull_request(pull_request)
648 pull_request = self.__get_pull_request(pull_request)
644 q = q.filter(ChangesetComment.pull_request == pull_request)
649 q = q.filter(ChangesetComment.pull_request == pull_request)
645 else:
650 else:
646 raise Exception('Please specify commit or pull_request')
651 raise Exception('Please specify commit or pull_request')
647 q = q.order_by(ChangesetComment.created_on)
652 q = q.order_by(ChangesetComment.created_on)
648 return q.all()
653 return q.all()
649
654
650 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
655 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
651 q = self._get_inline_comments_query(repo_id, revision, pull_request)
656 q = self._get_inline_comments_query(repo_id, revision, pull_request)
652 return self._group_comments_by_path_and_line_number(q)
657 return self._group_comments_by_path_and_line_number(q)
653
658
654 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
659 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
655 version=None):
660 version=None):
656 inline_comms = []
661 inline_comms = []
657 for fname, per_line_comments in inline_comments.iteritems():
662 for fname, per_line_comments in inline_comments.iteritems():
658 for lno, comments in per_line_comments.iteritems():
663 for lno, comments in per_line_comments.iteritems():
659 for comm in comments:
664 for comm in comments:
660 if not comm.outdated_at_version(version) and skip_outdated:
665 if not comm.outdated_at_version(version) and skip_outdated:
661 inline_comms.append(comm)
666 inline_comms.append(comm)
662
667
663 return inline_comms
668 return inline_comms
664
669
665 def get_outdated_comments(self, repo_id, pull_request):
670 def get_outdated_comments(self, repo_id, pull_request):
666 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
671 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
667 # of a pull request.
672 # of a pull request.
668 q = self._all_inline_comments_of_pull_request(pull_request)
673 q = self._all_inline_comments_of_pull_request(pull_request)
669 q = q.filter(
674 q = q.filter(
670 ChangesetComment.display_state ==
675 ChangesetComment.display_state ==
671 ChangesetComment.COMMENT_OUTDATED
676 ChangesetComment.COMMENT_OUTDATED
672 ).order_by(ChangesetComment.comment_id.asc())
677 ).order_by(ChangesetComment.comment_id.asc())
673
678
674 return self._group_comments_by_path_and_line_number(q)
679 return self._group_comments_by_path_and_line_number(q)
675
680
676 def _get_inline_comments_query(self, repo_id, revision, pull_request):
681 def _get_inline_comments_query(self, repo_id, revision, pull_request):
677 # TODO: johbo: Split this into two methods: One for PR and one for
682 # TODO: johbo: Split this into two methods: One for PR and one for
678 # commit.
683 # commit.
679 if revision:
684 if revision:
680 q = Session().query(ChangesetComment).filter(
685 q = Session().query(ChangesetComment).filter(
681 ChangesetComment.repo_id == repo_id,
686 ChangesetComment.repo_id == repo_id,
682 ChangesetComment.line_no != null(),
687 ChangesetComment.line_no != null(),
683 ChangesetComment.f_path != null(),
688 ChangesetComment.f_path != null(),
684 ChangesetComment.revision == revision)
689 ChangesetComment.revision == revision)
685
690
686 elif pull_request:
691 elif pull_request:
687 pull_request = self.__get_pull_request(pull_request)
692 pull_request = self.__get_pull_request(pull_request)
688 if not CommentsModel.use_outdated_comments(pull_request):
693 if not CommentsModel.use_outdated_comments(pull_request):
689 q = self._visible_inline_comments_of_pull_request(pull_request)
694 q = self._visible_inline_comments_of_pull_request(pull_request)
690 else:
695 else:
691 q = self._all_inline_comments_of_pull_request(pull_request)
696 q = self._all_inline_comments_of_pull_request(pull_request)
692
697
693 else:
698 else:
694 raise Exception('Please specify commit or pull_request_id')
699 raise Exception('Please specify commit or pull_request_id')
695 q = q.order_by(ChangesetComment.comment_id.asc())
700 q = q.order_by(ChangesetComment.comment_id.asc())
696 return q
701 return q
697
702
698 def _group_comments_by_path_and_line_number(self, q):
703 def _group_comments_by_path_and_line_number(self, q):
699 comments = q.all()
704 comments = q.all()
700 paths = collections.defaultdict(lambda: collections.defaultdict(list))
705 paths = collections.defaultdict(lambda: collections.defaultdict(list))
701 for co in comments:
706 for co in comments:
702 paths[co.f_path][co.line_no].append(co)
707 paths[co.f_path][co.line_no].append(co)
703 return paths
708 return paths
704
709
705 @classmethod
710 @classmethod
706 def needed_extra_diff_context(cls):
711 def needed_extra_diff_context(cls):
707 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
712 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
708
713
709 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
714 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
710 if not CommentsModel.use_outdated_comments(pull_request):
715 if not CommentsModel.use_outdated_comments(pull_request):
711 return
716 return
712
717
713 comments = self._visible_inline_comments_of_pull_request(pull_request)
718 comments = self._visible_inline_comments_of_pull_request(pull_request)
714 comments_to_outdate = comments.all()
719 comments_to_outdate = comments.all()
715
720
716 for comment in comments_to_outdate:
721 for comment in comments_to_outdate:
717 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
722 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
718
723
719 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
724 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
720 diff_line = _parse_comment_line_number(comment.line_no)
725 diff_line = _parse_comment_line_number(comment.line_no)
721
726
722 try:
727 try:
723 old_context = old_diff_proc.get_context_of_line(
728 old_context = old_diff_proc.get_context_of_line(
724 path=comment.f_path, diff_line=diff_line)
729 path=comment.f_path, diff_line=diff_line)
725 new_context = new_diff_proc.get_context_of_line(
730 new_context = new_diff_proc.get_context_of_line(
726 path=comment.f_path, diff_line=diff_line)
731 path=comment.f_path, diff_line=diff_line)
727 except (diffs.LineNotInDiffException,
732 except (diffs.LineNotInDiffException,
728 diffs.FileNotInDiffException):
733 diffs.FileNotInDiffException):
729 if not comment.draft:
734 if not comment.draft:
730 comment.display_state = ChangesetComment.COMMENT_OUTDATED
735 comment.display_state = ChangesetComment.COMMENT_OUTDATED
731 return
736 return
732
737
733 if old_context == new_context:
738 if old_context == new_context:
734 return
739 return
735
740
736 if self._should_relocate_diff_line(diff_line):
741 if self._should_relocate_diff_line(diff_line):
737 new_diff_lines = new_diff_proc.find_context(
742 new_diff_lines = new_diff_proc.find_context(
738 path=comment.f_path, context=old_context,
743 path=comment.f_path, context=old_context,
739 offset=self.DIFF_CONTEXT_BEFORE)
744 offset=self.DIFF_CONTEXT_BEFORE)
740 if not new_diff_lines and not comment.draft:
745 if not new_diff_lines and not comment.draft:
741 comment.display_state = ChangesetComment.COMMENT_OUTDATED
746 comment.display_state = ChangesetComment.COMMENT_OUTDATED
742 else:
747 else:
743 new_diff_line = self._choose_closest_diff_line(
748 new_diff_line = self._choose_closest_diff_line(
744 diff_line, new_diff_lines)
749 diff_line, new_diff_lines)
745 comment.line_no = _diff_to_comment_line_number(new_diff_line)
750 comment.line_no = _diff_to_comment_line_number(new_diff_line)
746 else:
751 else:
747 if not comment.draft:
752 if not comment.draft:
748 comment.display_state = ChangesetComment.COMMENT_OUTDATED
753 comment.display_state = ChangesetComment.COMMENT_OUTDATED
749
754
750 def _should_relocate_diff_line(self, diff_line):
755 def _should_relocate_diff_line(self, diff_line):
751 """
756 """
752 Checks if relocation shall be tried for the given `diff_line`.
757 Checks if relocation shall be tried for the given `diff_line`.
753
758
754 If a comment points into the first lines, then we can have a situation
759 If a comment points into the first lines, then we can have a situation
755 that after an update another line has been added on top. In this case
760 that after an update another line has been added on top. In this case
756 we would find the context still and move the comment around. This
761 we would find the context still and move the comment around. This
757 would be wrong.
762 would be wrong.
758 """
763 """
759 should_relocate = (
764 should_relocate = (
760 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
765 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
761 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
766 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
762 return should_relocate
767 return should_relocate
763
768
764 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
769 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
765 candidate = new_diff_lines[0]
770 candidate = new_diff_lines[0]
766 best_delta = _diff_line_delta(diff_line, candidate)
771 best_delta = _diff_line_delta(diff_line, candidate)
767 for new_diff_line in new_diff_lines[1:]:
772 for new_diff_line in new_diff_lines[1:]:
768 delta = _diff_line_delta(diff_line, new_diff_line)
773 delta = _diff_line_delta(diff_line, new_diff_line)
769 if delta < best_delta:
774 if delta < best_delta:
770 candidate = new_diff_line
775 candidate = new_diff_line
771 best_delta = delta
776 best_delta = delta
772 return candidate
777 return candidate
773
778
774 def _visible_inline_comments_of_pull_request(self, pull_request):
779 def _visible_inline_comments_of_pull_request(self, pull_request):
775 comments = self._all_inline_comments_of_pull_request(pull_request)
780 comments = self._all_inline_comments_of_pull_request(pull_request)
776 comments = comments.filter(
781 comments = comments.filter(
777 coalesce(ChangesetComment.display_state, '') !=
782 coalesce(ChangesetComment.display_state, '') !=
778 ChangesetComment.COMMENT_OUTDATED)
783 ChangesetComment.COMMENT_OUTDATED)
779 return comments
784 return comments
780
785
781 def _all_inline_comments_of_pull_request(self, pull_request):
786 def _all_inline_comments_of_pull_request(self, pull_request):
782 comments = Session().query(ChangesetComment)\
787 comments = Session().query(ChangesetComment)\
783 .filter(ChangesetComment.line_no != None)\
788 .filter(ChangesetComment.line_no != None)\
784 .filter(ChangesetComment.f_path != None)\
789 .filter(ChangesetComment.f_path != None)\
785 .filter(ChangesetComment.pull_request == pull_request)
790 .filter(ChangesetComment.pull_request == pull_request)
786 return comments
791 return comments
787
792
788 def _all_general_comments_of_pull_request(self, pull_request):
793 def _all_general_comments_of_pull_request(self, pull_request):
789 comments = Session().query(ChangesetComment)\
794 comments = Session().query(ChangesetComment)\
790 .filter(ChangesetComment.line_no == None)\
795 .filter(ChangesetComment.line_no == None)\
791 .filter(ChangesetComment.f_path == None)\
796 .filter(ChangesetComment.f_path == None)\
792 .filter(ChangesetComment.pull_request == pull_request)
797 .filter(ChangesetComment.pull_request == pull_request)
793
798
794 return comments
799 return comments
795
800
796 @staticmethod
801 @staticmethod
797 def use_outdated_comments(pull_request):
802 def use_outdated_comments(pull_request):
798 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
803 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
799 settings = settings_model.get_general_settings()
804 settings = settings_model.get_general_settings()
800 return settings.get('rhodecode_use_outdated_comments', False)
805 return settings.get('rhodecode_use_outdated_comments', False)
801
806
802 def trigger_commit_comment_hook(self, repo, user, action, data=None):
807 def trigger_commit_comment_hook(self, repo, user, action, data=None):
803 repo = self._get_repo(repo)
808 repo = self._get_repo(repo)
804 target_scm = repo.scm_instance()
809 target_scm = repo.scm_instance()
805 if action == 'create':
810 if action == 'create':
806 trigger_hook = hooks_utils.trigger_comment_commit_hooks
811 trigger_hook = hooks_utils.trigger_comment_commit_hooks
807 elif action == 'edit':
812 elif action == 'edit':
808 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
813 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
809 else:
814 else:
810 return
815 return
811
816
812 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
817 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
813 repo, action, trigger_hook)
818 repo, action, trigger_hook)
814 trigger_hook(
819 trigger_hook(
815 username=user.username,
820 username=user.username,
816 repo_name=repo.repo_name,
821 repo_name=repo.repo_name,
817 repo_type=target_scm.alias,
822 repo_type=target_scm.alias,
818 repo=repo,
823 repo=repo,
819 data=data)
824 data=data)
820
825
821
826
822 def _parse_comment_line_number(line_no):
827 def _parse_comment_line_number(line_no):
823 """
828 """
824 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
829 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
825 """
830 """
826 old_line = None
831 old_line = None
827 new_line = None
832 new_line = None
828 if line_no.startswith('o'):
833 if line_no.startswith('o'):
829 old_line = int(line_no[1:])
834 old_line = int(line_no[1:])
830 elif line_no.startswith('n'):
835 elif line_no.startswith('n'):
831 new_line = int(line_no[1:])
836 new_line = int(line_no[1:])
832 else:
837 else:
833 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
838 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
834 return diffs.DiffLineNumber(old_line, new_line)
839 return diffs.DiffLineNumber(old_line, new_line)
835
840
836
841
837 def _diff_to_comment_line_number(diff_line):
842 def _diff_to_comment_line_number(diff_line):
838 if diff_line.new is not None:
843 if diff_line.new is not None:
839 return u'n{}'.format(diff_line.new)
844 return u'n{}'.format(diff_line.new)
840 elif diff_line.old is not None:
845 elif diff_line.old is not None:
841 return u'o{}'.format(diff_line.old)
846 return u'o{}'.format(diff_line.old)
842 return u''
847 return u''
843
848
844
849
845 def _diff_line_delta(a, b):
850 def _diff_line_delta(a, b):
846 if None not in (a.new, b.new):
851 if None not in (a.new, b.new):
847 return abs(a.new - b.new)
852 return abs(a.new - b.new)
848 elif None not in (a.old, b.old):
853 elif None not in (a.old, b.old):
849 return abs(a.old - b.old)
854 return abs(a.old - b.old)
850 else:
855 else:
851 raise ValueError(
856 raise ValueError(
852 "Cannot compute delta between {} and {}".format(a, b))
857 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,1501 +1,1639 b''
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28
28 var linkifyComments = function(comments) {
29 var linkifyComments = function(comments) {
29 var firstCommentId = null;
30 var firstCommentId = null;
30 if (comments) {
31 if (comments) {
31 firstCommentId = $(comments[0]).data('comment-id');
32 firstCommentId = $(comments[0]).data('comment-id');
32 }
33 }
33
34
34 if (firstCommentId){
35 if (firstCommentId){
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 }
37 }
37 };
38 };
38
39
40
39 var bindToggleButtons = function() {
41 var bindToggleButtons = function() {
40 $('.comment-toggle').on('click', function() {
42 $('.comment-toggle').on('click', function() {
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
43 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 });
44 });
43 };
45 };
44
46
45
47
46
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 failHandler = failHandler || function() {};
49 failHandler = failHandler || function() {};
49 postData = toQueryString(postData);
50 postData = toQueryString(postData);
50 var request = $.ajax({
51 var request = $.ajax({
51 url: url,
52 url: url,
52 type: 'POST',
53 type: 'POST',
53 data: postData,
54 data: postData,
54 headers: {'X-PARTIAL-XHR': true}
55 headers: {'X-PARTIAL-XHR': true}
55 })
56 })
56 .done(function (data) {
57 .done(function (data) {
57 successHandler(data);
58 successHandler(data);
58 })
59 })
59 .fail(function (data, textStatus, errorThrown) {
60 .fail(function (data, textStatus, errorThrown) {
60 failHandler(data, textStatus, errorThrown)
61 failHandler(data, textStatus, errorThrown)
61 });
62 });
62 return request;
63 return request;
63 };
64 };
64
65
65
66
66
67
68 /* Comment form for main and inline comments */
67 /* Comment form for main and inline comments */
69 (function(mod) {
68 (function(mod) {
70
69
71 if (typeof exports == "object" && typeof module == "object") {
70 if (typeof exports == "object" && typeof module == "object") {
72 // CommonJS
71 // CommonJS
73 module.exports = mod();
72 module.exports = mod();
74 }
73 }
75 else {
74 else {
76 // Plain browser env
75 // Plain browser env
77 (this || window).CommentForm = mod();
76 (this || window).CommentForm = mod();
78 }
77 }
79
78
80 })(function() {
79 })(function() {
81 "use strict";
80 "use strict";
82
81
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
82 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
84
83
85 if (!(this instanceof CommentForm)) {
84 if (!(this instanceof CommentForm)) {
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
85 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
87 }
86 }
88
87
89 // bind the element instance to our Form
88 // bind the element instance to our Form
90 $(formElement).get(0).CommentForm = this;
89 $(formElement).get(0).CommentForm = this;
91
90
92 this.withLineNo = function(selector) {
91 this.withLineNo = function(selector) {
93 var lineNo = this.lineNo;
92 var lineNo = this.lineNo;
94 if (lineNo === undefined) {
93 if (lineNo === undefined) {
95 return selector
94 return selector
96 } else {
95 } else {
97 return selector + '_' + lineNo;
96 return selector + '_' + lineNo;
98 }
97 }
99 };
98 };
100
99
101 this.commitId = commitId;
100 this.commitId = commitId;
102 this.pullRequestId = pullRequestId;
101 this.pullRequestId = pullRequestId;
103 this.lineNo = lineNo;
102 this.lineNo = lineNo;
104 this.initAutocompleteActions = initAutocompleteActions;
103 this.initAutocompleteActions = initAutocompleteActions;
105
104
106 this.previewButton = this.withLineNo('#preview-btn');
105 this.previewButton = this.withLineNo('#preview-btn');
107 this.previewContainer = this.withLineNo('#preview-container');
106 this.previewContainer = this.withLineNo('#preview-container');
108
107
109 this.previewBoxSelector = this.withLineNo('#preview-box');
108 this.previewBoxSelector = this.withLineNo('#preview-box');
110
109
111 this.editButton = this.withLineNo('#edit-btn');
110 this.editButton = this.withLineNo('#edit-btn');
112 this.editContainer = this.withLineNo('#edit-container');
111 this.editContainer = this.withLineNo('#edit-container');
113 this.cancelButton = this.withLineNo('#cancel-btn');
112 this.cancelButton = this.withLineNo('#cancel-btn');
114 this.commentType = this.withLineNo('#comment_type');
113 this.commentType = this.withLineNo('#comment_type');
115
114
116 this.resolvesId = null;
115 this.resolvesId = null;
117 this.resolvesActionId = null;
116 this.resolvesActionId = null;
118
117
119 this.closesPr = '#close_pull_request';
118 this.closesPr = '#close_pull_request';
120
119
121 this.cmBox = this.withLineNo('#text');
120 this.cmBox = this.withLineNo('#text');
122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
121 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
123
122
124 this.statusChange = this.withLineNo('#change_status');
123 this.statusChange = this.withLineNo('#change_status');
125
124
126 this.submitForm = formElement;
125 this.submitForm = formElement;
127
126
128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
127 this.submitButton = $(this.submitForm).find('.submit-comment-action');
129 this.submitButtonText = this.submitButton.val();
128 this.submitButtonText = this.submitButton.val();
130
129
131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
130 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
132 this.submitDraftButtonText = this.submitDraftButton.val();
131 this.submitDraftButtonText = this.submitDraftButton.val();
133
132
134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
133 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
135 {'repo_name': templateContext.repo_name,
134 {'repo_name': templateContext.repo_name,
136 'commit_id': templateContext.commit_data.commit_id});
135 'commit_id': templateContext.commit_data.commit_id});
137
136
138 if (edit){
137 if (edit){
139 this.submitDraftButton.hide();
138 this.submitDraftButton.hide();
140 this.submitButtonText = _gettext('Update Comment');
139 this.submitButtonText = _gettext('Update Comment');
141 $(this.commentType).prop('disabled', true);
140 $(this.commentType).prop('disabled', true);
142 $(this.commentType).addClass('disabled');
141 $(this.commentType).addClass('disabled');
143 var editInfo =
142 var editInfo =
144 '';
143 '';
145 $(editInfo).insertBefore($(this.editButton).parent());
144 $(editInfo).insertBefore($(this.editButton).parent());
146 }
145 }
147
146
148 if (resolvesCommentId){
147 if (resolvesCommentId){
149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
148 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
149 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
151 $(this.commentType).prop('disabled', true);
150 $(this.commentType).prop('disabled', true);
152 $(this.commentType).addClass('disabled');
151 $(this.commentType).addClass('disabled');
153
152
154 // disable select
153 // disable select
155 setTimeout(function() {
154 setTimeout(function() {
156 $(self.statusChange).select2('readonly', true);
155 $(self.statusChange).select2('readonly', true);
157 }, 10);
156 }, 10);
158
157
159 var resolvedInfo = (
158 var resolvedInfo = (
160 '<li class="resolve-action">' +
159 '<li class="resolve-action">' +
161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
160 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
161 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
163 '</li>'
162 '</li>'
164 ).format(resolvesCommentId, _gettext('resolve comment'));
163 ).format(resolvesCommentId, _gettext('resolve comment'));
165 $(resolvedInfo).insertAfter($(this.commentType).parent());
164 $(resolvedInfo).insertAfter($(this.commentType).parent());
166 }
165 }
167
166
168 // based on commitId, or pullRequestId decide where do we submit
167 // based on commitId, or pullRequestId decide where do we submit
169 // out data
168 // out data
170 if (this.commitId){
169 if (this.commitId){
171 var pyurl = 'repo_commit_comment_create';
170 var pyurl = 'repo_commit_comment_create';
172 if(edit){
171 if(edit){
173 pyurl = 'repo_commit_comment_edit';
172 pyurl = 'repo_commit_comment_edit';
174 }
173 }
175 this.submitUrl = pyroutes.url(pyurl,
174 this.submitUrl = pyroutes.url(pyurl,
176 {'repo_name': templateContext.repo_name,
175 {'repo_name': templateContext.repo_name,
177 'commit_id': this.commitId,
176 'commit_id': this.commitId,
178 'comment_id': comment_id});
177 'comment_id': comment_id});
179 this.selfUrl = pyroutes.url('repo_commit',
178 this.selfUrl = pyroutes.url('repo_commit',
180 {'repo_name': templateContext.repo_name,
179 {'repo_name': templateContext.repo_name,
181 'commit_id': this.commitId});
180 'commit_id': this.commitId});
182
181
183 } else if (this.pullRequestId) {
182 } else if (this.pullRequestId) {
184 var pyurl = 'pullrequest_comment_create';
183 var pyurl = 'pullrequest_comment_create';
185 if(edit){
184 if(edit){
186 pyurl = 'pullrequest_comment_edit';
185 pyurl = 'pullrequest_comment_edit';
187 }
186 }
188 this.submitUrl = pyroutes.url(pyurl,
187 this.submitUrl = pyroutes.url(pyurl,
189 {'repo_name': templateContext.repo_name,
188 {'repo_name': templateContext.repo_name,
190 'pull_request_id': this.pullRequestId,
189 'pull_request_id': this.pullRequestId,
191 'comment_id': comment_id});
190 'comment_id': comment_id});
192 this.selfUrl = pyroutes.url('pullrequest_show',
191 this.selfUrl = pyroutes.url('pullrequest_show',
193 {'repo_name': templateContext.repo_name,
192 {'repo_name': templateContext.repo_name,
194 'pull_request_id': this.pullRequestId});
193 'pull_request_id': this.pullRequestId});
195
194
196 } else {
195 } else {
197 throw new Error(
196 throw new Error(
198 'CommentForm requires pullRequestId, or commitId to be specified.')
197 'CommentForm requires pullRequestId, or commitId to be specified.')
199 }
198 }
200
199
201 // FUNCTIONS and helpers
200 // FUNCTIONS and helpers
202 var self = this;
201 var self = this;
203
202
204 this.isInline = function(){
203 this.isInline = function(){
205 return this.lineNo && this.lineNo != 'general';
204 return this.lineNo && this.lineNo != 'general';
206 };
205 };
207
206
208 this.getCmInstance = function(){
207 this.getCmInstance = function(){
209 return this.cm
208 return this.cm
210 };
209 };
211
210
212 this.setPlaceholder = function(placeholder) {
211 this.setPlaceholder = function(placeholder) {
213 var cm = this.getCmInstance();
212 var cm = this.getCmInstance();
214 if (cm){
213 if (cm){
215 cm.setOption('placeholder', placeholder);
214 cm.setOption('placeholder', placeholder);
216 }
215 }
217 };
216 };
218
217
219 this.getCommentStatus = function() {
218 this.getCommentStatus = function() {
220 return $(this.submitForm).find(this.statusChange).val();
219 return $(this.submitForm).find(this.statusChange).val();
221 };
220 };
222
221
223 this.getCommentType = function() {
222 this.getCommentType = function() {
224 return $(this.submitForm).find(this.commentType).val();
223 return $(this.submitForm).find(this.commentType).val();
225 };
224 };
226
225
227 this.getDraftState = function () {
226 this.getDraftState = function () {
228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
227 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
229 var data = $(submitterElem).data('isDraft');
228 var data = $(submitterElem).data('isDraft');
230 return data
229 return data
231 }
230 }
232
231
233 this.getResolvesId = function() {
232 this.getResolvesId = function() {
234 return $(this.submitForm).find(this.resolvesId).val() || null;
233 return $(this.submitForm).find(this.resolvesId).val() || null;
235 };
234 };
236
235
237 this.getClosePr = function() {
236 this.getClosePr = function() {
238 return $(this.submitForm).find(this.closesPr).val() || null;
237 return $(this.submitForm).find(this.closesPr).val() || null;
239 };
238 };
240
239
241 this.markCommentResolved = function(resolvedCommentId){
240 this.markCommentResolved = function(resolvedCommentId){
242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
241 Rhodecode.comments.markCommentResolved(resolvedCommentId)
243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
244 };
242 };
245
243
246 this.isAllowedToSubmit = function() {
244 this.isAllowedToSubmit = function() {
247 var commentDisabled = $(this.submitButton).prop('disabled');
245 var commentDisabled = $(this.submitButton).prop('disabled');
248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
246 var draftDisabled = $(this.submitDraftButton).prop('disabled');
249 return !commentDisabled && !draftDisabled;
247 return !commentDisabled && !draftDisabled;
250 };
248 };
251
249
252 this.initStatusChangeSelector = function(){
250 this.initStatusChangeSelector = function(){
253 var formatChangeStatus = function(state, escapeMarkup) {
251 var formatChangeStatus = function(state, escapeMarkup) {
254 var originalOption = state.element;
252 var originalOption = state.element;
255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
253 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
256 return tmpl
254 return tmpl
257 };
255 };
258 var formatResult = function(result, container, query, escapeMarkup) {
256 var formatResult = function(result, container, query, escapeMarkup) {
259 return formatChangeStatus(result, escapeMarkup);
257 return formatChangeStatus(result, escapeMarkup);
260 };
258 };
261
259
262 var formatSelection = function(data, container, escapeMarkup) {
260 var formatSelection = function(data, container, escapeMarkup) {
263 return formatChangeStatus(data, escapeMarkup);
261 return formatChangeStatus(data, escapeMarkup);
264 };
262 };
265
263
266 $(this.submitForm).find(this.statusChange).select2({
264 $(this.submitForm).find(this.statusChange).select2({
267 placeholder: _gettext('Status Review'),
265 placeholder: _gettext('Status Review'),
268 formatResult: formatResult,
266 formatResult: formatResult,
269 formatSelection: formatSelection,
267 formatSelection: formatSelection,
270 containerCssClass: "drop-menu status_box_menu",
268 containerCssClass: "drop-menu status_box_menu",
271 dropdownCssClass: "drop-menu-dropdown",
269 dropdownCssClass: "drop-menu-dropdown",
272 dropdownAutoWidth: true,
270 dropdownAutoWidth: true,
273 minimumResultsForSearch: -1
271 minimumResultsForSearch: -1
274 });
272 });
275
273
276 $(this.submitForm).find(this.statusChange).on('change', function() {
274 $(this.submitForm).find(this.statusChange).on('change', function() {
277 var status = self.getCommentStatus();
275 var status = self.getCommentStatus();
278
276
279 if (status && !self.isInline()) {
277 if (status && !self.isInline()) {
280 $(self.submitButton).prop('disabled', false);
278 $(self.submitButton).prop('disabled', false);
281 $(self.submitDraftButton).prop('disabled', false);
279 $(self.submitDraftButton).prop('disabled', false);
282 }
280 }
283
281
284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
282 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
285 self.setPlaceholder(placeholderText)
283 self.setPlaceholder(placeholderText)
286 })
284 })
287 };
285 };
288
286
289 // reset the comment form into it's original state
287 // reset the comment form into it's original state
290 this.resetCommentFormState = function(content) {
288 this.resetCommentFormState = function(content) {
291 content = content || '';
289 content = content || '';
292
290
293 $(this.editContainer).show();
291 $(this.editContainer).show();
294 $(this.editButton).parent().addClass('active');
292 $(this.editButton).parent().addClass('active');
295
293
296 $(this.previewContainer).hide();
294 $(this.previewContainer).hide();
297 $(this.previewButton).parent().removeClass('active');
295 $(this.previewButton).parent().removeClass('active');
298
296
299 this.setActionButtonsDisabled(true);
297 this.setActionButtonsDisabled(true);
300 self.cm.setValue(content);
298 self.cm.setValue(content);
301 self.cm.setOption("readOnly", false);
299 self.cm.setOption("readOnly", false);
302
300
303 if (this.resolvesId) {
301 if (this.resolvesId) {
304 // destroy the resolve action
302 // destroy the resolve action
305 $(this.resolvesId).parent().remove();
303 $(this.resolvesId).parent().remove();
306 }
304 }
307 // reset closingPR flag
305 // reset closingPR flag
308 $('.close-pr-input').remove();
306 $('.close-pr-input').remove();
309
307
310 $(this.statusChange).select2('readonly', false);
308 $(this.statusChange).select2('readonly', false);
311 };
309 };
312
310
313 this.globalSubmitSuccessCallback = function(comment){
311 this.globalSubmitSuccessCallback = function(comment){
314 // default behaviour is to call GLOBAL hook, if it's registered.
312 // default behaviour is to call GLOBAL hook, if it's registered.
315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
313 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
316 commentFormGlobalSubmitSuccessCallback(comment);
314 commentFormGlobalSubmitSuccessCallback(comment);
317 }
315 }
318 };
316 };
319
317
320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
318 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
319 return _submitAjaxPOST(url, postData, successHandler, failHandler);
322 };
320 };
323
321
324 // overwrite a submitHandler, we need to do it for inline comments
322 // overwrite a submitHandler, we need to do it for inline comments
325 this.setHandleFormSubmit = function(callback) {
323 this.setHandleFormSubmit = function(callback) {
326 this.handleFormSubmit = callback;
324 this.handleFormSubmit = callback;
327 };
325 };
328
326
329 // overwrite a submitSuccessHandler
327 // overwrite a submitSuccessHandler
330 this.setGlobalSubmitSuccessCallback = function(callback) {
328 this.setGlobalSubmitSuccessCallback = function(callback) {
331 this.globalSubmitSuccessCallback = callback;
329 this.globalSubmitSuccessCallback = callback;
332 };
330 };
333
331
334 // default handler for for submit for main comments
332 // default handler for for submit for main comments
335 this.handleFormSubmit = function() {
333 this.handleFormSubmit = function() {
336 var text = self.cm.getValue();
334 var text = self.cm.getValue();
337 var status = self.getCommentStatus();
335 var status = self.getCommentStatus();
338 var commentType = self.getCommentType();
336 var commentType = self.getCommentType();
339 var isDraft = self.getDraftState();
337 var isDraft = self.getDraftState();
340 var resolvesCommentId = self.getResolvesId();
338 var resolvesCommentId = self.getResolvesId();
341 var closePullRequest = self.getClosePr();
339 var closePullRequest = self.getClosePr();
342
340
343 if (text === "" && !status) {
341 if (text === "" && !status) {
344 return;
342 return;
345 }
343 }
346
344
347 var excludeCancelBtn = false;
345 var excludeCancelBtn = false;
348 var submitEvent = true;
346 var submitEvent = true;
349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
347 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
350 self.cm.setOption("readOnly", true);
348 self.cm.setOption("readOnly", true);
351
349
352 var postData = {
350 var postData = {
353 'text': text,
351 'text': text,
354 'changeset_status': status,
352 'changeset_status': status,
355 'comment_type': commentType,
353 'comment_type': commentType,
356 'csrf_token': CSRF_TOKEN
354 'csrf_token': CSRF_TOKEN
357 };
355 };
358
356
359 if (resolvesCommentId) {
357 if (resolvesCommentId) {
360 postData['resolves_comment_id'] = resolvesCommentId;
358 postData['resolves_comment_id'] = resolvesCommentId;
361 }
359 }
362
360
363 if (closePullRequest) {
361 if (closePullRequest) {
364 postData['close_pull_request'] = true;
362 postData['close_pull_request'] = true;
365 }
363 }
366
364
367 // submitSuccess for general comments
365 // submitSuccess for general comments
368 var submitSuccessCallback = function(json_data) {
366 var submitSuccessCallback = function(json_data) {
369 // reload page if we change status for single commit.
367 // reload page if we change status for single commit.
370 if (status && self.commitId) {
368 if (status && self.commitId) {
371 location.reload(true);
369 location.reload(true);
372 } else {
370 } else {
373 // inject newly created comments, json_data is {<comment_id>: {}}
371 // inject newly created comments, json_data is {<comment_id>: {}}
374 Rhodecode.comments.attachGeneralComment(json_data)
372 Rhodecode.comments.attachGeneralComment(json_data)
375
373
376 self.resetCommentFormState();
374 self.resetCommentFormState();
377 timeagoActivate();
375 timeagoActivate();
378 tooltipActivate();
376 tooltipActivate();
379
377
380 // mark visually which comment was resolved
378 // mark visually which comment was resolved
381 if (resolvesCommentId) {
379 if (resolvesCommentId) {
382 self.markCommentResolved(resolvesCommentId);
380 self.markCommentResolved(resolvesCommentId);
383 }
381 }
384 }
382 }
385
383
386 // run global callback on submit
384 // run global callback on submit
387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
385 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
388
386
389 };
387 };
390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
388 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
391 var prefix = "Error while submitting comment.\n"
389 var prefix = "Error while submitting comment.\n"
392 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
390 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
393 ajaxErrorSwal(message);
391 ajaxErrorSwal(message);
394 self.resetCommentFormState(text);
392 self.resetCommentFormState(text);
395 };
393 };
396 self.submitAjaxPOST(
394 self.submitAjaxPOST(
397 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
395 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
398 };
396 };
399
397
400 this.previewSuccessCallback = function(o) {
398 this.previewSuccessCallback = function(o) {
401 $(self.previewBoxSelector).html(o);
399 $(self.previewBoxSelector).html(o);
402 $(self.previewBoxSelector).removeClass('unloaded');
400 $(self.previewBoxSelector).removeClass('unloaded');
403
401
404 // swap buttons, making preview active
402 // swap buttons, making preview active
405 $(self.previewButton).parent().addClass('active');
403 $(self.previewButton).parent().addClass('active');
406 $(self.editButton).parent().removeClass('active');
404 $(self.editButton).parent().removeClass('active');
407
405
408 // unlock buttons
406 // unlock buttons
409 self.setActionButtonsDisabled(false);
407 self.setActionButtonsDisabled(false);
410 };
408 };
411
409
412 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
410 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
413 excludeCancelBtn = excludeCancelBtn || false;
411 excludeCancelBtn = excludeCancelBtn || false;
414 submitEvent = submitEvent || false;
412 submitEvent = submitEvent || false;
415
413
416 $(this.editButton).prop('disabled', state);
414 $(this.editButton).prop('disabled', state);
417 $(this.previewButton).prop('disabled', state);
415 $(this.previewButton).prop('disabled', state);
418
416
419 if (!excludeCancelBtn) {
417 if (!excludeCancelBtn) {
420 $(this.cancelButton).prop('disabled', state);
418 $(this.cancelButton).prop('disabled', state);
421 }
419 }
422
420
423 var submitState = state;
421 var submitState = state;
424 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
422 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
425 // if the value of commit review status is set, we allow
423 // if the value of commit review status is set, we allow
426 // submit button, but only on Main form, isInline means inline
424 // submit button, but only on Main form, isInline means inline
427 submitState = false
425 submitState = false
428 }
426 }
429
427
430 $(this.submitButton).prop('disabled', submitState);
428 $(this.submitButton).prop('disabled', submitState);
431 $(this.submitDraftButton).prop('disabled', submitState);
429 $(this.submitDraftButton).prop('disabled', submitState);
432
430
433 if (submitEvent) {
431 if (submitEvent) {
434 var isDraft = self.getDraftState();
432 var isDraft = self.getDraftState();
435
433
436 if (isDraft) {
434 if (isDraft) {
437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
435 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
438 } else {
436 } else {
439 $(this.submitButton).val(_gettext('Submitting...'));
437 $(this.submitButton).val(_gettext('Submitting...'));
440 }
438 }
441
439
442 } else {
440 } else {
443 $(this.submitButton).val(this.submitButtonText);
441 $(this.submitButton).val(this.submitButtonText);
444 $(this.submitDraftButton).val(this.submitDraftButtonText);
442 $(this.submitDraftButton).val(this.submitDraftButtonText);
445 }
443 }
446
444
447 };
445 };
448
446
449 // lock preview/edit/submit buttons on load, but exclude cancel button
447 // lock preview/edit/submit buttons on load, but exclude cancel button
450 var excludeCancelBtn = true;
448 var excludeCancelBtn = true;
451 this.setActionButtonsDisabled(true, excludeCancelBtn);
449 this.setActionButtonsDisabled(true, excludeCancelBtn);
452
450
453 // anonymous users don't have access to initialized CM instance
451 // anonymous users don't have access to initialized CM instance
454 if (this.cm !== undefined){
452 if (this.cm !== undefined){
455 this.cm.on('change', function(cMirror) {
453 this.cm.on('change', function(cMirror) {
456 if (cMirror.getValue() === "") {
454 if (cMirror.getValue() === "") {
457 self.setActionButtonsDisabled(true, excludeCancelBtn)
455 self.setActionButtonsDisabled(true, excludeCancelBtn)
458 } else {
456 } else {
459 self.setActionButtonsDisabled(false, excludeCancelBtn)
457 self.setActionButtonsDisabled(false, excludeCancelBtn)
460 }
458 }
461 });
459 });
462 }
460 }
463
461
464 $(this.editButton).on('click', function(e) {
462 $(this.editButton).on('click', function(e) {
465 e.preventDefault();
463 e.preventDefault();
466
464
467 $(self.previewButton).parent().removeClass('active');
465 $(self.previewButton).parent().removeClass('active');
468 $(self.previewContainer).hide();
466 $(self.previewContainer).hide();
469
467
470 $(self.editButton).parent().addClass('active');
468 $(self.editButton).parent().addClass('active');
471 $(self.editContainer).show();
469 $(self.editContainer).show();
472
470
473 });
471 });
474
472
475 $(this.previewButton).on('click', function(e) {
473 $(this.previewButton).on('click', function(e) {
476 e.preventDefault();
474 e.preventDefault();
477 var text = self.cm.getValue();
475 var text = self.cm.getValue();
478
476
479 if (text === "") {
477 if (text === "") {
480 return;
478 return;
481 }
479 }
482
480
483 var postData = {
481 var postData = {
484 'text': text,
482 'text': text,
485 'renderer': templateContext.visual.default_renderer,
483 'renderer': templateContext.visual.default_renderer,
486 'csrf_token': CSRF_TOKEN
484 'csrf_token': CSRF_TOKEN
487 };
485 };
488
486
489 // lock ALL buttons on preview
487 // lock ALL buttons on preview
490 self.setActionButtonsDisabled(true);
488 self.setActionButtonsDisabled(true);
491
489
492 $(self.previewBoxSelector).addClass('unloaded');
490 $(self.previewBoxSelector).addClass('unloaded');
493 $(self.previewBoxSelector).html(_gettext('Loading ...'));
491 $(self.previewBoxSelector).html(_gettext('Loading ...'));
494
492
495 $(self.editContainer).hide();
493 $(self.editContainer).hide();
496 $(self.previewContainer).show();
494 $(self.previewContainer).show();
497
495
498 // by default we reset state of comment preserving the text
496 // by default we reset state of comment preserving the text
499 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
497 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
500 var prefix = "Error while preview of comment.\n"
498 var prefix = "Error while preview of comment.\n"
501 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
499 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
502 ajaxErrorSwal(message);
500 ajaxErrorSwal(message);
503
501
504 self.resetCommentFormState(text)
502 self.resetCommentFormState(text)
505 };
503 };
506 self.submitAjaxPOST(
504 self.submitAjaxPOST(
507 self.previewUrl, postData, self.previewSuccessCallback,
505 self.previewUrl, postData, self.previewSuccessCallback,
508 previewFailCallback);
506 previewFailCallback);
509
507
510 $(self.previewButton).parent().addClass('active');
508 $(self.previewButton).parent().addClass('active');
511 $(self.editButton).parent().removeClass('active');
509 $(self.editButton).parent().removeClass('active');
512 });
510 });
513
511
514 $(this.submitForm).submit(function(e) {
512 $(this.submitForm).submit(function(e) {
515 e.preventDefault();
513 e.preventDefault();
516 var allowedToSubmit = self.isAllowedToSubmit();
514 var allowedToSubmit = self.isAllowedToSubmit();
517 if (!allowedToSubmit){
515 if (!allowedToSubmit){
518 return false;
516 return false;
519 }
517 }
520
518
521 self.handleFormSubmit();
519 self.handleFormSubmit();
522 });
520 });
523
521
524 }
522 }
525
523
526 return CommentForm;
524 return CommentForm;
527 });
525 });
528
526
529 /* selector for comment versions */
527 /* selector for comment versions */
530 var initVersionSelector = function(selector, initialData) {
528 var initVersionSelector = function(selector, initialData) {
531
529
532 var formatResult = function(result, container, query, escapeMarkup) {
530 var formatResult = function(result, container, query, escapeMarkup) {
533
531
534 return renderTemplate('commentVersion', {
532 return renderTemplate('commentVersion', {
535 show_disabled: true,
533 show_disabled: true,
536 version: result.comment_version,
534 version: result.comment_version,
537 user_name: result.comment_author_username,
535 user_name: result.comment_author_username,
538 gravatar_url: result.comment_author_gravatar,
536 gravatar_url: result.comment_author_gravatar,
539 size: 16,
537 size: 16,
540 timeago_component: result.comment_created_on,
538 timeago_component: result.comment_created_on,
541 })
539 })
542 };
540 };
543
541
544 $(selector).select2({
542 $(selector).select2({
545 placeholder: "Edited",
543 placeholder: "Edited",
546 containerCssClass: "drop-menu-comment-history",
544 containerCssClass: "drop-menu-comment-history",
547 dropdownCssClass: "drop-menu-dropdown",
545 dropdownCssClass: "drop-menu-dropdown",
548 dropdownAutoWidth: true,
546 dropdownAutoWidth: true,
549 minimumResultsForSearch: -1,
547 minimumResultsForSearch: -1,
550 data: initialData,
548 data: initialData,
551 formatResult: formatResult,
549 formatResult: formatResult,
552 });
550 });
553
551
554 $(selector).on('select2-selecting', function (e) {
552 $(selector).on('select2-selecting', function (e) {
555 // hide the mast as we later do preventDefault()
553 // hide the mast as we later do preventDefault()
556 $("#select2-drop-mask").click();
554 $("#select2-drop-mask").click();
557 e.preventDefault();
555 e.preventDefault();
558 e.choice.action();
556 e.choice.action();
559 });
557 });
560
558
561 $(selector).on("select2-open", function() {
559 $(selector).on("select2-open", function() {
562 timeagoActivate();
560 timeagoActivate();
563 });
561 });
564 };
562 };
565
563
566 /* comments controller */
564 /* comments controller */
567 var CommentsController = function() {
565 var CommentsController = function() {
568 var mainComment = '#text';
566 var mainComment = '#text';
569 var self = this;
567 var self = this;
570
568
571 this.showVersion = function (comment_id, comment_history_id) {
569 this.showVersion = function (comment_id, comment_history_id) {
572
570
573 var historyViewUrl = pyroutes.url(
571 var historyViewUrl = pyroutes.url(
574 'repo_commit_comment_history_view',
572 'repo_commit_comment_history_view',
575 {
573 {
576 'repo_name': templateContext.repo_name,
574 'repo_name': templateContext.repo_name,
577 'commit_id': comment_id,
575 'commit_id': comment_id,
578 'comment_history_id': comment_history_id,
576 'comment_history_id': comment_history_id,
579 }
577 }
580 );
578 );
581 successRenderCommit = function (data) {
579 successRenderCommit = function (data) {
582 SwalNoAnimation.fire({
580 SwalNoAnimation.fire({
583 html: data,
581 html: data,
584 title: '',
582 title: '',
585 });
583 });
586 };
584 };
587 failRenderCommit = function () {
585 failRenderCommit = function () {
588 SwalNoAnimation.fire({
586 SwalNoAnimation.fire({
589 html: 'Error while loading comment history',
587 html: 'Error while loading comment history',
590 title: '',
588 title: '',
591 });
589 });
592 };
590 };
593 _submitAjaxPOST(
591 _submitAjaxPOST(
594 historyViewUrl, {'csrf_token': CSRF_TOKEN},
592 historyViewUrl, {'csrf_token': CSRF_TOKEN},
595 successRenderCommit,
593 successRenderCommit,
596 failRenderCommit
594 failRenderCommit
597 );
595 );
598 };
596 };
599
597
600 this.getLineNumber = function(node) {
598 this.getLineNumber = function(node) {
601 var $node = $(node);
599 var $node = $(node);
602 var lineNo = $node.closest('td').attr('data-line-no');
600 var lineNo = $node.closest('td').attr('data-line-no');
603 if (lineNo === undefined && $node.data('commentInline')){
601 if (lineNo === undefined && $node.data('commentInline')){
604 lineNo = $node.data('commentLineNo')
602 lineNo = $node.data('commentLineNo')
605 }
603 }
606
604
607 return lineNo
605 return lineNo
608 };
606 };
609
607
610 this.scrollToComment = function(node, offset, outdated) {
608 this.scrollToComment = function(node, offset, outdated) {
611 if (offset === undefined) {
609 if (offset === undefined) {
612 offset = 0;
610 offset = 0;
613 }
611 }
614 var outdated = outdated || false;
612 var outdated = outdated || false;
615 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
613 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
616
614
617 if (!node) {
615 if (!node) {
618 node = $('.comment-selected');
616 node = $('.comment-selected');
619 if (!node.length) {
617 if (!node.length) {
620 node = $('comment-current')
618 node = $('comment-current')
621 }
619 }
622 }
620 }
623
621
624 $wrapper = $(node).closest('div.comment');
622 $wrapper = $(node).closest('div.comment');
625
623
626 // show hidden comment when referenced.
624 // show hidden comment when referenced.
627 if (!$wrapper.is(':visible')){
625 if (!$wrapper.is(':visible')){
628 $wrapper.show();
626 $wrapper.show();
629 }
627 }
630
628
631 $comment = $(node).closest(klass);
629 $comment = $(node).closest(klass);
632 $comments = $(klass);
630 $comments = $(klass);
633
631
634 $('.comment-selected').removeClass('comment-selected');
632 $('.comment-selected').removeClass('comment-selected');
635
633
636 var nextIdx = $(klass).index($comment) + offset;
634 var nextIdx = $(klass).index($comment) + offset;
637 if (nextIdx >= $comments.length) {
635 if (nextIdx >= $comments.length) {
638 nextIdx = 0;
636 nextIdx = 0;
639 }
637 }
640 var $next = $(klass).eq(nextIdx);
638 var $next = $(klass).eq(nextIdx);
641
639
642 var $cb = $next.closest('.cb');
640 var $cb = $next.closest('.cb');
643 $cb.removeClass('cb-collapsed');
641 $cb.removeClass('cb-collapsed');
644
642
645 var $filediffCollapseState = $cb.closest('.filediff').prev();
643 var $filediffCollapseState = $cb.closest('.filediff').prev();
646 $filediffCollapseState.prop('checked', false);
644 $filediffCollapseState.prop('checked', false);
647 $next.addClass('comment-selected');
645 $next.addClass('comment-selected');
648 scrollToElement($next);
646 scrollToElement($next);
649 return false;
647 return false;
650 };
648 };
651
649
652 this.nextComment = function(node) {
650 this.nextComment = function(node) {
653 return self.scrollToComment(node, 1);
651 return self.scrollToComment(node, 1);
654 };
652 };
655
653
656 this.prevComment = function(node) {
654 this.prevComment = function(node) {
657 return self.scrollToComment(node, -1);
655 return self.scrollToComment(node, -1);
658 };
656 };
659
657
660 this.nextOutdatedComment = function(node) {
658 this.nextOutdatedComment = function(node) {
661 return self.scrollToComment(node, 1, true);
659 return self.scrollToComment(node, 1, true);
662 };
660 };
663
661
664 this.prevOutdatedComment = function(node) {
662 this.prevOutdatedComment = function(node) {
665 return self.scrollToComment(node, -1, true);
663 return self.scrollToComment(node, -1, true);
666 };
664 };
667
665
668 this.cancelComment = function (node) {
666 this.cancelComment = function (node) {
669 var $node = $(node);
667 var $node = $(node);
670 var edit = $(this).attr('edit');
668 var edit = $(this).attr('edit');
671 var $inlineComments = $node.closest('div.inline-comments');
669 var $inlineComments = $node.closest('div.inline-comments');
672
670
673 if (edit) {
671 if (edit) {
674 var $general_comments = null;
672 var $general_comments = null;
675 if (!$inlineComments.length) {
673 if (!$inlineComments.length) {
676 $general_comments = $('#comments');
674 $general_comments = $('#comments');
677 var $comment = $general_comments.parent().find('div.comment:hidden');
675 var $comment = $general_comments.parent().find('div.comment:hidden');
678 // show hidden general comment form
676 // show hidden general comment form
679 $('#cb-comment-general-form-placeholder').show();
677 $('#cb-comment-general-form-placeholder').show();
680 } else {
678 } else {
681 var $comment = $inlineComments.find('div.comment:hidden');
679 var $comment = $inlineComments.find('div.comment:hidden');
682 }
680 }
683 $comment.show();
681 $comment.show();
684 }
682 }
685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
683 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
686 $replyWrapper.removeClass('comment-form-active');
684 $replyWrapper.removeClass('comment-form-active');
687
685
688 var lastComment = $inlineComments.find('.comment-inline').last();
686 var lastComment = $inlineComments.find('.comment-inline').last();
689 if ($(lastComment).hasClass('comment-outdated')) {
687 if ($(lastComment).hasClass('comment-outdated')) {
690 $replyWrapper.hide();
688 $replyWrapper.hide();
691 }
689 }
692
690
693 $node.closest('.comment-inline-form').remove();
691 $node.closest('.comment-inline-form').remove();
694 return false;
692 return false;
695 };
693 };
696
694
697 this._deleteComment = function(node) {
695 this._deleteComment = function(node) {
698 var $node = $(node);
696 var $node = $(node);
699 var $td = $node.closest('td');
697 var $td = $node.closest('td');
700 var $comment = $node.closest('.comment');
698 var $comment = $node.closest('.comment');
701 var comment_id = $($comment).data('commentId');
699 var comment_id = $($comment).data('commentId');
702 var isDraft = $($comment).data('commentDraft');
700 var isDraft = $($comment).data('commentDraft');
703
701
704 var pullRequestId = templateContext.pull_request_data.pull_request_id;
702 var pullRequestId = templateContext.pull_request_data.pull_request_id;
705 var commitId = templateContext.commit_data.commit_id;
703 var commitId = templateContext.commit_data.commit_id;
706
704
707 if (pullRequestId) {
705 if (pullRequestId) {
708 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
706 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
709 } else if (commitId) {
707 } else if (commitId) {
710 var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
708 var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
711 }
709 }
712
710
713 var postData = {
711 var postData = {
714 'csrf_token': CSRF_TOKEN
712 'csrf_token': CSRF_TOKEN
715 };
713 };
716
714
717 $comment.addClass('comment-deleting');
715 $comment.addClass('comment-deleting');
718 $comment.hide('fast');
716 $comment.hide('fast');
719
717
720 var success = function(response) {
718 var success = function(response) {
721 $comment.remove();
719 $comment.remove();
722
720
723 if (window.updateSticky !== undefined) {
721 if (window.updateSticky !== undefined) {
724 // potentially our comments change the active window size, so we
722 // potentially our comments change the active window size, so we
725 // notify sticky elements
723 // notify sticky elements
726 updateSticky()
724 updateSticky()
727 }
725 }
728
726
729 if (window.refreshAllComments !== undefined && !isDraft) {
727 if (window.refreshAllComments !== undefined && !isDraft) {
730 // if we have this handler, run it, and refresh all comments boxes
728 // if we have this handler, run it, and refresh all comments boxes
731 refreshAllComments()
729 refreshAllComments()
732 }
730 }
733 else if (window.refreshDraftComments !== undefined && isDraft) {
731 else if (window.refreshDraftComments !== undefined && isDraft) {
734 // if we have this handler, run it, and refresh all comments boxes
732 // if we have this handler, run it, and refresh all comments boxes
735 refreshDraftComments();
733 refreshDraftComments();
736 }
734 }
737 return false;
735 return false;
738 };
736 };
739
737
740 var failure = function(jqXHR, textStatus, errorThrown) {
738 var failure = function(jqXHR, textStatus, errorThrown) {
741 var prefix = "Error while deleting this comment.\n"
739 var prefix = "Error while deleting this comment.\n"
742 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
740 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
743 ajaxErrorSwal(message);
741 ajaxErrorSwal(message);
744
742
745 $comment.show('fast');
743 $comment.show('fast');
746 $comment.removeClass('comment-deleting');
744 $comment.removeClass('comment-deleting');
747 return false;
745 return false;
748 };
746 };
749 ajaxPOST(url, postData, success, failure);
747 ajaxPOST(url, postData, success, failure);
750
748
751 }
749 }
752
750
753 this.deleteComment = function(node) {
751 this.deleteComment = function(node) {
754 var $comment = $(node).closest('.comment');
752 var $comment = $(node).closest('.comment');
755 var comment_id = $comment.attr('data-comment-id');
753 var comment_id = $comment.attr('data-comment-id');
756
754
757 SwalNoAnimation.fire({
755 SwalNoAnimation.fire({
758 title: 'Delete this comment?',
756 title: 'Delete this comment?',
759 icon: 'warning',
757 icon: 'warning',
760 showCancelButton: true,
758 showCancelButton: true,
761 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
759 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
762
760
763 }).then(function(result) {
761 }).then(function(result) {
764 if (result.value) {
762 if (result.value) {
765 self._deleteComment(node);
763 self._deleteComment(node);
766 }
764 }
767 })
765 })
768 };
766 };
769
767
770 this._finalizeDrafts = function(commentIds) {
768 this._finalizeDrafts = function(commentIds) {
771
769
772 var pullRequestId = templateContext.pull_request_data.pull_request_id;
770 var pullRequestId = templateContext.pull_request_data.pull_request_id;
773 var commitId = templateContext.commit_data.commit_id;
771 var commitId = templateContext.commit_data.commit_id;
774
772
775 if (pullRequestId) {
773 if (pullRequestId) {
776 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
774 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
777 } else if (commitId) {
775 } else if (commitId) {
778 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
776 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
779 }
777 }
780
778
781 // remove the drafts so we can lock them before submit.
779 // remove the drafts so we can lock them before submit.
782 $.each(commentIds, function(idx, val){
780 $.each(commentIds, function(idx, val){
783 $('#comment-{0}'.format(val)).remove();
781 $('#comment-{0}'.format(val)).remove();
784 })
782 })
785
783
786 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
784 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
787
785
788 var submitSuccessCallback = function(json_data) {
786 var submitSuccessCallback = function(json_data) {
789 self.attachInlineComment(json_data);
787 self.attachInlineComment(json_data);
790
788
791 if (window.refreshDraftComments !== undefined) {
789 if (window.refreshDraftComments !== undefined) {
792 // if we have this handler, run it, and refresh all comments boxes
790 // if we have this handler, run it, and refresh all comments boxes
793 refreshDraftComments()
791 refreshDraftComments()
794 }
792 }
795
793
796 return false;
794 return false;
797 };
795 };
798
796
799 ajaxPOST(url, postData, submitSuccessCallback)
797 ajaxPOST(url, postData, submitSuccessCallback)
800
798
801 }
799 }
802
800
803 this.finalizeDrafts = function(commentIds, callback) {
801 this.finalizeDrafts = function(commentIds, callback) {
804
802
805 SwalNoAnimation.fire({
803 SwalNoAnimation.fire({
806 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
804 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
807 icon: 'warning',
805 icon: 'warning',
808 showCancelButton: true,
806 showCancelButton: true,
809 confirmButtonText: _gettext('Yes'),
807 confirmButtonText: _gettext('Yes'),
810
808
811 }).then(function(result) {
809 }).then(function(result) {
812 if (result.value) {
810 if (result.value) {
813 if (callback !== undefined) {
811 if (callback !== undefined) {
814 callback(result)
812 callback(result)
815 }
813 }
816 self._finalizeDrafts(commentIds);
814 self._finalizeDrafts(commentIds);
817 }
815 }
818 })
816 })
819 };
817 };
820
818
821 this.toggleWideMode = function (node) {
819 this.toggleWideMode = function (node) {
822
820
823 if ($('#content').hasClass('wrapper')) {
821 if ($('#content').hasClass('wrapper')) {
824 $('#content').removeClass("wrapper");
822 $('#content').removeClass("wrapper");
825 $('#content').addClass("wide-mode-wrapper");
823 $('#content').addClass("wide-mode-wrapper");
826 $(node).addClass('btn-success');
824 $(node).addClass('btn-success');
827 return true
825 return true
828 } else {
826 } else {
829 $('#content').removeClass("wide-mode-wrapper");
827 $('#content').removeClass("wide-mode-wrapper");
830 $('#content').addClass("wrapper");
828 $('#content').addClass("wrapper");
831 $(node).removeClass('btn-success');
829 $(node).removeClass('btn-success');
832 return false
830 return false
833 }
831 }
834
832
835 };
833 };
836
834
837 /**
835 /**
838 * Turn off/on all comments in file diff
836 * Turn off/on all comments in file diff
839 */
837 */
840 this.toggleDiffComments = function(node) {
838 this.toggleDiffComments = function(node) {
841 // Find closes filediff container
839 // Find closes filediff container
842 var $filediff = $(node).closest('.filediff');
840 var $filediff = $(node).closest('.filediff');
843 if ($(node).hasClass('toggle-on')) {
841 if ($(node).hasClass('toggle-on')) {
844 var show = false;
842 var show = false;
845 } else if ($(node).hasClass('toggle-off')) {
843 } else if ($(node).hasClass('toggle-off')) {
846 var show = true;
844 var show = true;
847 }
845 }
848
846
849 // Toggle each individual comment block, so we can un-toggle single ones
847 // Toggle each individual comment block, so we can un-toggle single ones
850 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
848 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
851 self.toggleLineComments($(val), show)
849 self.toggleLineComments($(val), show)
852 })
850 })
853
851
854 // since we change the height of the diff container that has anchor points for upper
852 // since we change the height of the diff container that has anchor points for upper
855 // sticky header, we need to tell it to re-calculate those
853 // sticky header, we need to tell it to re-calculate those
856 if (window.updateSticky !== undefined) {
854 if (window.updateSticky !== undefined) {
857 // potentially our comments change the active window size, so we
855 // potentially our comments change the active window size, so we
858 // notify sticky elements
856 // notify sticky elements
859 updateSticky()
857 updateSticky()
860 }
858 }
861
859
862 return false;
860 return false;
863 }
861 }
864
862
865 this.toggleLineComments = function(node, show) {
863 this.toggleLineComments = function(node, show) {
866
864
867 var trElem = $(node).closest('tr')
865 var trElem = $(node).closest('tr')
868
866
869 if (show === true) {
867 if (show === true) {
870 // mark outdated comments as visible before the toggle;
868 // mark outdated comments as visible before the toggle;
871 $(trElem).find('.comment-outdated').show();
869 $(trElem).find('.comment-outdated').show();
872 $(trElem).removeClass('hide-line-comments');
870 $(trElem).removeClass('hide-line-comments');
873 } else if (show === false) {
871 } else if (show === false) {
874 $(trElem).find('.comment-outdated').hide();
872 $(trElem).find('.comment-outdated').hide();
875 $(trElem).addClass('hide-line-comments');
873 $(trElem).addClass('hide-line-comments');
876 } else {
874 } else {
877 // mark outdated comments as visible before the toggle;
875 // mark outdated comments as visible before the toggle;
878 $(trElem).find('.comment-outdated').show();
876 $(trElem).find('.comment-outdated').show();
879 $(trElem).toggleClass('hide-line-comments');
877 $(trElem).toggleClass('hide-line-comments');
880 }
878 }
881
879
882 // since we change the height of the diff container that has anchor points for upper
880 // since we change the height of the diff container that has anchor points for upper
883 // sticky header, we need to tell it to re-calculate those
881 // sticky header, we need to tell it to re-calculate those
884 if (window.updateSticky !== undefined) {
882 if (window.updateSticky !== undefined) {
885 // potentially our comments change the active window size, so we
883 // potentially our comments change the active window size, so we
886 // notify sticky elements
884 // notify sticky elements
887 updateSticky()
885 updateSticky()
888 }
886 }
889
887
890 };
888 };
891
889
892 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
890 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
893 var pullRequestId = templateContext.pull_request_data.pull_request_id;
891 var pullRequestId = templateContext.pull_request_data.pull_request_id;
894 var commitId = templateContext.commit_data.commit_id;
892 var commitId = templateContext.commit_data.commit_id;
895
893
896 var commentForm = new CommentForm(
894 var commentForm = new CommentForm(
897 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
895 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
898 var cm = commentForm.getCmInstance();
896 var cm = commentForm.getCmInstance();
899
897
900 if (resolvesCommentId){
898 if (resolvesCommentId){
901 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
899 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
902 }
900 }
903
901
904 setTimeout(function() {
902 setTimeout(function() {
905 // callbacks
903 // callbacks
906 if (cm !== undefined) {
904 if (cm !== undefined) {
907 commentForm.setPlaceholder(placeholderText);
905 commentForm.setPlaceholder(placeholderText);
908 if (commentForm.isInline()) {
906 if (commentForm.isInline()) {
909 cm.focus();
907 cm.focus();
910 cm.refresh();
908 cm.refresh();
911 }
909 }
912 }
910 }
913 }, 10);
911 }, 10);
914
912
915 // trigger scrolldown to the resolve comment, since it might be away
913 // trigger scrolldown to the resolve comment, since it might be away
916 // from the clicked
914 // from the clicked
917 if (resolvesCommentId){
915 if (resolvesCommentId){
918 var actionNode = $(commentForm.resolvesActionId).offset();
916 var actionNode = $(commentForm.resolvesActionId).offset();
919
917
920 setTimeout(function() {
918 setTimeout(function() {
921 if (actionNode) {
919 if (actionNode) {
922 $('body, html').animate({scrollTop: actionNode.top}, 10);
920 $('body, html').animate({scrollTop: actionNode.top}, 10);
923 }
921 }
924 }, 100);
922 }, 100);
925 }
923 }
926
924
927 // add dropzone support
925 // add dropzone support
928 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
926 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
929 var renderer = templateContext.visual.default_renderer;
927 var renderer = templateContext.visual.default_renderer;
930 if (renderer == 'rst') {
928 if (renderer == 'rst') {
931 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
929 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
932 if (isRendered){
930 if (isRendered){
933 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
931 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
934 }
932 }
935 } else if (renderer == 'markdown') {
933 } else if (renderer == 'markdown') {
936 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
934 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
937 if (isRendered){
935 if (isRendered){
938 attachmentUrl = '!' + attachmentUrl;
936 attachmentUrl = '!' + attachmentUrl;
939 }
937 }
940 } else {
938 } else {
941 var attachmentUrl = '{}'.format(attachmentStoreUrl);
939 var attachmentUrl = '{}'.format(attachmentStoreUrl);
942 }
940 }
943 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
941 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
944
942
945 return false;
943 return false;
946 };
944 };
947
945
948 //see: https://www.dropzonejs.com/#configuration
946 //see: https://www.dropzonejs.com/#configuration
949 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
947 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
950 {'repo_name': templateContext.repo_name,
948 {'repo_name': templateContext.repo_name,
951 'commit_id': templateContext.commit_data.commit_id})
949 'commit_id': templateContext.commit_data.commit_id})
952
950
953 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
951 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
954 if (previewTmpl !== undefined){
952 if (previewTmpl !== undefined){
955 var selectLink = $(formElement).find('.pick-attachment').get(0);
953 var selectLink = $(formElement).find('.pick-attachment').get(0);
956 $(formElement).find('.comment-attachment-uploader').dropzone({
954 $(formElement).find('.comment-attachment-uploader').dropzone({
957 url: storeUrl,
955 url: storeUrl,
958 headers: {"X-CSRF-Token": CSRF_TOKEN},
956 headers: {"X-CSRF-Token": CSRF_TOKEN},
959 paramName: function () {
957 paramName: function () {
960 return "attachment"
958 return "attachment"
961 }, // The name that will be used to transfer the file
959 }, // The name that will be used to transfer the file
962 clickable: selectLink,
960 clickable: selectLink,
963 parallelUploads: 1,
961 parallelUploads: 1,
964 maxFiles: 10,
962 maxFiles: 10,
965 maxFilesize: templateContext.attachment_store.max_file_size_mb,
963 maxFilesize: templateContext.attachment_store.max_file_size_mb,
966 uploadMultiple: false,
964 uploadMultiple: false,
967 autoProcessQueue: true, // if false queue will not be processed automatically.
965 autoProcessQueue: true, // if false queue will not be processed automatically.
968 createImageThumbnails: false,
966 createImageThumbnails: false,
969 previewTemplate: previewTmpl.innerHTML,
967 previewTemplate: previewTmpl.innerHTML,
970
968
971 accept: function (file, done) {
969 accept: function (file, done) {
972 done();
970 done();
973 },
971 },
974 init: function () {
972 init: function () {
975
973
976 this.on("sending", function (file, xhr, formData) {
974 this.on("sending", function (file, xhr, formData) {
977 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
975 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
978 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
976 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
979 });
977 });
980
978
981 this.on("success", function (file, response) {
979 this.on("success", function (file, response) {
982 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
980 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
983 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
981 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
984
982
985 var isRendered = false;
983 var isRendered = false;
986 var ext = file.name.split('.').pop();
984 var ext = file.name.split('.').pop();
987 var imageExts = templateContext.attachment_store.image_ext;
985 var imageExts = templateContext.attachment_store.image_ext;
988 if (imageExts.indexOf(ext) !== -1){
986 if (imageExts.indexOf(ext) !== -1){
989 isRendered = true;
987 isRendered = true;
990 }
988 }
991
989
992 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
990 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
993 });
991 });
994
992
995 this.on("error", function (file, errorMessage, xhr) {
993 this.on("error", function (file, errorMessage, xhr) {
996 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
994 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
997
995
998 var error = null;
996 var error = null;
999
997
1000 if (xhr !== undefined){
998 if (xhr !== undefined){
1001 var httpStatus = xhr.status + " " + xhr.statusText;
999 var httpStatus = xhr.status + " " + xhr.statusText;
1002 if (xhr !== undefined && xhr.status >= 500) {
1000 if (xhr !== undefined && xhr.status >= 500) {
1003 error = httpStatus;
1001 error = httpStatus;
1004 }
1002 }
1005 }
1003 }
1006
1004
1007 if (error === null) {
1005 if (error === null) {
1008 error = errorMessage.error || errorMessage || httpStatus;
1006 error = errorMessage.error || errorMessage || httpStatus;
1009 }
1007 }
1010 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
1008 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
1011
1009
1012 });
1010 });
1013 }
1011 }
1014 });
1012 });
1015 }
1013 }
1016 return commentForm;
1014 return commentForm;
1017 };
1015 };
1018
1016
1019 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
1017 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
1020
1018
1021 var tmpl = $('#cb-comment-general-form-template').html();
1019 var tmpl = $('#cb-comment-general-form-template').html();
1022 tmpl = tmpl.format(null, 'general');
1020 tmpl = tmpl.format(null, 'general');
1023 var $form = $(tmpl);
1021 var $form = $(tmpl);
1024
1022
1025 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
1023 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
1026 var curForm = $formPlaceholder.find('form');
1024 var curForm = $formPlaceholder.find('form');
1027 if (curForm){
1025 if (curForm){
1028 curForm.remove();
1026 curForm.remove();
1029 }
1027 }
1030 $formPlaceholder.append($form);
1028 $formPlaceholder.append($form);
1031
1029
1032 var _form = $($form[0]);
1030 var _form = $($form[0]);
1033 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
1031 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
1034 var edit = false;
1032 var edit = false;
1035 var comment_id = null;
1033 var comment_id = null;
1036 var commentForm = this.createCommentForm(
1034 var commentForm = this.createCommentForm(
1037 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
1035 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
1038 commentForm.initStatusChangeSelector();
1036 commentForm.initStatusChangeSelector();
1039
1037
1040 return commentForm;
1038 return commentForm;
1041 };
1039 };
1042
1040
1043 this.editComment = function(node, line_no, f_path) {
1041 this.editComment = function(node, line_no, f_path) {
1044 self.edit = true;
1042 self.edit = true;
1045 var $node = $(node);
1043 var $node = $(node);
1046 var $td = $node.closest('td');
1044 var $td = $node.closest('td');
1047
1045
1048 var $comment = $(node).closest('.comment');
1046 var $comment = $(node).closest('.comment');
1049 var comment_id = $($comment).data('commentId');
1047 var comment_id = $($comment).data('commentId');
1050 var isDraft = $($comment).data('commentDraft');
1048 var isDraft = $($comment).data('commentDraft');
1051 var $editForm = null
1049 var $editForm = null
1052
1050
1053 var $comments = $node.closest('div.inline-comments');
1051 var $comments = $node.closest('div.inline-comments');
1054 var $general_comments = null;
1052 var $general_comments = null;
1055
1053
1056 if($comments.length){
1054 if($comments.length){
1057 // inline comments setup
1055 // inline comments setup
1058 $editForm = $comments.find('.comment-inline-form');
1056 $editForm = $comments.find('.comment-inline-form');
1059 line_no = self.getLineNumber(node)
1057 line_no = self.getLineNumber(node)
1060 }
1058 }
1061 else{
1059 else{
1062 // general comments setup
1060 // general comments setup
1063 $comments = $('#comments');
1061 $comments = $('#comments');
1064 $editForm = $comments.find('.comment-inline-form');
1062 $editForm = $comments.find('.comment-inline-form');
1065 line_no = $comment[0].id
1063 line_no = $comment[0].id
1066 $('#cb-comment-general-form-placeholder').hide();
1064 $('#cb-comment-general-form-placeholder').hide();
1067 }
1065 }
1068
1066
1069 if ($editForm.length === 0) {
1067 if ($editForm.length === 0) {
1070
1068
1071 // unhide all comments if they are hidden for a proper REPLY mode
1069 // unhide all comments if they are hidden for a proper REPLY mode
1072 var $filediff = $node.closest('.filediff');
1070 var $filediff = $node.closest('.filediff');
1073 $filediff.removeClass('hide-comments');
1071 $filediff.removeClass('hide-comments');
1074
1072
1075 $editForm = self.createNewFormWrapper(f_path, line_no);
1073 $editForm = self.createNewFormWrapper(f_path, line_no);
1076 if(f_path && line_no) {
1074 if(f_path && line_no) {
1077 $editForm.addClass('comment-inline-form-edit')
1075 $editForm.addClass('comment-inline-form-edit')
1078 }
1076 }
1079
1077
1080 $comment.after($editForm)
1078 $comment.after($editForm)
1081
1079
1082 var _form = $($editForm[0]).find('form');
1080 var _form = $($editForm[0]).find('form');
1083 var autocompleteActions = ['as_note',];
1081 var autocompleteActions = ['as_note',];
1084 var commentForm = this.createCommentForm(
1082 var commentForm = this.createCommentForm(
1085 _form, line_no, '', autocompleteActions, resolvesCommentId,
1083 _form, line_no, '', autocompleteActions, resolvesCommentId,
1086 this.edit, comment_id);
1084 this.edit, comment_id);
1087 var old_comment_text_binary = $comment.attr('data-comment-text');
1085 var old_comment_text_binary = $comment.attr('data-comment-text');
1088 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1086 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1089 commentForm.cm.setValue(old_comment_text);
1087 commentForm.cm.setValue(old_comment_text);
1090 $comment.hide();
1088 $comment.hide();
1091 tooltipActivate();
1089 tooltipActivate();
1092
1090
1093 // set a CUSTOM submit handler for inline comment edit action.
1091 // set a CUSTOM submit handler for inline comment edit action.
1094 commentForm.setHandleFormSubmit(function(o) {
1092 commentForm.setHandleFormSubmit(function(o) {
1095 var text = commentForm.cm.getValue();
1093 var text = commentForm.cm.getValue();
1096 var commentType = commentForm.getCommentType();
1094 var commentType = commentForm.getCommentType();
1097
1095
1098 if (text === "") {
1096 if (text === "") {
1099 return;
1097 return;
1100 }
1098 }
1101
1099
1102 if (old_comment_text == text) {
1100 if (old_comment_text == text) {
1103 SwalNoAnimation.fire({
1101 SwalNoAnimation.fire({
1104 title: 'Unable to edit comment',
1102 title: 'Unable to edit comment',
1105 html: _gettext('Comment body was not changed.'),
1103 html: _gettext('Comment body was not changed.'),
1106 });
1104 });
1107 return;
1105 return;
1108 }
1106 }
1109 var excludeCancelBtn = false;
1107 var excludeCancelBtn = false;
1110 var submitEvent = true;
1108 var submitEvent = true;
1111 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1109 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1112 commentForm.cm.setOption("readOnly", true);
1110 commentForm.cm.setOption("readOnly", true);
1113
1111
1114 // Read last version known
1112 // Read last version known
1115 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1113 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1116 var version = versionSelector.data('lastVersion');
1114 var version = versionSelector.data('lastVersion');
1117
1115
1118 if (!version) {
1116 if (!version) {
1119 version = 0;
1117 version = 0;
1120 }
1118 }
1121
1119
1122 var postData = {
1120 var postData = {
1123 'text': text,
1121 'text': text,
1124 'f_path': f_path,
1122 'f_path': f_path,
1125 'line': line_no,
1123 'line': line_no,
1126 'comment_type': commentType,
1124 'comment_type': commentType,
1127 'draft': isDraft,
1125 'draft': isDraft,
1128 'version': version,
1126 'version': version,
1129 'csrf_token': CSRF_TOKEN
1127 'csrf_token': CSRF_TOKEN
1130 };
1128 };
1131
1129
1132 var submitSuccessCallback = function(json_data) {
1130 var submitSuccessCallback = function(json_data) {
1133 $editForm.remove();
1131 $editForm.remove();
1134 $comment.show();
1132 $comment.show();
1135 var postData = {
1133 var postData = {
1136 'text': text,
1134 'text': text,
1137 'renderer': $comment.attr('data-comment-renderer'),
1135 'renderer': $comment.attr('data-comment-renderer'),
1138 'csrf_token': CSRF_TOKEN
1136 'csrf_token': CSRF_TOKEN
1139 };
1137 };
1140
1138
1141 /* Inject new edited version selector */
1139 /* Inject new edited version selector */
1142 var updateCommentVersionDropDown = function () {
1140 var updateCommentVersionDropDown = function () {
1143 var versionSelectId = '#comment_versions_'+comment_id;
1141 var versionSelectId = '#comment_versions_'+comment_id;
1144 var preLoadVersionData = [
1142 var preLoadVersionData = [
1145 {
1143 {
1146 id: json_data['comment_version'],
1144 id: json_data['comment_version'],
1147 text: "v{0}".format(json_data['comment_version']),
1145 text: "v{0}".format(json_data['comment_version']),
1148 action: function () {
1146 action: function () {
1149 Rhodecode.comments.showVersion(
1147 Rhodecode.comments.showVersion(
1150 json_data['comment_id'],
1148 json_data['comment_id'],
1151 json_data['comment_history_id']
1149 json_data['comment_history_id']
1152 )
1150 )
1153 },
1151 },
1154 comment_version: json_data['comment_version'],
1152 comment_version: json_data['comment_version'],
1155 comment_author_username: json_data['comment_author_username'],
1153 comment_author_username: json_data['comment_author_username'],
1156 comment_author_gravatar: json_data['comment_author_gravatar'],
1154 comment_author_gravatar: json_data['comment_author_gravatar'],
1157 comment_created_on: json_data['comment_created_on'],
1155 comment_created_on: json_data['comment_created_on'],
1158 },
1156 },
1159 ]
1157 ]
1160
1158
1161
1159
1162 if ($(versionSelectId).data('select2')) {
1160 if ($(versionSelectId).data('select2')) {
1163 var oldData = $(versionSelectId).data('select2').opts.data.results;
1161 var oldData = $(versionSelectId).data('select2').opts.data.results;
1164 $(versionSelectId).select2("destroy");
1162 $(versionSelectId).select2("destroy");
1165 preLoadVersionData = oldData.concat(preLoadVersionData)
1163 preLoadVersionData = oldData.concat(preLoadVersionData)
1166 }
1164 }
1167
1165
1168 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1166 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1169
1167
1170 $comment.attr('data-comment-text', utf8ToB64(text));
1168 $comment.attr('data-comment-text', utf8ToB64(text));
1171
1169
1172 var versionSelector = $('#comment_versions_'+comment_id);
1170 var versionSelector = $('#comment_versions_'+comment_id);
1173
1171
1174 // set lastVersion so we know our last edit version
1172 // set lastVersion so we know our last edit version
1175 versionSelector.data('lastVersion', json_data['comment_version'])
1173 versionSelector.data('lastVersion', json_data['comment_version'])
1176 versionSelector.parent().show();
1174 versionSelector.parent().show();
1177 }
1175 }
1178 updateCommentVersionDropDown();
1176 updateCommentVersionDropDown();
1179
1177
1180 // by default we reset state of comment preserving the text
1178 // by default we reset state of comment preserving the text
1181 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1179 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1182 var prefix = "Error while editing this comment.\n"
1180 var prefix = "Error while editing this comment.\n"
1183 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1181 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1184 ajaxErrorSwal(message);
1182 ajaxErrorSwal(message);
1185 };
1183 };
1186
1184
1187 var successRenderCommit = function(o){
1185 var successRenderCommit = function(o){
1188 $comment.show();
1186 $comment.show();
1189 $comment[0].lastElementChild.innerHTML = o;
1187 $comment[0].lastElementChild.innerHTML = o;
1190 };
1188 };
1191
1189
1192 var previewUrl = pyroutes.url(
1190 var previewUrl = pyroutes.url(
1193 'repo_commit_comment_preview',
1191 'repo_commit_comment_preview',
1194 {'repo_name': templateContext.repo_name,
1192 {'repo_name': templateContext.repo_name,
1195 'commit_id': templateContext.commit_data.commit_id});
1193 'commit_id': templateContext.commit_data.commit_id});
1196
1194
1197 _submitAjaxPOST(
1195 _submitAjaxPOST(
1198 previewUrl, postData, successRenderCommit, failRenderCommit
1196 previewUrl, postData, successRenderCommit, failRenderCommit
1199 );
1197 );
1200
1198
1201 try {
1199 try {
1202 var html = json_data.rendered_text;
1200 var html = json_data.rendered_text;
1203 var lineno = json_data.line_no;
1201 var lineno = json_data.line_no;
1204 var target_id = json_data.target_id;
1202 var target_id = json_data.target_id;
1205
1203
1206 $comments.find('.cb-comment-add-button').before(html);
1204 $comments.find('.cb-comment-add-button').before(html);
1207
1205
1208 // run global callback on submit
1206 // run global callback on submit
1209 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1207 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1210
1208
1211 } catch (e) {
1209 } catch (e) {
1212 console.error(e);
1210 console.error(e);
1213 }
1211 }
1214
1212
1215 // re trigger the linkification of next/prev navigation
1213 // re trigger the linkification of next/prev navigation
1216 linkifyComments($('.inline-comment-injected'));
1214 linkifyComments($('.inline-comment-injected'));
1217 timeagoActivate();
1215 timeagoActivate();
1218 tooltipActivate();
1216 tooltipActivate();
1219
1217
1220 if (window.updateSticky !== undefined) {
1218 if (window.updateSticky !== undefined) {
1221 // potentially our comments change the active window size, so we
1219 // potentially our comments change the active window size, so we
1222 // notify sticky elements
1220 // notify sticky elements
1223 updateSticky()
1221 updateSticky()
1224 }
1222 }
1225
1223
1226 if (window.refreshAllComments !== undefined && !isDraft) {
1224 if (window.refreshAllComments !== undefined && !isDraft) {
1227 // if we have this handler, run it, and refresh all comments boxes
1225 // if we have this handler, run it, and refresh all comments boxes
1228 refreshAllComments()
1226 refreshAllComments()
1229 }
1227 }
1230 else if (window.refreshDraftComments !== undefined && isDraft) {
1228 else if (window.refreshDraftComments !== undefined && isDraft) {
1231 // if we have this handler, run it, and refresh all comments boxes
1229 // if we have this handler, run it, and refresh all comments boxes
1232 refreshDraftComments();
1230 refreshDraftComments();
1233 }
1231 }
1234
1232
1235 commentForm.setActionButtonsDisabled(false);
1233 commentForm.setActionButtonsDisabled(false);
1236
1234
1237 };
1235 };
1238
1236
1239 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1237 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1240 var prefix = "Error while editing comment.\n"
1238 var prefix = "Error while editing comment.\n"
1241 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1239 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1242 if (jqXHR.status == 409){
1240 if (jqXHR.status == 409){
1243 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1241 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1244 ajaxErrorSwal(message, 'Comment version mismatch.');
1242 ajaxErrorSwal(message, 'Comment version mismatch.');
1245 } else {
1243 } else {
1246 ajaxErrorSwal(message);
1244 ajaxErrorSwal(message);
1247 }
1245 }
1248
1246
1249 commentForm.resetCommentFormState(text)
1247 commentForm.resetCommentFormState(text)
1250 };
1248 };
1251 commentForm.submitAjaxPOST(
1249 commentForm.submitAjaxPOST(
1252 commentForm.submitUrl, postData,
1250 commentForm.submitUrl, postData,
1253 submitSuccessCallback,
1251 submitSuccessCallback,
1254 submitFailCallback);
1252 submitFailCallback);
1255 });
1253 });
1256 }
1254 }
1257
1255
1258 $editForm.addClass('comment-inline-form-open');
1256 $editForm.addClass('comment-inline-form-open');
1259 };
1257 };
1260
1258
1261 this.attachComment = function(json_data) {
1259 this.attachComment = function(json_data) {
1262 var self = this;
1260 var self = this;
1263 $.each(json_data, function(idx, val) {
1261 $.each(json_data, function(idx, val) {
1264 var json_data_elem = [val]
1262 var json_data_elem = [val]
1265 var isInline = val.comment_f_path && val.comment_lineno
1263 var isInline = val.comment_f_path && val.comment_lineno
1266
1264
1267 if (isInline) {
1265 if (isInline) {
1268 self.attachInlineComment(json_data_elem)
1266 self.attachInlineComment(json_data_elem)
1269 } else {
1267 } else {
1270 self.attachGeneralComment(json_data_elem)
1268 self.attachGeneralComment(json_data_elem)
1271 }
1269 }
1272 })
1270 })
1273
1271
1274 }
1272 }
1275
1273
1276 this.attachGeneralComment = function(json_data) {
1274 this.attachGeneralComment = function(json_data) {
1277 $.each(json_data, function(idx, val) {
1275 $.each(json_data, function(idx, val) {
1278 $('#injected_page_comments').append(val.rendered_text);
1276 $('#injected_page_comments').append(val.rendered_text);
1279 })
1277 })
1280 }
1278 }
1281
1279
1282 this.attachInlineComment = function(json_data) {
1280 this.attachInlineComment = function(json_data) {
1283
1281
1284 $.each(json_data, function (idx, val) {
1282 $.each(json_data, function (idx, val) {
1285 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1283 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1286 var html = val.rendered_text;
1284 var html = val.rendered_text;
1287 var $inlineComments = $('#' + val.target_id)
1285 var $inlineComments = $('#' + val.target_id)
1288 .find(line_qry)
1286 .find(line_qry)
1289 .find('.inline-comments');
1287 .find('.inline-comments');
1290
1288
1291 var lastComment = $inlineComments.find('.comment-inline').last();
1289 var lastComment = $inlineComments.find('.comment-inline').last();
1292
1290
1293 if (lastComment.length === 0) {
1291 if (lastComment.length === 0) {
1294 // first comment, we append simply
1292 // first comment, we append simply
1295 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1293 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1296 } else {
1294 } else {
1297 $(lastComment).after(html)
1295 $(lastComment).after(html)
1298 }
1296 }
1299
1297
1300 })
1298 })
1301
1299
1302 };
1300 };
1303
1301
1304 this.createNewFormWrapper = function(f_path, line_no) {
1302 this.createNewFormWrapper = function(f_path, line_no) {
1305 // create a new reply HTML form from template
1303 // create a new reply HTML form from template
1306 var tmpl = $('#cb-comment-inline-form-template').html();
1304 var tmpl = $('#cb-comment-inline-form-template').html();
1307 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1305 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1308 return $(tmpl);
1306 return $(tmpl);
1309 }
1307 }
1310
1308
1309 this.markCommentResolved = function(commentId) {
1310 $('#comment-label-{0}'.format(commentId)).find('.resolved').show();
1311 $('#comment-label-{0}'.format(commentId)).find('.resolve').hide();
1312 };
1313
1311 this.createComment = function(node, f_path, line_no, resolutionComment) {
1314 this.createComment = function(node, f_path, line_no, resolutionComment) {
1312 self.edit = false;
1315 self.edit = false;
1313 var $node = $(node);
1316 var $node = $(node);
1314 var $td = $node.closest('td');
1317 var $td = $node.closest('td');
1315 var resolvesCommentId = resolutionComment || null;
1318 var resolvesCommentId = resolutionComment || null;
1316
1319
1317 var $replyForm = $td.find('.comment-inline-form');
1320 var $replyForm = $td.find('.comment-inline-form');
1318
1321
1319 // if form isn't existing, we're generating a new one and injecting it.
1322 // if form isn't existing, we're generating a new one and injecting it.
1320 if ($replyForm.length === 0) {
1323 if ($replyForm.length === 0) {
1321
1324
1322 // unhide/expand all comments if they are hidden for a proper REPLY mode
1325 // unhide/expand all comments if they are hidden for a proper REPLY mode
1323 self.toggleLineComments($node, true);
1326 self.toggleLineComments($node, true);
1324
1327
1325 $replyForm = self.createNewFormWrapper(f_path, line_no);
1328 $replyForm = self.createNewFormWrapper(f_path, line_no);
1326
1329
1327 var $comments = $td.find('.inline-comments');
1330 var $comments = $td.find('.inline-comments');
1328
1331
1329 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1332 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1330 if ($comments.length===0) {
1333 if ($comments.length===0) {
1331 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
1334 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
1332 var $reply_container = $('#cb-comments-inline-container-template')
1335 var $reply_container = $('#cb-comments-inline-container-template')
1333 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1336 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1334 $td.append($($reply_container).html());
1337 $td.append($($reply_container).html());
1335 }
1338 }
1336
1339
1337 // default comment button exists, so we prepend the form for leaving initial comment
1340 // default comment button exists, so we prepend the form for leaving initial comment
1338 $td.find('.cb-comment-add-button').before($replyForm);
1341 $td.find('.cb-comment-add-button').before($replyForm);
1339 // set marker, that we have a open form
1342 // set marker, that we have a open form
1340 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1343 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1341 $replyWrapper.addClass('comment-form-active');
1344 $replyWrapper.addClass('comment-form-active');
1342
1345
1343 var lastComment = $comments.find('.comment-inline').last();
1346 var lastComment = $comments.find('.comment-inline').last();
1344 if ($(lastComment).hasClass('comment-outdated')) {
1347 if ($(lastComment).hasClass('comment-outdated')) {
1345 $replyWrapper.show();
1348 $replyWrapper.show();
1346 }
1349 }
1347
1350
1348 var _form = $($replyForm[0]).find('form');
1351 var _form = $($replyForm[0]).find('form');
1349 var autocompleteActions = ['as_note', 'as_todo'];
1352 var autocompleteActions = ['as_note', 'as_todo'];
1350 var comment_id=null;
1353 var comment_id=null;
1351 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1354 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1352 var commentForm = self.createCommentForm(
1355 var commentForm = self.createCommentForm(
1353 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1356 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1354 self.edit, comment_id);
1357 self.edit, comment_id);
1355
1358
1356 // set a CUSTOM submit handler for inline comments.
1359 // set a CUSTOM submit handler for inline comments.
1357 commentForm.setHandleFormSubmit(function(o) {
1360 commentForm.setHandleFormSubmit(function(o) {
1358 var text = commentForm.cm.getValue();
1361 var text = commentForm.cm.getValue();
1359 var commentType = commentForm.getCommentType();
1362 var commentType = commentForm.getCommentType();
1360 var resolvesCommentId = commentForm.getResolvesId();
1363 var resolvesCommentId = commentForm.getResolvesId();
1361 var isDraft = commentForm.getDraftState();
1364 var isDraft = commentForm.getDraftState();
1362
1365
1363 if (text === "") {
1366 if (text === "") {
1364 return;
1367 return;
1365 }
1368 }
1366
1369
1367 if (line_no === undefined) {
1370 if (line_no === undefined) {
1368 alert('Error: unable to fetch line number for this inline comment !');
1371 alert('Error: unable to fetch line number for this inline comment !');
1369 return;
1372 return;
1370 }
1373 }
1371
1374
1372 if (f_path === undefined) {
1375 if (f_path === undefined) {
1373 alert('Error: unable to fetch file path for this inline comment !');
1376 alert('Error: unable to fetch file path for this inline comment !');
1374 return;
1377 return;
1375 }
1378 }
1376
1379
1377 var excludeCancelBtn = false;
1380 var excludeCancelBtn = false;
1378 var submitEvent = true;
1381 var submitEvent = true;
1379 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1382 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1380 commentForm.cm.setOption("readOnly", true);
1383 commentForm.cm.setOption("readOnly", true);
1381 var postData = {
1384 var postData = {
1382 'text': text,
1385 'text': text,
1383 'f_path': f_path,
1386 'f_path': f_path,
1384 'line': line_no,
1387 'line': line_no,
1385 'comment_type': commentType,
1388 'comment_type': commentType,
1386 'draft': isDraft,
1389 'draft': isDraft,
1387 'csrf_token': CSRF_TOKEN
1390 'csrf_token': CSRF_TOKEN
1388 };
1391 };
1389 if (resolvesCommentId){
1392 if (resolvesCommentId){
1390 postData['resolves_comment_id'] = resolvesCommentId;
1393 postData['resolves_comment_id'] = resolvesCommentId;
1391 }
1394 }
1392
1395
1393 // submitSuccess for inline commits
1396 // submitSuccess for inline commits
1394 var submitSuccessCallback = function(json_data) {
1397 var submitSuccessCallback = function(json_data) {
1395
1398
1396 $replyForm.remove();
1399 $replyForm.remove();
1397 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1400 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1398
1401
1399 try {
1402 try {
1400
1403
1401 // inject newly created comments, json_data is {<comment_id>: {}}
1404 // inject newly created comments, json_data is {<comment_id>: {}}
1402 self.attachInlineComment(json_data)
1405 self.attachInlineComment(json_data)
1403
1406
1404 //mark visually which comment was resolved
1407 //mark visually which comment was resolved
1405 if (resolvesCommentId) {
1408 if (resolvesCommentId) {
1406 commentForm.markCommentResolved(resolvesCommentId);
1409 self.markCommentResolved(resolvesCommentId);
1407 }
1410 }
1408
1411
1409 // run global callback on submit
1412 // run global callback on submit
1410 commentForm.globalSubmitSuccessCallback({
1413 commentForm.globalSubmitSuccessCallback({
1411 draft: isDraft,
1414 draft: isDraft,
1412 comment_id: comment_id
1415 comment_id: comment_id
1413 });
1416 });
1414
1417
1415 } catch (e) {
1418 } catch (e) {
1416 console.error(e);
1419 console.error(e);
1417 }
1420 }
1418
1421
1419 if (window.updateSticky !== undefined) {
1422 if (window.updateSticky !== undefined) {
1420 // potentially our comments change the active window size, so we
1423 // potentially our comments change the active window size, so we
1421 // notify sticky elements
1424 // notify sticky elements
1422 updateSticky()
1425 updateSticky()
1423 }
1426 }
1424
1427
1425 if (window.refreshAllComments !== undefined && !isDraft) {
1428 if (window.refreshAllComments !== undefined && !isDraft) {
1426 // if we have this handler, run it, and refresh all comments boxes
1429 // if we have this handler, run it, and refresh all comments boxes
1427 refreshAllComments()
1430 refreshAllComments()
1428 }
1431 }
1429 else if (window.refreshDraftComments !== undefined && isDraft) {
1432 else if (window.refreshDraftComments !== undefined && isDraft) {
1430 // if we have this handler, run it, and refresh all comments boxes
1433 // if we have this handler, run it, and refresh all comments boxes
1431 refreshDraftComments();
1434 refreshDraftComments();
1432 }
1435 }
1433
1436
1434 commentForm.setActionButtonsDisabled(false);
1437 commentForm.setActionButtonsDisabled(false);
1435
1438
1436 // re trigger the linkification of next/prev navigation
1439 // re trigger the linkification of next/prev navigation
1437 linkifyComments($('.inline-comment-injected'));
1440 linkifyComments($('.inline-comment-injected'));
1438 timeagoActivate();
1441 timeagoActivate();
1439 tooltipActivate();
1442 tooltipActivate();
1440 };
1443 };
1441
1444
1442 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1445 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1443 var prefix = "Error while submitting comment.\n"
1446 var prefix = "Error while submitting comment.\n"
1444 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1447 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1445 ajaxErrorSwal(message);
1448 ajaxErrorSwal(message);
1446 commentForm.resetCommentFormState(text)
1449 commentForm.resetCommentFormState(text)
1447 };
1450 };
1448
1451
1449 commentForm.submitAjaxPOST(
1452 commentForm.submitAjaxPOST(
1450 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1453 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1451 });
1454 });
1452 }
1455 }
1453
1456
1454 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1457 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1455 $replyForm.addClass('comment-inline-form-open');
1458 $replyForm.addClass('comment-inline-form-open');
1456 tooltipActivate();
1459 tooltipActivate();
1457 };
1460 };
1458
1461
1459 this.createResolutionComment = function(commentId){
1462 this.createResolutionComment = function(commentId){
1460 // hide the trigger text
1463 // hide the trigger text
1461 $('#resolve-comment-{0}'.format(commentId)).hide();
1464 $('#resolve-comment-{0}'.format(commentId)).hide();
1462
1465
1463 var comment = $('#comment-'+commentId);
1466 var comment = $('#comment-'+commentId);
1464 var commentData = comment.data();
1467 var commentData = comment.data();
1465
1468
1466 if (commentData.commentInline) {
1469 if (commentData.commentInline) {
1467 var f_path = commentData.commentFPath;
1470 var f_path = commentData.commentFPath;
1468 var line_no = commentData.commentLineNo;
1471 var line_no = commentData.commentLineNo;
1469 this.createComment(comment, f_path, line_no, commentId)
1472 this.createComment(comment, f_path, line_no, commentId)
1470 } else {
1473 } else {
1471 this.createGeneralComment('general', "$placeholder", commentId)
1474 this.createGeneralComment('general', "$placeholder", commentId)
1472 }
1475 }
1473
1476
1474 return false;
1477 return false;
1475 };
1478 };
1476
1479
1477 this.submitResolution = function(commentId){
1480 this.submitResolution = function(commentId){
1478 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1481 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1479 var commentForm = form.get(0).CommentForm;
1482 var commentForm = form.get(0).CommentForm;
1480
1483
1481 var cm = commentForm.getCmInstance();
1484 var cm = commentForm.getCmInstance();
1482 var renderer = templateContext.visual.default_renderer;
1485 var renderer = templateContext.visual.default_renderer;
1483 if (renderer == 'rst'){
1486 if (renderer == 'rst'){
1484 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1487 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1485 } else if (renderer == 'markdown') {
1488 } else if (renderer == 'markdown') {
1486 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1489 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1487 } else {
1490 } else {
1488 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1491 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1489 }
1492 }
1490
1493
1491 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1494 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1492 form.submit();
1495 form.submit();
1493 return false;
1496 return false;
1494 };
1497 };
1495
1498
1499 this.resolveTodo = function (elem, todoId) {
1500 var commentId = todoId;
1501
1502 SwalNoAnimation.fire({
1503 title: 'Resolve TODO {0}'.format(todoId),
1504 showCancelButton: true,
1505 confirmButtonText: _gettext('Yes'),
1506 showLoaderOnConfirm: true,
1507
1508 allowOutsideClick: function () {
1509 !Swal.isLoading()
1510 },
1511 preConfirm: function () {
1512 var comment = $('#comment-' + commentId);
1513 var commentData = comment.data();
1514
1515 var f_path = null
1516 var line_no = null
1517 if (commentData.commentInline) {
1518 f_path = commentData.commentFPath;
1519 line_no = commentData.commentLineNo;
1520 }
1521
1522 var renderer = templateContext.visual.default_renderer;
1523 var commentBoxUrl = '{1}#comment-{0}'.format(commentId);
1524
1525 // Pull request case
1526 if (templateContext.pull_request_data.pull_request_id !== null) {
1527 var commentUrl = pyroutes.url('pullrequest_comment_create',
1528 {
1529 'repo_name': templateContext.repo_name,
1530 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1531 'comment_id': commentId
1532 });
1533 } else {
1534 var commentUrl = pyroutes.url('repo_commit_comment_create',
1535 {
1536 'repo_name': templateContext.repo_name,
1537 'commit_id': templateContext.commit_data.commit_id,
1538 'comment_id': commentId
1539 });
1540 }
1541
1542 if (renderer === 'rst') {
1543 commentBoxUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentUrl);
1544 } else if (renderer === 'markdown') {
1545 commentBoxUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentUrl);
1546 }
1547 var resolveText = _gettext('TODO from comment {0} was fixed.').format(commentBoxUrl);
1548
1549 var postData = {
1550 text: resolveText,
1551 comment_type: 'note',
1552 draft: false,
1553 csrf_token: CSRF_TOKEN,
1554 resolves_comment_id: commentId
1555 }
1556 if (commentData.commentInline) {
1557 postData['f_path'] = f_path;
1558 postData['line'] = line_no;
1559 }
1560
1561 return new Promise(function (resolve, reject) {
1562 $.ajax({
1563 type: 'POST',
1564 data: postData,
1565 url: commentUrl,
1566 headers: {'X-PARTIAL-XHR': true}
1567 })
1568 .done(function (data) {
1569 resolve(data);
1570 })
1571 .fail(function (jqXHR, textStatus, errorThrown) {
1572 var prefix = "Error while resolving TODO.\n"
1573 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1574 ajaxErrorSwal(message);
1575 });
1576 })
1577 }
1578
1579 })
1580 .then(function (result) {
1581 var success = function (json_data) {
1582 resolvesCommentId = commentId;
1583 var commentResolved = json_data[Object.keys(json_data)[0]]
1584
1585 try {
1586
1587 if (commentResolved.f_path) {
1588 // inject newly created comments, json_data is {<comment_id>: {}}
1589 self.attachInlineComment(json_data)
1590 } else {
1591 self.attachGeneralComment(json_data)
1592 }
1593
1594 //mark visually which comment was resolved
1595 if (resolvesCommentId) {
1596 self.markCommentResolved(resolvesCommentId);
1597 }
1598
1599 // run global callback on submit
1600 if (window.commentFormGlobalSubmitSuccessCallback !== undefined) {
1601 commentFormGlobalSubmitSuccessCallback({
1602 draft: false,
1603 comment_id: commentId
1604 });
1605 }
1606
1607 } catch (e) {
1608 console.error(e);
1609 }
1610
1611 if (window.updateSticky !== undefined) {
1612 // potentially our comments change the active window size, so we
1613 // notify sticky elements
1614 updateSticky()
1615 }
1616
1617 if (window.refreshAllComments !== undefined) {
1618 // if we have this handler, run it, and refresh all comments boxes
1619 refreshAllComments()
1620 }
1621 // re trigger the linkification of next/prev navigation
1622 linkifyComments($('.inline-comment-injected'));
1623 timeagoActivate();
1624 tooltipActivate();
1625 };
1626
1627 if (result.value) {
1628 $(elem).remove();
1629 success(result.value)
1630 }
1631 })
1632 };
1633
1496 };
1634 };
1497
1635
1498 window.commentHelp = function(renderer) {
1636 window.commentHelp = function(renderer) {
1499 var funcData = {'renderer': renderer}
1637 var funcData = {'renderer': renderer}
1500 return renderTemplate('commentHelpHovercard', funcData)
1638 return renderTemplate('commentHelpHovercard', funcData)
1501 } No newline at end of file
1639 }
@@ -1,270 +1,278 b''
1 <%text>
1 <%text>
2 <div style="display: none">
2 <div style="display: none">
3
3
4 <script>
4 <script>
5 var CG = new ColorGenerator();
5 var CG = new ColorGenerator();
6 </script>
6 </script>
7
7
8 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
8 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
9
9
10 <%
10 <%
11 if (size > 16) {
11 if (size > 16) {
12 var gravatar_class = 'gravatar gravatar-large';
12 var gravatar_class = 'gravatar gravatar-large';
13 } else {
13 } else {
14 var gravatar_class = 'gravatar';
14 var gravatar_class = 'gravatar';
15 }
15 }
16
16
17 if (tooltip) {
17 if (tooltip) {
18 var gravatar_class = gravatar_class + ' tooltip-hovercard';
18 var gravatar_class = gravatar_class + ' tooltip-hovercard';
19 }
19 }
20
20
21 var data_hovercard_alt = username;
21 var data_hovercard_alt = username;
22
22
23 %>
23 %>
24
24
25 <%
25 <%
26 if (show_disabled) {
26 if (show_disabled) {
27 var user_cls = 'user user-disabled';
27 var user_cls = 'user user-disabled';
28 } else {
28 } else {
29 var user_cls = 'user';
29 var user_cls = 'user';
30 }
30 }
31 var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
31 var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
32 %>
32 %>
33
33
34 <div class="rc-user">
34 <div class="rc-user">
35 <img class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" data-hovercard-url="<%= data_hovercard_url %>" data-hovercard-alt="<%= data_hovercard_alt %>" src="<%- gravatar_url -%>">
35 <img class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" data-hovercard-url="<%= data_hovercard_url %>" data-hovercard-alt="<%= data_hovercard_alt %>" src="<%- gravatar_url -%>">
36 <span class="<%= user_cls %>"> <%- user_link -%> </span>
36 <span class="<%= user_cls %>"> <%- user_link -%> </span>
37 </div>
37 </div>
38
38
39 </script>
39 </script>
40
40
41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
42 <%
42 <%
43 if (create) {
43 if (create) {
44 var edit_visibility = 'visible';
44 var edit_visibility = 'visible';
45 } else {
45 } else {
46 var edit_visibility = 'hidden';
46 var edit_visibility = 'hidden';
47 }
47 }
48
48
49 if (member.user_group && member.user_group.vote_rule) {
49 if (member.user_group && member.user_group.vote_rule) {
50 var reviewGroup = '<i class="icon-user-group"></i>';
50 var reviewGroup = '<i class="icon-user-group"></i>';
51 var reviewGroupColor = CG.asRGB(CG.getColor(member.user_group.vote_rule));
51 var reviewGroupColor = CG.asRGB(CG.getColor(member.user_group.vote_rule));
52 } else {
52 } else {
53 var reviewGroup = null;
53 var reviewGroup = null;
54 var reviewGroupColor = 'transparent';
54 var reviewGroupColor = 'transparent';
55 }
55 }
56 var rule_show = rule_show || false;
56 var rule_show = rule_show || false;
57
57
58 if (rule_show) {
58 if (rule_show) {
59 var rule_visibility = 'table-cell';
59 var rule_visibility = 'table-cell';
60 } else {
60 } else {
61 var rule_visibility = 'none';
61 var rule_visibility = 'none';
62 }
62 }
63
63
64 %>
64 %>
65
65
66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
67
67
68 <% if (create) { %>
68 <% if (create) { %>
69 <td style="width: 1px"></td>
69 <td style="width: 1px"></td>
70 <% } else { %>
70 <% } else { %>
71 <td style="width: 20px">
71 <td style="width: 20px">
72 <div class="tooltip presence-state" style="display: none; position: absolute; left: 2px" title="This users is currently at this page">
72 <div class="tooltip presence-state" style="display: none; position: absolute; left: 2px" title="This users is currently at this page">
73 <i class="icon-eye" style="color: #0ac878"></i>
73 <i class="icon-eye" style="color: #0ac878"></i>
74 </div>
74 </div>
75 <% if (role === 'reviewer') { %>
75 <% if (role === 'reviewer') { %>
76 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
76 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
77 <i class="icon-circle review-status-<%= review_status %>"></i>
77 <i class="icon-circle review-status-<%= review_status %>"></i>
78 </div>
78 </div>
79 <% } else if (role === 'observer') { %>
79 <% } else if (role === 'observer') { %>
80 <div class="tooltip" title="Observer without voting right.">
80 <div class="tooltip" title="Observer without voting right.">
81 <i class="icon-circle-thin"></i>
81 <i class="icon-circle-thin"></i>
82 </div>
82 </div>
83 <% } %>
83 <% } %>
84 </td>
84 </td>
85 <% } %>
85 <% } %>
86
86
87
87
88 <% if (mandatory) { %>
88 <% if (mandatory) { %>
89 <td style="text-align: right;width: 10px;">
89 <td style="text-align: right;width: 10px;">
90 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
90 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
91 <i class="icon-lock"></i>
91 <i class="icon-lock"></i>
92 </div>
92 </div>
93 </td>
93 </td>
94
94
95 <% } else { %>
95 <% } else { %>
96 <td style="text-align: right;width: 10px;">
96 <td style="text-align: right;width: 10px;">
97 <% if (allowed_to_update) { %>
97 <% if (allowed_to_update) { %>
98 <div class="<%=role %>_member_remove" onclick="reviewersController.removeMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
98 <div class="<%=role %>_member_remove" onclick="reviewersController.removeMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
99 <i class="icon-remove" style="color: #e85e4d;"></i>
99 <i class="icon-remove" style="color: #e85e4d;"></i>
100 </div>
100 </div>
101 <% } %>
101 <% } %>
102 </td>
102 </td>
103 <% } %>
103 <% } %>
104
104
105 <td>
105 <td>
106 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
106 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
107 <%-
107 <%-
108 renderTemplate('gravatarWithUser', {
108 renderTemplate('gravatarWithUser', {
109 'size': 16,
109 'size': 16,
110 'show_disabled': false,
110 'show_disabled': false,
111 'tooltip': true,
111 'tooltip': true,
112 'username': member.username,
112 'username': member.username,
113 'user_id': member.user_id,
113 'user_id': member.user_id,
114 'user_link': member.user_link,
114 'user_link': member.user_link,
115 'gravatar_url': member.gravatar_link
115 'gravatar_url': member.gravatar_link
116 })
116 })
117 %>
117 %>
118 </div>
118 </div>
119 <% if (reviewGroup !== null) { %>
119 <% if (reviewGroup !== null) { %>
120 <span class="tooltip" title="Member of review group from rule: `<%= member.user_group.name %>`" style="color: <%= reviewGroupColor %>">
120 <span class="tooltip" title="Member of review group from rule: `<%= member.user_group.name %>`" style="color: <%= reviewGroupColor %>">
121 <%- reviewGroup %>
121 <%- reviewGroup %>
122 </span>
122 </span>
123 <% } %>
123 <% } %>
124 </td>
124 </td>
125
125
126 </tr>
126 </tr>
127
127
128 <tr id="reviewer_<%= member.user_id %>_rules">
128 <tr id="reviewer_<%= member.user_id %>_rules">
129 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
129 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
130 <input type="hidden" name="__start__" value="reviewer:mapping">
130 <input type="hidden" name="__start__" value="reviewer:mapping">
131
131
132 <%if (member.user_group && member.user_group.vote_rule) { %>
132 <%if (member.user_group && member.user_group.vote_rule) { %>
133 <div class="reviewer_reason">
133 <div class="reviewer_reason">
134
134
135 <%if (member.user_group.vote_rule == -1) {%>
135 <%if (member.user_group.vote_rule == -1) {%>
136 - group votes required: ALL
136 - group votes required: ALL
137 <%} else {%>
137 <%} else {%>
138 - group votes required: <%= member.user_group.vote_rule %>
138 - group votes required: <%= member.user_group.vote_rule %>
139 <%}%>
139 <%}%>
140 </div>
140 </div>
141 <%} %>
141 <%} %>
142
142
143 <input type="hidden" name="__start__" value="reasons:sequence">
143 <input type="hidden" name="__start__" value="reasons:sequence">
144 <% for (var i = 0; i < reasons.length; i++) { %>
144 <% for (var i = 0; i < reasons.length; i++) { %>
145 <% var reason = reasons[i] %>
145 <% var reason = reasons[i] %>
146 <div class="reviewer_reason">- <%= reason %></div>
146 <div class="reviewer_reason">- <%= reason %></div>
147 <input type="hidden" name="reason" value="<%= reason %>">
147 <input type="hidden" name="reason" value="<%= reason %>">
148 <% } %>
148 <% } %>
149 <input type="hidden" name="__end__" value="reasons:sequence">
149 <input type="hidden" name="__end__" value="reasons:sequence">
150
150
151 <input type="hidden" name="__start__" value="rules:sequence">
151 <input type="hidden" name="__start__" value="rules:sequence">
152 <% for (var i = 0; i < member.rules.length; i++) { %>
152 <% for (var i = 0; i < member.rules.length; i++) { %>
153 <% var rule = member.rules[i] %>
153 <% var rule = member.rules[i] %>
154 <input type="hidden" name="rule_id" value="<%= rule %>">
154 <input type="hidden" name="rule_id" value="<%= rule %>">
155 <% } %>
155 <% } %>
156 <input type="hidden" name="__end__" value="rules:sequence">
156 <input type="hidden" name="__end__" value="rules:sequence">
157
157
158 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
158 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
159 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
159 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
160 <input type="hidden" name="role" value="<%= role %>"/>
160 <input type="hidden" name="role" value="<%= role %>"/>
161
161
162 <input type="hidden" name="__end__" value="reviewer:mapping">
162 <input type="hidden" name="__end__" value="reviewer:mapping">
163 </td>
163 </td>
164 </tr>
164 </tr>
165
165
166 </script>
166 </script>
167
167
168 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
168 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
169
169
170 <%
170 <%
171 if (size > 16) {
171 if (size > 16) {
172 var gravatar_class = 'gravatar gravatar-large';
172 var gravatar_class = 'gravatar gravatar-large';
173 } else {
173 } else {
174 var gravatar_class = 'gravatar';
174 var gravatar_class = 'gravatar';
175 }
175 }
176
176
177 %>
177 %>
178
178
179 <%
179 <%
180 if (show_disabled) {
180 if (show_disabled) {
181 var user_cls = 'user user-disabled';
181 var user_cls = 'user user-disabled';
182 } else {
182 } else {
183 var user_cls = 'user';
183 var user_cls = 'user';
184 }
184 }
185
185
186 %>
186 %>
187
187
188 <div style='line-height: 20px'>
188 <div style='line-height: 20px'>
189 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
189 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
190 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
190 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
191 </div>
191 </div>
192
192
193 </script>
193 </script>
194
194
195
195
196 <script id="ejs_sideBarCommentHovercard" type="text/template" class="ejsTemplate">
196 <script id="ejs_sideBarCommentHovercard" type="text/template" class="ejsTemplate">
197
197
198 <div>
198 <div>
199
199
200 <% if (is_todo) { %>
200 <% if (is_todo) { %>
201 <% if (inline) { %>
201 <% if (inline) { %>
202 <strong>Inline</strong> TODO (<code>#<%- comment_id -%></code>) on line: <%= line_no %>
202 <strong>Inline</strong> TODO (<code>#<%- comment_id -%></code>) on line: <%= line_no %>
203 <% if (version_info) { %>
203 <% if (version_info) { %>
204 <%= version_info %>
204 <%= version_info %>
205 <% } %>
205 <% } %>
206 <br/>
206 <br/>
207 File: <code><%- file_name -%></code>
207 File: <code><%- file_name -%></code>
208 <% } else { %>
208 <% } else { %>
209 <% if (review_status) { %>
209 <% if (review_status) { %>
210 <i class="icon-circle review-status-<%= review_status %>"></i>
210 <i class="icon-circle review-status-<%= review_status %>"></i>
211 <% } %>
211 <% } %>
212 <strong>General</strong> TODO (<code>#<%- comment_id -%></code>)
212 <strong>General</strong> TODO (<code>#<%- comment_id -%></code>)
213 <% if (version_info) { %>
213 <% if (version_info) { %>
214 <%= version_info %>
214 <%= version_info %>
215 <% } %>
215 <% } %>
216 <% } %>
216 <% } %>
217 <% } else { %>
217 <% } else { %>
218 <% if (inline) { %>
218 <% if (inline) { %>
219 <strong>Inline</strong> comment (<code>#<%- comment_id -%></code>) on line: <%= line_no %>
219 <strong>Inline</strong> comment (<code>#<%- comment_id -%></code>) on line: <%= line_no %>
220 <% if (version_info) { %>
220 <% if (version_info) { %>
221 <%= version_info %>
221 <%= version_info %>
222 <% } %>
222 <% } %>
223 <br/>
223 <br/>
224 File: <code><%- file_name -%></code>
224 File: <code><%- file_name -%></code>
225 <% } else { %>
225 <% } else { %>
226 <% if (review_status) { %>
226 <% if (review_status) { %>
227 <i class="icon-circle review-status-<%= review_status %>"></i>
227 <i class="icon-circle review-status-<%= review_status %>"></i>
228 <% } %>
228 <% } %>
229 <strong>General</strong> comment (<code>#<%- comment_id -%></code>)
229 <strong>General</strong> comment (<code>#<%- comment_id -%></code>)
230 <% if (version_info) { %>
230 <% if (version_info) { %>
231 <%= version_info %>
231 <%= version_info %>
232 <% } %>
232 <% } %>
233 <% } %>
233 <% } %>
234 <% } %>
234 <% } %>
235 <br/>
235 <br/>
236 Created:
236 Created:
237 <time class="timeago" title="<%= created_on %>" datetime="<%= datetime %>"><%= $.timeago(datetime) %></time>
237 <time class="timeago" title="<%= created_on %>" datetime="<%= datetime %>"><%= $.timeago(datetime) %></time>
238
238
239 <% if (is_todo) { %>
240 <div style="text-align: center; padding-top: 5px">
241 <a class="btn btn-sm" href="#resolveTodo<%- comment_id -%>" onclick="Rhodecode.comments.resolveTodo(this, '<%- comment_id -%>'); return false">
242 <strong>Resolve TODO</strong>
243 </a>
244 </div>
245 <% } %>
246
239 </div>
247 </div>
240
248
241 </script>
249 </script>
242
250
243 <script id="ejs_commentHelpHovercard" type="text/template" class="ejsTemplate">
251 <script id="ejs_commentHelpHovercard" type="text/template" class="ejsTemplate">
244
252
245 <div>
253 <div>
246 Use <strong>@username</strong> mention syntax to send direct notification to this RhodeCode user.<br/>
254 Use <strong>@username</strong> mention syntax to send direct notification to this RhodeCode user.<br/>
247 Typing / starts autocomplete for certain action, e.g set review status, or comment type. <br/>
255 Typing / starts autocomplete for certain action, e.g set review status, or comment type. <br/>
248 <br/>
256 <br/>
249 Use <strong>Cmd/ctrl+enter</strong> to submit comment, or <strong>Shift+Cmd/ctrl+enter</strong> to submit a draft.<br/>
257 Use <strong>Cmd/ctrl+enter</strong> to submit comment, or <strong>Shift+Cmd/ctrl+enter</strong> to submit a draft.<br/>
250 <br/>
258 <br/>
251 <strong>Draft comments</strong> are private to the author, and trigger no notification to others.<br/>
259 <strong>Draft comments</strong> are private to the author, and trigger no notification to others.<br/>
252 They are permanent until deleted, or converted to regular comments.<br/>
260 They are permanent until deleted, or converted to regular comments.<br/>
253 <br/>
261 <br/>
254 <br/>
262 <br/>
255 </div>
263 </div>
256
264
257 </script>
265 </script>
258
266
259
267
260
268
261 ##// END OF EJS Templates
269 ##// END OF EJS Templates
262 </div>
270 </div>
263
271
264
272
265 <script>
273 <script>
266 // registers the templates into global cache
274 // registers the templates into global cache
267 registerTemplates();
275 registerTemplates();
268 </script>
276 </script>
269
277
270 </%text>
278 </%text>
General Comments 0
You need to be logged in to leave comments. Login now