##// END OF EJS Templates
Refactoring of changeset_file_comments for more generic usage. In both It enables sharing code between changeset, and pull requests discussions
marcink -
r2439:ad19dfcd codereview
parent child Browse files
Show More
@@ -1,428 +1,428
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changeset
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 changeset controller for pylons showoing changes beetween
7 7 revisions
8 8
9 9 :created_on: Apr 25, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import logging
27 27 import traceback
28 28 from collections import defaultdict
29 29 from webob.exc import HTTPForbidden
30 30
31 31 from pylons import tmpl_context as c, url, request, response
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from pylons.decorators import jsonify
35 35
36 36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetError, \
37 37 ChangesetDoesNotExistError
38 38 from rhodecode.lib.vcs.nodes import FileNode
39 39
40 40 import rhodecode.lib.helpers as h
41 41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
42 42 from rhodecode.lib.base import BaseRepoController, render
43 43 from rhodecode.lib.utils import EmptyChangeset, action_logger
44 44 from rhodecode.lib.compat import OrderedDict
45 45 from rhodecode.lib import diffs
46 46 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 47 from rhodecode.model.comment import ChangesetCommentsModel
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.meta import Session
50 50 from rhodecode.lib.diffs import wrapped_diff
51 51 from rhodecode.model.repo import RepoModel
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 def _update_with_GET(params, GET):
57 57 for k in ['diff1', 'diff2', 'diff']:
58 58 params[k] += GET.getall(k)
59 59
60 60
61 61 def anchor_url(revision, path, GET):
62 62 fid = h.FID(revision, path)
63 63 return h.url.current(anchor=fid, **dict(GET))
64 64
65 65
66 66 def get_ignore_ws(fid, GET):
67 67 ig_ws_global = GET.get('ignorews')
68 68 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
69 69 if ig_ws:
70 70 try:
71 71 return int(ig_ws[0].split(':')[-1])
72 72 except:
73 73 pass
74 74 return ig_ws_global
75 75
76 76
77 77 def _ignorews_url(GET, fileid=None):
78 78 fileid = str(fileid) if fileid else None
79 79 params = defaultdict(list)
80 80 _update_with_GET(params, GET)
81 81 lbl = _('show white space')
82 82 ig_ws = get_ignore_ws(fileid, GET)
83 83 ln_ctx = get_line_ctx(fileid, GET)
84 84 # global option
85 85 if fileid is None:
86 86 if ig_ws is None:
87 87 params['ignorews'] += [1]
88 88 lbl = _('ignore white space')
89 89 ctx_key = 'context'
90 90 ctx_val = ln_ctx
91 91 # per file options
92 92 else:
93 93 if ig_ws is None:
94 94 params[fileid] += ['WS:1']
95 95 lbl = _('ignore white space')
96 96
97 97 ctx_key = fileid
98 98 ctx_val = 'C:%s' % ln_ctx
99 99 # if we have passed in ln_ctx pass it along to our params
100 100 if ln_ctx:
101 101 params[ctx_key] += [ctx_val]
102 102
103 103 params['anchor'] = fileid
104 104 img = h.image(h.url('/images/icons/text_strikethrough.png'), lbl, class_='icon')
105 105 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
106 106
107 107
108 108 def get_line_ctx(fid, GET):
109 109 ln_ctx_global = GET.get('context')
110 110 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
111 111
112 112 if ln_ctx:
113 113 retval = ln_ctx[0].split(':')[-1]
114 114 else:
115 115 retval = ln_ctx_global
116 116
117 117 try:
118 118 return int(retval)
119 119 except:
120 120 return
121 121
122 122
123 123 def _context_url(GET, fileid=None):
124 124 """
125 125 Generates url for context lines
126 126
127 127 :param fileid:
128 128 """
129 129
130 130 fileid = str(fileid) if fileid else None
131 131 ig_ws = get_ignore_ws(fileid, GET)
132 132 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
133 133
134 134 params = defaultdict(list)
135 135 _update_with_GET(params, GET)
136 136
137 137 # global option
138 138 if fileid is None:
139 139 if ln_ctx > 0:
140 140 params['context'] += [ln_ctx]
141 141
142 142 if ig_ws:
143 143 ig_ws_key = 'ignorews'
144 144 ig_ws_val = 1
145 145
146 146 # per file option
147 147 else:
148 148 params[fileid] += ['C:%s' % ln_ctx]
149 149 ig_ws_key = fileid
150 150 ig_ws_val = 'WS:%s' % 1
151 151
152 152 if ig_ws:
153 153 params[ig_ws_key] += [ig_ws_val]
154 154
155 155 lbl = _('%s line context') % ln_ctx
156 156
157 157 params['anchor'] = fileid
158 158 img = h.image(h.url('/images/icons/table_add.png'), lbl, class_='icon')
159 159 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
160 160
161 161
162 162 class ChangesetController(BaseRepoController):
163 163
164 164 @LoginRequired()
165 165 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
166 166 'repository.admin')
167 167 def __before__(self):
168 168 super(ChangesetController, self).__before__()
169 169 c.affected_files_cut_off = 60
170 170 repo_model = RepoModel()
171 171 c.users_array = repo_model.get_users_js()
172 172 c.users_groups_array = repo_model.get_users_groups_js()
173 173
174 174 def index(self, revision):
175 175
176 176 c.anchor_url = anchor_url
177 177 c.ignorews_url = _ignorews_url
178 178 c.context_url = _context_url
179 179 limit_off = request.GET.get('fulldiff')
180 180 #get ranges of revisions if preset
181 181 rev_range = revision.split('...')[:2]
182 182 enable_comments = True
183 183 try:
184 184 if len(rev_range) == 2:
185 185 enable_comments = False
186 186 rev_start = rev_range[0]
187 187 rev_end = rev_range[1]
188 188 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
189 189 end=rev_end)
190 190 else:
191 191 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
192 192
193 193 c.cs_ranges = list(rev_ranges)
194 194 if not c.cs_ranges:
195 195 raise RepositoryError('Changeset range returned empty result')
196 196
197 197 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
198 198 log.error(traceback.format_exc())
199 199 h.flash(str(e), category='warning')
200 200 return redirect(url('home'))
201 201
202 202 c.changes = OrderedDict()
203 203
204 204 c.lines_added = 0 # count of lines added
205 205 c.lines_deleted = 0 # count of lines removes
206 206
207 207 cumulative_diff = 0
208 208 c.cut_off = False # defines if cut off limit is reached
209 209 c.changeset_statuses = ChangesetStatus.STATUSES
210 210 c.comments = []
211 211 c.statuses = []
212 212 c.inline_comments = []
213 213 c.inline_cnt = 0
214 214 # Iterate over ranges (default changeset view is always one changeset)
215 215 for changeset in c.cs_ranges:
216 216
217 217 c.statuses.extend([ChangesetStatusModel()\
218 218 .get_status(c.rhodecode_db_repo.repo_id,
219 219 changeset.raw_id)])
220 220
221 221 c.comments.extend(ChangesetCommentsModel()\
222 222 .get_comments(c.rhodecode_db_repo.repo_id,
223 changeset.raw_id))
223 revision=changeset.raw_id))
224 224 inlines = ChangesetCommentsModel()\
225 225 .get_inline_comments(c.rhodecode_db_repo.repo_id,
226 changeset.raw_id)
226 revision=changeset.raw_id)
227 227 c.inline_comments.extend(inlines)
228 228 c.changes[changeset.raw_id] = []
229 229 try:
230 230 changeset_parent = changeset.parents[0]
231 231 except IndexError:
232 232 changeset_parent = None
233 233
234 234 #==================================================================
235 235 # ADDED FILES
236 236 #==================================================================
237 237 for node in changeset.added:
238 238 fid = h.FID(revision, node.path)
239 239 line_context_lcl = get_line_ctx(fid, request.GET)
240 240 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
241 241 lim = self.cut_off_limit
242 242 if cumulative_diff > self.cut_off_limit:
243 243 lim = -1 if limit_off is None else None
244 244 size, cs1, cs2, diff, st = wrapped_diff(
245 245 filenode_old=None,
246 246 filenode_new=node,
247 247 cut_off_limit=lim,
248 248 ignore_whitespace=ign_whitespace_lcl,
249 249 line_context=line_context_lcl,
250 250 enable_comments=enable_comments
251 251 )
252 252 cumulative_diff += size
253 253 c.lines_added += st[0]
254 254 c.lines_deleted += st[1]
255 255 c.changes[changeset.raw_id].append(
256 256 ('added', node, diff, cs1, cs2, st)
257 257 )
258 258
259 259 #==================================================================
260 260 # CHANGED FILES
261 261 #==================================================================
262 262 for node in changeset.changed:
263 263 try:
264 264 filenode_old = changeset_parent.get_node(node.path)
265 265 except ChangesetError:
266 266 log.warning('Unable to fetch parent node for diff')
267 267 filenode_old = FileNode(node.path, '', EmptyChangeset())
268 268
269 269 fid = h.FID(revision, node.path)
270 270 line_context_lcl = get_line_ctx(fid, request.GET)
271 271 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
272 272 lim = self.cut_off_limit
273 273 if cumulative_diff > self.cut_off_limit:
274 274 lim = -1 if limit_off is None else None
275 275 size, cs1, cs2, diff, st = wrapped_diff(
276 276 filenode_old=filenode_old,
277 277 filenode_new=node,
278 278 cut_off_limit=lim,
279 279 ignore_whitespace=ign_whitespace_lcl,
280 280 line_context=line_context_lcl,
281 281 enable_comments=enable_comments
282 282 )
283 283 cumulative_diff += size
284 284 c.lines_added += st[0]
285 285 c.lines_deleted += st[1]
286 286 c.changes[changeset.raw_id].append(
287 287 ('changed', node, diff, cs1, cs2, st)
288 288 )
289 289 #==================================================================
290 290 # REMOVED FILES
291 291 #==================================================================
292 292 for node in changeset.removed:
293 293 c.changes[changeset.raw_id].append(
294 294 ('removed', node, None, None, None, (0, 0))
295 295 )
296 296
297 297 # count inline comments
298 for path, lines in c.inline_comments:
298 for _, lines in c.inline_comments:
299 299 for comments in lines.values():
300 300 c.inline_cnt += len(comments)
301 301
302 302 if len(c.cs_ranges) == 1:
303 303 c.changeset = c.cs_ranges[0]
304 304 c.changes = c.changes[c.changeset.raw_id]
305 305
306 306 return render('changeset/changeset.html')
307 307 else:
308 308 return render('changeset/changeset_range.html')
309 309
310 310 def raw_changeset(self, revision):
311 311
312 312 method = request.GET.get('diff', 'show')
313 313 ignore_whitespace = request.GET.get('ignorews') == '1'
314 314 line_context = request.GET.get('context', 3)
315 315 try:
316 316 c.scm_type = c.rhodecode_repo.alias
317 317 c.changeset = c.rhodecode_repo.get_changeset(revision)
318 318 except RepositoryError:
319 319 log.error(traceback.format_exc())
320 320 return redirect(url('home'))
321 321 else:
322 322 try:
323 323 c.changeset_parent = c.changeset.parents[0]
324 324 except IndexError:
325 325 c.changeset_parent = None
326 326 c.changes = []
327 327
328 328 for node in c.changeset.added:
329 329 filenode_old = FileNode(node.path, '')
330 330 if filenode_old.is_binary or node.is_binary:
331 331 diff = _('binary file') + '\n'
332 332 else:
333 333 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
334 334 ignore_whitespace=ignore_whitespace,
335 335 context=line_context)
336 336 diff = diffs.DiffProcessor(f_gitdiff,
337 337 format='gitdiff').raw_diff()
338 338
339 339 cs1 = None
340 340 cs2 = node.changeset.raw_id
341 341 c.changes.append(('added', node, diff, cs1, cs2))
342 342
343 343 for node in c.changeset.changed:
344 344 filenode_old = c.changeset_parent.get_node(node.path)
345 345 if filenode_old.is_binary or node.is_binary:
346 346 diff = _('binary file')
347 347 else:
348 348 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
349 349 ignore_whitespace=ignore_whitespace,
350 350 context=line_context)
351 351 diff = diffs.DiffProcessor(f_gitdiff,
352 352 format='gitdiff').raw_diff()
353 353
354 354 cs1 = filenode_old.changeset.raw_id
355 355 cs2 = node.changeset.raw_id
356 356 c.changes.append(('changed', node, diff, cs1, cs2))
357 357
358 358 response.content_type = 'text/plain'
359 359
360 360 if method == 'download':
361 361 response.content_disposition = 'attachment; filename=%s.patch' \
362 362 % revision
363 363
364 364 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
365 365 for x in c.changeset.parents])
366 366
367 367 c.diffs = ''
368 368 for x in c.changes:
369 369 c.diffs += x[2]
370 370
371 371 return render('changeset/raw_changeset.html')
372 372
373 373 @jsonify
374 374 def comment(self, repo_name, revision):
375 375 status = request.POST.get('changeset_status')
376 376 change_status = request.POST.get('change_changeset_status')
377 377
378 378 comm = ChangesetCommentsModel().create(
379 379 text=request.POST.get('text'),
380 380 repo_id=c.rhodecode_db_repo.repo_id,
381 381 user_id=c.rhodecode_user.user_id,
382 382 revision=revision,
383 383 f_path=request.POST.get('f_path'),
384 384 line_no=request.POST.get('line'),
385 385 status_change=(ChangesetStatus.get_status_lbl(status)
386 386 if status and change_status else None)
387 387 )
388 388
389 389 # get status if set !
390 390 if status and change_status:
391 391 ChangesetStatusModel().set_status(
392 392 c.rhodecode_db_repo.repo_id,
393 393 revision,
394 394 status,
395 395 c.rhodecode_user.user_id,
396 396 comm,
397 397 )
398 398 action_logger(self.rhodecode_user,
399 399 'user_commented_revision:%s' % revision,
400 400 c.rhodecode_db_repo, self.ip_addr, self.sa)
401 401
402 402 Session.commit()
403 403
404 404 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
405 405 return redirect(h.url('changeset_home', repo_name=repo_name,
406 406 revision=revision))
407 407
408 408 data = {
409 409 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
410 410 }
411 411 if comm:
412 412 c.co = comm
413 413 data.update(comm.get_dict())
414 414 data.update({'rendered_text':
415 415 render('changeset/changeset_comment_block.html')})
416 416
417 417 return data
418 418
419 419 @jsonify
420 420 def delete_comment(self, repo_name, comment_id):
421 421 co = ChangesetComment.get(comment_id)
422 422 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
423 423 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
424 424 ChangesetCommentsModel().delete(comment=co)
425 425 Session.commit()
426 426 return True
427 427 else:
428 428 raise HTTPForbidden()
@@ -1,158 +1,181
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.comment
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 comments model for RhodeCode
7 7
8 8 :created_on: Nov 11, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons.i18n.translation import _
30 30 from sqlalchemy.util.compat import defaultdict
31 31
32 32 from rhodecode.lib.utils2 import extract_mentioned_users, safe_unicode
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.model import BaseModel
35 35 from rhodecode.model.db import ChangesetComment, User, Repository, Notification
36 36 from rhodecode.model.notification import NotificationModel
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class ChangesetCommentsModel(BaseModel):
42 42
43 43 def __get_changeset_comment(self, changeset_comment):
44 44 return self._get_instance(ChangesetComment, changeset_comment)
45 45
46 46 def _extract_mentions(self, s):
47 47 user_objects = []
48 48 for username in extract_mentioned_users(s):
49 49 user_obj = User.get_by_username(username, case_insensitive=True)
50 50 if user_obj:
51 51 user_objects.append(user_obj)
52 52 return user_objects
53 53
54 54 def create(self, text, repo_id, user_id, revision, f_path=None,
55 55 line_no=None, status_change=None):
56 56 """
57 57 Creates new comment for changeset. IF status_change is not none
58 58 this comment is associated with a status change of changeset
59 59
60 60 :param text:
61 61 :param repo_id:
62 62 :param user_id:
63 63 :param revision:
64 64 :param f_path:
65 65 :param line_no:
66 66 :param status_change:
67 67 """
68 68
69 69 if text:
70 70 repo = Repository.get(repo_id)
71 71 cs = repo.scm_instance.get_changeset(revision)
72 72 desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256))
73 73 author_email = cs.author_email
74 74 comment = ChangesetComment()
75 75 comment.repo = repo
76 76 comment.user_id = user_id
77 77 comment.revision = revision
78 78 comment.text = text
79 79 comment.f_path = f_path
80 80 comment.line_no = line_no
81 81
82 82 self.sa.add(comment)
83 83 self.sa.flush()
84 84 # make notification
85 85 line = ''
86 86 if line_no:
87 87 line = _('on line %s') % line_no
88 88 subj = safe_unicode(
89 89 h.link_to('Re commit: %(commit_desc)s %(line)s' % \
90 90 {'commit_desc': desc, 'line': line},
91 91 h.url('changeset_home', repo_name=repo.repo_name,
92 92 revision=revision,
93 93 anchor='comment-%s' % comment.comment_id,
94 94 qualified=True,
95 95 )
96 96 )
97 97 )
98 98
99 99 body = text
100 100
101 101 # get the current participants of this changeset
102 102 recipients = ChangesetComment.get_users(revision=revision)
103 103
104 104 # add changeset author if it's in rhodecode system
105 105 recipients += [User.get_by_email(author_email)]
106 106
107 107 # create notification objects, and emails
108 108 NotificationModel().create(
109 109 created_by=user_id, subject=subj, body=body,
110 110 recipients=recipients, type_=Notification.TYPE_CHANGESET_COMMENT,
111 111 email_kwargs={'status_change': status_change}
112 112 )
113 113
114 114 mention_recipients = set(self._extract_mentions(body))\
115 115 .difference(recipients)
116 116 if mention_recipients:
117 117 subj = _('[Mention]') + ' ' + subj
118 118 NotificationModel().create(
119 119 created_by=user_id, subject=subj, body=body,
120 120 recipients=mention_recipients,
121 121 type_=Notification.TYPE_CHANGESET_COMMENT,
122 122 email_kwargs={'status_change': status_change}
123 123 )
124 124
125 125 return comment
126 126
127 127 def delete(self, comment):
128 128 """
129 129 Deletes given comment
130 130
131 131 :param comment_id:
132 132 """
133 133 comment = self.__get_changeset_comment(comment)
134 134 self.sa.delete(comment)
135 135
136 136 return comment
137 137
138 def get_comments(self, repo_id, revision):
139 return ChangesetComment.query()\
138 def get_comments(self, repo_id, revision=None, pull_request_id=None):
139 """
140 Get's main comments based on revision or pull_request_id
141
142 :param repo_id:
143 :type repo_id:
144 :param revision:
145 :type revision:
146 :param pull_request_id:
147 :type pull_request_id:
148 """
149 q = ChangesetComment.query()\
140 150 .filter(ChangesetComment.repo_id == repo_id)\
141 .filter(ChangesetComment.revision == revision)\
142 151 .filter(ChangesetComment.line_no == None)\
143 .filter(ChangesetComment.f_path == None).all()
152 .filter(ChangesetComment.f_path == None)
153 if revision:
154 q = q.filter(ChangesetComment.revision == revision)
155 elif pull_request_id:
156 q = q.filter(ChangesetComment.pull_request_id == pull_request_id)
157 else:
158 raise Exception('Please specify revision or pull_request_id')
159 return q.all()
144 160
145 def get_inline_comments(self, repo_id, revision):
146 comments = self.sa.query(ChangesetComment)\
161 def get_inline_comments(self, repo_id, revision=None, pull_request_id=None):
162 q = self.sa.query(ChangesetComment)\
147 163 .filter(ChangesetComment.repo_id == repo_id)\
148 .filter(ChangesetComment.revision == revision)\
149 164 .filter(ChangesetComment.line_no != None)\
150 165 .filter(ChangesetComment.f_path != None)\
151 166 .order_by(ChangesetComment.comment_id.asc())\
152 .all()
167
168 if revision:
169 q = q.filter(ChangesetComment.revision == revision)
170 elif pull_request_id:
171 q = q.filter(ChangesetComment.pull_request_id == pull_request_id)
172 else:
173 raise Exception('Please specify revision or pull_request_id')
174
175 comments = q.all()
153 176
154 177 paths = defaultdict(lambda: defaultdict(list))
155 178
156 179 for co in comments:
157 180 paths[co.f_path][co.line_no].append(co)
158 181 return paths.items()
@@ -1,182 +1,176
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.html"/>
4 4
5 5 <%def name="title()">
6 6 ${_('%s Changeset') % c.repo_name} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} - ${c.rhodecode_name}
7 7 </%def>
8 8
9 9 <%def name="breadcrumbs_links()">
10 10 ${h.link_to(u'Home',h.url('/'))}
11 11 &raquo;
12 12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 13 &raquo;
14 14 ${_('Changeset')} - <span class='hash'>r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)}</span>
15 15 </%def>
16 16
17 17 <%def name="page_nav()">
18 18 ${self.menu('changelog')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <div class="box">
23 23 <!-- box / title -->
24 24 <div class="title">
25 25 ${self.breadcrumbs()}
26 26 </div>
27 27 <div class="table">
28 28 <div class="diffblock">
29 29 <div class="code-header">
30 30 <div class="hash">
31 31 r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)}
32 32 </div>
33 33 <div class="date">
34 34 ${h.fmt_date(c.changeset.date)}
35 35 </div>
36 36 <div class="changeset-status-container">
37 37 %if c.statuses:
38 38 <div title="${_('Changeset status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.statuses[0])}]</div>
39 39 <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses[0])}" /></div>
40 40 %endif
41 41 </div>
42 42 <div class="diff-actions">
43 43 <a href="${h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='show')}" class="tooltip" title="${h.tooltip(_('raw diff'))}"><img class="icon" src="${h.url('/images/icons/page_white.png')}"/></a>
44 44 <a href="${h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='download')}" class="tooltip" title="${h.tooltip(_('download diff'))}"><img class="icon" src="${h.url('/images/icons/page_white_get.png')}"/></a>
45 45 ${c.ignorews_url(request.GET)}
46 46 ${c.context_url(request.GET)}
47 47 </div>
48 48 <div class="comments-number" style="float:right;padding-right:5px">${ungettext("%d comment", "%d comments", len(c.comments)) % len(c.comments)} ${ungettext("(%d inline)", "(%d inline)", c.inline_cnt) % c.inline_cnt}</div>
49 49 </div>
50 50 </div>
51 51 <div id="changeset_content">
52 52 <div class="container">
53 53 <div class="left">
54 54 <div class="author">
55 55 <div class="gravatar">
56 56 <img alt="gravatar" src="${h.gravatar_url(h.email(c.changeset.author),20)}"/>
57 57 </div>
58 58 <span>${h.person(c.changeset.author)}</span><br/>
59 59 <span><a href="mailto:${h.email_or_none(c.changeset.author)}">${h.email_or_none(c.changeset.author)}</a></span><br/>
60 60 </div>
61 61 <div class="message">${h.urlify_commit(c.changeset.message, c.repo_name)}</div>
62 62 </div>
63 63 <div class="right">
64 64 <div class="changes">
65 65 % if len(c.changeset.affected_files) <= c.affected_files_cut_off:
66 66 <span class="removed" title="${_('removed')}">${len(c.changeset.removed)}</span>
67 67 <span class="changed" title="${_('changed')}">${len(c.changeset.changed)}</span>
68 68 <span class="added" title="${_('added')}">${len(c.changeset.added)}</span>
69 69 % else:
70 70 <span class="removed" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
71 71 <span class="changed" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
72 72 <span class="added" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
73 73 % endif
74 74 </div>
75 75
76 76 %if c.changeset.parents:
77 77 %for p_cs in reversed(c.changeset.parents):
78 78 <div class="parent">${_('Parent')}
79 79 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
80 80 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
81 81 </div>
82 82 %endfor
83 83 %else:
84 84 <div class="parent">${_('No parents')}</div>
85 85 %endif
86 86 <span class="logtags">
87 87 %if len(c.changeset.parents)>1:
88 88 <span class="merge">${_('merge')}</span>
89 89 %endif
90 90 %if c.changeset.branch:
91 91 <span class="branchtag" title="${'%s %s' % (_('branch'),c.changeset.branch)}">
92 92 ${h.link_to(c.changeset.branch,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}
93 93 </span>
94 94 %endif
95 95 %for tag in c.changeset.tags:
96 96 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
97 97 ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
98 98 %endfor
99 99 </span>
100 100 </div>
101 101 </div>
102 102 <span>
103 103 ${_('%s files affected with %s insertions and %s deletions:') % (len(c.changeset.affected_files),c.lines_added,c.lines_deleted)}
104 104 </span>
105 105 <div class="cs_files">
106 106 %for change,filenode,diff,cs1,cs2,stat in c.changes:
107 107 <div class="cs_${change}">
108 108 <div class="node">
109 109 %if change != 'removed':
110 110 ${h.link_to(h.safe_unicode(filenode.path),c.anchor_url(filenode.changeset.raw_id,filenode.path,request.GET)+"_target")}
111 111 %else:
112 112 ${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID('',filenode.path)))}
113 113 %endif
114 114 </div>
115 115 <div class="changes">${h.fancy_file_stats(stat)}</div>
116 116 </div>
117 117 %endfor
118 118 % if c.cut_off:
119 119 ${_('Changeset was too big and was cut off...')}
120 120 % endif
121 121 </div>
122 122 </div>
123 123
124 124 </div>
125 125 <script>
126 126 var _USERS_AC_DATA = ${c.users_array|n};
127 127 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
128 128 </script>
129 129 ## diff block
130 130 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
131 131 ${diff_block.diff_block(c.changes)}
132 132
133 133 ## template for inline comment form
134 134 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
135 135 ${comment.comment_inline_form(c.changeset)}
136 136
137 ## render comments
138 ${comment.comments(c.changeset)}
137 ## render comments main comments form and it status
138 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.changeset.raw_id),
139 h.changeset_status(c.rhodecode_db_repo, c.changeset.raw_id))}
140
139 141 <script type="text/javascript">
140 142 YUE.onDOMReady(function(){
141 143 AJAX_COMMENT_URL = "${url('changeset_comment',repo_name=c.repo_name,revision=c.changeset.raw_id)}";
142 144 AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}"
143 145 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
144 146 var show = 'none';
145 147 var target = e.currentTarget;
146 148 if(target.checked){
147 149 var show = ''
148 150 }
149 151 var boxid = YUD.getAttribute(target,'id_for');
150 152 var comments = YUQ('#{0} .inline-comments'.format(boxid));
151 153 for(c in comments){
152 154 YUD.setStyle(comments[c],'display',show);
153 155 }
154 156 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
155 157 for(c in btns){
156 158 YUD.setStyle(btns[c],'display',show);
157 159 }
158 160 })
159 161
160 162 YUE.on(YUQ('.line'),'click',function(e){
161 163 var tr = e.currentTarget;
162 164 injectInlineForm(tr);
163 165 });
164 166
165 167 // inject comments into they proper positions
166 168 var file_comments = YUQ('.inline-comment-placeholder');
167 169 renderInlineComments(file_comments);
168 170
169 YUE.on(YUD.get('show_changeset_status_box'),'change',function(e){
170 if(e.currentTarget.checked){
171 YUD.setStyle('status_block_container','display','');
172 }
173 else{
174 YUD.setStyle('status_block_container','display','none');
175 }
176 })
177 171 })
178 172
179 173 </script>
180 174
181 175 </div>
182 176 </%def>
@@ -1,142 +1,155
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 4 ## ${comment.comment_block(co)}
5 5 ##
6 6 <%def name="comment_block(co)">
7 7 <div class="comment" id="comment-${co.comment_id}" line="${co.line_no}">
8 8 <div class="comment-wrapp">
9 9 <div class="meta">
10 10 <div style="float:left"> <img src="${h.gravatar_url(co.author.email, 20)}" /> </div>
11 11 <div class="user">
12 12 ${co.author.username}
13 13 </div>
14 14 <div class="date">
15 15 ${h.age(co.modified_at)}
16 16 </div>
17 17 %if co.status_change:
18 18 <div style="float:left" class="changeset-status-container">
19 19 <div style="float:left;padding:0px 2px 0px 2px"><span style="font-size: 18px;">&rsaquo;</span></div>
20 20 <div title="${_('Changeset status')}" class="changeset-status-lbl"> ${co.status_change.status_lbl}</div>
21 21 <div class="changeset-status-ico"><img src="${h.url(str('/images/icons/flag_status_%s.png' % co.status_change.status))}" /></div>
22 22 </div>
23 23 %endif
24 24 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or co.author.user_id == c.rhodecode_user.user_id:
25 25 <div class="buttons">
26 26 <span onClick="deleteComment(${co.comment_id})" class="delete-comment ui-btn">${_('Delete')}</span>
27 27 </div>
28 28 %endif
29 29 </div>
30 30 <div class="text">
31 31 ${h.rst_w_mentions(co.text)|n}
32 32 </div>
33 33 </div>
34 34 </div>
35 35 </%def>
36 36
37 37
38 38 <%def name="comment_inline_form(changeset)">
39 39 <div id='comment-inline-form-template' style="display:none">
40 40 <div class="comment-inline-form ac">
41 41 %if c.rhodecode_user.username != 'default':
42 42 <div class="overlay"><div class="overlay-text">${_('Submitting...')}</div></div>
43 43 ${h.form(h.url('changeset_comment', repo_name=c.repo_name, revision=changeset.raw_id),class_='inline-form')}
44 44 <div class="clearfix">
45 45 <div class="comment-help">${_('Commenting on line {1}.')}
46 46 ${(_('Comments parsed using %s syntax with %s support.') % (
47 47 ('<a href="%s">RST</a>' % h.url('rst_help')),
48 48 ('<span style="color:#003367" class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
49 49 )
50 50 )|n
51 51 }
52 52 </div>
53 53 <div class="mentions-container" id="mentions_container_{1}"></div>
54 54 <textarea id="text_{1}" name="text" class="yui-ac-input"></textarea>
55 55 </div>
56 56 <div class="comment-button">
57 57 <input type="hidden" name="f_path" value="{0}">
58 58 <input type="hidden" name="line" value="{1}">
59 59 ${h.submit('save', _('Comment'), class_='ui-btn save-inline-form')}
60 60 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
61 61 </div>
62 62 ${h.end_form()}
63 63 %else:
64 64 ${h.form('')}
65 65 <div class="clearfix">
66 66 <div class="comment-help">
67 67 ${_('You need to be logged in to comment.')} <a href="${h.url('login_home',came_from=h.url.current())}">${_('Login now')}</a>
68 68 </div>
69 69 </div>
70 70 <div class="comment-button">
71 71 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
72 72 </div>
73 73 ${h.end_form()}
74 74 %endif
75 75 </div>
76 76 </div>
77 77 </%def>
78 78
79 79
80 <%def name="inlines(changeset)">
80 ## generates inlines taken from c.comments var
81 <%def name="inlines()">
81 82 <div class="comments-number">${ungettext("%d comment", "%d comments", len(c.comments)) % len(c.comments)} ${ungettext("(%d inline)", "(%d inline)", c.inline_cnt) % c.inline_cnt}</div>
82 83 %for path, lines in c.inline_comments:
83 84 % for line,comments in lines.iteritems():
84 85 <div style="display:none" class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
85 86 %for co in comments:
86 87 ${comment_block(co)}
87 88 %endfor
88 89 </div>
89 90 %endfor
90 91 %endfor
91 92
92 93 </%def>
93 94
94 95 ## MAIN COMMENT FORM
95 <%def name="comments(changeset)">
96 <%def name="comments(post_url, cur_status)">
96 97
97 98 <div class="comments">
98 99 <div id="inline-comments-container">
99 ${inlines(changeset)}
100 ## generate inlines for this changeset
101 ${inlines()}
100 102 </div>
101 103
102 104 %for co in c.comments:
103 105 <div id="comment-tr-${co.comment_id}">
104 106 ${comment_block(co)}
105 107 </div>
106 108 %endfor
107 109 %if c.rhodecode_user.username != 'default':
108 110 <div class="comment-form ac">
109 ${h.form(h.url('changeset_comment', repo_name=c.repo_name, revision=changeset.raw_id))}
111 ${h.form(post_url)}
110 112 <strong>${_('Leave a comment')}</strong>
111 113 <div class="clearfix">
112 114 <div class="comment-help">
113 115 ${(_('Comments parsed using %s syntax with %s support.') % (('<a href="%s">RST</a>' % h.url('rst_help')),
114 116 '<span style="color:#003367" class="tooltip" title="%s">@mention</span>' %
115 117 _('Use @username inside this text to send notification to this RhodeCode user')))|n}
116 118 | <span class="tooltip" title="${_('Check this to change current status of code-review for this changeset')}"> ${_('change status')}
117 119 <input style="vertical-align: bottom;margin-bottom:-2px" id="show_changeset_status_box" type="checkbox" name="change_changeset_status" />
118 120 </span>
119 121 </div>
120 122 <div id="status_block_container" class="status-block" style="display:none">
121 123 %for status,lbl in c.changeset_statuses:
122 124 <div class="">
123 <img src="${h.url('/images/icons/flag_status_%s.png' % status)}" /> <input ${'checked="checked"' if status == h.changeset_status(c.rhodecode_db_repo, c.changeset.raw_id) else ''}" type="radio" name="changeset_status" value="${status}"> <label>${lbl}</label>
125 <img src="${h.url('/images/icons/flag_status_%s.png' % status)}" /> <input ${'checked="checked"' if status == cur_status else ''}" type="radio" name="changeset_status" value="${status}"> <label>${lbl}</label>
124 126 </div>
125 127 %endfor
126 128 </div>
127 129 <div class="mentions-container" id="mentions_container"></div>
128 130 ${h.textarea('text')}
129 131 </div>
130 132 <div class="comment-button">
131 133 ${h.submit('save', _('Comment'), class_='ui-button')}
132 134 </div>
133 135 ${h.end_form()}
134 136 </div>
135 137 %endif
136 138 </div>
137 139 <script>
138 140 YUE.onDOMReady(function () {
139 141 MentionsAutoComplete('text', 'mentions_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
142
143 // changeset status box listener
144 YUE.on(YUD.get('show_changeset_status_box'),'change',function(e){
145 if(e.currentTarget.checked){
146 YUD.setStyle('status_block_container','display','');
147 }
148 else{
149 YUD.setStyle('status_block_container','display','none');
150 }
151 })
152
140 153 });
141 154 </script>
142 155 </%def>
General Comments 0
You need to be logged in to leave comments. Login now