##// END OF EJS Templates
comments: show links to unresolved todos...
marcink -
r1344:639b2044 default
parent child Browse files
Show More
@@ -1,1009 +1,1009 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.comment import CommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
63 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69 def __before__(self):
69 def __before__(self):
70 super(PullrequestsController, self).__before__()
70 super(PullrequestsController, self).__before__()
71
71
72 def _load_compare_data(self, pull_request, inline_comments):
72 def _load_compare_data(self, pull_request, inline_comments):
73 """
73 """
74 Load context data needed for generating compare diff
74 Load context data needed for generating compare diff
75
75
76 :param pull_request: object related to the request
76 :param pull_request: object related to the request
77 :param enable_comments: flag to determine if comments are included
77 :param enable_comments: flag to determine if comments are included
78 """
78 """
79 source_repo = pull_request.source_repo
79 source_repo = pull_request.source_repo
80 source_ref_id = pull_request.source_ref_parts.commit_id
80 source_ref_id = pull_request.source_ref_parts.commit_id
81
81
82 target_repo = pull_request.target_repo
82 target_repo = pull_request.target_repo
83 target_ref_id = pull_request.target_ref_parts.commit_id
83 target_ref_id = pull_request.target_ref_parts.commit_id
84
84
85 # despite opening commits for bookmarks/branches/tags, we always
85 # despite opening commits for bookmarks/branches/tags, we always
86 # convert this to rev to prevent changes after bookmark or branch change
86 # convert this to rev to prevent changes after bookmark or branch change
87 c.source_ref_type = 'rev'
87 c.source_ref_type = 'rev'
88 c.source_ref = source_ref_id
88 c.source_ref = source_ref_id
89
89
90 c.target_ref_type = 'rev'
90 c.target_ref_type = 'rev'
91 c.target_ref = target_ref_id
91 c.target_ref = target_ref_id
92
92
93 c.source_repo = source_repo
93 c.source_repo = source_repo
94 c.target_repo = target_repo
94 c.target_repo = target_repo
95
95
96 c.fulldiff = bool(request.GET.get('fulldiff'))
96 c.fulldiff = bool(request.GET.get('fulldiff'))
97
97
98 # diff_limit is the old behavior, will cut off the whole diff
98 # diff_limit is the old behavior, will cut off the whole diff
99 # if the limit is applied otherwise will just hide the
99 # if the limit is applied otherwise will just hide the
100 # big files from the front-end
100 # big files from the front-end
101 diff_limit = self.cut_off_limit_diff
101 diff_limit = self.cut_off_limit_diff
102 file_limit = self.cut_off_limit_file
102 file_limit = self.cut_off_limit_file
103
103
104 pre_load = ["author", "branch", "date", "message"]
104 pre_load = ["author", "branch", "date", "message"]
105
105
106 c.commit_ranges = []
106 c.commit_ranges = []
107 source_commit = EmptyCommit()
107 source_commit = EmptyCommit()
108 target_commit = EmptyCommit()
108 target_commit = EmptyCommit()
109 c.missing_requirements = False
109 c.missing_requirements = False
110 try:
110 try:
111 c.commit_ranges = [
111 c.commit_ranges = [
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 for rev in pull_request.revisions]
113 for rev in pull_request.revisions]
114
114
115 c.statuses = source_repo.statuses(
115 c.statuses = source_repo.statuses(
116 [x.raw_id for x in c.commit_ranges])
116 [x.raw_id for x in c.commit_ranges])
117
117
118 target_commit = source_repo.get_commit(
118 target_commit = source_repo.get_commit(
119 commit_id=safe_str(target_ref_id))
119 commit_id=safe_str(target_ref_id))
120 source_commit = source_repo.get_commit(
120 source_commit = source_repo.get_commit(
121 commit_id=safe_str(source_ref_id))
121 commit_id=safe_str(source_ref_id))
122 except RepositoryRequirementError:
122 except RepositoryRequirementError:
123 c.missing_requirements = True
123 c.missing_requirements = True
124
124
125 # auto collapse if we have more than limit
125 # auto collapse if we have more than limit
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128
128
129 c.changes = {}
129 c.changes = {}
130 c.missing_commits = False
130 c.missing_commits = False
131 if (c.missing_requirements or
131 if (c.missing_requirements or
132 isinstance(source_commit, EmptyCommit) or
132 isinstance(source_commit, EmptyCommit) or
133 source_commit == target_commit):
133 source_commit == target_commit):
134 _parsed = []
134 _parsed = []
135 c.missing_commits = True
135 c.missing_commits = True
136 else:
136 else:
137 vcs_diff = PullRequestModel().get_diff(pull_request)
137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 diff_processor = diffs.DiffProcessor(
138 diff_processor = diffs.DiffProcessor(
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 file_limit=file_limit, show_full_diff=c.fulldiff)
140 file_limit=file_limit, show_full_diff=c.fulldiff)
141
141
142 _parsed = diff_processor.prepare()
142 _parsed = diff_processor.prepare()
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144
144
145 included_files = {}
145 included_files = {}
146 for f in _parsed:
146 for f in _parsed:
147 included_files[f['filename']] = f['stats']
147 included_files[f['filename']] = f['stats']
148
148
149 c.deleted_files = [fname for fname in inline_comments if
149 c.deleted_files = [fname for fname in inline_comments if
150 fname not in included_files]
150 fname not in included_files]
151
151
152 c.deleted_files_comments = collections.defaultdict(dict)
152 c.deleted_files_comments = collections.defaultdict(dict)
153 for fname, per_line_comments in inline_comments.items():
153 for fname, per_line_comments in inline_comments.items():
154 if fname in c.deleted_files:
154 if fname in c.deleted_files:
155 c.deleted_files_comments[fname]['stats'] = 0
155 c.deleted_files_comments[fname]['stats'] = 0
156 c.deleted_files_comments[fname]['comments'] = list()
156 c.deleted_files_comments[fname]['comments'] = list()
157 for lno, comments in per_line_comments.items():
157 for lno, comments in per_line_comments.items():
158 c.deleted_files_comments[fname]['comments'].extend(comments)
158 c.deleted_files_comments[fname]['comments'].extend(comments)
159
159
160 def _node_getter(commit):
160 def _node_getter(commit):
161 def get_node(fname):
161 def get_node(fname):
162 try:
162 try:
163 return commit.get_node(fname)
163 return commit.get_node(fname)
164 except NodeDoesNotExistError:
164 except NodeDoesNotExistError:
165 return None
165 return None
166 return get_node
166 return get_node
167
167
168 c.diffset = codeblocks.DiffSet(
168 c.diffset = codeblocks.DiffSet(
169 repo_name=c.repo_name,
169 repo_name=c.repo_name,
170 source_repo_name=c.source_repo.repo_name,
170 source_repo_name=c.source_repo.repo_name,
171 source_node_getter=_node_getter(target_commit),
171 source_node_getter=_node_getter(target_commit),
172 target_node_getter=_node_getter(source_commit),
172 target_node_getter=_node_getter(source_commit),
173 comments=inline_comments
173 comments=inline_comments
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175
175
176 def _extract_ordering(self, request):
176 def _extract_ordering(self, request):
177 column_index = safe_int(request.GET.get('order[0][column]'))
177 column_index = safe_int(request.GET.get('order[0][column]'))
178 order_dir = request.GET.get('order[0][dir]', 'desc')
178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 order_by = request.GET.get(
179 order_by = request.GET.get(
180 'columns[%s][data][sort]' % column_index, 'name_raw')
180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 return order_by, order_dir
181 return order_by, order_dir
182
182
183 @LoginRequired()
183 @LoginRequired()
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 'repository.admin')
185 'repository.admin')
186 @HasAcceptedRepoType('git', 'hg')
186 @HasAcceptedRepoType('git', 'hg')
187 def show_all(self, repo_name):
187 def show_all(self, repo_name):
188 # filter types
188 # filter types
189 c.active = 'open'
189 c.active = 'open'
190 c.source = str2bool(request.GET.get('source'))
190 c.source = str2bool(request.GET.get('source'))
191 c.closed = str2bool(request.GET.get('closed'))
191 c.closed = str2bool(request.GET.get('closed'))
192 c.my = str2bool(request.GET.get('my'))
192 c.my = str2bool(request.GET.get('my'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 c.repo_name = repo_name
195 c.repo_name = repo_name
196
196
197 opened_by = None
197 opened_by = None
198 if c.my:
198 if c.my:
199 c.active = 'my'
199 c.active = 'my'
200 opened_by = [c.rhodecode_user.user_id]
200 opened_by = [c.rhodecode_user.user_id]
201
201
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 if c.closed:
203 if c.closed:
204 c.active = 'closed'
204 c.active = 'closed'
205 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
206
206
207 if c.awaiting_review and not c.source:
207 if c.awaiting_review and not c.source:
208 c.active = 'awaiting'
208 c.active = 'awaiting'
209 if c.source and not c.awaiting_review:
209 if c.source and not c.awaiting_review:
210 c.active = 'source'
210 c.active = 'source'
211 if c.awaiting_my_review:
211 if c.awaiting_my_review:
212 c.active = 'awaiting_my'
212 c.active = 'awaiting_my'
213
213
214 data = self._get_pull_requests_list(
214 data = self._get_pull_requests_list(
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 if not request.is_xhr:
216 if not request.is_xhr:
217 c.data = json.dumps(data['data'])
217 c.data = json.dumps(data['data'])
218 c.records_total = data['recordsTotal']
218 c.records_total = data['recordsTotal']
219 return render('/pullrequests/pullrequests.mako')
219 return render('/pullrequests/pullrequests.mako')
220 else:
220 else:
221 return json.dumps(data)
221 return json.dumps(data)
222
222
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 # pagination
224 # pagination
225 start = safe_int(request.GET.get('start'), 0)
225 start = safe_int(request.GET.get('start'), 0)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 order_by, order_dir = self._extract_ordering(request)
227 order_by, order_dir = self._extract_ordering(request)
228
228
229 if c.awaiting_review:
229 if c.awaiting_review:
230 pull_requests = PullRequestModel().get_awaiting_review(
230 pull_requests = PullRequestModel().get_awaiting_review(
231 repo_name, source=c.source, opened_by=opened_by,
231 repo_name, source=c.source, opened_by=opened_by,
232 statuses=statuses, offset=start, length=length,
232 statuses=statuses, offset=start, length=length,
233 order_by=order_by, order_dir=order_dir)
233 order_by=order_by, order_dir=order_dir)
234 pull_requests_total_count = PullRequestModel(
234 pull_requests_total_count = PullRequestModel(
235 ).count_awaiting_review(
235 ).count_awaiting_review(
236 repo_name, source=c.source, statuses=statuses,
236 repo_name, source=c.source, statuses=statuses,
237 opened_by=opened_by)
237 opened_by=opened_by)
238 elif c.awaiting_my_review:
238 elif c.awaiting_my_review:
239 pull_requests = PullRequestModel().get_awaiting_my_review(
239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 repo_name, source=c.source, opened_by=opened_by,
240 repo_name, source=c.source, opened_by=opened_by,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 offset=start, length=length, order_by=order_by,
242 offset=start, length=length, order_by=order_by,
243 order_dir=order_dir)
243 order_dir=order_dir)
244 pull_requests_total_count = PullRequestModel(
244 pull_requests_total_count = PullRequestModel(
245 ).count_awaiting_my_review(
245 ).count_awaiting_my_review(
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 statuses=statuses, opened_by=opened_by)
247 statuses=statuses, opened_by=opened_by)
248 else:
248 else:
249 pull_requests = PullRequestModel().get_all(
249 pull_requests = PullRequestModel().get_all(
250 repo_name, source=c.source, opened_by=opened_by,
250 repo_name, source=c.source, opened_by=opened_by,
251 statuses=statuses, offset=start, length=length,
251 statuses=statuses, offset=start, length=length,
252 order_by=order_by, order_dir=order_dir)
252 order_by=order_by, order_dir=order_dir)
253 pull_requests_total_count = PullRequestModel().count_all(
253 pull_requests_total_count = PullRequestModel().count_all(
254 repo_name, source=c.source, statuses=statuses,
254 repo_name, source=c.source, statuses=statuses,
255 opened_by=opened_by)
255 opened_by=opened_by)
256
256
257 from rhodecode.lib.utils import PartialRenderer
257 from rhodecode.lib.utils import PartialRenderer
258 _render = PartialRenderer('data_table/_dt_elements.mako')
258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 data = []
259 data = []
260 for pr in pull_requests:
260 for pr in pull_requests:
261 comments = CommentsModel().get_all_comments(
261 comments = CommentsModel().get_all_comments(
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263
263
264 data.append({
264 data.append({
265 'name': _render('pullrequest_name',
265 'name': _render('pullrequest_name',
266 pr.pull_request_id, pr.target_repo.repo_name),
266 pr.pull_request_id, pr.target_repo.repo_name),
267 'name_raw': pr.pull_request_id,
267 'name_raw': pr.pull_request_id,
268 'status': _render('pullrequest_status',
268 'status': _render('pullrequest_status',
269 pr.calculated_review_status()),
269 pr.calculated_review_status()),
270 'title': _render(
270 'title': _render(
271 'pullrequest_title', pr.title, pr.description),
271 'pullrequest_title', pr.title, pr.description),
272 'description': h.escape(pr.description),
272 'description': h.escape(pr.description),
273 'updated_on': _render('pullrequest_updated_on',
273 'updated_on': _render('pullrequest_updated_on',
274 h.datetime_to_time(pr.updated_on)),
274 h.datetime_to_time(pr.updated_on)),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 'created_on': _render('pullrequest_updated_on',
276 'created_on': _render('pullrequest_updated_on',
277 h.datetime_to_time(pr.created_on)),
277 h.datetime_to_time(pr.created_on)),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 'author': _render('pullrequest_author',
279 'author': _render('pullrequest_author',
280 pr.author.full_contact, ),
280 pr.author.full_contact, ),
281 'author_raw': pr.author.full_name,
281 'author_raw': pr.author.full_name,
282 'comments': _render('pullrequest_comments', len(comments)),
282 'comments': _render('pullrequest_comments', len(comments)),
283 'comments_raw': len(comments),
283 'comments_raw': len(comments),
284 'closed': pr.is_closed(),
284 'closed': pr.is_closed(),
285 })
285 })
286 # json used to render the grid
286 # json used to render the grid
287 data = ({
287 data = ({
288 'data': data,
288 'data': data,
289 'recordsTotal': pull_requests_total_count,
289 'recordsTotal': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
291 })
291 })
292 return data
292 return data
293
293
294 @LoginRequired()
294 @LoginRequired()
295 @NotAnonymous()
295 @NotAnonymous()
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 'repository.admin')
297 'repository.admin')
298 @HasAcceptedRepoType('git', 'hg')
298 @HasAcceptedRepoType('git', 'hg')
299 def index(self):
299 def index(self):
300 source_repo = c.rhodecode_db_repo
300 source_repo = c.rhodecode_db_repo
301
301
302 try:
302 try:
303 source_repo.scm_instance().get_commit()
303 source_repo.scm_instance().get_commit()
304 except EmptyRepositoryError:
304 except EmptyRepositoryError:
305 h.flash(h.literal(_('There are no commits yet')),
305 h.flash(h.literal(_('There are no commits yet')),
306 category='warning')
306 category='warning')
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308
308
309 commit_id = request.GET.get('commit')
309 commit_id = request.GET.get('commit')
310 branch_ref = request.GET.get('branch')
310 branch_ref = request.GET.get('branch')
311 bookmark_ref = request.GET.get('bookmark')
311 bookmark_ref = request.GET.get('bookmark')
312
312
313 try:
313 try:
314 source_repo_data = PullRequestModel().generate_repo_data(
314 source_repo_data = PullRequestModel().generate_repo_data(
315 source_repo, commit_id=commit_id,
315 source_repo, commit_id=commit_id,
316 branch=branch_ref, bookmark=bookmark_ref)
316 branch=branch_ref, bookmark=bookmark_ref)
317 except CommitDoesNotExistError as e:
317 except CommitDoesNotExistError as e:
318 log.exception(e)
318 log.exception(e)
319 h.flash(_('Commit does not exist'), 'error')
319 h.flash(_('Commit does not exist'), 'error')
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321
321
322 default_target_repo = source_repo
322 default_target_repo = source_repo
323
323
324 if source_repo.parent:
324 if source_repo.parent:
325 parent_vcs_obj = source_repo.parent.scm_instance()
325 parent_vcs_obj = source_repo.parent.scm_instance()
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 # change default if we have a parent repo
327 # change default if we have a parent repo
328 default_target_repo = source_repo.parent
328 default_target_repo = source_repo.parent
329
329
330 target_repo_data = PullRequestModel().generate_repo_data(
330 target_repo_data = PullRequestModel().generate_repo_data(
331 default_target_repo)
331 default_target_repo)
332
332
333 selected_source_ref = source_repo_data['refs']['selected_ref']
333 selected_source_ref = source_repo_data['refs']['selected_ref']
334
334
335 title_source_ref = selected_source_ref.split(':', 2)[1]
335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 c.default_title = PullRequestModel().generate_pullrequest_title(
336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 source=source_repo.repo_name,
337 source=source_repo.repo_name,
338 source_ref=title_source_ref,
338 source_ref=title_source_ref,
339 target=default_target_repo.repo_name
339 target=default_target_repo.repo_name
340 )
340 )
341
341
342 c.default_repo_data = {
342 c.default_repo_data = {
343 'source_repo_name': source_repo.repo_name,
343 'source_repo_name': source_repo.repo_name,
344 'source_refs_json': json.dumps(source_repo_data),
344 'source_refs_json': json.dumps(source_repo_data),
345 'target_repo_name': default_target_repo.repo_name,
345 'target_repo_name': default_target_repo.repo_name,
346 'target_refs_json': json.dumps(target_repo_data),
346 'target_refs_json': json.dumps(target_repo_data),
347 }
347 }
348 c.default_source_ref = selected_source_ref
348 c.default_source_ref = selected_source_ref
349
349
350 return render('/pullrequests/pullrequest.mako')
350 return render('/pullrequests/pullrequest.mako')
351
351
352 @LoginRequired()
352 @LoginRequired()
353 @NotAnonymous()
353 @NotAnonymous()
354 @XHRRequired()
354 @XHRRequired()
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 'repository.admin')
356 'repository.admin')
357 @jsonify
357 @jsonify
358 def get_repo_refs(self, repo_name, target_repo_name):
358 def get_repo_refs(self, repo_name, target_repo_name):
359 repo = Repository.get_by_repo_name(target_repo_name)
359 repo = Repository.get_by_repo_name(target_repo_name)
360 if not repo:
360 if not repo:
361 raise HTTPNotFound
361 raise HTTPNotFound
362 return PullRequestModel().generate_repo_data(repo)
362 return PullRequestModel().generate_repo_data(repo)
363
363
364 @LoginRequired()
364 @LoginRequired()
365 @NotAnonymous()
365 @NotAnonymous()
366 @XHRRequired()
366 @XHRRequired()
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 'repository.admin')
368 'repository.admin')
369 @jsonify
369 @jsonify
370 def get_repo_destinations(self, repo_name):
370 def get_repo_destinations(self, repo_name):
371 repo = Repository.get_by_repo_name(repo_name)
371 repo = Repository.get_by_repo_name(repo_name)
372 if not repo:
372 if not repo:
373 raise HTTPNotFound
373 raise HTTPNotFound
374 filter_query = request.GET.get('query')
374 filter_query = request.GET.get('query')
375
375
376 query = Repository.query() \
376 query = Repository.query() \
377 .order_by(func.length(Repository.repo_name)) \
377 .order_by(func.length(Repository.repo_name)) \
378 .filter(or_(
378 .filter(or_(
379 Repository.repo_name == repo.repo_name,
379 Repository.repo_name == repo.repo_name,
380 Repository.fork_id == repo.repo_id))
380 Repository.fork_id == repo.repo_id))
381
381
382 if filter_query:
382 if filter_query:
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 query = query.filter(
384 query = query.filter(
385 Repository.repo_name.ilike(ilike_expression))
385 Repository.repo_name.ilike(ilike_expression))
386
386
387 add_parent = False
387 add_parent = False
388 if repo.parent:
388 if repo.parent:
389 if filter_query in repo.parent.repo_name:
389 if filter_query in repo.parent.repo_name:
390 parent_vcs_obj = repo.parent.scm_instance()
390 parent_vcs_obj = repo.parent.scm_instance()
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 add_parent = True
392 add_parent = True
393
393
394 limit = 20 - 1 if add_parent else 20
394 limit = 20 - 1 if add_parent else 20
395 all_repos = query.limit(limit).all()
395 all_repos = query.limit(limit).all()
396 if add_parent:
396 if add_parent:
397 all_repos += [repo.parent]
397 all_repos += [repo.parent]
398
398
399 repos = []
399 repos = []
400 for obj in self.scm_model.get_repos(all_repos):
400 for obj in self.scm_model.get_repos(all_repos):
401 repos.append({
401 repos.append({
402 'id': obj['name'],
402 'id': obj['name'],
403 'text': obj['name'],
403 'text': obj['name'],
404 'type': 'repo',
404 'type': 'repo',
405 'obj': obj['dbrepo']
405 'obj': obj['dbrepo']
406 })
406 })
407
407
408 data = {
408 data = {
409 'more': False,
409 'more': False,
410 'results': [{
410 'results': [{
411 'text': _('Repositories'),
411 'text': _('Repositories'),
412 'children': repos
412 'children': repos
413 }] if repos else []
413 }] if repos else []
414 }
414 }
415 return data
415 return data
416
416
417 @LoginRequired()
417 @LoginRequired()
418 @NotAnonymous()
418 @NotAnonymous()
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 'repository.admin')
420 'repository.admin')
421 @HasAcceptedRepoType('git', 'hg')
421 @HasAcceptedRepoType('git', 'hg')
422 @auth.CSRFRequired()
422 @auth.CSRFRequired()
423 def create(self, repo_name):
423 def create(self, repo_name):
424 repo = Repository.get_by_repo_name(repo_name)
424 repo = Repository.get_by_repo_name(repo_name)
425 if not repo:
425 if not repo:
426 raise HTTPNotFound
426 raise HTTPNotFound
427
427
428 controls = peppercorn.parse(request.POST.items())
428 controls = peppercorn.parse(request.POST.items())
429
429
430 try:
430 try:
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 except formencode.Invalid as errors:
432 except formencode.Invalid as errors:
433 if errors.error_dict.get('revisions'):
433 if errors.error_dict.get('revisions'):
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 elif errors.error_dict.get('pullrequest_title'):
435 elif errors.error_dict.get('pullrequest_title'):
436 msg = _('Pull request requires a title with min. 3 chars')
436 msg = _('Pull request requires a title with min. 3 chars')
437 else:
437 else:
438 msg = _('Error creating pull request: {}').format(errors)
438 msg = _('Error creating pull request: {}').format(errors)
439 log.exception(msg)
439 log.exception(msg)
440 h.flash(msg, 'error')
440 h.flash(msg, 'error')
441
441
442 # would rather just go back to form ...
442 # would rather just go back to form ...
443 return redirect(url('pullrequest_home', repo_name=repo_name))
443 return redirect(url('pullrequest_home', repo_name=repo_name))
444
444
445 source_repo = _form['source_repo']
445 source_repo = _form['source_repo']
446 source_ref = _form['source_ref']
446 source_ref = _form['source_ref']
447 target_repo = _form['target_repo']
447 target_repo = _form['target_repo']
448 target_ref = _form['target_ref']
448 target_ref = _form['target_ref']
449 commit_ids = _form['revisions'][::-1]
449 commit_ids = _form['revisions'][::-1]
450 reviewers = [
450 reviewers = [
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452
452
453 # find the ancestor for this pr
453 # find the ancestor for this pr
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456
456
457 source_scm = source_db_repo.scm_instance()
457 source_scm = source_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
459
459
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462
462
463 ancestor = source_scm.get_common_ancestor(
463 ancestor = source_scm.get_common_ancestor(
464 source_commit.raw_id, target_commit.raw_id, target_scm)
464 source_commit.raw_id, target_commit.raw_id, target_scm)
465
465
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468
468
469 pullrequest_title = _form['pullrequest_title']
469 pullrequest_title = _form['pullrequest_title']
470 title_source_ref = source_ref.split(':', 2)[1]
470 title_source_ref = source_ref.split(':', 2)[1]
471 if not pullrequest_title:
471 if not pullrequest_title:
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 source=source_repo,
473 source=source_repo,
474 source_ref=title_source_ref,
474 source_ref=title_source_ref,
475 target=target_repo
475 target=target_repo
476 )
476 )
477
477
478 description = _form['pullrequest_desc']
478 description = _form['pullrequest_desc']
479 try:
479 try:
480 pull_request = PullRequestModel().create(
480 pull_request = PullRequestModel().create(
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 target_ref, commit_ids, reviewers, pullrequest_title,
482 target_ref, commit_ids, reviewers, pullrequest_title,
483 description
483 description
484 )
484 )
485 Session().commit()
485 Session().commit()
486 h.flash(_('Successfully opened new pull request'),
486 h.flash(_('Successfully opened new pull request'),
487 category='success')
487 category='success')
488 except Exception as e:
488 except Exception as e:
489 msg = _('Error occurred during sending pull request')
489 msg = _('Error occurred during sending pull request')
490 log.exception(msg)
490 log.exception(msg)
491 h.flash(msg, category='error')
491 h.flash(msg, category='error')
492 return redirect(url('pullrequest_home', repo_name=repo_name))
492 return redirect(url('pullrequest_home', repo_name=repo_name))
493
493
494 return redirect(url('pullrequest_show', repo_name=target_repo,
494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 pull_request_id=pull_request.pull_request_id))
495 pull_request_id=pull_request.pull_request_id))
496
496
497 @LoginRequired()
497 @LoginRequired()
498 @NotAnonymous()
498 @NotAnonymous()
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 'repository.admin')
500 'repository.admin')
501 @auth.CSRFRequired()
501 @auth.CSRFRequired()
502 @jsonify
502 @jsonify
503 def update(self, repo_name, pull_request_id):
503 def update(self, repo_name, pull_request_id):
504 pull_request_id = safe_int(pull_request_id)
504 pull_request_id = safe_int(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
506 # only owner or admin can update it
506 # only owner or admin can update it
507 allowed_to_update = PullRequestModel().check_user_update(
507 allowed_to_update = PullRequestModel().check_user_update(
508 pull_request, c.rhodecode_user)
508 pull_request, c.rhodecode_user)
509 if allowed_to_update:
509 if allowed_to_update:
510 controls = peppercorn.parse(request.POST.items())
510 controls = peppercorn.parse(request.POST.items())
511
511
512 if 'review_members' in controls:
512 if 'review_members' in controls:
513 self._update_reviewers(
513 self._update_reviewers(
514 pull_request_id, controls['review_members'])
514 pull_request_id, controls['review_members'])
515 elif str2bool(request.POST.get('update_commits', 'false')):
515 elif str2bool(request.POST.get('update_commits', 'false')):
516 self._update_commits(pull_request)
516 self._update_commits(pull_request)
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 self._reject_close(pull_request)
518 self._reject_close(pull_request)
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 self._edit_pull_request(pull_request)
520 self._edit_pull_request(pull_request)
521 else:
521 else:
522 raise HTTPBadRequest()
522 raise HTTPBadRequest()
523 return True
523 return True
524 raise HTTPForbidden()
524 raise HTTPForbidden()
525
525
526 def _edit_pull_request(self, pull_request):
526 def _edit_pull_request(self, pull_request):
527 try:
527 try:
528 PullRequestModel().edit(
528 PullRequestModel().edit(
529 pull_request, request.POST.get('title'),
529 pull_request, request.POST.get('title'),
530 request.POST.get('description'))
530 request.POST.get('description'))
531 except ValueError:
531 except ValueError:
532 msg = _(u'Cannot update closed pull requests.')
532 msg = _(u'Cannot update closed pull requests.')
533 h.flash(msg, category='error')
533 h.flash(msg, category='error')
534 return
534 return
535 else:
535 else:
536 Session().commit()
536 Session().commit()
537
537
538 msg = _(u'Pull request title & description updated.')
538 msg = _(u'Pull request title & description updated.')
539 h.flash(msg, category='success')
539 h.flash(msg, category='success')
540 return
540 return
541
541
542 def _update_commits(self, pull_request):
542 def _update_commits(self, pull_request):
543 resp = PullRequestModel().update_commits(pull_request)
543 resp = PullRequestModel().update_commits(pull_request)
544
544
545 if resp.executed:
545 if resp.executed:
546 msg = _(
546 msg = _(
547 u'Pull request updated to "{source_commit_id}" with '
547 u'Pull request updated to "{source_commit_id}" with '
548 u'{count_added} added, {count_removed} removed commits.')
548 u'{count_added} added, {count_removed} removed commits.')
549 msg = msg.format(
549 msg = msg.format(
550 source_commit_id=pull_request.source_ref_parts.commit_id,
550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 count_added=len(resp.changes.added),
551 count_added=len(resp.changes.added),
552 count_removed=len(resp.changes.removed))
552 count_removed=len(resp.changes.removed))
553 h.flash(msg, category='success')
553 h.flash(msg, category='success')
554
554
555 registry = get_current_registry()
555 registry = get_current_registry()
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 if channelstream_config.get('enabled'):
558 if channelstream_config.get('enabled'):
559 message = msg + (
559 message = msg + (
560 ' - <a onclick="window.location.reload()">'
560 ' - <a onclick="window.location.reload()">'
561 '<strong>{}</strong></a>'.format(_('Reload page')))
561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 channel = '/repo${}$/pr/{}'.format(
562 channel = '/repo${}$/pr/{}'.format(
563 pull_request.target_repo.repo_name,
563 pull_request.target_repo.repo_name,
564 pull_request.pull_request_id
564 pull_request.pull_request_id
565 )
565 )
566 payload = {
566 payload = {
567 'type': 'message',
567 'type': 'message',
568 'user': 'system',
568 'user': 'system',
569 'exclude_users': [request.user.username],
569 'exclude_users': [request.user.username],
570 'channel': channel,
570 'channel': channel,
571 'message': {
571 'message': {
572 'message': message,
572 'message': message,
573 'level': 'success',
573 'level': 'success',
574 'topic': '/notifications'
574 'topic': '/notifications'
575 }
575 }
576 }
576 }
577 channelstream_request(
577 channelstream_request(
578 channelstream_config, [payload], '/message',
578 channelstream_config, [payload], '/message',
579 raise_exc=False)
579 raise_exc=False)
580 else:
580 else:
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 warning_reasons = [
582 warning_reasons = [
583 UpdateFailureReason.NO_CHANGE,
583 UpdateFailureReason.NO_CHANGE,
584 UpdateFailureReason.WRONG_REF_TPYE,
584 UpdateFailureReason.WRONG_REF_TPYE,
585 ]
585 ]
586 category = 'warning' if resp.reason in warning_reasons else 'error'
586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 h.flash(msg, category=category)
587 h.flash(msg, category=category)
588
588
589 @auth.CSRFRequired()
589 @auth.CSRFRequired()
590 @LoginRequired()
590 @LoginRequired()
591 @NotAnonymous()
591 @NotAnonymous()
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 'repository.admin')
593 'repository.admin')
594 def merge(self, repo_name, pull_request_id):
594 def merge(self, repo_name, pull_request_id):
595 """
595 """
596 POST /{repo_name}/pull-request/{pull_request_id}
596 POST /{repo_name}/pull-request/{pull_request_id}
597
597
598 Merge will perform a server-side merge of the specified
598 Merge will perform a server-side merge of the specified
599 pull request, if the pull request is approved and mergeable.
599 pull request, if the pull request is approved and mergeable.
600 After successful merging, the pull request is automatically
600 After successful merging, the pull request is automatically
601 closed, with a relevant comment.
601 closed, with a relevant comment.
602 """
602 """
603 pull_request_id = safe_int(pull_request_id)
603 pull_request_id = safe_int(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
605 user = c.rhodecode_user
605 user = c.rhodecode_user
606
606
607 check = MergeCheck.validate(pull_request, user)
607 check = MergeCheck.validate(pull_request, user)
608 merge_possible = not check.failed
608 merge_possible = not check.failed
609
609
610 for err_type, error_msg in check.errors:
610 for err_type, error_msg in check.errors:
611 h.flash(error_msg, category=err_type)
611 h.flash(error_msg, category=err_type)
612
612
613 if merge_possible:
613 if merge_possible:
614 log.debug("Pre-conditions checked, trying to merge.")
614 log.debug("Pre-conditions checked, trying to merge.")
615 extras = vcs_operation_context(
615 extras = vcs_operation_context(
616 request.environ, repo_name=pull_request.target_repo.repo_name,
616 request.environ, repo_name=pull_request.target_repo.repo_name,
617 username=user.username, action='push',
617 username=user.username, action='push',
618 scm=pull_request.target_repo.repo_type)
618 scm=pull_request.target_repo.repo_type)
619 self._merge_pull_request(pull_request, user, extras)
619 self._merge_pull_request(pull_request, user, extras)
620
620
621 return redirect(url(
621 return redirect(url(
622 'pullrequest_show',
622 'pullrequest_show',
623 repo_name=pull_request.target_repo.repo_name,
623 repo_name=pull_request.target_repo.repo_name,
624 pull_request_id=pull_request.pull_request_id))
624 pull_request_id=pull_request.pull_request_id))
625
625
626 def _merge_pull_request(self, pull_request, user, extras):
626 def _merge_pull_request(self, pull_request, user, extras):
627 merge_resp = PullRequestModel().merge(
627 merge_resp = PullRequestModel().merge(
628 pull_request, user, extras=extras)
628 pull_request, user, extras=extras)
629
629
630 if merge_resp.executed:
630 if merge_resp.executed:
631 log.debug("The merge was successful, closing the pull request.")
631 log.debug("The merge was successful, closing the pull request.")
632 PullRequestModel().close_pull_request(
632 PullRequestModel().close_pull_request(
633 pull_request.pull_request_id, user)
633 pull_request.pull_request_id, user)
634 Session().commit()
634 Session().commit()
635 msg = _('Pull request was successfully merged and closed.')
635 msg = _('Pull request was successfully merged and closed.')
636 h.flash(msg, category='success')
636 h.flash(msg, category='success')
637 else:
637 else:
638 log.debug(
638 log.debug(
639 "The merge was not successful. Merge response: %s",
639 "The merge was not successful. Merge response: %s",
640 merge_resp)
640 merge_resp)
641 msg = PullRequestModel().merge_status_message(
641 msg = PullRequestModel().merge_status_message(
642 merge_resp.failure_reason)
642 merge_resp.failure_reason)
643 h.flash(msg, category='error')
643 h.flash(msg, category='error')
644
644
645 def _update_reviewers(self, pull_request_id, review_members):
645 def _update_reviewers(self, pull_request_id, review_members):
646 reviewers = [
646 reviewers = [
647 (int(r['user_id']), r['reasons']) for r in review_members]
647 (int(r['user_id']), r['reasons']) for r in review_members]
648 PullRequestModel().update_reviewers(pull_request_id, reviewers)
648 PullRequestModel().update_reviewers(pull_request_id, reviewers)
649 Session().commit()
649 Session().commit()
650
650
651 def _reject_close(self, pull_request):
651 def _reject_close(self, pull_request):
652 if pull_request.is_closed():
652 if pull_request.is_closed():
653 raise HTTPForbidden()
653 raise HTTPForbidden()
654
654
655 PullRequestModel().close_pull_request_with_comment(
655 PullRequestModel().close_pull_request_with_comment(
656 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
656 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
657 Session().commit()
657 Session().commit()
658
658
659 @LoginRequired()
659 @LoginRequired()
660 @NotAnonymous()
660 @NotAnonymous()
661 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
661 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
662 'repository.admin')
662 'repository.admin')
663 @auth.CSRFRequired()
663 @auth.CSRFRequired()
664 @jsonify
664 @jsonify
665 def delete(self, repo_name, pull_request_id):
665 def delete(self, repo_name, pull_request_id):
666 pull_request_id = safe_int(pull_request_id)
666 pull_request_id = safe_int(pull_request_id)
667 pull_request = PullRequest.get_or_404(pull_request_id)
667 pull_request = PullRequest.get_or_404(pull_request_id)
668 # only owner can delete it !
668 # only owner can delete it !
669 if pull_request.author.user_id == c.rhodecode_user.user_id:
669 if pull_request.author.user_id == c.rhodecode_user.user_id:
670 PullRequestModel().delete(pull_request)
670 PullRequestModel().delete(pull_request)
671 Session().commit()
671 Session().commit()
672 h.flash(_('Successfully deleted pull request'),
672 h.flash(_('Successfully deleted pull request'),
673 category='success')
673 category='success')
674 return redirect(url('my_account_pullrequests'))
674 return redirect(url('my_account_pullrequests'))
675 raise HTTPForbidden()
675 raise HTTPForbidden()
676
676
677 def _get_pr_version(self, pull_request_id, version=None):
677 def _get_pr_version(self, pull_request_id, version=None):
678 pull_request_id = safe_int(pull_request_id)
678 pull_request_id = safe_int(pull_request_id)
679 at_version = None
679 at_version = None
680
680
681 if version and version == 'latest':
681 if version and version == 'latest':
682 pull_request_ver = PullRequest.get(pull_request_id)
682 pull_request_ver = PullRequest.get(pull_request_id)
683 pull_request_obj = pull_request_ver
683 pull_request_obj = pull_request_ver
684 _org_pull_request_obj = pull_request_obj
684 _org_pull_request_obj = pull_request_obj
685 at_version = 'latest'
685 at_version = 'latest'
686 elif version:
686 elif version:
687 pull_request_ver = PullRequestVersion.get_or_404(version)
687 pull_request_ver = PullRequestVersion.get_or_404(version)
688 pull_request_obj = pull_request_ver
688 pull_request_obj = pull_request_ver
689 _org_pull_request_obj = pull_request_ver.pull_request
689 _org_pull_request_obj = pull_request_ver.pull_request
690 at_version = pull_request_ver.pull_request_version_id
690 at_version = pull_request_ver.pull_request_version_id
691 else:
691 else:
692 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
692 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
693
693
694 pull_request_display_obj = PullRequest.get_pr_display_object(
694 pull_request_display_obj = PullRequest.get_pr_display_object(
695 pull_request_obj, _org_pull_request_obj)
695 pull_request_obj, _org_pull_request_obj)
696 return _org_pull_request_obj, pull_request_obj, \
696 return _org_pull_request_obj, pull_request_obj, \
697 pull_request_display_obj, at_version
697 pull_request_display_obj, at_version
698
698
699 def _get_pr_version_changes(self, version, pull_request_latest):
699 def _get_pr_version_changes(self, version, pull_request_latest):
700 """
700 """
701 Generate changes commits, and diff data based on the current pr version
701 Generate changes commits, and diff data based on the current pr version
702 """
702 """
703
703
704 #TODO(marcink): save those changes as JSON metadata for chaching later.
704 #TODO(marcink): save those changes as JSON metadata for chaching later.
705
705
706 # fake the version to add the "initial" state object
706 # fake the version to add the "initial" state object
707 pull_request_initial = PullRequest.get_pr_display_object(
707 pull_request_initial = PullRequest.get_pr_display_object(
708 pull_request_latest, pull_request_latest,
708 pull_request_latest, pull_request_latest,
709 internal_methods=['get_commit', 'versions'])
709 internal_methods=['get_commit', 'versions'])
710 pull_request_initial.revisions = []
710 pull_request_initial.revisions = []
711 pull_request_initial.source_repo.get_commit = types.MethodType(
711 pull_request_initial.source_repo.get_commit = types.MethodType(
712 lambda *a, **k: EmptyCommit(), pull_request_initial)
712 lambda *a, **k: EmptyCommit(), pull_request_initial)
713 pull_request_initial.source_repo.scm_instance = types.MethodType(
713 pull_request_initial.source_repo.scm_instance = types.MethodType(
714 lambda *a, **k: EmptyRepository(), pull_request_initial)
714 lambda *a, **k: EmptyRepository(), pull_request_initial)
715
715
716 _changes_versions = [pull_request_latest] + \
716 _changes_versions = [pull_request_latest] + \
717 list(reversed(c.versions)) + \
717 list(reversed(c.versions)) + \
718 [pull_request_initial]
718 [pull_request_initial]
719
719
720 if version == 'latest':
720 if version == 'latest':
721 index = 0
721 index = 0
722 else:
722 else:
723 for pos, prver in enumerate(_changes_versions):
723 for pos, prver in enumerate(_changes_versions):
724 ver = getattr(prver, 'pull_request_version_id', -1)
724 ver = getattr(prver, 'pull_request_version_id', -1)
725 if ver == safe_int(version):
725 if ver == safe_int(version):
726 index = pos
726 index = pos
727 break
727 break
728 else:
728 else:
729 index = 0
729 index = 0
730
730
731 cur_obj = _changes_versions[index]
731 cur_obj = _changes_versions[index]
732 prev_obj = _changes_versions[index + 1]
732 prev_obj = _changes_versions[index + 1]
733
733
734 old_commit_ids = set(prev_obj.revisions)
734 old_commit_ids = set(prev_obj.revisions)
735 new_commit_ids = set(cur_obj.revisions)
735 new_commit_ids = set(cur_obj.revisions)
736
736
737 changes = PullRequestModel()._calculate_commit_id_changes(
737 changes = PullRequestModel()._calculate_commit_id_changes(
738 old_commit_ids, new_commit_ids)
738 old_commit_ids, new_commit_ids)
739
739
740 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
740 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
741 cur_obj, prev_obj)
741 cur_obj, prev_obj)
742 file_changes = PullRequestModel()._calculate_file_changes(
742 file_changes = PullRequestModel()._calculate_file_changes(
743 old_diff_data, new_diff_data)
743 old_diff_data, new_diff_data)
744 return changes, file_changes
744 return changes, file_changes
745
745
746 @LoginRequired()
746 @LoginRequired()
747 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
747 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
748 'repository.admin')
748 'repository.admin')
749 def show(self, repo_name, pull_request_id):
749 def show(self, repo_name, pull_request_id):
750 pull_request_id = safe_int(pull_request_id)
750 pull_request_id = safe_int(pull_request_id)
751 version = request.GET.get('version')
751 version = request.GET.get('version')
752 merge_checks = request.GET.get('merge_checks')
752 merge_checks = request.GET.get('merge_checks')
753
753
754 (pull_request_latest,
754 (pull_request_latest,
755 pull_request_at_ver,
755 pull_request_at_ver,
756 pull_request_display_obj,
756 pull_request_display_obj,
757 at_version) = self._get_pr_version(pull_request_id, version=version)
757 at_version) = self._get_pr_version(pull_request_id, version=version)
758
758
759 c.template_context['pull_request_data']['pull_request_id'] = \
759 c.template_context['pull_request_data']['pull_request_id'] = \
760 pull_request_id
760 pull_request_id
761
761
762 # pull_requests repo_name we opened it against
762 # pull_requests repo_name we opened it against
763 # ie. target_repo must match
763 # ie. target_repo must match
764 if repo_name != pull_request_at_ver.target_repo.repo_name:
764 if repo_name != pull_request_at_ver.target_repo.repo_name:
765 raise HTTPNotFound
765 raise HTTPNotFound
766
766
767 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
767 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
768 pull_request_at_ver)
768 pull_request_at_ver)
769
769
770 c.ancestor = None # TODO: add ancestor here
770 c.ancestor = None # TODO: add ancestor here
771 c.pull_request = pull_request_display_obj
771 c.pull_request = pull_request_display_obj
772 c.pull_request_latest = pull_request_latest
772 c.pull_request_latest = pull_request_latest
773
773
774 pr_closed = pull_request_latest.is_closed()
774 pr_closed = pull_request_latest.is_closed()
775 if at_version and not at_version == 'latest':
775 if at_version and not at_version == 'latest':
776 c.allowed_to_change_status = False
776 c.allowed_to_change_status = False
777 c.allowed_to_update = False
777 c.allowed_to_update = False
778 c.allowed_to_merge = False
778 c.allowed_to_merge = False
779 c.allowed_to_delete = False
779 c.allowed_to_delete = False
780 c.allowed_to_comment = False
780 c.allowed_to_comment = False
781 else:
781 else:
782 c.allowed_to_change_status = PullRequestModel(). \
782 c.allowed_to_change_status = PullRequestModel(). \
783 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
783 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
784 c.allowed_to_update = PullRequestModel().check_user_update(
784 c.allowed_to_update = PullRequestModel().check_user_update(
785 pull_request_latest, c.rhodecode_user) and not pr_closed
785 pull_request_latest, c.rhodecode_user) and not pr_closed
786 c.allowed_to_merge = PullRequestModel().check_user_merge(
786 c.allowed_to_merge = PullRequestModel().check_user_merge(
787 pull_request_latest, c.rhodecode_user) and not pr_closed
787 pull_request_latest, c.rhodecode_user) and not pr_closed
788 c.allowed_to_delete = PullRequestModel().check_user_delete(
788 c.allowed_to_delete = PullRequestModel().check_user_delete(
789 pull_request_latest, c.rhodecode_user) and not pr_closed
789 pull_request_latest, c.rhodecode_user) and not pr_closed
790 c.allowed_to_comment = not pr_closed
790 c.allowed_to_comment = not pr_closed
791
791
792 cc_model = CommentsModel()
792 cc_model = CommentsModel()
793
793
794 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
794 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
795 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
795 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
796
796
797 c.versions = pull_request_display_obj.versions()
797 c.versions = pull_request_display_obj.versions()
798 c.at_version = at_version
798 c.at_version = at_version
799 c.at_version_num = at_version if at_version and at_version != 'latest' else None
799 c.at_version_num = at_version if at_version and at_version != 'latest' else None
800 c.at_version_pos = ChangesetComment.get_index_from_version(
800 c.at_version_pos = ChangesetComment.get_index_from_version(
801 c.at_version_num, c.versions)
801 c.at_version_num, c.versions)
802
802
803 # GENERAL COMMENTS with versions #
803 # GENERAL COMMENTS with versions #
804 q = cc_model._all_general_comments_of_pull_request(pull_request_latest)
804 q = cc_model._all_general_comments_of_pull_request(pull_request_latest)
805 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
805 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
806
806
807 # pick comments we want to render at current version
807 # pick comments we want to render at current version
808 c.comment_versions = cc_model.aggregate_comments(
808 c.comment_versions = cc_model.aggregate_comments(
809 general_comments, c.versions, c.at_version_num)
809 general_comments, c.versions, c.at_version_num)
810 c.comments = c.comment_versions[c.at_version_num]['until']
810 c.comments = c.comment_versions[c.at_version_num]['until']
811
811
812 # INLINE COMMENTS with versions #
812 # INLINE COMMENTS with versions #
813 q = cc_model._all_inline_comments_of_pull_request(pull_request_latest)
813 q = cc_model._all_inline_comments_of_pull_request(pull_request_latest)
814 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
814 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
815 c.inline_versions = cc_model.aggregate_comments(
815 c.inline_versions = cc_model.aggregate_comments(
816 inline_comments, c.versions, c.at_version_num, inline=True)
816 inline_comments, c.versions, c.at_version_num, inline=True)
817
817
818 # if we use version, then do not show later comments
818 # if we use version, then do not show later comments
819 # than current version
819 # than current version
820 display_inline_comments = collections.defaultdict(lambda: collections.defaultdict(list))
820 display_inline_comments = collections.defaultdict(lambda: collections.defaultdict(list))
821 for co in inline_comments:
821 for co in inline_comments:
822 if c.at_version_num:
822 if c.at_version_num:
823 # pick comments that are at least UPTO given version, so we
823 # pick comments that are at least UPTO given version, so we
824 # don't render comments for higher version
824 # don't render comments for higher version
825 should_render = co.pull_request_version_id and \
825 should_render = co.pull_request_version_id and \
826 co.pull_request_version_id <= c.at_version_num
826 co.pull_request_version_id <= c.at_version_num
827 else:
827 else:
828 # showing all, for 'latest'
828 # showing all, for 'latest'
829 should_render = True
829 should_render = True
830
830
831 if should_render:
831 if should_render:
832 display_inline_comments[co.f_path][co.line_no].append(co)
832 display_inline_comments[co.f_path][co.line_no].append(co)
833
833
834 _merge_check = MergeCheck.validate(
834 _merge_check = MergeCheck.validate(
835 pull_request_latest, user=c.rhodecode_user)
835 pull_request_latest, user=c.rhodecode_user)
836 c.pr_merge_errors = _merge_check.errors
836 c.pr_merge_errors = _merge_check.error_details
837 c.pr_merge_possible = not _merge_check.failed
837 c.pr_merge_possible = not _merge_check.failed
838 c.pr_merge_message = _merge_check.merge_msg
838 c.pr_merge_message = _merge_check.merge_msg
839
839
840 if merge_checks:
840 if merge_checks:
841 return render('/pullrequests/pullrequest_merge_checks.mako')
841 return render('/pullrequests/pullrequest_merge_checks.mako')
842
842
843 # load compare data into template context
843 # load compare data into template context
844 self._load_compare_data(pull_request_at_ver, display_inline_comments)
844 self._load_compare_data(pull_request_at_ver, display_inline_comments)
845
845
846 # this is a hack to properly display links, when creating PR, the
846 # this is a hack to properly display links, when creating PR, the
847 # compare view and others uses different notation, and
847 # compare view and others uses different notation, and
848 # compare_commits.mako renders links based on the target_repo.
848 # compare_commits.mako renders links based on the target_repo.
849 # We need to swap that here to generate it properly on the html side
849 # We need to swap that here to generate it properly on the html side
850 c.target_repo = c.source_repo
850 c.target_repo = c.source_repo
851
851
852 if c.allowed_to_update:
852 if c.allowed_to_update:
853 force_close = ('forced_closed', _('Close Pull Request'))
853 force_close = ('forced_closed', _('Close Pull Request'))
854 statuses = ChangesetStatus.STATUSES + [force_close]
854 statuses = ChangesetStatus.STATUSES + [force_close]
855 else:
855 else:
856 statuses = ChangesetStatus.STATUSES
856 statuses = ChangesetStatus.STATUSES
857 c.commit_statuses = statuses
857 c.commit_statuses = statuses
858
858
859 c.changes = None
859 c.changes = None
860 c.file_changes = None
860 c.file_changes = None
861
861
862 c.show_version_changes = 1 # control flag, not used yet
862 c.show_version_changes = 1 # control flag, not used yet
863
863
864 if at_version and c.show_version_changes:
864 if at_version and c.show_version_changes:
865 c.changes, c.file_changes = self._get_pr_version_changes(
865 c.changes, c.file_changes = self._get_pr_version_changes(
866 version, pull_request_latest)
866 version, pull_request_latest)
867
867
868 return render('/pullrequests/pullrequest_show.mako')
868 return render('/pullrequests/pullrequest_show.mako')
869
869
870 @LoginRequired()
870 @LoginRequired()
871 @NotAnonymous()
871 @NotAnonymous()
872 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
872 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
873 'repository.admin')
873 'repository.admin')
874 @auth.CSRFRequired()
874 @auth.CSRFRequired()
875 @jsonify
875 @jsonify
876 def comment(self, repo_name, pull_request_id):
876 def comment(self, repo_name, pull_request_id):
877 pull_request_id = safe_int(pull_request_id)
877 pull_request_id = safe_int(pull_request_id)
878 pull_request = PullRequest.get_or_404(pull_request_id)
878 pull_request = PullRequest.get_or_404(pull_request_id)
879 if pull_request.is_closed():
879 if pull_request.is_closed():
880 raise HTTPForbidden()
880 raise HTTPForbidden()
881
881
882 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
882 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
883 # as a changeset status, still we want to send it in one value.
883 # as a changeset status, still we want to send it in one value.
884 status = request.POST.get('changeset_status', None)
884 status = request.POST.get('changeset_status', None)
885 text = request.POST.get('text')
885 text = request.POST.get('text')
886 comment_type = request.POST.get('comment_type')
886 comment_type = request.POST.get('comment_type')
887 resolves_comment_id = request.POST.get('resolves_comment_id', None)
887 resolves_comment_id = request.POST.get('resolves_comment_id', None)
888
888
889 if status and '_closed' in status:
889 if status and '_closed' in status:
890 close_pr = True
890 close_pr = True
891 status = status.replace('_closed', '')
891 status = status.replace('_closed', '')
892 else:
892 else:
893 close_pr = False
893 close_pr = False
894
894
895 forced = (status == 'forced')
895 forced = (status == 'forced')
896 if forced:
896 if forced:
897 status = 'rejected'
897 status = 'rejected'
898
898
899 allowed_to_change_status = PullRequestModel().check_user_change_status(
899 allowed_to_change_status = PullRequestModel().check_user_change_status(
900 pull_request, c.rhodecode_user)
900 pull_request, c.rhodecode_user)
901
901
902 if status and allowed_to_change_status:
902 if status and allowed_to_change_status:
903 message = (_('Status change %(transition_icon)s %(status)s')
903 message = (_('Status change %(transition_icon)s %(status)s')
904 % {'transition_icon': '>',
904 % {'transition_icon': '>',
905 'status': ChangesetStatus.get_status_lbl(status)})
905 'status': ChangesetStatus.get_status_lbl(status)})
906 if close_pr:
906 if close_pr:
907 message = _('Closing with') + ' ' + message
907 message = _('Closing with') + ' ' + message
908 text = text or message
908 text = text or message
909 comm = CommentsModel().create(
909 comm = CommentsModel().create(
910 text=text,
910 text=text,
911 repo=c.rhodecode_db_repo.repo_id,
911 repo=c.rhodecode_db_repo.repo_id,
912 user=c.rhodecode_user.user_id,
912 user=c.rhodecode_user.user_id,
913 pull_request=pull_request_id,
913 pull_request=pull_request_id,
914 f_path=request.POST.get('f_path'),
914 f_path=request.POST.get('f_path'),
915 line_no=request.POST.get('line'),
915 line_no=request.POST.get('line'),
916 status_change=(ChangesetStatus.get_status_lbl(status)
916 status_change=(ChangesetStatus.get_status_lbl(status)
917 if status and allowed_to_change_status else None),
917 if status and allowed_to_change_status else None),
918 status_change_type=(status
918 status_change_type=(status
919 if status and allowed_to_change_status else None),
919 if status and allowed_to_change_status else None),
920 closing_pr=close_pr,
920 closing_pr=close_pr,
921 comment_type=comment_type,
921 comment_type=comment_type,
922 resolves_comment_id=resolves_comment_id
922 resolves_comment_id=resolves_comment_id
923 )
923 )
924
924
925 if allowed_to_change_status:
925 if allowed_to_change_status:
926 old_calculated_status = pull_request.calculated_review_status()
926 old_calculated_status = pull_request.calculated_review_status()
927 # get status if set !
927 # get status if set !
928 if status:
928 if status:
929 ChangesetStatusModel().set_status(
929 ChangesetStatusModel().set_status(
930 c.rhodecode_db_repo.repo_id,
930 c.rhodecode_db_repo.repo_id,
931 status,
931 status,
932 c.rhodecode_user.user_id,
932 c.rhodecode_user.user_id,
933 comm,
933 comm,
934 pull_request=pull_request_id
934 pull_request=pull_request_id
935 )
935 )
936
936
937 Session().flush()
937 Session().flush()
938 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
938 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
939 # we now calculate the status of pull request, and based on that
939 # we now calculate the status of pull request, and based on that
940 # calculation we set the commits status
940 # calculation we set the commits status
941 calculated_status = pull_request.calculated_review_status()
941 calculated_status = pull_request.calculated_review_status()
942 if old_calculated_status != calculated_status:
942 if old_calculated_status != calculated_status:
943 PullRequestModel()._trigger_pull_request_hook(
943 PullRequestModel()._trigger_pull_request_hook(
944 pull_request, c.rhodecode_user, 'review_status_change')
944 pull_request, c.rhodecode_user, 'review_status_change')
945
945
946 calculated_status_lbl = ChangesetStatus.get_status_lbl(
946 calculated_status_lbl = ChangesetStatus.get_status_lbl(
947 calculated_status)
947 calculated_status)
948
948
949 if close_pr:
949 if close_pr:
950 status_completed = (
950 status_completed = (
951 calculated_status in [ChangesetStatus.STATUS_APPROVED,
951 calculated_status in [ChangesetStatus.STATUS_APPROVED,
952 ChangesetStatus.STATUS_REJECTED])
952 ChangesetStatus.STATUS_REJECTED])
953 if forced or status_completed:
953 if forced or status_completed:
954 PullRequestModel().close_pull_request(
954 PullRequestModel().close_pull_request(
955 pull_request_id, c.rhodecode_user)
955 pull_request_id, c.rhodecode_user)
956 else:
956 else:
957 h.flash(_('Closing pull request on other statuses than '
957 h.flash(_('Closing pull request on other statuses than '
958 'rejected or approved is forbidden. '
958 'rejected or approved is forbidden. '
959 'Calculated status from all reviewers '
959 'Calculated status from all reviewers '
960 'is currently: %s') % calculated_status_lbl,
960 'is currently: %s') % calculated_status_lbl,
961 category='warning')
961 category='warning')
962
962
963 Session().commit()
963 Session().commit()
964
964
965 if not request.is_xhr:
965 if not request.is_xhr:
966 return redirect(h.url('pullrequest_show', repo_name=repo_name,
966 return redirect(h.url('pullrequest_show', repo_name=repo_name,
967 pull_request_id=pull_request_id))
967 pull_request_id=pull_request_id))
968
968
969 data = {
969 data = {
970 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
970 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
971 }
971 }
972 if comm:
972 if comm:
973 c.co = comm
973 c.co = comm
974 c.inline_comment = True if comm.line_no else False
974 c.inline_comment = True if comm.line_no else False
975 data.update(comm.get_dict())
975 data.update(comm.get_dict())
976 data.update({'rendered_text':
976 data.update({'rendered_text':
977 render('changeset/changeset_comment_block.mako')})
977 render('changeset/changeset_comment_block.mako')})
978
978
979 return data
979 return data
980
980
981 @LoginRequired()
981 @LoginRequired()
982 @NotAnonymous()
982 @NotAnonymous()
983 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
983 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
984 'repository.admin')
984 'repository.admin')
985 @auth.CSRFRequired()
985 @auth.CSRFRequired()
986 @jsonify
986 @jsonify
987 def delete_comment(self, repo_name, comment_id):
987 def delete_comment(self, repo_name, comment_id):
988 return self._delete_comment(comment_id)
988 return self._delete_comment(comment_id)
989
989
990 def _delete_comment(self, comment_id):
990 def _delete_comment(self, comment_id):
991 comment_id = safe_int(comment_id)
991 comment_id = safe_int(comment_id)
992 co = ChangesetComment.get_or_404(comment_id)
992 co = ChangesetComment.get_or_404(comment_id)
993 if co.pull_request.is_closed():
993 if co.pull_request.is_closed():
994 # don't allow deleting comments on closed pull request
994 # don't allow deleting comments on closed pull request
995 raise HTTPForbidden()
995 raise HTTPForbidden()
996
996
997 is_owner = co.author.user_id == c.rhodecode_user.user_id
997 is_owner = co.author.user_id == c.rhodecode_user.user_id
998 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
998 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
999 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
999 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1000 old_calculated_status = co.pull_request.calculated_review_status()
1000 old_calculated_status = co.pull_request.calculated_review_status()
1001 CommentsModel().delete(comment=co)
1001 CommentsModel().delete(comment=co)
1002 Session().commit()
1002 Session().commit()
1003 calculated_status = co.pull_request.calculated_review_status()
1003 calculated_status = co.pull_request.calculated_review_status()
1004 if old_calculated_status != calculated_status:
1004 if old_calculated_status != calculated_status:
1005 PullRequestModel()._trigger_pull_request_hook(
1005 PullRequestModel()._trigger_pull_request_hook(
1006 co.pull_request, c.rhodecode_user, 'review_status_change')
1006 co.pull_request, c.rhodecode_user, 'review_status_change')
1007 return True
1007 return True
1008 else:
1008 else:
1009 raise HTTPForbidden()
1009 raise HTTPForbidden()
@@ -1,547 +1,558 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: 0.6;
8
7 .comments {
9 .comments {
8 width: 100%;
10 width: 100%;
9 }
11 }
10
12
11 tr.inline-comments div {
13 tr.inline-comments div {
12 max-width: 100%;
14 max-width: 100%;
13
15
14 p {
16 p {
15 white-space: normal;
17 white-space: normal;
16 }
18 }
17
19
18 code, pre, .code, dd {
20 code, pre, .code, dd {
19 overflow-x: auto;
21 overflow-x: auto;
20 width: 1062px;
22 width: 1062px;
21 }
23 }
22
24
23 dd {
25 dd {
24 width: auto;
26 width: auto;
25 }
27 }
26 }
28 }
27
29
28 #injected_page_comments {
30 #injected_page_comments {
29 .comment-previous-link,
31 .comment-previous-link,
30 .comment-next-link,
32 .comment-next-link,
31 .comment-links-divider {
33 .comment-links-divider {
32 display: none;
34 display: none;
33 }
35 }
34 }
36 }
35
37
36 .add-comment {
38 .add-comment {
37 margin-bottom: 10px;
39 margin-bottom: 10px;
38 }
40 }
39 .hide-comment-button .add-comment {
41 .hide-comment-button .add-comment {
40 display: none;
42 display: none;
41 }
43 }
42
44
43 .comment-bubble {
45 .comment-bubble {
44 color: @grey4;
46 color: @grey4;
45 margin-top: 4px;
47 margin-top: 4px;
46 margin-right: 30px;
48 margin-right: 30px;
47 visibility: hidden;
49 visibility: hidden;
48 }
50 }
49
51
50 .comment-label {
52 .comment-label {
51 float: left;
53 float: left;
52
54
53 padding: 0.4em 0.4em;
55 padding: 0.4em 0.4em;
54 margin: 3px 5px 0px -10px;
56 margin: 3px 5px 0px -10px;
55 display: inline-block;
57 display: inline-block;
56 min-height: 0;
58 min-height: 0;
57
59
58 text-align: center;
60 text-align: center;
59 font-size: 10px;
61 font-size: 10px;
60 line-height: .8em;
62 line-height: .8em;
61
63
62 font-family: @text-italic;
64 font-family: @text-italic;
63 background: #fff none;
65 background: #fff none;
64 color: @grey4;
66 color: @grey4;
65 border: 1px solid @grey4;
67 border: 1px solid @grey4;
66 white-space: nowrap;
68 white-space: nowrap;
67
69
68 text-transform: uppercase;
70 text-transform: uppercase;
69 min-width: 40px;
71 min-width: 40px;
70
72
71 &.todo {
73 &.todo {
72 color: @color5;
74 color: @color5;
73 font-family: @text-bold-italic;
75 font-family: @text-bold-italic;
74 }
76 }
75
77
76 .resolve {
78 .resolve {
77 cursor: pointer;
79 cursor: pointer;
78 text-decoration: underline;
80 text-decoration: underline;
79 }
81 }
80
82
81 .resolved {
83 .resolved {
82 text-decoration: line-through;
84 text-decoration: line-through;
83 color: @color1;
85 color: @color1;
84 }
86 }
85 .resolved a {
87 .resolved a {
86 text-decoration: line-through;
88 text-decoration: line-through;
87 color: @color1;
89 color: @color1;
88 }
90 }
89 .resolve-text {
91 .resolve-text {
90 color: @color1;
92 color: @color1;
91 margin: 2px 8px;
93 margin: 2px 8px;
92 font-family: @text-italic;
94 font-family: @text-italic;
93 }
95 }
94
95 }
96 }
96
97
97
98
98 .comment {
99 .comment {
99
100
100 &.comment-general {
101 &.comment-general {
101 border: 1px solid @grey5;
102 border: 1px solid @grey5;
102 padding: 5px 5px 5px 5px;
103 padding: 5px 5px 5px 5px;
103 }
104 }
104
105
105 margin: @padding 0;
106 margin: @padding 0;
106 padding: 4px 0 0 0;
107 padding: 4px 0 0 0;
107 line-height: 1em;
108 line-height: 1em;
108
109
109 .rc-user {
110 .rc-user {
110 min-width: 0;
111 min-width: 0;
111 margin: 0px .5em 0 0;
112 margin: 0px .5em 0 0;
112
113
113 .user {
114 .user {
114 display: inline;
115 display: inline;
115 }
116 }
116 }
117 }
117
118
118 .meta {
119 .meta {
119 position: relative;
120 position: relative;
120 width: 100%;
121 width: 100%;
121 border-bottom: 1px solid @grey5;
122 border-bottom: 1px solid @grey5;
122 margin: -5px 0px;
123 margin: -5px 0px;
123 line-height: 24px;
124 line-height: 24px;
124
125
125 &:hover .permalink {
126 &:hover .permalink {
126 visibility: visible;
127 visibility: visible;
127 color: @rcblue;
128 color: @rcblue;
128 }
129 }
129 }
130 }
130
131
131 .author,
132 .author,
132 .date {
133 .date {
133 display: inline;
134 display: inline;
134
135
135 &:after {
136 &:after {
136 content: ' | ';
137 content: ' | ';
137 color: @grey5;
138 color: @grey5;
138 }
139 }
139 }
140 }
140
141
141 .author-general img {
142 .author-general img {
142 top: 3px;
143 top: 3px;
143 }
144 }
144 .author-inline img {
145 .author-inline img {
145 top: 3px;
146 top: 3px;
146 }
147 }
147
148
148 .status-change,
149 .status-change,
149 .permalink,
150 .permalink,
150 .changeset-status-lbl {
151 .changeset-status-lbl {
151 display: inline;
152 display: inline;
152 }
153 }
153
154
154 .permalink {
155 .permalink {
155 visibility: hidden;
156 visibility: hidden;
156 }
157 }
157
158
158 .comment-links-divider {
159 .comment-links-divider {
159 display: inline;
160 display: inline;
160 }
161 }
161
162
162 .comment-links-block {
163 .comment-links-block {
163 float:right;
164 float:right;
164 text-align: right;
165 text-align: right;
165 min-width: 85px;
166 min-width: 85px;
166
167
167 [class^="icon-"]:before,
168 [class^="icon-"]:before,
168 [class*=" icon-"]:before {
169 [class*=" icon-"]:before {
169 margin-left: 0;
170 margin-left: 0;
170 margin-right: 0;
171 margin-right: 0;
171 }
172 }
172 }
173 }
173
174
174 .comment-previous-link {
175 .comment-previous-link {
175 display: inline-block;
176 display: inline-block;
176
177
177 .arrow_comment_link{
178 .arrow_comment_link{
178 cursor: pointer;
179 cursor: pointer;
179 i {
180 i {
180 font-size:10px;
181 font-size:10px;
181 }
182 }
182 }
183 }
183 .arrow_comment_link.disabled {
184 .arrow_comment_link.disabled {
184 cursor: default;
185 cursor: default;
185 color: @grey5;
186 color: @grey5;
186 }
187 }
187 }
188 }
188
189
189 .comment-next-link {
190 .comment-next-link {
190 display: inline-block;
191 display: inline-block;
191
192
192 .arrow_comment_link{
193 .arrow_comment_link{
193 cursor: pointer;
194 cursor: pointer;
194 i {
195 i {
195 font-size:10px;
196 font-size:10px;
196 }
197 }
197 }
198 }
198 .arrow_comment_link.disabled {
199 .arrow_comment_link.disabled {
199 cursor: default;
200 cursor: default;
200 color: @grey5;
201 color: @grey5;
201 }
202 }
202 }
203 }
203
204
204 .flag_status {
205 .flag_status {
205 display: inline-block;
206 display: inline-block;
206 margin: -2px .5em 0 .25em
207 margin: -2px .5em 0 .25em
207 }
208 }
208
209
209 .delete-comment {
210 .delete-comment {
210 display: inline-block;
211 display: inline-block;
211 color: @rcblue;
212 color: @rcblue;
212
213
213 &:hover {
214 &:hover {
214 cursor: pointer;
215 cursor: pointer;
215 }
216 }
216 }
217 }
217
218
218 .text {
219 .text {
219 clear: both;
220 clear: both;
220 .border-radius(@border-radius);
221 .border-radius(@border-radius);
221 .box-sizing(border-box);
222 .box-sizing(border-box);
222
223
223 .markdown-block p,
224 .markdown-block p,
224 .rst-block p {
225 .rst-block p {
225 margin: .5em 0 !important;
226 margin: .5em 0 !important;
226 // TODO: lisa: This is needed because of other rst !important rules :[
227 // TODO: lisa: This is needed because of other rst !important rules :[
227 }
228 }
228 }
229 }
229
230
230 .pr-version {
231 .pr-version {
231 float: left;
232 float: left;
232 margin: 0px 4px;
233 margin: 0px 4px;
233 }
234 }
234 .pr-version-inline {
235 .pr-version-inline {
235 float: left;
236 float: left;
236 margin: 0px 4px;
237 margin: 0px 4px;
237 }
238 }
238 .pr-version-num {
239 .pr-version-num {
239 font-size: 10px;
240 font-size: 10px;
240 }
241 }
241
242 }
242 }
243
243
244 @comment-padding: 5px;
244 @comment-padding: 5px;
245
245
246 .general-comments {
247 .comment-outdated {
248 opacity: @comment-outdated-opacity;
249 }
250 }
251
246 .inline-comments {
252 .inline-comments {
247 border-radius: @border-radius;
253 border-radius: @border-radius;
248 .comment {
254 .comment {
249 margin: 0;
255 margin: 0;
250 border-radius: @border-radius;
256 border-radius: @border-radius;
251 }
257 }
252 .comment-outdated {
258 .comment-outdated {
253 opacity: 0.5;
259 opacity: @comment-outdated-opacity;
254 }
260 }
255
261
256 .comment-inline {
262 .comment-inline {
257 background: white;
263 background: white;
258 padding: @comment-padding @comment-padding;
264 padding: @comment-padding @comment-padding;
259 border: @comment-padding solid @grey6;
265 border: @comment-padding solid @grey6;
260
266
261 .text {
267 .text {
262 border: none;
268 border: none;
263 }
269 }
264 .meta {
270 .meta {
265 border-bottom: 1px solid @grey6;
271 border-bottom: 1px solid @grey6;
266 margin: -5px 0px;
272 margin: -5px 0px;
267 line-height: 24px;
273 line-height: 24px;
268 }
274 }
269 }
275 }
270 .comment-selected {
276 .comment-selected {
271 border-left: 6px solid @comment-highlight-color;
277 border-left: 6px solid @comment-highlight-color;
272 }
278 }
273 .comment-inline-form {
279 .comment-inline-form {
274 padding: @comment-padding;
280 padding: @comment-padding;
275 display: none;
281 display: none;
276 }
282 }
277 .cb-comment-add-button {
283 .cb-comment-add-button {
278 margin: @comment-padding;
284 margin: @comment-padding;
279 }
285 }
280 /* hide add comment button when form is open */
286 /* hide add comment button when form is open */
281 .comment-inline-form-open ~ .cb-comment-add-button {
287 .comment-inline-form-open ~ .cb-comment-add-button {
282 display: none;
288 display: none;
283 }
289 }
284 .comment-inline-form-open {
290 .comment-inline-form-open {
285 display: block;
291 display: block;
286 }
292 }
287 /* hide add comment button when form but no comments */
293 /* hide add comment button when form but no comments */
288 .comment-inline-form:first-child + .cb-comment-add-button {
294 .comment-inline-form:first-child + .cb-comment-add-button {
289 display: none;
295 display: none;
290 }
296 }
291 /* hide add comment button when no comments or form */
297 /* hide add comment button when no comments or form */
292 .cb-comment-add-button:first-child {
298 .cb-comment-add-button:first-child {
293 display: none;
299 display: none;
294 }
300 }
295 /* hide add comment button when only comment is being deleted */
301 /* hide add comment button when only comment is being deleted */
296 .comment-deleting:first-child + .cb-comment-add-button {
302 .comment-deleting:first-child + .cb-comment-add-button {
297 display: none;
303 display: none;
298 }
304 }
299 }
305 }
300
306
301
307
302 .show-outdated-comments {
308 .show-outdated-comments {
303 display: inline;
309 display: inline;
304 color: @rcblue;
310 color: @rcblue;
305 }
311 }
306
312
307 // Comment Form
313 // Comment Form
308 div.comment-form {
314 div.comment-form {
309 margin-top: 20px;
315 margin-top: 20px;
310 }
316 }
311
317
312 .comment-form strong {
318 .comment-form strong {
313 display: block;
319 display: block;
314 margin-bottom: 15px;
320 margin-bottom: 15px;
315 }
321 }
316
322
317 .comment-form textarea {
323 .comment-form textarea {
318 width: 100%;
324 width: 100%;
319 height: 100px;
325 height: 100px;
320 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
326 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
321 }
327 }
322
328
323 form.comment-form {
329 form.comment-form {
324 margin-top: 10px;
330 margin-top: 10px;
325 margin-left: 10px;
331 margin-left: 10px;
326 }
332 }
327
333
328 .comment-inline-form .comment-block-ta,
334 .comment-inline-form .comment-block-ta,
329 .comment-form .comment-block-ta,
335 .comment-form .comment-block-ta,
330 .comment-form .preview-box {
336 .comment-form .preview-box {
331 .border-radius(@border-radius);
337 .border-radius(@border-radius);
332 .box-sizing(border-box);
338 .box-sizing(border-box);
333 background-color: white;
339 background-color: white;
334 }
340 }
335
341
336 .comment-form-submit {
342 .comment-form-submit {
337 margin-top: 5px;
343 margin-top: 5px;
338 margin-left: 525px;
344 margin-left: 525px;
339 }
345 }
340
346
341 .file-comments {
347 .file-comments {
342 display: none;
348 display: none;
343 }
349 }
344
350
345 .comment-form .preview-box.unloaded,
351 .comment-form .preview-box.unloaded,
346 .comment-inline-form .preview-box.unloaded {
352 .comment-inline-form .preview-box.unloaded {
347 height: 50px;
353 height: 50px;
348 text-align: center;
354 text-align: center;
349 padding: 20px;
355 padding: 20px;
350 background-color: white;
356 background-color: white;
351 }
357 }
352
358
353 .comment-footer {
359 .comment-footer {
354 position: relative;
360 position: relative;
355 width: 100%;
361 width: 100%;
356 min-height: 42px;
362 min-height: 42px;
357
363
358 .status_box,
364 .status_box,
359 .cancel-button {
365 .cancel-button {
360 float: left;
366 float: left;
361 display: inline-block;
367 display: inline-block;
362 }
368 }
363
369
364 .action-buttons {
370 .action-buttons {
365 float: right;
371 float: right;
366 display: inline-block;
372 display: inline-block;
367 }
373 }
368 }
374 }
369
375
370 .comment-form {
376 .comment-form {
371
377
372 .comment {
378 .comment {
373 margin-left: 10px;
379 margin-left: 10px;
374 }
380 }
375
381
376 .comment-help {
382 .comment-help {
377 color: @grey4;
383 color: @grey4;
378 padding: 5px 0 5px 0;
384 padding: 5px 0 5px 0;
379 }
385 }
380
386
381 .comment-title {
387 .comment-title {
382 padding: 5px 0 5px 0;
388 padding: 5px 0 5px 0;
383 }
389 }
384
390
385 .comment-button {
391 .comment-button {
386 display: inline-block;
392 display: inline-block;
387 }
393 }
388
394
389 .comment-button-input {
395 .comment-button-input {
390 margin-right: 0;
396 margin-right: 0;
391 }
397 }
392
398
393 .comment-footer {
399 .comment-footer {
394 margin-bottom: 110px;
400 margin-bottom: 110px;
395 margin-top: 10px;
401 margin-top: 10px;
396 }
402 }
397 }
403 }
398
404
399
405
400 .comment-form-login {
406 .comment-form-login {
401 .comment-help {
407 .comment-help {
402 padding: 0.9em; //same as the button
408 padding: 0.9em; //same as the button
403 }
409 }
404
410
405 div.clearfix {
411 div.clearfix {
406 clear: both;
412 clear: both;
407 width: 100%;
413 width: 100%;
408 display: block;
414 display: block;
409 }
415 }
410 }
416 }
411
417
412 .comment-type {
418 .comment-type {
413 margin: 0px;
419 margin: 0px;
414 border-radius: inherit;
420 border-radius: inherit;
415 border-color: @grey6;
421 border-color: @grey6;
416 }
422 }
417
423
418 .preview-box {
424 .preview-box {
419 min-height: 105px;
425 min-height: 105px;
420 margin-bottom: 15px;
426 margin-bottom: 15px;
421 background-color: white;
427 background-color: white;
422 .border-radius(@border-radius);
428 .border-radius(@border-radius);
423 .box-sizing(border-box);
429 .box-sizing(border-box);
424 }
430 }
425
431
426 .add-another-button {
432 .add-another-button {
427 margin-left: 10px;
433 margin-left: 10px;
428 margin-top: 10px;
434 margin-top: 10px;
429 margin-bottom: 10px;
435 margin-bottom: 10px;
430 }
436 }
431
437
432 .comment .buttons {
438 .comment .buttons {
433 float: right;
439 float: right;
434 margin: -1px 0px 0px 0px;
440 margin: -1px 0px 0px 0px;
435 }
441 }
436
442
437 // Inline Comment Form
443 // Inline Comment Form
438 .injected_diff .comment-inline-form,
444 .injected_diff .comment-inline-form,
439 .comment-inline-form {
445 .comment-inline-form {
440 background-color: white;
446 background-color: white;
441 margin-top: 10px;
447 margin-top: 10px;
442 margin-bottom: 20px;
448 margin-bottom: 20px;
443 }
449 }
444
450
445 .inline-form {
451 .inline-form {
446 padding: 10px 7px;
452 padding: 10px 7px;
447 }
453 }
448
454
449 .inline-form div {
455 .inline-form div {
450 max-width: 100%;
456 max-width: 100%;
451 }
457 }
452
458
453 .overlay {
459 .overlay {
454 display: none;
460 display: none;
455 position: absolute;
461 position: absolute;
456 width: 100%;
462 width: 100%;
457 text-align: center;
463 text-align: center;
458 vertical-align: middle;
464 vertical-align: middle;
459 font-size: 16px;
465 font-size: 16px;
460 background: none repeat scroll 0 0 white;
466 background: none repeat scroll 0 0 white;
461
467
462 &.submitting {
468 &.submitting {
463 display: block;
469 display: block;
464 opacity: 0.5;
470 opacity: 0.5;
465 z-index: 100;
471 z-index: 100;
466 }
472 }
467 }
473 }
468 .comment-inline-form .overlay.submitting .overlay-text {
474 .comment-inline-form .overlay.submitting .overlay-text {
469 margin-top: 5%;
475 margin-top: 5%;
470 }
476 }
471
477
472 .comment-inline-form .clearfix,
478 .comment-inline-form .clearfix,
473 .comment-form .clearfix {
479 .comment-form .clearfix {
474 .border-radius(@border-radius);
480 .border-radius(@border-radius);
475 margin: 0px;
481 margin: 0px;
476 }
482 }
477
483
478 .comment-inline-form .comment-footer {
484 .comment-inline-form .comment-footer {
479 margin: 10px 0px 0px 0px;
485 margin: 10px 0px 0px 0px;
480 }
486 }
481
487
482 .hide-inline-form-button {
488 .hide-inline-form-button {
483 margin-left: 5px;
489 margin-left: 5px;
484 }
490 }
485 .comment-button .hide-inline-form {
491 .comment-button .hide-inline-form {
486 background: white;
492 background: white;
487 }
493 }
488
494
489 .comment-area {
495 .comment-area {
490 padding: 8px 12px;
496 padding: 8px 12px;
491 border: 1px solid @grey5;
497 border: 1px solid @grey5;
492 .border-radius(@border-radius);
498 .border-radius(@border-radius);
499
500 .resolve-action {
501 padding: 1px 0px 0px 6px;
502 }
503
493 }
504 }
494
505
495 .comment-area-header .nav-links {
506 .comment-area-header .nav-links {
496 display: flex;
507 display: flex;
497 flex-flow: row wrap;
508 flex-flow: row wrap;
498 -webkit-flex-flow: row wrap;
509 -webkit-flex-flow: row wrap;
499 width: 100%;
510 width: 100%;
500 }
511 }
501
512
502 .comment-area-footer {
513 .comment-area-footer {
503 display: flex;
514 display: flex;
504 }
515 }
505
516
506 .comment-footer .toolbar {
517 .comment-footer .toolbar {
507
518
508 }
519 }
509
520
510 .nav-links {
521 .nav-links {
511 padding: 0;
522 padding: 0;
512 margin: 0;
523 margin: 0;
513 list-style: none;
524 list-style: none;
514 height: auto;
525 height: auto;
515 border-bottom: 1px solid @grey5;
526 border-bottom: 1px solid @grey5;
516 }
527 }
517 .nav-links li {
528 .nav-links li {
518 display: inline-block;
529 display: inline-block;
519 }
530 }
520 .nav-links li:before {
531 .nav-links li:before {
521 content: "";
532 content: "";
522 }
533 }
523 .nav-links li a.disabled {
534 .nav-links li a.disabled {
524 cursor: not-allowed;
535 cursor: not-allowed;
525 }
536 }
526
537
527 .nav-links li.active a {
538 .nav-links li.active a {
528 border-bottom: 2px solid @rcblue;
539 border-bottom: 2px solid @rcblue;
529 color: #000;
540 color: #000;
530 font-weight: 600;
541 font-weight: 600;
531 }
542 }
532 .nav-links li a {
543 .nav-links li a {
533 display: inline-block;
544 display: inline-block;
534 padding: 0px 10px 5px 10px;
545 padding: 0px 10px 5px 10px;
535 margin-bottom: -1px;
546 margin-bottom: -1px;
536 font-size: 14px;
547 font-size: 14px;
537 line-height: 28px;
548 line-height: 28px;
538 color: #8f8f8f;
549 color: #8f8f8f;
539 border-bottom: 2px solid transparent;
550 border-bottom: 2px solid transparent;
540 }
551 }
541
552
542 .toolbar-text {
553 .toolbar-text {
543 float: left;
554 float: left;
544 margin: -5px 0px 0px 0px;
555 margin: -5px 0px 0px 0px;
545 font-size: 12px;
556 font-size: 12px;
546 }
557 }
547
558
@@ -1,802 +1,808 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 var linkifyComments = function(comments) {
28 var linkifyComments = function(comments) {
29 var firstCommentId = null;
29 var firstCommentId = null;
30 if (comments) {
30 if (comments) {
31 firstCommentId = $(comments[0]).data('comment-id');
31 firstCommentId = $(comments[0]).data('comment-id');
32 }
32 }
33
33
34 if (firstCommentId){
34 if (firstCommentId){
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 }
36 }
37 };
37 };
38
38
39 var bindToggleButtons = function() {
39 var bindToggleButtons = function() {
40 $('.comment-toggle').on('click', function() {
40 $('.comment-toggle').on('click', function() {
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 });
42 });
43 };
43 };
44
44
45 /* Comment form for main and inline comments */
45 /* Comment form for main and inline comments */
46 (function(mod) {
46 (function(mod) {
47
47
48 if (typeof exports == "object" && typeof module == "object") {
48 if (typeof exports == "object" && typeof module == "object") {
49 // CommonJS
49 // CommonJS
50 module.exports = mod();
50 module.exports = mod();
51 }
51 }
52 else {
52 else {
53 // Plain browser env
53 // Plain browser env
54 (this || window).CommentForm = mod();
54 (this || window).CommentForm = mod();
55 }
55 }
56
56
57 })(function() {
57 })(function() {
58 "use strict";
58 "use strict";
59
59
60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
61 if (!(this instanceof CommentForm)) {
61 if (!(this instanceof CommentForm)) {
62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
63 }
63 }
64
64
65 // bind the element instance to our Form
65 // bind the element instance to our Form
66 $(formElement).get(0).CommentForm = this;
66 $(formElement).get(0).CommentForm = this;
67
67
68 this.withLineNo = function(selector) {
68 this.withLineNo = function(selector) {
69 var lineNo = this.lineNo;
69 var lineNo = this.lineNo;
70 if (lineNo === undefined) {
70 if (lineNo === undefined) {
71 return selector
71 return selector
72 } else {
72 } else {
73 return selector + '_' + lineNo;
73 return selector + '_' + lineNo;
74 }
74 }
75 };
75 };
76
76
77 this.commitId = commitId;
77 this.commitId = commitId;
78 this.pullRequestId = pullRequestId;
78 this.pullRequestId = pullRequestId;
79 this.lineNo = lineNo;
79 this.lineNo = lineNo;
80 this.initAutocompleteActions = initAutocompleteActions;
80 this.initAutocompleteActions = initAutocompleteActions;
81
81
82 this.previewButton = this.withLineNo('#preview-btn');
82 this.previewButton = this.withLineNo('#preview-btn');
83 this.previewContainer = this.withLineNo('#preview-container');
83 this.previewContainer = this.withLineNo('#preview-container');
84
84
85 this.previewBoxSelector = this.withLineNo('#preview-box');
85 this.previewBoxSelector = this.withLineNo('#preview-box');
86
86
87 this.editButton = this.withLineNo('#edit-btn');
87 this.editButton = this.withLineNo('#edit-btn');
88 this.editContainer = this.withLineNo('#edit-container');
88 this.editContainer = this.withLineNo('#edit-container');
89 this.cancelButton = this.withLineNo('#cancel-btn');
89 this.cancelButton = this.withLineNo('#cancel-btn');
90 this.commentType = this.withLineNo('#comment_type');
90 this.commentType = this.withLineNo('#comment_type');
91
91
92 this.resolvesId = null;
92 this.resolvesId = null;
93 this.resolvesActionId = null;
93 this.resolvesActionId = null;
94
94
95 this.cmBox = this.withLineNo('#text');
95 this.cmBox = this.withLineNo('#text');
96 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
96 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
97
97
98 this.statusChange = this.withLineNo('#change_status');
98 this.statusChange = this.withLineNo('#change_status');
99
99
100 this.submitForm = formElement;
100 this.submitForm = formElement;
101 this.submitButton = $(this.submitForm).find('input[type="submit"]');
101 this.submitButton = $(this.submitForm).find('input[type="submit"]');
102 this.submitButtonText = this.submitButton.val();
102 this.submitButtonText = this.submitButton.val();
103
103
104 this.previewUrl = pyroutes.url('changeset_comment_preview',
104 this.previewUrl = pyroutes.url('changeset_comment_preview',
105 {'repo_name': templateContext.repo_name});
105 {'repo_name': templateContext.repo_name});
106
106
107 if (resolvesCommentId){
107 if (resolvesCommentId){
108 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
108 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
109 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
109 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
110 $(this.commentType).prop('disabled', true);
110 $(this.commentType).prop('disabled', true);
111 $(this.commentType).addClass('disabled');
111 $(this.commentType).addClass('disabled');
112
112
113 // disable select
113 // disable select
114 setTimeout(function() {
114 setTimeout(function() {
115 $(self.statusChange).select2('readonly', true);
115 $(self.statusChange).select2('readonly', true);
116 }, 10);
116 }, 10);
117
117
118 var resolvedInfo = (
118 var resolvedInfo = (
119 '<li class="">' +
119 '<li class="resolve-action">' +
120 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
120 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
121 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
121 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
122 '</li>'
122 '</li>'
123 ).format(resolvesCommentId, _gettext('resolve comment'));
123 ).format(resolvesCommentId, _gettext('resolve comment'));
124 $(resolvedInfo).insertAfter($(this.commentType).parent());
124 $(resolvedInfo).insertAfter($(this.commentType).parent());
125 }
125 }
126
126
127 // based on commitId, or pullRequestId decide where do we submit
127 // based on commitId, or pullRequestId decide where do we submit
128 // out data
128 // out data
129 if (this.commitId){
129 if (this.commitId){
130 this.submitUrl = pyroutes.url('changeset_comment',
130 this.submitUrl = pyroutes.url('changeset_comment',
131 {'repo_name': templateContext.repo_name,
131 {'repo_name': templateContext.repo_name,
132 'revision': this.commitId});
132 'revision': this.commitId});
133 this.selfUrl = pyroutes.url('changeset_home',
133 this.selfUrl = pyroutes.url('changeset_home',
134 {'repo_name': templateContext.repo_name,
134 {'repo_name': templateContext.repo_name,
135 'revision': this.commitId});
135 'revision': this.commitId});
136
136
137 } else if (this.pullRequestId) {
137 } else if (this.pullRequestId) {
138 this.submitUrl = pyroutes.url('pullrequest_comment',
138 this.submitUrl = pyroutes.url('pullrequest_comment',
139 {'repo_name': templateContext.repo_name,
139 {'repo_name': templateContext.repo_name,
140 'pull_request_id': this.pullRequestId});
140 'pull_request_id': this.pullRequestId});
141 this.selfUrl = pyroutes.url('pullrequest_show',
141 this.selfUrl = pyroutes.url('pullrequest_show',
142 {'repo_name': templateContext.repo_name,
142 {'repo_name': templateContext.repo_name,
143 'pull_request_id': this.pullRequestId});
143 'pull_request_id': this.pullRequestId});
144
144
145 } else {
145 } else {
146 throw new Error(
146 throw new Error(
147 'CommentForm requires pullRequestId, or commitId to be specified.')
147 'CommentForm requires pullRequestId, or commitId to be specified.')
148 }
148 }
149
149
150 // FUNCTIONS and helpers
150 // FUNCTIONS and helpers
151 var self = this;
151 var self = this;
152
152
153 this.isInline = function(){
153 this.isInline = function(){
154 return this.lineNo && this.lineNo != 'general';
154 return this.lineNo && this.lineNo != 'general';
155 };
155 };
156
156
157 this.getCmInstance = function(){
157 this.getCmInstance = function(){
158 return this.cm
158 return this.cm
159 };
159 };
160
160
161 this.setPlaceholder = function(placeholder) {
161 this.setPlaceholder = function(placeholder) {
162 var cm = this.getCmInstance();
162 var cm = this.getCmInstance();
163 if (cm){
163 if (cm){
164 cm.setOption('placeholder', placeholder);
164 cm.setOption('placeholder', placeholder);
165 }
165 }
166 };
166 };
167
167
168 this.getCommentStatus = function() {
168 this.getCommentStatus = function() {
169 return $(this.submitForm).find(this.statusChange).val();
169 return $(this.submitForm).find(this.statusChange).val();
170 };
170 };
171 this.getCommentType = function() {
171 this.getCommentType = function() {
172 return $(this.submitForm).find(this.commentType).val();
172 return $(this.submitForm).find(this.commentType).val();
173 };
173 };
174
174
175 this.getResolvesId = function() {
175 this.getResolvesId = function() {
176 return $(this.submitForm).find(this.resolvesId).val() || null;
176 return $(this.submitForm).find(this.resolvesId).val() || null;
177 };
177 };
178 this.markCommentResolved = function(resolvedCommentId){
178 this.markCommentResolved = function(resolvedCommentId){
179 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
179 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
180 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
180 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
181 };
181 };
182
182
183 this.isAllowedToSubmit = function() {
183 this.isAllowedToSubmit = function() {
184 return !$(this.submitButton).prop('disabled');
184 return !$(this.submitButton).prop('disabled');
185 };
185 };
186
186
187 this.initStatusChangeSelector = function(){
187 this.initStatusChangeSelector = function(){
188 var formatChangeStatus = function(state, escapeMarkup) {
188 var formatChangeStatus = function(state, escapeMarkup) {
189 var originalOption = state.element;
189 var originalOption = state.element;
190 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
190 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
191 '<span>' + escapeMarkup(state.text) + '</span>';
191 '<span>' + escapeMarkup(state.text) + '</span>';
192 };
192 };
193 var formatResult = function(result, container, query, escapeMarkup) {
193 var formatResult = function(result, container, query, escapeMarkup) {
194 return formatChangeStatus(result, escapeMarkup);
194 return formatChangeStatus(result, escapeMarkup);
195 };
195 };
196
196
197 var formatSelection = function(data, container, escapeMarkup) {
197 var formatSelection = function(data, container, escapeMarkup) {
198 return formatChangeStatus(data, escapeMarkup);
198 return formatChangeStatus(data, escapeMarkup);
199 };
199 };
200
200
201 $(this.submitForm).find(this.statusChange).select2({
201 $(this.submitForm).find(this.statusChange).select2({
202 placeholder: _gettext('Status Review'),
202 placeholder: _gettext('Status Review'),
203 formatResult: formatResult,
203 formatResult: formatResult,
204 formatSelection: formatSelection,
204 formatSelection: formatSelection,
205 containerCssClass: "drop-menu status_box_menu",
205 containerCssClass: "drop-menu status_box_menu",
206 dropdownCssClass: "drop-menu-dropdown",
206 dropdownCssClass: "drop-menu-dropdown",
207 dropdownAutoWidth: true,
207 dropdownAutoWidth: true,
208 minimumResultsForSearch: -1
208 minimumResultsForSearch: -1
209 });
209 });
210 $(this.submitForm).find(this.statusChange).on('change', function() {
210 $(this.submitForm).find(this.statusChange).on('change', function() {
211 var status = self.getCommentStatus();
211 var status = self.getCommentStatus();
212 if (status && !self.isInline()) {
212 if (status && !self.isInline()) {
213 $(self.submitButton).prop('disabled', false);
213 $(self.submitButton).prop('disabled', false);
214 }
214 }
215
215
216 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
216 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
217 self.setPlaceholder(placeholderText)
217 self.setPlaceholder(placeholderText)
218 })
218 })
219 };
219 };
220
220
221 // reset the comment form into it's original state
221 // reset the comment form into it's original state
222 this.resetCommentFormState = function(content) {
222 this.resetCommentFormState = function(content) {
223 content = content || '';
223 content = content || '';
224
224
225 $(this.editContainer).show();
225 $(this.editContainer).show();
226 $(this.editButton).parent().addClass('active');
226 $(this.editButton).parent().addClass('active');
227
227
228 $(this.previewContainer).hide();
228 $(this.previewContainer).hide();
229 $(this.previewButton).parent().removeClass('active');
229 $(this.previewButton).parent().removeClass('active');
230
230
231 this.setActionButtonsDisabled(true);
231 this.setActionButtonsDisabled(true);
232 self.cm.setValue(content);
232 self.cm.setValue(content);
233 self.cm.setOption("readOnly", false);
233 self.cm.setOption("readOnly", false);
234
234
235 if (this.resolvesId) {
235 if (this.resolvesId) {
236 // destroy the resolve action
236 // destroy the resolve action
237 $(this.resolvesId).parent().remove();
237 $(this.resolvesId).parent().remove();
238 }
238 }
239
239
240 $(this.statusChange).select2('readonly', false);
240 $(this.statusChange).select2('readonly', false);
241 };
241 };
242
242
243 this.globalSubmitSuccessCallback = function(){
243 this.globalSubmitSuccessCallback = function(){
244 // default behaviour is to call GLOBAL hook, if it's registered.
244 // default behaviour is to call GLOBAL hook, if it's registered.
245 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
245 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
246 commentFormGlobalSubmitSuccessCallback()
246 commentFormGlobalSubmitSuccessCallback()
247 }
247 }
248 };
248 };
249
249
250 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
250 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
251 failHandler = failHandler || function() {};
251 failHandler = failHandler || function() {};
252 var postData = toQueryString(postData);
252 var postData = toQueryString(postData);
253 var request = $.ajax({
253 var request = $.ajax({
254 url: url,
254 url: url,
255 type: 'POST',
255 type: 'POST',
256 data: postData,
256 data: postData,
257 headers: {'X-PARTIAL-XHR': true}
257 headers: {'X-PARTIAL-XHR': true}
258 })
258 })
259 .done(function(data) {
259 .done(function(data) {
260 successHandler(data);
260 successHandler(data);
261 })
261 })
262 .fail(function(data, textStatus, errorThrown){
262 .fail(function(data, textStatus, errorThrown){
263 alert(
263 alert(
264 "Error while submitting comment.\n" +
264 "Error while submitting comment.\n" +
265 "Error code {0} ({1}).".format(data.status, data.statusText));
265 "Error code {0} ({1}).".format(data.status, data.statusText));
266 failHandler()
266 failHandler()
267 });
267 });
268 return request;
268 return request;
269 };
269 };
270
270
271 // overwrite a submitHandler, we need to do it for inline comments
271 // overwrite a submitHandler, we need to do it for inline comments
272 this.setHandleFormSubmit = function(callback) {
272 this.setHandleFormSubmit = function(callback) {
273 this.handleFormSubmit = callback;
273 this.handleFormSubmit = callback;
274 };
274 };
275
275
276 // overwrite a submitSuccessHandler
276 // overwrite a submitSuccessHandler
277 this.setGlobalSubmitSuccessCallback = function(callback) {
277 this.setGlobalSubmitSuccessCallback = function(callback) {
278 this.globalSubmitSuccessCallback = callback;
278 this.globalSubmitSuccessCallback = callback;
279 };
279 };
280
280
281 // default handler for for submit for main comments
281 // default handler for for submit for main comments
282 this.handleFormSubmit = function() {
282 this.handleFormSubmit = function() {
283 var text = self.cm.getValue();
283 var text = self.cm.getValue();
284 var status = self.getCommentStatus();
284 var status = self.getCommentStatus();
285 var commentType = self.getCommentType();
285 var commentType = self.getCommentType();
286 var resolvesCommentId = self.getResolvesId();
286 var resolvesCommentId = self.getResolvesId();
287
287
288 if (text === "" && !status) {
288 if (text === "" && !status) {
289 return;
289 return;
290 }
290 }
291
291
292 var excludeCancelBtn = false;
292 var excludeCancelBtn = false;
293 var submitEvent = true;
293 var submitEvent = true;
294 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
294 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
295 self.cm.setOption("readOnly", true);
295 self.cm.setOption("readOnly", true);
296
296
297 var postData = {
297 var postData = {
298 'text': text,
298 'text': text,
299 'changeset_status': status,
299 'changeset_status': status,
300 'comment_type': commentType,
300 'comment_type': commentType,
301 'csrf_token': CSRF_TOKEN
301 'csrf_token': CSRF_TOKEN
302 };
302 };
303 if (resolvesCommentId){
303 if (resolvesCommentId){
304 postData['resolves_comment_id'] = resolvesCommentId;
304 postData['resolves_comment_id'] = resolvesCommentId;
305 }
305 }
306
306
307 var submitSuccessCallback = function(o) {
307 var submitSuccessCallback = function(o) {
308 // reload page if we change status for single commit.
308 // reload page if we change status for single commit.
309 if (status && self.commitId) {
309 if (status && self.commitId) {
310 location.reload(true);
310 location.reload(true);
311 } else {
311 } else {
312 $('#injected_page_comments').append(o.rendered_text);
312 $('#injected_page_comments').append(o.rendered_text);
313 self.resetCommentFormState();
313 self.resetCommentFormState();
314 timeagoActivate();
314 timeagoActivate();
315
315
316 // mark visually which comment was resolved
316 // mark visually which comment was resolved
317 if (resolvesCommentId) {
317 if (resolvesCommentId) {
318 self.markCommentResolved(resolvesCommentId);
318 self.markCommentResolved(resolvesCommentId);
319 }
319 }
320 }
320 }
321
321
322 // run global callback on submit
322 // run global callback on submit
323 self.globalSubmitSuccessCallback();
323 self.globalSubmitSuccessCallback();
324
324
325 };
325 };
326 var submitFailCallback = function(){
326 var submitFailCallback = function(){
327 self.resetCommentFormState(text);
327 self.resetCommentFormState(text);
328 };
328 };
329 self.submitAjaxPOST(
329 self.submitAjaxPOST(
330 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
330 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
331 };
331 };
332
332
333 this.previewSuccessCallback = function(o) {
333 this.previewSuccessCallback = function(o) {
334 $(self.previewBoxSelector).html(o);
334 $(self.previewBoxSelector).html(o);
335 $(self.previewBoxSelector).removeClass('unloaded');
335 $(self.previewBoxSelector).removeClass('unloaded');
336
336
337 // swap buttons, making preview active
337 // swap buttons, making preview active
338 $(self.previewButton).parent().addClass('active');
338 $(self.previewButton).parent().addClass('active');
339 $(self.editButton).parent().removeClass('active');
339 $(self.editButton).parent().removeClass('active');
340
340
341 // unlock buttons
341 // unlock buttons
342 self.setActionButtonsDisabled(false);
342 self.setActionButtonsDisabled(false);
343 };
343 };
344
344
345 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
345 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
346 excludeCancelBtn = excludeCancelBtn || false;
346 excludeCancelBtn = excludeCancelBtn || false;
347 submitEvent = submitEvent || false;
347 submitEvent = submitEvent || false;
348
348
349 $(this.editButton).prop('disabled', state);
349 $(this.editButton).prop('disabled', state);
350 $(this.previewButton).prop('disabled', state);
350 $(this.previewButton).prop('disabled', state);
351
351
352 if (!excludeCancelBtn) {
352 if (!excludeCancelBtn) {
353 $(this.cancelButton).prop('disabled', state);
353 $(this.cancelButton).prop('disabled', state);
354 }
354 }
355
355
356 var submitState = state;
356 var submitState = state;
357 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
357 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
358 // if the value of commit review status is set, we allow
358 // if the value of commit review status is set, we allow
359 // submit button, but only on Main form, lineNo means inline
359 // submit button, but only on Main form, lineNo means inline
360 submitState = false
360 submitState = false
361 }
361 }
362 $(this.submitButton).prop('disabled', submitState);
362 $(this.submitButton).prop('disabled', submitState);
363 if (submitEvent) {
363 if (submitEvent) {
364 $(this.submitButton).val(_gettext('Submitting...'));
364 $(this.submitButton).val(_gettext('Submitting...'));
365 } else {
365 } else {
366 $(this.submitButton).val(this.submitButtonText);
366 $(this.submitButton).val(this.submitButtonText);
367 }
367 }
368
368
369 };
369 };
370
370
371 // lock preview/edit/submit buttons on load, but exclude cancel button
371 // lock preview/edit/submit buttons on load, but exclude cancel button
372 var excludeCancelBtn = true;
372 var excludeCancelBtn = true;
373 this.setActionButtonsDisabled(true, excludeCancelBtn);
373 this.setActionButtonsDisabled(true, excludeCancelBtn);
374
374
375 // anonymous users don't have access to initialized CM instance
375 // anonymous users don't have access to initialized CM instance
376 if (this.cm !== undefined){
376 if (this.cm !== undefined){
377 this.cm.on('change', function(cMirror) {
377 this.cm.on('change', function(cMirror) {
378 if (cMirror.getValue() === "") {
378 if (cMirror.getValue() === "") {
379 self.setActionButtonsDisabled(true, excludeCancelBtn)
379 self.setActionButtonsDisabled(true, excludeCancelBtn)
380 } else {
380 } else {
381 self.setActionButtonsDisabled(false, excludeCancelBtn)
381 self.setActionButtonsDisabled(false, excludeCancelBtn)
382 }
382 }
383 });
383 });
384 }
384 }
385
385
386 $(this.editButton).on('click', function(e) {
386 $(this.editButton).on('click', function(e) {
387 e.preventDefault();
387 e.preventDefault();
388
388
389 $(self.previewButton).parent().removeClass('active');
389 $(self.previewButton).parent().removeClass('active');
390 $(self.previewContainer).hide();
390 $(self.previewContainer).hide();
391
391
392 $(self.editButton).parent().addClass('active');
392 $(self.editButton).parent().addClass('active');
393 $(self.editContainer).show();
393 $(self.editContainer).show();
394
394
395 });
395 });
396
396
397 $(this.previewButton).on('click', function(e) {
397 $(this.previewButton).on('click', function(e) {
398 e.preventDefault();
398 e.preventDefault();
399 var text = self.cm.getValue();
399 var text = self.cm.getValue();
400
400
401 if (text === "") {
401 if (text === "") {
402 return;
402 return;
403 }
403 }
404
404
405 var postData = {
405 var postData = {
406 'text': text,
406 'text': text,
407 'renderer': templateContext.visual.default_renderer,
407 'renderer': templateContext.visual.default_renderer,
408 'csrf_token': CSRF_TOKEN
408 'csrf_token': CSRF_TOKEN
409 };
409 };
410
410
411 // lock ALL buttons on preview
411 // lock ALL buttons on preview
412 self.setActionButtonsDisabled(true);
412 self.setActionButtonsDisabled(true);
413
413
414 $(self.previewBoxSelector).addClass('unloaded');
414 $(self.previewBoxSelector).addClass('unloaded');
415 $(self.previewBoxSelector).html(_gettext('Loading ...'));
415 $(self.previewBoxSelector).html(_gettext('Loading ...'));
416
416
417 $(self.editContainer).hide();
417 $(self.editContainer).hide();
418 $(self.previewContainer).show();
418 $(self.previewContainer).show();
419
419
420 // by default we reset state of comment preserving the text
420 // by default we reset state of comment preserving the text
421 var previewFailCallback = function(){
421 var previewFailCallback = function(){
422 self.resetCommentFormState(text)
422 self.resetCommentFormState(text)
423 };
423 };
424 self.submitAjaxPOST(
424 self.submitAjaxPOST(
425 self.previewUrl, postData, self.previewSuccessCallback,
425 self.previewUrl, postData, self.previewSuccessCallback,
426 previewFailCallback);
426 previewFailCallback);
427
427
428 $(self.previewButton).parent().addClass('active');
428 $(self.previewButton).parent().addClass('active');
429 $(self.editButton).parent().removeClass('active');
429 $(self.editButton).parent().removeClass('active');
430 });
430 });
431
431
432 $(this.submitForm).submit(function(e) {
432 $(this.submitForm).submit(function(e) {
433 e.preventDefault();
433 e.preventDefault();
434 var allowedToSubmit = self.isAllowedToSubmit();
434 var allowedToSubmit = self.isAllowedToSubmit();
435 if (!allowedToSubmit){
435 if (!allowedToSubmit){
436 return false;
436 return false;
437 }
437 }
438 self.handleFormSubmit();
438 self.handleFormSubmit();
439 });
439 });
440
440
441 }
441 }
442
442
443 return CommentForm;
443 return CommentForm;
444 });
444 });
445
445
446 /* comments controller */
446 /* comments controller */
447 var CommentsController = function() {
447 var CommentsController = function() {
448 var mainComment = '#text';
448 var mainComment = '#text';
449 var self = this;
449 var self = this;
450
450
451 this.cancelComment = function(node) {
451 this.cancelComment = function(node) {
452 var $node = $(node);
452 var $node = $(node);
453 var $td = $node.closest('td');
453 var $td = $node.closest('td');
454 $node.closest('.comment-inline-form').remove();
454 $node.closest('.comment-inline-form').remove();
455 return false;
455 return false;
456 };
456 };
457
457
458 this.getLineNumber = function(node) {
458 this.getLineNumber = function(node) {
459 var $node = $(node);
459 var $node = $(node);
460 return $node.closest('td').attr('data-line-number');
460 return $node.closest('td').attr('data-line-number');
461 };
461 };
462
462
463 this.scrollToComment = function(node, offset, outdated) {
463 this.scrollToComment = function(node, offset, outdated) {
464 var outdated = outdated || false;
464 var outdated = outdated || false;
465 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
465 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
466
466
467 if (!node) {
467 if (!node) {
468 node = $('.comment-selected');
468 node = $('.comment-selected');
469 if (!node.length) {
469 if (!node.length) {
470 node = $('comment-current')
470 node = $('comment-current')
471 }
471 }
472 }
472 }
473 $wrapper = $(node).closest('div.comment');
473 $comment = $(node).closest(klass);
474 $comment = $(node).closest(klass);
474 $comments = $(klass);
475 $comments = $(klass);
475
476
477 // show hidden comment when referenced.
478 if (!$wrapper.is(':visible')){
479 $wrapper.show();
480 }
481
476 $('.comment-selected').removeClass('comment-selected');
482 $('.comment-selected').removeClass('comment-selected');
477
483
478 var nextIdx = $(klass).index($comment) + offset;
484 var nextIdx = $(klass).index($comment) + offset;
479 if (nextIdx >= $comments.length) {
485 if (nextIdx >= $comments.length) {
480 nextIdx = 0;
486 nextIdx = 0;
481 }
487 }
482 var $next = $(klass).eq(nextIdx);
488 var $next = $(klass).eq(nextIdx);
483 var $cb = $next.closest('.cb');
489 var $cb = $next.closest('.cb');
484 $cb.removeClass('cb-collapsed');
490 $cb.removeClass('cb-collapsed');
485
491
486 var $filediffCollapseState = $cb.closest('.filediff').prev();
492 var $filediffCollapseState = $cb.closest('.filediff').prev();
487 $filediffCollapseState.prop('checked', false);
493 $filediffCollapseState.prop('checked', false);
488 $next.addClass('comment-selected');
494 $next.addClass('comment-selected');
489 scrollToElement($next);
495 scrollToElement($next);
490 return false;
496 return false;
491 };
497 };
492
498
493 this.nextComment = function(node) {
499 this.nextComment = function(node) {
494 return self.scrollToComment(node, 1);
500 return self.scrollToComment(node, 1);
495 };
501 };
496
502
497 this.prevComment = function(node) {
503 this.prevComment = function(node) {
498 return self.scrollToComment(node, -1);
504 return self.scrollToComment(node, -1);
499 };
505 };
500
506
501 this.nextOutdatedComment = function(node) {
507 this.nextOutdatedComment = function(node) {
502 return self.scrollToComment(node, 1, true);
508 return self.scrollToComment(node, 1, true);
503 };
509 };
504
510
505 this.prevOutdatedComment = function(node) {
511 this.prevOutdatedComment = function(node) {
506 return self.scrollToComment(node, -1, true);
512 return self.scrollToComment(node, -1, true);
507 };
513 };
508
514
509 this.deleteComment = function(node) {
515 this.deleteComment = function(node) {
510 if (!confirm(_gettext('Delete this comment?'))) {
516 if (!confirm(_gettext('Delete this comment?'))) {
511 return false;
517 return false;
512 }
518 }
513 var $node = $(node);
519 var $node = $(node);
514 var $td = $node.closest('td');
520 var $td = $node.closest('td');
515 var $comment = $node.closest('.comment');
521 var $comment = $node.closest('.comment');
516 var comment_id = $comment.attr('data-comment-id');
522 var comment_id = $comment.attr('data-comment-id');
517 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
523 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
518 var postData = {
524 var postData = {
519 '_method': 'delete',
525 '_method': 'delete',
520 'csrf_token': CSRF_TOKEN
526 'csrf_token': CSRF_TOKEN
521 };
527 };
522
528
523 $comment.addClass('comment-deleting');
529 $comment.addClass('comment-deleting');
524 $comment.hide('fast');
530 $comment.hide('fast');
525
531
526 var success = function(response) {
532 var success = function(response) {
527 $comment.remove();
533 $comment.remove();
528 return false;
534 return false;
529 };
535 };
530 var failure = function(data, textStatus, xhr) {
536 var failure = function(data, textStatus, xhr) {
531 alert("error processing request: " + textStatus);
537 alert("error processing request: " + textStatus);
532 $comment.show('fast');
538 $comment.show('fast');
533 $comment.removeClass('comment-deleting');
539 $comment.removeClass('comment-deleting');
534 return false;
540 return false;
535 };
541 };
536 ajaxPOST(url, postData, success, failure);
542 ajaxPOST(url, postData, success, failure);
537 };
543 };
538
544
539 this.toggleWideMode = function (node) {
545 this.toggleWideMode = function (node) {
540 if ($('#content').hasClass('wrapper')) {
546 if ($('#content').hasClass('wrapper')) {
541 $('#content').removeClass("wrapper");
547 $('#content').removeClass("wrapper");
542 $('#content').addClass("wide-mode-wrapper");
548 $('#content').addClass("wide-mode-wrapper");
543 $(node).addClass('btn-success');
549 $(node).addClass('btn-success');
544 } else {
550 } else {
545 $('#content').removeClass("wide-mode-wrapper");
551 $('#content').removeClass("wide-mode-wrapper");
546 $('#content').addClass("wrapper");
552 $('#content').addClass("wrapper");
547 $(node).removeClass('btn-success');
553 $(node).removeClass('btn-success');
548 }
554 }
549 return false;
555 return false;
550 };
556 };
551
557
552 this.toggleComments = function(node, show) {
558 this.toggleComments = function(node, show) {
553 var $filediff = $(node).closest('.filediff');
559 var $filediff = $(node).closest('.filediff');
554 if (show === true) {
560 if (show === true) {
555 $filediff.removeClass('hide-comments');
561 $filediff.removeClass('hide-comments');
556 } else if (show === false) {
562 } else if (show === false) {
557 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
563 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
558 $filediff.addClass('hide-comments');
564 $filediff.addClass('hide-comments');
559 } else {
565 } else {
560 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
566 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
561 $filediff.toggleClass('hide-comments');
567 $filediff.toggleClass('hide-comments');
562 }
568 }
563 return false;
569 return false;
564 };
570 };
565
571
566 this.toggleLineComments = function(node) {
572 this.toggleLineComments = function(node) {
567 self.toggleComments(node, true);
573 self.toggleComments(node, true);
568 var $node = $(node);
574 var $node = $(node);
569 $node.closest('tr').toggleClass('hide-line-comments');
575 $node.closest('tr').toggleClass('hide-line-comments');
570 };
576 };
571
577
572 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
578 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
573 var pullRequestId = templateContext.pull_request_data.pull_request_id;
579 var pullRequestId = templateContext.pull_request_data.pull_request_id;
574 var commitId = templateContext.commit_data.commit_id;
580 var commitId = templateContext.commit_data.commit_id;
575
581
576 var commentForm = new CommentForm(
582 var commentForm = new CommentForm(
577 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
583 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
578 var cm = commentForm.getCmInstance();
584 var cm = commentForm.getCmInstance();
579
585
580 if (resolvesCommentId){
586 if (resolvesCommentId){
581 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
587 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
582 }
588 }
583
589
584 setTimeout(function() {
590 setTimeout(function() {
585 // callbacks
591 // callbacks
586 if (cm !== undefined) {
592 if (cm !== undefined) {
587 commentForm.setPlaceholder(placeholderText);
593 commentForm.setPlaceholder(placeholderText);
588 if (commentForm.isInline()) {
594 if (commentForm.isInline()) {
589 cm.focus();
595 cm.focus();
590 cm.refresh();
596 cm.refresh();
591 }
597 }
592 }
598 }
593 }, 10);
599 }, 10);
594
600
595 // trigger scrolldown to the resolve comment, since it might be away
601 // trigger scrolldown to the resolve comment, since it might be away
596 // from the clicked
602 // from the clicked
597 if (resolvesCommentId){
603 if (resolvesCommentId){
598 var actionNode = $(commentForm.resolvesActionId).offset();
604 var actionNode = $(commentForm.resolvesActionId).offset();
599
605
600 setTimeout(function() {
606 setTimeout(function() {
601 if (actionNode) {
607 if (actionNode) {
602 $('body, html').animate({scrollTop: actionNode.top}, 10);
608 $('body, html').animate({scrollTop: actionNode.top}, 10);
603 }
609 }
604 }, 100);
610 }, 100);
605 }
611 }
606
612
607 return commentForm;
613 return commentForm;
608 };
614 };
609
615
610 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
616 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
611
617
612 var tmpl = $('#cb-comment-general-form-template').html();
618 var tmpl = $('#cb-comment-general-form-template').html();
613 tmpl = tmpl.format(null, 'general');
619 tmpl = tmpl.format(null, 'general');
614 var $form = $(tmpl);
620 var $form = $(tmpl);
615
621
616 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
622 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
617 var curForm = $formPlaceholder.find('form');
623 var curForm = $formPlaceholder.find('form');
618 if (curForm){
624 if (curForm){
619 curForm.remove();
625 curForm.remove();
620 }
626 }
621 $formPlaceholder.append($form);
627 $formPlaceholder.append($form);
622
628
623 var _form = $($form[0]);
629 var _form = $($form[0]);
624 var commentForm = this.createCommentForm(
630 var commentForm = this.createCommentForm(
625 _form, lineNo, placeholderText, true, resolvesCommentId);
631 _form, lineNo, placeholderText, true, resolvesCommentId);
626 commentForm.initStatusChangeSelector();
632 commentForm.initStatusChangeSelector();
627
633
628 return commentForm;
634 return commentForm;
629 };
635 };
630
636
631 this.createComment = function(node, resolutionComment) {
637 this.createComment = function(node, resolutionComment) {
632 var resolvesCommentId = resolutionComment || null;
638 var resolvesCommentId = resolutionComment || null;
633 var $node = $(node);
639 var $node = $(node);
634 var $td = $node.closest('td');
640 var $td = $node.closest('td');
635 var $form = $td.find('.comment-inline-form');
641 var $form = $td.find('.comment-inline-form');
636
642
637 if (!$form.length) {
643 if (!$form.length) {
638
644
639 var $filediff = $node.closest('.filediff');
645 var $filediff = $node.closest('.filediff');
640 $filediff.removeClass('hide-comments');
646 $filediff.removeClass('hide-comments');
641 var f_path = $filediff.attr('data-f-path');
647 var f_path = $filediff.attr('data-f-path');
642 var lineno = self.getLineNumber(node);
648 var lineno = self.getLineNumber(node);
643 // create a new HTML from template
649 // create a new HTML from template
644 var tmpl = $('#cb-comment-inline-form-template').html();
650 var tmpl = $('#cb-comment-inline-form-template').html();
645 tmpl = tmpl.format(f_path, lineno);
651 tmpl = tmpl.format(f_path, lineno);
646 $form = $(tmpl);
652 $form = $(tmpl);
647
653
648 var $comments = $td.find('.inline-comments');
654 var $comments = $td.find('.inline-comments');
649 if (!$comments.length) {
655 if (!$comments.length) {
650 $comments = $(
656 $comments = $(
651 $('#cb-comments-inline-container-template').html());
657 $('#cb-comments-inline-container-template').html());
652 $td.append($comments);
658 $td.append($comments);
653 }
659 }
654
660
655 $td.find('.cb-comment-add-button').before($form);
661 $td.find('.cb-comment-add-button').before($form);
656
662
657 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
663 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
658 var _form = $($form[0]).find('form');
664 var _form = $($form[0]).find('form');
659
665
660 var commentForm = this.createCommentForm(
666 var commentForm = this.createCommentForm(
661 _form, lineno, placeholderText, false, resolvesCommentId);
667 _form, lineno, placeholderText, false, resolvesCommentId);
662
668
663 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
669 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
664 form: _form,
670 form: _form,
665 parent: $td[0],
671 parent: $td[0],
666 lineno: lineno,
672 lineno: lineno,
667 f_path: f_path}
673 f_path: f_path}
668 );
674 );
669
675
670 // set a CUSTOM submit handler for inline comments.
676 // set a CUSTOM submit handler for inline comments.
671 commentForm.setHandleFormSubmit(function(o) {
677 commentForm.setHandleFormSubmit(function(o) {
672 var text = commentForm.cm.getValue();
678 var text = commentForm.cm.getValue();
673 var commentType = commentForm.getCommentType();
679 var commentType = commentForm.getCommentType();
674 var resolvesCommentId = commentForm.getResolvesId();
680 var resolvesCommentId = commentForm.getResolvesId();
675
681
676 if (text === "") {
682 if (text === "") {
677 return;
683 return;
678 }
684 }
679
685
680 if (lineno === undefined) {
686 if (lineno === undefined) {
681 alert('missing line !');
687 alert('missing line !');
682 return;
688 return;
683 }
689 }
684 if (f_path === undefined) {
690 if (f_path === undefined) {
685 alert('missing file path !');
691 alert('missing file path !');
686 return;
692 return;
687 }
693 }
688
694
689 var excludeCancelBtn = false;
695 var excludeCancelBtn = false;
690 var submitEvent = true;
696 var submitEvent = true;
691 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
697 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
692 commentForm.cm.setOption("readOnly", true);
698 commentForm.cm.setOption("readOnly", true);
693 var postData = {
699 var postData = {
694 'text': text,
700 'text': text,
695 'f_path': f_path,
701 'f_path': f_path,
696 'line': lineno,
702 'line': lineno,
697 'comment_type': commentType,
703 'comment_type': commentType,
698 'csrf_token': CSRF_TOKEN
704 'csrf_token': CSRF_TOKEN
699 };
705 };
700 if (resolvesCommentId){
706 if (resolvesCommentId){
701 postData['resolves_comment_id'] = resolvesCommentId;
707 postData['resolves_comment_id'] = resolvesCommentId;
702 }
708 }
703
709
704 var submitSuccessCallback = function(json_data) {
710 var submitSuccessCallback = function(json_data) {
705 $form.remove();
711 $form.remove();
706 try {
712 try {
707 var html = json_data.rendered_text;
713 var html = json_data.rendered_text;
708 var lineno = json_data.line_no;
714 var lineno = json_data.line_no;
709 var target_id = json_data.target_id;
715 var target_id = json_data.target_id;
710
716
711 $comments.find('.cb-comment-add-button').before(html);
717 $comments.find('.cb-comment-add-button').before(html);
712
718
713 //mark visually which comment was resolved
719 //mark visually which comment was resolved
714 if (resolvesCommentId) {
720 if (resolvesCommentId) {
715 commentForm.markCommentResolved(resolvesCommentId);
721 commentForm.markCommentResolved(resolvesCommentId);
716 }
722 }
717
723
718 // run global callback on submit
724 // run global callback on submit
719 commentForm.globalSubmitSuccessCallback();
725 commentForm.globalSubmitSuccessCallback();
720
726
721 } catch (e) {
727 } catch (e) {
722 console.error(e);
728 console.error(e);
723 }
729 }
724
730
725 // re trigger the linkification of next/prev navigation
731 // re trigger the linkification of next/prev navigation
726 linkifyComments($('.inline-comment-injected'));
732 linkifyComments($('.inline-comment-injected'));
727 timeagoActivate();
733 timeagoActivate();
728 commentForm.setActionButtonsDisabled(false);
734 commentForm.setActionButtonsDisabled(false);
729
735
730 };
736 };
731 var submitFailCallback = function(){
737 var submitFailCallback = function(){
732 commentForm.resetCommentFormState(text)
738 commentForm.resetCommentFormState(text)
733 };
739 };
734 commentForm.submitAjaxPOST(
740 commentForm.submitAjaxPOST(
735 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
741 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
736 });
742 });
737 }
743 }
738
744
739 $form.addClass('comment-inline-form-open');
745 $form.addClass('comment-inline-form-open');
740 };
746 };
741
747
742 this.createResolutionComment = function(commentId){
748 this.createResolutionComment = function(commentId){
743 // hide the trigger text
749 // hide the trigger text
744 $('#resolve-comment-{0}'.format(commentId)).hide();
750 $('#resolve-comment-{0}'.format(commentId)).hide();
745
751
746 var comment = $('#comment-'+commentId);
752 var comment = $('#comment-'+commentId);
747 var commentData = comment.data();
753 var commentData = comment.data();
748 if (commentData.commentInline) {
754 if (commentData.commentInline) {
749 this.createComment(comment, commentId)
755 this.createComment(comment, commentId)
750 } else {
756 } else {
751 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
757 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
752 }
758 }
753
759
754 return false;
760 return false;
755 };
761 };
756
762
757 this.submitResolution = function(commentId){
763 this.submitResolution = function(commentId){
758 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
764 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
759 var commentForm = form.get(0).CommentForm;
765 var commentForm = form.get(0).CommentForm;
760
766
761 var cm = commentForm.getCmInstance();
767 var cm = commentForm.getCmInstance();
762 var renderer = templateContext.visual.default_renderer;
768 var renderer = templateContext.visual.default_renderer;
763 if (renderer == 'rst'){
769 if (renderer == 'rst'){
764 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
770 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
765 } else if (renderer == 'markdown') {
771 } else if (renderer == 'markdown') {
766 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
772 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
767 } else {
773 } else {
768 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
774 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
769 }
775 }
770
776
771 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
777 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
772 form.submit();
778 form.submit();
773 return false;
779 return false;
774 };
780 };
775
781
776 this.renderInlineComments = function(file_comments) {
782 this.renderInlineComments = function(file_comments) {
777 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
783 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
778
784
779 for (var i = 0; i < file_comments.length; i++) {
785 for (var i = 0; i < file_comments.length; i++) {
780 var box = file_comments[i];
786 var box = file_comments[i];
781
787
782 var target_id = $(box).attr('target_id');
788 var target_id = $(box).attr('target_id');
783
789
784 // actually comments with line numbers
790 // actually comments with line numbers
785 var comments = box.children;
791 var comments = box.children;
786
792
787 for (var j = 0; j < comments.length; j++) {
793 for (var j = 0; j < comments.length; j++) {
788 var data = {
794 var data = {
789 'rendered_text': comments[j].outerHTML,
795 'rendered_text': comments[j].outerHTML,
790 'line_no': $(comments[j]).attr('line'),
796 'line_no': $(comments[j]).attr('line'),
791 'target_id': target_id
797 'target_id': target_id
792 };
798 };
793 }
799 }
794 }
800 }
795
801
796 // since order of injection is random, we're now re-iterating
802 // since order of injection is random, we're now re-iterating
797 // from correct order and filling in links
803 // from correct order and filling in links
798 linkifyComments($('.inline-comment-injected'));
804 linkifyComments($('.inline-comment-injected'));
799 firefoxAnchorFix();
805 firefoxAnchorFix();
800 };
806 };
801
807
802 };
808 };
@@ -1,44 +1,50 b''
1
1
2 <div class="pull-request-wrap">
2 <div class="pull-request-wrap">
3
3
4
4
5 % if c.pr_merge_possible:
5 % if c.pr_merge_possible:
6 <h2 class="merge-status">
6 <h2 class="merge-status">
7 <span class="merge-icon success"><i class="icon-true"></i></span>
7 <span class="merge-icon success"><i class="icon-true"></i></span>
8 ${_('This pull request can be merged automatically.')}
8 ${_('This pull request can be merged automatically.')}
9 </h2>
9 </h2>
10 % else:
10 % else:
11 <h2 class="merge-status">
11 <h2 class="merge-status">
12 <span class="merge-icon warning"><i class="icon-false"></i></span>
12 <span class="merge-icon warning"><i class="icon-false"></i></span>
13 ${_('Merge is not currently possible because of below failed checks.')}
13 ${_('Merge is not currently possible because of below failed checks.')}
14 </h2>
14 </h2>
15 % endif
15 % endif
16
16
17 <ul>
17 <ul>
18 % for pr_check_type, pr_check_msg in c.pr_merge_errors:
18 % for pr_check_key, pr_check_details in c.pr_merge_errors.items():
19 <% pr_check_type = pr_check_details['error_type'] %>
19 <li>
20 <li>
20 <span class="merge-message ${pr_check_type}" data-role="merge-message">
21 <span class="merge-message ${pr_check_type}" data-role="merge-message">
21 - ${pr_check_msg}
22 - ${pr_check_details['message']}
23 % if pr_check_key == 'todo':
24 % for co in pr_check_details['details']:
25 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))"> #${co.comment_id}</a>${'' if loop.last else ','}
26 % endfor
27 % endif
22 </span>
28 </span>
23 </li>
29 </li>
24 % endfor
30 % endfor
25 </ul>
31 </ul>
26
32
27 <div class="pull-request-merge-actions">
33 <div class="pull-request-merge-actions">
28 % if c.allowed_to_merge:
34 % if c.allowed_to_merge:
29 <div class="pull-right">
35 <div class="pull-right">
30 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
36 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
31 <% merge_disabled = ' disabled' if c.pr_merge_possible is False else '' %>
37 <% merge_disabled = ' disabled' if c.pr_merge_possible is False else '' %>
32 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
38 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
33 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
39 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
34 ${h.end_form()}
40 ${h.end_form()}
35 </div>
41 </div>
36 % elif c.rhodecode_user.username != h.DEFAULT_USER:
42 % elif c.rhodecode_user.username != h.DEFAULT_USER:
37 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
43 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
38 <input type="submit" value="${_('Merge Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
44 <input type="submit" value="${_('Merge Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
39 % else:
45 % else:
40 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
46 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
41 % endif
47 % endif
42 </div>
48 </div>
43 </div>
49 </div>
44
50
General Comments 0
You need to be logged in to leave comments. Login now