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