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