##// END OF EJS Templates
Implemented #670 Implementation of Roles in Pull Request...
marcink -
r3104:c77d5c63 beta
parent child Browse files
Show More
@@ -1,477 +1,487 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.pullrequests
3 rhodecode.controllers.pullrequests
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 pull requests controller for rhodecode for initializing pull requests
6 pull requests controller for rhodecode for initializing pull requests
7
7
8 :created_on: May 7, 2012
8 :created_on: May 7, 2012
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import formencode
27 import formencode
28
28
29 from webob.exc import HTTPNotFound, HTTPForbidden
29 from webob.exc import HTTPNotFound, HTTPForbidden
30 from collections import defaultdict
30 from collections import defaultdict
31 from itertools import groupby
31 from itertools import groupby
32
32
33 from pylons import request, response, session, tmpl_context as c, url
33 from pylons import request, response, session, tmpl_context as c, url
34 from pylons.controllers.util import abort, redirect
34 from pylons.controllers.util import abort, redirect
35 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36
36
37 from rhodecode.lib.compat import json
37 from rhodecode.lib.compat import json
38 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.base import BaseRepoController, render
39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
40 NotAnonymous
40 NotAnonymous
41 from rhodecode.lib import helpers as h
41 from rhodecode.lib import helpers as h
42 from rhodecode.lib import diffs
42 from rhodecode.lib import diffs
43 from rhodecode.lib.utils import action_logger, jsonify
43 from rhodecode.lib.utils import action_logger, jsonify
44 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
44 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 from rhodecode.lib.diffs import LimitedDiffContainer
46 from rhodecode.lib.diffs import LimitedDiffContainer
47 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
47 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
48 ChangesetComment
48 ChangesetComment
49 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.pull_request import PullRequestModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52 from rhodecode.model.comment import ChangesetCommentsModel
52 from rhodecode.model.comment import ChangesetCommentsModel
53 from rhodecode.model.changeset_status import ChangesetStatusModel
53 from rhodecode.model.changeset_status import ChangesetStatusModel
54 from rhodecode.model.forms import PullRequestForm
54 from rhodecode.model.forms import PullRequestForm
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 class PullrequestsController(BaseRepoController):
59 class PullrequestsController(BaseRepoController):
60
60
61 @LoginRequired()
61 @LoginRequired()
62 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
62 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
63 'repository.admin')
63 'repository.admin')
64 def __before__(self):
64 def __before__(self):
65 super(PullrequestsController, self).__before__()
65 super(PullrequestsController, self).__before__()
66 repo_model = RepoModel()
66 repo_model = RepoModel()
67 c.users_array = repo_model.get_users_js()
67 c.users_array = repo_model.get_users_js()
68 c.users_groups_array = repo_model.get_users_groups_js()
68 c.users_groups_array = repo_model.get_users_groups_js()
69
69
70 def _get_repo_refs(self, repo):
70 def _get_repo_refs(self, repo):
71 hist_l = []
71 hist_l = []
72
72
73 branches_group = ([('branch:%s:%s' % (k, v), k) for
73 branches_group = ([('branch:%s:%s' % (k, v), k) for
74 k, v in repo.branches.iteritems()], _("Branches"))
74 k, v in repo.branches.iteritems()], _("Branches"))
75 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
75 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
76 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
76 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
77 tags_group = ([('tag:%s:%s' % (k, v), k) for
77 tags_group = ([('tag:%s:%s' % (k, v), k) for
78 k, v in repo.tags.iteritems()], _("Tags"))
78 k, v in repo.tags.iteritems()], _("Tags"))
79
79
80 hist_l.append(bookmarks_group)
80 hist_l.append(bookmarks_group)
81 hist_l.append(branches_group)
81 hist_l.append(branches_group)
82 hist_l.append(tags_group)
82 hist_l.append(tags_group)
83
83
84 return hist_l
84 return hist_l
85
85
86 def _get_default_rev(self, repo):
86 def _get_default_rev(self, repo):
87 """
87 """
88 Get's default revision to do compare on pull request
88 Get's default revision to do compare on pull request
89
89
90 :param repo:
90 :param repo:
91 """
91 """
92 repo = repo.scm_instance
92 repo = repo.scm_instance
93 if 'default' in repo.branches:
93 if 'default' in repo.branches:
94 return 'default'
94 return 'default'
95 else:
95 else:
96 #if repo doesn't have default branch return first found
96 #if repo doesn't have default branch return first found
97 return repo.branches.keys()[0]
97 return repo.branches.keys()[0]
98
98
99 def _get_is_allowed_change_status(self, pull_request):
100 owner = self.rhodecode_user.user_id == pull_request.user_id
101 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
102 pull_request.reviewers]
103 return (self.rhodecode_user.admin or owner or reviewer)
104
99 def show_all(self, repo_name):
105 def show_all(self, repo_name):
100 c.pull_requests = PullRequestModel().get_all(repo_name)
106 c.pull_requests = PullRequestModel().get_all(repo_name)
101 c.repo_name = repo_name
107 c.repo_name = repo_name
102 return render('/pullrequests/pullrequest_show_all.html')
108 return render('/pullrequests/pullrequest_show_all.html')
103
109
104 @NotAnonymous()
110 @NotAnonymous()
105 def index(self):
111 def index(self):
106 org_repo = c.rhodecode_db_repo
112 org_repo = c.rhodecode_db_repo
107
113
108 if org_repo.scm_instance.alias != 'hg':
114 if org_repo.scm_instance.alias != 'hg':
109 log.error('Review not available for GIT REPOS')
115 log.error('Review not available for GIT REPOS')
110 raise HTTPNotFound
116 raise HTTPNotFound
111
117
112 try:
118 try:
113 org_repo.scm_instance.get_changeset()
119 org_repo.scm_instance.get_changeset()
114 except EmptyRepositoryError, e:
120 except EmptyRepositoryError, e:
115 h.flash(h.literal(_('There are no changesets yet')),
121 h.flash(h.literal(_('There are no changesets yet')),
116 category='warning')
122 category='warning')
117 redirect(url('summary_home', repo_name=org_repo.repo_name))
123 redirect(url('summary_home', repo_name=org_repo.repo_name))
118
124
119 other_repos_info = {}
125 other_repos_info = {}
120
126
121 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
127 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
122 c.org_repos = []
128 c.org_repos = []
123 c.other_repos = []
129 c.other_repos = []
124 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
130 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
125 org_repo.user.username, c.repo_name))
131 org_repo.user.username, c.repo_name))
126 )
132 )
127
133
128 # add org repo to other so we can open pull request agains itself
134 # add org repo to other so we can open pull request agains itself
129 c.other_repos.extend(c.org_repos)
135 c.other_repos.extend(c.org_repos)
130
136
131 c.default_pull_request = org_repo.repo_name # repo name pre-selected
137 c.default_pull_request = org_repo.repo_name # repo name pre-selected
132 c.default_pull_request_rev = self._get_default_rev(org_repo) # revision pre-selected
138 c.default_pull_request_rev = self._get_default_rev(org_repo) # revision pre-selected
133 c.default_revs = self._get_repo_refs(org_repo.scm_instance)
139 c.default_revs = self._get_repo_refs(org_repo.scm_instance)
134 #add orginal repo
140 #add orginal repo
135 other_repos_info[org_repo.repo_name] = {
141 other_repos_info[org_repo.repo_name] = {
136 'gravatar': h.gravatar_url(org_repo.user.email, 24),
142 'gravatar': h.gravatar_url(org_repo.user.email, 24),
137 'description': org_repo.description,
143 'description': org_repo.description,
138 'revs': h.select('other_ref', '', c.default_revs, class_='refs')
144 'revs': h.select('other_ref', '', c.default_revs, class_='refs')
139 }
145 }
140
146
141 #gather forks and add to this list
147 #gather forks and add to this list
142 for fork in org_repo.forks:
148 for fork in org_repo.forks:
143 c.other_repos.append((fork.repo_name, '%s/%s' % (
149 c.other_repos.append((fork.repo_name, '%s/%s' % (
144 fork.user.username, fork.repo_name))
150 fork.user.username, fork.repo_name))
145 )
151 )
146 other_repos_info[fork.repo_name] = {
152 other_repos_info[fork.repo_name] = {
147 'gravatar': h.gravatar_url(fork.user.email, 24),
153 'gravatar': h.gravatar_url(fork.user.email, 24),
148 'description': fork.description,
154 'description': fork.description,
149 'revs': h.select('other_ref', '',
155 'revs': h.select('other_ref', '',
150 self._get_repo_refs(fork.scm_instance),
156 self._get_repo_refs(fork.scm_instance),
151 class_='refs')
157 class_='refs')
152 }
158 }
153 #add parents of this fork also, but only if it's not empty
159 #add parents of this fork also, but only if it's not empty
154 if org_repo.parent and org_repo.parent.scm_instance.revisions:
160 if org_repo.parent and org_repo.parent.scm_instance.revisions:
155 c.default_pull_request = org_repo.parent.repo_name
161 c.default_pull_request = org_repo.parent.repo_name
156 c.default_pull_request_rev = self._get_default_rev(org_repo.parent)
162 c.default_pull_request_rev = self._get_default_rev(org_repo.parent)
157 c.default_revs = self._get_repo_refs(org_repo.parent.scm_instance)
163 c.default_revs = self._get_repo_refs(org_repo.parent.scm_instance)
158 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
164 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
159 org_repo.parent.user.username,
165 org_repo.parent.user.username,
160 org_repo.parent.repo_name))
166 org_repo.parent.repo_name))
161 )
167 )
162 other_repos_info[org_repo.parent.repo_name] = {
168 other_repos_info[org_repo.parent.repo_name] = {
163 'gravatar': h.gravatar_url(org_repo.parent.user.email, 24),
169 'gravatar': h.gravatar_url(org_repo.parent.user.email, 24),
164 'description': org_repo.parent.description,
170 'description': org_repo.parent.description,
165 'revs': h.select('other_ref', '',
171 'revs': h.select('other_ref', '',
166 self._get_repo_refs(org_repo.parent.scm_instance),
172 self._get_repo_refs(org_repo.parent.scm_instance),
167 class_='refs')
173 class_='refs')
168 }
174 }
169
175
170 c.other_repos_info = json.dumps(other_repos_info)
176 c.other_repos_info = json.dumps(other_repos_info)
171 c.review_members = [org_repo.user]
177 c.review_members = [org_repo.user]
172 return render('/pullrequests/pullrequest.html')
178 return render('/pullrequests/pullrequest.html')
173
179
174 @NotAnonymous()
180 @NotAnonymous()
175 def create(self, repo_name):
181 def create(self, repo_name):
176 repo = RepoModel()._get_repo(repo_name)
182 repo = RepoModel()._get_repo(repo_name)
177 try:
183 try:
178 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
184 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
179 except formencode.Invalid, errors:
185 except formencode.Invalid, errors:
180 log.error(traceback.format_exc())
186 log.error(traceback.format_exc())
181 if errors.error_dict.get('revisions'):
187 if errors.error_dict.get('revisions'):
182 msg = 'Revisions: %s' % errors.error_dict['revisions']
188 msg = 'Revisions: %s' % errors.error_dict['revisions']
183 elif errors.error_dict.get('pullrequest_title'):
189 elif errors.error_dict.get('pullrequest_title'):
184 msg = _('Pull request requires a title with min. 3 chars')
190 msg = _('Pull request requires a title with min. 3 chars')
185 else:
191 else:
186 msg = _('error during creation of pull request')
192 msg = _('error during creation of pull request')
187
193
188 h.flash(msg, 'error')
194 h.flash(msg, 'error')
189 return redirect(url('pullrequest_home', repo_name=repo_name))
195 return redirect(url('pullrequest_home', repo_name=repo_name))
190
196
191 org_repo = _form['org_repo']
197 org_repo = _form['org_repo']
192 org_ref = _form['org_ref']
198 org_ref = _form['org_ref']
193 other_repo = _form['other_repo']
199 other_repo = _form['other_repo']
194 other_ref = _form['other_ref']
200 other_ref = _form['other_ref']
195 revisions = _form['revisions']
201 revisions = _form['revisions']
196 reviewers = _form['review_members']
202 reviewers = _form['review_members']
197
203
198 # if we have cherry picked pull request we don't care what is in
204 # if we have cherry picked pull request we don't care what is in
199 # org_ref/other_ref
205 # org_ref/other_ref
200 rev_start = request.POST.get('rev_start')
206 rev_start = request.POST.get('rev_start')
201 rev_end = request.POST.get('rev_end')
207 rev_end = request.POST.get('rev_end')
202
208
203 if rev_start and rev_end:
209 if rev_start and rev_end:
204 # this is swapped to simulate that rev_end is a revision from
210 # this is swapped to simulate that rev_end is a revision from
205 # parent of the fork
211 # parent of the fork
206 org_ref = 'rev:%s:%s' % (rev_end, rev_end)
212 org_ref = 'rev:%s:%s' % (rev_end, rev_end)
207 other_ref = 'rev:%s:%s' % (rev_start, rev_start)
213 other_ref = 'rev:%s:%s' % (rev_start, rev_start)
208
214
209 title = _form['pullrequest_title']
215 title = _form['pullrequest_title']
210 description = _form['pullrequest_desc']
216 description = _form['pullrequest_desc']
211
217
212 try:
218 try:
213 pull_request = PullRequestModel().create(
219 pull_request = PullRequestModel().create(
214 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
220 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
215 other_ref, revisions, reviewers, title, description
221 other_ref, revisions, reviewers, title, description
216 )
222 )
217 Session().commit()
223 Session().commit()
218 h.flash(_('Successfully opened new pull request'),
224 h.flash(_('Successfully opened new pull request'),
219 category='success')
225 category='success')
220 except Exception:
226 except Exception:
221 h.flash(_('Error occurred during sending pull request'),
227 h.flash(_('Error occurred during sending pull request'),
222 category='error')
228 category='error')
223 log.error(traceback.format_exc())
229 log.error(traceback.format_exc())
224 return redirect(url('pullrequest_home', repo_name=repo_name))
230 return redirect(url('pullrequest_home', repo_name=repo_name))
225
231
226 return redirect(url('pullrequest_show', repo_name=other_repo,
232 return redirect(url('pullrequest_show', repo_name=other_repo,
227 pull_request_id=pull_request.pull_request_id))
233 pull_request_id=pull_request.pull_request_id))
228
234
229 @NotAnonymous()
235 @NotAnonymous()
230 @jsonify
236 @jsonify
231 def update(self, repo_name, pull_request_id):
237 def update(self, repo_name, pull_request_id):
232 pull_request = PullRequest.get_or_404(pull_request_id)
238 pull_request = PullRequest.get_or_404(pull_request_id)
233 if pull_request.is_closed():
239 if pull_request.is_closed():
234 raise HTTPForbidden()
240 raise HTTPForbidden()
235 #only owner or admin can update it
241 #only owner or admin can update it
236 owner = pull_request.author.user_id == c.rhodecode_user.user_id
242 owner = pull_request.author.user_id == c.rhodecode_user.user_id
237 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
243 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
238 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
244 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
239 request.POST.get('reviewers_ids', '').split(',')))
245 request.POST.get('reviewers_ids', '').split(',')))
240
246
241 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
247 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
242 Session().commit()
248 Session().commit()
243 return True
249 return True
244 raise HTTPForbidden()
250 raise HTTPForbidden()
245
251
246 @NotAnonymous()
252 @NotAnonymous()
247 @jsonify
253 @jsonify
248 def delete(self, repo_name, pull_request_id):
254 def delete(self, repo_name, pull_request_id):
249 pull_request = PullRequest.get_or_404(pull_request_id)
255 pull_request = PullRequest.get_or_404(pull_request_id)
250 #only owner can delete it !
256 #only owner can delete it !
251 if pull_request.author.user_id == c.rhodecode_user.user_id:
257 if pull_request.author.user_id == c.rhodecode_user.user_id:
252 PullRequestModel().delete(pull_request)
258 PullRequestModel().delete(pull_request)
253 Session().commit()
259 Session().commit()
254 h.flash(_('Successfully deleted pull request'),
260 h.flash(_('Successfully deleted pull request'),
255 category='success')
261 category='success')
256 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
262 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
257 raise HTTPForbidden()
263 raise HTTPForbidden()
258
264
259 def _load_compare_data(self, pull_request, enable_comments=True):
265 def _load_compare_data(self, pull_request, enable_comments=True):
260 """
266 """
261 Load context data needed for generating compare diff
267 Load context data needed for generating compare diff
262
268
263 :param pull_request:
269 :param pull_request:
264 :type pull_request:
270 :type pull_request:
265 """
271 """
266 rev_start = request.GET.get('rev_start')
272 rev_start = request.GET.get('rev_start')
267 rev_end = request.GET.get('rev_end')
273 rev_end = request.GET.get('rev_end')
268
274
269 org_repo = pull_request.org_repo
275 org_repo = pull_request.org_repo
270 (org_ref_type,
276 (org_ref_type,
271 org_ref_name,
277 org_ref_name,
272 org_ref_rev) = pull_request.org_ref.split(':')
278 org_ref_rev) = pull_request.org_ref.split(':')
273
279
274 other_repo = org_repo
280 other_repo = org_repo
275 (other_ref_type,
281 (other_ref_type,
276 other_ref_name,
282 other_ref_name,
277 other_ref_rev) = pull_request.other_ref.split(':')
283 other_ref_rev) = pull_request.other_ref.split(':')
278
284
279 # despite opening revisions for bookmarks/branches/tags, we always
285 # despite opening revisions for bookmarks/branches/tags, we always
280 # convert this to rev to prevent changes after book or branch change
286 # convert this to rev to prevent changes after book or branch change
281 org_ref = ('rev', org_ref_rev)
287 org_ref = ('rev', org_ref_rev)
282 other_ref = ('rev', other_ref_rev)
288 other_ref = ('rev', other_ref_rev)
283
289
284 c.org_repo = org_repo
290 c.org_repo = org_repo
285 c.other_repo = other_repo
291 c.other_repo = other_repo
286
292
287 c.fulldiff = fulldiff = request.GET.get('fulldiff')
293 c.fulldiff = fulldiff = request.GET.get('fulldiff')
288
294
289 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
295 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
290
296
291 other_ref = ('rev', getattr(c.cs_ranges[0].parents[0]
297 other_ref = ('rev', getattr(c.cs_ranges[0].parents[0]
292 if c.cs_ranges[0].parents
298 if c.cs_ranges[0].parents
293 else EmptyChangeset(), 'raw_id'))
299 else EmptyChangeset(), 'raw_id'))
294
300
295 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
301 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
296 c.target_repo = c.repo_name
302 c.target_repo = c.repo_name
297 # defines that we need hidden inputs with changesets
303 # defines that we need hidden inputs with changesets
298 c.as_form = request.GET.get('as_form', False)
304 c.as_form = request.GET.get('as_form', False)
299
305
300 c.org_ref = org_ref[1]
306 c.org_ref = org_ref[1]
301 c.other_ref = other_ref[1]
307 c.other_ref = other_ref[1]
302
308
303 diff_limit = self.cut_off_limit if not fulldiff else None
309 diff_limit = self.cut_off_limit if not fulldiff else None
304
310
305 #we swap org/other ref since we run a simple diff on one repo
311 #we swap org/other ref since we run a simple diff on one repo
306 _diff = diffs.differ(org_repo, other_ref, other_repo, org_ref)
312 _diff = diffs.differ(org_repo, other_ref, other_repo, org_ref)
307
313
308 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
314 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
309 diff_limit=diff_limit)
315 diff_limit=diff_limit)
310 _parsed = diff_processor.prepare()
316 _parsed = diff_processor.prepare()
311
317
312 c.limited_diff = False
318 c.limited_diff = False
313 if isinstance(_parsed, LimitedDiffContainer):
319 if isinstance(_parsed, LimitedDiffContainer):
314 c.limited_diff = True
320 c.limited_diff = True
315
321
316 c.files = []
322 c.files = []
317 c.changes = {}
323 c.changes = {}
318 c.lines_added = 0
324 c.lines_added = 0
319 c.lines_deleted = 0
325 c.lines_deleted = 0
320 for f in _parsed:
326 for f in _parsed:
321 st = f['stats']
327 st = f['stats']
322 if st[0] != 'b':
328 if st[0] != 'b':
323 c.lines_added += st[0]
329 c.lines_added += st[0]
324 c.lines_deleted += st[1]
330 c.lines_deleted += st[1]
325 fid = h.FID('', f['filename'])
331 fid = h.FID('', f['filename'])
326 c.files.append([fid, f['operation'], f['filename'], f['stats']])
332 c.files.append([fid, f['operation'], f['filename'], f['stats']])
327 diff = diff_processor.as_html(enable_comments=enable_comments,
333 diff = diff_processor.as_html(enable_comments=enable_comments,
328 parsed_lines=[f])
334 parsed_lines=[f])
329 c.changes[fid] = [f['operation'], f['filename'], diff]
335 c.changes[fid] = [f['operation'], f['filename'], diff]
330
336
331 def show(self, repo_name, pull_request_id):
337 def show(self, repo_name, pull_request_id):
332 repo_model = RepoModel()
338 repo_model = RepoModel()
333 c.users_array = repo_model.get_users_js()
339 c.users_array = repo_model.get_users_js()
334 c.users_groups_array = repo_model.get_users_groups_js()
340 c.users_groups_array = repo_model.get_users_groups_js()
335 c.pull_request = PullRequest.get_or_404(pull_request_id)
341 c.pull_request = PullRequest.get_or_404(pull_request_id)
336 c.target_repo = c.pull_request.org_repo.repo_name
342 c.target_repo = c.pull_request.org_repo.repo_name
337
343 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
338 cc_model = ChangesetCommentsModel()
344 cc_model = ChangesetCommentsModel()
339 cs_model = ChangesetStatusModel()
345 cs_model = ChangesetStatusModel()
340 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
346 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
341 pull_request=c.pull_request,
347 pull_request=c.pull_request,
342 with_revisions=True)
348 with_revisions=True)
343
349
344 cs_statuses = defaultdict(list)
350 cs_statuses = defaultdict(list)
345 for st in _cs_statuses:
351 for st in _cs_statuses:
346 cs_statuses[st.author.username] += [st]
352 cs_statuses[st.author.username] += [st]
347
353
348 c.pull_request_reviewers = []
354 c.pull_request_reviewers = []
349 c.pull_request_pending_reviewers = []
355 c.pull_request_pending_reviewers = []
350 for o in c.pull_request.reviewers:
356 for o in c.pull_request.reviewers:
351 st = cs_statuses.get(o.user.username, None)
357 st = cs_statuses.get(o.user.username, None)
352 if st:
358 if st:
353 sorter = lambda k: k.version
359 sorter = lambda k: k.version
354 st = [(x, list(y)[0])
360 st = [(x, list(y)[0])
355 for x, y in (groupby(sorted(st, key=sorter), sorter))]
361 for x, y in (groupby(sorted(st, key=sorter), sorter))]
356 else:
362 else:
357 c.pull_request_pending_reviewers.append(o.user)
363 c.pull_request_pending_reviewers.append(o.user)
358 c.pull_request_reviewers.append([o.user, st])
364 c.pull_request_reviewers.append([o.user, st])
359
365
360 # pull_requests repo_name we opened it against
366 # pull_requests repo_name we opened it against
361 # ie. other_repo must match
367 # ie. other_repo must match
362 if repo_name != c.pull_request.other_repo.repo_name:
368 if repo_name != c.pull_request.other_repo.repo_name:
363 raise HTTPNotFound
369 raise HTTPNotFound
364
370
365 # load compare data into template context
371 # load compare data into template context
366 enable_comments = not c.pull_request.is_closed()
372 enable_comments = not c.pull_request.is_closed()
367 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
373 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
368
374
369 # inline comments
375 # inline comments
370 c.inline_cnt = 0
376 c.inline_cnt = 0
371 c.inline_comments = cc_model.get_inline_comments(
377 c.inline_comments = cc_model.get_inline_comments(
372 c.rhodecode_db_repo.repo_id,
378 c.rhodecode_db_repo.repo_id,
373 pull_request=pull_request_id)
379 pull_request=pull_request_id)
374 # count inline comments
380 # count inline comments
375 for __, lines in c.inline_comments:
381 for __, lines in c.inline_comments:
376 for comments in lines.values():
382 for comments in lines.values():
377 c.inline_cnt += len(comments)
383 c.inline_cnt += len(comments)
378 # comments
384 # comments
379 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
385 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
380 pull_request=pull_request_id)
386 pull_request=pull_request_id)
381
387
382 try:
388 try:
383 cur_status = c.statuses[c.pull_request.revisions[0]][0]
389 cur_status = c.statuses[c.pull_request.revisions[0]][0]
384 except:
390 except:
385 log.error(traceback.format_exc())
391 log.error(traceback.format_exc())
386 cur_status = 'undefined'
392 cur_status = 'undefined'
387 if c.pull_request.is_closed() and 0:
393 if c.pull_request.is_closed() and 0:
388 c.current_changeset_status = cur_status
394 c.current_changeset_status = cur_status
389 else:
395 else:
390 # changeset(pull-request) status calulation based on reviewers
396 # changeset(pull-request) status calulation based on reviewers
391 c.current_changeset_status = cs_model.calculate_status(
397 c.current_changeset_status = cs_model.calculate_status(
392 c.pull_request_reviewers,
398 c.pull_request_reviewers,
393 )
399 )
394 c.changeset_statuses = ChangesetStatus.STATUSES
400 c.changeset_statuses = ChangesetStatus.STATUSES
395
401
396 return render('/pullrequests/pullrequest_show.html')
402 return render('/pullrequests/pullrequest_show.html')
397
403
398 @NotAnonymous()
404 @NotAnonymous()
399 @jsonify
405 @jsonify
400 def comment(self, repo_name, pull_request_id):
406 def comment(self, repo_name, pull_request_id):
401 pull_request = PullRequest.get_or_404(pull_request_id)
407 pull_request = PullRequest.get_or_404(pull_request_id)
402 if pull_request.is_closed():
408 if pull_request.is_closed():
403 raise HTTPForbidden()
409 raise HTTPForbidden()
404
410
405 status = request.POST.get('changeset_status')
411 status = request.POST.get('changeset_status')
406 change_status = request.POST.get('change_changeset_status')
412 change_status = request.POST.get('change_changeset_status')
407 text = request.POST.get('text')
413 text = request.POST.get('text')
408 if status and change_status:
414
415 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
416 if status and change_status and allowed_to_change_status:
409 text = text or (_('Status change -> %s')
417 text = text or (_('Status change -> %s')
410 % ChangesetStatus.get_status_lbl(status))
418 % ChangesetStatus.get_status_lbl(status))
411 comm = ChangesetCommentsModel().create(
419 comm = ChangesetCommentsModel().create(
412 text=text,
420 text=text,
413 repo=c.rhodecode_db_repo.repo_id,
421 repo=c.rhodecode_db_repo.repo_id,
414 user=c.rhodecode_user.user_id,
422 user=c.rhodecode_user.user_id,
415 pull_request=pull_request_id,
423 pull_request=pull_request_id,
416 f_path=request.POST.get('f_path'),
424 f_path=request.POST.get('f_path'),
417 line_no=request.POST.get('line'),
425 line_no=request.POST.get('line'),
418 status_change=(ChangesetStatus.get_status_lbl(status)
426 status_change=(ChangesetStatus.get_status_lbl(status)
419 if status and change_status else None)
427 if status and change_status and allowed_to_change_status else None)
420 )
428 )
421
429
422 # get status if set !
423 if status and change_status:
424 ChangesetStatusModel().set_status(
425 c.rhodecode_db_repo.repo_id,
426 status,
427 c.rhodecode_user.user_id,
428 comm,
429 pull_request=pull_request_id
430 )
431 action_logger(self.rhodecode_user,
430 action_logger(self.rhodecode_user,
432 'user_commented_pull_request:%s' % pull_request_id,
431 'user_commented_pull_request:%s' % pull_request_id,
433 c.rhodecode_db_repo, self.ip_addr, self.sa)
432 c.rhodecode_db_repo, self.ip_addr, self.sa)
434
433
435 if request.POST.get('save_close'):
434 if allowed_to_change_status:
436 if status in ['rejected', 'approved']:
435 # get status if set !
437 PullRequestModel().close_pull_request(pull_request_id)
436 if status and change_status:
438 action_logger(self.rhodecode_user,
437 ChangesetStatusModel().set_status(
439 'user_closed_pull_request:%s' % pull_request_id,
438 c.rhodecode_db_repo.repo_id,
440 c.rhodecode_db_repo, self.ip_addr, self.sa)
439 status,
441 else:
440 c.rhodecode_user.user_id,
442 h.flash(_('Closing pull request on other statuses than '
441 comm,
443 'rejected or approved forbidden'),
442 pull_request=pull_request_id
444 category='warning')
443 )
444
445 if request.POST.get('save_close'):
446 if status in ['rejected', 'approved']:
447 PullRequestModel().close_pull_request(pull_request_id)
448 action_logger(self.rhodecode_user,
449 'user_closed_pull_request:%s' % pull_request_id,
450 c.rhodecode_db_repo, self.ip_addr, self.sa)
451 else:
452 h.flash(_('Closing pull request on other statuses than '
453 'rejected or approved forbidden'),
454 category='warning')
445
455
446 Session().commit()
456 Session().commit()
447
457
448 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
458 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
449 return redirect(h.url('pullrequest_show', repo_name=repo_name,
459 return redirect(h.url('pullrequest_show', repo_name=repo_name,
450 pull_request_id=pull_request_id))
460 pull_request_id=pull_request_id))
451
461
452 data = {
462 data = {
453 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
463 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
454 }
464 }
455 if comm:
465 if comm:
456 c.co = comm
466 c.co = comm
457 data.update(comm.get_dict())
467 data.update(comm.get_dict())
458 data.update({'rendered_text':
468 data.update({'rendered_text':
459 render('changeset/changeset_comment_block.html')})
469 render('changeset/changeset_comment_block.html')})
460
470
461 return data
471 return data
462
472
463 @NotAnonymous()
473 @NotAnonymous()
464 @jsonify
474 @jsonify
465 def delete_comment(self, repo_name, comment_id):
475 def delete_comment(self, repo_name, comment_id):
466 co = ChangesetComment.get(comment_id)
476 co = ChangesetComment.get(comment_id)
467 if co.pull_request.is_closed():
477 if co.pull_request.is_closed():
468 #don't allow deleting comments on closed pull request
478 #don't allow deleting comments on closed pull request
469 raise HTTPForbidden()
479 raise HTTPForbidden()
470
480
471 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
481 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
472 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
482 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
473 ChangesetCommentsModel().delete(comment=co)
483 ChangesetCommentsModel().delete(comment=co)
474 Session().commit()
484 Session().commit()
475 return True
485 return True
476 else:
486 else:
477 raise HTTPForbidden()
487 raise HTTPForbidden()
@@ -1,172 +1,176 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 ## ${comment.comment_block(co)}
4 ## ${comment.comment_block(co)}
5 ##
5 ##
6 <%def name="comment_block(co)">
6 <%def name="comment_block(co)">
7 <div class="comment" id="comment-${co.comment_id}" line="${co.line_no}">
7 <div class="comment" id="comment-${co.comment_id}" line="${co.line_no}">
8 <div class="comment-wrapp">
8 <div class="comment-wrapp">
9 <div class="meta">
9 <div class="meta">
10 <div style="float:left"> <img src="${h.gravatar_url(co.author.email, 20)}" /> </div>
10 <div style="float:left"> <img src="${h.gravatar_url(co.author.email, 20)}" /> </div>
11 <div class="user">
11 <div class="user">
12 ${co.author.username}
12 ${co.author.username}
13 </div>
13 </div>
14 <div class="date">
14 <div class="date">
15 ${h.age(co.modified_at)} <a class="permalink" href="#comment-${co.comment_id}">&para;</a>
15 ${h.age(co.modified_at)} <a class="permalink" href="#comment-${co.comment_id}">&para;</a>
16 </div>
16 </div>
17 %if co.status_change:
17 %if co.status_change:
18 <div style="float:left" class="changeset-status-container">
18 <div style="float:left" class="changeset-status-container">
19 <div style="float:left;padding:0px 2px 0px 2px"><span style="font-size: 18px;">&rsaquo;</span></div>
19 <div style="float:left;padding:0px 2px 0px 2px"><span style="font-size: 18px;">&rsaquo;</span></div>
20 <div title="${_('Changeset status')}" class="changeset-status-lbl"> ${co.status_change[0].status_lbl}</div>
20 <div title="${_('Changeset status')}" class="changeset-status-lbl"> ${co.status_change[0].status_lbl}</div>
21 <div class="changeset-status-ico"><img src="${h.url(str('/images/icons/flag_status_%s.png' % co.status_change[0].status))}" /></div>
21 <div class="changeset-status-ico"><img src="${h.url(str('/images/icons/flag_status_%s.png' % co.status_change[0].status))}" /></div>
22 </div>
22 </div>
23 %endif
23 %endif
24 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or co.author.user_id == c.rhodecode_user.user_id:
24 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or co.author.user_id == c.rhodecode_user.user_id:
25 <div class="buttons">
25 <div class="buttons">
26 <span onClick="deleteComment(${co.comment_id})" class="delete-comment ui-btn">${_('Delete')}</span>
26 <span onClick="deleteComment(${co.comment_id})" class="delete-comment ui-btn">${_('Delete')}</span>
27 </div>
27 </div>
28 %endif
28 %endif
29 </div>
29 </div>
30 <div class="text">
30 <div class="text">
31 ${h.rst_w_mentions(co.text)|n}
31 ${h.rst_w_mentions(co.text)|n}
32 </div>
32 </div>
33 </div>
33 </div>
34 </div>
34 </div>
35 </%def>
35 </%def>
36
36
37
37
38 <%def name="comment_inline_form()">
38 <%def name="comment_inline_form()">
39 <div id='comment-inline-form-template' style="display:none">
39 <div id='comment-inline-form-template' style="display:none">
40 <div class="comment-inline-form ac">
40 <div class="comment-inline-form ac">
41 %if c.rhodecode_user.username != 'default':
41 %if c.rhodecode_user.username != 'default':
42 <div class="overlay"><div class="overlay-text">${_('Submitting...')}</div></div>
42 <div class="overlay"><div class="overlay-text">${_('Submitting...')}</div></div>
43 ${h.form('#', class_='inline-form')}
43 ${h.form('#', class_='inline-form')}
44 <div class="clearfix">
44 <div class="clearfix">
45 <div class="comment-help">${_('Commenting on line {1}.')}
45 <div class="comment-help">${_('Commenting on line {1}.')}
46 ${(_('Comments parsed using %s syntax with %s support.') % (
46 ${(_('Comments parsed using %s syntax with %s support.') % (
47 ('<a href="%s">RST</a>' % h.url('rst_help')),
47 ('<a href="%s">RST</a>' % h.url('rst_help')),
48 ('<span style="color:#003367" class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
48 ('<span style="color:#003367" class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
49 )
49 )
50 )|n
50 )|n
51 }
51 }
52 </div>
52 </div>
53 <div class="mentions-container" id="mentions_container_{1}"></div>
53 <div class="mentions-container" id="mentions_container_{1}"></div>
54 <textarea id="text_{1}" name="text" class="yui-ac-input"></textarea>
54 <textarea id="text_{1}" name="text" class="yui-ac-input"></textarea>
55 </div>
55 </div>
56 <div class="comment-button">
56 <div class="comment-button">
57 <input type="hidden" name="f_path" value="{0}">
57 <input type="hidden" name="f_path" value="{0}">
58 <input type="hidden" name="line" value="{1}">
58 <input type="hidden" name="line" value="{1}">
59 ${h.submit('save', _('Comment'), class_='ui-btn save-inline-form')}
59 ${h.submit('save', _('Comment'), class_='ui-btn save-inline-form')}
60 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
60 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
61 </div>
61 </div>
62 ${h.end_form()}
62 ${h.end_form()}
63 %else:
63 %else:
64 ${h.form('')}
64 ${h.form('')}
65 <div class="clearfix">
65 <div class="clearfix">
66 <div class="comment-help">
66 <div class="comment-help">
67 ${_('You need to be logged in to comment.')} <a href="${h.url('login_home',came_from=h.url.current())}">${_('Login now')}</a>
67 ${_('You need to be logged in to comment.')} <a href="${h.url('login_home',came_from=h.url.current())}">${_('Login now')}</a>
68 </div>
68 </div>
69 </div>
69 </div>
70 <div class="comment-button">
70 <div class="comment-button">
71 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
71 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
72 </div>
72 </div>
73 ${h.end_form()}
73 ${h.end_form()}
74 %endif
74 %endif
75 </div>
75 </div>
76 </div>
76 </div>
77 </%def>
77 </%def>
78
78
79
79
80 ## generates inlines taken from c.comments var
80 ## generates inlines taken from c.comments var
81 <%def name="inlines()">
81 <%def name="inlines()">
82 <div class="comments-number">${ungettext("%d comment", "%d comments", len(c.comments)) % len(c.comments)} ${ungettext("(%d inline)", "(%d inline)", c.inline_cnt) % c.inline_cnt}</div>
82 <div class="comments-number">${ungettext("%d comment", "%d comments", len(c.comments)) % len(c.comments)} ${ungettext("(%d inline)", "(%d inline)", c.inline_cnt) % c.inline_cnt}</div>
83 %for path, lines in c.inline_comments:
83 %for path, lines in c.inline_comments:
84 % for line,comments in lines.iteritems():
84 % for line,comments in lines.iteritems():
85 <div style="display:none" class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
85 <div style="display:none" class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
86 %for co in comments:
86 %for co in comments:
87 ${comment_block(co)}
87 ${comment_block(co)}
88 %endfor
88 %endfor
89 </div>
89 </div>
90 %endfor
90 %endfor
91 %endfor
91 %endfor
92
92
93 </%def>
93 </%def>
94
94
95 ## generate inline comments and the main ones
95 ## generate inline comments and the main ones
96 <%def name="generate_comments()">
96 <%def name="generate_comments()">
97 <div class="comments">
97 <div class="comments">
98 <div id="inline-comments-container">
98 <div id="inline-comments-container">
99 ## generate inlines for this changeset
99 ## generate inlines for this changeset
100 ${inlines()}
100 ${inlines()}
101 </div>
101 </div>
102
102
103 %for co in c.comments:
103 %for co in c.comments:
104 <div id="comment-tr-${co.comment_id}">
104 <div id="comment-tr-${co.comment_id}">
105 ${comment_block(co)}
105 ${comment_block(co)}
106 </div>
106 </div>
107 %endfor
107 %endfor
108 </div>
108 </div>
109 </%def>
109 </%def>
110
110
111 ## MAIN COMMENT FORM
111 ## MAIN COMMENT FORM
112 <%def name="comments(post_url, cur_status, close_btn=False)">
112 <%def name="comments(post_url, cur_status, close_btn=False, change_status=True)">
113
113
114 <div class="comments">
114 <div class="comments">
115 %if c.rhodecode_user.username != 'default':
115 %if c.rhodecode_user.username != 'default':
116 <div class="comment-form ac">
116 <div class="comment-form ac">
117 ${h.form(post_url)}
117 ${h.form(post_url)}
118 <strong>${_('Leave a comment')}</strong>
118 <strong>${_('Leave a comment')}</strong>
119 <div class="clearfix">
119 <div class="clearfix">
120 <div class="comment-help">
120 <div class="comment-help">
121 ${(_('Comments parsed using %s syntax with %s support.') % (('<a href="%s">RST</a>' % h.url('rst_help')),
121 ${(_('Comments parsed using %s syntax with %s support.') % (('<a href="%s">RST</a>' % h.url('rst_help')),
122 '<span style="color:#003367" class="tooltip" title="%s">@mention</span>' %
122 '<span style="color:#003367" class="tooltip" title="%s">@mention</span>' %
123 _('Use @username inside this text to send notification to this RhodeCode user')))|n}
123 _('Use @username inside this text to send notification to this RhodeCode user')))|n}
124 %if change_status:
124 | <label for="show_changeset_status_box" class="tooltip" title="${_('Check this to change current status of code-review for this changeset')}"> ${_('change status')}</label>
125 | <label for="show_changeset_status_box" class="tooltip" title="${_('Check this to change current status of code-review for this changeset')}"> ${_('change status')}</label>
125 <input style="vertical-align: bottom;margin-bottom:-2px" id="show_changeset_status_box" type="checkbox" name="change_changeset_status" />
126 <input style="vertical-align: bottom;margin-bottom:-2px" id="show_changeset_status_box" type="checkbox" name="change_changeset_status" />
127 %endif
126 </div>
128 </div>
129 %if change_status:
127 <div id="status_block_container" class="status-block" style="display:none">
130 <div id="status_block_container" class="status-block" style="display:none">
128 %for status,lbl in c.changeset_statuses:
131 %for status,lbl in c.changeset_statuses:
129 <div class="">
132 <div class="">
130 <img src="${h.url('/images/icons/flag_status_%s.png' % status)}" /> <input ${'checked="checked"' if status == cur_status else ''}" type="radio" class="status_change_radio" name="changeset_status" id="${status}" value="${status}">
133 <img src="${h.url('/images/icons/flag_status_%s.png' % status)}" /> <input ${'checked="checked"' if status == cur_status else ''}" type="radio" class="status_change_radio" name="changeset_status" id="${status}" value="${status}">
131 <label for="${status}">${lbl}</label>
134 <label for="${status}">${lbl}</label>
132 </div>
135 </div>
133 %endfor
136 %endfor
134 </div>
137 </div>
138 %endif
135 <div class="mentions-container" id="mentions_container"></div>
139 <div class="mentions-container" id="mentions_container"></div>
136 ${h.textarea('text')}
140 ${h.textarea('text')}
137 </div>
141 </div>
138 <div class="comment-button">
142 <div class="comment-button">
139 ${h.submit('save', _('Comment'), class_="ui-btn large")}
143 ${h.submit('save', _('Comment'), class_="ui-btn large")}
140 %if close_btn:
144 %if close_btn and change_status:
141 ${h.submit('save_close', _('Comment and close'), class_='ui-btn blue large %s' % 'hidden' if cur_status in ['not_reviewd','under_review'] else '')}
145 ${h.submit('save_close', _('Comment and close'), class_='ui-btn blue large %s' % ('hidden' if cur_status in ['not_reviewed','under_review'] else ''))}
142 %endif
146 %endif
143 </div>
147 </div>
144 ${h.end_form()}
148 ${h.end_form()}
145 </div>
149 </div>
146 %endif
150 %endif
147 </div>
151 </div>
148 <script>
152 <script>
149 YUE.onDOMReady(function () {
153 YUE.onDOMReady(function () {
150 MentionsAutoComplete('text', 'mentions_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
154 MentionsAutoComplete('text', 'mentions_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
151
155
152 // changeset status box listener
156 // changeset status box listener
153 YUE.on(YUD.get('show_changeset_status_box'),'change',function(e){
157 YUE.on(YUD.get('show_changeset_status_box'),'change',function(e){
154 if(e.currentTarget.checked){
158 if(e.currentTarget.checked){
155 YUD.setStyle('status_block_container','display','');
159 YUD.setStyle('status_block_container','display','');
156 }
160 }
157 else{
161 else{
158 YUD.setStyle('status_block_container','display','none');
162 YUD.setStyle('status_block_container','display','none');
159 }
163 }
160 })
164 })
161 YUE.on(YUQ('.status_change_radio'), 'change',function(e){
165 YUE.on(YUQ('.status_change_radio'), 'change',function(e){
162 var val = e.currentTarget.value;
166 var val = e.currentTarget.value;
163 if (val == 'approved' || val == 'rejected') {
167 if (val == 'approved' || val == 'rejected') {
164 YUD.removeClass('save_close', 'hidden');
168 YUD.removeClass('save_close', 'hidden');
165 }else{
169 }else{
166 YUD.addClass('save_close', 'hidden');
170 YUD.addClass('save_close', 'hidden');
167 }
171 }
168 })
172 })
169
173
170 });
174 });
171 </script>
175 </script>
172 </%def>
176 </%def>
@@ -1,217 +1,217 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${c.repo_name} ${_('Pull request #%s') % c.pull_request.pull_request_id}
4 ${c.repo_name} ${_('Pull request #%s') % c.pull_request.pull_request_id}
5 </%def>
5 </%def>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${h.link_to(_(u'Home'),h.url('/'))}
8 ${h.link_to(_(u'Home'),h.url('/'))}
9 &raquo;
9 &raquo;
10 ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
10 ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
11 &raquo;
11 &raquo;
12 ${_('Pull request #%s') % c.pull_request.pull_request_id}
12 ${_('Pull request #%s') % c.pull_request.pull_request_id}
13 </%def>
13 </%def>
14
14
15 <%def name="main()">
15 <%def name="main()">
16
16
17 <div class="box">
17 <div class="box">
18 <!-- box / title -->
18 <!-- box / title -->
19 <div class="title">
19 <div class="title">
20 ${self.breadcrumbs()}
20 ${self.breadcrumbs()}
21 </div>
21 </div>
22 %if c.pull_request.is_closed():
22 %if c.pull_request.is_closed():
23 <div style="padding:10px; font-size:22px;width:100%;text-align: center; color:#88D882">${_('Closed %s') % (h.age(c.pull_request.updated_on))} ${_('with status %s') % h.changeset_status_lbl(c.current_changeset_status)}</div>
23 <div style="padding:10px; font-size:22px;width:100%;text-align: center; color:#88D882">${_('Closed %s') % (h.age(c.pull_request.updated_on))} ${_('with status %s') % h.changeset_status_lbl(c.current_changeset_status)}</div>
24 %endif
24 %endif
25 <h3>${_('Title')}: ${c.pull_request.title}</h3>
25 <h3>${_('Title')}: ${c.pull_request.title}</h3>
26
26
27 <div class="form">
27 <div class="form">
28 <div id="summary" class="fields">
28 <div id="summary" class="fields">
29 <div class="field">
29 <div class="field">
30 <div class="label-summary">
30 <div class="label-summary">
31 <label>${_('Status')}:</label>
31 <label>${_('Status')}:</label>
32 </div>
32 </div>
33 <div class="input">
33 <div class="input">
34 <div class="changeset-status-container" style="float:none;clear:both">
34 <div class="changeset-status-container" style="float:none;clear:both">
35 %if c.current_changeset_status:
35 %if c.current_changeset_status:
36 <div title="${_('Pull request status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.current_changeset_status)}]</div>
36 <div title="${_('Pull request status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.current_changeset_status)}]</div>
37 <div class="changeset-status-ico" style="padding:1px 4px"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
37 <div class="changeset-status-ico" style="padding:1px 4px"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
38 %endif
38 %endif
39 </div>
39 </div>
40 </div>
40 </div>
41 </div>
41 </div>
42 <div class="field">
42 <div class="field">
43 <div class="label-summary">
43 <div class="label-summary">
44 <label>${_('Still not reviewed by')}:</label>
44 <label>${_('Still not reviewed by')}:</label>
45 </div>
45 </div>
46 <div class="input">
46 <div class="input">
47 % if len(c.pull_request_pending_reviewers) > 0:
47 % if len(c.pull_request_pending_reviewers) > 0:
48 <div class="tooltip" title="${h.tooltip(','.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
48 <div class="tooltip" title="${h.tooltip(','.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
49 %else:
49 %else:
50 <div>${_('pull request was reviewed by all reviewers')}</div>
50 <div>${_('pull request was reviewed by all reviewers')}</div>
51 %endif
51 %endif
52 </div>
52 </div>
53 </div>
53 </div>
54 </div>
54 </div>
55 </div>
55 </div>
56 <div style="white-space:pre-wrap;padding:3px 3px 5px 20px">${h.literal(c.pull_request.description)}</div>
56 <div style="white-space:pre-wrap;padding:3px 3px 5px 20px">${h.literal(c.pull_request.description)}</div>
57 <div style="padding:4px 4px 10px 20px">
57 <div style="padding:4px 4px 10px 20px">
58 <div>${_('Created on')}: ${h.fmt_date(c.pull_request.created_on)}</div>
58 <div>${_('Created on')}: ${h.fmt_date(c.pull_request.created_on)}</div>
59 </div>
59 </div>
60
60
61 <div style="overflow: auto;">
61 <div style="overflow: auto;">
62 ##DIFF
62 ##DIFF
63 <div class="table" style="float:left;clear:none">
63 <div class="table" style="float:left;clear:none">
64 <div id="body" class="diffblock">
64 <div id="body" class="diffblock">
65 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
65 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
66 </div>
66 </div>
67 <div id="changeset_compare_view_content">
67 <div id="changeset_compare_view_content">
68 ##CS
68 ##CS
69 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
69 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
70 <%include file="/compare/compare_cs.html" />
70 <%include file="/compare/compare_cs.html" />
71
71
72 ## FILES
72 ## FILES
73 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
73 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
74
74
75 % if c.limited_diff:
75 % if c.limited_diff:
76 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
76 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
77 % else:
77 % else:
78 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
78 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
79 %endif
79 %endif
80
80
81 </div>
81 </div>
82 <div class="cs_files">
82 <div class="cs_files">
83 %if not c.files:
83 %if not c.files:
84 <span class="empty_data">${_('No files')}</span>
84 <span class="empty_data">${_('No files')}</span>
85 %endif
85 %endif
86 %for fid, change, f, stat in c.files:
86 %for fid, change, f, stat in c.files:
87 <div class="cs_${change}">
87 <div class="cs_${change}">
88 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
88 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
89 <div class="changes">${h.fancy_file_stats(stat)}</div>
89 <div class="changes">${h.fancy_file_stats(stat)}</div>
90 </div>
90 </div>
91 %endfor
91 %endfor
92 </div>
92 </div>
93 % if c.limited_diff:
93 % if c.limited_diff:
94 <h5>${_('Changeset was too big and was cut off...')}</h5>
94 <h5>${_('Changeset was too big and was cut off...')}</h5>
95 % endif
95 % endif
96 </div>
96 </div>
97 </div>
97 </div>
98 ## REVIEWERS
98 ## REVIEWERS
99 <div style="float:left; border-left:1px dashed #eee">
99 <div style="float:left; border-left:1px dashed #eee">
100 <h4>${_('Pull request reviewers')}</h4>
100 <h4>${_('Pull request reviewers')}</h4>
101 <div id="reviewers" style="padding:0px 0px 5px 10px">
101 <div id="reviewers" style="padding:0px 0px 5px 10px">
102 ## members goes here !
102 ## members goes here !
103 <div class="group_members_wrap" style="min-height:45px">
103 <div class="group_members_wrap" style="min-height:45px">
104 <ul id="review_members" class="group_members">
104 <ul id="review_members" class="group_members">
105 %for member,status in c.pull_request_reviewers:
105 %for member,status in c.pull_request_reviewers:
106 <li id="reviewer_${member.user_id}">
106 <li id="reviewer_${member.user_id}">
107 <div class="reviewers_member">
107 <div class="reviewers_member">
108 <div style="float:left;padding:0px 3px 0px 0px" class="tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
108 <div style="float:left;padding:0px 3px 0px 0px" class="tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
109 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
109 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
110 </div>
110 </div>
111 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
111 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
112 <div style="float:left">${member.full_name} (${_('owner')})</div>
112 <div style="float:left">${member.full_name} (${_('owner')})</div>
113 <input type="hidden" value="${member.user_id}" name="review_members" />
113 <input type="hidden" value="${member.user_id}" name="review_members" />
114 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id):
114 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id):
115 <span class="delete_icon action_button" onclick="removeReviewer(${member.user_id})"></span>
115 <span class="delete_icon action_button" onclick="removeReviewer(${member.user_id})"></span>
116 %endif
116 %endif
117 </div>
117 </div>
118 </li>
118 </li>
119 %endfor
119 %endfor
120 </ul>
120 </ul>
121 </div>
121 </div>
122 %if not c.pull_request.is_closed():
122 %if not c.pull_request.is_closed():
123 <div class='ac'>
123 <div class='ac'>
124 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id:
124 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or c.pull_request.author.user_id == c.rhodecode_user.user_id:
125 <div class="reviewer_ac">
125 <div class="reviewer_ac">
126 ${h.text('user', class_='yui-ac-input')}
126 ${h.text('user', class_='yui-ac-input')}
127 <span class="help-block">${_('Add reviewer to this pull request.')}</span>
127 <span class="help-block">${_('Add reviewer to this pull request.')}</span>
128 <div id="reviewers_container"></div>
128 <div id="reviewers_container"></div>
129 </div>
129 </div>
130 <div style="padding:0px 10px">
130 <div style="padding:0px 10px">
131 <span id="update_pull_request" class="ui-btn xsmall">${_('save')}</span>
131 <span id="update_pull_request" class="ui-btn xsmall">${_('save')}</span>
132 </div>
132 </div>
133 %endif
133 %endif
134 </div>
134 </div>
135 %endif
135 %endif
136 </div>
136 </div>
137 </div>
137 </div>
138 </div>
138 </div>
139 <script>
139 <script>
140 var _USERS_AC_DATA = ${c.users_array|n};
140 var _USERS_AC_DATA = ${c.users_array|n};
141 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
141 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
142 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
142 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
143 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
143 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
144 AJAX_UPDATE_PULLREQUEST = "${url('pullrequest_update',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}"
144 AJAX_UPDATE_PULLREQUEST = "${url('pullrequest_update',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}"
145 </script>
145 </script>
146
146
147 ## diff block
147 ## diff block
148 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
148 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
149 %for fid, change, f, stat in c.files:
149 %for fid, change, f, stat in c.files:
150 ${diff_block.diff_block_simple([c.changes[fid]])}
150 ${diff_block.diff_block_simple([c.changes[fid]])}
151 %endfor
151 %endfor
152 % if c.limited_diff:
152 % if c.limited_diff:
153 <h4>${_('Changeset was too big and was cut off...')}</h4>
153 <h4>${_('Changeset was too big and was cut off...')}</h4>
154 % endif
154 % endif
155
155
156
156
157 ## template for inline comment form
157 ## template for inline comment form
158 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
158 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
159 ${comment.comment_inline_form()}
159 ${comment.comment_inline_form()}
160
160
161 ## render comments and inlines
161 ## render comments and inlines
162 ${comment.generate_comments()}
162 ${comment.generate_comments()}
163
163
164 % if not c.pull_request.is_closed():
164 % if not c.pull_request.is_closed():
165 ## main comment form and it status
165 ## main comment form and it status
166 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
166 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
167 pull_request_id=c.pull_request.pull_request_id),
167 pull_request_id=c.pull_request.pull_request_id),
168 c.current_changeset_status,
168 c.current_changeset_status,
169 close_btn=True)}
169 close_btn=True, change_status=c.allowed_to_change_status)}
170 %endif
170 %endif
171
171
172 <script type="text/javascript">
172 <script type="text/javascript">
173 YUE.onDOMReady(function(){
173 YUE.onDOMReady(function(){
174 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
174 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
175
175
176 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
176 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
177 var show = 'none';
177 var show = 'none';
178 var target = e.currentTarget;
178 var target = e.currentTarget;
179 if(target.checked){
179 if(target.checked){
180 var show = ''
180 var show = ''
181 }
181 }
182 var boxid = YUD.getAttribute(target,'id_for');
182 var boxid = YUD.getAttribute(target,'id_for');
183 var comments = YUQ('#{0} .inline-comments'.format(boxid));
183 var comments = YUQ('#{0} .inline-comments'.format(boxid));
184 for(c in comments){
184 for(c in comments){
185 YUD.setStyle(comments[c],'display',show);
185 YUD.setStyle(comments[c],'display',show);
186 }
186 }
187 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
187 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
188 for(c in btns){
188 for(c in btns){
189 YUD.setStyle(btns[c],'display',show);
189 YUD.setStyle(btns[c],'display',show);
190 }
190 }
191 })
191 })
192
192
193 YUE.on(YUQ('.line'),'click',function(e){
193 YUE.on(YUQ('.line'),'click',function(e){
194 var tr = e.currentTarget;
194 var tr = e.currentTarget;
195 injectInlineForm(tr);
195 injectInlineForm(tr);
196 });
196 });
197
197
198 // inject comments into they proper positions
198 // inject comments into they proper positions
199 var file_comments = YUQ('.inline-comment-placeholder');
199 var file_comments = YUQ('.inline-comment-placeholder');
200 renderInlineComments(file_comments);
200 renderInlineComments(file_comments);
201
201
202 YUE.on(YUD.get('update_pull_request'),'click',function(e){
202 YUE.on(YUD.get('update_pull_request'),'click',function(e){
203
203
204 var reviewers_ids = [];
204 var reviewers_ids = [];
205 var ids = YUQ('#review_members input');
205 var ids = YUQ('#review_members input');
206 for(var i=0; i<ids.length;i++){
206 for(var i=0; i<ids.length;i++){
207 var id = ids[i].value
207 var id = ids[i].value
208 reviewers_ids.push(id);
208 reviewers_ids.push(id);
209 }
209 }
210 updateReviewers(reviewers_ids);
210 updateReviewers(reviewers_ids);
211 })
211 })
212 })
212 })
213 </script>
213 </script>
214
214
215 </div>
215 </div>
216
216
217 </%def>
217 </%def>
General Comments 0
You need to be logged in to leave comments. Login now