##// END OF EJS Templates
cleanup: use obj.foo_id instead of obj.foo.foo_id...
Søren Løvborg -
r6197:e99a33d7 default
parent child Browse files
Show More
@@ -1,140 +1,140 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.notifications
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 notifications controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Nov 23, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30
31 31 from pylons import request
32 32 from pylons import tmpl_context as c
33 33 from webob.exc import HTTPBadRequest, HTTPForbidden
34 34
35 35 from kallithea.model.db import Notification
36 36 from kallithea.model.notification import NotificationModel
37 37 from kallithea.model.meta import Session
38 38 from kallithea.lib.auth import LoginRequired, NotAnonymous
39 39 from kallithea.lib.base import BaseController, render
40 40 from kallithea.lib import helpers as h
41 41 from kallithea.lib.helpers import Page
42 42 from kallithea.lib.utils2 import safe_int
43 43
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class NotificationsController(BaseController):
49 49 """REST Controller styled on the Atom Publishing Protocol"""
50 50 # To properly map this controller, ensure your config/routing.py
51 51 # file has a resource setup:
52 52 # map.resource('notification', 'notifications', controller='_admin/notifications',
53 53 # path_prefix='/_admin', name_prefix='_admin_')
54 54
55 55 @LoginRequired()
56 56 @NotAnonymous()
57 57 def __before__(self):
58 58 super(NotificationsController, self).__before__()
59 59
60 60 def index(self, format='html'):
61 61 c.user = self.authuser
62 62 notif = NotificationModel().query_for_user(self.authuser.user_id,
63 63 filter_=request.GET.getall('type'))
64 64
65 65 p = safe_int(request.GET.get('page'), 1)
66 66 c.notifications = Page(notif, page=p, items_per_page=10)
67 67 c.pull_request_type = Notification.TYPE_PULL_REQUEST
68 68 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
69 69 Notification.TYPE_PULL_REQUEST_COMMENT]
70 70
71 71 _current_filter = request.GET.getall('type')
72 72 c.current_filter = 'all'
73 73 if _current_filter == [c.pull_request_type]:
74 74 c.current_filter = 'pull_request'
75 75 elif _current_filter == c.comment_type:
76 76 c.current_filter = 'comment'
77 77
78 78 return render('admin/notifications/notifications.html')
79 79
80 80 def mark_all_read(self):
81 81 if request.environ.get('HTTP_X_PARTIAL_XHR'):
82 82 nm = NotificationModel()
83 83 # mark all read
84 84 nm.mark_all_read_for_user(self.authuser.user_id,
85 85 filter_=request.GET.getall('type'))
86 86 Session().commit()
87 87 c.user = self.authuser
88 88 notif = nm.query_for_user(self.authuser.user_id,
89 89 filter_=request.GET.getall('type'))
90 90 c.notifications = Page(notif, page=1, items_per_page=10)
91 91 return render('admin/notifications/notifications_data.html')
92 92
93 93 def update(self, notification_id):
94 94 try:
95 95 no = Notification.get(notification_id)
96 owner = all(un.user.user_id == c.authuser.user_id
96 owner = all(un.user_id == c.authuser.user_id
97 97 for un in no.notifications_to_users)
98 98 if h.HasPermissionAny('hg.admin')() or owner:
99 99 # deletes only notification2user
100 100 NotificationModel().mark_read(c.authuser.user_id, no)
101 101 Session().commit()
102 102 return 'ok'
103 103 except Exception:
104 104 Session().rollback()
105 105 log.error(traceback.format_exc())
106 106 raise HTTPBadRequest()
107 107
108 108 def delete(self, notification_id):
109 109 try:
110 110 no = Notification.get(notification_id)
111 owner = any(un.user.user_id == c.authuser.user_id
111 owner = any(un.user_id == c.authuser.user_id
112 112 for un in no.notifications_to_users)
113 113 if h.HasPermissionAny('hg.admin')() or owner:
114 114 # deletes only notification2user
115 115 NotificationModel().delete(c.authuser.user_id, no)
116 116 Session().commit()
117 117 return 'ok'
118 118 except Exception:
119 119 Session().rollback()
120 120 log.error(traceback.format_exc())
121 121 raise HTTPBadRequest()
122 122
123 123 def show(self, notification_id, format='html'):
124 124 notification = Notification.get_or_404(notification_id)
125 125
126 126 unotification = NotificationModel() \
127 127 .get_user_notification(self.authuser.user_id, notification)
128 128
129 129 # if this association to user is not valid, we don't want to show
130 130 # this message
131 131 if unotification is None:
132 132 raise HTTPForbidden()
133 133
134 134 if not unotification.read:
135 135 unotification.mark_as_read()
136 136 Session().commit()
137 137
138 138 c.notification = notification
139 139 c.user = self.authuser
140 140 return render('admin/notifications/show_notification.html')
@@ -1,589 +1,589 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.repos
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Repositories controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 7, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31 from formencode import htmlfill
32 32 from pylons import request, tmpl_context as c
33 33 from pylons.i18n.translation import _
34 34 from sqlalchemy.sql.expression import func
35 35 from webob.exc import HTTPFound, HTTPInternalServerError, HTTPForbidden, HTTPNotFound
36 36
37 37 from kallithea.config.routing import url
38 38 from kallithea.lib import helpers as h
39 39 from kallithea.lib.auth import LoginRequired, \
40 40 HasRepoPermissionAnyDecorator, NotAnonymous, HasPermissionAny
41 41 from kallithea.lib.base import BaseRepoController, render
42 42 from kallithea.lib.utils import action_logger, jsonify
43 43 from kallithea.lib.vcs import RepositoryError
44 44 from kallithea.model.meta import Session
45 45 from kallithea.model.db import User, Repository, UserFollowing, RepoGroup, \
46 46 Setting, RepositoryField
47 47 from kallithea.model.forms import RepoForm, RepoFieldForm, RepoPermsForm
48 48 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices, RepoList
49 49 from kallithea.model.repo import RepoModel
50 50 from kallithea.lib.compat import json
51 51 from kallithea.lib.exceptions import AttachedForksError
52 52 from kallithea.lib.utils2 import safe_int
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class ReposController(BaseRepoController):
58 58 """
59 59 REST Controller styled on the Atom Publishing Protocol"""
60 60 # To properly map this controller, ensure your config/routing.py
61 61 # file has a resource setup:
62 62 # map.resource('repo', 'repos')
63 63
64 64 @LoginRequired()
65 65 def __before__(self):
66 66 super(ReposController, self).__before__()
67 67
68 68 def _load_repo(self):
69 69 repo_obj = c.db_repo
70 70
71 71 if repo_obj is None:
72 72 h.not_mapped_error(c.repo_name)
73 73 raise HTTPFound(location=url('repos'))
74 74
75 75 return repo_obj
76 76
77 77 def __load_defaults(self, repo=None):
78 78 top_perms = ['hg.create.repository']
79 79 repo_group_perms = ['group.admin']
80 80 if HasPermissionAny('hg.create.write_on_repogroup.true')():
81 81 repo_group_perms.append('group.write')
82 82 extras = [] if repo is None else [repo.group]
83 83
84 84 c.repo_groups = AvailableRepoGroupChoices(top_perms, repo_group_perms, extras)
85 85
86 86 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo)
87 87
88 88 def __load_data(self):
89 89 """
90 90 Load defaults settings for edit, and update
91 91 """
92 92 c.repo_info = self._load_repo()
93 93 self.__load_defaults(c.repo_info)
94 94
95 95 defaults = RepoModel()._get_defaults(c.repo_name)
96 96 defaults['clone_uri'] = c.repo_info.clone_uri_hidden # don't show password
97 97
98 98 return defaults
99 99
100 100 def index(self, format='html'):
101 101 _list = Repository.query(sorted=True).all()
102 102
103 103 c.repos_list = RepoList(_list, perm_set=['repository.admin'])
104 104 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
105 105 admin=True,
106 106 super_user_actions=True)
107 107 #json used to render the grid
108 108 c.data = json.dumps(repos_data)
109 109
110 110 return render('admin/repos/repos.html')
111 111
112 112 @NotAnonymous()
113 113 def create(self):
114 114 self.__load_defaults()
115 115 form_result = {}
116 116 try:
117 117 # CanWriteGroup validators checks permissions of this POST
118 118 form_result = RepoForm(repo_groups=c.repo_groups,
119 119 landing_revs=c.landing_revs_choices)() \
120 120 .to_python(dict(request.POST))
121 121
122 122 # create is done sometimes async on celery, db transaction
123 123 # management is handled there.
124 124 task = RepoModel().create(form_result, self.authuser.user_id)
125 125 task_id = task.task_id
126 126 except formencode.Invalid as errors:
127 127 log.info(errors)
128 128 return htmlfill.render(
129 129 render('admin/repos/repo_add.html'),
130 130 defaults=errors.value,
131 131 errors=errors.error_dict or {},
132 132 prefix_error=False,
133 133 force_defaults=False,
134 134 encoding="UTF-8")
135 135
136 136 except Exception:
137 137 log.error(traceback.format_exc())
138 138 msg = (_('Error creating repository %s')
139 139 % form_result.get('repo_name'))
140 140 h.flash(msg, category='error')
141 141 raise HTTPFound(location=url('home'))
142 142
143 143 raise HTTPFound(location=h.url('repo_creating_home',
144 144 repo_name=form_result['repo_name_full'],
145 145 task_id=task_id))
146 146
147 147 @NotAnonymous()
148 148 def create_repository(self):
149 149 self.__load_defaults()
150 150 if not c.repo_groups:
151 151 raise HTTPForbidden
152 152 parent_group = request.GET.get('parent_group')
153 153
154 154 ## apply the defaults from defaults page
155 155 defaults = Setting.get_default_repo_settings(strip_prefix=True)
156 156 if parent_group:
157 157 prg = RepoGroup.get(parent_group)
158 158 if prg is None or not any(rgc[0] == prg.group_id
159 159 for rgc in c.repo_groups):
160 160 raise HTTPForbidden
161 161 defaults.update({'repo_group': parent_group})
162 162
163 163 return htmlfill.render(
164 164 render('admin/repos/repo_add.html'),
165 165 defaults=defaults,
166 166 errors={},
167 167 prefix_error=False,
168 168 encoding="UTF-8",
169 169 force_defaults=False)
170 170
171 171 @LoginRequired()
172 172 @NotAnonymous()
173 173 def repo_creating(self, repo_name):
174 174 c.repo = repo_name
175 175 c.task_id = request.GET.get('task_id')
176 176 if not c.repo:
177 177 raise HTTPNotFound()
178 178 return render('admin/repos/repo_creating.html')
179 179
180 180 @LoginRequired()
181 181 @NotAnonymous()
182 182 @jsonify
183 183 def repo_check(self, repo_name):
184 184 c.repo = repo_name
185 185 task_id = request.GET.get('task_id')
186 186
187 187 if task_id and task_id not in ['None']:
188 188 from kallithea import CELERY_ON
189 189 from kallithea.lib import celerypylons
190 190 if CELERY_ON:
191 191 task = celerypylons.result.AsyncResult(task_id)
192 192 if task.failed():
193 193 raise HTTPInternalServerError(task.traceback)
194 194
195 195 repo = Repository.get_by_repo_name(repo_name)
196 196 if repo and repo.repo_state == Repository.STATE_CREATED:
197 197 if repo.clone_uri:
198 198 h.flash(_('Created repository %s from %s')
199 199 % (repo.repo_name, repo.clone_uri_hidden), category='success')
200 200 else:
201 201 repo_url = h.link_to(repo.repo_name,
202 202 h.url('summary_home',
203 203 repo_name=repo.repo_name))
204 204 fork = repo.fork
205 205 if fork is not None:
206 206 fork_name = fork.repo_name
207 207 h.flash(h.literal(_('Forked repository %s as %s')
208 208 % (fork_name, repo_url)), category='success')
209 209 else:
210 210 h.flash(h.literal(_('Created repository %s') % repo_url),
211 211 category='success')
212 212 return {'result': True}
213 213 return {'result': False}
214 214
215 215 @HasRepoPermissionAnyDecorator('repository.admin')
216 216 def update(self, repo_name):
217 217 c.repo_info = self._load_repo()
218 218 self.__load_defaults(c.repo_info)
219 219 c.active = 'settings'
220 220 c.repo_fields = RepositoryField.query() \
221 221 .filter(RepositoryField.repository == c.repo_info).all()
222 222
223 223 repo_model = RepoModel()
224 224 changed_name = repo_name
225 225 repo = Repository.get_by_repo_name(repo_name)
226 226 old_data = {
227 227 'repo_name': repo_name,
228 228 'repo_group': repo.group.get_dict() if repo.group else {},
229 229 'repo_type': repo.repo_type,
230 230 }
231 231 _form = RepoForm(edit=True, old_data=old_data,
232 232 repo_groups=c.repo_groups,
233 233 landing_revs=c.landing_revs_choices)()
234 234
235 235 try:
236 236 form_result = _form.to_python(dict(request.POST))
237 237 repo = repo_model.update(repo_name, **form_result)
238 238 ScmModel().mark_for_invalidation(repo_name)
239 239 h.flash(_('Repository %s updated successfully') % repo_name,
240 240 category='success')
241 241 changed_name = repo.repo_name
242 242 action_logger(self.authuser, 'admin_updated_repo',
243 243 changed_name, self.ip_addr, self.sa)
244 244 Session().commit()
245 245 except formencode.Invalid as errors:
246 246 log.info(errors)
247 247 defaults = self.__load_data()
248 248 defaults.update(errors.value)
249 249 c.users_array = repo_model.get_users_js()
250 250 return htmlfill.render(
251 251 render('admin/repos/repo_edit.html'),
252 252 defaults=defaults,
253 253 errors=errors.error_dict or {},
254 254 prefix_error=False,
255 255 encoding="UTF-8",
256 256 force_defaults=False)
257 257
258 258 except Exception:
259 259 log.error(traceback.format_exc())
260 260 h.flash(_('Error occurred during update of repository %s') \
261 261 % repo_name, category='error')
262 262 raise HTTPFound(location=url('edit_repo', repo_name=changed_name))
263 263
264 264 @HasRepoPermissionAnyDecorator('repository.admin')
265 265 def delete(self, repo_name):
266 266 repo_model = RepoModel()
267 267 repo = repo_model.get_by_repo_name(repo_name)
268 268 if not repo:
269 269 h.not_mapped_error(repo_name)
270 270 raise HTTPFound(location=url('repos'))
271 271 try:
272 272 _forks = repo.forks.count()
273 273 handle_forks = None
274 274 if _forks and request.POST.get('forks'):
275 275 do = request.POST['forks']
276 276 if do == 'detach_forks':
277 277 handle_forks = 'detach'
278 278 h.flash(_('Detached %s forks') % _forks, category='success')
279 279 elif do == 'delete_forks':
280 280 handle_forks = 'delete'
281 281 h.flash(_('Deleted %s forks') % _forks, category='success')
282 282 repo_model.delete(repo, forks=handle_forks)
283 283 action_logger(self.authuser, 'admin_deleted_repo',
284 284 repo_name, self.ip_addr, self.sa)
285 285 ScmModel().mark_for_invalidation(repo_name)
286 286 h.flash(_('Deleted repository %s') % repo_name, category='success')
287 287 Session().commit()
288 288 except AttachedForksError:
289 289 h.flash(_('Cannot delete repository %s which still has forks')
290 290 % repo_name, category='warning')
291 291
292 292 except Exception:
293 293 log.error(traceback.format_exc())
294 294 h.flash(_('An error occurred during deletion of %s') % repo_name,
295 295 category='error')
296 296
297 297 if repo.group:
298 298 raise HTTPFound(location=url('repos_group_home', group_name=repo.group.group_name))
299 299 raise HTTPFound(location=url('repos'))
300 300
301 301 @HasRepoPermissionAnyDecorator('repository.admin')
302 302 def edit(self, repo_name):
303 303 defaults = self.__load_data()
304 304 c.repo_fields = RepositoryField.query() \
305 305 .filter(RepositoryField.repository == c.repo_info).all()
306 306 repo_model = RepoModel()
307 307 c.users_array = repo_model.get_users_js()
308 308 c.active = 'settings'
309 309 return htmlfill.render(
310 310 render('admin/repos/repo_edit.html'),
311 311 defaults=defaults,
312 312 encoding="UTF-8",
313 313 force_defaults=False)
314 314
315 315 @HasRepoPermissionAnyDecorator('repository.admin')
316 316 def edit_permissions(self, repo_name):
317 317 c.repo_info = self._load_repo()
318 318 repo_model = RepoModel()
319 319 c.users_array = repo_model.get_users_js()
320 320 c.user_groups_array = repo_model.get_user_groups_js()
321 321 c.active = 'permissions'
322 322 defaults = RepoModel()._get_defaults(repo_name)
323 323
324 324 return htmlfill.render(
325 325 render('admin/repos/repo_edit.html'),
326 326 defaults=defaults,
327 327 encoding="UTF-8",
328 328 force_defaults=False)
329 329
330 330 def edit_permissions_update(self, repo_name):
331 331 form = RepoPermsForm()().to_python(request.POST)
332 332 RepoModel()._update_permissions(repo_name, form['perms_new'],
333 333 form['perms_updates'])
334 334 #TODO: implement this
335 335 #action_logger(self.authuser, 'admin_changed_repo_permissions',
336 336 # repo_name, self.ip_addr, self.sa)
337 337 Session().commit()
338 338 h.flash(_('Repository permissions updated'), category='success')
339 339 raise HTTPFound(location=url('edit_repo_perms', repo_name=repo_name))
340 340
341 341 def edit_permissions_revoke(self, repo_name):
342 342 try:
343 343 obj_type = request.POST.get('obj_type')
344 344 obj_id = None
345 345 if obj_type == 'user':
346 346 obj_id = safe_int(request.POST.get('user_id'))
347 347 elif obj_type == 'user_group':
348 348 obj_id = safe_int(request.POST.get('user_group_id'))
349 349
350 350 if obj_type == 'user':
351 351 RepoModel().revoke_user_permission(repo=repo_name, user=obj_id)
352 352 elif obj_type == 'user_group':
353 353 RepoModel().revoke_user_group_permission(
354 354 repo=repo_name, group_name=obj_id
355 355 )
356 356 #TODO: implement this
357 357 #action_logger(self.authuser, 'admin_revoked_repo_permissions',
358 358 # repo_name, self.ip_addr, self.sa)
359 359 Session().commit()
360 360 except Exception:
361 361 log.error(traceback.format_exc())
362 362 h.flash(_('An error occurred during revoking of permission'),
363 363 category='error')
364 364 raise HTTPInternalServerError()
365 365
366 366 @HasRepoPermissionAnyDecorator('repository.admin')
367 367 def edit_fields(self, repo_name):
368 368 c.repo_info = self._load_repo()
369 369 c.repo_fields = RepositoryField.query() \
370 370 .filter(RepositoryField.repository == c.repo_info).all()
371 371 c.active = 'fields'
372 372 if request.POST:
373 373
374 374 raise HTTPFound(location=url('repo_edit_fields'))
375 375 return render('admin/repos/repo_edit.html')
376 376
377 377 @HasRepoPermissionAnyDecorator('repository.admin')
378 378 def create_repo_field(self, repo_name):
379 379 try:
380 380 form_result = RepoFieldForm()().to_python(dict(request.POST))
381 381 new_field = RepositoryField()
382 382 new_field.repository = Repository.get_by_repo_name(repo_name)
383 383 new_field.field_key = form_result['new_field_key']
384 384 new_field.field_type = form_result['new_field_type'] # python type
385 385 new_field.field_value = form_result['new_field_value'] # set initial blank value
386 386 new_field.field_desc = form_result['new_field_desc']
387 387 new_field.field_label = form_result['new_field_label']
388 388 Session().add(new_field)
389 389 Session().commit()
390 390 except Exception as e:
391 391 log.error(traceback.format_exc())
392 392 msg = _('An error occurred during creation of field')
393 393 if isinstance(e, formencode.Invalid):
394 394 msg += ". " + e.msg
395 395 h.flash(msg, category='error')
396 396 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
397 397
398 398 @HasRepoPermissionAnyDecorator('repository.admin')
399 399 def delete_repo_field(self, repo_name, field_id):
400 400 field = RepositoryField.get_or_404(field_id)
401 401 try:
402 402 Session().delete(field)
403 403 Session().commit()
404 404 except Exception as e:
405 405 log.error(traceback.format_exc())
406 406 msg = _('An error occurred during removal of field')
407 407 h.flash(msg, category='error')
408 408 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
409 409
410 410 @HasRepoPermissionAnyDecorator('repository.admin')
411 411 def edit_advanced(self, repo_name):
412 412 c.repo_info = self._load_repo()
413 413 c.default_user_id = User.get_default_user().user_id
414 414 c.in_public_journal = UserFollowing.query() \
415 415 .filter(UserFollowing.user_id == c.default_user_id) \
416 416 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
417 417
418 418 _repos = Repository.query(sorted=True).all()
419 419 read_access_repos = RepoList(_repos)
420 420 c.repos_list = [(None, _('-- Not a fork --'))]
421 421 c.repos_list += [(x.repo_id, x.repo_name)
422 422 for x in read_access_repos
423 423 if x.repo_id != c.repo_info.repo_id]
424 424
425 425 defaults = {
426 'id_fork_of': c.repo_info.fork.repo_id if c.repo_info.fork else ''
426 'id_fork_of': c.repo_info.fork_id if c.repo_info.fork_id else ''
427 427 }
428 428
429 429 c.active = 'advanced'
430 430 if request.POST:
431 431 raise HTTPFound(location=url('repo_edit_advanced'))
432 432 return htmlfill.render(
433 433 render('admin/repos/repo_edit.html'),
434 434 defaults=defaults,
435 435 encoding="UTF-8",
436 436 force_defaults=False)
437 437
438 438 @HasRepoPermissionAnyDecorator('repository.admin')
439 439 def edit_advanced_journal(self, repo_name):
440 440 """
441 441 Sets this repository to be visible in public journal,
442 442 in other words asking default user to follow this repo
443 443
444 444 :param repo_name:
445 445 """
446 446
447 447 try:
448 448 repo_id = Repository.get_by_repo_name(repo_name).repo_id
449 449 user_id = User.get_default_user().user_id
450 450 self.scm_model.toggle_following_repo(repo_id, user_id)
451 451 h.flash(_('Updated repository visibility in public journal'),
452 452 category='success')
453 453 Session().commit()
454 454 except Exception:
455 455 h.flash(_('An error occurred during setting this'
456 456 ' repository in public journal'),
457 457 category='error')
458 458 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
459 459
460 460
461 461 @HasRepoPermissionAnyDecorator('repository.admin')
462 462 def edit_advanced_fork(self, repo_name):
463 463 """
464 464 Mark given repository as a fork of another
465 465
466 466 :param repo_name:
467 467 """
468 468 try:
469 469 fork_id = request.POST.get('id_fork_of')
470 470 repo = ScmModel().mark_as_fork(repo_name, fork_id,
471 471 self.authuser.username)
472 472 fork = repo.fork.repo_name if repo.fork else _('Nothing')
473 473 Session().commit()
474 474 h.flash(_('Marked repository %s as fork of %s') % (repo_name, fork),
475 475 category='success')
476 476 except RepositoryError as e:
477 477 log.error(traceback.format_exc())
478 478 h.flash(str(e), category='error')
479 479 except Exception as e:
480 480 log.error(traceback.format_exc())
481 481 h.flash(_('An error occurred during this operation'),
482 482 category='error')
483 483
484 484 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
485 485
486 486 @HasRepoPermissionAnyDecorator('repository.admin')
487 487 def edit_advanced_locking(self, repo_name):
488 488 """
489 489 Unlock repository when it is locked !
490 490
491 491 :param repo_name:
492 492 """
493 493 try:
494 494 repo = Repository.get_by_repo_name(repo_name)
495 495 if request.POST.get('set_lock'):
496 496 Repository.lock(repo, c.authuser.user_id)
497 497 h.flash(_('Repository has been locked'), category='success')
498 498 elif request.POST.get('set_unlock'):
499 499 Repository.unlock(repo)
500 500 h.flash(_('Repository has been unlocked'), category='success')
501 501 except Exception as e:
502 502 log.error(traceback.format_exc())
503 503 h.flash(_('An error occurred during unlocking'),
504 504 category='error')
505 505 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
506 506
507 507 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
508 508 def toggle_locking(self, repo_name):
509 509 try:
510 510 repo = Repository.get_by_repo_name(repo_name)
511 511
512 512 if repo.enable_locking:
513 513 if repo.locked[0]:
514 514 Repository.unlock(repo)
515 515 h.flash(_('Repository has been unlocked'), category='success')
516 516 else:
517 517 Repository.lock(repo, c.authuser.user_id)
518 518 h.flash(_('Repository has been locked'), category='success')
519 519
520 520 except Exception as e:
521 521 log.error(traceback.format_exc())
522 522 h.flash(_('An error occurred during unlocking'),
523 523 category='error')
524 524 raise HTTPFound(location=url('summary_home', repo_name=repo_name))
525 525
526 526 @HasRepoPermissionAnyDecorator('repository.admin')
527 527 def edit_caches(self, repo_name):
528 528 c.repo_info = self._load_repo()
529 529 c.active = 'caches'
530 530 if request.POST:
531 531 try:
532 532 ScmModel().mark_for_invalidation(repo_name)
533 533 Session().commit()
534 534 h.flash(_('Cache invalidation successful'),
535 535 category='success')
536 536 except Exception as e:
537 537 log.error(traceback.format_exc())
538 538 h.flash(_('An error occurred during cache invalidation'),
539 539 category='error')
540 540
541 541 raise HTTPFound(location=url('edit_repo_caches', repo_name=c.repo_name))
542 542 return render('admin/repos/repo_edit.html')
543 543
544 544 @HasRepoPermissionAnyDecorator('repository.admin')
545 545 def edit_remote(self, repo_name):
546 546 c.repo_info = self._load_repo()
547 547 c.active = 'remote'
548 548 if request.POST:
549 549 try:
550 550 ScmModel().pull_changes(repo_name, self.authuser.username)
551 551 h.flash(_('Pulled from remote location'), category='success')
552 552 except Exception as e:
553 553 log.error(traceback.format_exc())
554 554 h.flash(_('An error occurred during pull from remote location'),
555 555 category='error')
556 556 raise HTTPFound(location=url('edit_repo_remote', repo_name=c.repo_name))
557 557 return render('admin/repos/repo_edit.html')
558 558
559 559 @HasRepoPermissionAnyDecorator('repository.admin')
560 560 def edit_statistics(self, repo_name):
561 561 c.repo_info = self._load_repo()
562 562 repo = c.repo_info.scm_instance
563 563
564 564 if c.repo_info.stats:
565 565 # this is on what revision we ended up so we add +1 for count
566 566 last_rev = c.repo_info.stats.stat_on_revision + 1
567 567 else:
568 568 last_rev = 0
569 569 c.stats_revision = last_rev
570 570
571 571 c.repo_last_rev = repo.count() if repo.revisions else 0
572 572
573 573 if last_rev == 0 or c.repo_last_rev == 0:
574 574 c.stats_percentage = 0
575 575 else:
576 576 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
577 577
578 578 c.active = 'statistics'
579 579 if request.POST:
580 580 try:
581 581 RepoModel().delete_stats(repo_name)
582 582 Session().commit()
583 583 except Exception as e:
584 584 log.error(traceback.format_exc())
585 585 h.flash(_('An error occurred during deletion of repository stats'),
586 586 category='error')
587 587 raise HTTPFound(location=url('edit_repo_statistics', repo_name=c.repo_name))
588 588
589 589 return render('admin/repos/repo_edit.html')
@@ -1,473 +1,473 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.changeset
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 changeset controller showing changes between revisions
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 25, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 from collections import defaultdict
31 31
32 32 from pylons import tmpl_context as c, request, response
33 33 from pylons.i18n.translation import _
34 34 from webob.exc import HTTPFound, HTTPForbidden, HTTPBadRequest, HTTPNotFound
35 35
36 36 from kallithea.lib.utils import jsonify
37 37 from kallithea.lib.vcs.exceptions import RepositoryError, \
38 38 ChangesetDoesNotExistError, EmptyRepositoryError
39 39
40 40 from kallithea.lib.compat import json
41 41 import kallithea.lib.helpers as h
42 42 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
43 43 NotAnonymous
44 44 from kallithea.lib.base import BaseRepoController, render
45 45 from kallithea.lib.utils import action_logger
46 46 from kallithea.lib.compat import OrderedDict
47 47 from kallithea.lib import diffs
48 48 from kallithea.model.db import ChangesetComment, ChangesetStatus
49 49 from kallithea.model.comment import ChangesetCommentsModel
50 50 from kallithea.model.changeset_status import ChangesetStatusModel
51 51 from kallithea.model.meta import Session
52 52 from kallithea.model.repo import RepoModel
53 53 from kallithea.lib.diffs import LimitedDiffContainer
54 54 from kallithea.lib.exceptions import StatusChangeOnClosedPullRequestError
55 55 from kallithea.lib.vcs.backends.base import EmptyChangeset
56 56 from kallithea.lib.utils2 import safe_unicode
57 57 from kallithea.lib.graphmod import graph_data
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 def _update_with_GET(params, GET):
63 63 for k in ['diff1', 'diff2', 'diff']:
64 64 params[k] += GET.getall(k)
65 65
66 66
67 67 def anchor_url(revision, path, GET):
68 68 fid = h.FID(revision, path)
69 69 return h.url.current(anchor=fid, **dict(GET))
70 70
71 71
72 72 def get_ignore_ws(fid, GET):
73 73 ig_ws_global = GET.get('ignorews')
74 74 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
75 75 if ig_ws:
76 76 try:
77 77 return int(ig_ws[0].split(':')[-1])
78 78 except ValueError:
79 79 raise HTTPBadRequest()
80 80 return ig_ws_global
81 81
82 82
83 83 def _ignorews_url(GET, fileid=None):
84 84 fileid = str(fileid) if fileid else None
85 85 params = defaultdict(list)
86 86 _update_with_GET(params, GET)
87 87 lbl = _('Show whitespace')
88 88 ig_ws = get_ignore_ws(fileid, GET)
89 89 ln_ctx = get_line_ctx(fileid, GET)
90 90 # global option
91 91 if fileid is None:
92 92 if ig_ws is None:
93 93 params['ignorews'] += [1]
94 94 lbl = _('Ignore whitespace')
95 95 ctx_key = 'context'
96 96 ctx_val = ln_ctx
97 97 # per file options
98 98 else:
99 99 if ig_ws is None:
100 100 params[fileid] += ['WS:1']
101 101 lbl = _('Ignore whitespace')
102 102
103 103 ctx_key = fileid
104 104 ctx_val = 'C:%s' % ln_ctx
105 105 # if we have passed in ln_ctx pass it along to our params
106 106 if ln_ctx:
107 107 params[ctx_key] += [ctx_val]
108 108
109 109 params['anchor'] = fileid
110 110 icon = h.literal('<i class="icon-strike"></i>')
111 111 return h.link_to(icon, h.url.current(**params), title=lbl, class_='tooltip')
112 112
113 113
114 114 def get_line_ctx(fid, GET):
115 115 ln_ctx_global = GET.get('context')
116 116 if fid:
117 117 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
118 118 else:
119 119 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
120 120 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
121 121 if ln_ctx:
122 122 ln_ctx = [ln_ctx]
123 123
124 124 if ln_ctx:
125 125 retval = ln_ctx[0].split(':')[-1]
126 126 else:
127 127 retval = ln_ctx_global
128 128
129 129 try:
130 130 return int(retval)
131 131 except Exception:
132 132 return 3
133 133
134 134
135 135 def _context_url(GET, fileid=None):
136 136 """
137 137 Generates url for context lines
138 138
139 139 :param fileid:
140 140 """
141 141
142 142 fileid = str(fileid) if fileid else None
143 143 ig_ws = get_ignore_ws(fileid, GET)
144 144 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
145 145
146 146 params = defaultdict(list)
147 147 _update_with_GET(params, GET)
148 148
149 149 # global option
150 150 if fileid is None:
151 151 if ln_ctx > 0:
152 152 params['context'] += [ln_ctx]
153 153
154 154 if ig_ws:
155 155 ig_ws_key = 'ignorews'
156 156 ig_ws_val = 1
157 157
158 158 # per file option
159 159 else:
160 160 params[fileid] += ['C:%s' % ln_ctx]
161 161 ig_ws_key = fileid
162 162 ig_ws_val = 'WS:%s' % 1
163 163
164 164 if ig_ws:
165 165 params[ig_ws_key] += [ig_ws_val]
166 166
167 167 lbl = _('Increase diff context to %(num)s lines') % {'num': ln_ctx}
168 168
169 169 params['anchor'] = fileid
170 170 icon = h.literal('<i class="icon-sort"></i>')
171 171 return h.link_to(icon, h.url.current(**params), title=lbl, class_='tooltip')
172 172
173 173
174 174 # Could perhaps be nice to have in the model but is too high level ...
175 175 def create_comment(text, status, f_path, line_no, revision=None, pull_request_id=None, closing_pr=None):
176 176 """Comment functionality shared between changesets and pullrequests"""
177 177 f_path = f_path or None
178 178 line_no = line_no or None
179 179
180 180 comment = ChangesetCommentsModel().create(
181 181 text=text,
182 182 repo=c.db_repo.repo_id,
183 183 author=c.authuser.user_id,
184 184 revision=revision,
185 185 pull_request=pull_request_id,
186 186 f_path=f_path,
187 187 line_no=line_no,
188 188 status_change=ChangesetStatus.get_status_lbl(status) if status else None,
189 189 closing_pr=closing_pr,
190 190 )
191 191
192 192 return comment
193 193
194 194
195 195 class ChangesetController(BaseRepoController):
196 196
197 197 def __before__(self):
198 198 super(ChangesetController, self).__before__()
199 199 c.affected_files_cut_off = 60
200 200
201 201 def __load_data(self):
202 202 repo_model = RepoModel()
203 203 c.users_array = repo_model.get_users_js()
204 204 c.user_groups_array = repo_model.get_user_groups_js()
205 205
206 206 def _index(self, revision, method):
207 207 c.pull_request = None
208 208 c.anchor_url = anchor_url
209 209 c.ignorews_url = _ignorews_url
210 210 c.context_url = _context_url
211 211 c.fulldiff = fulldiff = request.GET.get('fulldiff')
212 212 #get ranges of revisions if preset
213 213 rev_range = revision.split('...')[:2]
214 214 enable_comments = True
215 215 c.cs_repo = c.db_repo
216 216 try:
217 217 if len(rev_range) == 2:
218 218 enable_comments = False
219 219 rev_start = rev_range[0]
220 220 rev_end = rev_range[1]
221 221 rev_ranges = c.db_repo_scm_instance.get_changesets(start=rev_start,
222 222 end=rev_end)
223 223 else:
224 224 rev_ranges = [c.db_repo_scm_instance.get_changeset(revision)]
225 225
226 226 c.cs_ranges = list(rev_ranges)
227 227 if not c.cs_ranges:
228 228 raise RepositoryError('Changeset range returned empty result')
229 229
230 230 except (ChangesetDoesNotExistError, EmptyRepositoryError):
231 231 log.debug(traceback.format_exc())
232 232 msg = _('Such revision does not exist for this repository')
233 233 h.flash(msg, category='error')
234 234 raise HTTPNotFound()
235 235
236 236 c.changes = OrderedDict()
237 237
238 238 c.lines_added = 0 # count of lines added
239 239 c.lines_deleted = 0 # count of lines removes
240 240
241 241 c.changeset_statuses = ChangesetStatus.STATUSES
242 242 comments = dict()
243 243 c.statuses = []
244 244 c.inline_comments = []
245 245 c.inline_cnt = 0
246 246
247 247 # Iterate over ranges (default changeset view is always one changeset)
248 248 for changeset in c.cs_ranges:
249 249 if method == 'show':
250 250 c.statuses.extend([ChangesetStatusModel().get_status(
251 251 c.db_repo.repo_id, changeset.raw_id)])
252 252
253 253 # Changeset comments
254 254 comments.update((com.comment_id, com)
255 255 for com in ChangesetCommentsModel()
256 256 .get_comments(c.db_repo.repo_id,
257 257 revision=changeset.raw_id))
258 258
259 259 # Status change comments - mostly from pull requests
260 260 comments.update((st.changeset_comment_id, st.comment)
261 261 for st in ChangesetStatusModel()
262 262 .get_statuses(c.db_repo.repo_id,
263 263 changeset.raw_id, with_revisions=True)
264 264 if st.changeset_comment_id is not None)
265 265
266 266 inlines = ChangesetCommentsModel() \
267 267 .get_inline_comments(c.db_repo.repo_id,
268 268 revision=changeset.raw_id)
269 269 c.inline_comments.extend(inlines)
270 270
271 271 cs2 = changeset.raw_id
272 272 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset().raw_id
273 273 context_lcl = get_line_ctx('', request.GET)
274 274 ign_whitespace_lcl = get_ignore_ws('', request.GET)
275 275
276 276 _diff = c.db_repo_scm_instance.get_diff(cs1, cs2,
277 277 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
278 278 diff_limit = self.cut_off_limit if not fulldiff else None
279 279 diff_processor = diffs.DiffProcessor(_diff,
280 280 vcs=c.db_repo_scm_instance.alias,
281 281 format='gitdiff',
282 282 diff_limit=diff_limit)
283 283 file_diff_data = OrderedDict()
284 284 if method == 'show':
285 285 _parsed = diff_processor.prepare()
286 286 c.limited_diff = False
287 287 if isinstance(_parsed, LimitedDiffContainer):
288 288 c.limited_diff = True
289 289 for f in _parsed:
290 290 st = f['stats']
291 291 c.lines_added += st['added']
292 292 c.lines_deleted += st['deleted']
293 293 filename = f['filename']
294 294 fid = h.FID(changeset.raw_id, filename)
295 295 url_fid = h.FID('', filename)
296 296 diff = diff_processor.as_html(enable_comments=enable_comments,
297 297 parsed_lines=[f])
298 298 file_diff_data[fid] = (url_fid, f['operation'], f['old_filename'], filename, diff, st)
299 299 else:
300 300 # downloads/raw we only need RAW diff nothing else
301 301 diff = diff_processor.as_raw()
302 302 file_diff_data[''] = (None, None, None, diff, None)
303 303 c.changes[changeset.raw_id] = (cs1, cs2, file_diff_data)
304 304
305 305 #sort comments in creation order
306 306 c.comments = [com for com_id, com in sorted(comments.items())]
307 307
308 308 # count inline comments
309 309 for __, lines in c.inline_comments:
310 310 for comments in lines.values():
311 311 c.inline_cnt += len(comments)
312 312
313 313 if len(c.cs_ranges) == 1:
314 314 c.changeset = c.cs_ranges[0]
315 315 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
316 316 for x in c.changeset.parents])
317 317 if method == 'download':
318 318 response.content_type = 'text/plain'
319 319 response.content_disposition = 'attachment; filename=%s.diff' \
320 320 % revision[:12]
321 321 return diff
322 322 elif method == 'patch':
323 323 response.content_type = 'text/plain'
324 324 c.diff = safe_unicode(diff)
325 325 return render('changeset/patch_changeset.html')
326 326 elif method == 'raw':
327 327 response.content_type = 'text/plain'
328 328 return diff
329 329 elif method == 'show':
330 330 self.__load_data()
331 331 if len(c.cs_ranges) == 1:
332 332 return render('changeset/changeset.html')
333 333 else:
334 334 c.cs_ranges_org = None
335 335 c.cs_comments = {}
336 336 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
337 337 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
338 338 return render('changeset/changeset_range.html')
339 339
340 340 @LoginRequired()
341 341 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
342 342 'repository.admin')
343 343 def index(self, revision, method='show'):
344 344 return self._index(revision, method=method)
345 345
346 346 @LoginRequired()
347 347 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
348 348 'repository.admin')
349 349 def changeset_raw(self, revision):
350 350 return self._index(revision, method='raw')
351 351
352 352 @LoginRequired()
353 353 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
354 354 'repository.admin')
355 355 def changeset_patch(self, revision):
356 356 return self._index(revision, method='patch')
357 357
358 358 @LoginRequired()
359 359 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
360 360 'repository.admin')
361 361 def changeset_download(self, revision):
362 362 return self._index(revision, method='download')
363 363
364 364 @LoginRequired()
365 365 @NotAnonymous()
366 366 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
367 367 'repository.admin')
368 368 @jsonify
369 369 def comment(self, repo_name, revision):
370 370 assert request.environ.get('HTTP_X_PARTIAL_XHR')
371 371
372 372 status = request.POST.get('changeset_status')
373 373 text = request.POST.get('text', '').strip()
374 374
375 375 c.comment = create_comment(
376 376 text,
377 377 status,
378 378 revision=revision,
379 379 f_path=request.POST.get('f_path'),
380 380 line_no=request.POST.get('line'),
381 381 )
382 382
383 383 # get status if set !
384 384 if status:
385 385 # if latest status was from pull request and it's closed
386 386 # disallow changing status ! RLY?
387 387 try:
388 388 ChangesetStatusModel().set_status(
389 389 c.db_repo.repo_id,
390 390 status,
391 391 c.authuser.user_id,
392 392 c.comment,
393 393 revision=revision,
394 394 dont_allow_on_closed_pull_request=True,
395 395 )
396 396 except StatusChangeOnClosedPullRequestError:
397 397 log.debug('cannot change status on %s with closed pull request', revision)
398 398 raise HTTPBadRequest()
399 399
400 400 action_logger(self.authuser,
401 401 'user_commented_revision:%s' % revision,
402 402 c.db_repo, self.ip_addr, self.sa)
403 403
404 404 Session().commit()
405 405
406 406 data = {
407 407 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
408 408 }
409 409 if c.comment is not None:
410 410 data.update(c.comment.get_dict())
411 411 data.update({'rendered_text':
412 412 render('changeset/changeset_comment_block.html')})
413 413
414 414 return data
415 415
416 416 @LoginRequired()
417 417 @NotAnonymous()
418 418 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
419 419 'repository.admin')
420 420 @jsonify
421 421 def delete_comment(self, repo_name, comment_id):
422 422 co = ChangesetComment.get_or_404(comment_id)
423 423 if co.repo.repo_name != repo_name:
424 424 raise HTTPNotFound()
425 owner = co.author.user_id == c.authuser.user_id
425 owner = co.author_id == c.authuser.user_id
426 426 repo_admin = h.HasRepoPermissionAny('repository.admin')(repo_name)
427 427 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
428 428 ChangesetCommentsModel().delete(comment=co)
429 429 Session().commit()
430 430 return True
431 431 else:
432 432 raise HTTPForbidden()
433 433
434 434 @LoginRequired()
435 435 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
436 436 'repository.admin')
437 437 @jsonify
438 438 def changeset_info(self, repo_name, revision):
439 439 if request.is_xhr:
440 440 try:
441 441 return c.db_repo_scm_instance.get_changeset(revision)
442 442 except ChangesetDoesNotExistError as e:
443 443 return EmptyChangeset(message=str(e))
444 444 else:
445 445 raise HTTPBadRequest()
446 446
447 447 @LoginRequired()
448 448 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
449 449 'repository.admin')
450 450 @jsonify
451 451 def changeset_children(self, repo_name, revision):
452 452 if request.is_xhr:
453 453 changeset = c.db_repo_scm_instance.get_changeset(revision)
454 454 result = {"results": []}
455 455 if changeset.children:
456 456 result = {"results": changeset.children}
457 457 return result
458 458 else:
459 459 raise HTTPBadRequest()
460 460
461 461 @LoginRequired()
462 462 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
463 463 'repository.admin')
464 464 @jsonify
465 465 def changeset_parents(self, repo_name, revision):
466 466 if request.is_xhr:
467 467 changeset = c.db_repo_scm_instance.get_changeset(revision)
468 468 result = {"results": []}
469 469 if changeset.parents:
470 470 result = {"results": changeset.parents}
471 471 return result
472 472 else:
473 473 raise HTTPBadRequest()
@@ -1,370 +1,370 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.journal
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Journal controller
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Nov 21, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26
27 27 """
28 28
29 29 import logging
30 30 import traceback
31 31 from itertools import groupby
32 32
33 33 from sqlalchemy import or_
34 34 from sqlalchemy.orm import joinedload
35 35 from sqlalchemy.sql.expression import func
36 36
37 37 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
38 38
39 39 from webob.exc import HTTPBadRequest
40 40 from pylons import request, tmpl_context as c, response
41 41 from pylons.i18n.translation import _
42 42
43 43 from kallithea.config.routing import url
44 44 from kallithea.controllers.admin.admin import _journal_filter
45 45 from kallithea.model.db import UserLog, UserFollowing, Repository, User
46 46 from kallithea.model.meta import Session
47 47 from kallithea.model.repo import RepoModel
48 48 import kallithea.lib.helpers as h
49 49 from kallithea.lib.helpers import Page
50 50 from kallithea.lib.auth import LoginRequired, NotAnonymous
51 51 from kallithea.lib.base import BaseController, render
52 52 from kallithea.lib.utils2 import safe_int, AttributeDict
53 53 from kallithea.lib.compat import json
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class JournalController(BaseController):
59 59
60 60 def __before__(self):
61 61 super(JournalController, self).__before__()
62 62 self.language = 'en-us'
63 63 self.ttl = "5"
64 64 self.feed_nr = 20
65 65 c.search_term = request.GET.get('filter')
66 66
67 67 def _get_daily_aggregate(self, journal):
68 68 groups = []
69 69 for k, g in groupby(journal, lambda x: x.action_as_day):
70 70 user_group = []
71 71 #groupby username if it's a present value, else fallback to journal username
72 72 for _unused, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
73 73 l = list(g2)
74 74 user_group.append((l[0].user, l))
75 75
76 76 groups.append((k, user_group,))
77 77
78 78 return groups
79 79
80 80 def _get_journal_data(self, following_repos):
81 repo_ids = [x.follows_repository.repo_id for x in following_repos
82 if x.follows_repository is not None]
83 user_ids = [x.follows_user.user_id for x in following_repos
84 if x.follows_user is not None]
81 repo_ids = [x.follows_repo_id for x in following_repos
82 if x.follows_repo_id is not None]
83 user_ids = [x.follows_user_id for x in following_repos
84 if x.follows_user_id is not None]
85 85
86 86 filtering_criterion = None
87 87
88 88 if repo_ids and user_ids:
89 89 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
90 90 UserLog.user_id.in_(user_ids))
91 91 if repo_ids and not user_ids:
92 92 filtering_criterion = UserLog.repository_id.in_(repo_ids)
93 93 if not repo_ids and user_ids:
94 94 filtering_criterion = UserLog.user_id.in_(user_ids)
95 95 if filtering_criterion is not None:
96 96 journal = self.sa.query(UserLog) \
97 97 .options(joinedload(UserLog.user)) \
98 98 .options(joinedload(UserLog.repository))
99 99 #filter
100 100 journal = _journal_filter(journal, c.search_term)
101 101 journal = journal.filter(filtering_criterion) \
102 102 .order_by(UserLog.action_date.desc())
103 103 else:
104 104 journal = []
105 105
106 106 return journal
107 107
108 108 def _atom_feed(self, repos, public=True):
109 109 journal = self._get_journal_data(repos)
110 110 if public:
111 111 _link = h.canonical_url('public_journal_atom')
112 112 _desc = '%s %s %s' % (c.site_name, _('Public Journal'),
113 113 'atom feed')
114 114 else:
115 115 _link = h.canonical_url('journal_atom')
116 116 _desc = '%s %s %s' % (c.site_name, _('Journal'), 'atom feed')
117 117
118 118 feed = Atom1Feed(title=_desc,
119 119 link=_link,
120 120 description=_desc,
121 121 language=self.language,
122 122 ttl=self.ttl)
123 123
124 124 for entry in journal[:self.feed_nr]:
125 125 user = entry.user
126 126 if user is None:
127 127 #fix deleted users
128 128 user = AttributeDict({'short_contact': entry.username,
129 129 'email': '',
130 130 'full_contact': ''})
131 131 action, action_extra, ico = h.action_parser(entry, feed=True)
132 132 title = "%s - %s %s" % (user.short_contact, action(),
133 133 entry.repository.repo_name)
134 134 desc = action_extra()
135 135 _url = None
136 136 if entry.repository is not None:
137 137 _url = h.canonical_url('changelog_home',
138 138 repo_name=entry.repository.repo_name)
139 139
140 140 feed.add_item(title=title,
141 141 pubdate=entry.action_date,
142 142 link=_url or h.canonical_url(''),
143 143 author_email=user.email,
144 144 author_name=user.full_contact,
145 145 description=desc)
146 146
147 147 response.content_type = feed.mime_type
148 148 return feed.writeString('utf-8')
149 149
150 150 def _rss_feed(self, repos, public=True):
151 151 journal = self._get_journal_data(repos)
152 152 if public:
153 153 _link = h.canonical_url('public_journal_atom')
154 154 _desc = '%s %s %s' % (c.site_name, _('Public Journal'),
155 155 'rss feed')
156 156 else:
157 157 _link = h.canonical_url('journal_atom')
158 158 _desc = '%s %s %s' % (c.site_name, _('Journal'), 'rss feed')
159 159
160 160 feed = Rss201rev2Feed(title=_desc,
161 161 link=_link,
162 162 description=_desc,
163 163 language=self.language,
164 164 ttl=self.ttl)
165 165
166 166 for entry in journal[:self.feed_nr]:
167 167 user = entry.user
168 168 if user is None:
169 169 #fix deleted users
170 170 user = AttributeDict({'short_contact': entry.username,
171 171 'email': '',
172 172 'full_contact': ''})
173 173 action, action_extra, ico = h.action_parser(entry, feed=True)
174 174 title = "%s - %s %s" % (user.short_contact, action(),
175 175 entry.repository.repo_name)
176 176 desc = action_extra()
177 177 _url = None
178 178 if entry.repository is not None:
179 179 _url = h.canonical_url('changelog_home',
180 180 repo_name=entry.repository.repo_name)
181 181
182 182 feed.add_item(title=title,
183 183 pubdate=entry.action_date,
184 184 link=_url or h.canonical_url(''),
185 185 author_email=user.email,
186 186 author_name=user.full_contact,
187 187 description=desc)
188 188
189 189 response.content_type = feed.mime_type
190 190 return feed.writeString('utf-8')
191 191
192 192 @LoginRequired()
193 193 @NotAnonymous()
194 194 def index(self):
195 195 # Return a rendered template
196 196 p = safe_int(request.GET.get('page'), 1)
197 197 c.user = User.get(self.authuser.user_id)
198 198 c.following = self.sa.query(UserFollowing) \
199 199 .filter(UserFollowing.user_id == self.authuser.user_id) \
200 200 .options(joinedload(UserFollowing.follows_repository)) \
201 201 .all()
202 202
203 203 journal = self._get_journal_data(c.following)
204 204
205 205 def url_generator(**kw):
206 206 return url.current(filter=c.search_term, **kw)
207 207
208 208 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
209 209 c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager)
210 210
211 211 if request.environ.get('HTTP_X_PARTIAL_XHR'):
212 212 return render('journal/journal_data.html')
213 213
214 214 repos_list = Repository.query(sorted=True) \
215 215 .filter_by(owner_id=self.authuser.user_id).all()
216 216
217 217 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
218 218 admin=True)
219 219 #json used to render the grid
220 220 c.data = json.dumps(repos_data)
221 221
222 222 watched_repos_data = []
223 223
224 224 ## watched repos
225 225 _render = RepoModel._render_datatable
226 226
227 227 def quick_menu(repo_name):
228 228 return _render('quick_menu', repo_name)
229 229
230 230 def repo_lnk(name, rtype, rstate, private, fork_of):
231 231 return _render('repo_name', name, rtype, rstate, private, fork_of,
232 232 short_name=False, admin=False)
233 233
234 234 def last_rev(repo_name, cs_cache):
235 235 return _render('revision', repo_name, cs_cache.get('revision'),
236 236 cs_cache.get('raw_id'), cs_cache.get('author'),
237 237 cs_cache.get('message'))
238 238
239 239 def desc(desc):
240 240 from pylons import tmpl_context as c
241 241 return h.urlify_text(desc, truncate=60, stylize=c.visual.stylify_metatags)
242 242
243 243 def repo_actions(repo_name):
244 244 return _render('repo_actions', repo_name)
245 245
246 246 def owner_actions(user_id, username):
247 247 return _render('user_name', user_id, username)
248 248
249 249 def toogle_follow(repo_id):
250 250 return _render('toggle_follow', repo_id)
251 251
252 252 for entry in c.following:
253 253 repo = entry.follows_repository
254 254 cs_cache = repo.changeset_cache
255 255 row = {
256 256 "menu": quick_menu(repo.repo_name),
257 257 "raw_name": repo.repo_name,
258 258 "name": repo_lnk(repo.repo_name, repo.repo_type,
259 259 repo.repo_state, repo.private, repo.fork),
260 260 "last_changeset": last_rev(repo.repo_name, cs_cache),
261 261 "last_rev_raw": cs_cache.get('revision'),
262 262 "action": toogle_follow(repo.repo_id)
263 263 }
264 264
265 265 watched_repos_data.append(row)
266 266
267 267 c.watched_data = json.dumps({
268 268 "totalRecords": len(c.following),
269 269 "startIndex": 0,
270 270 "sort": "name",
271 271 "dir": "asc",
272 272 "records": watched_repos_data
273 273 })
274 274 return render('journal/journal.html')
275 275
276 276 @LoginRequired(api_access=True)
277 277 @NotAnonymous()
278 278 def journal_atom(self):
279 279 """
280 280 Produce an atom-1.0 feed via feedgenerator module
281 281 """
282 282 following = self.sa.query(UserFollowing) \
283 283 .filter(UserFollowing.user_id == self.authuser.user_id) \
284 284 .options(joinedload(UserFollowing.follows_repository)) \
285 285 .all()
286 286 return self._atom_feed(following, public=False)
287 287
288 288 @LoginRequired(api_access=True)
289 289 @NotAnonymous()
290 290 def journal_rss(self):
291 291 """
292 292 Produce an rss feed via feedgenerator module
293 293 """
294 294 following = self.sa.query(UserFollowing) \
295 295 .filter(UserFollowing.user_id == self.authuser.user_id) \
296 296 .options(joinedload(UserFollowing.follows_repository)) \
297 297 .all()
298 298 return self._rss_feed(following, public=False)
299 299
300 300 @LoginRequired()
301 301 @NotAnonymous()
302 302 def toggle_following(self):
303 303 user_id = request.POST.get('follows_user_id')
304 304 if user_id:
305 305 try:
306 306 self.scm_model.toggle_following_user(user_id,
307 307 self.authuser.user_id)
308 308 Session.commit()
309 309 return 'ok'
310 310 except Exception:
311 311 log.error(traceback.format_exc())
312 312 raise HTTPBadRequest()
313 313
314 314 repo_id = request.POST.get('follows_repo_id')
315 315 if repo_id:
316 316 try:
317 317 self.scm_model.toggle_following_repo(repo_id,
318 318 self.authuser.user_id)
319 319 Session.commit()
320 320 return 'ok'
321 321 except Exception:
322 322 log.error(traceback.format_exc())
323 323 raise HTTPBadRequest()
324 324
325 325 raise HTTPBadRequest()
326 326
327 327 @LoginRequired()
328 328 def public_journal(self):
329 329 # Return a rendered template
330 330 p = safe_int(request.GET.get('page'), 1)
331 331
332 332 c.following = self.sa.query(UserFollowing) \
333 333 .filter(UserFollowing.user_id == self.authuser.user_id) \
334 334 .options(joinedload(UserFollowing.follows_repository)) \
335 335 .all()
336 336
337 337 journal = self._get_journal_data(c.following)
338 338
339 339 c.journal_pager = Page(journal, page=p, items_per_page=20)
340 340
341 341 c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager)
342 342
343 343 if request.environ.get('HTTP_X_PARTIAL_XHR'):
344 344 return render('journal/journal_data.html')
345 345
346 346 return render('journal/public_journal.html')
347 347
348 348 @LoginRequired(api_access=True)
349 349 def public_journal_atom(self):
350 350 """
351 351 Produce an atom-1.0 feed via feedgenerator module
352 352 """
353 353 c.following = self.sa.query(UserFollowing) \
354 354 .filter(UserFollowing.user_id == self.authuser.user_id) \
355 355 .options(joinedload(UserFollowing.follows_repository)) \
356 356 .all()
357 357
358 358 return self._atom_feed(c.following)
359 359
360 360 @LoginRequired(api_access=True)
361 361 def public_journal_rss(self):
362 362 """
363 363 Produce an rss2 feed via feedgenerator module
364 364 """
365 365 c.following = self.sa.query(UserFollowing) \
366 366 .filter(UserFollowing.user_id == self.authuser.user_id) \
367 367 .options(joinedload(UserFollowing.follows_repository)) \
368 368 .all()
369 369
370 370 return self._rss_feed(c.following)
@@ -1,840 +1,840 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.pullrequests
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 pull requests controller for Kallithea for initializing pull requests
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: May 7, 2012
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31 import re
32 32
33 33 from pylons import request, tmpl_context as c
34 34 from pylons.i18n.translation import _
35 35 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest
36 36
37 37 from kallithea.config.routing import url
38 38 from kallithea.lib.vcs.utils.hgcompat import unionrepo
39 39 from kallithea.lib.compat import json, OrderedDict
40 40 from kallithea.lib.base import BaseRepoController, render
41 41 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
42 42 NotAnonymous
43 43 from kallithea.lib.helpers import Page
44 44 from kallithea.lib import helpers as h
45 45 from kallithea.lib import diffs
46 46 from kallithea.lib.exceptions import UserInvalidException
47 47 from kallithea.lib.utils import action_logger, jsonify
48 48 from kallithea.lib.vcs.utils import safe_str
49 49 from kallithea.lib.vcs.exceptions import EmptyRepositoryError, ChangesetDoesNotExistError
50 50 from kallithea.lib.diffs import LimitedDiffContainer
51 51 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
52 52 PullRequestReviewers, User
53 53 from kallithea.model.pull_request import PullRequestModel
54 54 from kallithea.model.meta import Session
55 55 from kallithea.model.repo import RepoModel
56 56 from kallithea.model.comment import ChangesetCommentsModel
57 57 from kallithea.model.changeset_status import ChangesetStatusModel
58 58 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
59 59 from kallithea.lib.utils2 import safe_int
60 60 from kallithea.controllers.changeset import _ignorews_url, _context_url, \
61 61 create_comment
62 62 from kallithea.controllers.compare import CompareController
63 63 from kallithea.lib.graphmod import graph_data
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 class PullrequestsController(BaseRepoController):
69 69
70 70 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
71 71 """return a structure with repo's interesting changesets, suitable for
72 72 the selectors in pullrequest.html
73 73
74 74 rev: a revision that must be in the list somehow and selected by default
75 75 branch: a branch that must be in the list and selected by default - even if closed
76 76 branch_rev: a revision of which peers should be preferred and available."""
77 77 # list named branches that has been merged to this named branch - it should probably merge back
78 78 peers = []
79 79
80 80 if rev:
81 81 rev = safe_str(rev)
82 82
83 83 if branch:
84 84 branch = safe_str(branch)
85 85
86 86 if branch_rev:
87 87 branch_rev = safe_str(branch_rev)
88 88 # a revset not restricting to merge() would be better
89 89 # (especially because it would get the branch point)
90 90 # ... but is currently too expensive
91 91 # including branches of children could be nice too
92 92 peerbranches = set()
93 93 for i in repo._repo.revs(
94 94 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
95 95 branch_rev, branch_rev):
96 96 abranch = repo.get_changeset(i).branch
97 97 if abranch not in peerbranches:
98 98 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
99 99 peers.append((n, abranch))
100 100 peerbranches.add(abranch)
101 101
102 102 selected = None
103 103 tiprev = repo.tags.get('tip')
104 104 tipbranch = None
105 105
106 106 branches = []
107 107 for abranch, branchrev in repo.branches.iteritems():
108 108 n = 'branch:%s:%s' % (abranch, branchrev)
109 109 desc = abranch
110 110 if branchrev == tiprev:
111 111 tipbranch = abranch
112 112 desc = '%s (current tip)' % desc
113 113 branches.append((n, desc))
114 114 if rev == branchrev:
115 115 selected = n
116 116 if branch == abranch:
117 117 if not rev:
118 118 selected = n
119 119 branch = None
120 120 if branch: # branch not in list - it is probably closed
121 121 branchrev = repo.closed_branches.get(branch)
122 122 if branchrev:
123 123 n = 'branch:%s:%s' % (branch, branchrev)
124 124 branches.append((n, _('%s (closed)') % branch))
125 125 selected = n
126 126 branch = None
127 127 if branch:
128 128 log.debug('branch %r not found in %s', branch, repo)
129 129
130 130 bookmarks = []
131 131 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
132 132 n = 'book:%s:%s' % (bookmark, bookmarkrev)
133 133 bookmarks.append((n, bookmark))
134 134 if rev == bookmarkrev:
135 135 selected = n
136 136
137 137 tags = []
138 138 for tag, tagrev in repo.tags.iteritems():
139 139 if tag == 'tip':
140 140 continue
141 141 n = 'tag:%s:%s' % (tag, tagrev)
142 142 tags.append((n, tag))
143 143 # note: even if rev == tagrev, don't select the static tag - it must be chosen explicitly
144 144
145 145 # prio 1: rev was selected as existing entry above
146 146
147 147 # prio 2: create special entry for rev; rev _must_ be used
148 148 specials = []
149 149 if rev and selected is None:
150 150 selected = 'rev:%s:%s' % (rev, rev)
151 151 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
152 152
153 153 # prio 3: most recent peer branch
154 154 if peers and not selected:
155 155 selected = peers[0][0]
156 156
157 157 # prio 4: tip revision
158 158 if not selected:
159 159 if h.is_hg(repo):
160 160 if tipbranch:
161 161 selected = 'branch:%s:%s' % (tipbranch, tiprev)
162 162 else:
163 163 selected = 'tag:null:' + repo.EMPTY_CHANGESET
164 164 tags.append((selected, 'null'))
165 165 else:
166 166 if 'master' in repo.branches:
167 167 selected = 'branch:master:%s' % repo.branches['master']
168 168 else:
169 169 k, v = repo.branches.items()[0]
170 170 selected = 'branch:%s:%s' % (k, v)
171 171
172 172 groups = [(specials, _("Special")),
173 173 (peers, _("Peer branches")),
174 174 (bookmarks, _("Bookmarks")),
175 175 (branches, _("Branches")),
176 176 (tags, _("Tags")),
177 177 ]
178 178 return [g for g in groups if g[0]], selected
179 179
180 180 def _get_is_allowed_change_status(self, pull_request):
181 181 if pull_request.is_closed():
182 182 return False
183 183
184 184 owner = self.authuser.user_id == pull_request.owner_id
185 185 reviewer = PullRequestReviewers.query() \
186 186 .filter(PullRequestReviewers.pull_request == pull_request) \
187 187 .filter(PullRequestReviewers.user_id == self.authuser.user_id) \
188 188 .count() != 0
189 189
190 190 return self.authuser.admin or owner or reviewer
191 191
192 192 @LoginRequired()
193 193 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
194 194 'repository.admin')
195 195 def show_all(self, repo_name):
196 196 c.from_ = request.GET.get('from_') or ''
197 197 c.closed = request.GET.get('closed') or ''
198 198 p = safe_int(request.GET.get('page'), 1)
199 199
200 200 q = PullRequest.query(include_closed=c.closed, sorted=True)
201 201 if c.from_:
202 202 q = q.filter_by(org_repo=c.db_repo)
203 203 else:
204 204 q = q.filter_by(other_repo=c.db_repo)
205 205 c.pull_requests = q.all()
206 206
207 207 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
208 208
209 209 return render('/pullrequests/pullrequest_show_all.html')
210 210
211 211 @LoginRequired()
212 212 @NotAnonymous()
213 213 def show_my(self):
214 214 c.closed = request.GET.get('closed') or ''
215 215
216 216 c.my_pull_requests = PullRequest.query(
217 217 include_closed=c.closed,
218 218 sorted=True,
219 219 ).filter_by(owner_id=self.authuser.user_id).all()
220 220
221 221 c.participate_in_pull_requests = []
222 222 c.participate_in_pull_requests_todo = []
223 223 done_status = set([ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED])
224 224 for pr in PullRequest.query(
225 225 include_closed=c.closed,
226 226 reviewer_id=self.authuser.user_id,
227 227 sorted=True,
228 228 ):
229 229 status = pr.user_review_status(c.authuser.user_id) # very inefficient!!!
230 230 if status in done_status:
231 231 c.participate_in_pull_requests.append(pr)
232 232 else:
233 233 c.participate_in_pull_requests_todo.append(pr)
234 234
235 235 return render('/pullrequests/pullrequest_show_my.html')
236 236
237 237 @LoginRequired()
238 238 @NotAnonymous()
239 239 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
240 240 'repository.admin')
241 241 def index(self):
242 242 org_repo = c.db_repo
243 243 org_scm_instance = org_repo.scm_instance
244 244 try:
245 245 org_scm_instance.get_changeset()
246 246 except EmptyRepositoryError as e:
247 247 h.flash(h.literal(_('There are no changesets yet')),
248 248 category='warning')
249 249 raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))
250 250
251 251 org_rev = request.GET.get('rev_end')
252 252 # rev_start is not directly useful - its parent could however be used
253 253 # as default for other and thus give a simple compare view
254 254 rev_start = request.GET.get('rev_start')
255 255 other_rev = None
256 256 if rev_start:
257 257 starters = org_repo.get_changeset(rev_start).parents
258 258 if starters:
259 259 other_rev = starters[0].raw_id
260 260 else:
261 261 other_rev = org_repo.scm_instance.EMPTY_CHANGESET
262 262 branch = request.GET.get('branch')
263 263
264 264 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
265 265 c.default_cs_repo = org_repo.repo_name
266 266 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
267 267
268 268 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
269 269 if default_cs_ref_type != 'branch':
270 270 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
271 271
272 272 # add org repo to other so we can open pull request against peer branches on itself
273 273 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
274 274
275 275 if org_repo.parent:
276 276 # add parent of this fork also and select it.
277 277 # use the same branch on destination as on source, if available.
278 278 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
279 279 c.a_repo = org_repo.parent
280 280 c.a_refs, c.default_a_ref = self._get_repo_refs(
281 281 org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
282 282
283 283 else:
284 284 c.a_repo = org_repo
285 285 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
286 286
287 287 # gather forks and add to this list ... even though it is rare to
288 288 # request forks to pull from their parent
289 289 for fork in org_repo.forks:
290 290 c.a_repos.append((fork.repo_name, fork.repo_name))
291 291
292 292 return render('/pullrequests/pullrequest.html')
293 293
294 294 @LoginRequired()
295 295 @NotAnonymous()
296 296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 297 'repository.admin')
298 298 @jsonify
299 299 def repo_info(self, repo_name):
300 300 repo = c.db_repo
301 301 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
302 302 return {
303 303 'description': repo.description.split('\n', 1)[0],
304 304 'selected_ref': selected_ref,
305 305 'refs': refs,
306 306 }
307 307
308 308 @LoginRequired()
309 309 @NotAnonymous()
310 310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
311 311 'repository.admin')
312 312 def create(self, repo_name):
313 313 repo = c.db_repo
314 314 try:
315 315 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
316 316 except formencode.Invalid as errors:
317 317 log.error(traceback.format_exc())
318 318 log.error(str(errors))
319 319 msg = _('Error creating pull request: %s') % errors.msg
320 320 h.flash(msg, 'error')
321 321 raise HTTPBadRequest
322 322
323 323 # heads up: org and other might seem backward here ...
324 324 org_repo_name = _form['org_repo']
325 325 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
326 326 org_repo = RepoModel()._get_repo(org_repo_name)
327 327 (org_ref_type,
328 328 org_ref_name,
329 329 org_rev) = org_ref.split(':')
330 330 if org_ref_type == 'rev':
331 331 org_ref_type = 'branch'
332 332 cs = org_repo.scm_instance.get_changeset(org_rev)
333 333 org_ref = '%s:%s:%s' % (org_ref_type, cs.branch, cs.raw_id)
334 334
335 335 other_repo_name = _form['other_repo']
336 336 other_ref = _form['other_ref'] # will have symbolic name and head revision
337 337 other_repo = RepoModel()._get_repo(other_repo_name)
338 338 (other_ref_type,
339 339 other_ref_name,
340 340 other_rev) = other_ref.split(':')
341 341 if other_ref_type == 'rev':
342 342 cs = other_repo.scm_instance.get_changeset(other_rev)
343 343 other_ref_name = cs.raw_id[:12]
344 344 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, cs.raw_id)
345 345
346 346 cs_ranges, _cs_ranges_not, ancestor_rev = \
347 347 CompareController._get_changesets(org_repo.scm_instance.alias,
348 348 other_repo.scm_instance, other_rev, # org and other "swapped"
349 349 org_repo.scm_instance, org_rev,
350 350 )
351 351 if ancestor_rev is None:
352 352 ancestor_rev = org_repo.scm_instance.EMPTY_CHANGESET
353 353 revisions = [cs_.raw_id for cs_ in cs_ranges]
354 354
355 355 # hack: ancestor_rev is not an other_rev but we want to show the
356 356 # requested destination and have the exact ancestor
357 357 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
358 358
359 359 reviewers = _form['review_members']
360 360
361 361 title = _form['pullrequest_title']
362 362 if not title:
363 363 if org_repo_name == other_repo_name:
364 364 title = '%s to %s' % (h.short_ref(org_ref_type, org_ref_name),
365 365 h.short_ref(other_ref_type, other_ref_name))
366 366 else:
367 367 title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
368 368 other_repo_name, h.short_ref(other_ref_type, other_ref_name))
369 369 description = _form['pullrequest_desc'].strip() or _('No description')
370 370 try:
371 371 pull_request = PullRequestModel().create(
372 372 self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
373 373 other_ref, revisions, reviewers, title, description
374 374 )
375 375 Session().commit()
376 376 h.flash(_('Successfully opened new pull request'),
377 377 category='success')
378 378 except UserInvalidException as u:
379 379 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
380 380 raise HTTPBadRequest()
381 381 except Exception:
382 382 h.flash(_('Error occurred while creating pull request'),
383 383 category='error')
384 384 log.error(traceback.format_exc())
385 385 raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
386 386
387 387 raise HTTPFound(location=pull_request.url())
388 388
389 389 def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewers_ids):
390 390 org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
391 391 org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
392 392 new_org_rev = self._get_ref_rev(org_repo, 'rev', new_rev)
393 393
394 394 other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
395 395 other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
396 396 #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
397 397 new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
398 398
399 399 cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
400 400 other_repo.scm_instance, new_other_rev, # org and other "swapped"
401 401 org_repo.scm_instance, new_org_rev)
402 402
403 403 old_revisions = set(old_pull_request.revisions)
404 404 revisions = [cs.raw_id for cs in cs_ranges]
405 405 new_revisions = [r for r in revisions if r not in old_revisions]
406 406 lost = old_revisions.difference(revisions)
407 407
408 408 infos = ['This is a new iteration of %s "%s".' %
409 409 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
410 410 pull_request_id=old_pull_request.pull_request_id),
411 411 old_pull_request.title)]
412 412
413 413 if lost:
414 414 infos.append(_('Missing changesets since the previous iteration:'))
415 415 for r in old_pull_request.revisions:
416 416 if r in lost:
417 417 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
418 418 infos.append(' %s %s' % (h.short_id(r), rev_desc))
419 419
420 420 if new_revisions:
421 421 infos.append(_('New changesets on %s %s since the previous iteration:') % (org_ref_type, org_ref_name))
422 422 for r in reversed(revisions):
423 423 if r in new_revisions:
424 424 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
425 425 infos.append(' %s %s' % (h.short_id(r), h.shorter(rev_desc, 80)))
426 426
427 427 if ancestor_rev == other_rev:
428 428 infos.append(_("Ancestor didn't change - diff since previous iteration:"))
429 429 infos.append(h.canonical_url('compare_url',
430 430 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
431 431 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
432 432 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
433 433 )) # note: linear diff, merge or not doesn't matter
434 434 else:
435 435 infos.append(_('This iteration is based on another %s revision and there is no simple diff.') % other_ref_name)
436 436 else:
437 437 infos.append(_('No changes found on %s %s since previous iteration.') % (org_ref_type, org_ref_name))
438 438 # TODO: fail?
439 439
440 440 # hack: ancestor_rev is not an other_ref but we want to show the
441 441 # requested destination and have the exact ancestor
442 442 new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
443 443 new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
444 444
445 445 try:
446 446 title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
447 447 v = int(old_v) + 1
448 448 except (AttributeError, ValueError):
449 449 v = 2
450 450 title = '%s (v%s)' % (title.strip(), v)
451 451
452 452 # using a mail-like separator, insert new iteration info in description with latest first
453 453 descriptions = description.replace('\r\n', '\n').split('\n-- \n', 1)
454 454 description = descriptions[0].strip() + '\n\n-- \n' + '\n'.join(infos)
455 455 if len(descriptions) > 1:
456 456 description += '\n\n' + descriptions[1].strip()
457 457
458 458 try:
459 459 pull_request = PullRequestModel().create(
460 460 self.authuser.user_id,
461 461 old_pull_request.org_repo.repo_name, new_org_ref,
462 462 old_pull_request.other_repo.repo_name, new_other_ref,
463 463 revisions, reviewers_ids, title, description
464 464 )
465 465 except UserInvalidException as u:
466 466 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
467 467 raise HTTPBadRequest()
468 468 except Exception:
469 469 h.flash(_('Error occurred while creating pull request'),
470 470 category='error')
471 471 log.error(traceback.format_exc())
472 472 raise HTTPFound(location=old_pull_request.url())
473 473
474 474 ChangesetCommentsModel().create(
475 475 text=_('Closed, next iteration: %s .') % pull_request.url(canonical=True),
476 repo=old_pull_request.other_repo.repo_id,
476 repo=old_pull_request.other_repo_id,
477 477 author=c.authuser.user_id,
478 478 pull_request=old_pull_request.pull_request_id,
479 479 closing_pr=True)
480 480 PullRequestModel().close_pull_request(old_pull_request.pull_request_id)
481 481
482 482 Session().commit()
483 483 h.flash(_('New pull request iteration created'),
484 484 category='success')
485 485
486 486 raise HTTPFound(location=pull_request.url())
487 487
488 488 # pullrequest_post for PR editing
489 489 @LoginRequired()
490 490 @NotAnonymous()
491 491 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
492 492 'repository.admin')
493 493 def post(self, repo_name, pull_request_id):
494 494 pull_request = PullRequest.get_or_404(pull_request_id)
495 495 if pull_request.is_closed():
496 496 raise HTTPForbidden()
497 497 assert pull_request.other_repo.repo_name == repo_name
498 498 #only owner or admin can update it
499 499 owner = pull_request.owner_id == c.authuser.user_id
500 500 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
501 501 if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner):
502 502 raise HTTPForbidden()
503 503
504 504 _form = PullRequestPostForm()().to_python(request.POST)
505 505 reviewers_ids = [int(s) for s in _form['review_members']]
506 506
507 507 if _form['updaterev']:
508 508 return self.create_new_iteration(pull_request,
509 509 _form['updaterev'],
510 510 _form['pullrequest_title'],
511 511 _form['pullrequest_desc'],
512 512 reviewers_ids)
513 513
514 514 old_description = pull_request.description
515 515 pull_request.title = _form['pullrequest_title']
516 516 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
517 517 pull_request.owner = User.get_by_username(_form['owner'])
518 518 user = User.get(c.authuser.user_id)
519 519 try:
520 520 PullRequestModel().mention_from_description(user, pull_request, old_description)
521 521 PullRequestModel().update_reviewers(user, pull_request_id, reviewers_ids)
522 522 except UserInvalidException as u:
523 523 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
524 524 raise HTTPBadRequest()
525 525
526 526 Session().commit()
527 527 h.flash(_('Pull request updated'), category='success')
528 528
529 529 raise HTTPFound(location=pull_request.url())
530 530
531 531 @LoginRequired()
532 532 @NotAnonymous()
533 533 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
534 534 'repository.admin')
535 535 @jsonify
536 536 def delete(self, repo_name, pull_request_id):
537 537 pull_request = PullRequest.get_or_404(pull_request_id)
538 538 #only owner can delete it !
539 if pull_request.owner.user_id == c.authuser.user_id:
539 if pull_request.owner_id == c.authuser.user_id:
540 540 PullRequestModel().delete(pull_request)
541 541 Session().commit()
542 542 h.flash(_('Successfully deleted pull request'),
543 543 category='success')
544 544 raise HTTPFound(location=url('my_pullrequests'))
545 545 raise HTTPForbidden()
546 546
547 547 @LoginRequired()
548 548 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
549 549 'repository.admin')
550 550 def show(self, repo_name, pull_request_id, extra=None):
551 551 repo_model = RepoModel()
552 552 c.users_array = repo_model.get_users_js()
553 553 c.user_groups_array = repo_model.get_user_groups_js()
554 554 c.pull_request = PullRequest.get_or_404(pull_request_id)
555 555 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
556 556 cc_model = ChangesetCommentsModel()
557 557 cs_model = ChangesetStatusModel()
558 558
559 559 # pull_requests repo_name we opened it against
560 560 # ie. other_repo must match
561 561 if repo_name != c.pull_request.other_repo.repo_name:
562 562 raise HTTPNotFound
563 563
564 564 # load compare data into template context
565 565 c.cs_repo = c.pull_request.org_repo
566 566 (c.cs_ref_type,
567 567 c.cs_ref_name,
568 568 c.cs_rev) = c.pull_request.org_ref.split(':')
569 569
570 570 c.a_repo = c.pull_request.other_repo
571 571 (c.a_ref_type,
572 572 c.a_ref_name,
573 573 c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor
574 574
575 575 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
576 576 c.cs_repo = c.cs_repo
577 577 try:
578 578 c.cs_ranges = [org_scm_instance.get_changeset(x)
579 579 for x in c.pull_request.revisions]
580 580 except ChangesetDoesNotExistError:
581 581 c.cs_ranges = []
582 582 h.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name),
583 583 'error')
584 584 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
585 585 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
586 586 c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
587 587
588 588 c.is_range = False
589 589 try:
590 590 if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
591 591 cs_a = org_scm_instance.get_changeset(c.a_rev)
592 592 root_parents = c.cs_ranges[0].parents
593 593 c.is_range = cs_a in root_parents
594 594 #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
595 595 except ChangesetDoesNotExistError: # probably because c.a_rev not found
596 596 pass
597 597 except IndexError: # probably because c.cs_ranges is empty, probably because revisions are missing
598 598 pass
599 599
600 600 avail_revs = set()
601 601 avail_show = []
602 602 c.cs_branch_name = c.cs_ref_name
603 603 c.a_branch_name = None
604 604 other_scm_instance = c.a_repo.scm_instance
605 605 c.update_msg = ""
606 606 c.update_msg_other = ""
607 607 try:
608 608 if not c.cs_ranges:
609 609 c.update_msg = _('Error: changesets not found when displaying pull request from %s.') % c.cs_rev
610 610 elif org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
611 611 if c.cs_ref_type != 'branch':
612 612 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
613 613 c.a_branch_name = c.a_ref_name
614 614 if c.a_ref_type != 'branch':
615 615 try:
616 616 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
617 617 except EmptyRepositoryError:
618 618 c.a_branch_name = 'null' # not a branch name ... but close enough
619 619 # candidates: descendants of old head that are on the right branch
620 620 # and not are the old head itself ...
621 621 # and nothing at all if old head is a descendant of target ref name
622 622 if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
623 623 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
624 624 elif c.pull_request.is_closed():
625 625 c.update_msg = _('This pull request has been closed and can not be updated.')
626 626 else: # look for descendants of PR head on source branch in org repo
627 627 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
628 628 revs[0], c.cs_branch_name)
629 629 if len(avail_revs) > 1: # more than just revs[0]
630 630 # also show changesets that not are descendants but would be merged in
631 631 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
632 632 if org_scm_instance.path != other_scm_instance.path:
633 633 # Note: org_scm_instance.path must come first so all
634 634 # valid revision numbers are 100% org_scm compatible
635 635 # - both for avail_revs and for revset results
636 636 hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
637 637 org_scm_instance.path,
638 638 other_scm_instance.path)
639 639 else:
640 640 hgrepo = org_scm_instance._repo
641 641 show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s',
642 642 avail_revs, revs[0], targethead))
643 643 c.update_msg = _('The following additional changes are available on %s:') % c.cs_branch_name
644 644 else:
645 645 show = set()
646 646 avail_revs = set() # drop revs[0]
647 647 c.update_msg = _('No additional changesets found for iterating on this pull request.')
648 648
649 649 # TODO: handle branch heads that not are tip-most
650 650 brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
651 651 if brevs:
652 652 # also show changesets that are on branch but neither ancestors nor descendants
653 653 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
654 654 show.add(revs[0]) # make sure graph shows this so we can see how they relate
655 655 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
656 656 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
657 657
658 658 avail_show = sorted(show, reverse=True)
659 659
660 660 elif org_scm_instance.alias == 'git':
661 661 c.cs_repo.scm_instance.get_changeset(c.cs_rev) # check it exists - raise ChangesetDoesNotExistError if not
662 662 c.update_msg = _("Git pull requests don't support iterating yet.")
663 663 except ChangesetDoesNotExistError:
664 664 c.update_msg = _('Error: some changesets not found when displaying pull request from %s.') % c.cs_rev
665 665
666 666 c.avail_revs = avail_revs
667 667 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
668 668 c.avail_jsdata = json.dumps(graph_data(org_scm_instance, avail_show))
669 669
670 670 raw_ids = [x.raw_id for x in c.cs_ranges]
671 671 c.cs_comments = c.cs_repo.get_comments(raw_ids)
672 672 c.statuses = c.cs_repo.statuses(raw_ids)
673 673
674 674 ignore_whitespace = request.GET.get('ignorews') == '1'
675 675 line_context = safe_int(request.GET.get('context'), 3)
676 676 c.ignorews_url = _ignorews_url
677 677 c.context_url = _context_url
678 678 c.fulldiff = request.GET.get('fulldiff')
679 679 diff_limit = self.cut_off_limit if not c.fulldiff else None
680 680
681 681 # we swap org/other ref since we run a simple diff on one repo
682 682 log.debug('running diff between %s and %s in %s',
683 683 c.a_rev, c.cs_rev, org_scm_instance.path)
684 684 try:
685 685 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
686 686 ignore_whitespace=ignore_whitespace,
687 687 context=line_context)
688 688 except ChangesetDoesNotExistError:
689 689 txtdiff = _("The diff can't be shown - the PR revisions could not be found.")
690 690 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
691 691 diff_limit=diff_limit)
692 692 _parsed = diff_processor.prepare()
693 693
694 694 c.limited_diff = False
695 695 if isinstance(_parsed, LimitedDiffContainer):
696 696 c.limited_diff = True
697 697
698 698 c.file_diff_data = OrderedDict()
699 699 c.lines_added = 0
700 700 c.lines_deleted = 0
701 701
702 702 for f in _parsed:
703 703 st = f['stats']
704 704 c.lines_added += st['added']
705 705 c.lines_deleted += st['deleted']
706 706 filename = f['filename']
707 707 fid = h.FID('', filename)
708 708 diff = diff_processor.as_html(enable_comments=True,
709 709 parsed_lines=[f])
710 710 c.file_diff_data[fid] = (None, f['operation'], f['old_filename'], filename, diff, st)
711 711
712 712 # inline comments
713 713 c.inline_cnt = 0
714 714 c.inline_comments = cc_model.get_inline_comments(
715 715 c.db_repo.repo_id,
716 716 pull_request=pull_request_id)
717 717 # count inline comments
718 718 for __, lines in c.inline_comments:
719 719 for comments in lines.values():
720 720 c.inline_cnt += len(comments)
721 721 # comments
722 722 c.comments = cc_model.get_comments(c.db_repo.repo_id,
723 723 pull_request=pull_request_id)
724 724
725 725 # (badly named) pull-request status calculation based on reviewer votes
726 726 (c.pull_request_reviewers,
727 727 c.pull_request_pending_reviewers,
728 728 c.current_voting_result,
729 729 ) = cs_model.calculate_pull_request_result(c.pull_request)
730 730 c.changeset_statuses = ChangesetStatus.STATUSES
731 731
732 732 c.as_form = False
733 733 c.ancestor = None # there is one - but right here we don't know which
734 734 return render('/pullrequests/pullrequest_show.html')
735 735
736 736 @LoginRequired()
737 737 @NotAnonymous()
738 738 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
739 739 'repository.admin')
740 740 @jsonify
741 741 def comment(self, repo_name, pull_request_id):
742 742 pull_request = PullRequest.get_or_404(pull_request_id)
743 743
744 744 status = request.POST.get('changeset_status')
745 745 close_pr = request.POST.get('save_close')
746 746 delete = request.POST.get('save_delete')
747 747 f_path = request.POST.get('f_path')
748 748 line_no = request.POST.get('line')
749 749
750 750 if (status or close_pr or delete) and (f_path or line_no):
751 751 # status votes and closing is only possible in general comments
752 752 raise HTTPBadRequest()
753 753
754 754 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
755 755 if not allowed_to_change_status:
756 756 if status or close_pr:
757 757 h.flash(_('No permission to change pull request status'), 'error')
758 758 raise HTTPForbidden()
759 759
760 760 if delete == "delete":
761 if (pull_request.owner.user_id == c.authuser.user_id or
761 if (pull_request.owner_id == c.authuser.user_id or
762 762 h.HasPermissionAny('hg.admin')() or
763 763 h.HasRepoPermissionAny('repository.admin')(pull_request.org_repo.repo_name) or
764 764 h.HasRepoPermissionAny('repository.admin')(pull_request.other_repo.repo_name)
765 765 ) and not pull_request.is_closed():
766 766 PullRequestModel().delete(pull_request)
767 767 Session().commit()
768 768 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
769 769 category='success')
770 770 return {
771 771 'location': url('my_pullrequests'), # or repo pr list?
772 772 }
773 773 raise HTTPFound(location=url('my_pullrequests')) # or repo pr list?
774 774 raise HTTPForbidden()
775 775
776 776 text = request.POST.get('text', '').strip()
777 777
778 778 comment = create_comment(
779 779 text,
780 780 status,
781 781 pull_request_id=pull_request_id,
782 782 f_path=f_path,
783 783 line_no=line_no,
784 784 closing_pr=close_pr,
785 785 )
786 786
787 787 action_logger(self.authuser,
788 788 'user_commented_pull_request:%s' % pull_request_id,
789 789 c.db_repo, self.ip_addr, self.sa)
790 790
791 791 if status:
792 792 ChangesetStatusModel().set_status(
793 793 c.db_repo.repo_id,
794 794 status,
795 795 c.authuser.user_id,
796 796 comment,
797 797 pull_request=pull_request_id
798 798 )
799 799
800 800 if close_pr:
801 801 PullRequestModel().close_pull_request(pull_request_id)
802 802 action_logger(self.authuser,
803 803 'user_closed_pull_request:%s' % pull_request_id,
804 804 c.db_repo, self.ip_addr, self.sa)
805 805
806 806 Session().commit()
807 807
808 808 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
809 809 raise HTTPFound(location=pull_request.url())
810 810
811 811 data = {
812 812 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
813 813 }
814 814 if comment is not None:
815 815 c.comment = comment
816 816 data.update(comment.get_dict())
817 817 data.update({'rendered_text':
818 818 render('changeset/changeset_comment_block.html')})
819 819
820 820 return data
821 821
822 822 @LoginRequired()
823 823 @NotAnonymous()
824 824 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
825 825 'repository.admin')
826 826 @jsonify
827 827 def delete_comment(self, repo_name, comment_id):
828 828 co = ChangesetComment.get(comment_id)
829 829 if co.pull_request.is_closed():
830 830 #don't allow deleting comments on closed pull request
831 831 raise HTTPForbidden()
832 832
833 owner = co.author.user_id == c.authuser.user_id
833 owner = co.author_id == c.authuser.user_id
834 834 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
835 835 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
836 836 ChangesetCommentsModel().delete(comment=co)
837 837 Session().commit()
838 838 return True
839 839 else:
840 840 raise HTTPForbidden()
@@ -1,388 +1,388 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.model.user_group
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 user group model for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Oct 1, 2011
23 23 :author: nvinot, marcink
24 24 """
25 25
26 26
27 27 import logging
28 28 import traceback
29 29
30 30 from kallithea.model import BaseModel
31 31 from kallithea.model.db import UserGroupMember, UserGroup, \
32 32 UserGroupRepoToPerm, Permission, UserGroupToPerm, User, UserUserGroupToPerm, \
33 33 UserGroupUserGroupToPerm
34 34 from kallithea.lib.exceptions import UserGroupsAssignedException, \
35 35 RepoGroupAssignmentError
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class UserGroupModel(BaseModel):
41 41
42 42 def _get_user_group(self, user_group):
43 43 return UserGroup.guess_instance(user_group,
44 44 callback=UserGroup.get_by_group_name)
45 45
46 46 def _create_default_perms(self, user_group):
47 47 # create default permission
48 48 default_perm = 'usergroup.read'
49 49 def_user = User.get_default_user()
50 50 for p in def_user.user_perms:
51 51 if p.permission.permission_name.startswith('usergroup.'):
52 52 default_perm = p.permission.permission_name
53 53 break
54 54
55 55 user_group_to_perm = UserUserGroupToPerm()
56 56 user_group_to_perm.permission = Permission.get_by_key(default_perm)
57 57
58 58 user_group_to_perm.user_group = user_group
59 59 user_group_to_perm.user_id = def_user.user_id
60 60 return user_group_to_perm
61 61
62 62 def _update_permissions(self, user_group, perms_new=None,
63 63 perms_updates=None):
64 64 from kallithea.lib.auth import HasUserGroupPermissionAny
65 65 if not perms_new:
66 66 perms_new = []
67 67 if not perms_updates:
68 68 perms_updates = []
69 69
70 70 # update permissions
71 71 for member, perm, member_type in perms_updates:
72 72 if member_type == 'user':
73 73 # this updates existing one
74 74 self.grant_user_permission(
75 75 user_group=user_group, user=member, perm=perm
76 76 )
77 77 else:
78 78 #check if we have permissions to alter this usergroup
79 79 if HasUserGroupPermissionAny('usergroup.read', 'usergroup.write',
80 80 'usergroup.admin')(member):
81 81 self.grant_user_group_permission(
82 82 target_user_group=user_group, user_group=member, perm=perm
83 83 )
84 84 # set new permissions
85 85 for member, perm, member_type in perms_new:
86 86 if member_type == 'user':
87 87 self.grant_user_permission(
88 88 user_group=user_group, user=member, perm=perm
89 89 )
90 90 else:
91 91 #check if we have permissions to alter this usergroup
92 92 if HasUserGroupPermissionAny('usergroup.read', 'usergroup.write',
93 93 'usergroup.admin')(member):
94 94 self.grant_user_group_permission(
95 95 target_user_group=user_group, user_group=member, perm=perm
96 96 )
97 97
98 98 def get(self, user_group_id, cache=False):
99 99 return UserGroup.get(user_group_id)
100 100
101 101 def get_group(self, user_group):
102 102 return self._get_user_group(user_group)
103 103
104 104 def get_by_name(self, name, cache=False, case_insensitive=False):
105 105 return UserGroup.get_by_group_name(name, cache, case_insensitive)
106 106
107 107 def create(self, name, description, owner, active=True, group_data=None):
108 108 try:
109 109 new_user_group = UserGroup()
110 110 new_user_group.owner = self._get_user(owner)
111 111 new_user_group.users_group_name = name
112 112 new_user_group.user_group_description = description
113 113 new_user_group.users_group_active = active
114 114 if group_data:
115 115 new_user_group.group_data = group_data
116 116 self.sa.add(new_user_group)
117 117 perm_obj = self._create_default_perms(new_user_group)
118 118 self.sa.add(perm_obj)
119 119
120 120 self.grant_user_permission(user_group=new_user_group,
121 121 user=owner, perm='usergroup.admin')
122 122
123 123 return new_user_group
124 124 except Exception:
125 125 log.error(traceback.format_exc())
126 126 raise
127 127
128 128 def update(self, user_group, form_data):
129 129
130 130 try:
131 131 user_group = self._get_user_group(user_group)
132 132
133 133 for k, v in form_data.items():
134 134 if k == 'users_group_members':
135 135 user_group.members = []
136 136 self.sa.flush()
137 137 members_list = []
138 138 if v:
139 139 v = [v] if isinstance(v, basestring) else v
140 140 for u_id in set(v):
141 141 member = UserGroupMember(user_group.users_group_id, u_id)
142 142 members_list.append(member)
143 143 setattr(user_group, 'members', members_list)
144 144 setattr(user_group, k, v)
145 145
146 146 self.sa.add(user_group)
147 147 except Exception:
148 148 log.error(traceback.format_exc())
149 149 raise
150 150
151 151 def delete(self, user_group, force=False):
152 152 """
153 153 Deletes user group, unless force flag is used
154 154 raises exception if there are members in that group, else deletes
155 155 group and users
156 156
157 157 :param user_group:
158 158 :param force:
159 159 """
160 160 user_group = self._get_user_group(user_group)
161 161 try:
162 162 # check if this group is not assigned to repo
163 163 assigned_groups = UserGroupRepoToPerm.query() \
164 164 .filter(UserGroupRepoToPerm.users_group == user_group).all()
165 165 assigned_groups = [x.repository.repo_name for x in assigned_groups]
166 166
167 167 if assigned_groups and not force:
168 168 raise UserGroupsAssignedException(
169 169 'User Group assigned to %s' % ", ".join(assigned_groups))
170 170 self.sa.delete(user_group)
171 171 except Exception:
172 172 log.error(traceback.format_exc())
173 173 raise
174 174
175 175 def add_user_to_group(self, user_group, user):
176 176 user_group = self._get_user_group(user_group)
177 177 user = self._get_user(user)
178 178
179 179 for m in user_group.members:
180 180 u = m.user
181 181 if u.user_id == user.user_id:
182 182 # user already in the group, skip
183 183 return True
184 184
185 185 try:
186 186 user_group_member = UserGroupMember()
187 187 user_group_member.user = user
188 188 user_group_member.users_group = user_group
189 189
190 190 user_group.members.append(user_group_member)
191 191 user.group_member.append(user_group_member)
192 192
193 193 self.sa.add(user_group_member)
194 194 return user_group_member
195 195 except Exception:
196 196 log.error(traceback.format_exc())
197 197 raise
198 198
199 199 def remove_user_from_group(self, user_group, user):
200 200 user_group = self._get_user_group(user_group)
201 201 user = self._get_user(user)
202 202
203 203 user_group_member = None
204 204 for m in user_group.members:
205 if m.user.user_id == user.user_id:
205 if m.user_id == user.user_id:
206 206 # Found this user's membership row
207 207 user_group_member = m
208 208 break
209 209
210 210 if user_group_member:
211 211 try:
212 212 self.sa.delete(user_group_member)
213 213 return True
214 214 except Exception:
215 215 log.error(traceback.format_exc())
216 216 raise
217 217 else:
218 218 # User isn't in that group
219 219 return False
220 220
221 221 def has_perm(self, user_group, perm):
222 222 user_group = self._get_user_group(user_group)
223 223 perm = self._get_perm(perm)
224 224
225 225 return UserGroupToPerm.query() \
226 226 .filter(UserGroupToPerm.users_group == user_group) \
227 227 .filter(UserGroupToPerm.permission == perm).scalar() is not None
228 228
229 229 def grant_perm(self, user_group, perm):
230 230 user_group = self._get_user_group(user_group)
231 231 perm = self._get_perm(perm)
232 232
233 233 # if this permission is already granted skip it
234 234 _perm = UserGroupToPerm.query() \
235 235 .filter(UserGroupToPerm.users_group == user_group) \
236 236 .filter(UserGroupToPerm.permission == perm) \
237 237 .scalar()
238 238 if _perm:
239 239 return
240 240
241 241 new = UserGroupToPerm()
242 242 new.users_group = user_group
243 243 new.permission = perm
244 244 self.sa.add(new)
245 245 return new
246 246
247 247 def revoke_perm(self, user_group, perm):
248 248 user_group = self._get_user_group(user_group)
249 249 perm = self._get_perm(perm)
250 250
251 251 obj = UserGroupToPerm.query() \
252 252 .filter(UserGroupToPerm.users_group == user_group) \
253 253 .filter(UserGroupToPerm.permission == perm).scalar()
254 254 if obj is not None:
255 255 self.sa.delete(obj)
256 256
257 257 def grant_user_permission(self, user_group, user, perm):
258 258 """
259 259 Grant permission for user on given user group, or update
260 260 existing one if found
261 261
262 262 :param user_group: Instance of UserGroup, users_group_id,
263 263 or users_group_name
264 264 :param user: Instance of User, user_id or username
265 265 :param perm: Instance of Permission, or permission_name
266 266 """
267 267
268 268 user_group = self._get_user_group(user_group)
269 269 user = self._get_user(user)
270 270 permission = self._get_perm(perm)
271 271
272 272 # check if we have that permission already
273 273 obj = self.sa.query(UserUserGroupToPerm) \
274 274 .filter(UserUserGroupToPerm.user == user) \
275 275 .filter(UserUserGroupToPerm.user_group == user_group) \
276 276 .scalar()
277 277 if obj is None:
278 278 # create new !
279 279 obj = UserUserGroupToPerm()
280 280 obj.user_group = user_group
281 281 obj.user = user
282 282 obj.permission = permission
283 283 self.sa.add(obj)
284 284 log.debug('Granted perm %s to %s on %s', perm, user, user_group)
285 285 return obj
286 286
287 287 def revoke_user_permission(self, user_group, user):
288 288 """
289 289 Revoke permission for user on given repository group
290 290
291 291 :param user_group: Instance of RepoGroup, repositories_group_id,
292 292 or repositories_group name
293 293 :param user: Instance of User, user_id or username
294 294 """
295 295
296 296 user_group = self._get_user_group(user_group)
297 297 user = self._get_user(user)
298 298
299 299 obj = self.sa.query(UserUserGroupToPerm) \
300 300 .filter(UserUserGroupToPerm.user == user) \
301 301 .filter(UserUserGroupToPerm.user_group == user_group) \
302 302 .scalar()
303 303 if obj is not None:
304 304 self.sa.delete(obj)
305 305 log.debug('Revoked perm on %s on %s', user_group, user)
306 306
307 307 def grant_user_group_permission(self, target_user_group, user_group, perm):
308 308 """
309 309 Grant user group permission for given target_user_group
310 310
311 311 :param target_user_group:
312 312 :param user_group:
313 313 :param perm:
314 314 """
315 315 target_user_group = self._get_user_group(target_user_group)
316 316 user_group = self._get_user_group(user_group)
317 317 permission = self._get_perm(perm)
318 318 # forbid assigning same user group to itself
319 319 if target_user_group == user_group:
320 320 raise RepoGroupAssignmentError('target repo:%s cannot be '
321 321 'assigned to itself' % target_user_group)
322 322
323 323 # check if we have that permission already
324 324 obj = self.sa.query(UserGroupUserGroupToPerm) \
325 325 .filter(UserGroupUserGroupToPerm.target_user_group == target_user_group) \
326 326 .filter(UserGroupUserGroupToPerm.user_group == user_group) \
327 327 .scalar()
328 328 if obj is None:
329 329 # create new !
330 330 obj = UserGroupUserGroupToPerm()
331 331 obj.user_group = user_group
332 332 obj.target_user_group = target_user_group
333 333 obj.permission = permission
334 334 self.sa.add(obj)
335 335 log.debug('Granted perm %s to %s on %s', perm, target_user_group, user_group)
336 336 return obj
337 337
338 338 def revoke_user_group_permission(self, target_user_group, user_group):
339 339 """
340 340 Revoke user group permission for given target_user_group
341 341
342 342 :param target_user_group:
343 343 :param user_group:
344 344 """
345 345 target_user_group = self._get_user_group(target_user_group)
346 346 user_group = self._get_user_group(user_group)
347 347
348 348 obj = self.sa.query(UserGroupUserGroupToPerm) \
349 349 .filter(UserGroupUserGroupToPerm.target_user_group == target_user_group) \
350 350 .filter(UserGroupUserGroupToPerm.user_group == user_group) \
351 351 .scalar()
352 352 if obj is not None:
353 353 self.sa.delete(obj)
354 354 log.debug('Revoked perm on %s on %s', target_user_group, user_group)
355 355
356 356 def enforce_groups(self, user, groups, extern_type=None):
357 357 user = self._get_user(user)
358 358 log.debug('Enforcing groups %s on user %s', user, groups)
359 359 current_groups = user.group_member
360 360 # find the external created groups
361 361 externals = [x.users_group for x in current_groups
362 362 if 'extern_type' in x.users_group.group_data]
363 363
364 364 # calculate from what groups user should be removed
365 365 # externals that are not in groups
366 366 for gr in externals:
367 367 if gr.users_group_name not in groups:
368 368 log.debug('Removing user %s from user group %s', user, gr)
369 369 self.remove_user_from_group(gr, user)
370 370
371 371 # now we calculate in which groups user should be == groups params
372 372 owner = User.get_first_admin().username
373 373 for gr in set(groups):
374 374 existing_group = UserGroup.get_by_group_name(gr)
375 375 if not existing_group:
376 376 desc = u'Automatically created from plugin:%s' % extern_type
377 377 # we use first admin account to set the owner of the group
378 378 existing_group = UserGroupModel().create(gr, desc, owner,
379 379 group_data={'extern_type': extern_type})
380 380
381 381 # we can only add users to special groups created via plugins
382 382 managed = 'extern_type' in existing_group.group_data
383 383 if managed:
384 384 log.debug('Adding user %s to user group %s', user, gr)
385 385 UserGroupModel().add_user_to_group(existing_group, user)
386 386 else:
387 387 log.debug('Skipping addition to group %s since it is '
388 388 'not managed by auth plugins' % gr)
@@ -1,188 +1,188 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 4 ## ${comment.comment_block(co)}
5 5 ##
6 6 <%def name="comment_block(co)">
7 7 <div class="comment" id="comment-${co.comment_id}" line="${co.line_no}">
8 8 <div class="comment-prev-next-links"></div>
9 9 <div class="comment-wrapp">
10 10 <div class="meta">
11 11 ${h.gravatar_div(co.author.email, size=20, div_style="float:left")}
12 12 <div class="user">
13 13 ${co.author.full_name_and_username}
14 14 </div>
15 15
16 16 <span>
17 17 ${h.age(co.modified_at)}
18 18 %if co.pull_request:
19 19 ${_('on pull request')}
20 20 <a href="${co.pull_request.url()}">"${co.pull_request.title or _("No title")}"</a>
21 21 %else:
22 22 ${_('on this changeset')}
23 23 %endif
24 24 <a class="permalink" href="${co.url()}">&para;</a>
25 25 </span>
26 26
27 %if co.author.user_id == c.authuser.user_id or h.HasRepoPermissionAny('repository.admin')(c.repo_name):
27 %if co.author_id == c.authuser.user_id or h.HasRepoPermissionAny('repository.admin')(c.repo_name):
28 28 %if co.deletable():
29 29 <div onClick="confirm('${_("Delete comment?")}') && deleteComment(${co.comment_id})" class="buttons delete-comment btn btn-mini" style="margin:0 5px">${_('Delete')}</div>
30 30 %endif
31 31 %endif
32 32 </div>
33 33 <div class="text">
34 34 %if co.status_change:
35 35 <div class="automatic-comment">
36 36 <p>
37 37 <span title="${_('Changeset status')}" class="changeset-status-lbl">${_("Status change")}: ${co.status_change[0].status_lbl}</span>
38 38 <span class="changeset-status-ico"><i class="icon-circle changeset-status-${co.status_change[0].status}"></i></span>
39 39 </p>
40 40 </div>
41 41 %endif
42 42 %if co.text:
43 43 ${h.render_w_mentions(co.text, c.repo_name)|n}
44 44 %endif
45 45 </div>
46 46 </div>
47 47 </div>
48 48 </%def>
49 49
50 50
51 51 <%def name="comment_inline_form()">
52 52 <div id='comment-inline-form-template' style="display:none">
53 53 <div class="ac">
54 54 %if c.authuser.username != 'default':
55 55 ${h.form('#', class_='inline-form')}
56 56 <div class="clearfix">
57 57 <div class="comment-help">${_('Commenting on line.')}
58 58 <span style="color:#577632" class="tooltip">${_('Comments are in plain text. Use @username inside this text to notify another user.')|n}</span>
59 59 </div>
60 60 <div class="mentions-container"></div>
61 61 <textarea name="text" class="comment-block-ta yui-ac-input"></textarea>
62 62
63 63 <div id="status_block_container" class="status-block general-only hidden">
64 64 %if c.pull_request is None:
65 65 ${_('Set changeset status')}:
66 66 %else:
67 67 ${_('Vote for pull request status')}:
68 68 %endif
69 69 <span class="general-only cs-only">
70 70 </span>
71 71 <input type="radio" class="status_change_radio" name="changeset_status" id="changeset_status_unchanged" value="" checked="checked" />
72 72 <label for="changeset_status_unchanged">
73 73 ${_('No change')}
74 74 </label>
75 75 %for status, lbl in c.changeset_statuses:
76 76 <label>
77 77 <input type="radio" class="status_change_radio" name="changeset_status" id="${status}" value="${status}">
78 78 ${lbl}<i class="icon-circle changeset-status-${status}" /></i>
79 79 </label>
80 80 %endfor
81 81
82 82 %if c.pull_request is not None and ( \
83 83 h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) \
84 or c.pull_request.owner.user_id == c.authuser.user_id):
84 or c.pull_request.owner_id == c.authuser.user_id):
85 85 <div>
86 86 ${_('Finish pull request')}:
87 87 <label>
88 88 <input id="save_close" type="checkbox" name="save_close">
89 89 ${_("Close")}
90 90 </label>
91 91 <label>
92 92 <input id="save_delete" type="checkbox" name="save_delete" value="delete">
93 93 ${_("Delete")}
94 94 </label>
95 95 </div>
96 96 %endif
97 97 </div>
98 98
99 99 </div>
100 100 <div class="comment-button">
101 101 <div class="submitting-overlay">${_('Submitting ...')}</div>
102 102 ${h.submit('save', _('Comment'), class_='btn btn-small save-inline-form')}
103 103 ${h.reset('hide-inline-form', _('Cancel'), class_='btn btn-small hide-inline-form')}
104 104 </div>
105 105 ${h.end_form()}
106 106 %else:
107 107 ${h.form('')}
108 108 <div class="clearfix">
109 109 <div class="comment-help">
110 110 ${_('You need to be logged in to comment.')} <a href="${h.url('login_home', came_from=request.path_qs)}">${_('Login now')}</a>
111 111 </div>
112 112 </div>
113 113 <div class="comment-button">
114 114 ${h.reset('hide-inline-form', _('Hide'), class_='btn btn-small hide-inline-form')}
115 115 </div>
116 116 ${h.end_form()}
117 117 %endif
118 118 </div>
119 119 </div>
120 120 </%def>
121 121
122 122
123 123 ## show comment count as "x comments (y inline, z general)"
124 124 <%def name="comment_count(inline_cnt, general_cnt)">
125 125 ${'%s (%s, %s)' % (
126 126 ungettext("%d comment", "%d comments", inline_cnt + general_cnt) % (inline_cnt + general_cnt),
127 127 ungettext("%d inline", "%d inline", inline_cnt) % inline_cnt,
128 128 ungettext("%d general", "%d general", general_cnt) % general_cnt
129 129 )}
130 130 <span class="firstlink"></span>
131 131 </%def>
132 132
133 133
134 134 ## generate inline comments and the main ones
135 135 <%def name="generate_comments()">
136 136 ## original location of comments ... but the ones outside diff context remains here
137 137 <div class="comments inline-comments">
138 138 %for f_path, lines in c.inline_comments:
139 139 %for line_no, comments in lines.iteritems():
140 140 <div class="comments-list-chunk" data-f_path="${f_path}" data-line_no="${line_no}" data-target-id="${h.safeid(h.safe_unicode(f_path))}_${line_no}">
141 141 %for co in comments:
142 142 ${comment_block(co)}
143 143 %endfor
144 144 </div>
145 145 %endfor
146 146 %endfor
147 147
148 148 <div class="comments-list-chunk" data-f_path="" data-line_no="" data-target-id="general-comments">
149 149 %for co in c.comments:
150 150 ${comment_block(co)}
151 151 %endfor
152 152 </div>
153 153 </div>
154 154 <div class="comments-number">
155 155 ${comment_count(c.inline_cnt, len(c.comments))}
156 156 </div>
157 157 </%def>
158 158
159 159 ## MAIN COMMENT FORM
160 160 <%def name="comments(change_status=True)">
161 161
162 162 ## global, shared for all edit boxes
163 163 <div class="mentions-container" id="mentions_container"></div>
164 164
165 165 <div class="inline-comments inline-comments-general
166 166 ${'show-general-status' if change_status else ''}">
167 167 <div id="comments-general-comments" class="">
168 168 ## comment_div for general comments
169 169 </div>
170 170 </div>
171 171
172 172 <script>
173 173
174 174 $(document).ready(function () {
175 175
176 176 $(window).on('beforeunload', function(){
177 177 var $textareas = $('.comment-inline-form textarea[name=text]');
178 178 if($textareas.size() > 1 ||
179 179 $textareas.val()) {
180 180 // this message will not be displayed on all browsers
181 181 // (e.g. some versions of Firefox), but the user will still be warned
182 182 return 'There are uncommitted comments.';
183 183 }
184 184 });
185 185
186 186 });
187 187 </script>
188 188 </%def>
@@ -1,90 +1,90 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%def name="pullrequest_overview(pullrequests)">
4 4
5 5 %if not len(pullrequests):
6 6 <div class="normal-indent empty_data">${_('No entries')}</div>
7 7 <% return %>
8 8 %endif
9 9
10 10 <div class="table">
11 11 <table>
12 12 <thead>
13 13 <tr>
14 14 <th class="left">${_('Vote')}</th>
15 15 <th class="left">${_('Title')}</th>
16 16 <th class="left">${_('Owner')}</th>
17 17 <th class="left">${_('Age')}</th>
18 18 <th class="left">${_('From')}</th>
19 19 <th class="left">${_('To')}</th>
20 20 <th class="right" style="padding-right:5px">${_('Delete')}</th>
21 21 </tr>
22 22 </thead>
23 23 % for pr in pullrequests:
24 24 <tr class="${'pr-closed' if pr.is_closed() else ''}">
25 25 <td width="80px">
26 26 <% status = pr.user_review_status(c.authuser.user_id) %>
27 27 %if status:
28 28 <i class="icon-circle changeset-status-${status}" title="${_("You voted: %s") % h.changeset_status_lbl(status)}"></i>
29 29 %else:
30 30 <i class="icon-circle changeset-status-not_reviewed" title="${_("You didn't vote")}"></i>
31 31 %endif
32 32 </td>
33 33 <td>
34 34 <a href="${pr.url()}">
35 35 ${pr.title or _("(no title)")}
36 36 %if pr.is_closed():
37 37 <span class="pr-closed-tag">${_('Closed')}</span>
38 38 %endif
39 39 </a>
40 40 </td>
41 41 <td>
42 42 ${pr.owner.full_name_and_username}
43 43 </td>
44 44 <td>
45 45 <span class="tooltip" title="${h.fmt_date(pr.created_on)}">
46 46 ${h.age(pr.created_on)}
47 47 </span>
48 48 </td>
49 49 <td>
50 50 <% org_ref_name=pr.org_ref.rsplit(':', 2)[-2] %>
51 51 <a href="${h.url('summary_home', repo_name=pr.org_repo.repo_name, anchor=org_ref_name)}">
52 52 ${pr.org_repo.repo_name}#${org_ref_name}
53 53 </a>
54 54 </td>
55 55 <td>
56 56 <% other_ref_name=pr.other_ref.rsplit(':', 2)[-2] %>
57 57 <a href="${h.url('summary_home', repo_name=pr.other_repo.repo_name, anchor=other_ref_name)}">
58 58 ${pr.other_repo.repo_name}#${other_ref_name}
59 59 </a>
60 60 </td>
61 61 <td style="text-align:right">
62 %if pr.owner.user_id == c.authuser.user_id:
62 %if pr.owner_id == c.authuser.user_id:
63 63 ${h.form(url('pullrequest_delete', repo_name=pr.other_repo.repo_name, pull_request_id=pr.pull_request_id), style="display:inline-block")}
64 64 <button class="action_button"
65 65 id="remove_${pr.pull_request_id}"
66 66 name="remove_${pr.pull_request_id}"
67 67 title="${_('Delete Pull Request')}"
68 68 onclick="return confirm('${_('Confirm to delete this pull request')}')
69 69 && ((${len(pr.comments)} == 0) ||
70 70 confirm('${_('Confirm again to delete this pull request with %s comments') % len(pr.comments)}'))
71 71 ">
72 72 <i class="icon-minus-circled"></i>
73 73 </button>
74 74 ${h.end_form()}
75 75 %endif
76 76 </td>
77 77 </tr>
78 78 % endfor
79 79 </table>
80 80 </div>
81 81
82 82 %if hasattr(pullrequests, 'pager'):
83 83 <div class="notification-paginator">
84 84 <div class="pagination-wh pagination-left">
85 85 ${pullrequests.pager('$link_previous ~2~ $link_next', **request.GET.mixed())}
86 86 </div>
87 87 </div>
88 88 %endif
89 89
90 90 </%def>
@@ -1,427 +1,427 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 4
5 5 <%block name="title">
6 6 ${_('%s Pull Request %s') % (c.repo_name, c.pull_request.nice_id())}
7 7 </%block>
8 8
9 9 <%def name="breadcrumbs_links()">
10 10 ${_('Pull request %s from %s#%s') % (c.pull_request.nice_id(), c.pull_request.org_repo.repo_name, c.cs_branch_name)}
11 11 </%def>
12 12
13 13 <%block name="header_menu">
14 14 ${self.menu('repositories')}
15 15 </%block>
16 16
17 17 <%def name="main()">
18 <% editable = not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or c.pull_request.owner.user_id == c.authuser.user_id) %>
18 <% editable = not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or c.pull_request.owner_id == c.authuser.user_id) %>
19 19 ${self.repo_context_bar('showpullrequest')}
20 20 <div class="box">
21 21 <!-- box / title -->
22 22 <div class="title">
23 23 ${self.breadcrumbs()}
24 24 </div>
25 25
26 26 ${h.form(url('pullrequest_post', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), method='post', id='pull_request_form')}
27 27 <div class="form pr-box" style="float: left">
28 28 <div class="pr-details-title ${'closed' if c.pull_request.is_closed() else ''}">
29 29 ${_('Title')}: ${c.pull_request.title}
30 30 %if c.pull_request.is_closed():
31 31 (${_('Closed')})
32 32 %endif
33 33 </div>
34 34 <div id="pr-summary" class="fields">
35 35
36 36 <div class="field pr-not-edit" style="min-height:37px">
37 37 <div class="label-summary">
38 38 <label>${_('Description')}:</label>
39 39 %if editable:
40 40 <div style="margin: 5px">
41 41 <a class="btn btn-small" onclick="$('.pr-do-edit').show();$('.pr-not-edit').hide()">${_("Edit")}</a>
42 42 </div>
43 43 %endif
44 44 </div>
45 45 <div class="input">
46 46 <div class="formatted-fixed">${h.urlify_text(c.pull_request.description, c.pull_request.org_repo.repo_name)}</div>
47 47 </div>
48 48 </div>
49 49
50 50 %if editable:
51 51 <div class="pr-do-edit" style="display:none">
52 52 <div class="field">
53 53 <div class="label-summary">
54 54 <label for="pullrequest_title">${_('Title')}:</label>
55 55 </div>
56 56 <div class="input">
57 57 ${h.text('pullrequest_title',class_="large",value=c.pull_request.title,placeholder=_('Summarize the changes'))}
58 58 </div>
59 59 </div>
60 60
61 61 <div class="field">
62 62 <div class="label-summary label-textarea">
63 63 <label for="pullrequest_desc">${_('Description')}:</label>
64 64 </div>
65 65 <div class="textarea text-area editor">
66 66 ${h.textarea('pullrequest_desc',size=30,content=c.pull_request.description,placeholder=_('Write a short description on this pull request'))}
67 67 </div>
68 68 </div>
69 69 </div>
70 70 %endif
71 71
72 72 <div class="field">
73 73 <div class="label-summary">
74 74 <label>${_('Reviewer voting result')}:</label>
75 75 </div>
76 76 <div class="input">
77 77 <div class="changeset-status-container" style="float:none;clear:both">
78 78 %if c.current_voting_result:
79 79 <span class="changeset-status-ico" style="padding:0px 4px 0px 0px">
80 80 <i class="icon-circle changeset-status-${c.current_voting_result}" title="${_('Pull request status calculated from votes')}"></i></span>
81 81 <span class="changeset-status-lbl tooltip" title="${_('Pull request status calculated from votes')}">
82 82 %if c.pull_request.is_closed():
83 83 ${_('Closed')},
84 84 %endif
85 85 ${h.changeset_status_lbl(c.current_voting_result)}
86 86 </span>
87 87 %endif
88 88 </div>
89 89 </div>
90 90 </div>
91 91 <div class="field">
92 92 <div class="label-summary">
93 93 <label>${_('Still not reviewed by')}:</label>
94 94 </div>
95 95 <div class="input">
96 96 % if len(c.pull_request_pending_reviewers) > 0:
97 97 <div class="tooltip" title="${', '.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>
98 98 % elif len(c.pull_request_reviewers) > 0:
99 99 <div>${_('Pull request was reviewed by all reviewers')}</div>
100 100 %else:
101 101 <div>${_('There are no reviewers')}</div>
102 102 %endif
103 103 </div>
104 104 </div>
105 105 <div class="field">
106 106 <div class="label-summary">
107 107 <label>${_('Origin')}:</label>
108 108 </div>
109 109 <div class="input">
110 110 <div>
111 111 ${h.link_to_ref(c.pull_request.org_repo.repo_name, c.cs_ref_type, c.cs_ref_name, c.cs_rev)}
112 112 %if c.cs_ref_type != 'branch':
113 113 ${_('on')} ${h.link_to_ref(c.pull_request.org_repo.repo_name, 'branch', c.cs_branch_name)}
114 114 %endif
115 115 </div>
116 116 </div>
117 117 </div>
118 118 <div class="field">
119 119 <div class="label-summary">
120 120 <label>${_('Target')}:</label>
121 121 </div>
122 122 <div class="input">
123 123 %if c.is_range:
124 124 ${_("This is just a range of changesets and doesn't have a target or a real merge ancestor.")}
125 125 %else:
126 126 ${h.link_to_ref(c.pull_request.other_repo.repo_name, c.a_ref_type, c.a_ref_name)}
127 127 ## we don't know other rev - c.a_rev is ancestor and not necessarily on other_name_branch branch
128 128 %endif
129 129 </div>
130 130 </div>
131 131 <div class="field">
132 132 <div class="label-summary">
133 133 <label>${_('Pull changes')}:</label>
134 134 </div>
135 135 <div class="input">
136 136 %if c.cs_ranges:
137 137 <div>
138 138 ## TODO: use cs_ranges[-1] or org_ref_parts[1] in both cases?
139 139 %if h.is_hg(c.pull_request.org_repo):
140 140 <span style="font-family: monospace">hg pull ${c.pull_request.org_repo.clone_url()} -r ${h.short_id(c.cs_ranges[-1].raw_id)}</span>
141 141 %elif h.is_git(c.pull_request.org_repo):
142 142 <span style="font-family: monospace">git pull ${c.pull_request.org_repo.clone_url()} ${c.pull_request.org_ref_parts[1]}</span>
143 143 %endif
144 144 </div>
145 145 %endif
146 146 </div>
147 147 </div>
148 148 <div class="field">
149 149 <div class="label-summary">
150 150 <label>${_('Created on')}:</label>
151 151 </div>
152 152 <div class="input">
153 153 <div>${h.fmt_date(c.pull_request.created_on)}</div>
154 154 </div>
155 155 </div>
156 156 <div class="field">
157 157 <div class="label-summary">
158 158 <label>${_('Owner')}:</label>
159 159 </div>
160 160 <div class="input pr-not-edit">
161 161 ${h.gravatar_div(c.pull_request.owner.email, size=20)}
162 162 <span>${c.pull_request.owner.full_name_and_username}</span><br/>
163 163 <span><a href="mailto:${c.pull_request.owner.email}">${c.pull_request.owner.email}</a></span><br/>
164 164 </div>
165 165 <div class="input pr-do-edit ac" style="display:none">
166 166 ${h.text('owner', class_="large", value=c.pull_request.owner.username, placeholder=_('Username'))}
167 167 <div id="owner_completion_container"></div>
168 168 </div>
169 169 </div>
170 170
171 171 <div class="field">
172 172 <div class="label-summary">
173 173 <label>${_('Next iteration')}:</label>
174 174 </div>
175 175 <div class="input">
176 176 <div class="msg-div">${c.update_msg}</div>
177 177 %if c.avail_revs:
178 178 <div id="updaterevs" style="max-height:200px; overflow-y:auto; overflow-x:hidden; margin-bottom: 10px; padding: 1px 0">
179 179 <div style="height:0">
180 180 <canvas id="avail_graph_canvas" style="width:0"></canvas>
181 181 </div>
182 182 <table id="updaterevs-table" class="noborder" style="padding-left:50px">
183 183 %for cnt, cs in enumerate(c.avail_cs):
184 184 <tr id="chg_available_${cnt+1}" class="${'mergerow' if len(cs.parents) > 1 and not (editable and cs.revision in c.avail_revs) else ''}">
185 185 %if c.cs_ranges and cs.revision == c.cs_ranges[-1].revision:
186 186 <td>
187 187 %if editable:
188 188 ${h.radio(name='updaterev', value='', checked=True)}
189 189 %endif
190 190 </td>
191 191 <td colspan="4">${_("Current revision - no change")}</td>
192 192 %else:
193 193 <td>
194 194 %if editable and cs.revision in c.avail_revs:
195 195 ${h.radio(name='updaterev', value=cs.raw_id)}
196 196 %endif
197 197 </td>
198 198 <td style="width: 120px"><span class="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
199 199 <td>${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id), class_='changeset_hash')}</td>
200 200 <td>
201 201 <div style="float: right; margin-top: -4px;">
202 202 %for tag in cs.tags:
203 203 <div class="tagtag" title="${_('Tag %s') % tag}">
204 204 ${h.link_to(tag,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
205 205 </div>
206 206 %endfor
207 207 </div>
208 208 <div class="message" style="white-space:normal; height:1.1em; max-width: 500px; padding:0">${h.urlify_text(cs.message, c.repo_name)}</div>
209 209 </td>
210 210 %endif
211 211 </tr>
212 212 %endfor
213 213 </table>
214 214 </div>
215 215 <div class="msg-div">(${_("Pull request iterations do not change content once created. Select a revision and save to make a new iteration.")})</div>
216 216 %endif
217 217 <div class="msg-div">${c.update_msg_other}</div>
218 218 </div>
219 219 </div>
220 220 </div>
221 221 </div>
222 222 ## REVIEWERS
223 223 <div style="float:left; border-left:1px dashed #eee">
224 224 <div class="pr-details-title">${_('Pull Request Reviewers')}</div>
225 225 <div id="reviewers" style="padding:0px 0px 5px 10px">
226 226 ## members goes here !
227 227 <div>
228 228 <ul id="review_members">
229 229 %for member,status in c.pull_request_reviewers:
230 230 ## WARNING: the HTML below is duplicate with
231 231 ## kallithea/public/js/base.js
232 232 ## If you change something here it should be reflected in the template too.
233 233 <li id="reviewer_${member.user_id}">
234 234 <div class="reviewers_member">
235 235 <div class="reviewer_status tooltip" title="${h.changeset_status_lbl(status)}">
236 236 <i class="icon-circle changeset-status-${status}"></i>
237 237 </div>
238 238 ${h.gravatar_div(member.email, size=14, div_class="reviewer_gravatar gravatar")}
239 239 <div style="float:left;">
240 240 ${member.full_name_and_username}
241 241 %if c.pull_request.owner_id == member.user_id:
242 242 (${_('Owner')})
243 243 %endif
244 244 </div>
245 245 <input type="hidden" value="${member.user_id}" name="review_members" />
246 246 %if editable:
247 247 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id})" title="${_('Remove reviewer')}">
248 248 <i class="icon-minus-circled"></i>
249 249 </div>
250 250 %endif
251 251 </div>
252 252 </li>
253 253 %endfor
254 254 </ul>
255 255 </div>
256 256 %if editable:
257 257 <div class='ac'>
258 258 <div class="reviewer_ac">
259 259 ${h.text('user', class_='yui-ac-input',placeholder=_('Type name of reviewer to add'))}
260 260 <div id="reviewers_container"></div>
261 261 </div>
262 262 </div>
263 263 %endif
264 264 </div>
265 265
266 266 %if not c.pull_request_reviewers:
267 267 <div class="pr-details-title">${_('Potential Reviewers')}</div>
268 268 <div style="margin: 10px 0 10px 10px; max-width: 250px">
269 269 <div>
270 270 ${_('Click to add the repository owner as reviewer:')}
271 271 </div>
272 272 <ul style="margin-top: 10px">
273 273 %for u in [c.pull_request.other_repo.owner]:
274 274 <li>
275 275 <a class="missing_reviewer missing_reviewer_${u.user_id}"
276 276 user_id="${u.user_id}"
277 277 fname="${u.name}"
278 278 lname="${u.lastname}"
279 279 nname="${u.username}"
280 280 gravatar_lnk="${h.gravatar_url(u.email, size=28, default='default')}"
281 281 gravatar_size="14"
282 282 title="Click to add reviewer to the list, then Save Changes.">${u.full_name}</a>
283 283 </li>
284 284 %endfor
285 285 </ul>
286 286 </div>
287 287 %endif
288 288 </div>
289 289 <div class="form" style="clear:both">
290 290 <div class="fields">
291 291 %if editable:
292 292 <div class="buttons">
293 293 ${h.submit('pr-form-save',_('Save Changes'),class_="btn btn-small")}
294 294 ${h.submit('pr-form-clone',_('Create New Iteration with Changes'),class_="btn btn-small",disabled='disabled')}
295 295 ${h.reset('pr-form-reset',_('Cancel Changes'),class_="btn btn-small")}
296 296 </div>
297 297 %endif
298 298 </div>
299 299 </div>
300 300 ${h.end_form()}
301 301 </div>
302 302
303 303 <div class="box">
304 304 <div class="title">
305 305 <div class="breadcrumbs">${_('Pull Request Content')}</div>
306 306 </div>
307 307 <div class="table">
308 308 <div id="changeset_compare_view_content">
309 309 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
310 310 ${comment.comment_count(c.inline_cnt, len(c.comments))}
311 311 </div>
312 312 ##CS
313 313 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
314 314 ${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}
315 315 </div>
316 316 <%include file="/compare/compare_cs.html" />
317 317
318 318 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
319 319 ${_('Common ancestor')}:
320 320 ${h.link_to(h.short_id(c.a_rev),h.url('changeset_home',repo_name=c.a_repo.repo_name,revision=c.a_rev), class_="changeset_hash")}
321 321 </div>
322 322
323 323 ## FILES
324 324 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
325 325
326 326 % if c.limited_diff:
327 327 ${ungettext('%s file changed', '%s files changed', len(c.file_diff_data)) % len(c.file_diff_data)}:
328 328 % else:
329 329 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.file_diff_data)) % (len(c.file_diff_data),c.lines_added,c.lines_deleted)}:
330 330 %endif
331 331
332 332 </div>
333 333 <div class="cs_files">
334 334 %if not c.file_diff_data:
335 335 <span class="empty_data">${_('No files')}</span>
336 336 %endif
337 337 %for fid, (url_fid, op, a_path, path, diff, stats) in c.file_diff_data.iteritems():
338 338 <div class="cs_${op}">
339 339 <div class="node">
340 340 <i class="icon-diff-${op}"></i>
341 341 ${h.link_to(h.safe_unicode(path), '#%s' % fid)}
342 342 </div>
343 343 <div class="changes">${h.fancy_file_stats(stats)}</div>
344 344 </div>
345 345 %endfor
346 346 %if c.limited_diff:
347 347 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h5>
348 348 %endif
349 349 </div>
350 350 </div>
351 351 </div>
352 352 <script>
353 353 var _USERS_AC_DATA = ${c.users_array|n};
354 354 var _GROUPS_AC_DATA = ${c.user_groups_array|n};
355 355 // TODO: switch this to pyroutes
356 356 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
357 357 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
358 358
359 359 pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
360 360 pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
361 361
362 362 </script>
363 363
364 364 ## diff block
365 365 <div class="commentable-diff">
366 366 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
367 367 ${diff_block.diff_block_js()}
368 368 ${diff_block.diff_block(c.a_repo.repo_name, c.a_ref_type, c.a_ref_name, c.a_rev,
369 369 c.cs_repo.repo_name, c.cs_ref_type, c.cs_ref_name, c.cs_rev, c.file_diff_data)}
370 370 % if c.limited_diff:
371 371 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h4>
372 372 % endif
373 373 </div>
374 374
375 375 ## template for inline comment form
376 376 ${comment.comment_inline_form()}
377 377
378 378 ## render comments and inlines
379 379 ${comment.generate_comments()}
380 380
381 381 ## main comment form and it status
382 382 ${comment.comments(change_status=c.allowed_to_change_status)}
383 383
384 384 <script type="text/javascript">
385 385 $(document).ready(function(){
386 386 PullRequestAutoComplete($('#user'), $('#reviewers_container'), _USERS_AC_DATA);
387 387 SimpleUserAutoComplete($('#owner'), $('#owner_completion_container'), _USERS_AC_DATA);
388 388
389 389 $('.code-difftable').on('click', '.add-bubble', function(e){
390 390 show_comment_form($(this));
391 391 });
392 392
393 393 var avail_jsdata = ${c.avail_jsdata|n};
394 394 var avail_r = new BranchRenderer('avail_graph_canvas', 'updaterevs-table', 'chg_available_');
395 395 avail_r.render(avail_jsdata,40);
396 396
397 397 move_comments($(".comments .comments-list-chunk"));
398 398
399 399 $('#updaterevs input').change(function(e){
400 400 var update = !!e.target.value;
401 401 $('#pr-form-save').prop('disabled',update);
402 402 $('#pr-form-clone').prop('disabled',!update);
403 403 });
404 404 var $org_review_members = $('#review_members').clone();
405 405 $('#pr-form-reset').click(function(e){
406 406 $('.pr-do-edit').hide();
407 407 $('.pr-not-edit').show();
408 408 $('#pr-form-save').prop('disabled',false);
409 409 $('#pr-form-clone').prop('disabled',true);
410 410 $('#review_members').html($org_review_members);
411 411 });
412 412
413 413 // hack: re-navigate to target after JS is done ... if a target is set and setting href thus won't reload
414 414 if (window.location.hash != "") {
415 415 window.location.href = window.location.href;
416 416 }
417 417
418 418 $('.missing_reviewer').click(function(){
419 419 var $this = $(this);
420 420 addReviewMember($this.attr('user_id'), $this.attr('fname'), $this.attr('lname'), $this.attr('nname'), $this.attr('gravatar_lnk'), $this.attr('gravatar_size'));
421 421 });
422 422 });
423 423 </script>
424 424
425 425 </div>
426 426
427 427 </%def>
@@ -1,284 +1,284 b''
1 1 import os
2 2 import re
3 3
4 4 import mock
5 5 import routes.util
6 6
7 7 from kallithea.tests.base import *
8 8 from kallithea.lib import helpers as h
9 9 from kallithea.model.db import User, Notification, UserNotification
10 10 from kallithea.model.user import UserModel
11 11 from kallithea.model.meta import Session
12 12 from kallithea.model.notification import NotificationModel, EmailNotificationModel
13 13
14 14 import kallithea.lib.celerylib
15 15 import kallithea.lib.celerylib.tasks
16 16
17 17
18 18 class TestNotifications(TestController):
19 19
20 20 def setup_method(self, method):
21 21 Session.remove()
22 22 u1 = UserModel().create_or_update(username=u'u1',
23 23 password=u'qweqwe',
24 24 email=u'u1@example.com',
25 25 firstname=u'u1', lastname=u'u1')
26 26 Session().commit()
27 27 self.u1 = u1.user_id
28 28
29 29 u2 = UserModel().create_or_update(username=u'u2',
30 30 password=u'qweqwe',
31 31 email=u'u2@example.com',
32 32 firstname=u'u2', lastname=u'u3')
33 33 Session().commit()
34 34 self.u2 = u2.user_id
35 35
36 36 u3 = UserModel().create_or_update(username=u'u3',
37 37 password=u'qweqwe',
38 38 email=u'u3@example.com',
39 39 firstname=u'u3', lastname=u'u3')
40 40 Session().commit()
41 41 self.u3 = u3.user_id
42 42
43 43 self.remove_all_notifications()
44 44 assert [] == Notification.query().all()
45 45 assert [] == UserNotification.query().all()
46 46
47 47 def test_create_notification(self):
48 48 usrs = [self.u1, self.u2]
49 49 def send_email(recipients, subject, body='', html_body='', headers=None, author=None):
50 50 assert recipients == ['u2@example.com']
51 51 assert subject == 'Test Message'
52 52 assert body == u"hi there"
53 53 assert '>hi there<' in html_body
54 54 assert author.username == 'u1'
55 55 with mock.patch.object(kallithea.lib.celerylib.tasks, 'send_email', send_email):
56 56 notification = NotificationModel().create(created_by=self.u1,
57 57 subject=u'subj', body=u'hi there',
58 58 recipients=usrs)
59 59 Session().commit()
60 60 u1 = User.get(self.u1)
61 61 u2 = User.get(self.u2)
62 62 u3 = User.get(self.u3)
63 63 notifications = Notification.query().all()
64 64 assert len(notifications) == 1
65 65
66 66 assert notifications[0].recipients == [u1, u2]
67 67 assert notification.notification_id == notifications[0].notification_id
68 68
69 69 unotification = UserNotification.query() \
70 70 .filter(UserNotification.notification == notification).all()
71 71
72 72 assert len(unotification) == len(usrs)
73 assert set([x.user.user_id for x in unotification]) == set(usrs)
73 assert set([x.user_id for x in unotification]) == set(usrs)
74 74
75 75 def test_user_notifications(self):
76 76 notification1 = NotificationModel().create(created_by=self.u1,
77 77 subject=u'subj', body=u'hi there1',
78 78 recipients=[self.u3])
79 79 Session().commit()
80 80 notification2 = NotificationModel().create(created_by=self.u1,
81 81 subject=u'subj', body=u'hi there2',
82 82 recipients=[self.u3])
83 83 Session().commit()
84 84 u3 = Session().query(User).get(self.u3)
85 85
86 86 assert sorted([x.notification for x in u3.notifications]) == sorted([notification2, notification1])
87 87
88 88 def test_delete_notifications(self):
89 89 notification = NotificationModel().create(created_by=self.u1,
90 90 subject=u'title', body=u'hi there3',
91 91 recipients=[self.u3, self.u1, self.u2])
92 92 Session().commit()
93 93 notifications = Notification.query().all()
94 94 assert notification in notifications
95 95
96 96 Notification.delete(notification.notification_id)
97 97 Session().commit()
98 98
99 99 notifications = Notification.query().all()
100 100 assert not notification in notifications
101 101
102 102 un = UserNotification.query().filter(UserNotification.notification
103 103 == notification).all()
104 104 assert un == []
105 105
106 106 def test_delete_association(self):
107 107 notification = NotificationModel().create(created_by=self.u1,
108 108 subject=u'title', body=u'hi there3',
109 109 recipients=[self.u3, self.u1, self.u2])
110 110 Session().commit()
111 111
112 112 unotification = UserNotification.query() \
113 113 .filter(UserNotification.notification ==
114 114 notification) \
115 115 .filter(UserNotification.user_id == self.u3) \
116 116 .scalar()
117 117
118 118 assert unotification.user_id == self.u3
119 119
120 120 NotificationModel().delete(self.u3,
121 121 notification.notification_id)
122 122 Session().commit()
123 123
124 124 u3notification = UserNotification.query() \
125 125 .filter(UserNotification.notification ==
126 126 notification) \
127 127 .filter(UserNotification.user_id == self.u3) \
128 128 .scalar()
129 129
130 130 assert u3notification == None
131 131
132 132 # notification object is still there
133 133 assert Notification.query().all() == [notification]
134 134
135 135 #u1 and u2 still have assignments
136 136 u1notification = UserNotification.query() \
137 137 .filter(UserNotification.notification ==
138 138 notification) \
139 139 .filter(UserNotification.user_id == self.u1) \
140 140 .scalar()
141 141 assert u1notification != None
142 142 u2notification = UserNotification.query() \
143 143 .filter(UserNotification.notification ==
144 144 notification) \
145 145 .filter(UserNotification.user_id == self.u2) \
146 146 .scalar()
147 147 assert u2notification != None
148 148
149 149 def test_notification_counter(self):
150 150 NotificationModel().create(created_by=self.u1,
151 151 subject=u'title', body=u'hi there_delete',
152 152 recipients=[self.u3, self.u1])
153 153 Session().commit()
154 154
155 155 assert NotificationModel().get_unread_cnt_for_user(self.u1) == 0
156 156 assert NotificationModel().get_unread_cnt_for_user(self.u2) == 0
157 157 assert NotificationModel().get_unread_cnt_for_user(self.u3) == 1
158 158
159 159 notification = NotificationModel().create(created_by=self.u1,
160 160 subject=u'title', body=u'hi there3',
161 161 recipients=[self.u3, self.u1, self.u2])
162 162 Session().commit()
163 163
164 164 assert NotificationModel().get_unread_cnt_for_user(self.u1) == 0
165 165 assert NotificationModel().get_unread_cnt_for_user(self.u2) == 1
166 166 assert NotificationModel().get_unread_cnt_for_user(self.u3) == 2
167 167
168 168 @mock.patch.object(h, 'canonical_url', (lambda arg, **kwargs: 'http://%s/?%s' % (arg, '&'.join('%s=%s' % (k, v) for (k, v) in sorted(kwargs.items())))))
169 169 def test_dump_html_mails(self):
170 170 # Exercise all notification types and dump them to one big html file
171 171 l = []
172 172
173 173 def send_email(recipients, subject, body='', html_body='', headers=None, author=None):
174 174 l.append('<hr/>\n')
175 175 l.append('<h1>%s</h1>\n' % desc) # desc is from outer scope
176 176 l.append('<pre>\n')
177 177 l.append('From: %s\n' % author.username)
178 178 l.append('To: %s\n' % ' '.join(recipients))
179 179 l.append('Subject: %s\n' % subject)
180 180 l.append('</pre>\n')
181 181 l.append('<hr/>\n')
182 182 l.append('<pre>%s</pre>\n' % body)
183 183 l.append('<hr/>\n')
184 184 l.append(html_body)
185 185 l.append('<hr/>\n')
186 186
187 187 with mock.patch.object(kallithea.lib.celerylib.tasks, 'send_email', send_email):
188 188 pr_kwargs = dict(
189 189 pr_nice_id='#7',
190 190 pr_title='The Title',
191 191 pr_title_short='The Title',
192 192 pr_url='http://pr.org/7',
193 193 pr_target_repo='http://mainline.com/repo',
194 194 pr_target_branch='trunk',
195 195 pr_source_repo='https://dev.org/repo',
196 196 pr_source_branch='devbranch',
197 197 pr_owner=User.get(self.u2),
198 198 pr_owner_username='u2'
199 199 )
200 200
201 201 for type_, body, kwargs in [
202 202 (Notification.TYPE_CHANGESET_COMMENT,
203 203 u'This is the new comment.\n\n - and here it ends indented.',
204 204 dict(
205 205 short_id='cafe1234',
206 206 raw_id='cafe1234c0ffeecafe',
207 207 branch='brunch',
208 208 cs_comment_user='Opinionated User (jsmith)',
209 209 cs_comment_url='http://comment.org',
210 210 is_mention=[False, True],
211 211 message='This changeset did something clever which is hard to explain',
212 212 message_short='This changeset did something cl...',
213 213 status_change=[None, 'Approved'],
214 214 cs_target_repo='repo_target',
215 215 cs_url='http://changeset.com',
216 216 cs_author=User.get(self.u2))),
217 217 (Notification.TYPE_MESSAGE,
218 218 u'This is the body of the test message\n - nothing interesting here except indentation.',
219 219 dict()),
220 220 #(Notification.TYPE_MENTION, '$body', None), # not used
221 221 (Notification.TYPE_REGISTRATION,
222 222 u'Registration body',
223 223 dict(
224 224 new_username='newbie',
225 225 registered_user_url='http://newbie.org',
226 226 new_email='new@email.com',
227 227 new_full_name='New Full Name')),
228 228 (Notification.TYPE_PULL_REQUEST,
229 229 u'This PR is awesome because it does stuff\n - please approve indented!',
230 230 dict(
231 231 pr_user_created='Requesting User (root)', # pr_owner should perhaps be used for @mention in description ...
232 232 is_mention=[False, True],
233 233 pr_revisions=[('123abc'*7, "Introduce one and two\n\nand that's it"), ('567fed'*7, 'Make one plus two equal tree')],
234 234 org_repo_name='repo_org',
235 235 **pr_kwargs)),
236 236 (Notification.TYPE_PULL_REQUEST_COMMENT,
237 237 u'Me too!\n\n - and indented on second line',
238 238 dict(
239 239 closing_pr=[False, True],
240 240 is_mention=[False, True],
241 241 pr_comment_user='Opinionated User (jsmith)',
242 242 pr_comment_url='http://pr.org/comment',
243 243 status_change=[None, 'Under Review'],
244 244 **pr_kwargs)),
245 245 ]:
246 246 kwargs['repo_name'] = u'repo/name'
247 247 params = [(type_, type_, body, kwargs)]
248 248 for param_name in ['is_mention', 'status_change', 'closing_pr']: # TODO: inline/general
249 249 if not isinstance(kwargs.get(param_name), list):
250 250 continue
251 251 new_params = []
252 252 for v in kwargs[param_name]:
253 253 for desc, type_, body, kwargs in params:
254 254 kwargs = dict(kwargs)
255 255 kwargs[param_name] = v
256 256 new_params.append(('%s, %s=%r' % (desc, param_name, v), type_, body, kwargs))
257 257 params = new_params
258 258
259 259 for desc, type_, body, kwargs in params:
260 260 # desc is used as "global" variable
261 261 notification = NotificationModel().create(created_by=self.u1,
262 262 subject=u'unused', body=body, email_kwargs=kwargs,
263 263 recipients=[self.u2], type_=type_)
264 264
265 265 # Email type TYPE_PASSWORD_RESET has no corresponding notification type - test it directly:
266 266 desc = 'TYPE_PASSWORD_RESET'
267 267 kwargs = dict(user='John Doe', reset_token='decbf64715098db5b0bd23eab44bd792670ab746', reset_url='http://reset.com/decbf64715098db5b0bd23eab44bd792670ab746')
268 268 kallithea.lib.celerylib.tasks.send_email(['john@doe.com'],
269 269 "Password reset link",
270 270 EmailNotificationModel().get_email_tmpl(EmailNotificationModel.TYPE_PASSWORD_RESET, 'txt', **kwargs),
271 271 EmailNotificationModel().get_email_tmpl(EmailNotificationModel.TYPE_PASSWORD_RESET, 'html', **kwargs),
272 272 author=User.get(self.u1))
273 273
274 274 out = '<!doctype html>\n<html lang="en">\n<head><title>Notifications</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>\n<body>\n%s\n</body>\n</html>\n' % \
275 275 re.sub(r'<(/?(?:!doctype|html|head|title|meta|body)\b[^>]*)>', r'<!--\1-->', ''.join(l))
276 276
277 277 outfn = os.path.join(os.path.dirname(__file__), 'test_dump_html_mails.out.html')
278 278 reffn = os.path.join(os.path.dirname(__file__), 'test_dump_html_mails.ref.html')
279 279 with file(outfn, 'w') as f:
280 280 f.write(out)
281 281 with file(reffn) as f:
282 282 ref = f.read()
283 283 assert ref == out # copy test_dump_html_mails.out.html to test_dump_html_mails.ref.html to update expectations
284 284 os.unlink(outfn)
General Comments 0
You need to be logged in to leave comments. Login now