##// END OF EJS Templates
comments: small fixes for range commits comments
milka -
r4551:d3ec0df2 default
parent child Browse files
Show More
@@ -1,802 +1,802 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 from pyramid.httpexceptions import (
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.apps.file_store import utils as store_utils
31 from rhodecode.apps.file_store import utils as store_utils
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33
33
34 from rhodecode.lib import diffs, codeblocks, channelstream
34 from rhodecode.lib import diffs, codeblocks, channelstream
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.diffs import (
39 from rhodecode.lib.diffs import (
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 get_diff_whitespace_flag)
41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 import rhodecode.lib.helpers as h
43 import rhodecode.lib.helpers as h
44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 RepositoryError, CommitDoesNotExistError)
47 RepositoryError, CommitDoesNotExistError)
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 ChangesetCommentHistory
49 ChangesetCommentHistory
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.settings import VcsSettingsModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 def _update_with_GET(params, request):
58 def _update_with_GET(params, request):
59 for k in ['diff1', 'diff2', 'diff']:
59 for k in ['diff1', 'diff2', 'diff']:
60 params[k] += request.GET.getall(k)
60 params[k] += request.GET.getall(k)
61
61
62
62
63 class RepoCommitsView(RepoAppView):
63 class RepoCommitsView(RepoAppView):
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context(include_app_defaults=True)
65 c = self._get_local_tmpl_context(include_app_defaults=True)
66 c.rhodecode_repo = self.rhodecode_vcs_repo
66 c.rhodecode_repo = self.rhodecode_vcs_repo
67
67
68 return c
68 return c
69
69
70 def _is_diff_cache_enabled(self, target_repo):
70 def _is_diff_cache_enabled(self, target_repo):
71 caching_enabled = self._get_general_setting(
71 caching_enabled = self._get_general_setting(
72 target_repo, 'rhodecode_diff_cache')
72 target_repo, 'rhodecode_diff_cache')
73 log.debug('Diff caching enabled: %s', caching_enabled)
73 log.debug('Diff caching enabled: %s', caching_enabled)
74 return caching_enabled
74 return caching_enabled
75
75
76 def _commit(self, commit_id_range, method):
76 def _commit(self, commit_id_range, method):
77 _ = self.request.translate
77 _ = self.request.translate
78 c = self.load_default_context()
78 c = self.load_default_context()
79 c.fulldiff = self.request.GET.get('fulldiff')
79 c.fulldiff = self.request.GET.get('fulldiff')
80
80
81 # fetch global flags of ignore ws or context lines
81 # fetch global flags of ignore ws or context lines
82 diff_context = get_diff_context(self.request)
82 diff_context = get_diff_context(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84
84
85 # diff_limit will cut off the whole diff if the limit is applied
85 # diff_limit will cut off the whole diff if the limit is applied
86 # otherwise it will just hide the big files from the front-end
86 # otherwise it will just hide the big files from the front-end
87 diff_limit = c.visual.cut_off_limit_diff
87 diff_limit = c.visual.cut_off_limit_diff
88 file_limit = c.visual.cut_off_limit_file
88 file_limit = c.visual.cut_off_limit_file
89
89
90 # get ranges of commit ids if preset
90 # get ranges of commit ids if preset
91 commit_range = commit_id_range.split('...')[:2]
91 commit_range = commit_id_range.split('...')[:2]
92
92
93 try:
93 try:
94 pre_load = ['affected_files', 'author', 'branch', 'date',
94 pre_load = ['affected_files', 'author', 'branch', 'date',
95 'message', 'parents']
95 'message', 'parents']
96 if self.rhodecode_vcs_repo.alias == 'hg':
96 if self.rhodecode_vcs_repo.alias == 'hg':
97 pre_load += ['hidden', 'obsolete', 'phase']
97 pre_load += ['hidden', 'obsolete', 'phase']
98
98
99 if len(commit_range) == 2:
99 if len(commit_range) == 2:
100 commits = self.rhodecode_vcs_repo.get_commits(
100 commits = self.rhodecode_vcs_repo.get_commits(
101 start_id=commit_range[0], end_id=commit_range[1],
101 start_id=commit_range[0], end_id=commit_range[1],
102 pre_load=pre_load, translate_tags=False)
102 pre_load=pre_load, translate_tags=False)
103 commits = list(commits)
103 commits = list(commits)
104 else:
104 else:
105 commits = [self.rhodecode_vcs_repo.get_commit(
105 commits = [self.rhodecode_vcs_repo.get_commit(
106 commit_id=commit_id_range, pre_load=pre_load)]
106 commit_id=commit_id_range, pre_load=pre_load)]
107
107
108 c.commit_ranges = commits
108 c.commit_ranges = commits
109 if not c.commit_ranges:
109 if not c.commit_ranges:
110 raise RepositoryError('The commit range returned an empty result')
110 raise RepositoryError('The commit range returned an empty result')
111 except CommitDoesNotExistError as e:
111 except CommitDoesNotExistError as e:
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 h.flash(msg, category='error')
113 h.flash(msg, category='error')
114 raise HTTPNotFound()
114 raise HTTPNotFound()
115 except Exception:
115 except Exception:
116 log.exception("General failure")
116 log.exception("General failure")
117 raise HTTPNotFound()
117 raise HTTPNotFound()
118 single_commit = len(c.commit_ranges) == 1
118 single_commit = len(c.commit_ranges) == 1
119
119
120 c.changes = OrderedDict()
120 c.changes = OrderedDict()
121 c.lines_added = 0
121 c.lines_added = 0
122 c.lines_deleted = 0
122 c.lines_deleted = 0
123
123
124 # auto collapse if we have more than limit
124 # auto collapse if we have more than limit
125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127
127
128 c.commit_statuses = ChangesetStatus.STATUSES
128 c.commit_statuses = ChangesetStatus.STATUSES
129 c.inline_comments = []
129 c.inline_comments = []
130 c.files = []
130 c.files = []
131
131
132 c.comments = []
132 c.comments = []
133 c.unresolved_comments = []
133 c.unresolved_comments = []
134 c.resolved_comments = []
134 c.resolved_comments = []
135
135
136 # Single commit
136 # Single commit
137 if single_commit:
137 if single_commit:
138 commit = c.commit_ranges[0]
138 commit = c.commit_ranges[0]
139 c.comments = CommentsModel().get_comments(
139 c.comments = CommentsModel().get_comments(
140 self.db_repo.repo_id,
140 self.db_repo.repo_id,
141 revision=commit.raw_id)
141 revision=commit.raw_id)
142
142
143 # comments from PR
143 # comments from PR
144 statuses = ChangesetStatusModel().get_statuses(
144 statuses = ChangesetStatusModel().get_statuses(
145 self.db_repo.repo_id, commit.raw_id,
145 self.db_repo.repo_id, commit.raw_id,
146 with_revisions=True)
146 with_revisions=True)
147
147
148 prs = set()
148 prs = set()
149 reviewers = list()
149 reviewers = list()
150 reviewers_duplicates = set() # to not have duplicates from multiple votes
150 reviewers_duplicates = set() # to not have duplicates from multiple votes
151 for c_status in statuses:
151 for c_status in statuses:
152
152
153 # extract associated pull-requests from votes
153 # extract associated pull-requests from votes
154 if c_status.pull_request:
154 if c_status.pull_request:
155 prs.add(c_status.pull_request)
155 prs.add(c_status.pull_request)
156
156
157 # extract reviewers
157 # extract reviewers
158 _user_id = c_status.author.user_id
158 _user_id = c_status.author.user_id
159 if _user_id not in reviewers_duplicates:
159 if _user_id not in reviewers_duplicates:
160 reviewers.append(
160 reviewers.append(
161 StrictAttributeDict({
161 StrictAttributeDict({
162 'user': c_status.author,
162 'user': c_status.author,
163
163
164 # fake attributed for commit, page that we don't have
164 # fake attributed for commit, page that we don't have
165 # but we share the display with PR page
165 # but we share the display with PR page
166 'mandatory': False,
166 'mandatory': False,
167 'reasons': [],
167 'reasons': [],
168 'rule_user_group_data': lambda: None
168 'rule_user_group_data': lambda: None
169 })
169 })
170 )
170 )
171 reviewers_duplicates.add(_user_id)
171 reviewers_duplicates.add(_user_id)
172
172
173 c.reviewers_count = len(reviewers)
173 c.reviewers_count = len(reviewers)
174 c.observers_count = 0
174 c.observers_count = 0
175
175
176 # from associated statuses, check the pull requests, and
176 # from associated statuses, check the pull requests, and
177 # show comments from them
177 # show comments from them
178 for pr in prs:
178 for pr in prs:
179 c.comments.extend(pr.comments)
179 c.comments.extend(pr.comments)
180
180
181 c.unresolved_comments = CommentsModel()\
181 c.unresolved_comments = CommentsModel()\
182 .get_commit_unresolved_todos(commit.raw_id)
182 .get_commit_unresolved_todos(commit.raw_id)
183 c.resolved_comments = CommentsModel()\
183 c.resolved_comments = CommentsModel()\
184 .get_commit_resolved_todos(commit.raw_id)
184 .get_commit_resolved_todos(commit.raw_id)
185
185
186 c.inline_comments_flat = CommentsModel()\
186 c.inline_comments_flat = CommentsModel()\
187 .get_commit_inline_comments(commit.raw_id)
187 .get_commit_inline_comments(commit.raw_id)
188
188
189 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
189 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
190 statuses, reviewers)
190 statuses, reviewers)
191
191
192 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
192 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
193
193
194 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
194 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
195
195
196 for review_obj, member, reasons, mandatory, status in review_statuses:
196 for review_obj, member, reasons, mandatory, status in review_statuses:
197 member_reviewer = h.reviewer_as_json(
197 member_reviewer = h.reviewer_as_json(
198 member, reasons=reasons, mandatory=mandatory, role=None,
198 member, reasons=reasons, mandatory=mandatory, role=None,
199 user_group=None
199 user_group=None
200 )
200 )
201
201
202 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
202 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
203 member_reviewer['review_status'] = current_review_status
203 member_reviewer['review_status'] = current_review_status
204 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
204 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
205 member_reviewer['allowed_to_update'] = False
205 member_reviewer['allowed_to_update'] = False
206 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
206 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
207
207
208 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
208 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
209
209
210 # NOTE(marcink): this uses the same voting logic as in pull-requests
210 # NOTE(marcink): this uses the same voting logic as in pull-requests
211 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
211 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
212 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
212 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
213
213
214 diff = None
214 diff = None
215 # Iterate over ranges (default commit view is always one commit)
215 # Iterate over ranges (default commit view is always one commit)
216 for commit in c.commit_ranges:
216 for commit in c.commit_ranges:
217 c.changes[commit.raw_id] = []
217 c.changes[commit.raw_id] = []
218
218
219 commit2 = commit
219 commit2 = commit
220 commit1 = commit.first_parent
220 commit1 = commit.first_parent
221
221
222 if method == 'show':
222 if method == 'show':
223 inline_comments = CommentsModel().get_inline_comments(
223 inline_comments = CommentsModel().get_inline_comments(
224 self.db_repo.repo_id, revision=commit.raw_id)
224 self.db_repo.repo_id, revision=commit.raw_id)
225 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
225 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
226 inline_comments))
226 inline_comments))
227 c.inline_comments = inline_comments
227 c.inline_comments = inline_comments
228
228
229 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
229 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
230 self.db_repo)
230 self.db_repo)
231 cache_file_path = diff_cache_exist(
231 cache_file_path = diff_cache_exist(
232 cache_path, 'diff', commit.raw_id,
232 cache_path, 'diff', commit.raw_id,
233 hide_whitespace_changes, diff_context, c.fulldiff)
233 hide_whitespace_changes, diff_context, c.fulldiff)
234
234
235 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
235 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
236 force_recache = str2bool(self.request.GET.get('force_recache'))
236 force_recache = str2bool(self.request.GET.get('force_recache'))
237
237
238 cached_diff = None
238 cached_diff = None
239 if caching_enabled:
239 if caching_enabled:
240 cached_diff = load_cached_diff(cache_file_path)
240 cached_diff = load_cached_diff(cache_file_path)
241
241
242 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
242 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
243 if not force_recache and has_proper_diff_cache:
243 if not force_recache and has_proper_diff_cache:
244 diffset = cached_diff['diff']
244 diffset = cached_diff['diff']
245 else:
245 else:
246 vcs_diff = self.rhodecode_vcs_repo.get_diff(
246 vcs_diff = self.rhodecode_vcs_repo.get_diff(
247 commit1, commit2,
247 commit1, commit2,
248 ignore_whitespace=hide_whitespace_changes,
248 ignore_whitespace=hide_whitespace_changes,
249 context=diff_context)
249 context=diff_context)
250
250
251 diff_processor = diffs.DiffProcessor(
251 diff_processor = diffs.DiffProcessor(
252 vcs_diff, format='newdiff', diff_limit=diff_limit,
252 vcs_diff, format='newdiff', diff_limit=diff_limit,
253 file_limit=file_limit, show_full_diff=c.fulldiff)
253 file_limit=file_limit, show_full_diff=c.fulldiff)
254
254
255 _parsed = diff_processor.prepare()
255 _parsed = diff_processor.prepare()
256
256
257 diffset = codeblocks.DiffSet(
257 diffset = codeblocks.DiffSet(
258 repo_name=self.db_repo_name,
258 repo_name=self.db_repo_name,
259 source_node_getter=codeblocks.diffset_node_getter(commit1),
259 source_node_getter=codeblocks.diffset_node_getter(commit1),
260 target_node_getter=codeblocks.diffset_node_getter(commit2))
260 target_node_getter=codeblocks.diffset_node_getter(commit2))
261
261
262 diffset = self.path_filter.render_patchset_filtered(
262 diffset = self.path_filter.render_patchset_filtered(
263 diffset, _parsed, commit1.raw_id, commit2.raw_id)
263 diffset, _parsed, commit1.raw_id, commit2.raw_id)
264
264
265 # save cached diff
265 # save cached diff
266 if caching_enabled:
266 if caching_enabled:
267 cache_diff(cache_file_path, diffset, None)
267 cache_diff(cache_file_path, diffset, None)
268
268
269 c.limited_diff = diffset.limited_diff
269 c.limited_diff = diffset.limited_diff
270 c.changes[commit.raw_id] = diffset
270 c.changes[commit.raw_id] = diffset
271 else:
271 else:
272 # TODO(marcink): no cache usage here...
272 # TODO(marcink): no cache usage here...
273 _diff = self.rhodecode_vcs_repo.get_diff(
273 _diff = self.rhodecode_vcs_repo.get_diff(
274 commit1, commit2,
274 commit1, commit2,
275 ignore_whitespace=hide_whitespace_changes, context=diff_context)
275 ignore_whitespace=hide_whitespace_changes, context=diff_context)
276 diff_processor = diffs.DiffProcessor(
276 diff_processor = diffs.DiffProcessor(
277 _diff, format='newdiff', diff_limit=diff_limit,
277 _diff, format='newdiff', diff_limit=diff_limit,
278 file_limit=file_limit, show_full_diff=c.fulldiff)
278 file_limit=file_limit, show_full_diff=c.fulldiff)
279 # downloads/raw we only need RAW diff nothing else
279 # downloads/raw we only need RAW diff nothing else
280 diff = self.path_filter.get_raw_patch(diff_processor)
280 diff = self.path_filter.get_raw_patch(diff_processor)
281 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
281 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
282
282
283 # sort comments by how they were generated
283 # sort comments by how they were generated
284 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
284 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
285 c.at_version_num = None
285 c.at_version_num = None
286
286
287 if len(c.commit_ranges) == 1:
287 if len(c.commit_ranges) == 1:
288 c.commit = c.commit_ranges[0]
288 c.commit = c.commit_ranges[0]
289 c.parent_tmpl = ''.join(
289 c.parent_tmpl = ''.join(
290 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
290 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
291
291
292 if method == 'download':
292 if method == 'download':
293 response = Response(diff)
293 response = Response(diff)
294 response.content_type = 'text/plain'
294 response.content_type = 'text/plain'
295 response.content_disposition = (
295 response.content_disposition = (
296 'attachment; filename=%s.diff' % commit_id_range[:12])
296 'attachment; filename=%s.diff' % commit_id_range[:12])
297 return response
297 return response
298 elif method == 'patch':
298 elif method == 'patch':
299 c.diff = safe_unicode(diff)
299 c.diff = safe_unicode(diff)
300 patch = render(
300 patch = render(
301 'rhodecode:templates/changeset/patch_changeset.mako',
301 'rhodecode:templates/changeset/patch_changeset.mako',
302 self._get_template_context(c), self.request)
302 self._get_template_context(c), self.request)
303 response = Response(patch)
303 response = Response(patch)
304 response.content_type = 'text/plain'
304 response.content_type = 'text/plain'
305 return response
305 return response
306 elif method == 'raw':
306 elif method == 'raw':
307 response = Response(diff)
307 response = Response(diff)
308 response.content_type = 'text/plain'
308 response.content_type = 'text/plain'
309 return response
309 return response
310 elif method == 'show':
310 elif method == 'show':
311 if len(c.commit_ranges) == 1:
311 if len(c.commit_ranges) == 1:
312 html = render(
312 html = render(
313 'rhodecode:templates/changeset/changeset.mako',
313 'rhodecode:templates/changeset/changeset.mako',
314 self._get_template_context(c), self.request)
314 self._get_template_context(c), self.request)
315 return Response(html)
315 return Response(html)
316 else:
316 else:
317 c.ancestor = None
317 c.ancestor = None
318 c.target_repo = self.db_repo
318 c.target_repo = self.db_repo
319 html = render(
319 html = render(
320 'rhodecode:templates/changeset/changeset_range.mako',
320 'rhodecode:templates/changeset/changeset_range.mako',
321 self._get_template_context(c), self.request)
321 self._get_template_context(c), self.request)
322 return Response(html)
322 return Response(html)
323
323
324 raise HTTPBadRequest()
324 raise HTTPBadRequest()
325
325
326 @LoginRequired()
326 @LoginRequired()
327 @HasRepoPermissionAnyDecorator(
327 @HasRepoPermissionAnyDecorator(
328 'repository.read', 'repository.write', 'repository.admin')
328 'repository.read', 'repository.write', 'repository.admin')
329 @view_config(
329 @view_config(
330 route_name='repo_commit', request_method='GET',
330 route_name='repo_commit', request_method='GET',
331 renderer=None)
331 renderer=None)
332 def repo_commit_show(self):
332 def repo_commit_show(self):
333 commit_id = self.request.matchdict['commit_id']
333 commit_id = self.request.matchdict['commit_id']
334 return self._commit(commit_id, method='show')
334 return self._commit(commit_id, method='show')
335
335
336 @LoginRequired()
336 @LoginRequired()
337 @HasRepoPermissionAnyDecorator(
337 @HasRepoPermissionAnyDecorator(
338 'repository.read', 'repository.write', 'repository.admin')
338 'repository.read', 'repository.write', 'repository.admin')
339 @view_config(
339 @view_config(
340 route_name='repo_commit_raw', request_method='GET',
340 route_name='repo_commit_raw', request_method='GET',
341 renderer=None)
341 renderer=None)
342 @view_config(
342 @view_config(
343 route_name='repo_commit_raw_deprecated', request_method='GET',
343 route_name='repo_commit_raw_deprecated', request_method='GET',
344 renderer=None)
344 renderer=None)
345 def repo_commit_raw(self):
345 def repo_commit_raw(self):
346 commit_id = self.request.matchdict['commit_id']
346 commit_id = self.request.matchdict['commit_id']
347 return self._commit(commit_id, method='raw')
347 return self._commit(commit_id, method='raw')
348
348
349 @LoginRequired()
349 @LoginRequired()
350 @HasRepoPermissionAnyDecorator(
350 @HasRepoPermissionAnyDecorator(
351 'repository.read', 'repository.write', 'repository.admin')
351 'repository.read', 'repository.write', 'repository.admin')
352 @view_config(
352 @view_config(
353 route_name='repo_commit_patch', request_method='GET',
353 route_name='repo_commit_patch', request_method='GET',
354 renderer=None)
354 renderer=None)
355 def repo_commit_patch(self):
355 def repo_commit_patch(self):
356 commit_id = self.request.matchdict['commit_id']
356 commit_id = self.request.matchdict['commit_id']
357 return self._commit(commit_id, method='patch')
357 return self._commit(commit_id, method='patch')
358
358
359 @LoginRequired()
359 @LoginRequired()
360 @HasRepoPermissionAnyDecorator(
360 @HasRepoPermissionAnyDecorator(
361 'repository.read', 'repository.write', 'repository.admin')
361 'repository.read', 'repository.write', 'repository.admin')
362 @view_config(
362 @view_config(
363 route_name='repo_commit_download', request_method='GET',
363 route_name='repo_commit_download', request_method='GET',
364 renderer=None)
364 renderer=None)
365 def repo_commit_download(self):
365 def repo_commit_download(self):
366 commit_id = self.request.matchdict['commit_id']
366 commit_id = self.request.matchdict['commit_id']
367 return self._commit(commit_id, method='download')
367 return self._commit(commit_id, method='download')
368
368
369 @LoginRequired()
369 @LoginRequired()
370 @NotAnonymous()
370 @NotAnonymous()
371 @HasRepoPermissionAnyDecorator(
371 @HasRepoPermissionAnyDecorator(
372 'repository.read', 'repository.write', 'repository.admin')
372 'repository.read', 'repository.write', 'repository.admin')
373 @CSRFRequired()
373 @CSRFRequired()
374 @view_config(
374 @view_config(
375 route_name='repo_commit_comment_create', request_method='POST',
375 route_name='repo_commit_comment_create', request_method='POST',
376 renderer='json_ext')
376 renderer='json_ext')
377 def repo_commit_comment_create(self):
377 def repo_commit_comment_create(self):
378 _ = self.request.translate
378 _ = self.request.translate
379 commit_id = self.request.matchdict['commit_id']
379 commit_id = self.request.matchdict['commit_id']
380
380
381 c = self.load_default_context()
381 c = self.load_default_context()
382 status = self.request.POST.get('changeset_status', None)
382 status = self.request.POST.get('changeset_status', None)
383 is_draft = str2bool(self.request.POST.get('draft'))
383 is_draft = str2bool(self.request.POST.get('draft'))
384 text = self.request.POST.get('text')
384 text = self.request.POST.get('text')
385 comment_type = self.request.POST.get('comment_type')
385 comment_type = self.request.POST.get('comment_type')
386 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
386 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
387 f_path = self.request.POST.get('f_path')
387 f_path = self.request.POST.get('f_path')
388 line_no = self.request.POST.get('line')
388 line_no = self.request.POST.get('line')
389 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
389 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
390
390
391 if status:
391 if status:
392 text = text or (_('Status change %(transition_icon)s %(status)s')
392 text = text or (_('Status change %(transition_icon)s %(status)s')
393 % {'transition_icon': '>',
393 % {'transition_icon': '>',
394 'status': ChangesetStatus.get_status_lbl(status)})
394 'status': ChangesetStatus.get_status_lbl(status)})
395
395
396 multi_commit_ids = []
396 multi_commit_ids = []
397 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
397 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
398 if _commit_id not in ['', None, EmptyCommit.raw_id]:
398 if _commit_id not in ['', None, EmptyCommit.raw_id]:
399 if _commit_id not in multi_commit_ids:
399 if _commit_id not in multi_commit_ids:
400 multi_commit_ids.append(_commit_id)
400 multi_commit_ids.append(_commit_id)
401
401
402 commit_ids = multi_commit_ids or [commit_id]
402 commit_ids = multi_commit_ids or [commit_id]
403
403
404 data = {}
404 data = {}
405 # Multiple comments for each passed commit id
405 # Multiple comments for each passed commit id
406 for current_id in filter(None, commit_ids):
406 for current_id in filter(None, commit_ids):
407 comment = CommentsModel().create(
407 comment = CommentsModel().create(
408 text=text,
408 text=text,
409 repo=self.db_repo.repo_id,
409 repo=self.db_repo.repo_id,
410 user=self._rhodecode_db_user.user_id,
410 user=self._rhodecode_db_user.user_id,
411 commit_id=current_id,
411 commit_id=current_id,
412 f_path=f_path,
412 f_path=f_path,
413 line_no=line_no,
413 line_no=line_no,
414 status_change=(ChangesetStatus.get_status_lbl(status)
414 status_change=(ChangesetStatus.get_status_lbl(status)
415 if status else None),
415 if status else None),
416 status_change_type=status,
416 status_change_type=status,
417 comment_type=comment_type,
417 comment_type=comment_type,
418 is_draft=is_draft,
418 is_draft=is_draft,
419 resolves_comment_id=resolves_comment_id,
419 resolves_comment_id=resolves_comment_id,
420 auth_user=self._rhodecode_user,
420 auth_user=self._rhodecode_user,
421 send_email=not is_draft, # skip notification for draft comments
421 send_email=not is_draft, # skip notification for draft comments
422 )
422 )
423 is_inline = comment.is_inline
423 is_inline = comment.is_inline
424
424
425 # get status if set !
425 # get status if set !
426 if status:
426 if status:
427 # if latest status was from pull request and it's closed
427 # if latest status was from pull request and it's closed
428 # disallow changing status !
428 # disallow changing status !
429 # dont_allow_on_closed_pull_request = True !
429 # dont_allow_on_closed_pull_request = True !
430
430
431 try:
431 try:
432 ChangesetStatusModel().set_status(
432 ChangesetStatusModel().set_status(
433 self.db_repo.repo_id,
433 self.db_repo.repo_id,
434 status,
434 status,
435 self._rhodecode_db_user.user_id,
435 self._rhodecode_db_user.user_id,
436 comment,
436 comment,
437 revision=current_id,
437 revision=current_id,
438 dont_allow_on_closed_pull_request=True
438 dont_allow_on_closed_pull_request=True
439 )
439 )
440 except StatusChangeOnClosedPullRequestError:
440 except StatusChangeOnClosedPullRequestError:
441 msg = _('Changing the status of a commit associated with '
441 msg = _('Changing the status of a commit associated with '
442 'a closed pull request is not allowed')
442 'a closed pull request is not allowed')
443 log.exception(msg)
443 log.exception(msg)
444 h.flash(msg, category='warning')
444 h.flash(msg, category='warning')
445 raise HTTPFound(h.route_path(
445 raise HTTPFound(h.route_path(
446 'repo_commit', repo_name=self.db_repo_name,
446 'repo_commit', repo_name=self.db_repo_name,
447 commit_id=current_id))
447 commit_id=current_id))
448
448
449 # skip notifications for drafts
449 # skip notifications for drafts
450 if not is_draft:
450 if not is_draft:
451 commit = self.db_repo.get_commit(current_id)
451 commit = self.db_repo.get_commit(current_id)
452 CommentsModel().trigger_commit_comment_hook(
452 CommentsModel().trigger_commit_comment_hook(
453 self.db_repo, self._rhodecode_user, 'create',
453 self.db_repo, self._rhodecode_user, 'create',
454 data={'comment': comment, 'commit': commit})
454 data={'comment': comment, 'commit': commit})
455
455
456 comment_id = comment.comment_id
456 comment_id = comment.comment_id
457 data[comment_id] = {
457 data[comment_id] = {
458 'target_id': target_elem_id
458 'target_id': target_elem_id
459 }
459 }
460 c.co = comment
460 c.co = comment
461 c.at_version_num = 0
461 c.at_version_num = 0
462 c.is_new = True
462 c.is_new = True
463 rendered_comment = render(
463 rendered_comment = render(
464 'rhodecode:templates/changeset/changeset_comment_block.mako',
464 'rhodecode:templates/changeset/changeset_comment_block.mako',
465 self._get_template_context(c), self.request)
465 self._get_template_context(c), self.request)
466
466
467 data[comment_id].update(comment.get_dict())
467 data[comment_id].update(comment.get_dict())
468 data[comment_id].update({'rendered_text': rendered_comment})
468 data[comment_id].update({'rendered_text': rendered_comment})
469
469
470 # skip channelstream for draft comments
470 # skip channelstream for draft comments
471 if not is_draft:
471 if not is_draft:
472 comment_broadcast_channel = channelstream.comment_channel(
472 comment_broadcast_channel = channelstream.comment_channel(
473 self.db_repo_name, commit_obj=commit)
473 self.db_repo_name, commit_obj=commit)
474
474
475 comment_data = data
475 comment_data = data
476 comment_type = 'inline' if is_inline else 'general'
476 posted_comment_type = 'inline' if is_inline else 'general'
477 channelstream.comment_channelstream_push(
477 channelstream.comment_channelstream_push(
478 self.request, comment_broadcast_channel, self._rhodecode_user,
478 self.request, comment_broadcast_channel, self._rhodecode_user,
479 _('posted a new {} comment').format(comment_type),
479 _('posted a new {} comment').format(posted_comment_type),
480 comment_data=comment_data)
480 comment_data=comment_data)
481
481
482 # finalize, commit and redirect
482 # finalize, commit and redirect
483 Session().commit()
483 Session().commit()
484
484
485 return data
485 return data
486
486
487 @LoginRequired()
487 @LoginRequired()
488 @NotAnonymous()
488 @NotAnonymous()
489 @HasRepoPermissionAnyDecorator(
489 @HasRepoPermissionAnyDecorator(
490 'repository.read', 'repository.write', 'repository.admin')
490 'repository.read', 'repository.write', 'repository.admin')
491 @CSRFRequired()
491 @CSRFRequired()
492 @view_config(
492 @view_config(
493 route_name='repo_commit_comment_preview', request_method='POST',
493 route_name='repo_commit_comment_preview', request_method='POST',
494 renderer='string', xhr=True)
494 renderer='string', xhr=True)
495 def repo_commit_comment_preview(self):
495 def repo_commit_comment_preview(self):
496 # Technically a CSRF token is not needed as no state changes with this
496 # Technically a CSRF token is not needed as no state changes with this
497 # call. However, as this is a POST is better to have it, so automated
497 # call. However, as this is a POST is better to have it, so automated
498 # tools don't flag it as potential CSRF.
498 # tools don't flag it as potential CSRF.
499 # Post is required because the payload could be bigger than the maximum
499 # Post is required because the payload could be bigger than the maximum
500 # allowed by GET.
500 # allowed by GET.
501
501
502 text = self.request.POST.get('text')
502 text = self.request.POST.get('text')
503 renderer = self.request.POST.get('renderer') or 'rst'
503 renderer = self.request.POST.get('renderer') or 'rst'
504 if text:
504 if text:
505 return h.render(text, renderer=renderer, mentions=True,
505 return h.render(text, renderer=renderer, mentions=True,
506 repo_name=self.db_repo_name)
506 repo_name=self.db_repo_name)
507 return ''
507 return ''
508
508
509 @LoginRequired()
509 @LoginRequired()
510 @HasRepoPermissionAnyDecorator(
510 @HasRepoPermissionAnyDecorator(
511 'repository.read', 'repository.write', 'repository.admin')
511 'repository.read', 'repository.write', 'repository.admin')
512 @CSRFRequired()
512 @CSRFRequired()
513 @view_config(
513 @view_config(
514 route_name='repo_commit_comment_history_view', request_method='POST',
514 route_name='repo_commit_comment_history_view', request_method='POST',
515 renderer='string', xhr=True)
515 renderer='string', xhr=True)
516 def repo_commit_comment_history_view(self):
516 def repo_commit_comment_history_view(self):
517 c = self.load_default_context()
517 c = self.load_default_context()
518
518
519 comment_history_id = self.request.matchdict['comment_history_id']
519 comment_history_id = self.request.matchdict['comment_history_id']
520 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
520 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
521 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
521 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
522
522
523 if is_repo_comment:
523 if is_repo_comment:
524 c.comment_history = comment_history
524 c.comment_history = comment_history
525
525
526 rendered_comment = render(
526 rendered_comment = render(
527 'rhodecode:templates/changeset/comment_history.mako',
527 'rhodecode:templates/changeset/comment_history.mako',
528 self._get_template_context(c)
528 self._get_template_context(c)
529 , self.request)
529 , self.request)
530 return rendered_comment
530 return rendered_comment
531 else:
531 else:
532 log.warning('No permissions for user %s to show comment_history_id: %s',
532 log.warning('No permissions for user %s to show comment_history_id: %s',
533 self._rhodecode_db_user, comment_history_id)
533 self._rhodecode_db_user, comment_history_id)
534 raise HTTPNotFound()
534 raise HTTPNotFound()
535
535
536 @LoginRequired()
536 @LoginRequired()
537 @NotAnonymous()
537 @NotAnonymous()
538 @HasRepoPermissionAnyDecorator(
538 @HasRepoPermissionAnyDecorator(
539 'repository.read', 'repository.write', 'repository.admin')
539 'repository.read', 'repository.write', 'repository.admin')
540 @CSRFRequired()
540 @CSRFRequired()
541 @view_config(
541 @view_config(
542 route_name='repo_commit_comment_attachment_upload', request_method='POST',
542 route_name='repo_commit_comment_attachment_upload', request_method='POST',
543 renderer='json_ext', xhr=True)
543 renderer='json_ext', xhr=True)
544 def repo_commit_comment_attachment_upload(self):
544 def repo_commit_comment_attachment_upload(self):
545 c = self.load_default_context()
545 c = self.load_default_context()
546 upload_key = 'attachment'
546 upload_key = 'attachment'
547
547
548 file_obj = self.request.POST.get(upload_key)
548 file_obj = self.request.POST.get(upload_key)
549
549
550 if file_obj is None:
550 if file_obj is None:
551 self.request.response.status = 400
551 self.request.response.status = 400
552 return {'store_fid': None,
552 return {'store_fid': None,
553 'access_path': None,
553 'access_path': None,
554 'error': '{} data field is missing'.format(upload_key)}
554 'error': '{} data field is missing'.format(upload_key)}
555
555
556 if not hasattr(file_obj, 'filename'):
556 if not hasattr(file_obj, 'filename'):
557 self.request.response.status = 400
557 self.request.response.status = 400
558 return {'store_fid': None,
558 return {'store_fid': None,
559 'access_path': None,
559 'access_path': None,
560 'error': 'filename cannot be read from the data field'}
560 'error': 'filename cannot be read from the data field'}
561
561
562 filename = file_obj.filename
562 filename = file_obj.filename
563 file_display_name = filename
563 file_display_name = filename
564
564
565 metadata = {
565 metadata = {
566 'user_uploaded': {'username': self._rhodecode_user.username,
566 'user_uploaded': {'username': self._rhodecode_user.username,
567 'user_id': self._rhodecode_user.user_id,
567 'user_id': self._rhodecode_user.user_id,
568 'ip': self._rhodecode_user.ip_addr}}
568 'ip': self._rhodecode_user.ip_addr}}
569
569
570 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
570 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
571 allowed_extensions = [
571 allowed_extensions = [
572 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
572 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
573 '.pptx', '.txt', '.xlsx', '.zip']
573 '.pptx', '.txt', '.xlsx', '.zip']
574 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
574 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
575
575
576 try:
576 try:
577 storage = store_utils.get_file_storage(self.request.registry.settings)
577 storage = store_utils.get_file_storage(self.request.registry.settings)
578 store_uid, metadata = storage.save_file(
578 store_uid, metadata = storage.save_file(
579 file_obj.file, filename, extra_metadata=metadata,
579 file_obj.file, filename, extra_metadata=metadata,
580 extensions=allowed_extensions, max_filesize=max_file_size)
580 extensions=allowed_extensions, max_filesize=max_file_size)
581 except FileNotAllowedException:
581 except FileNotAllowedException:
582 self.request.response.status = 400
582 self.request.response.status = 400
583 permitted_extensions = ', '.join(allowed_extensions)
583 permitted_extensions = ', '.join(allowed_extensions)
584 error_msg = 'File `{}` is not allowed. ' \
584 error_msg = 'File `{}` is not allowed. ' \
585 'Only following extensions are permitted: {}'.format(
585 'Only following extensions are permitted: {}'.format(
586 filename, permitted_extensions)
586 filename, permitted_extensions)
587 return {'store_fid': None,
587 return {'store_fid': None,
588 'access_path': None,
588 'access_path': None,
589 'error': error_msg}
589 'error': error_msg}
590 except FileOverSizeException:
590 except FileOverSizeException:
591 self.request.response.status = 400
591 self.request.response.status = 400
592 limit_mb = h.format_byte_size_binary(max_file_size)
592 limit_mb = h.format_byte_size_binary(max_file_size)
593 return {'store_fid': None,
593 return {'store_fid': None,
594 'access_path': None,
594 'access_path': None,
595 'error': 'File {} is exceeding allowed limit of {}.'.format(
595 'error': 'File {} is exceeding allowed limit of {}.'.format(
596 filename, limit_mb)}
596 filename, limit_mb)}
597
597
598 try:
598 try:
599 entry = FileStore.create(
599 entry = FileStore.create(
600 file_uid=store_uid, filename=metadata["filename"],
600 file_uid=store_uid, filename=metadata["filename"],
601 file_hash=metadata["sha256"], file_size=metadata["size"],
601 file_hash=metadata["sha256"], file_size=metadata["size"],
602 file_display_name=file_display_name,
602 file_display_name=file_display_name,
603 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
603 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
604 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
604 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
605 scope_repo_id=self.db_repo.repo_id
605 scope_repo_id=self.db_repo.repo_id
606 )
606 )
607 Session().add(entry)
607 Session().add(entry)
608 Session().commit()
608 Session().commit()
609 log.debug('Stored upload in DB as %s', entry)
609 log.debug('Stored upload in DB as %s', entry)
610 except Exception:
610 except Exception:
611 log.exception('Failed to store file %s', filename)
611 log.exception('Failed to store file %s', filename)
612 self.request.response.status = 400
612 self.request.response.status = 400
613 return {'store_fid': None,
613 return {'store_fid': None,
614 'access_path': None,
614 'access_path': None,
615 'error': 'File {} failed to store in DB.'.format(filename)}
615 'error': 'File {} failed to store in DB.'.format(filename)}
616
616
617 Session().commit()
617 Session().commit()
618
618
619 return {
619 return {
620 'store_fid': store_uid,
620 'store_fid': store_uid,
621 'access_path': h.route_path(
621 'access_path': h.route_path(
622 'download_file', fid=store_uid),
622 'download_file', fid=store_uid),
623 'fqn_access_path': h.route_url(
623 'fqn_access_path': h.route_url(
624 'download_file', fid=store_uid),
624 'download_file', fid=store_uid),
625 'repo_access_path': h.route_path(
625 'repo_access_path': h.route_path(
626 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
626 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
627 'repo_fqn_access_path': h.route_url(
627 'repo_fqn_access_path': h.route_url(
628 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
628 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
629 }
629 }
630
630
631 @LoginRequired()
631 @LoginRequired()
632 @NotAnonymous()
632 @NotAnonymous()
633 @HasRepoPermissionAnyDecorator(
633 @HasRepoPermissionAnyDecorator(
634 'repository.read', 'repository.write', 'repository.admin')
634 'repository.read', 'repository.write', 'repository.admin')
635 @CSRFRequired()
635 @CSRFRequired()
636 @view_config(
636 @view_config(
637 route_name='repo_commit_comment_delete', request_method='POST',
637 route_name='repo_commit_comment_delete', request_method='POST',
638 renderer='json_ext')
638 renderer='json_ext')
639 def repo_commit_comment_delete(self):
639 def repo_commit_comment_delete(self):
640 commit_id = self.request.matchdict['commit_id']
640 commit_id = self.request.matchdict['commit_id']
641 comment_id = self.request.matchdict['comment_id']
641 comment_id = self.request.matchdict['comment_id']
642
642
643 comment = ChangesetComment.get_or_404(comment_id)
643 comment = ChangesetComment.get_or_404(comment_id)
644 if not comment:
644 if not comment:
645 log.debug('Comment with id:%s not found, skipping', comment_id)
645 log.debug('Comment with id:%s not found, skipping', comment_id)
646 # comment already deleted in another call probably
646 # comment already deleted in another call probably
647 return True
647 return True
648
648
649 if comment.immutable:
649 if comment.immutable:
650 # don't allow deleting comments that are immutable
650 # don't allow deleting comments that are immutable
651 raise HTTPForbidden()
651 raise HTTPForbidden()
652
652
653 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
653 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
654 super_admin = h.HasPermissionAny('hg.admin')()
654 super_admin = h.HasPermissionAny('hg.admin')()
655 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
655 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
656 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
656 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
657 comment_repo_admin = is_repo_admin and is_repo_comment
657 comment_repo_admin = is_repo_admin and is_repo_comment
658
658
659 if super_admin or comment_owner or comment_repo_admin:
659 if super_admin or comment_owner or comment_repo_admin:
660 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
660 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
661 Session().commit()
661 Session().commit()
662 return True
662 return True
663 else:
663 else:
664 log.warning('No permissions for user %s to delete comment_id: %s',
664 log.warning('No permissions for user %s to delete comment_id: %s',
665 self._rhodecode_db_user, comment_id)
665 self._rhodecode_db_user, comment_id)
666 raise HTTPNotFound()
666 raise HTTPNotFound()
667
667
668 @LoginRequired()
668 @LoginRequired()
669 @NotAnonymous()
669 @NotAnonymous()
670 @HasRepoPermissionAnyDecorator(
670 @HasRepoPermissionAnyDecorator(
671 'repository.read', 'repository.write', 'repository.admin')
671 'repository.read', 'repository.write', 'repository.admin')
672 @CSRFRequired()
672 @CSRFRequired()
673 @view_config(
673 @view_config(
674 route_name='repo_commit_comment_edit', request_method='POST',
674 route_name='repo_commit_comment_edit', request_method='POST',
675 renderer='json_ext')
675 renderer='json_ext')
676 def repo_commit_comment_edit(self):
676 def repo_commit_comment_edit(self):
677 self.load_default_context()
677 self.load_default_context()
678
678
679 comment_id = self.request.matchdict['comment_id']
679 comment_id = self.request.matchdict['comment_id']
680 comment = ChangesetComment.get_or_404(comment_id)
680 comment = ChangesetComment.get_or_404(comment_id)
681
681
682 if comment.immutable:
682 if comment.immutable:
683 # don't allow deleting comments that are immutable
683 # don't allow deleting comments that are immutable
684 raise HTTPForbidden()
684 raise HTTPForbidden()
685
685
686 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
686 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
687 super_admin = h.HasPermissionAny('hg.admin')()
687 super_admin = h.HasPermissionAny('hg.admin')()
688 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
688 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
689 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
689 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
690 comment_repo_admin = is_repo_admin and is_repo_comment
690 comment_repo_admin = is_repo_admin and is_repo_comment
691
691
692 if super_admin or comment_owner or comment_repo_admin:
692 if super_admin or comment_owner or comment_repo_admin:
693 text = self.request.POST.get('text')
693 text = self.request.POST.get('text')
694 version = self.request.POST.get('version')
694 version = self.request.POST.get('version')
695 if text == comment.text:
695 if text == comment.text:
696 log.warning(
696 log.warning(
697 'Comment(repo): '
697 'Comment(repo): '
698 'Trying to create new version '
698 'Trying to create new version '
699 'with the same comment body {}'.format(
699 'with the same comment body {}'.format(
700 comment_id,
700 comment_id,
701 )
701 )
702 )
702 )
703 raise HTTPNotFound()
703 raise HTTPNotFound()
704
704
705 if version.isdigit():
705 if version.isdigit():
706 version = int(version)
706 version = int(version)
707 else:
707 else:
708 log.warning(
708 log.warning(
709 'Comment(repo): Wrong version type {} {} '
709 'Comment(repo): Wrong version type {} {} '
710 'for comment {}'.format(
710 'for comment {}'.format(
711 version,
711 version,
712 type(version),
712 type(version),
713 comment_id,
713 comment_id,
714 )
714 )
715 )
715 )
716 raise HTTPNotFound()
716 raise HTTPNotFound()
717
717
718 try:
718 try:
719 comment_history = CommentsModel().edit(
719 comment_history = CommentsModel().edit(
720 comment_id=comment_id,
720 comment_id=comment_id,
721 text=text,
721 text=text,
722 auth_user=self._rhodecode_user,
722 auth_user=self._rhodecode_user,
723 version=version,
723 version=version,
724 )
724 )
725 except CommentVersionMismatch:
725 except CommentVersionMismatch:
726 raise HTTPConflict()
726 raise HTTPConflict()
727
727
728 if not comment_history:
728 if not comment_history:
729 raise HTTPNotFound()
729 raise HTTPNotFound()
730
730
731 commit_id = self.request.matchdict['commit_id']
731 commit_id = self.request.matchdict['commit_id']
732 commit = self.db_repo.get_commit(commit_id)
732 commit = self.db_repo.get_commit(commit_id)
733 CommentsModel().trigger_commit_comment_hook(
733 CommentsModel().trigger_commit_comment_hook(
734 self.db_repo, self._rhodecode_user, 'edit',
734 self.db_repo, self._rhodecode_user, 'edit',
735 data={'comment': comment, 'commit': commit})
735 data={'comment': comment, 'commit': commit})
736
736
737 Session().commit()
737 Session().commit()
738 return {
738 return {
739 'comment_history_id': comment_history.comment_history_id,
739 'comment_history_id': comment_history.comment_history_id,
740 'comment_id': comment.comment_id,
740 'comment_id': comment.comment_id,
741 'comment_version': comment_history.version,
741 'comment_version': comment_history.version,
742 'comment_author_username': comment_history.author.username,
742 'comment_author_username': comment_history.author.username,
743 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
743 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
744 'comment_created_on': h.age_component(comment_history.created_on,
744 'comment_created_on': h.age_component(comment_history.created_on,
745 time_is_local=True),
745 time_is_local=True),
746 }
746 }
747 else:
747 else:
748 log.warning('No permissions for user %s to edit comment_id: %s',
748 log.warning('No permissions for user %s to edit comment_id: %s',
749 self._rhodecode_db_user, comment_id)
749 self._rhodecode_db_user, comment_id)
750 raise HTTPNotFound()
750 raise HTTPNotFound()
751
751
752 @LoginRequired()
752 @LoginRequired()
753 @HasRepoPermissionAnyDecorator(
753 @HasRepoPermissionAnyDecorator(
754 'repository.read', 'repository.write', 'repository.admin')
754 'repository.read', 'repository.write', 'repository.admin')
755 @view_config(
755 @view_config(
756 route_name='repo_commit_data', request_method='GET',
756 route_name='repo_commit_data', request_method='GET',
757 renderer='json_ext', xhr=True)
757 renderer='json_ext', xhr=True)
758 def repo_commit_data(self):
758 def repo_commit_data(self):
759 commit_id = self.request.matchdict['commit_id']
759 commit_id = self.request.matchdict['commit_id']
760 self.load_default_context()
760 self.load_default_context()
761
761
762 try:
762 try:
763 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
763 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
764 except CommitDoesNotExistError as e:
764 except CommitDoesNotExistError as e:
765 return EmptyCommit(message=str(e))
765 return EmptyCommit(message=str(e))
766
766
767 @LoginRequired()
767 @LoginRequired()
768 @HasRepoPermissionAnyDecorator(
768 @HasRepoPermissionAnyDecorator(
769 'repository.read', 'repository.write', 'repository.admin')
769 'repository.read', 'repository.write', 'repository.admin')
770 @view_config(
770 @view_config(
771 route_name='repo_commit_children', request_method='GET',
771 route_name='repo_commit_children', request_method='GET',
772 renderer='json_ext', xhr=True)
772 renderer='json_ext', xhr=True)
773 def repo_commit_children(self):
773 def repo_commit_children(self):
774 commit_id = self.request.matchdict['commit_id']
774 commit_id = self.request.matchdict['commit_id']
775 self.load_default_context()
775 self.load_default_context()
776
776
777 try:
777 try:
778 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
778 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
779 children = commit.children
779 children = commit.children
780 except CommitDoesNotExistError:
780 except CommitDoesNotExistError:
781 children = []
781 children = []
782
782
783 result = {"results": children}
783 result = {"results": children}
784 return result
784 return result
785
785
786 @LoginRequired()
786 @LoginRequired()
787 @HasRepoPermissionAnyDecorator(
787 @HasRepoPermissionAnyDecorator(
788 'repository.read', 'repository.write', 'repository.admin')
788 'repository.read', 'repository.write', 'repository.admin')
789 @view_config(
789 @view_config(
790 route_name='repo_commit_parents', request_method='GET',
790 route_name='repo_commit_parents', request_method='GET',
791 renderer='json_ext')
791 renderer='json_ext')
792 def repo_commit_parents(self):
792 def repo_commit_parents(self):
793 commit_id = self.request.matchdict['commit_id']
793 commit_id = self.request.matchdict['commit_id']
794 self.load_default_context()
794 self.load_default_context()
795
795
796 try:
796 try:
797 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
797 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
798 parents = commit.parents
798 parents = commit.parents
799 except CommitDoesNotExistError:
799 except CommitDoesNotExistError:
800 parents = []
800 parents = []
801 result = {"results": parents}
801 result = {"results": parents}
802 return result
802 return result
@@ -1,750 +1,746 b''
1 // comments.less
1 // comments.less
2 // For use in RhodeCode applications;
2 // For use in RhodeCode applications;
3 // see style guide documentation for guidelines.
3 // see style guide documentation for guidelines.
4
4
5
5
6 // Comments
6 // Comments
7 @comment-outdated-opacity: 1.0;
7 @comment-outdated-opacity: 1.0;
8
8
9 .comments {
9 .comments {
10 width: 100%;
10 width: 100%;
11 }
11 }
12
12
13 .comments-heading {
13 .comments-heading {
14 margin-bottom: -1px;
14 margin-bottom: -1px;
15 background: @grey6;
15 background: @grey6;
16 display: block;
16 display: block;
17 padding: 10px 0px;
17 padding: 10px 0px;
18 font-size: 18px
18 font-size: 18px
19 }
19 }
20
20
21 #comment-tr-show {
21 #comment-tr-show {
22 padding: 5px 0;
22 padding: 5px 0;
23 }
23 }
24
24
25 tr.inline-comments div {
25 tr.inline-comments div {
26 max-width: 100%;
26 max-width: 100%;
27
27
28 p {
28 p {
29 white-space: normal;
29 white-space: normal;
30 }
30 }
31
31
32 code, pre, .code, dd {
32 code, pre, .code, dd {
33 overflow-x: auto;
33 overflow-x: auto;
34 width: 1062px;
34 width: 1062px;
35 }
35 }
36
36
37 dd {
37 dd {
38 width: auto;
38 width: auto;
39 }
39 }
40 }
40 }
41
41
42 #injected_page_comments {
42 #injected_page_comments {
43 .comment-previous-link,
43 .comment-previous-link,
44 .comment-next-link,
44 .comment-next-link,
45 .comment-links-divider {
45 .comment-links-divider {
46 display: none;
46 display: none;
47 }
47 }
48 }
48 }
49
49
50 .add-comment {
50 .add-comment {
51 margin-bottom: 10px;
51 margin-bottom: 10px;
52 }
52 }
53 .hide-comment-button .add-comment {
53 .hide-comment-button .add-comment {
54 display: none;
54 display: none;
55 }
55 }
56
56
57 .comment-bubble {
57 .comment-bubble {
58 color: @grey4;
58 color: @grey4;
59 margin-top: 4px;
59 margin-top: 4px;
60 margin-right: 30px;
60 margin-right: 30px;
61 visibility: hidden;
61 visibility: hidden;
62 }
62 }
63
63
64 .comment-draft {
64 .comment-draft {
65 float: left;
65 float: left;
66 margin-right: 10px;
66 margin-right: 10px;
67 font-weight: 400;
67 font-weight: 400;
68 color: @color-draft;
68 color: @color-draft;
69 }
69 }
70
70
71 .comment-new {
71 .comment-new {
72 float: left;
72 float: left;
73 margin-right: 10px;
73 margin-right: 10px;
74 font-weight: 400;
74 font-weight: 400;
75 color: @color-new;
75 color: @color-new;
76 }
76 }
77
77
78 .comment-label {
78 .comment-label {
79 float: left;
79 float: left;
80
80
81 padding: 0 8px 0 0;
81 padding: 0 8px 0 0;
82 min-height: 0;
82 min-height: 0;
83
83
84 text-align: center;
84 text-align: center;
85 font-size: 10px;
85 font-size: 10px;
86
86
87 font-family: @text-italic;
87 font-family: @text-italic;
88 font-style: italic;
88 font-style: italic;
89 background: #fff none;
89 background: #fff none;
90 color: @grey3;
90 color: @grey3;
91 white-space: nowrap;
91 white-space: nowrap;
92
92
93 text-transform: uppercase;
93 text-transform: uppercase;
94 min-width: 50px;
94 min-width: 50px;
95
95
96 &.todo {
96 &.todo {
97 color: @color5;
97 color: @color5;
98 font-style: italic;
98 font-style: italic;
99 font-weight: @text-bold-italic-weight;
99 font-weight: @text-bold-italic-weight;
100 font-family: @text-bold-italic;
100 font-family: @text-bold-italic;
101 }
101 }
102
102
103 .resolve {
103 .resolve {
104 cursor: pointer;
104 cursor: pointer;
105 text-decoration: underline;
105 text-decoration: underline;
106 }
106 }
107
107
108 .resolved {
108 .resolved {
109 text-decoration: line-through;
109 text-decoration: line-through;
110 color: @color1;
110 color: @color1;
111 }
111 }
112 .resolved a {
112 .resolved a {
113 text-decoration: line-through;
113 text-decoration: line-through;
114 color: @color1;
114 color: @color1;
115 }
115 }
116 .resolve-text {
116 .resolve-text {
117 color: @color1;
117 color: @color1;
118 margin: 2px 8px;
118 margin: 2px 8px;
119 font-family: @text-italic;
119 font-family: @text-italic;
120 font-style: italic;
120 font-style: italic;
121 }
121 }
122 }
122 }
123
123
124 .has-spacer-after {
124 .has-spacer-after {
125 &:after {
125 &:after {
126 content: ' | ';
126 content: ' | ';
127 color: @grey5;
127 color: @grey5;
128 }
128 }
129 }
129 }
130
130
131 .has-spacer-before {
131 .has-spacer-before {
132 &:before {
132 &:before {
133 content: ' | ';
133 content: ' | ';
134 color: @grey5;
134 color: @grey5;
135 }
135 }
136 }
136 }
137
137
138 .comment {
138 .comment {
139
139
140 &.comment-general {
140 &.comment-general {
141 border: 1px solid @grey5;
141 border: 1px solid @grey5;
142 padding: 5px 5px 5px 5px;
142 padding: 5px 5px 5px 5px;
143 }
143 }
144
144
145 margin: @padding 0;
145 margin: @padding 0;
146 padding: 4px 0 0 0;
146 padding: 4px 0 0 0;
147 line-height: 1em;
147 line-height: 1em;
148
148
149 .rc-user {
149 .rc-user {
150 min-width: 0;
150 min-width: 0;
151 margin: 0px .5em 0 0;
151 margin: 0px .5em 0 0;
152
152
153 .user {
153 .user {
154 display: inline;
154 display: inline;
155 }
155 }
156 }
156 }
157
157
158 .meta {
158 .meta {
159 position: relative;
159 position: relative;
160 width: 100%;
160 width: 100%;
161 border-bottom: 1px solid @grey5;
161 border-bottom: 1px solid @grey5;
162 margin: -5px 0px;
162 margin: -5px 0px;
163 line-height: 24px;
163 line-height: 24px;
164
164
165 &:hover .permalink {
165 &:hover .permalink {
166 visibility: visible;
166 visibility: visible;
167 color: @rcblue;
167 color: @rcblue;
168 }
168 }
169 }
169 }
170
170
171 .author,
171 .author,
172 .date {
172 .date {
173 display: inline;
173 display: inline;
174
174
175 &:after {
175 &:after {
176 content: ' | ';
176 content: ' | ';
177 color: @grey5;
177 color: @grey5;
178 }
178 }
179 }
179 }
180
180
181 .author-general img {
181 .author-general img {
182 top: 3px;
182 top: 3px;
183 }
183 }
184 .author-inline img {
184 .author-inline img {
185 top: 3px;
185 top: 3px;
186 }
186 }
187
187
188 .status-change,
188 .status-change,
189 .permalink,
189 .permalink,
190 .changeset-status-lbl {
190 .changeset-status-lbl {
191 display: inline;
191 display: inline;
192 }
192 }
193
193
194 .permalink {
194 .permalink {
195 visibility: hidden;
195 visibility: hidden;
196 }
196 }
197
197
198 .comment-links-divider {
198 .comment-links-divider {
199 display: inline;
199 display: inline;
200 }
200 }
201
201
202 .comment-links-block {
202 .comment-links-block {
203 float:right;
203 float:right;
204 text-align: right;
204 text-align: right;
205 min-width: 85px;
205 min-width: 85px;
206
206
207 [class^="icon-"]:before,
207 [class^="icon-"]:before,
208 [class*=" icon-"]:before {
208 [class*=" icon-"]:before {
209 margin-left: 0;
209 margin-left: 0;
210 margin-right: 0;
210 margin-right: 0;
211 }
211 }
212 }
212 }
213
213
214 .comment-previous-link {
214 .comment-previous-link {
215 display: inline-block;
215 display: inline-block;
216
216
217 .arrow_comment_link{
217 .arrow_comment_link{
218 cursor: pointer;
218 cursor: pointer;
219 i {
219 i {
220 font-size:10px;
220 font-size:10px;
221 }
221 }
222 }
222 }
223 .arrow_comment_link.disabled {
223 .arrow_comment_link.disabled {
224 cursor: default;
224 cursor: default;
225 color: @grey5;
225 color: @grey5;
226 }
226 }
227 }
227 }
228
228
229 .comment-next-link {
229 .comment-next-link {
230 display: inline-block;
230 display: inline-block;
231
231
232 .arrow_comment_link{
232 .arrow_comment_link{
233 cursor: pointer;
233 cursor: pointer;
234 i {
234 i {
235 font-size:10px;
235 font-size:10px;
236 }
236 }
237 }
237 }
238 .arrow_comment_link.disabled {
238 .arrow_comment_link.disabled {
239 cursor: default;
239 cursor: default;
240 color: @grey5;
240 color: @grey5;
241 }
241 }
242 }
242 }
243
243
244 .delete-comment {
244 .delete-comment {
245 display: inline-block;
245 display: inline-block;
246 color: @rcblue;
246 color: @rcblue;
247
247
248 &:hover {
248 &:hover {
249 cursor: pointer;
249 cursor: pointer;
250 }
250 }
251 }
251 }
252
252
253 .text {
253 .text {
254 clear: both;
254 clear: both;
255 .border-radius(@border-radius);
255 .border-radius(@border-radius);
256 .box-sizing(border-box);
256 .box-sizing(border-box);
257
257
258 .markdown-block p,
258 .markdown-block p,
259 .rst-block p {
259 .rst-block p {
260 margin: .5em 0 !important;
260 margin: .5em 0 !important;
261 // TODO: lisa: This is needed because of other rst !important rules :[
261 // TODO: lisa: This is needed because of other rst !important rules :[
262 }
262 }
263 }
263 }
264
264
265 .pr-version {
265 .pr-version {
266 display: inline-block;
266 display: inline-block;
267 }
267 }
268 .pr-version-inline {
268 .pr-version-inline {
269 display: inline-block;
269 display: inline-block;
270 }
270 }
271 .pr-version-num {
271 .pr-version-num {
272 font-size: 10px;
272 font-size: 10px;
273 }
273 }
274 }
274 }
275
275
276 @comment-padding: 5px;
276 @comment-padding: 5px;
277
277
278 .general-comments {
278 .general-comments {
279 .comment-outdated {
279 .comment-outdated {
280 opacity: @comment-outdated-opacity;
280 opacity: @comment-outdated-opacity;
281 }
281 }
282
282
283 .comment-outdated-label {
283 .comment-outdated-label {
284 color: @grey3;
284 color: @grey3;
285 padding-right: 4px;
285 padding-right: 4px;
286 }
286 }
287 }
287 }
288
288
289 .inline-comments {
289 .inline-comments {
290
290
291 .comment {
291 .comment {
292 margin: 0;
292 margin: 0;
293 }
293 }
294
294
295 .comment-outdated {
295 .comment-outdated {
296 opacity: @comment-outdated-opacity;
296 opacity: @comment-outdated-opacity;
297 }
297 }
298
298
299 .comment-outdated-label {
299 .comment-outdated-label {
300 color: @grey3;
300 color: @grey3;
301 padding-right: 4px;
301 padding-right: 4px;
302 }
302 }
303
303
304 .comment-inline {
304 .comment-inline {
305
305
306 &:first-child {
306 &:first-child {
307 margin: 4px 4px 0 4px;
307 margin: 4px 4px 0 4px;
308 border-top: 1px solid @grey5;
308 border-top: 1px solid @grey5;
309 border-bottom: 0 solid @grey5;
309 border-bottom: 0 solid @grey5;
310 border-left: 1px solid @grey5;
310 border-left: 1px solid @grey5;
311 border-right: 1px solid @grey5;
311 border-right: 1px solid @grey5;
312 .border-radius-top(4px);
312 .border-radius-top(4px);
313 }
313 }
314
314
315 &:only-child {
315 &:only-child {
316 margin: 4px 4px 0 4px;
316 margin: 4px 4px 0 4px;
317 border-top: 1px solid @grey5;
317 border-top: 1px solid @grey5;
318 border-bottom: 0 solid @grey5;
318 border-bottom: 0 solid @grey5;
319 border-left: 1px solid @grey5;
319 border-left: 1px solid @grey5;
320 border-right: 1px solid @grey5;
320 border-right: 1px solid @grey5;
321 .border-radius-top(4px);
321 .border-radius-top(4px);
322 }
322 }
323
323
324 background: white;
324 background: white;
325 padding: @comment-padding @comment-padding;
325 padding: @comment-padding @comment-padding;
326 margin: 0 4px 0 4px;
326 margin: 0 4px 0 4px;
327 border-top: 0 solid @grey5;
327 border-top: 0 solid @grey5;
328 border-bottom: 0 solid @grey5;
328 border-bottom: 0 solid @grey5;
329 border-left: 1px solid @grey5;
329 border-left: 1px solid @grey5;
330 border-right: 1px solid @grey5;
330 border-right: 1px solid @grey5;
331
331
332 .text {
332 .text {
333 border: none;
333 border: none;
334 }
334 }
335
335
336 .meta {
336 .meta {
337 border-bottom: 1px solid @grey6;
337 border-bottom: 1px solid @grey6;
338 margin: -5px 0px;
338 margin: -5px 0px;
339 line-height: 24px;
339 line-height: 24px;
340 }
340 }
341
341
342 }
342 }
343 .comment-selected {
343 .comment-selected {
344 border-left: 6px solid @comment-highlight-color;
344 border-left: 6px solid @comment-highlight-color;
345 }
345 }
346
346
347 .comment-inline-form-open {
347 .comment-inline-form-open {
348 display: block !important;
348 display: block !important;
349 }
349 }
350
350
351 .comment-inline-form {
351 .comment-inline-form {
352 display: none;
352 display: none;
353 }
353 }
354
354
355 .comment-inline-form-edit {
355 .comment-inline-form-edit {
356 padding: 0;
356 padding: 0;
357 margin: 0px 4px 2px 4px;
357 margin: 0px 4px 2px 4px;
358 }
358 }
359
359
360 .reply-thread-container {
360 .reply-thread-container {
361 display: table;
361 display: table;
362 width: 100%;
362 width: 100%;
363 padding: 0px 4px 4px 4px;
363 padding: 0px 4px 4px 4px;
364 }
364 }
365
365
366 .reply-thread-container-wrapper {
366 .reply-thread-container-wrapper {
367 margin: 0 4px 4px 4px;
367 margin: 0 4px 4px 4px;
368 border-top: 0 solid @grey5;
368 border-top: 0 solid @grey5;
369 border-bottom: 1px solid @grey5;
369 border-bottom: 1px solid @grey5;
370 border-left: 1px solid @grey5;
370 border-left: 1px solid @grey5;
371 border-right: 1px solid @grey5;
371 border-right: 1px solid @grey5;
372 .border-radius-bottom(4px);
372 .border-radius-bottom(4px);
373 }
373 }
374
374
375 .reply-thread-gravatar {
375 .reply-thread-gravatar {
376 display: table-cell;
376 display: table-cell;
377 width: 24px;
377 width: 24px;
378 height: 24px;
378 height: 24px;
379 padding-top: 10px;
379 padding-top: 10px;
380 padding-left: 10px;
380 padding-left: 10px;
381 background-color: #eeeeee;
381 background-color: #eeeeee;
382 vertical-align: top;
382 vertical-align: top;
383 }
383 }
384
384
385 .reply-thread-reply-button {
385 .reply-thread-reply-button {
386 display: table-cell;
386 display: table-cell;
387 width: 100%;
387 width: 100%;
388 height: 33px;
388 height: 33px;
389 padding: 3px 8px;
389 padding: 3px 8px;
390 margin-left: 8px;
390 margin-left: 8px;
391 background-color: #eeeeee;
391 background-color: #eeeeee;
392 }
392 }
393
393
394 .reply-thread-reply-button .cb-comment-add-button {
394 .reply-thread-reply-button .cb-comment-add-button {
395 border-radius: 4px;
395 border-radius: 4px;
396 width: 100%;
396 width: 100%;
397 padding: 6px 2px;
397 padding: 6px 2px;
398 text-align: left;
398 text-align: left;
399 cursor: text;
399 cursor: text;
400 color: @grey3;
400 color: @grey3;
401 }
401 }
402 .reply-thread-reply-button .cb-comment-add-button:hover {
402 .reply-thread-reply-button .cb-comment-add-button:hover {
403 background-color: white;
403 background-color: white;
404 color: @grey2;
404 color: @grey2;
405 }
405 }
406
406
407 .reply-thread-last {
407 .reply-thread-last {
408 display: table-cell;
408 display: table-cell;
409 width: 10px;
409 width: 10px;
410 }
410 }
411
411
412 /* Hide reply box when it's a first element,
412 /* Hide reply box when it's a first element,
413 can happen when drafts are saved but not shown to specific user,
413 can happen when drafts are saved but not shown to specific user,
414 or there are outdated comments hidden
414 or there are outdated comments hidden
415 */
415 */
416 .reply-thread-container-wrapper:first-child:not(.comment-form-active) {
416 .reply-thread-container-wrapper:first-child:not(.comment-form-active) {
417 display: none;
417 display: none;
418 }
418 }
419
419
420 .reply-thread-container-wrapper.comment-outdated {
420 .reply-thread-container-wrapper.comment-outdated {
421 display: none
421 display: none
422 }
422 }
423
423
424 /* hide add comment button when form is open */
424 /* hide add comment button when form is open */
425 .comment-inline-form-open ~ .cb-comment-add-button {
425 .comment-inline-form-open ~ .cb-comment-add-button {
426 display: none;
426 display: none;
427 }
427 }
428
428
429 /* hide add comment button when only comment is being deleted */
429 /* hide add comment button when only comment is being deleted */
430 .comment-deleting:first-child + .cb-comment-add-button {
430 .comment-deleting:first-child + .cb-comment-add-button {
431 display: none;
431 display: none;
432 }
432 }
433
433
434 /* hide add comment button when form but no comments */
434 /* hide add comment button when form but no comments */
435 .comment-inline-form:first-child + .cb-comment-add-button {
435 .comment-inline-form:first-child + .cb-comment-add-button {
436 display: none;
436 display: none;
437 }
437 }
438
438
439 }
439 }
440
440
441 .show-outdated-comments {
441 .show-outdated-comments {
442 display: inline;
442 display: inline;
443 color: @rcblue;
443 color: @rcblue;
444 }
444 }
445
445
446 // Comment Form
446 // Comment Form
447 div.comment-form {
447 div.comment-form {
448 margin-top: 20px;
448 margin-top: 20px;
449 }
449 }
450
450
451 .comment-form strong {
451 .comment-form strong {
452 display: block;
452 display: block;
453 margin-bottom: 15px;
453 margin-bottom: 15px;
454 }
454 }
455
455
456 .comment-form textarea {
456 .comment-form textarea {
457 width: 100%;
457 width: 100%;
458 height: 100px;
458 height: 100px;
459 font-family: @text-monospace;
459 font-family: @text-monospace;
460 }
460 }
461
461
462 form.comment-form {
462 form.comment-form {
463 margin-top: 10px;
463 margin-top: 10px;
464 margin-left: 10px;
464 margin-left: 10px;
465 }
465 }
466
466
467 .comment-inline-form .comment-block-ta,
467 .comment-inline-form .comment-block-ta,
468 .comment-form .comment-block-ta,
468 .comment-form .comment-block-ta,
469 .comment-form .preview-box {
469 .comment-form .preview-box {
470 .border-radius(@border-radius);
470 .border-radius(@border-radius);
471 .box-sizing(border-box);
471 .box-sizing(border-box);
472 background-color: white;
472 background-color: white;
473 }
473 }
474
474
475 .comment-form-submit {
475 .comment-form-submit {
476 margin-top: 5px;
476 margin-top: 5px;
477 margin-left: 525px;
477 margin-left: 525px;
478 }
478 }
479
479
480 .file-comments {
480 .file-comments {
481 display: none;
481 display: none;
482 }
482 }
483
483
484 .comment-form .preview-box.unloaded,
484 .comment-form .preview-box.unloaded,
485 .comment-inline-form .preview-box.unloaded {
485 .comment-inline-form .preview-box.unloaded {
486 height: 50px;
486 height: 50px;
487 text-align: center;
487 text-align: center;
488 padding: 20px;
488 padding: 20px;
489 background-color: white;
489 background-color: white;
490 }
490 }
491
491
492 .comment-footer {
492 .comment-footer {
493 display: table;
493 display: table;
494 width: 100%;
494 width: 100%;
495 height: 42px;
495 height: 42px;
496
496
497 .comment-status-box,
497 .comment-status-box,
498 .cancel-button {
498 .cancel-button {
499 display: inline-block;
499 display: inline-block;
500 }
500 }
501
501
502 .comment-status-box {
502 .comment-status-box {
503 margin-left: 10px;
503 margin-left: 10px;
504 }
504 }
505
505
506 .action-buttons {
506 .action-buttons {
507 display: table-cell;
507 display: table-cell;
508 padding: 5px 0 5px 2px;
508 padding: 5px 0 5px 2px;
509 }
509 }
510
510
511 .toolbar-text {
511 .toolbar-text {
512 height: 42px;
512 height: 42px;
513 display: table-cell;
513 display: table-cell;
514 vertical-align: bottom;
514 vertical-align: bottom;
515 font-size: 11px;
515 font-size: 11px;
516 color: @grey4;
516 color: @grey4;
517 text-align: right;
517 text-align: right;
518
518
519 a {
519 a {
520 color: @grey4;
520 color: @grey4;
521 }
521 }
522
522
523 p {
524 padding: 0;
525 margin: 0;
526 }
527 }
523 }
528
524
529 .action-buttons-extra {
525 .action-buttons-extra {
530 display: inline-block;
526 display: inline-block;
531 }
527 }
532 }
528 }
533
529
534 .comment-form {
530 .comment-form {
535
531
536 .comment {
532 .comment {
537 margin-left: 10px;
533 margin-left: 10px;
538 }
534 }
539
535
540 .comment-help {
536 .comment-help {
541 color: @grey4;
537 color: @grey4;
542 padding: 5px 0 5px 0;
538 padding: 5px 0 5px 0;
543 }
539 }
544
540
545 .comment-title {
541 .comment-title {
546 padding: 5px 0 5px 0;
542 padding: 5px 0 5px 0;
547 }
543 }
548
544
549 .comment-button {
545 .comment-button {
550 display: inline-block;
546 display: inline-block;
551 }
547 }
552
548
553 .comment-button-input {
549 .comment-button-input {
554 margin-right: 0;
550 margin-right: 0;
555 }
551 }
556
552
557 #save_general {
553 #save_general {
558 margin-left: -6px;
554 margin-left: -6px;
559 }
555 }
560
556
561 }
557 }
562
558
563
559
564 .comment-form-login {
560 .comment-form-login {
565 .comment-help {
561 .comment-help {
566 padding: 0.7em; //same as the button
562 padding: 0.7em; //same as the button
567 }
563 }
568
564
569 div.clearfix {
565 div.clearfix {
570 clear: both;
566 clear: both;
571 width: 100%;
567 width: 100%;
572 display: block;
568 display: block;
573 }
569 }
574 }
570 }
575
571
576 .comment-version-select {
572 .comment-version-select {
577 margin: 0px;
573 margin: 0px;
578 border-radius: inherit;
574 border-radius: inherit;
579 border-color: @grey6;
575 border-color: @grey6;
580 height: 20px;
576 height: 20px;
581 }
577 }
582
578
583 .comment-type {
579 .comment-type {
584 margin: 0px;
580 margin: 0px;
585 border-radius: inherit;
581 border-radius: inherit;
586 border-color: @grey6;
582 border-color: @grey6;
587 }
583 }
588
584
589 .preview-box {
585 .preview-box {
590 min-height: 105px;
586 min-height: 105px;
591 margin-bottom: 15px;
587 margin-bottom: 15px;
592 background-color: white;
588 background-color: white;
593 .border-radius(@border-radius);
589 .border-radius(@border-radius);
594 .box-sizing(border-box);
590 .box-sizing(border-box);
595 }
591 }
596
592
597 .add-another-button {
593 .add-another-button {
598 margin-left: 10px;
594 margin-left: 10px;
599 margin-top: 10px;
595 margin-top: 10px;
600 margin-bottom: 10px;
596 margin-bottom: 10px;
601 }
597 }
602
598
603 .comment .buttons {
599 .comment .buttons {
604 float: right;
600 float: right;
605 margin: -1px 0px 0px 0px;
601 margin: -1px 0px 0px 0px;
606 }
602 }
607
603
608 // Inline Comment Form
604 // Inline Comment Form
609 .injected_diff .comment-inline-form,
605 .injected_diff .comment-inline-form,
610 .comment-inline-form {
606 .comment-inline-form {
611 background-color: white;
607 background-color: white;
612 margin-top: 4px;
608 margin-top: 4px;
613 margin-bottom: 10px;
609 margin-bottom: 10px;
614 }
610 }
615
611
616 .inline-form {
612 .inline-form {
617 padding: 10px 7px;
613 padding: 10px 7px;
618 }
614 }
619
615
620 .inline-form div {
616 .inline-form div {
621 max-width: 100%;
617 max-width: 100%;
622 }
618 }
623
619
624 .overlay {
620 .overlay {
625 display: none;
621 display: none;
626 position: absolute;
622 position: absolute;
627 width: 100%;
623 width: 100%;
628 text-align: center;
624 text-align: center;
629 vertical-align: middle;
625 vertical-align: middle;
630 font-size: 16px;
626 font-size: 16px;
631 background: none repeat scroll 0 0 white;
627 background: none repeat scroll 0 0 white;
632
628
633 &.submitting {
629 &.submitting {
634 display: block;
630 display: block;
635 opacity: 0.5;
631 opacity: 0.5;
636 z-index: 100;
632 z-index: 100;
637 }
633 }
638 }
634 }
639 .comment-inline-form .overlay.submitting .overlay-text {
635 .comment-inline-form .overlay.submitting .overlay-text {
640 margin-top: 5%;
636 margin-top: 5%;
641 }
637 }
642
638
643 .comment-inline-form .clearfix,
639 .comment-inline-form .clearfix,
644 .comment-form .clearfix {
640 .comment-form .clearfix {
645 .border-radius(@border-radius);
641 .border-radius(@border-radius);
646 margin: 0px;
642 margin: 0px;
647 }
643 }
648
644
649
645
650 .hide-inline-form-button {
646 .hide-inline-form-button {
651 margin-left: 5px;
647 margin-left: 5px;
652 }
648 }
653 .comment-button .hide-inline-form {
649 .comment-button .hide-inline-form {
654 background: white;
650 background: white;
655 }
651 }
656
652
657 .comment-area {
653 .comment-area {
658 padding: 6px 8px;
654 padding: 6px 8px;
659 border: 1px solid @grey5;
655 border: 1px solid @grey5;
660 .border-radius(@border-radius);
656 .border-radius(@border-radius);
661
657
662 .resolve-action {
658 .resolve-action {
663 padding: 1px 0px 0px 6px;
659 padding: 1px 0px 0px 6px;
664 }
660 }
665
661
666 }
662 }
667
663
668 comment-area-text {
664 comment-area-text {
669 color: @grey3;
665 color: @grey3;
670 }
666 }
671
667
672 .comment-area-header {
668 .comment-area-header {
673 height: 35px;
669 height: 35px;
674 border-bottom: 1px solid @grey5;
670 border-bottom: 1px solid @grey5;
675 }
671 }
676
672
677 .comment-area-header .nav-links {
673 .comment-area-header .nav-links {
678 display: flex;
674 display: flex;
679 flex-flow: row wrap;
675 flex-flow: row wrap;
680 -webkit-flex-flow: row wrap;
676 -webkit-flex-flow: row wrap;
681 width: 100%;
677 width: 100%;
682 border: none;
678 border: none;
683 }
679 }
684
680
685 .comment-area-footer {
681 .comment-area-footer {
686 min-height: 30px;
682 min-height: 30px;
687 }
683 }
688
684
689 .comment-footer .toolbar {
685 .comment-footer .toolbar {
690
686
691 }
687 }
692
688
693 .comment-attachment-uploader {
689 .comment-attachment-uploader {
694 border: 1px dashed white;
690 border: 1px dashed white;
695 border-radius: @border-radius;
691 border-radius: @border-radius;
696 margin-top: -10px;
692 margin-top: -10px;
697 line-height: 30px;
693 line-height: 30px;
698 &.dz-drag-hover {
694 &.dz-drag-hover {
699 border-color: @grey3;
695 border-color: @grey3;
700 }
696 }
701
697
702 .dz-error-message {
698 .dz-error-message {
703 padding-top: 0;
699 padding-top: 0;
704 }
700 }
705 }
701 }
706
702
707 .comment-attachment-text {
703 .comment-attachment-text {
708 clear: both;
704 clear: both;
709 font-size: 11px;
705 font-size: 11px;
710 color: #8F8F8F;
706 color: #8F8F8F;
711 width: 100%;
707 width: 100%;
712 .pick-attachment {
708 .pick-attachment {
713 color: #8F8F8F;
709 color: #8F8F8F;
714 }
710 }
715 .pick-attachment:hover {
711 .pick-attachment:hover {
716 color: @rcblue;
712 color: @rcblue;
717 }
713 }
718 }
714 }
719
715
720 .nav-links {
716 .nav-links {
721 padding: 0;
717 padding: 0;
722 margin: 0;
718 margin: 0;
723 list-style: none;
719 list-style: none;
724 height: auto;
720 height: auto;
725 border-bottom: 1px solid @grey5;
721 border-bottom: 1px solid @grey5;
726 }
722 }
727 .nav-links li {
723 .nav-links li {
728 display: inline-block;
724 display: inline-block;
729 list-style-type: none;
725 list-style-type: none;
730 }
726 }
731
727
732 .nav-links li a.disabled {
728 .nav-links li a.disabled {
733 cursor: not-allowed;
729 cursor: not-allowed;
734 }
730 }
735
731
736 .nav-links li.active a {
732 .nav-links li.active a {
737 border-bottom: 2px solid @rcblue;
733 border-bottom: 2px solid @rcblue;
738 color: #000;
734 color: #000;
739 font-weight: 600;
735 font-weight: 600;
740 }
736 }
741 .nav-links li a {
737 .nav-links li a {
742 display: inline-block;
738 display: inline-block;
743 padding: 0px 10px 5px 10px;
739 padding: 0px 10px 5px 10px;
744 margin-bottom: -1px;
740 margin-bottom: -1px;
745 font-size: 14px;
741 font-size: 14px;
746 line-height: 28px;
742 line-height: 28px;
747 color: #8f8f8f;
743 color: #8f8f8f;
748 border-bottom: 2px solid transparent;
744 border-bottom: 2px solid transparent;
749 }
745 }
750
746
@@ -1,558 +1,557 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6 <%namespace name="base" file="/base/base.mako"/>
7
7
8 <%!
8 <%!
9 from rhodecode.lib import html_filters
9 from rhodecode.lib import html_filters
10 %>
10 %>
11
11
12
12
13 <%def name="comment_block(comment, inline=False, active_pattern_entries=None, is_new=False)">
13 <%def name="comment_block(comment, inline=False, active_pattern_entries=None, is_new=False)">
14
14
15 <%
15 <%
16 from rhodecode.model.comment import CommentsModel
16 from rhodecode.model.comment import CommentsModel
17 comment_model = CommentsModel()
17 comment_model = CommentsModel()
18
18
19 comment_ver = comment.get_index_version(getattr(c, 'versions', []))
19 comment_ver = comment.get_index_version(getattr(c, 'versions', []))
20 latest_ver = len(getattr(c, 'versions', []))
20 latest_ver = len(getattr(c, 'versions', []))
21 visible_for_user = True
21 visible_for_user = True
22 if comment.draft:
22 if comment.draft:
23 visible_for_user = comment.user_id == c.rhodecode_user.user_id
23 visible_for_user = comment.user_id == c.rhodecode_user.user_id
24 %>
24 %>
25
25
26 % if inline:
26 % if inline:
27 <% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
27 <% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
28 % else:
28 % else:
29 <% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
29 <% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
30 % endif
30 % endif
31
31
32 % if visible_for_user:
32 % if visible_for_user:
33 <div class="comment
33 <div class="comment
34 ${'comment-inline' if inline else 'comment-general'}
34 ${'comment-inline' if inline else 'comment-general'}
35 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
35 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
36 id="comment-${comment.comment_id}"
36 id="comment-${comment.comment_id}"
37 line="${comment.line_no}"
37 line="${comment.line_no}"
38 data-comment-id="${comment.comment_id}"
38 data-comment-id="${comment.comment_id}"
39 data-comment-type="${comment.comment_type}"
39 data-comment-type="${comment.comment_type}"
40 data-comment-draft=${h.json.dumps(comment.draft)}
40 data-comment-draft=${h.json.dumps(comment.draft)}
41 data-comment-renderer="${comment.renderer}"
41 data-comment-renderer="${comment.renderer}"
42 data-comment-text="${comment.text | html_filters.base64,n}"
42 data-comment-text="${comment.text | html_filters.base64,n}"
43 data-comment-f-path="${comment.f_path}"
43 data-comment-f-path="${comment.f_path}"
44 data-comment-line-no="${comment.line_no}"
44 data-comment-line-no="${comment.line_no}"
45 data-comment-inline=${h.json.dumps(inline)}
45 data-comment-inline=${h.json.dumps(inline)}
46 style="${'display: none;' if outdated_at_ver else ''}">
46 style="${'display: none;' if outdated_at_ver else ''}">
47
47
48 <div class="meta">
48 <div class="meta">
49 <div class="comment-type-label">
49 <div class="comment-type-label">
50 % if comment.draft:
50 % if comment.draft:
51 <div class="tooltip comment-draft" title="${_('Draft comments are only visible to the author until submitted')}.">
51 <div class="tooltip comment-draft" title="${_('Draft comments are only visible to the author until submitted')}.">
52 DRAFT
52 DRAFT
53 </div>
53 </div>
54 % elif is_new:
54 % elif is_new:
55 <div class="tooltip comment-new" title="${_('This comment was added while you browsed this page')}.">
55 <div class="tooltip comment-new" title="${_('This comment was added while you browsed this page')}.">
56 NEW
56 NEW
57 </div>
57 </div>
58 % endif
58 % endif
59
59
60 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
60 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
61
61
62 ## TODO COMMENT
62 ## TODO COMMENT
63 % if comment.comment_type == 'todo':
63 % if comment.comment_type == 'todo':
64 % if comment.resolved:
64 % if comment.resolved:
65 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
65 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
66 <i class="icon-flag-filled"></i>
66 <i class="icon-flag-filled"></i>
67 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
67 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
68 </div>
68 </div>
69 % else:
69 % else:
70 <div class="resolved tooltip" style="display: none">
70 <div class="resolved tooltip" style="display: none">
71 <span>${comment.comment_type}</span>
71 <span>${comment.comment_type}</span>
72 </div>
72 </div>
73 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to create resolution comment.')}">
73 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to create resolution comment.')}">
74 <i class="icon-flag-filled"></i>
74 <i class="icon-flag-filled"></i>
75 ${comment.comment_type}
75 ${comment.comment_type}
76 </div>
76 </div>
77 % endif
77 % endif
78 ## NOTE COMMENT
78 ## NOTE COMMENT
79 % else:
79 % else:
80 ## RESOLVED NOTE
80 ## RESOLVED NOTE
81 % if comment.resolved_comment:
81 % if comment.resolved_comment:
82 <div class="tooltip" title="${_('This comment resolves TODO #{}').format(comment.resolved_comment.comment_id)}">
82 <div class="tooltip" title="${_('This comment resolves TODO #{}').format(comment.resolved_comment.comment_id)}">
83 fix
83 fix
84 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
84 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
85 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
85 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
86 </a>
86 </a>
87 </div>
87 </div>
88 ## STATUS CHANGE NOTE
88 ## STATUS CHANGE NOTE
89 % elif not comment.is_inline and comment.status_change:
89 % elif not comment.is_inline and comment.status_change:
90 <%
90 <%
91 if comment.pull_request:
91 if comment.pull_request:
92 status_change_title = 'Status of review for pull request !{}'.format(comment.pull_request.pull_request_id)
92 status_change_title = 'Status of review for pull request !{}'.format(comment.pull_request.pull_request_id)
93 else:
93 else:
94 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
94 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
95 %>
95 %>
96
96
97 <i class="icon-circle review-status-${comment.review_status}"></i>
97 <i class="icon-circle review-status-${comment.review_status}"></i>
98 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
98 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
99 ${comment.review_status_lbl}
99 ${comment.review_status_lbl}
100 </div>
100 </div>
101 % else:
101 % else:
102 <div>
102 <div>
103 <i class="icon-comment"></i>
103 <i class="icon-comment"></i>
104 ${(comment.comment_type or 'note')}
104 ${(comment.comment_type or 'note')}
105 </div>
105 </div>
106 % endif
106 % endif
107 % endif
107 % endif
108
108
109 </div>
109 </div>
110 </div>
110 </div>
111 ## NOTE 0 and .. => because we disable it for now until UI ready
111 ## NOTE 0 and .. => because we disable it for now until UI ready
112 % if 0 and comment.status_change:
112 % if 0 and comment.status_change:
113 <div class="pull-left">
113 <div class="pull-left">
114 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
114 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
115 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
115 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
116 ${'!{}'.format(comment.pull_request.pull_request_id)}
116 ${'!{}'.format(comment.pull_request.pull_request_id)}
117 </a>
117 </a>
118 </span>
118 </span>
119 </div>
119 </div>
120 % endif
120 % endif
121 ## Since only author can see drafts, we don't show it
121 ## Since only author can see drafts, we don't show it
122 % if not comment.draft:
122 % if not comment.draft:
123 <div class="author ${'author-inline' if inline else 'author-general'}">
123 <div class="author ${'author-inline' if inline else 'author-general'}">
124 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
124 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
125 </div>
125 </div>
126 % endif
126 % endif
127
127
128 <div class="date">
128 <div class="date">
129 ${h.age_component(comment.modified_at, time_is_local=True)}
129 ${h.age_component(comment.modified_at, time_is_local=True)}
130 </div>
130 </div>
131
131
132 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
132 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
133 <span class="tag authortag tooltip" title="${_('Pull request author')}">
133 <span class="tag authortag tooltip" title="${_('Pull request author')}">
134 ${_('author')}
134 ${_('author')}
135 </span>
135 </span>
136 % endif
136 % endif
137
137
138 <%
138 <%
139 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
139 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
140 %>
140 %>
141
141
142 % if comment.history:
142 % if comment.history:
143 <div class="date">
143 <div class="date">
144
144
145 <input id="${comment_version_selector}" name="${comment_version_selector}"
145 <input id="${comment_version_selector}" name="${comment_version_selector}"
146 type="hidden"
146 type="hidden"
147 data-last-version="${comment.history[-1].version}">
147 data-last-version="${comment.history[-1].version}">
148
148
149 <script type="text/javascript">
149 <script type="text/javascript">
150
150
151 var preLoadVersionData = [
151 var preLoadVersionData = [
152 % for comment_history in comment.history:
152 % for comment_history in comment.history:
153 {
153 {
154 id: ${comment_history.comment_history_id},
154 id: ${comment_history.comment_history_id},
155 text: 'v${comment_history.version}',
155 text: 'v${comment_history.version}',
156 action: function () {
156 action: function () {
157 Rhodecode.comments.showVersion(
157 Rhodecode.comments.showVersion(
158 "${comment.comment_id}",
158 "${comment.comment_id}",
159 "${comment_history.comment_history_id}"
159 "${comment_history.comment_history_id}"
160 )
160 )
161 },
161 },
162 comment_version: "${comment_history.version}",
162 comment_version: "${comment_history.version}",
163 comment_author_username: "${comment_history.author.username}",
163 comment_author_username: "${comment_history.author.username}",
164 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
164 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
165 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
165 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
166 },
166 },
167 % endfor
167 % endfor
168 ]
168 ]
169 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
169 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
170
170
171 </script>
171 </script>
172
172
173 </div>
173 </div>
174 % else:
174 % else:
175 <div class="date" style="display: none">
175 <div class="date" style="display: none">
176 <input id="${comment_version_selector}" name="${comment_version_selector}"
176 <input id="${comment_version_selector}" name="${comment_version_selector}"
177 type="hidden"
177 type="hidden"
178 data-last-version="0">
178 data-last-version="0">
179 </div>
179 </div>
180 %endif
180 %endif
181
181
182 <div class="comment-links-block">
182 <div class="comment-links-block">
183
183
184 % if inline:
184 % if inline:
185 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
185 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
186 % if outdated_at_ver:
186 % if outdated_at_ver:
187 <strong class="comment-outdated-label">outdated</strong> <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
187 <strong class="comment-outdated-label">outdated</strong> <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
188 <code class="action-divider">|</code>
188 <code class="action-divider">|</code>
189 % elif comment_ver:
189 % elif comment_ver:
190 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
190 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
191 <code class="action-divider">|</code>
191 <code class="action-divider">|</code>
192 % endif
192 % endif
193 </a>
193 </a>
194 % else:
194 % else:
195 % if comment_ver:
195 % if comment_ver:
196
196
197 % if comment.outdated:
197 % if comment.outdated:
198 <a class="pr-version"
198 <a class="pr-version"
199 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
199 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
200 >
200 >
201 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}
201 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}
202 </a>
202 </a>
203 <code class="action-divider">|</code>
203 <code class="action-divider">|</code>
204 % else:
204 % else:
205 <a class="tooltip pr-version"
205 <a class="tooltip pr-version"
206 title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}"
206 title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}"
207 href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
207 href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
208 >
208 >
209 <code class="pr-version-num">${'v{}'.format(comment_ver)}</code>
209 <code class="pr-version-num">${'v{}'.format(comment_ver)}</code>
210 </a>
210 </a>
211 <code class="action-divider">|</code>
211 <code class="action-divider">|</code>
212 % endif
212 % endif
213
213
214 % endif
214 % endif
215 % endif
215 % endif
216
216
217 <details class="details-reset details-inline-block">
217 <details class="details-reset details-inline-block">
218 <summary class="noselect"><i class="icon-options cursor-pointer"></i></summary>
218 <summary class="noselect"><i class="icon-options cursor-pointer"></i></summary>
219 <details-menu class="details-dropdown">
219 <details-menu class="details-dropdown">
220
220
221 <div class="dropdown-item">
221 <div class="dropdown-item">
222 ${_('Comment')} #${comment.comment_id}
222 ${_('Comment')} #${comment.comment_id}
223 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${comment_model.get_url(comment,request, permalink=True, anchor='comment-{}'.format(comment.comment_id))}" title="${_('Copy permalink')}"></span>
223 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${comment_model.get_url(comment,request, permalink=True, anchor='comment-{}'.format(comment.comment_id))}" title="${_('Copy permalink')}"></span>
224 </div>
224 </div>
225
225
226 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
226 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
227 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
227 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
228 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
228 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
229 ## permissions to delete
229 ## permissions to delete
230 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
230 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
231 <div class="dropdown-divider"></div>
231 <div class="dropdown-divider"></div>
232 <div class="dropdown-item">
232 <div class="dropdown-item">
233 <a onclick="return Rhodecode.comments.editComment(this, '${comment.line_no}', '${comment.f_path}');" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
233 <a onclick="return Rhodecode.comments.editComment(this, '${comment.line_no}', '${comment.f_path}');" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
234 </div>
234 </div>
235 <div class="dropdown-item">
235 <div class="dropdown-item">
236 <a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
236 <a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
237 </div>
237 </div>
238 ## Only available in EE edition
238 ## Only available in EE edition
239 % if comment.draft and c.rhodecode_edition_id == 'EE':
239 % if comment.draft and c.rhodecode_edition_id == 'EE':
240 <div class="dropdown-item">
240 <div class="dropdown-item">
241 <a onclick="return Rhodecode.comments.finalizeDrafts([${comment.comment_id}]);" class="btn btn-link btn-sm finalize-draft-comment">${_('Submit draft')}</a>
241 <a onclick="return Rhodecode.comments.finalizeDrafts([${comment.comment_id}]);" class="btn btn-link btn-sm finalize-draft-comment">${_('Submit draft')}</a>
242 </div>
242 </div>
243 % endif
243 % endif
244 %else:
244 %else:
245 <div class="dropdown-divider"></div>
245 <div class="dropdown-divider"></div>
246 <div class="dropdown-item">
246 <div class="dropdown-item">
247 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
247 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
248 </div>
248 </div>
249 <div class="dropdown-item">
249 <div class="dropdown-item">
250 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
250 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
251 </div>
251 </div>
252 %endif
252 %endif
253 %else:
253 %else:
254 <div class="dropdown-divider"></div>
254 <div class="dropdown-divider"></div>
255 <div class="dropdown-item">
255 <div class="dropdown-item">
256 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
256 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
257 </div>
257 </div>
258 <div class="dropdown-item">
258 <div class="dropdown-item">
259 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
259 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
260 </div>
260 </div>
261 %endif
261 %endif
262 </details-menu>
262 </details-menu>
263 </details>
263 </details>
264
264
265 <code class="action-divider">|</code>
265 <code class="action-divider">|</code>
266 % if outdated_at_ver:
266 % if outdated_at_ver:
267 <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
267 <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
268 <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
268 <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
269 % else:
269 % else:
270 <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
270 <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
271 <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
271 <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
272 % endif
272 % endif
273
273
274 </div>
274 </div>
275 </div>
275 </div>
276 <div class="text">
276 <div class="text">
277 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
277 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
278 </div>
278 </div>
279
279
280 </div>
280 </div>
281 % endif
281 % endif
282 </%def>
282 </%def>
283
283
284 ## generate main comments
284 ## generate main comments
285 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
285 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
286 <%
286 <%
287 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
287 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
288 %>
288 %>
289
289
290 <div class="general-comments" id="comments">
290 <div class="general-comments" id="comments">
291 %for comment in comments:
291 %for comment in comments:
292 <div id="comment-tr-${comment.comment_id}">
292 <div id="comment-tr-${comment.comment_id}">
293 ## only render comments that are not from pull request, or from
293 ## only render comments that are not from pull request, or from
294 ## pull request and a status change
294 ## pull request and a status change
295 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
295 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
296 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
296 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
297 %endif
297 %endif
298 </div>
298 </div>
299 %endfor
299 %endfor
300 ## to anchor ajax comments
300 ## to anchor ajax comments
301 <div id="injected_page_comments"></div>
301 <div id="injected_page_comments"></div>
302 </div>
302 </div>
303 </%def>
303 </%def>
304
304
305
305
306 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
306 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
307
307
308 <div class="comments">
308 <div class="comments">
309 <%
309 <%
310 if is_pull_request:
310 if is_pull_request:
311 placeholder = _('Leave a comment on this Pull Request.')
311 placeholder = _('Leave a comment on this Pull Request.')
312 elif is_compare:
312 elif is_compare:
313 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
313 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
314 else:
314 else:
315 placeholder = _('Leave a comment on this Commit.')
315 placeholder = _('Leave a comment on this Commit.')
316 %>
316 %>
317
317
318 % if c.rhodecode_user.username != h.DEFAULT_USER:
318 % if c.rhodecode_user.username != h.DEFAULT_USER:
319 <div class="js-template" id="cb-comment-general-form-template">
319 <div class="js-template" id="cb-comment-general-form-template">
320 ## template generated for injection
320 ## template generated for injection
321 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
321 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
322 </div>
322 </div>
323
323
324 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
324 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
325 ## inject form here
325 ## inject form here
326 </div>
326 </div>
327 <script type="text/javascript">
327 <script type="text/javascript">
328 var lineNo = 'general';
329 var resolvesCommentId = null;
328 var resolvesCommentId = null;
330 var generalCommentForm = Rhodecode.comments.createGeneralComment(
329 var generalCommentForm = Rhodecode.comments.createGeneralComment(
331 lineNo, "${placeholder}", resolvesCommentId);
330 'general', "${placeholder}", resolvesCommentId);
332
331
333 // set custom success callback on rangeCommit
332 // set custom success callback on rangeCommit
334 % if is_compare:
333 % if is_compare:
335 generalCommentForm.setHandleFormSubmit(function(o) {
334 generalCommentForm.setHandleFormSubmit(function(o) {
336 var self = generalCommentForm;
335 var self = generalCommentForm;
337
336
338 var text = self.cm.getValue();
337 var text = self.cm.getValue();
339 var status = self.getCommentStatus();
338 var status = self.getCommentStatus();
340 var commentType = self.getCommentType();
339 var commentType = self.getCommentType();
341 var isDraft = self.getDraftState();
340 var isDraft = self.getDraftState();
342
341
343 if (text === "" && !status) {
342 if (text === "" && !status) {
344 return;
343 return;
345 }
344 }
346
345
347 // we can pick which commits we want to make the comment by
346 // we can pick which commits we want to make the comment by
348 // selecting them via click on preview pane, this will alter the hidden inputs
347 // selecting them via click on preview pane, this will alter the hidden inputs
349 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
348 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
350
349
351 var commitIds = [];
350 var commitIds = [];
352 $('#changeset_compare_view_content .compare_select').each(function(el) {
351 $('#changeset_compare_view_content .compare_select').each(function(el) {
353 var commitId = this.id.replace('row-', '');
352 var commitId = this.id.replace('row-', '');
354 if ($(this).hasClass('hl') || !cherryPicked) {
353 if ($(this).hasClass('hl') || !cherryPicked) {
355 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
354 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
356 commitIds.push(commitId);
355 commitIds.push(commitId);
357 } else {
356 } else {
358 $("input[data-commit-id='{0}']".format(commitId)).val('')
357 $("input[data-commit-id='{0}']".format(commitId)).val('')
359 }
358 }
360 });
359 });
361
360
362 self.setActionButtonsDisabled(true);
361 self.setActionButtonsDisabled(true);
363 self.cm.setOption("readOnly", true);
362 self.cm.setOption("readOnly", true);
364 var postData = {
363 var postData = {
365 'text': text,
364 'text': text,
366 'changeset_status': status,
365 'changeset_status': status,
367 'comment_type': commentType,
366 'comment_type': commentType,
368 'draft': isDraft,
367 'draft': isDraft,
369 'commit_ids': commitIds,
368 'commit_ids': commitIds,
370 'csrf_token': CSRF_TOKEN
369 'csrf_token': CSRF_TOKEN
371 };
370 };
372
371
373 var submitSuccessCallback = function(o) {
372 var submitSuccessCallback = function(o) {
374 location.reload(true);
373 location.reload(true);
375 };
374 };
376 var submitFailCallback = function(){
375 var submitFailCallback = function(){
377 self.resetCommentFormState(text)
376 self.resetCommentFormState(text)
378 };
377 };
379 self.submitAjaxPOST(
378 self.submitAjaxPOST(
380 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
379 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
381 });
380 });
382 % endif
381 % endif
383
382
384 </script>
383 </script>
385 % else:
384 % else:
386 ## form state when not logged in
385 ## form state when not logged in
387 <div class="comment-form ac">
386 <div class="comment-form ac">
388
387
389 <div class="comment-area">
388 <div class="comment-area">
390 <div class="comment-area-header">
389 <div class="comment-area-header">
391 <ul class="nav-links clearfix">
390 <ul class="nav-links clearfix">
392 <li class="active">
391 <li class="active">
393 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
392 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
394 </li>
393 </li>
395 <li class="">
394 <li class="">
396 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
395 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
397 </li>
396 </li>
398 </ul>
397 </ul>
399 </div>
398 </div>
400
399
401 <div class="comment-area-write" style="display: block;">
400 <div class="comment-area-write" style="display: block;">
402 <div id="edit-container">
401 <div id="edit-container">
403 <div style="padding: 20px 0px 0px 0;">
402 <div style="padding: 20px 0px 0px 0;">
404 ${_('You need to be logged in to leave comments.')}
403 ${_('You need to be logged in to leave comments.')}
405 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
404 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
406 </div>
405 </div>
407 </div>
406 </div>
408 <div id="preview-container" class="clearfix" style="display: none;">
407 <div id="preview-container" class="clearfix" style="display: none;">
409 <div id="preview-box" class="preview-box"></div>
408 <div id="preview-box" class="preview-box"></div>
410 </div>
409 </div>
411 </div>
410 </div>
412
411
413 <div class="comment-area-footer">
412 <div class="comment-area-footer">
414 <div class="toolbar">
413 <div class="toolbar">
415 <div class="toolbar-text">
414 <div class="toolbar-text">
416 </div>
415 </div>
417 </div>
416 </div>
418 </div>
417 </div>
419 </div>
418 </div>
420
419
421 <div class="comment-footer">
420 <div class="comment-footer">
422 </div>
421 </div>
423
422
424 </div>
423 </div>
425 % endif
424 % endif
426
425
427 <script type="text/javascript">
426 <script type="text/javascript">
428 bindToggleButtons();
427 bindToggleButtons();
429 </script>
428 </script>
430 </div>
429 </div>
431 </%def>
430 </%def>
432
431
433
432
434 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
433 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
435
434
436 ## comment injected based on assumption that user is logged in
435 ## comment injected based on assumption that user is logged in
437 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
436 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
438
437
439 <div class="comment-area">
438 <div class="comment-area">
440 <div class="comment-area-header">
439 <div class="comment-area-header">
441 <div class="pull-left">
440 <div class="pull-left">
442 <ul class="nav-links clearfix">
441 <ul class="nav-links clearfix">
443 <li class="active">
442 <li class="active">
444 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
443 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
445 </li>
444 </li>
446 <li class="">
445 <li class="">
447 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
446 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
448 </li>
447 </li>
449 </ul>
448 </ul>
450 </div>
449 </div>
451 <div class="pull-right">
450 <div class="pull-right">
452 <span class="comment-area-text">${_('Mark as')}:</span>
451 <span class="comment-area-text">${_('Mark as')}:</span>
453 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
452 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
454 % for val in c.visual.comment_types:
453 % for val in c.visual.comment_types:
455 <option value="${val}">${val.upper()}</option>
454 <option value="${val}">${val.upper()}</option>
456 % endfor
455 % endfor
457 </select>
456 </select>
458 </div>
457 </div>
459 </div>
458 </div>
460
459
461 <div class="comment-area-write" style="display: block;">
460 <div class="comment-area-write" style="display: block;">
462 <div id="edit-container_${lineno_id}" style="margin-top: -1px">
461 <div id="edit-container_${lineno_id}" style="margin-top: -1px">
463 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
462 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
464 </div>
463 </div>
465 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
464 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
466 <div id="preview-box_${lineno_id}" class="preview-box"></div>
465 <div id="preview-box_${lineno_id}" class="preview-box"></div>
467 </div>
466 </div>
468 </div>
467 </div>
469
468
470 <div class="comment-area-footer comment-attachment-uploader">
469 <div class="comment-area-footer comment-attachment-uploader">
471 <div class="toolbar">
470 <div class="toolbar">
472
471
473 <div class="comment-attachment-text">
472 <div class="comment-attachment-text">
474 <div class="dropzone-text">
473 <div class="dropzone-text">
475 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
474 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
476 </div>
475 </div>
477 <div class="dropzone-upload" style="display:none">
476 <div class="dropzone-upload" style="display:none">
478 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
477 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
479 </div>
478 </div>
480 </div>
479 </div>
481
480
482 ## comments dropzone template, empty on purpose
481 ## comments dropzone template, empty on purpose
483 <div style="display: none" class="comment-attachment-uploader-template">
482 <div style="display: none" class="comment-attachment-uploader-template">
484 <div class="dz-file-preview" style="margin: 0">
483 <div class="dz-file-preview" style="margin: 0">
485 <div class="dz-error-message"></div>
484 <div class="dz-error-message"></div>
486 </div>
485 </div>
487 </div>
486 </div>
488
487
489 </div>
488 </div>
490 </div>
489 </div>
491 </div>
490 </div>
492
491
493 <div class="comment-footer">
492 <div class="comment-footer">
494
493
495 ## inject extra inputs into the form
494 ## inject extra inputs into the form
496 % if form_extras and isinstance(form_extras, (list, tuple)):
495 % if form_extras and isinstance(form_extras, (list, tuple)):
497 <div id="comment_form_extras">
496 <div id="comment_form_extras">
498 % for form_ex_el in form_extras:
497 % for form_ex_el in form_extras:
499 ${form_ex_el|n}
498 ${form_ex_el|n}
500 % endfor
499 % endfor
501 </div>
500 </div>
502 % endif
501 % endif
503
502
504 <div class="action-buttons">
503 <div class="action-buttons">
505 % if form_type != 'inline':
504 % if form_type != 'inline':
506 <div class="action-buttons-extra"></div>
505 <div class="action-buttons-extra"></div>
507 % endif
506 % endif
508
507
509 <input class="btn btn-success comment-button-input submit-comment-action" id="save_${lineno_id}" name="save" type="submit" value="${_('Add comment')}" data-is-draft=false onclick="$(this).addClass('submitter')">
508 <input class="btn btn-success comment-button-input submit-comment-action" id="save_${lineno_id}" name="save" type="submit" value="${_('Add comment')}" data-is-draft=false onclick="$(this).addClass('submitter')">
510
509
511 % if form_type == 'inline':
510 % if form_type == 'inline':
512 % if c.rhodecode_edition_id == 'EE':
511 % if c.rhodecode_edition_id == 'EE':
513 ## Disable the button for CE, the "real" validation is in the backend code anyway
512 ## Disable the button for CE, the "real" validation is in the backend code anyway
514 <input class="btn btn-warning comment-button-input submit-draft-action" id="save_draft_${lineno_id}" name="save_draft" type="submit" value="${_('Add draft')}" data-is-draft=true onclick="$(this).addClass('submitter')">
513 <input class="btn btn-warning comment-button-input submit-draft-action" id="save_draft_${lineno_id}" name="save_draft" type="submit" value="${_('Add draft')}" data-is-draft=true onclick="$(this).addClass('submitter')">
515 % else:
514 % else:
516 <input class="btn btn-warning comment-button-input submit-draft-action disabled" disabled="disabled" type="submit" value="${_('Add draft')}" onclick="return false;" title="Draft comments only available in EE edition of RhodeCode">
515 <input class="btn btn-warning comment-button-input submit-draft-action disabled" disabled="disabled" type="submit" value="${_('Add draft')}" onclick="return false;" title="Draft comments only available in EE edition of RhodeCode">
517 % endif
516 % endif
518 % endif
517 % endif
519
518
520 % if review_statuses:
519 % if review_statuses:
521 <div class="comment-status-box">
520 <div class="comment-status-box">
522 <select id="change_status_${lineno_id}" name="changeset_status">
521 <select id="change_status_${lineno_id}" name="changeset_status">
523 <option></option> ## Placeholder
522 <option></option> ## Placeholder
524 % for status, lbl in review_statuses:
523 % for status, lbl in review_statuses:
525 <option value="${status}" data-status="${status}">${lbl}</option>
524 <option value="${status}" data-status="${status}">${lbl}</option>
526 %if is_pull_request and change_status and status in ('approved', 'rejected'):
525 %if is_pull_request and change_status and status in ('approved', 'rejected'):
527 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
526 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
528 %endif
527 %endif
529 % endfor
528 % endfor
530 </select>
529 </select>
531 </div>
530 </div>
532 % endif
531 % endif
533
532
534 ## inline for has a file, and line-number together with cancel hide button.
533 ## inline for has a file, and line-number together with cancel hide button.
535 % if form_type == 'inline':
534 % if form_type == 'inline':
536 <input type="hidden" name="f_path" value="{0}">
535 <input type="hidden" name="f_path" value="{0}">
537 <input type="hidden" name="line" value="${lineno_id}">
536 <input type="hidden" name="line" value="${lineno_id}">
538 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
537 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
539 <i class="icon-cancel-circled2"></i>
538 <i class="icon-cancel-circled2"></i>
540 </button>
539 </button>
541 % endif
540 % endif
542 </div>
541 </div>
543
542
544 <div class="toolbar-text">
543 <div class="toolbar-text">
545 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
544 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
546 <p>${_('Styling with {} is supported.').format(renderer_url)|n}
545 <span>${_('Styling with {} is supported.').format(renderer_url)|n}
547
546
548 <i class="icon-info-circled tooltip-hovercard"
547 <i class="icon-info-circled tooltip-hovercard"
549 data-hovercard-alt="ALT"
548 data-hovercard-alt="ALT"
550 data-hovercard-url="javascript:commentHelp('${c.visual.default_renderer.upper()}')"
549 data-hovercard-url="javascript:commentHelp('${c.visual.default_renderer.upper()}')"
551 data-comment-json-b64='${h.b64(h.json.dumps({}))}'></i>
550 data-comment-json-b64='${h.b64(h.json.dumps({}))}'></i>
552 </p>
551 </span>
553 </div>
552 </div>
554 </div>
553 </div>
555
554
556 </form>
555 </form>
557
556
558 </%def> No newline at end of file
557 </%def>
@@ -1,125 +1,123 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.mako"/>
2 <%inherit file="/base/base.mako"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('%s Commits') % c.repo_name} -
5 ${_('%s Commits') % c.repo_name} -
6 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
6 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
7 ...
7 ...
8 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
8 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
9 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
9 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
10 %if c.rhodecode_name:
10 %if c.rhodecode_name:
11 &middot; ${h.branding(c.rhodecode_name)}
11 &middot; ${h.branding(c.rhodecode_name)}
12 %endif
12 %endif
13 </%def>
13 </%def>
14
14
15 <%def name="breadcrumbs_links()"></%def>
15 <%def name="breadcrumbs_links()"></%def>
16
16
17 <%def name="menu_bar_nav()">
17 <%def name="menu_bar_nav()">
18 ${self.menu_items(active='repositories')}
18 ${self.menu_items(active='repositories')}
19 </%def>
19 </%def>
20
20
21 <%def name="menu_bar_subnav()">
21 <%def name="menu_bar_subnav()">
22 ${self.repo_menu(active='commits')}
22 ${self.repo_menu(active='commits')}
23 </%def>
23 </%def>
24
24
25 <%def name="main()">
25 <%def name="main()">
26
26
27 <div class="box">
27 <div class="box">
28 <div class="summary changeset">
28 <div class="summary changeset">
29 <div class="summary-detail">
29 <div class="summary-detail">
30 <div class="summary-detail-header">
30 <div class="summary-detail-header">
31 <span class="breadcrumbs files_location">
31 <span class="breadcrumbs files_location">
32 <h4>
32 <h4>
33 ${_('Commit Range')}
33 ${_('Commit Range')}
34 </h4>
34 </h4>
35 </span>
35 </span>
36
36
37 <div class="clear-fix"></div>
37 <div class="clear-fix"></div>
38 </div>
38 </div>
39
39
40 <div class="fieldset">
40 <div class="fieldset">
41 <div class="left-label-summary">
41 <div class="left-label-summary">
42 <p class="spacing">${_('Range')}:</p>
42 <p class="spacing">${_('Range')}:</p>
43 <div class="right-label-summary">
43 <div class="right-label-summary">
44 <div class="code-header" >
44 <div class="code-header" >
45 <div class="compare_header">
45 <div class="compare_header">
46 <code class="fieldset-text-line">
46 <code class="fieldset-text-line">
47 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
47 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
48 ...
48 ...
49 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
49 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
50 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
50 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
51 </code>
51 </code>
52 </div>
52 </div>
53 </div>
53 </div>
54 </div>
54 </div>
55 </div>
55 </div>
56 </div>
56 </div>
57
57
58 <div class="fieldset">
58 <div class="fieldset">
59 <div class="left-label-summary">
59 <div class="left-label-summary">
60 <p class="spacing">${_('Diff Option')}:</p>
60 <p class="spacing">${_('Diff Option')}:</p>
61 <div class="right-label-summary">
61 <div class="right-label-summary">
62 <div class="code-header" >
62 <div class="code-header" >
63 <div class="compare_header">
63 <div class="compare_header">
64 <a class="btn btn-primary" href="${h.route_path('repo_compare',
64 <a class="btn btn-primary" href="${h.route_path('repo_compare',
65 repo_name=c.repo_name,
65 repo_name=c.repo_name,
66 source_ref_type='rev',
66 source_ref_type='rev',
67 source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'),
67 source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'),
68 target_ref_type='rev',
68 target_ref_type='rev',
69 target_ref=c.commit_ranges[-1].raw_id)}"
69 target_ref=c.commit_ranges[-1].raw_id)}"
70 >
70 >
71 ${_('Show combined diff')}
71 ${_('Show combined diff')}
72 </a>
72 </a>
73 </div>
73 </div>
74 </div>
74 </div>
75 </div>
75 </div>
76 </div>
76 </div>
77 </div>
77 </div>
78
78
79 <div class="clear-fix"></div>
79 <div class="clear-fix"></div>
80 </div> <!-- end summary-detail -->
80 </div> <!-- end summary-detail -->
81 </div> <!-- end summary -->
81 </div> <!-- end summary -->
82
82
83 <div id="changeset_compare_view_content">
83 <div id="changeset_compare_view_content">
84 <div class="pull-left">
84 <div class="pull-left">
85 <div class="btn-group">
85 <div class="btn-group">
86 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
86 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
87 % if c.collapse_all_commits:
87 % if c.collapse_all_commits:
88 <i class="icon-plus-squared-alt icon-no-margin"></i>
88 <i class="icon-plus-squared-alt icon-no-margin"></i>
89 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
89 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
90 % else:
90 % else:
91 <i class="icon-minus-squared-alt icon-no-margin"></i>
91 <i class="icon-minus-squared-alt icon-no-margin"></i>
92 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
92 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
93 % endif
93 % endif
94 </a>
94 </a>
95 </div>
95 </div>
96 </div>
96 </div>
97 ## Commit range generated below
97 ## Commit range generated below
98 <%include file="../compare/compare_commits.mako"/>
98 <%include file="../compare/compare_commits.mako"/>
99 <div class="cs_files">
99 <div class="cs_files">
100 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
100 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
101 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
101 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
102 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
102 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
103
103
104 %for commit in c.commit_ranges:
104 %for commit in c.commit_ranges:
105 ## commit range header for each individual diff
105 ## commit range header for each individual diff
106 <h3>
106 <h3>
107
108
109 <a class="tooltip-hovercard revision" data-hovercard-alt="Commit: ${commit.short_id}" data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=c.repo_name, commit_id=commit.raw_id)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">
107 <a class="tooltip-hovercard revision" data-hovercard-alt="Commit: ${commit.short_id}" data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=c.repo_name, commit_id=commit.raw_id)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">
110 ${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}
108 ${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}
111 </a>
109 </a>
112 </h3>
110 </h3>
113
111
114 ${cbdiffs.render_diffset_menu(c.changes[commit.raw_id])}
112 ${cbdiffs.render_diffset_menu(c.changes[commit.raw_id])}
115 ${cbdiffs.render_diffset(
113 ${cbdiffs.render_diffset(
116 diffset=c.changes[commit.raw_id],
114 diffset=c.changes[commit.raw_id],
117 collapse_when_files_over=5,
115 collapse_when_files_over=5,
118 commit=commit,
116 commit=commit,
119 )}
117 )}
120 %endfor
118 %endfor
121 </div>
119 </div>
122 </div>
120 </div>
123 </div>
121 </div>
124
122
125 </%def>
123 </%def>
General Comments 0
You need to be logged in to leave comments. Login now