##// END OF EJS Templates
controllers: simplify request.GET.get for safe_int...
Mads Kiilerich -
r5992:fb64046d default
parent child Browse files
Show More
@@ -1,148 +1,148 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.admin
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Controller for Admin panel of Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 7, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28
29 29 import logging
30 30
31 31 from pylons import request, tmpl_context as c, url
32 32 from sqlalchemy.orm import joinedload
33 33 from whoosh.qparser.default import QueryParser
34 34 from whoosh.qparser.dateparse import DateParserPlugin
35 35 from whoosh import query
36 36 from sqlalchemy.sql.expression import or_, and_, func
37 37
38 38 from kallithea.model.db import UserLog
39 39 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
40 40 from kallithea.lib.base import BaseController, render
41 41 from kallithea.lib.utils2 import safe_int, remove_prefix, remove_suffix
42 42 from kallithea.lib.indexers import JOURNAL_SCHEMA
43 43 from kallithea.lib.helpers import Page
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 def _journal_filter(user_log, search_term):
50 50 """
51 51 Filters sqlalchemy user_log based on search_term with whoosh Query language
52 52 http://packages.python.org/Whoosh/querylang.html
53 53
54 54 :param user_log:
55 55 :param search_term:
56 56 """
57 57 log.debug('Initial search term: %r', search_term)
58 58 qry = None
59 59 if search_term:
60 60 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
61 61 qp.add_plugin(DateParserPlugin())
62 62 qry = qp.parse(unicode(search_term))
63 63 log.debug('Filtering using parsed query %r', qry)
64 64
65 65 def wildcard_handler(col, wc_term):
66 66 if wc_term.startswith('*') and not wc_term.endswith('*'):
67 67 #postfix == endswith
68 68 wc_term = remove_prefix(wc_term, prefix='*')
69 69 return func.lower(col).endswith(wc_term)
70 70 elif wc_term.startswith('*') and wc_term.endswith('*'):
71 71 #wildcard == ilike
72 72 wc_term = remove_prefix(wc_term, prefix='*')
73 73 wc_term = remove_suffix(wc_term, suffix='*')
74 74 return func.lower(col).contains(wc_term)
75 75
76 76 def get_filterion(field, val, term):
77 77
78 78 if field == 'repository':
79 79 field = getattr(UserLog, 'repository_name')
80 80 elif field == 'ip':
81 81 field = getattr(UserLog, 'user_ip')
82 82 elif field == 'date':
83 83 field = getattr(UserLog, 'action_date')
84 84 elif field == 'username':
85 85 field = getattr(UserLog, 'username')
86 86 else:
87 87 field = getattr(UserLog, field)
88 88 log.debug('filter field: %s val=>%s', field, val)
89 89
90 90 #sql filtering
91 91 if isinstance(term, query.Wildcard):
92 92 return wildcard_handler(field, val)
93 93 elif isinstance(term, query.Prefix):
94 94 return func.lower(field).startswith(func.lower(val))
95 95 elif isinstance(term, query.DateRange):
96 96 return and_(field >= val[0], field <= val[1])
97 97 return func.lower(field) == func.lower(val)
98 98
99 99 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard,
100 100 query.DateRange)):
101 101 if not isinstance(qry, query.And):
102 102 qry = [qry]
103 103 for term in qry:
104 104 field = term.fieldname
105 105 val = (term.text if not isinstance(term, query.DateRange)
106 106 else [term.startdate, term.enddate])
107 107 user_log = user_log.filter(get_filterion(field, val, term))
108 108 elif isinstance(qry, query.Or):
109 109 filters = []
110 110 for term in qry:
111 111 field = term.fieldname
112 112 val = (term.text if not isinstance(term, query.DateRange)
113 113 else [term.startdate, term.enddate])
114 114 filters.append(get_filterion(field, val, term))
115 115 user_log = user_log.filter(or_(*filters))
116 116
117 117 return user_log
118 118
119 119
120 120 class AdminController(BaseController):
121 121
122 122 @LoginRequired()
123 123 def __before__(self):
124 124 super(AdminController, self).__before__()
125 125
126 126 @HasPermissionAllDecorator('hg.admin')
127 127 def index(self):
128 128 users_log = UserLog.query() \
129 129 .options(joinedload(UserLog.user)) \
130 130 .options(joinedload(UserLog.repository))
131 131
132 132 #FILTERING
133 133 c.search_term = request.GET.get('filter')
134 134 users_log = _journal_filter(users_log, c.search_term)
135 135
136 136 users_log = users_log.order_by(UserLog.action_date.desc())
137 137
138 p = safe_int(request.GET.get('page', 1), 1)
138 p = safe_int(request.GET.get('page'), 1)
139 139
140 140 def url_generator(**kw):
141 141 return url.current(filter=c.search_term, **kw)
142 142
143 143 c.users_log = Page(users_log, page=p, items_per_page=10, url=url_generator)
144 144
145 145 if request.environ.get('HTTP_X_PARTIAL_XHR'):
146 146 return render('admin/admin_log.html')
147 147
148 148 return render('admin/admin.html')
@@ -1,292 +1,292 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.gists
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 gist controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: May 9, 2013
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import time
29 29 import logging
30 30 import traceback
31 31 import formencode.htmlfill
32 32
33 33 from pylons import request, response, tmpl_context as c, url
34 34 from pylons.i18n.translation import _
35 35 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden
36 36
37 37 from kallithea.model.forms import GistForm
38 38 from kallithea.model.gist import GistModel
39 39 from kallithea.model.meta import Session
40 40 from kallithea.model.db import Gist, User
41 41 from kallithea.lib import helpers as h
42 42 from kallithea.lib.base import BaseController, render
43 43 from kallithea.lib.auth import LoginRequired, NotAnonymous
44 44 from kallithea.lib.utils import jsonify
45 45 from kallithea.lib.utils2 import safe_int, safe_unicode, time_to_datetime
46 46 from kallithea.lib.helpers import Page
47 47 from sqlalchemy.sql.expression import or_
48 48 from kallithea.lib.vcs.exceptions import VCSError, NodeNotChangedError
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class GistsController(BaseController):
54 54 """REST Controller styled on the Atom Publishing Protocol"""
55 55
56 56 def __load_defaults(self, extra_values=None):
57 57 c.lifetime_values = [
58 58 (str(-1), _('Forever')),
59 59 (str(5), _('5 minutes')),
60 60 (str(60), _('1 hour')),
61 61 (str(60 * 24), _('1 day')),
62 62 (str(60 * 24 * 30), _('1 month')),
63 63 ]
64 64 if extra_values:
65 65 c.lifetime_values.append(extra_values)
66 66 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
67 67
68 68 @LoginRequired()
69 69 def index(self):
70 70 """GET /admin/gists: All items in the collection"""
71 71 # url('gists')
72 72 not_default_user = not c.authuser.is_default_user
73 73 c.show_private = request.GET.get('private') and not_default_user
74 74 c.show_public = request.GET.get('public') and not_default_user
75 75
76 76 gists = Gist().query() \
77 77 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time())) \
78 78 .order_by(Gist.created_on.desc())
79 79
80 80 # MY private
81 81 if c.show_private and not c.show_public:
82 82 gists = gists.filter(Gist.gist_type == Gist.GIST_PRIVATE) \
83 83 .filter(Gist.gist_owner == c.authuser.user_id)
84 84 # MY public
85 85 elif c.show_public and not c.show_private:
86 86 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC) \
87 87 .filter(Gist.gist_owner == c.authuser.user_id)
88 88
89 89 # MY public+private
90 90 elif c.show_private and c.show_public:
91 91 gists = gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
92 92 Gist.gist_type == Gist.GIST_PRIVATE)) \
93 93 .filter(Gist.gist_owner == c.authuser.user_id)
94 94
95 95 # default show ALL public gists
96 96 if not c.show_public and not c.show_private:
97 97 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
98 98
99 99 c.gists = gists
100 p = safe_int(request.GET.get('page', 1), 1)
100 p = safe_int(request.GET.get('page'), 1)
101 101 c.gists_pager = Page(c.gists, page=p, items_per_page=10)
102 102 return render('admin/gists/index.html')
103 103
104 104 @LoginRequired()
105 105 @NotAnonymous()
106 106 def create(self):
107 107 """POST /admin/gists: Create a new item"""
108 108 # url('gists')
109 109 self.__load_defaults()
110 110 gist_form = GistForm([x[0] for x in c.lifetime_values])()
111 111 try:
112 112 form_result = gist_form.to_python(dict(request.POST))
113 113 #TODO: multiple files support, from the form
114 114 filename = form_result['filename'] or Gist.DEFAULT_FILENAME
115 115 nodes = {
116 116 filename: {
117 117 'content': form_result['content'],
118 118 'lexer': form_result['mimetype'] # None is autodetect
119 119 }
120 120 }
121 121 _public = form_result['public']
122 122 gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE
123 123 gist = GistModel().create(
124 124 description=form_result['description'],
125 125 owner=c.authuser.user_id,
126 126 gist_mapping=nodes,
127 127 gist_type=gist_type,
128 128 lifetime=form_result['lifetime']
129 129 )
130 130 Session().commit()
131 131 new_gist_id = gist.gist_access_id
132 132 except formencode.Invalid as errors:
133 133 defaults = errors.value
134 134
135 135 return formencode.htmlfill.render(
136 136 render('admin/gists/new.html'),
137 137 defaults=defaults,
138 138 errors=errors.error_dict or {},
139 139 prefix_error=False,
140 140 encoding="UTF-8",
141 141 force_defaults=False)
142 142
143 143 except Exception as e:
144 144 log.error(traceback.format_exc())
145 145 h.flash(_('Error occurred during gist creation'), category='error')
146 146 raise HTTPFound(location=url('new_gist'))
147 147 raise HTTPFound(location=url('gist', gist_id=new_gist_id))
148 148
149 149 @LoginRequired()
150 150 @NotAnonymous()
151 151 def new(self, format='html'):
152 152 """GET /admin/gists/new: Form to create a new item"""
153 153 # url('new_gist')
154 154 self.__load_defaults()
155 155 return render('admin/gists/new.html')
156 156
157 157 @LoginRequired()
158 158 @NotAnonymous()
159 159 def update(self, gist_id):
160 160 """PUT /admin/gists/gist_id: Update an existing item"""
161 161 # Forms posted to this method should contain a hidden field:
162 162 # <input type="hidden" name="_method" value="PUT" />
163 163 # Or using helpers:
164 164 # h.form(url('gist', gist_id=ID),
165 165 # method='put')
166 166 # url('gist', gist_id=ID)
167 167
168 168 @LoginRequired()
169 169 @NotAnonymous()
170 170 def delete(self, gist_id):
171 171 """DELETE /admin/gists/gist_id: Delete an existing item"""
172 172 # Forms posted to this method should contain a hidden field:
173 173 # <input type="hidden" name="_method" value="DELETE" />
174 174 # Or using helpers:
175 175 # h.form(url('gist', gist_id=ID),
176 176 # method='delete')
177 177 # url('gist', gist_id=ID)
178 178 gist = GistModel().get_gist(gist_id)
179 179 owner = gist.gist_owner == c.authuser.user_id
180 180 if h.HasPermissionAny('hg.admin')() or owner:
181 181 GistModel().delete(gist)
182 182 Session().commit()
183 183 h.flash(_('Deleted gist %s') % gist.gist_access_id, category='success')
184 184 else:
185 185 raise HTTPForbidden()
186 186
187 187 raise HTTPFound(location=url('gists'))
188 188
189 189 @LoginRequired()
190 190 def show(self, gist_id, revision='tip', format='html', f_path=None):
191 191 """GET /admin/gists/gist_id: Show a specific item"""
192 192 # url('gist', gist_id=ID)
193 193 c.gist = Gist.get_or_404(gist_id)
194 194
195 195 #check if this gist is not expired
196 196 if c.gist.gist_expires != -1:
197 197 if time.time() > c.gist.gist_expires:
198 198 log.error('Gist expired at %s',
199 199 time_to_datetime(c.gist.gist_expires))
200 200 raise HTTPNotFound()
201 201 try:
202 202 c.file_changeset, c.files = GistModel().get_gist_files(gist_id,
203 203 revision=revision)
204 204 except VCSError:
205 205 log.error(traceback.format_exc())
206 206 raise HTTPNotFound()
207 207 if format == 'raw':
208 208 content = '\n\n'.join([f.content for f in c.files if (f_path is None or safe_unicode(f.path) == f_path)])
209 209 response.content_type = 'text/plain'
210 210 return content
211 211 return render('admin/gists/show.html')
212 212
213 213 @LoginRequired()
214 214 @NotAnonymous()
215 215 def edit(self, gist_id, format='html'):
216 216 """GET /admin/gists/gist_id/edit: Form to edit an existing item"""
217 217 # url('edit_gist', gist_id=ID)
218 218 c.gist = Gist.get_or_404(gist_id)
219 219
220 220 #check if this gist is not expired
221 221 if c.gist.gist_expires != -1:
222 222 if time.time() > c.gist.gist_expires:
223 223 log.error('Gist expired at %s',
224 224 time_to_datetime(c.gist.gist_expires))
225 225 raise HTTPNotFound()
226 226 try:
227 227 c.file_changeset, c.files = GistModel().get_gist_files(gist_id)
228 228 except VCSError:
229 229 log.error(traceback.format_exc())
230 230 raise HTTPNotFound()
231 231
232 232 self.__load_defaults(extra_values=('0', _('Unmodified')))
233 233 rendered = render('admin/gists/edit.html')
234 234
235 235 if request.POST:
236 236 rpost = request.POST
237 237 nodes = {}
238 238 for org_filename, filename, mimetype, content in zip(
239 239 rpost.getall('org_files'),
240 240 rpost.getall('files'),
241 241 rpost.getall('mimetypes'),
242 242 rpost.getall('contents')):
243 243
244 244 nodes[org_filename] = {
245 245 'org_filename': org_filename,
246 246 'filename': filename,
247 247 'content': content,
248 248 'lexer': mimetype,
249 249 }
250 250 try:
251 251 GistModel().update(
252 252 gist=c.gist,
253 253 description=rpost['description'],
254 254 owner=c.gist.owner,
255 255 gist_mapping=nodes,
256 256 gist_type=c.gist.gist_type,
257 257 lifetime=rpost['lifetime']
258 258 )
259 259
260 260 Session().commit()
261 261 h.flash(_('Successfully updated gist content'), category='success')
262 262 except NodeNotChangedError:
263 263 # raised if nothing was changed in repo itself. We anyway then
264 264 # store only DB stuff for gist
265 265 Session().commit()
266 266 h.flash(_('Successfully updated gist data'), category='success')
267 267 except Exception:
268 268 log.error(traceback.format_exc())
269 269 h.flash(_('Error occurred during update of gist %s') % gist_id,
270 270 category='error')
271 271
272 272 raise HTTPFound(location=url('gist', gist_id=gist_id))
273 273
274 274 return rendered
275 275
276 276 @LoginRequired()
277 277 @NotAnonymous()
278 278 @jsonify
279 279 def check_revision(self, gist_id):
280 280 c.gist = Gist.get_or_404(gist_id)
281 281 last_rev = c.gist.scm_instance.get_changeset()
282 282 success = True
283 283 revision = request.POST.get('revision')
284 284
285 285 ##TODO: maybe move this to model ?
286 286 if revision != last_rev.raw_id:
287 287 log.error('Last revision %s is different than submitted %s',
288 288 revision, last_rev)
289 289 # our gist has newer version than we
290 290 success = False
291 291
292 292 return {'success': success}
@@ -1,170 +1,170 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.notifications
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 notifications controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Nov 23, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30
31 31 from pylons import request
32 32 from pylons import tmpl_context as c
33 33 from webob.exc import HTTPBadRequest, HTTPForbidden
34 34
35 35 from kallithea.model.db import Notification
36 36 from kallithea.model.notification import NotificationModel
37 37 from kallithea.model.meta import Session
38 38 from kallithea.lib.auth import LoginRequired, NotAnonymous
39 39 from kallithea.lib.base import BaseController, render
40 40 from kallithea.lib import helpers as h
41 41 from kallithea.lib.helpers import Page
42 42 from kallithea.lib.utils2 import safe_int
43 43
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class NotificationsController(BaseController):
49 49 """REST Controller styled on the Atom Publishing Protocol"""
50 50 # To properly map this controller, ensure your config/routing.py
51 51 # file has a resource setup:
52 52 # map.resource('notification', 'notifications', controller='_admin/notifications',
53 53 # path_prefix='/_admin', name_prefix='_admin_')
54 54
55 55 @LoginRequired()
56 56 @NotAnonymous()
57 57 def __before__(self):
58 58 super(NotificationsController, self).__before__()
59 59
60 60 def index(self, format='html'):
61 61 """GET /_admin/notifications: All items in the collection"""
62 62 # url('notifications')
63 63 c.user = self.authuser
64 64 notif = NotificationModel().query_for_user(self.authuser.user_id,
65 65 filter_=request.GET.getall('type'))
66 66
67 p = safe_int(request.GET.get('page', 1), 1)
67 p = safe_int(request.GET.get('page'), 1)
68 68 c.notifications = Page(notif, page=p, items_per_page=10)
69 69 c.pull_request_type = Notification.TYPE_PULL_REQUEST
70 70 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
71 71 Notification.TYPE_PULL_REQUEST_COMMENT]
72 72
73 73 _current_filter = request.GET.getall('type')
74 74 c.current_filter = 'all'
75 75 if _current_filter == [c.pull_request_type]:
76 76 c.current_filter = 'pull_request'
77 77 elif _current_filter == c.comment_type:
78 78 c.current_filter = 'comment'
79 79
80 80 return render('admin/notifications/notifications.html')
81 81
82 82 def mark_all_read(self):
83 83 if request.environ.get('HTTP_X_PARTIAL_XHR'):
84 84 nm = NotificationModel()
85 85 # mark all read
86 86 nm.mark_all_read_for_user(self.authuser.user_id,
87 87 filter_=request.GET.getall('type'))
88 88 Session().commit()
89 89 c.user = self.authuser
90 90 notif = nm.query_for_user(self.authuser.user_id,
91 91 filter_=request.GET.getall('type'))
92 92 c.notifications = Page(notif, page=1, items_per_page=10)
93 93 return render('admin/notifications/notifications_data.html')
94 94
95 95 def create(self):
96 96 """POST /_admin/notifications: Create a new item"""
97 97 # url('notifications')
98 98
99 99 def new(self, format='html'):
100 100 """GET /_admin/notifications/new: Form to create a new item"""
101 101 # url('new_notification')
102 102
103 103 def update(self, notification_id):
104 104 """PUT /_admin/notifications/id: Update an existing item"""
105 105 # Forms posted to this method should contain a hidden field:
106 106 # <input type="hidden" name="_method" value="PUT" />
107 107 # Or using helpers:
108 108 # h.form(url('notification', notification_id=ID),
109 109 # method='put')
110 110 # url('notification', notification_id=ID)
111 111 try:
112 112 no = Notification.get(notification_id)
113 113 owner = all(un.user.user_id == c.authuser.user_id
114 114 for un in no.notifications_to_users)
115 115 if h.HasPermissionAny('hg.admin')() or owner:
116 116 # deletes only notification2user
117 117 NotificationModel().mark_read(c.authuser.user_id, no)
118 118 Session().commit()
119 119 return 'ok'
120 120 except Exception:
121 121 Session().rollback()
122 122 log.error(traceback.format_exc())
123 123 raise HTTPBadRequest()
124 124
125 125 def delete(self, notification_id):
126 126 """DELETE /_admin/notifications/id: Delete an existing item"""
127 127 # Forms posted to this method should contain a hidden field:
128 128 # <input type="hidden" name="_method" value="DELETE" />
129 129 # Or using helpers:
130 130 # h.form(url('notification', notification_id=ID),
131 131 # method='delete')
132 132 # url('notification', notification_id=ID)
133 133 try:
134 134 no = Notification.get(notification_id)
135 135 owner = any(un.user.user_id == c.authuser.user_id
136 136 for un in no.notifications_to_users)
137 137 if h.HasPermissionAny('hg.admin')() or owner:
138 138 # deletes only notification2user
139 139 NotificationModel().delete(c.authuser.user_id, no)
140 140 Session().commit()
141 141 return 'ok'
142 142 except Exception:
143 143 Session().rollback()
144 144 log.error(traceback.format_exc())
145 145 raise HTTPBadRequest()
146 146
147 147 def show(self, notification_id, format='html'):
148 148 """GET /_admin/notifications/id: Show a specific item"""
149 149 # url('notification', notification_id=ID)
150 150 notification = Notification.get_or_404(notification_id)
151 151
152 152 unotification = NotificationModel() \
153 153 .get_user_notification(self.authuser.user_id, notification)
154 154
155 155 # if this association to user is not valid, we don't want to show
156 156 # this message
157 157 if unotification is None:
158 158 raise HTTPForbidden()
159 159
160 160 if not unotification.read:
161 161 unotification.mark_as_read()
162 162 Session().commit()
163 163
164 164 c.notification = notification
165 165 c.user = self.authuser
166 166 return render('admin/notifications/show_notification.html')
167 167
168 168 def edit(self, notification_id, format='html'):
169 169 """GET /_admin/notifications/id/edit: Form to edit an existing item"""
170 170 # url('edit_notification', notification_id=ID)
@@ -1,196 +1,196 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.changelog
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 changelog controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 21, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30
31 31 from pylons import request, url, session, tmpl_context as c
32 32 from pylons.i18n.translation import _
33 33 from webob.exc import HTTPFound, HTTPNotFound, HTTPBadRequest
34 34
35 35 import kallithea.lib.helpers as h
36 36 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 37 from kallithea.lib.base import BaseRepoController, render
38 38 from kallithea.lib.helpers import RepoPage
39 39 from kallithea.lib.compat import json
40 40 from kallithea.lib.graphmod import graph_data
41 41 from kallithea.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
42 42 ChangesetError, NodeDoesNotExistError, EmptyRepositoryError
43 43 from kallithea.lib.utils2 import safe_int, safe_str
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 def _load_changelog_summary():
50 50 p = safe_int(request.GET.get('page'), 1)
51 51 size = safe_int(request.GET.get('size'), 10)
52 52
53 53 def url_generator(**kw):
54 54 return url('changelog_summary_home',
55 55 repo_name=c.db_repo.repo_name, size=size, **kw)
56 56
57 57 collection = c.db_repo_scm_instance
58 58
59 59 c.repo_changesets = RepoPage(collection, page=p,
60 60 items_per_page=size,
61 61 url=url_generator)
62 62 page_revisions = [x.raw_id for x in list(c.repo_changesets)]
63 63 c.comments = c.db_repo.get_comments(page_revisions)
64 64 c.statuses = c.db_repo.statuses(page_revisions)
65 65
66 66
67 67 class ChangelogController(BaseRepoController):
68 68
69 69 def __before__(self):
70 70 super(ChangelogController, self).__before__()
71 71 c.affected_files_cut_off = 60
72 72
73 73 @staticmethod
74 74 def __get_cs(rev, repo):
75 75 """
76 76 Safe way to get changeset. If error occur fail with error message.
77 77
78 78 :param rev: revision to fetch
79 79 :param repo: repo instance
80 80 """
81 81
82 82 try:
83 83 return c.db_repo_scm_instance.get_changeset(rev)
84 84 except EmptyRepositoryError as e:
85 85 h.flash(h.literal(_('There are no changesets yet')),
86 86 category='error')
87 87 except RepositoryError as e:
88 88 log.error(traceback.format_exc())
89 89 h.flash(safe_str(e), category='error')
90 90 raise HTTPBadRequest()
91 91
92 92 @LoginRequired()
93 93 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
94 94 'repository.admin')
95 95 def index(self, repo_name, revision=None, f_path=None):
96 96 # Fix URL after page size form submission via GET
97 97 # TODO: Somehow just don't send this extra junk in the GET URL
98 98 if request.GET.get('set'):
99 99 request.GET.pop('set', None)
100 100 if revision is None:
101 101 raise HTTPFound(location=url('changelog_home', repo_name=repo_name, **request.GET))
102 102 raise HTTPFound(location=url('changelog_file_home', repo_name=repo_name, revision=revision, f_path=f_path, **request.GET))
103 103
104 104 limit = 2000
105 105 default = 100
106 106 if request.GET.get('size'):
107 107 c.size = max(min(safe_int(request.GET.get('size')), limit), 1)
108 108 session['changelog_size'] = c.size
109 109 session.save()
110 110 else:
111 111 c.size = int(session.get('changelog_size', default))
112 112 # min size must be 1
113 113 c.size = max(c.size, 1)
114 p = safe_int(request.GET.get('page', 1), 1)
114 p = safe_int(request.GET.get('page'), 1)
115 115 branch_name = request.GET.get('branch', None)
116 116 if (branch_name and
117 117 branch_name not in c.db_repo_scm_instance.branches and
118 118 branch_name not in c.db_repo_scm_instance.closed_branches and
119 119 not revision):
120 120 raise HTTPFound(location=url('changelog_file_home', repo_name=c.repo_name,
121 121 revision=branch_name, f_path=f_path or ''))
122 122
123 123 if revision == 'tip':
124 124 revision = None
125 125
126 126 c.changelog_for_path = f_path
127 127 try:
128 128
129 129 if f_path:
130 130 log.debug('generating changelog for path %s', f_path)
131 131 # get the history for the file !
132 132 tip_cs = c.db_repo_scm_instance.get_changeset()
133 133 try:
134 134 collection = tip_cs.get_file_history(f_path)
135 135 except (NodeDoesNotExistError, ChangesetError):
136 136 #this node is not present at tip !
137 137 try:
138 138 cs = self.__get_cs(revision, repo_name)
139 139 collection = cs.get_file_history(f_path)
140 140 except RepositoryError as e:
141 141 h.flash(safe_str(e), category='warning')
142 142 raise HTTPFound(location=h.url('changelog_home', repo_name=repo_name))
143 143 collection = list(reversed(collection))
144 144 else:
145 145 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
146 146 branch_name=branch_name)
147 147 c.total_cs = len(collection)
148 148
149 149 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
150 150 items_per_page=c.size, branch=branch_name,)
151 151
152 152 page_revisions = [x.raw_id for x in c.pagination]
153 153 c.comments = c.db_repo.get_comments(page_revisions)
154 154 c.statuses = c.db_repo.statuses(page_revisions)
155 155 except EmptyRepositoryError as e:
156 156 h.flash(safe_str(e), category='warning')
157 157 raise HTTPFound(location=url('summary_home', repo_name=c.repo_name))
158 158 except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
159 159 log.error(traceback.format_exc())
160 160 h.flash(safe_str(e), category='error')
161 161 raise HTTPFound(location=url('changelog_home', repo_name=c.repo_name))
162 162
163 163 c.branch_name = branch_name
164 164 c.branch_filters = [('', _('None'))] + \
165 165 [(k, k) for k in c.db_repo_scm_instance.branches.keys()]
166 166 if c.db_repo_scm_instance.closed_branches:
167 167 prefix = _('(closed)') + ' '
168 168 c.branch_filters += [('-', '-')] + \
169 169 [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches.keys()]
170 170 revs = []
171 171 if not f_path:
172 172 revs = [x.revision for x in c.pagination]
173 173 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
174 174
175 175 c.revision = revision # requested revision ref
176 176 c.first_revision = c.pagination[0] # pagination is never empty here!
177 177 return render('changelog/changelog.html')
178 178
179 179 @LoginRequired()
180 180 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
181 181 'repository.admin')
182 182 def changelog_details(self, cs):
183 183 if request.environ.get('HTTP_X_PARTIAL_XHR'):
184 184 c.cs = c.db_repo_scm_instance.get_changeset(cs)
185 185 return render('changelog/changelog_details.html')
186 186 raise HTTPNotFound()
187 187
188 188 @LoginRequired()
189 189 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
190 190 'repository.admin')
191 191 def changelog_summary(self, repo_name):
192 192 if request.environ.get('HTTP_X_PARTIAL_XHR'):
193 193 _load_changelog_summary()
194 194
195 195 return render('changelog/changelog_summary_data.html')
196 196 raise HTTPNotFound()
@@ -1,59 +1,59 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.followers
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Followers controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 23, 2011
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29
30 30 from pylons import tmpl_context as c, request
31 31
32 32 from kallithea.lib.helpers import Page
33 33 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 34 from kallithea.lib.base import BaseRepoController, render
35 35 from kallithea.model.db import UserFollowing
36 36 from kallithea.lib.utils2 import safe_int
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class FollowersController(BaseRepoController):
42 42
43 43 def __before__(self):
44 44 super(FollowersController, self).__before__()
45 45
46 46 @LoginRequired()
47 47 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
48 48 'repository.admin')
49 49 def followers(self, repo_name):
50 p = safe_int(request.GET.get('page', 1), 1)
50 p = safe_int(request.GET.get('page'), 1)
51 51 repo_id = c.db_repo.repo_id
52 52 d = UserFollowing.get_repo_followers(repo_id) \
53 53 .order_by(UserFollowing.follows_from)
54 54 c.followers_pager = Page(d, page=p, items_per_page=20)
55 55
56 56 if request.environ.get('HTTP_X_PARTIAL_XHR'):
57 57 return render('/followers/followers_data.html')
58 58
59 59 return render('/followers/followers.html')
@@ -1,191 +1,191 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.forks
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 forks controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 23, 2011
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import formencode
30 30 import traceback
31 31 from formencode import htmlfill
32 32
33 33 from pylons import tmpl_context as c, request, url
34 34 from pylons.i18n.translation import _
35 35 from webob.exc import HTTPFound
36 36
37 37 import kallithea.lib.helpers as h
38 38
39 39 from kallithea.lib.helpers import Page
40 40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
41 41 NotAnonymous, HasRepoPermissionAny, HasPermissionAnyDecorator, HasPermissionAny
42 42 from kallithea.lib.base import BaseRepoController, render
43 43 from kallithea.model.db import Repository, UserFollowing, User, Ui
44 44 from kallithea.model.repo import RepoModel
45 45 from kallithea.model.forms import RepoForkForm
46 46 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices
47 47 from kallithea.lib.utils2 import safe_int
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 class ForksController(BaseRepoController):
53 53
54 54 def __before__(self):
55 55 super(ForksController, self).__before__()
56 56
57 57 def __load_defaults(self):
58 58 repo_group_perms = ['group.admin']
59 59 if HasPermissionAny('hg.create.write_on_repogroup.true')():
60 60 repo_group_perms.append('group.write')
61 61 c.repo_groups = AvailableRepoGroupChoices(['hg.create.repository'], repo_group_perms)
62 62
63 63 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs()
64 64
65 65 c.can_update = Ui.get_by_key('hooks', Ui.HOOK_UPDATE).ui_active
66 66
67 67 def __load_data(self, repo_name=None):
68 68 """
69 69 Load defaults settings for edit, and update
70 70
71 71 :param repo_name:
72 72 """
73 73 self.__load_defaults()
74 74
75 75 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
76 76 repo = db_repo.scm_instance
77 77
78 78 if c.repo_info is None:
79 79 h.not_mapped_error(repo_name)
80 80 raise HTTPFound(location=url('repos'))
81 81
82 82 c.default_user_id = User.get_default_user().user_id
83 83 c.in_public_journal = UserFollowing.query() \
84 84 .filter(UserFollowing.user_id == c.default_user_id) \
85 85 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
86 86
87 87 if c.repo_info.stats:
88 88 last_rev = c.repo_info.stats.stat_on_revision+1
89 89 else:
90 90 last_rev = 0
91 91 c.stats_revision = last_rev
92 92
93 93 c.repo_last_rev = repo.count() if repo.revisions else 0
94 94
95 95 if last_rev == 0 or c.repo_last_rev == 0:
96 96 c.stats_percentage = 0
97 97 else:
98 98 c.stats_percentage = '%.2f' % ((float((last_rev)) /
99 99 c.repo_last_rev) * 100)
100 100
101 101 defaults = RepoModel()._get_defaults(repo_name)
102 102 # alter the description to indicate a fork
103 103 defaults['description'] = ('fork of repository: %s \n%s'
104 104 % (defaults['repo_name'],
105 105 defaults['description']))
106 106 # add suffix to fork
107 107 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
108 108
109 109 return defaults
110 110
111 111 @LoginRequired()
112 112 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
113 113 'repository.admin')
114 114 def forks(self, repo_name):
115 p = safe_int(request.GET.get('page', 1), 1)
115 p = safe_int(request.GET.get('page'), 1)
116 116 repo_id = c.db_repo.repo_id
117 117 d = []
118 118 for r in Repository.get_repo_forks(repo_id):
119 119 if not HasRepoPermissionAny(
120 120 'repository.read', 'repository.write', 'repository.admin'
121 121 )(r.repo_name, 'get forks check'):
122 122 continue
123 123 d.append(r)
124 124 c.forks_pager = Page(d, page=p, items_per_page=20)
125 125
126 126 if request.environ.get('HTTP_X_PARTIAL_XHR'):
127 127 return render('/forks/forks_data.html')
128 128
129 129 return render('/forks/forks.html')
130 130
131 131 @LoginRequired()
132 132 @NotAnonymous()
133 133 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
134 134 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
135 135 'repository.admin')
136 136 def fork(self, repo_name):
137 137 c.repo_info = Repository.get_by_repo_name(repo_name)
138 138 if not c.repo_info:
139 139 h.not_mapped_error(repo_name)
140 140 raise HTTPFound(location=url('home'))
141 141
142 142 defaults = self.__load_data(repo_name)
143 143
144 144 return htmlfill.render(
145 145 render('forks/fork.html'),
146 146 defaults=defaults,
147 147 encoding="UTF-8",
148 148 force_defaults=False)
149 149
150 150 @LoginRequired()
151 151 @NotAnonymous()
152 152 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
153 153 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
154 154 'repository.admin')
155 155 def fork_create(self, repo_name):
156 156 self.__load_defaults()
157 157 c.repo_info = Repository.get_by_repo_name(repo_name)
158 158 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
159 159 repo_groups=c.repo_groups,
160 160 landing_revs=c.landing_revs_choices)()
161 161 form_result = {}
162 162 task_id = None
163 163 try:
164 164 form_result = _form.to_python(dict(request.POST))
165 165
166 166 # an approximation that is better than nothing
167 167 if not Ui.get_by_key('hooks', Ui.HOOK_UPDATE).ui_active:
168 168 form_result['update_after_clone'] = False
169 169
170 170 # create fork is done sometimes async on celery, db transaction
171 171 # management is handled there.
172 172 task = RepoModel().create_fork(form_result, self.authuser.user_id)
173 173 from celery.result import BaseAsyncResult
174 174 if isinstance(task, BaseAsyncResult):
175 175 task_id = task.task_id
176 176 except formencode.Invalid as errors:
177 177 return htmlfill.render(
178 178 render('forks/fork.html'),
179 179 defaults=errors.value,
180 180 errors=errors.error_dict or {},
181 181 prefix_error=False,
182 182 encoding="UTF-8",
183 183 force_defaults=False)
184 184 except Exception:
185 185 log.error(traceback.format_exc())
186 186 h.flash(_('An error occurred during repository forking %s') %
187 187 repo_name, category='error')
188 188
189 189 raise HTTPFound(location=h.url('repo_creating_home',
190 190 repo_name=form_result['repo_name_full'],
191 191 task_id=task_id))
@@ -1,371 +1,371 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.journal
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Journal controller for pylons
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Nov 21, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26
27 27 """
28 28
29 29 import logging
30 30 import traceback
31 31 from itertools import groupby
32 32
33 33 from sqlalchemy import or_
34 34 from sqlalchemy.orm import joinedload
35 35 from sqlalchemy.sql.expression import func
36 36
37 37 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
38 38
39 39 from webob.exc import HTTPBadRequest
40 40 from pylons import request, tmpl_context as c, response, url
41 41 from pylons.i18n.translation import _
42 42
43 43 from kallithea.controllers.admin.admin import _journal_filter
44 44 from kallithea.model.db import UserLog, UserFollowing, Repository, User
45 45 from kallithea.model.meta import Session
46 46 from kallithea.model.repo import RepoModel
47 47 import kallithea.lib.helpers as h
48 48 from kallithea.lib.helpers import Page
49 49 from kallithea.lib.auth import LoginRequired, NotAnonymous
50 50 from kallithea.lib.base import BaseController, render
51 51 from kallithea.lib.utils2 import safe_int, AttributeDict
52 52 from kallithea.lib.compat import json
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class JournalController(BaseController):
58 58
59 59 def __before__(self):
60 60 super(JournalController, self).__before__()
61 61 self.language = 'en-us'
62 62 self.ttl = "5"
63 63 self.feed_nr = 20
64 64 c.search_term = request.GET.get('filter')
65 65
66 66 def _get_daily_aggregate(self, journal):
67 67 groups = []
68 68 for k, g in groupby(journal, lambda x: x.action_as_day):
69 69 user_group = []
70 70 #groupby username if it's a present value, else fallback to journal username
71 71 for _unused, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
72 72 l = list(g2)
73 73 user_group.append((l[0].user, l))
74 74
75 75 groups.append((k, user_group,))
76 76
77 77 return groups
78 78
79 79 def _get_journal_data(self, following_repos):
80 80 repo_ids = [x.follows_repository.repo_id for x in following_repos
81 81 if x.follows_repository is not None]
82 82 user_ids = [x.follows_user.user_id for x in following_repos
83 83 if x.follows_user is not None]
84 84
85 85 filtering_criterion = None
86 86
87 87 if repo_ids and user_ids:
88 88 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
89 89 UserLog.user_id.in_(user_ids))
90 90 if repo_ids and not user_ids:
91 91 filtering_criterion = UserLog.repository_id.in_(repo_ids)
92 92 if not repo_ids and user_ids:
93 93 filtering_criterion = UserLog.user_id.in_(user_ids)
94 94 if filtering_criterion is not None:
95 95 journal = self.sa.query(UserLog) \
96 96 .options(joinedload(UserLog.user)) \
97 97 .options(joinedload(UserLog.repository))
98 98 #filter
99 99 journal = _journal_filter(journal, c.search_term)
100 100 journal = journal.filter(filtering_criterion) \
101 101 .order_by(UserLog.action_date.desc())
102 102 else:
103 103 journal = []
104 104
105 105 return journal
106 106
107 107 def _atom_feed(self, repos, public=True):
108 108 journal = self._get_journal_data(repos)
109 109 if public:
110 110 _link = h.canonical_url('public_journal_atom')
111 111 _desc = '%s %s %s' % (c.site_name, _('Public Journal'),
112 112 'atom feed')
113 113 else:
114 114 _link = h.canonical_url('journal_atom')
115 115 _desc = '%s %s %s' % (c.site_name, _('Journal'), 'atom feed')
116 116
117 117 feed = Atom1Feed(title=_desc,
118 118 link=_link,
119 119 description=_desc,
120 120 language=self.language,
121 121 ttl=self.ttl)
122 122
123 123 for entry in journal[:self.feed_nr]:
124 124 user = entry.user
125 125 if user is None:
126 126 #fix deleted users
127 127 user = AttributeDict({'short_contact': entry.username,
128 128 'email': '',
129 129 'full_contact': ''})
130 130 action, action_extra, ico = h.action_parser(entry, feed=True)
131 131 title = "%s - %s %s" % (user.short_contact, action(),
132 132 entry.repository.repo_name)
133 133 desc = action_extra()
134 134 _url = None
135 135 if entry.repository is not None:
136 136 _url = h.canonical_url('changelog_home',
137 137 repo_name=entry.repository.repo_name)
138 138
139 139 feed.add_item(title=title,
140 140 pubdate=entry.action_date,
141 141 link=_url or h.canonical_url(''),
142 142 author_email=user.email,
143 143 author_name=user.full_contact,
144 144 description=desc)
145 145
146 146 response.content_type = feed.mime_type
147 147 return feed.writeString('utf-8')
148 148
149 149 def _rss_feed(self, repos, public=True):
150 150 journal = self._get_journal_data(repos)
151 151 if public:
152 152 _link = h.canonical_url('public_journal_atom')
153 153 _desc = '%s %s %s' % (c.site_name, _('Public Journal'),
154 154 'rss feed')
155 155 else:
156 156 _link = h.canonical_url('journal_atom')
157 157 _desc = '%s %s %s' % (c.site_name, _('Journal'), 'rss feed')
158 158
159 159 feed = Rss201rev2Feed(title=_desc,
160 160 link=_link,
161 161 description=_desc,
162 162 language=self.language,
163 163 ttl=self.ttl)
164 164
165 165 for entry in journal[:self.feed_nr]:
166 166 user = entry.user
167 167 if user is None:
168 168 #fix deleted users
169 169 user = AttributeDict({'short_contact': entry.username,
170 170 'email': '',
171 171 'full_contact': ''})
172 172 action, action_extra, ico = h.action_parser(entry, feed=True)
173 173 title = "%s - %s %s" % (user.short_contact, action(),
174 174 entry.repository.repo_name)
175 175 desc = action_extra()
176 176 _url = None
177 177 if entry.repository is not None:
178 178 _url = h.canonical_url('changelog_home',
179 179 repo_name=entry.repository.repo_name)
180 180
181 181 feed.add_item(title=title,
182 182 pubdate=entry.action_date,
183 183 link=_url or h.canonical_url(''),
184 184 author_email=user.email,
185 185 author_name=user.full_contact,
186 186 description=desc)
187 187
188 188 response.content_type = feed.mime_type
189 189 return feed.writeString('utf-8')
190 190
191 191 @LoginRequired()
192 192 @NotAnonymous()
193 193 def index(self):
194 194 # Return a rendered template
195 p = safe_int(request.GET.get('page', 1), 1)
195 p = safe_int(request.GET.get('page'), 1)
196 196 c.user = User.get(self.authuser.user_id)
197 197 c.following = self.sa.query(UserFollowing) \
198 198 .filter(UserFollowing.user_id == self.authuser.user_id) \
199 199 .options(joinedload(UserFollowing.follows_repository)) \
200 200 .all()
201 201
202 202 journal = self._get_journal_data(c.following)
203 203
204 204 def url_generator(**kw):
205 205 return url.current(filter=c.search_term, **kw)
206 206
207 207 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
208 208 c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager)
209 209
210 210 if request.environ.get('HTTP_X_PARTIAL_XHR'):
211 211 return render('journal/journal_data.html')
212 212
213 213 repos_list = Session().query(Repository) \
214 214 .filter(Repository.user_id ==
215 215 self.authuser.user_id) \
216 216 .order_by(func.lower(Repository.repo_name)).all()
217 217
218 218 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
219 219 admin=True)
220 220 #json used to render the grid
221 221 c.data = json.dumps(repos_data)
222 222
223 223 watched_repos_data = []
224 224
225 225 ## watched repos
226 226 _render = RepoModel._render_datatable
227 227
228 228 def quick_menu(repo_name):
229 229 return _render('quick_menu', repo_name)
230 230
231 231 def repo_lnk(name, rtype, rstate, private, fork_of):
232 232 return _render('repo_name', name, rtype, rstate, private, fork_of,
233 233 short_name=False, admin=False)
234 234
235 235 def last_rev(repo_name, cs_cache):
236 236 return _render('revision', repo_name, cs_cache.get('revision'),
237 237 cs_cache.get('raw_id'), cs_cache.get('author'),
238 238 cs_cache.get('message'))
239 239
240 240 def desc(desc):
241 241 from pylons import tmpl_context as c
242 242 return h.urlify_text(desc, truncate=60, stylize=c.visual.stylify_metatags)
243 243
244 244 def repo_actions(repo_name):
245 245 return _render('repo_actions', repo_name)
246 246
247 247 def owner_actions(user_id, username):
248 248 return _render('user_name', user_id, username)
249 249
250 250 def toogle_follow(repo_id):
251 251 return _render('toggle_follow', repo_id)
252 252
253 253 for entry in c.following:
254 254 repo = entry.follows_repository
255 255 cs_cache = repo.changeset_cache
256 256 row = {
257 257 "menu": quick_menu(repo.repo_name),
258 258 "raw_name": repo.repo_name,
259 259 "name": repo_lnk(repo.repo_name, repo.repo_type,
260 260 repo.repo_state, repo.private, repo.fork),
261 261 "last_changeset": last_rev(repo.repo_name, cs_cache),
262 262 "last_rev_raw": cs_cache.get('revision'),
263 263 "action": toogle_follow(repo.repo_id)
264 264 }
265 265
266 266 watched_repos_data.append(row)
267 267
268 268 c.watched_data = json.dumps({
269 269 "totalRecords": len(c.following),
270 270 "startIndex": 0,
271 271 "sort": "name",
272 272 "dir": "asc",
273 273 "records": watched_repos_data
274 274 })
275 275 return render('journal/journal.html')
276 276
277 277 @LoginRequired(api_access=True)
278 278 @NotAnonymous()
279 279 def journal_atom(self):
280 280 """
281 281 Produce an atom-1.0 feed via feedgenerator module
282 282 """
283 283 following = self.sa.query(UserFollowing) \
284 284 .filter(UserFollowing.user_id == self.authuser.user_id) \
285 285 .options(joinedload(UserFollowing.follows_repository)) \
286 286 .all()
287 287 return self._atom_feed(following, public=False)
288 288
289 289 @LoginRequired(api_access=True)
290 290 @NotAnonymous()
291 291 def journal_rss(self):
292 292 """
293 293 Produce an rss feed via feedgenerator module
294 294 """
295 295 following = self.sa.query(UserFollowing) \
296 296 .filter(UserFollowing.user_id == self.authuser.user_id) \
297 297 .options(joinedload(UserFollowing.follows_repository)) \
298 298 .all()
299 299 return self._rss_feed(following, public=False)
300 300
301 301 @LoginRequired()
302 302 @NotAnonymous()
303 303 def toggle_following(self):
304 304 user_id = request.POST.get('follows_user_id')
305 305 if user_id:
306 306 try:
307 307 self.scm_model.toggle_following_user(user_id,
308 308 self.authuser.user_id)
309 309 Session.commit()
310 310 return 'ok'
311 311 except Exception:
312 312 log.error(traceback.format_exc())
313 313 raise HTTPBadRequest()
314 314
315 315 repo_id = request.POST.get('follows_repo_id')
316 316 if repo_id:
317 317 try:
318 318 self.scm_model.toggle_following_repo(repo_id,
319 319 self.authuser.user_id)
320 320 Session.commit()
321 321 return 'ok'
322 322 except Exception:
323 323 log.error(traceback.format_exc())
324 324 raise HTTPBadRequest()
325 325
326 326 raise HTTPBadRequest()
327 327
328 328 @LoginRequired()
329 329 def public_journal(self):
330 330 # Return a rendered template
331 p = safe_int(request.GET.get('page', 1), 1)
331 p = safe_int(request.GET.get('page'), 1)
332 332
333 333 c.following = self.sa.query(UserFollowing) \
334 334 .filter(UserFollowing.user_id == self.authuser.user_id) \
335 335 .options(joinedload(UserFollowing.follows_repository)) \
336 336 .all()
337 337
338 338 journal = self._get_journal_data(c.following)
339 339
340 340 c.journal_pager = Page(journal, page=p, items_per_page=20)
341 341
342 342 c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager)
343 343
344 344 if request.environ.get('HTTP_X_PARTIAL_XHR'):
345 345 return render('journal/journal_data.html')
346 346
347 347 return render('journal/public_journal.html')
348 348
349 349 @LoginRequired(api_access=True)
350 350 def public_journal_atom(self):
351 351 """
352 352 Produce an atom-1.0 feed via feedgenerator module
353 353 """
354 354 c.following = self.sa.query(UserFollowing) \
355 355 .filter(UserFollowing.user_id == self.authuser.user_id) \
356 356 .options(joinedload(UserFollowing.follows_repository)) \
357 357 .all()
358 358
359 359 return self._atom_feed(c.following)
360 360
361 361 @LoginRequired(api_access=True)
362 362 def public_journal_rss(self):
363 363 """
364 364 Produce an rss2 feed via feedgenerator module
365 365 """
366 366 c.following = self.sa.query(UserFollowing) \
367 367 .filter(UserFollowing.user_id == self.authuser.user_id) \
368 368 .options(joinedload(UserFollowing.follows_repository)) \
369 369 .all()
370 370
371 371 return self._rss_feed(c.following)
@@ -1,830 +1,830 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.pullrequests
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 pull requests controller for Kallithea for initializing pull requests
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: May 7, 2012
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31 import re
32 32
33 33 from pylons import request, tmpl_context as c, url
34 34 from pylons.i18n.translation import _
35 35 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest
36 36
37 37 from kallithea.lib.vcs.utils.hgcompat import unionrepo
38 38 from kallithea.lib.compat import json
39 39 from kallithea.lib.base import BaseRepoController, render
40 40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
41 41 NotAnonymous
42 42 from kallithea.lib.helpers import Page
43 43 from kallithea.lib import helpers as h
44 44 from kallithea.lib import diffs
45 45 from kallithea.lib.exceptions import UserInvalidException
46 46 from kallithea.lib.utils import action_logger, jsonify
47 47 from kallithea.lib.vcs.utils import safe_str
48 48 from kallithea.lib.vcs.exceptions import EmptyRepositoryError, ChangesetDoesNotExistError
49 49 from kallithea.lib.diffs import LimitedDiffContainer
50 50 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
51 51 PullRequestReviewers, User
52 52 from kallithea.model.pull_request import PullRequestModel
53 53 from kallithea.model.meta import Session
54 54 from kallithea.model.repo import RepoModel
55 55 from kallithea.model.comment import ChangesetCommentsModel
56 56 from kallithea.model.changeset_status import ChangesetStatusModel
57 57 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
58 58 from kallithea.lib.utils2 import safe_int
59 59 from kallithea.controllers.changeset import _ignorews_url, _context_url, \
60 60 create_comment
61 61 from kallithea.controllers.compare import CompareController
62 62 from kallithea.lib.graphmod import graph_data
63 63
64 64 log = logging.getLogger(__name__)
65 65
66 66
67 67 class PullrequestsController(BaseRepoController):
68 68
69 69 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
70 70 """return a structure with repo's interesting changesets, suitable for
71 71 the selectors in pullrequest.html
72 72
73 73 rev: a revision that must be in the list somehow and selected by default
74 74 branch: a branch that must be in the list and selected by default - even if closed
75 75 branch_rev: a revision of which peers should be preferred and available."""
76 76 # list named branches that has been merged to this named branch - it should probably merge back
77 77 peers = []
78 78
79 79 if rev:
80 80 rev = safe_str(rev)
81 81
82 82 if branch:
83 83 branch = safe_str(branch)
84 84
85 85 if branch_rev:
86 86 branch_rev = safe_str(branch_rev)
87 87 # a revset not restricting to merge() would be better
88 88 # (especially because it would get the branch point)
89 89 # ... but is currently too expensive
90 90 # including branches of children could be nice too
91 91 peerbranches = set()
92 92 for i in repo._repo.revs(
93 93 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
94 94 branch_rev, branch_rev):
95 95 abranch = repo.get_changeset(i).branch
96 96 if abranch not in peerbranches:
97 97 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
98 98 peers.append((n, abranch))
99 99 peerbranches.add(abranch)
100 100
101 101 selected = None
102 102 tiprev = repo.tags.get('tip')
103 103 tipbranch = None
104 104
105 105 branches = []
106 106 for abranch, branchrev in repo.branches.iteritems():
107 107 n = 'branch:%s:%s' % (abranch, branchrev)
108 108 desc = abranch
109 109 if branchrev == tiprev:
110 110 tipbranch = abranch
111 111 desc = '%s (current tip)' % desc
112 112 branches.append((n, desc))
113 113 if rev == branchrev:
114 114 selected = n
115 115 if branch == abranch:
116 116 if not rev:
117 117 selected = n
118 118 branch = None
119 119 if branch: # branch not in list - it is probably closed
120 120 branchrev = repo.closed_branches.get(branch)
121 121 if branchrev:
122 122 n = 'branch:%s:%s' % (branch, branchrev)
123 123 branches.append((n, _('%s (closed)') % branch))
124 124 selected = n
125 125 branch = None
126 126 if branch:
127 127 log.debug('branch %r not found in %s', branch, repo)
128 128
129 129 bookmarks = []
130 130 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
131 131 n = 'book:%s:%s' % (bookmark, bookmarkrev)
132 132 bookmarks.append((n, bookmark))
133 133 if rev == bookmarkrev:
134 134 selected = n
135 135
136 136 tags = []
137 137 for tag, tagrev in repo.tags.iteritems():
138 138 if tag == 'tip':
139 139 continue
140 140 n = 'tag:%s:%s' % (tag, tagrev)
141 141 tags.append((n, tag))
142 142 if rev == tagrev:
143 143 selected = n
144 144
145 145 # prio 1: rev was selected as existing entry above
146 146
147 147 # prio 2: create special entry for rev; rev _must_ be used
148 148 specials = []
149 149 if rev and selected is None:
150 150 selected = 'rev:%s:%s' % (rev, rev)
151 151 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
152 152
153 153 # prio 3: most recent peer branch
154 154 if peers and not selected:
155 155 selected = peers[0][0]
156 156
157 157 # prio 4: tip revision
158 158 if not selected:
159 159 if h.is_hg(repo):
160 160 if tipbranch:
161 161 selected = 'branch:%s:%s' % (tipbranch, tiprev)
162 162 else:
163 163 selected = 'tag:null:' + repo.EMPTY_CHANGESET
164 164 tags.append((selected, 'null'))
165 165 else:
166 166 if 'master' in repo.branches:
167 167 selected = 'branch:master:%s' % repo.branches['master']
168 168 else:
169 169 k, v = repo.branches.items()[0]
170 170 selected = 'branch:%s:%s' % (k, v)
171 171
172 172 groups = [(specials, _("Special")),
173 173 (peers, _("Peer branches")),
174 174 (bookmarks, _("Bookmarks")),
175 175 (branches, _("Branches")),
176 176 (tags, _("Tags")),
177 177 ]
178 178 return [g for g in groups if g[0]], selected
179 179
180 180 def _get_is_allowed_change_status(self, pull_request):
181 181 if pull_request.is_closed():
182 182 return False
183 183
184 184 owner = self.authuser.user_id == pull_request.user_id
185 185 reviewer = PullRequestReviewers.query() \
186 186 .filter(PullRequestReviewers.pull_request == pull_request) \
187 187 .filter(PullRequestReviewers.user_id == self.authuser.user_id) \
188 188 .count() != 0
189 189
190 190 return self.authuser.admin or owner or reviewer
191 191
192 192 @LoginRequired()
193 193 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
194 194 'repository.admin')
195 195 def show_all(self, repo_name):
196 196 c.from_ = request.GET.get('from_') or ''
197 197 c.closed = request.GET.get('closed') or ''
198 198 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
199 199 c.repo_name = repo_name
200 p = safe_int(request.GET.get('page', 1), 1)
200 p = safe_int(request.GET.get('page'), 1)
201 201
202 202 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
203 203
204 204 return render('/pullrequests/pullrequest_show_all.html')
205 205
206 206 @LoginRequired()
207 207 @NotAnonymous()
208 208 def show_my(self):
209 209 c.closed = request.GET.get('closed') or ''
210 210
211 211 def _filter(pr):
212 212 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
213 213 if not c.closed:
214 214 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
215 215 return s
216 216
217 217 c.my_pull_requests = _filter(PullRequest.query() \
218 218 .filter(PullRequest.user_id ==
219 219 self.authuser.user_id) \
220 220 .all())
221 221
222 222 c.participate_in_pull_requests = []
223 223 c.participate_in_pull_requests_todo = []
224 224 done_status = set([ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED])
225 225 for pr in _filter(PullRequest.query()
226 226 .join(PullRequestReviewers)
227 227 .filter(PullRequestReviewers.user_id ==
228 228 self.authuser.user_id)
229 229 ):
230 230 status = pr.user_review_status(c.authuser.user_id) # very inefficient!!!
231 231 if status in done_status:
232 232 c.participate_in_pull_requests.append(pr)
233 233 else:
234 234 c.participate_in_pull_requests_todo.append(pr)
235 235
236 236 return render('/pullrequests/pullrequest_show_my.html')
237 237
238 238 @LoginRequired()
239 239 @NotAnonymous()
240 240 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
241 241 'repository.admin')
242 242 def index(self):
243 243 org_repo = c.db_repo
244 244 org_scm_instance = org_repo.scm_instance
245 245 try:
246 246 org_scm_instance.get_changeset()
247 247 except EmptyRepositoryError as e:
248 248 h.flash(h.literal(_('There are no changesets yet')),
249 249 category='warning')
250 250 raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))
251 251
252 252 org_rev = request.GET.get('rev_end')
253 253 # rev_start is not directly useful - its parent could however be used
254 254 # as default for other and thus give a simple compare view
255 255 rev_start = request.GET.get('rev_start')
256 256 other_rev = None
257 257 if rev_start:
258 258 starters = org_repo.get_changeset(rev_start).parents
259 259 if starters:
260 260 other_rev = starters[0].raw_id
261 261 else:
262 262 other_rev = org_repo.scm_instance.EMPTY_CHANGESET
263 263 branch = request.GET.get('branch')
264 264
265 265 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
266 266 c.default_cs_repo = org_repo.repo_name
267 267 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
268 268
269 269 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
270 270 if default_cs_ref_type != 'branch':
271 271 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
272 272
273 273 # add org repo to other so we can open pull request against peer branches on itself
274 274 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
275 275
276 276 if org_repo.parent:
277 277 # add parent of this fork also and select it.
278 278 # use the same branch on destination as on source, if available.
279 279 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
280 280 c.a_repo = org_repo.parent
281 281 c.a_refs, c.default_a_ref = self._get_repo_refs(
282 282 org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
283 283
284 284 else:
285 285 c.a_repo = org_repo
286 286 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
287 287
288 288 # gather forks and add to this list ... even though it is rare to
289 289 # request forks to pull from their parent
290 290 for fork in org_repo.forks:
291 291 c.a_repos.append((fork.repo_name, fork.repo_name))
292 292
293 293 return render('/pullrequests/pullrequest.html')
294 294
295 295 @LoginRequired()
296 296 @NotAnonymous()
297 297 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
298 298 'repository.admin')
299 299 @jsonify
300 300 def repo_info(self, repo_name):
301 301 repo = RepoModel()._get_repo(repo_name)
302 302 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
303 303 return {
304 304 'description': repo.description.split('\n', 1)[0],
305 305 'selected_ref': selected_ref,
306 306 'refs': refs,
307 307 }
308 308
309 309 @LoginRequired()
310 310 @NotAnonymous()
311 311 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
312 312 'repository.admin')
313 313 def create(self, repo_name):
314 314 repo = RepoModel()._get_repo(repo_name)
315 315 try:
316 316 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
317 317 except formencode.Invalid as errors:
318 318 log.error(traceback.format_exc())
319 319 log.error(str(errors))
320 320 msg = _('Error creating pull request: %s') % errors.msg
321 321 h.flash(msg, 'error')
322 322 raise HTTPBadRequest
323 323
324 324 # heads up: org and other might seem backward here ...
325 325 org_repo_name = _form['org_repo']
326 326 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
327 327 org_repo = RepoModel()._get_repo(org_repo_name)
328 328 (org_ref_type,
329 329 org_ref_name,
330 330 org_rev) = org_ref.split(':')
331 331 if org_ref_type == 'rev':
332 332 org_ref_type = 'branch'
333 333 cs = org_repo.scm_instance.get_changeset(org_rev)
334 334 org_ref = '%s:%s:%s' % (org_ref_type, cs.branch, cs.raw_id)
335 335
336 336 other_repo_name = _form['other_repo']
337 337 other_ref = _form['other_ref'] # will have symbolic name and head revision
338 338 other_repo = RepoModel()._get_repo(other_repo_name)
339 339 (other_ref_type,
340 340 other_ref_name,
341 341 other_rev) = other_ref.split(':')
342 342
343 343 cs_ranges, _cs_ranges_not, ancestor_rev = \
344 344 CompareController._get_changesets(org_repo.scm_instance.alias,
345 345 other_repo.scm_instance, other_rev, # org and other "swapped"
346 346 org_repo.scm_instance, org_rev,
347 347 )
348 348 if ancestor_rev is None:
349 349 ancestor_rev = org_repo.scm_instance.EMPTY_CHANGESET
350 350 revisions = [cs_.raw_id for cs_ in cs_ranges]
351 351
352 352 # hack: ancestor_rev is not an other_rev but we want to show the
353 353 # requested destination and have the exact ancestor
354 354 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
355 355
356 356 reviewers = _form['review_members']
357 357
358 358 title = _form['pullrequest_title']
359 359 if not title:
360 360 if org_repo_name == other_repo_name:
361 361 title = '%s to %s' % (h.short_ref(org_ref_type, org_ref_name),
362 362 h.short_ref(other_ref_type, other_ref_name))
363 363 else:
364 364 title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
365 365 other_repo_name, h.short_ref(other_ref_type, other_ref_name))
366 366 description = _form['pullrequest_desc'].strip() or _('No description')
367 367 try:
368 368 pull_request = PullRequestModel().create(
369 369 self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
370 370 other_ref, revisions, reviewers, title, description
371 371 )
372 372 Session().commit()
373 373 h.flash(_('Successfully opened new pull request'),
374 374 category='success')
375 375 except UserInvalidException as u:
376 376 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
377 377 raise HTTPBadRequest()
378 378 except Exception:
379 379 h.flash(_('Error occurred while creating pull request'),
380 380 category='error')
381 381 log.error(traceback.format_exc())
382 382 raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
383 383
384 384 raise HTTPFound(location=pull_request.url())
385 385
386 386 def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewers_ids):
387 387 org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
388 388 org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
389 389 new_org_rev = self._get_ref_rev(org_repo, 'rev', new_rev)
390 390
391 391 other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
392 392 other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
393 393 #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
394 394 new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
395 395
396 396 cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
397 397 other_repo.scm_instance, new_other_rev, # org and other "swapped"
398 398 org_repo.scm_instance, new_org_rev)
399 399
400 400 old_revisions = set(old_pull_request.revisions)
401 401 revisions = [cs.raw_id for cs in cs_ranges]
402 402 new_revisions = [r for r in revisions if r not in old_revisions]
403 403 lost = old_revisions.difference(revisions)
404 404
405 405 infos = ['This is a new iteration of %s "%s".' %
406 406 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
407 407 pull_request_id=old_pull_request.pull_request_id),
408 408 old_pull_request.title)]
409 409
410 410 if lost:
411 411 infos.append(_('Missing changesets since the previous iteration:'))
412 412 for r in old_pull_request.revisions:
413 413 if r in lost:
414 414 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
415 415 infos.append(' %s "%s"' % (h.short_id(r), rev_desc))
416 416
417 417 if new_revisions:
418 418 infos.append(_('New changesets on %s %s since the previous iteration:') % (org_ref_type, org_ref_name))
419 419 for r in reversed(revisions):
420 420 if r in new_revisions:
421 421 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
422 422 infos.append(' %s %s' % (h.short_id(r), h.shorter(rev_desc, 80)))
423 423
424 424 if ancestor_rev == other_rev:
425 425 infos.append(_("Ancestor didn't change - diff since previous iteration:"))
426 426 infos.append(h.canonical_url('compare_url',
427 427 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
428 428 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
429 429 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
430 430 )) # note: linear diff, merge or not doesn't matter
431 431 else:
432 432 infos.append(_('This iteration is based on another %s revision and there is no simple diff.') % other_ref_name)
433 433 else:
434 434 infos.append(_('No changes found on %s %s since previous iteration.') % (org_ref_type, org_ref_name))
435 435 # TODO: fail?
436 436
437 437 # hack: ancestor_rev is not an other_ref but we want to show the
438 438 # requested destination and have the exact ancestor
439 439 new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
440 440 new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
441 441
442 442 try:
443 443 title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
444 444 v = int(old_v) + 1
445 445 except (AttributeError, ValueError):
446 446 v = 2
447 447 title = '%s (v%s)' % (title.strip(), v)
448 448
449 449 # using a mail-like separator, insert new iteration info in description with latest first
450 450 descriptions = description.replace('\r\n', '\n').split('\n-- \n', 1)
451 451 description = descriptions[0].strip() + '\n\n-- \n' + '\n'.join(infos)
452 452 if len(descriptions) > 1:
453 453 description += '\n\n' + descriptions[1].strip()
454 454
455 455 try:
456 456 pull_request = PullRequestModel().create(
457 457 self.authuser.user_id,
458 458 old_pull_request.org_repo.repo_name, new_org_ref,
459 459 old_pull_request.other_repo.repo_name, new_other_ref,
460 460 revisions, reviewers_ids, title, description
461 461 )
462 462 except UserInvalidException as u:
463 463 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
464 464 raise HTTPBadRequest()
465 465 except Exception:
466 466 h.flash(_('Error occurred while creating pull request'),
467 467 category='error')
468 468 log.error(traceback.format_exc())
469 469 raise HTTPFound(location=old_pull_request.url())
470 470
471 471 ChangesetCommentsModel().create(
472 472 text=_('Closed, next iteration: %s .') % pull_request.url(canonical=True),
473 473 repo=old_pull_request.other_repo.repo_id,
474 474 user=c.authuser.user_id,
475 475 pull_request=old_pull_request.pull_request_id,
476 476 closing_pr=True)
477 477 PullRequestModel().close_pull_request(old_pull_request.pull_request_id)
478 478
479 479 Session().commit()
480 480 h.flash(_('New pull request iteration created'),
481 481 category='success')
482 482
483 483 raise HTTPFound(location=pull_request.url())
484 484
485 485 # pullrequest_post for PR editing
486 486 @LoginRequired()
487 487 @NotAnonymous()
488 488 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
489 489 'repository.admin')
490 490 def post(self, repo_name, pull_request_id):
491 491 pull_request = PullRequest.get_or_404(pull_request_id)
492 492 if pull_request.is_closed():
493 493 raise HTTPForbidden()
494 494 assert pull_request.other_repo.repo_name == repo_name
495 495 #only owner or admin can update it
496 496 owner = pull_request.owner.user_id == c.authuser.user_id
497 497 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
498 498 if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner):
499 499 raise HTTPForbidden()
500 500
501 501 _form = PullRequestPostForm()().to_python(request.POST)
502 502 reviewers_ids = [int(s) for s in _form['review_members']]
503 503
504 504 if _form['updaterev']:
505 505 return self.create_new_iteration(pull_request,
506 506 _form['updaterev'],
507 507 _form['pullrequest_title'],
508 508 _form['pullrequest_desc'],
509 509 reviewers_ids)
510 510
511 511 old_description = pull_request.description
512 512 pull_request.title = _form['pullrequest_title']
513 513 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
514 514 pull_request.owner = User.get_by_username(_form['owner'])
515 515 user = User.get(c.authuser.user_id)
516 516 try:
517 517 PullRequestModel().mention_from_description(user, pull_request, old_description)
518 518 PullRequestModel().update_reviewers(user, pull_request_id, reviewers_ids)
519 519 except UserInvalidException as u:
520 520 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
521 521 raise HTTPBadRequest()
522 522
523 523 Session().commit()
524 524 h.flash(_('Pull request updated'), category='success')
525 525
526 526 raise HTTPFound(location=pull_request.url())
527 527
528 528 @LoginRequired()
529 529 @NotAnonymous()
530 530 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
531 531 'repository.admin')
532 532 @jsonify
533 533 def delete(self, repo_name, pull_request_id):
534 534 pull_request = PullRequest.get_or_404(pull_request_id)
535 535 #only owner can delete it !
536 536 if pull_request.owner.user_id == c.authuser.user_id:
537 537 PullRequestModel().delete(pull_request)
538 538 Session().commit()
539 539 h.flash(_('Successfully deleted pull request'),
540 540 category='success')
541 541 raise HTTPFound(location=url('my_pullrequests'))
542 542 raise HTTPForbidden()
543 543
544 544 @LoginRequired()
545 545 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
546 546 'repository.admin')
547 547 def show(self, repo_name, pull_request_id, extra=None):
548 548 repo_model = RepoModel()
549 549 c.users_array = repo_model.get_users_js()
550 550 c.user_groups_array = repo_model.get_user_groups_js()
551 551 c.pull_request = PullRequest.get_or_404(pull_request_id)
552 552 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
553 553 cc_model = ChangesetCommentsModel()
554 554 cs_model = ChangesetStatusModel()
555 555
556 556 # pull_requests repo_name we opened it against
557 557 # ie. other_repo must match
558 558 if repo_name != c.pull_request.other_repo.repo_name:
559 559 raise HTTPNotFound
560 560
561 561 # load compare data into template context
562 562 c.cs_repo = c.pull_request.org_repo
563 563 (c.cs_ref_type,
564 564 c.cs_ref_name,
565 565 c.cs_rev) = c.pull_request.org_ref.split(':')
566 566
567 567 c.a_repo = c.pull_request.other_repo
568 568 (c.a_ref_type,
569 569 c.a_ref_name,
570 570 c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor
571 571
572 572 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
573 573 c.cs_repo = c.cs_repo
574 574 try:
575 575 c.cs_ranges = [org_scm_instance.get_changeset(x)
576 576 for x in c.pull_request.revisions]
577 577 except ChangesetDoesNotExistError:
578 578 c.cs_ranges = []
579 579 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
580 580 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
581 581 c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
582 582
583 583 c.is_range = False
584 584 if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
585 585 cs_a = org_scm_instance.get_changeset(c.a_rev)
586 586 root_parents = c.cs_ranges[0].parents
587 587 c.is_range = cs_a in root_parents
588 588 #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
589 589
590 590 avail_revs = set()
591 591 avail_show = []
592 592 c.cs_branch_name = c.cs_ref_name
593 593 other_scm_instance = c.a_repo.scm_instance
594 594 c.update_msg = ""
595 595 c.update_msg_other = ""
596 596 try:
597 597 if org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
598 598 if c.cs_ref_type != 'branch':
599 599 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
600 600 c.a_branch_name = c.a_ref_name
601 601 if c.a_ref_type != 'branch':
602 602 try:
603 603 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
604 604 except EmptyRepositoryError:
605 605 c.a_branch_name = 'null' # not a branch name ... but close enough
606 606 # candidates: descendants of old head that are on the right branch
607 607 # and not are the old head itself ...
608 608 # and nothing at all if old head is a descendant of target ref name
609 609 if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
610 610 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
611 611 elif c.pull_request.is_closed():
612 612 c.update_msg = _('This pull request has been closed and can not be updated.')
613 613 else: # look for descendants of PR head on source branch in org repo
614 614 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
615 615 revs[0], c.cs_branch_name)
616 616 if len(avail_revs) > 1: # more than just revs[0]
617 617 # also show changesets that not are descendants but would be merged in
618 618 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
619 619 if org_scm_instance.path != other_scm_instance.path:
620 620 # Note: org_scm_instance.path must come first so all
621 621 # valid revision numbers are 100% org_scm compatible
622 622 # - both for avail_revs and for revset results
623 623 hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
624 624 org_scm_instance.path,
625 625 other_scm_instance.path)
626 626 else:
627 627 hgrepo = org_scm_instance._repo
628 628 show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s',
629 629 avail_revs, revs[0], targethead))
630 630 c.update_msg = _('The following changes are available on %s:') % c.cs_branch_name
631 631 else:
632 632 show = set()
633 633 avail_revs = set() # drop revs[0]
634 634 c.update_msg = _('No changesets found for iterating on this pull request.')
635 635
636 636 # TODO: handle branch heads that not are tip-most
637 637 brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
638 638 if brevs:
639 639 # also show changesets that are on branch but neither ancestors nor descendants
640 640 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
641 641 show.add(revs[0]) # make sure graph shows this so we can see how they relate
642 642 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
643 643 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
644 644
645 645 avail_show = sorted(show, reverse=True)
646 646
647 647 elif org_scm_instance.alias == 'git':
648 648 c.cs_repo.scm_instance.get_changeset(c.cs_rev) # check it exists - raise ChangesetDoesNotExistError if not
649 649 c.update_msg = _("Git pull requests don't support iterating yet.")
650 650 except ChangesetDoesNotExistError:
651 651 c.update_msg = _('Error: revision %s was not found. Please create a new pull request!') % c.cs_rev
652 652
653 653 c.avail_revs = avail_revs
654 654 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
655 655 c.avail_jsdata = json.dumps(graph_data(org_scm_instance, avail_show))
656 656
657 657 raw_ids = [x.raw_id for x in c.cs_ranges]
658 658 c.cs_comments = c.cs_repo.get_comments(raw_ids)
659 659 c.statuses = c.cs_repo.statuses(raw_ids)
660 660
661 661 ignore_whitespace = request.GET.get('ignorews') == '1'
662 662 line_context = safe_int(request.GET.get('context'), 3)
663 663 c.ignorews_url = _ignorews_url
664 664 c.context_url = _context_url
665 665 c.fulldiff = request.GET.get('fulldiff')
666 666 diff_limit = self.cut_off_limit if not c.fulldiff else None
667 667
668 668 # we swap org/other ref since we run a simple diff on one repo
669 669 log.debug('running diff between %s and %s in %s',
670 670 c.a_rev, c.cs_rev, org_scm_instance.path)
671 671 try:
672 672 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
673 673 ignore_whitespace=ignore_whitespace,
674 674 context=line_context)
675 675 except ChangesetDoesNotExistError:
676 676 txtdiff = _("The diff can't be shown - the PR revisions could not be found.")
677 677 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
678 678 diff_limit=diff_limit)
679 679 _parsed = diff_processor.prepare()
680 680
681 681 c.limited_diff = False
682 682 if isinstance(_parsed, LimitedDiffContainer):
683 683 c.limited_diff = True
684 684
685 685 c.files = []
686 686 c.changes = {}
687 687 c.lines_added = 0
688 688 c.lines_deleted = 0
689 689
690 690 for f in _parsed:
691 691 st = f['stats']
692 692 c.lines_added += st['added']
693 693 c.lines_deleted += st['deleted']
694 694 fid = h.FID('', f['filename'])
695 695 c.files.append([fid, f['operation'], f['filename'], f['stats']])
696 696 htmldiff = diff_processor.as_html(enable_comments=True,
697 697 parsed_lines=[f])
698 698 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
699 699
700 700 # inline comments
701 701 c.inline_cnt = 0
702 702 c.inline_comments = cc_model.get_inline_comments(
703 703 c.db_repo.repo_id,
704 704 pull_request=pull_request_id)
705 705 # count inline comments
706 706 for __, lines in c.inline_comments:
707 707 for comments in lines.values():
708 708 c.inline_cnt += len(comments)
709 709 # comments
710 710 c.comments = cc_model.get_comments(c.db_repo.repo_id,
711 711 pull_request=pull_request_id)
712 712
713 713 # (badly named) pull-request status calculation based on reviewer votes
714 714 (c.pull_request_reviewers,
715 715 c.pull_request_pending_reviewers,
716 716 c.current_voting_result,
717 717 ) = cs_model.calculate_pull_request_result(c.pull_request)
718 718 c.changeset_statuses = ChangesetStatus.STATUSES
719 719
720 720 c.as_form = False
721 721 c.ancestor = None # there is one - but right here we don't know which
722 722 return render('/pullrequests/pullrequest_show.html')
723 723
724 724 @LoginRequired()
725 725 @NotAnonymous()
726 726 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
727 727 'repository.admin')
728 728 @jsonify
729 729 def comment(self, repo_name, pull_request_id):
730 730 pull_request = PullRequest.get_or_404(pull_request_id)
731 731
732 732 status = request.POST.get('changeset_status')
733 733 close_pr = request.POST.get('save_close')
734 734 delete = request.POST.get('save_delete')
735 735 f_path = request.POST.get('f_path')
736 736 line_no = request.POST.get('line')
737 737
738 738 if (status or close_pr or delete) and (f_path or line_no):
739 739 # status votes and closing is only possible in general comments
740 740 raise HTTPBadRequest()
741 741
742 742 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
743 743 if not allowed_to_change_status:
744 744 if status or close_pr:
745 745 h.flash(_('No permission to change pull request status'), 'error')
746 746 raise HTTPForbidden()
747 747
748 748 if delete == "delete":
749 749 if (pull_request.owner.user_id == c.authuser.user_id or
750 750 h.HasPermissionAny('hg.admin')() or
751 751 h.HasRepoPermissionAny('repository.admin')(pull_request.org_repo.repo_name) or
752 752 h.HasRepoPermissionAny('repository.admin')(pull_request.other_repo.repo_name)
753 753 ) and not pull_request.is_closed():
754 754 PullRequestModel().delete(pull_request)
755 755 Session().commit()
756 756 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
757 757 category='success')
758 758 return {
759 759 'location': url('my_pullrequests'), # or repo pr list?
760 760 }
761 761 raise HTTPFound(location=url('my_pullrequests')) # or repo pr list?
762 762 raise HTTPForbidden()
763 763
764 764 text = request.POST.get('text', '').strip()
765 765 if close_pr:
766 766 text = _('Closing.') + '\n' + text
767 767
768 768 comment = create_comment(
769 769 text,
770 770 status,
771 771 pull_request_id=pull_request_id,
772 772 f_path=f_path,
773 773 line_no=line_no,
774 774 closing_pr=close_pr,
775 775 )
776 776
777 777 action_logger(self.authuser,
778 778 'user_commented_pull_request:%s' % pull_request_id,
779 779 c.db_repo, self.ip_addr, self.sa)
780 780
781 781 if status:
782 782 ChangesetStatusModel().set_status(
783 783 c.db_repo.repo_id,
784 784 status,
785 785 c.authuser.user_id,
786 786 comment,
787 787 pull_request=pull_request_id
788 788 )
789 789
790 790 if close_pr:
791 791 PullRequestModel().close_pull_request(pull_request_id)
792 792 action_logger(self.authuser,
793 793 'user_closed_pull_request:%s' % pull_request_id,
794 794 c.db_repo, self.ip_addr, self.sa)
795 795
796 796 Session().commit()
797 797
798 798 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
799 799 raise HTTPFound(location=pull_request.url())
800 800
801 801 data = {
802 802 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
803 803 }
804 804 if comment is not None:
805 805 c.comment = comment
806 806 data.update(comment.get_dict())
807 807 data.update({'rendered_text':
808 808 render('changeset/changeset_comment_block.html')})
809 809
810 810 return data
811 811
812 812 @LoginRequired()
813 813 @NotAnonymous()
814 814 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
815 815 'repository.admin')
816 816 @jsonify
817 817 def delete_comment(self, repo_name, comment_id):
818 818 co = ChangesetComment.get(comment_id)
819 819 if co.pull_request.is_closed():
820 820 #don't allow deleting comments on closed pull request
821 821 raise HTTPForbidden()
822 822
823 823 owner = co.author.user_id == c.authuser.user_id
824 824 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
825 825 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
826 826 ChangesetCommentsModel().delete(comment=co)
827 827 Session().commit()
828 828 return True
829 829 else:
830 830 raise HTTPForbidden()
@@ -1,147 +1,147 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.search
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Search controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Aug 7, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import urllib
31 31 from pylons.i18n.translation import _
32 32 from pylons import request, config, tmpl_context as c
33 33
34 34 from whoosh.index import open_dir, EmptyIndexError
35 35 from whoosh.qparser import QueryParser, QueryParserError
36 36 from whoosh.query import Phrase, Prefix
37 37 from webhelpers.util import update_params
38 38
39 39 from kallithea.lib.auth import LoginRequired
40 40 from kallithea.lib.base import BaseRepoController, render
41 41 from kallithea.lib.indexers import CHGSETS_SCHEMA, SCHEMA, CHGSET_IDX_NAME, \
42 42 IDX_NAME, WhooshResultWrapper
43 43 from kallithea.model.repo import RepoModel
44 44 from kallithea.lib.utils2 import safe_str, safe_int
45 45 from kallithea.lib.helpers import Page
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class SearchController(BaseRepoController):
51 51
52 52 def __before__(self):
53 53 super(SearchController, self).__before__()
54 54
55 55 @LoginRequired()
56 56 def index(self, repo_name=None):
57 57 c.repo_name = repo_name
58 58 c.formated_results = []
59 59 c.runtime = ''
60 60 c.cur_query = request.GET.get('q', None)
61 61 c.cur_type = request.GET.get('type', 'content')
62 62 c.cur_search = search_type = {'content': 'content',
63 63 'commit': 'message',
64 64 'path': 'path',
65 65 'repository': 'repository'
66 66 }.get(c.cur_type, 'content')
67 67
68 68 index_name = {
69 69 'content': IDX_NAME,
70 70 'commit': CHGSET_IDX_NAME,
71 71 'path': IDX_NAME
72 72 }.get(c.cur_type, IDX_NAME)
73 73
74 74 schema_defn = {
75 75 'content': SCHEMA,
76 76 'commit': CHGSETS_SCHEMA,
77 77 'path': SCHEMA
78 78 }.get(c.cur_type, SCHEMA)
79 79
80 80 log.debug('IDX: %s', index_name)
81 81 log.debug('SCHEMA: %s', schema_defn)
82 82
83 83 if c.cur_query:
84 84 cur_query = c.cur_query.lower()
85 85 log.debug(cur_query)
86 86
87 87 if c.cur_query:
88 p = safe_int(request.GET.get('page', 1), 1)
88 p = safe_int(request.GET.get('page'), 1)
89 89 highlight_items = set()
90 90 try:
91 91 idx = open_dir(config['app_conf']['index_dir'],
92 92 indexname=index_name)
93 93 searcher = idx.searcher()
94 94
95 95 qp = QueryParser(search_type, schema=schema_defn)
96 96 if c.repo_name:
97 97 cur_query = u'repository:%s %s' % (c.repo_name, cur_query)
98 98 try:
99 99 query = qp.parse(unicode(cur_query))
100 100 # extract words for highlight
101 101 if isinstance(query, Phrase):
102 102 highlight_items.update(query.words)
103 103 elif isinstance(query, Prefix):
104 104 highlight_items.add(query.text)
105 105 else:
106 106 for i in query.all_terms():
107 107 if i[0] in ['content', 'message']:
108 108 highlight_items.add(i[1])
109 109
110 110 matcher = query.matcher(searcher)
111 111
112 112 log.debug('query: %s', query)
113 113 log.debug('hl terms: %s', highlight_items)
114 114 results = searcher.search(query)
115 115 res_ln = len(results)
116 116 c.runtime = '%s results (%.3f seconds)' % (
117 117 res_ln, results.runtime
118 118 )
119 119
120 120 def url_generator(**kw):
121 121 q = urllib.quote(safe_str(c.cur_query))
122 122 return update_params("?q=%s&type=%s" \
123 123 % (q, safe_str(c.cur_type)), **kw)
124 124 repo_location = RepoModel().repos_path
125 125 c.formated_results = Page(
126 126 WhooshResultWrapper(search_type, searcher, matcher,
127 127 highlight_items, repo_location),
128 128 page=p,
129 129 item_count=res_ln,
130 130 items_per_page=10,
131 131 url=url_generator
132 132 )
133 133
134 134 except QueryParserError:
135 135 c.runtime = _('Invalid search query. Try quoting it.')
136 136 searcher.close()
137 137 except (EmptyIndexError, IOError):
138 138 log.error(traceback.format_exc())
139 139 log.error('Empty Index data')
140 140 c.runtime = _('There is no index to search in. '
141 141 'Please run whoosh indexer')
142 142 except Exception:
143 143 log.error(traceback.format_exc())
144 144 c.runtime = _('An error occurred during search operation.')
145 145
146 146 # Return a rendered template
147 147 return render('/search/search.html')
General Comments 0
You need to be logged in to leave comments. Login now