##// END OF EJS Templates
apps: cleanup imports
marcink -
r2080:1eba6b1f default
parent child Browse files
Show More
@@ -1,413 +1,413 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27
28 from pyramid.httpexceptions import HTTPNotFound, HTTPForbidden, HTTPFound
28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32
33 33 from rhodecode.apps._base import BaseAppView
34 34 from rhodecode.lib import helpers as h
35 35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 36 from rhodecode.lib.utils2 import time_to_datetime
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
39 39 from rhodecode.model.gist import GistModel
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.db import Gist, User, or_
42 42 from rhodecode.model import validation_schema
43 43 from rhodecode.model.validation_schema.schemas import gist_schema
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class GistView(BaseAppView):
50 50
51 51 def load_default_context(self):
52 52 _ = self.request.translate
53 53 c = self._get_local_tmpl_context()
54 54 c.user = c.auth_user.get_instance()
55 55
56 56 c.lifetime_values = [
57 57 (-1, _('forever')),
58 58 (5, _('5 minutes')),
59 59 (60, _('1 hour')),
60 60 (60 * 24, _('1 day')),
61 61 (60 * 24 * 30, _('1 month')),
62 62 ]
63 63
64 64 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
65 65 c.acl_options = [
66 66 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
67 67 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
68 68 ]
69 69
70 70 self._register_global_c(c)
71 71 return c
72 72
73 73 @LoginRequired()
74 74 @view_config(
75 75 route_name='gists_show', request_method='GET',
76 76 renderer='rhodecode:templates/admin/gists/index.mako')
77 77 def gist_show_all(self):
78 78 c = self.load_default_context()
79 79
80 80 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
81 81 c.show_private = self.request.GET.get('private') and not_default_user
82 82 c.show_public = self.request.GET.get('public') and not_default_user
83 83 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
84 84
85 85 gists = _gists = Gist().query()\
86 86 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
87 87 .order_by(Gist.created_on.desc())
88 88
89 89 c.active = 'public'
90 90 # MY private
91 91 if c.show_private and not c.show_public:
92 92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
93 93 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
94 94 c.active = 'my_private'
95 95 # MY public
96 96 elif c.show_public and not c.show_private:
97 97 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
98 98 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
99 99 c.active = 'my_public'
100 100 # MY public+private
101 101 elif c.show_private and c.show_public:
102 102 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
103 103 Gist.gist_type == Gist.GIST_PRIVATE))\
104 104 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
105 105 c.active = 'my_all'
106 106 # Show all by super-admin
107 107 elif c.show_all:
108 108 c.active = 'all'
109 109 gists = _gists
110 110
111 111 # default show ALL public gists
112 112 if not c.show_public and not c.show_private and not c.show_all:
113 113 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
114 114 c.active = 'public'
115 115
116 116 _render = self.request.get_partial_renderer(
117 117 'data_table/_dt_elements.mako')
118 118
119 119 data = []
120 120
121 121 for gist in gists:
122 122 data.append({
123 123 'created_on': _render('gist_created', gist.created_on),
124 124 'created_on_raw': gist.created_on,
125 125 'type': _render('gist_type', gist.gist_type),
126 126 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
127 127 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
128 128 'author_raw': h.escape(gist.owner.full_contact),
129 129 'expires': _render('gist_expires', gist.gist_expires),
130 130 'description': _render('gist_description', gist.gist_description)
131 131 })
132 132 c.data = json.dumps(data)
133 133
134 134 return self._get_template_context(c)
135 135
136 136 @LoginRequired()
137 137 @NotAnonymous()
138 138 @view_config(
139 139 route_name='gists_new', request_method='GET',
140 140 renderer='rhodecode:templates/admin/gists/new.mako')
141 141 def gist_new(self):
142 142 c = self.load_default_context()
143 143 return self._get_template_context(c)
144 144
145 145 @LoginRequired()
146 146 @NotAnonymous()
147 147 @CSRFRequired()
148 148 @view_config(
149 149 route_name='gists_create', request_method='POST',
150 150 renderer='rhodecode:templates/admin/gists/new.mako')
151 151 def gist_create(self):
152 152 _ = self.request.translate
153 153 c = self.load_default_context()
154 154
155 155 data = dict(self.request.POST)
156 156 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
157 157 data['nodes'] = [{
158 158 'filename': data['filename'],
159 159 'content': data.get('content'),
160 160 'mimetype': data.get('mimetype') # None is autodetect
161 161 }]
162 162
163 163 data['gist_type'] = (
164 164 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
165 165 data['gist_acl_level'] = (
166 166 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
167 167
168 168 schema = gist_schema.GistSchema().bind(
169 169 lifetime_options=[x[0] for x in c.lifetime_values])
170 170
171 171 try:
172 172
173 173 schema_data = schema.deserialize(data)
174 174 # convert to safer format with just KEYs so we sure no duplicates
175 175 schema_data['nodes'] = gist_schema.sequence_to_nodes(
176 176 schema_data['nodes'])
177 177
178 178 gist = GistModel().create(
179 179 gist_id=schema_data['gistid'], # custom access id not real ID
180 180 description=schema_data['description'],
181 181 owner=self._rhodecode_user.user_id,
182 182 gist_mapping=schema_data['nodes'],
183 183 gist_type=schema_data['gist_type'],
184 184 lifetime=schema_data['lifetime'],
185 185 gist_acl_level=schema_data['gist_acl_level']
186 186 )
187 187 Session().commit()
188 188 new_gist_id = gist.gist_access_id
189 189 except validation_schema.Invalid as errors:
190 190 defaults = data
191 191 errors = errors.asdict()
192 192
193 193 if 'nodes.0.content' in errors:
194 194 errors['content'] = errors['nodes.0.content']
195 195 del errors['nodes.0.content']
196 196 if 'nodes.0.filename' in errors:
197 197 errors['filename'] = errors['nodes.0.filename']
198 198 del errors['nodes.0.filename']
199 199
200 200 data = render('rhodecode:templates/admin/gists/new.mako',
201 201 self._get_template_context(c), self.request)
202 202 html = formencode.htmlfill.render(
203 203 data,
204 204 defaults=defaults,
205 205 errors=errors,
206 206 prefix_error=False,
207 207 encoding="UTF-8",
208 208 force_defaults=False
209 209 )
210 210 return Response(html)
211 211
212 212 except Exception:
213 213 log.exception("Exception while trying to create a gist")
214 214 h.flash(_('Error occurred during gist creation'), category='error')
215 215 raise HTTPFound(h.route_url('gists_new'))
216 216 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
217 217
218 218 @LoginRequired()
219 219 @NotAnonymous()
220 220 @CSRFRequired()
221 221 @view_config(
222 222 route_name='gist_delete', request_method='POST')
223 223 def gist_delete(self):
224 224 _ = self.request.translate
225 225 gist_id = self.request.matchdict['gist_id']
226 226
227 227 c = self.load_default_context()
228 228 c.gist = Gist.get_or_404(gist_id)
229 229
230 230 owner = c.gist.gist_owner == self._rhodecode_user.user_id
231 231 if not (h.HasPermissionAny('hg.admin')() or owner):
232 232 log.warning('Deletion of Gist was forbidden '
233 233 'by unauthorized user: `%s`', self._rhodecode_user)
234 234 raise HTTPNotFound()
235 235
236 236 GistModel().delete(c.gist)
237 237 Session().commit()
238 238 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
239 239
240 240 raise HTTPFound(h.route_url('gists_show'))
241 241
242 242 def _get_gist(self, gist_id):
243 243
244 244 gist = Gist.get_or_404(gist_id)
245 245
246 246 # Check if this gist is expired
247 247 if gist.gist_expires != -1:
248 248 if time.time() > gist.gist_expires:
249 249 log.error(
250 250 'Gist expired at %s', time_to_datetime(gist.gist_expires))
251 251 raise HTTPNotFound()
252 252
253 253 # check if this gist requires a login
254 254 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
255 255 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
256 256 log.error("Anonymous user %s tried to access protected gist `%s`",
257 257 self._rhodecode_user, gist_id)
258 258 raise HTTPNotFound()
259 259 return gist
260 260
261 261 @LoginRequired()
262 262 @view_config(
263 263 route_name='gist_show', request_method='GET',
264 264 renderer='rhodecode:templates/admin/gists/show.mako')
265 265 @view_config(
266 266 route_name='gist_show_rev', request_method='GET',
267 267 renderer='rhodecode:templates/admin/gists/show.mako')
268 268 @view_config(
269 269 route_name='gist_show_formatted', request_method='GET',
270 270 renderer=None)
271 271 @view_config(
272 272 route_name='gist_show_formatted_path', request_method='GET',
273 273 renderer=None)
274 274 def gist_show(self):
275 275 gist_id = self.request.matchdict['gist_id']
276 276
277 277 # TODO(marcink): expose those via matching dict
278 278 revision = self.request.matchdict.get('revision', 'tip')
279 279 f_path = self.request.matchdict.get('f_path', None)
280 280 return_format = self.request.matchdict.get('format')
281 281
282 282 c = self.load_default_context()
283 283 c.gist = self._get_gist(gist_id)
284 284 c.render = not self.request.GET.get('no-render', False)
285 285
286 286 try:
287 287 c.file_last_commit, c.files = GistModel().get_gist_files(
288 288 gist_id, revision=revision)
289 289 except VCSError:
290 290 log.exception("Exception in gist show")
291 291 raise HTTPNotFound()
292 292
293 293 if return_format == 'raw':
294 294 content = '\n\n'.join([f.content for f in c.files
295 295 if (f_path is None or f.path == f_path)])
296 296 response = Response(content)
297 297 response.content_type = 'text/plain'
298 298 return response
299 299
300 300 return self._get_template_context(c)
301 301
302 302 @LoginRequired()
303 303 @NotAnonymous()
304 304 @view_config(
305 305 route_name='gist_edit', request_method='GET',
306 306 renderer='rhodecode:templates/admin/gists/edit.mako')
307 307 def gist_edit(self):
308 308 _ = self.request.translate
309 309 gist_id = self.request.matchdict['gist_id']
310 310 c = self.load_default_context()
311 311 c.gist = self._get_gist(gist_id)
312 312
313 313 owner = c.gist.gist_owner == self._rhodecode_user.user_id
314 314 if not (h.HasPermissionAny('hg.admin')() or owner):
315 315 raise HTTPNotFound()
316 316
317 317 try:
318 318 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
319 319 except VCSError:
320 320 log.exception("Exception in gist edit")
321 321 raise HTTPNotFound()
322 322
323 323 if c.gist.gist_expires == -1:
324 324 expiry = _('never')
325 325 else:
326 326 # this cannot use timeago, since it's used in select2 as a value
327 327 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
328 328
329 329 c.lifetime_values.append(
330 330 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
331 331 )
332 332
333 333 return self._get_template_context(c)
334 334
335 335 @LoginRequired()
336 336 @NotAnonymous()
337 337 @CSRFRequired()
338 338 @view_config(
339 339 route_name='gist_update', request_method='POST',
340 340 renderer='rhodecode:templates/admin/gists/edit.mako')
341 341 def gist_update(self):
342 342 _ = self.request.translate
343 343 gist_id = self.request.matchdict['gist_id']
344 344 c = self.load_default_context()
345 345 c.gist = self._get_gist(gist_id)
346 346
347 347 owner = c.gist.gist_owner == self._rhodecode_user.user_id
348 348 if not (h.HasPermissionAny('hg.admin')() or owner):
349 349 raise HTTPNotFound()
350 350
351 351 data = peppercorn.parse(self.request.POST.items())
352 352
353 353 schema = gist_schema.GistSchema()
354 354 schema = schema.bind(
355 355 # '0' is special value to leave lifetime untouched
356 356 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
357 357 )
358 358
359 359 try:
360 360 schema_data = schema.deserialize(data)
361 361 # convert to safer format with just KEYs so we sure no duplicates
362 362 schema_data['nodes'] = gist_schema.sequence_to_nodes(
363 363 schema_data['nodes'])
364 364
365 365 GistModel().update(
366 366 gist=c.gist,
367 367 description=schema_data['description'],
368 368 owner=c.gist.owner,
369 369 gist_mapping=schema_data['nodes'],
370 370 lifetime=schema_data['lifetime'],
371 371 gist_acl_level=schema_data['gist_acl_level']
372 372 )
373 373
374 374 Session().commit()
375 375 h.flash(_('Successfully updated gist content'), category='success')
376 376 except NodeNotChangedError:
377 377 # raised if nothing was changed in repo itself. We anyway then
378 378 # store only DB stuff for gist
379 379 Session().commit()
380 380 h.flash(_('Successfully updated gist data'), category='success')
381 381 except validation_schema.Invalid as errors:
382 382 errors = h.escape(errors.asdict())
383 383 h.flash(_('Error occurred during update of gist {}: {}').format(
384 384 gist_id, errors), category='error')
385 385 except Exception:
386 386 log.exception("Exception in gist edit")
387 387 h.flash(_('Error occurred during update of gist %s') % gist_id,
388 388 category='error')
389 389
390 390 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
391 391
392 392 @LoginRequired()
393 393 @NotAnonymous()
394 394 @view_config(
395 395 route_name='gist_edit_check_revision', request_method='GET',
396 396 renderer='json_ext')
397 397 def gist_edit_check_revision(self):
398 398 _ = self.request.translate
399 399 gist_id = self.request.matchdict['gist_id']
400 400 c = self.load_default_context()
401 401 c.gist = self._get_gist(gist_id)
402 402
403 403 last_rev = c.gist.scm_instance().get_commit()
404 404 success = True
405 405 revision = self.request.GET.get('revision')
406 406
407 407 if revision != last_rev.raw_id:
408 408 log.error('Last revision %s is different then submitted %s'
409 409 % (revision, last_rev))
410 410 # our gist has newer version than we
411 411 success = False
412 412
413 413 return {'success': success}
@@ -1,321 +1,321 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22 import logging
23 23
24 24 from pyramid.view import view_config
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.auth import LoginRequired, NotAnonymous, \
29 HasRepoGroupPermissionAnyDecorator
28 from rhodecode.lib.auth import (
29 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator)
30 30 from rhodecode.lib.index import searcher_from_config
31 31 from rhodecode.lib.utils2 import safe_unicode, str2bool
32 32 from rhodecode.lib.ext_json import json
33 33 from rhodecode.model.db import (
34 34 func, or_, in_filter_generator, Repository, RepoGroup)
35 35 from rhodecode.model.repo import RepoModel
36 36 from rhodecode.model.repo_group import RepoGroupModel
37 37 from rhodecode.model.scm import RepoGroupList, RepoList
38 38 from rhodecode.model.user import UserModel
39 39 from rhodecode.model.user_group import UserGroupModel
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class HomeView(BaseAppView):
45 45
46 46 def load_default_context(self):
47 47 c = self._get_local_tmpl_context()
48 48 c.user = c.auth_user.get_instance()
49 49 self._register_global_c(c)
50 50 return c
51 51
52 52 @LoginRequired()
53 53 @view_config(
54 54 route_name='user_autocomplete_data', request_method='GET',
55 55 renderer='json_ext', xhr=True)
56 56 def user_autocomplete_data(self):
57 57 query = self.request.GET.get('query')
58 58 active = str2bool(self.request.GET.get('active') or True)
59 59 include_groups = str2bool(self.request.GET.get('user_groups'))
60 60 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
61 61 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
62 62
63 63 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
64 64 query, active, include_groups)
65 65
66 66 _users = UserModel().get_users(
67 67 name_contains=query, only_active=active)
68 68
69 69 def maybe_skip_default_user(usr):
70 70 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
71 71 return False
72 72 return True
73 73 _users = filter(maybe_skip_default_user, _users)
74 74
75 75 if include_groups:
76 76 # extend with user groups
77 77 _user_groups = UserGroupModel().get_user_groups(
78 78 name_contains=query, only_active=active,
79 79 expand_groups=expand_groups)
80 80 _users = _users + _user_groups
81 81
82 82 return {'suggestions': _users}
83 83
84 84 @LoginRequired()
85 85 @NotAnonymous()
86 86 @view_config(
87 87 route_name='user_group_autocomplete_data', request_method='GET',
88 88 renderer='json_ext', xhr=True)
89 89 def user_group_autocomplete_data(self):
90 90 query = self.request.GET.get('query')
91 91 active = str2bool(self.request.GET.get('active') or True)
92 92 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
93 93
94 94 log.debug('generating user group list, query:%s, active:%s',
95 95 query, active)
96 96
97 97 _user_groups = UserGroupModel().get_user_groups(
98 98 name_contains=query, only_active=active,
99 99 expand_groups=expand_groups)
100 100 _user_groups = _user_groups
101 101
102 102 return {'suggestions': _user_groups}
103 103
104 104 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
105 105 allowed_ids = self._rhodecode_user.repo_acl_ids(
106 106 ['repository.read', 'repository.write', 'repository.admin'],
107 107 cache=False, name_filter=name_contains)
108 108
109 109 query = Repository.query()\
110 110 .order_by(func.length(Repository.repo_name))\
111 111 .order_by(Repository.repo_name)\
112 112 .filter(or_(
113 113 # generate multiple IN to fix limitation problems
114 114 *in_filter_generator(Repository.repo_id, allowed_ids)
115 115 ))
116 116
117 117 if repo_type:
118 118 query = query.filter(Repository.repo_type == repo_type)
119 119
120 120 if name_contains:
121 121 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
122 122 query = query.filter(
123 123 Repository.repo_name.ilike(ilike_expression))
124 124 query = query.limit(limit)
125 125
126 126 acl_repo_iter = query
127 127
128 128 return [
129 129 {
130 130 'id': obj.repo_name,
131 131 'text': obj.repo_name,
132 132 'type': 'repo',
133 133 'obj': {'repo_type': obj.repo_type, 'private': obj.private},
134 134 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
135 135 }
136 136 for obj in acl_repo_iter]
137 137
138 138 def _get_repo_group_list(self, name_contains=None, limit=20):
139 139 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
140 140 ['group.read', 'group.write', 'group.admin'],
141 141 cache=False, name_filter=name_contains)
142 142
143 143 query = RepoGroup.query()\
144 144 .order_by(func.length(RepoGroup.group_name))\
145 145 .order_by(RepoGroup.group_name) \
146 146 .filter(or_(
147 147 # generate multiple IN to fix limitation problems
148 148 *in_filter_generator(RepoGroup.group_id, allowed_ids)
149 149 ))
150 150
151 151 if name_contains:
152 152 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
153 153 query = query.filter(
154 154 RepoGroup.group_name.ilike(ilike_expression))
155 155 query = query.limit(limit)
156 156
157 157 acl_repo_iter = query
158 158
159 159 return [
160 160 {
161 161 'id': obj.group_name,
162 162 'text': obj.group_name,
163 163 'type': 'group',
164 164 'obj': {},
165 165 'url': h.route_path(
166 166 'repo_group_home', repo_group_name=obj.group_name)
167 167 }
168 168 for obj in acl_repo_iter]
169 169
170 170 def _get_hash_commit_list(self, auth_user, query=None):
171 171 if not query or len(query) < 3:
172 172 return []
173 173
174 174 commit_hashes = re.compile('(?:commit:)([0-9a-f]{2,40})').findall(query)
175 175
176 176 if len(commit_hashes) != 1:
177 177 return []
178 178
179 179 commit_hash_prefix = commit_hashes[0]
180 180
181 181 searcher = searcher_from_config(self.request.registry.settings)
182 182 result = searcher.search(
183 183 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user,
184 184 raise_on_exc=False)
185 185
186 186 return [
187 187 {
188 188 'id': entry['commit_id'],
189 189 'text': entry['commit_id'],
190 190 'type': 'commit',
191 191 'obj': {'repo': entry['repository']},
192 192 'url': h.route_path(
193 193 'repo_commit',
194 194 repo_name=entry['repository'], commit_id=entry['commit_id'])
195 195 }
196 196 for entry in result['results']]
197 197
198 198 @LoginRequired()
199 199 @view_config(
200 200 route_name='repo_list_data', request_method='GET',
201 201 renderer='json_ext', xhr=True)
202 202 def repo_list_data(self):
203 203 _ = self.request.translate
204 204
205 205 query = self.request.GET.get('query')
206 206 repo_type = self.request.GET.get('repo_type')
207 207 log.debug('generating repo list, query:%s, repo_type:%s',
208 208 query, repo_type)
209 209
210 210 res = []
211 211 repos = self._get_repo_list(query, repo_type=repo_type)
212 212 if repos:
213 213 res.append({
214 214 'text': _('Repositories'),
215 215 'children': repos
216 216 })
217 217
218 218 data = {
219 219 'more': False,
220 220 'results': res
221 221 }
222 222 return data
223 223
224 224 @LoginRequired()
225 225 @view_config(
226 226 route_name='goto_switcher_data', request_method='GET',
227 227 renderer='json_ext', xhr=True)
228 228 def goto_switcher_data(self):
229 229 c = self.load_default_context()
230 230
231 231 _ = self.request.translate
232 232
233 233 query = self.request.GET.get('query')
234 234 log.debug('generating goto switcher list, query %s', query)
235 235
236 236 res = []
237 237 repo_groups = self._get_repo_group_list(query)
238 238 if repo_groups:
239 239 res.append({
240 240 'text': _('Groups'),
241 241 'children': repo_groups
242 242 })
243 243
244 244 repos = self._get_repo_list(query)
245 245 if repos:
246 246 res.append({
247 247 'text': _('Repositories'),
248 248 'children': repos
249 249 })
250 250
251 251 commits = self._get_hash_commit_list(c.auth_user, query)
252 252 if commits:
253 253 unique_repos = {}
254 254 for commit in commits:
255 255 unique_repos.setdefault(commit['obj']['repo'], []
256 256 ).append(commit)
257 257
258 258 for repo in unique_repos:
259 259 res.append({
260 260 'text': _('Commits in %(repo)s') % {'repo': repo},
261 261 'children': unique_repos[repo]
262 262 })
263 263
264 264 data = {
265 265 'more': False,
266 266 'results': res
267 267 }
268 268 return data
269 269
270 270 def _get_groups_and_repos(self, repo_group_id=None):
271 271 # repo groups groups
272 272 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
273 273 _perms = ['group.read', 'group.write', 'group.admin']
274 274 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
275 275 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
276 276 repo_group_list=repo_group_list_acl, admin=False)
277 277
278 278 # repositories
279 279 repo_list = Repository.get_all_repos(group_id=repo_group_id)
280 280 _perms = ['repository.read', 'repository.write', 'repository.admin']
281 281 repo_list_acl = RepoList(repo_list, perm_set=_perms)
282 282 repo_data = RepoModel().get_repos_as_dict(
283 283 repo_list=repo_list_acl, admin=False)
284 284
285 285 return repo_data, repo_group_data
286 286
287 287 @LoginRequired()
288 288 @view_config(
289 289 route_name='home', request_method='GET',
290 290 renderer='rhodecode:templates/index.mako')
291 291 def main_page(self):
292 292 c = self.load_default_context()
293 293 c.repo_group = None
294 294
295 295 repo_data, repo_group_data = self._get_groups_and_repos()
296 296 # json used to render the grids
297 297 c.repos_data = json.dumps(repo_data)
298 298 c.repo_groups_data = json.dumps(repo_group_data)
299 299
300 300 return self._get_template_context(c)
301 301
302 302 @LoginRequired()
303 303 @HasRepoGroupPermissionAnyDecorator(
304 304 'group.read', 'group.write', 'group.admin')
305 305 @view_config(
306 306 route_name='repo_group_home', request_method='GET',
307 307 renderer='rhodecode:templates/index_repo_group.mako')
308 308 @view_config(
309 309 route_name='repo_group_home_slash', request_method='GET',
310 310 renderer='rhodecode:templates/index_repo_group.mako')
311 311 def repo_group_main_page(self):
312 312 c = self.load_default_context()
313 313 c.repo_group = self.request.db_repo_group
314 314 repo_data, repo_group_data = self._get_groups_and_repos(
315 315 c.repo_group.group_id)
316 316
317 317 # json used to render the grids
318 318 c.repos_data = json.dumps(repo_data)
319 319 c.repo_groups_data = json.dumps(repo_group_data)
320 320
321 321 return self._get_template_context(c)
@@ -1,425 +1,426 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import collections
23 23 import datetime
24 24 import formencode
25 import formencode.htmlfill
25 26 import logging
26 27 import urlparse
27 28
28 29 from pyramid.httpexceptions import HTTPFound
29 30 from pyramid.view import view_config
30 31 from recaptcha.client.captcha import submit
31 32
32 33 from rhodecode.apps._base import BaseAppView
33 34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
34 35 from rhodecode.events import UserRegistered
35 36 from rhodecode.lib import helpers as h
36 37 from rhodecode.lib import audit_logger
37 38 from rhodecode.lib.auth import (
38 39 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
39 40 from rhodecode.lib.base import get_ip_addr
40 41 from rhodecode.lib.exceptions import UserCreationError
41 42 from rhodecode.lib.utils2 import safe_str
42 43 from rhodecode.model.db import User, UserApiKeys
43 44 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
44 45 from rhodecode.model.meta import Session
45 46 from rhodecode.model.auth_token import AuthTokenModel
46 47 from rhodecode.model.settings import SettingsModel
47 48 from rhodecode.model.user import UserModel
48 49 from rhodecode.translation import _
49 50
50 51
51 52 log = logging.getLogger(__name__)
52 53
53 54 CaptchaData = collections.namedtuple(
54 55 'CaptchaData', 'active, private_key, public_key')
55 56
56 57
57 58 def _store_user_in_session(session, username, remember=False):
58 59 user = User.get_by_username(username, case_insensitive=True)
59 60 auth_user = AuthUser(user.user_id)
60 61 auth_user.set_authenticated()
61 62 cs = auth_user.get_cookie_store()
62 63 session['rhodecode_user'] = cs
63 64 user.update_lastlogin()
64 65 Session().commit()
65 66
66 67 # If they want to be remembered, update the cookie
67 68 if remember:
68 69 _year = (datetime.datetime.now() +
69 70 datetime.timedelta(seconds=60 * 60 * 24 * 365))
70 71 session._set_cookie_expires(_year)
71 72
72 73 session.save()
73 74
74 75 safe_cs = cs.copy()
75 76 safe_cs['password'] = '****'
76 77 log.info('user %s is now authenticated and stored in '
77 78 'session, session attrs %s', username, safe_cs)
78 79
79 80 # dumps session attrs back to cookie
80 81 session._update_cookie_out()
81 82 # we set new cookie
82 83 headers = None
83 84 if session.request['set_cookie']:
84 85 # send set-cookie headers back to response to update cookie
85 86 headers = [('Set-Cookie', session.request['cookie_out'])]
86 87 return headers
87 88
88 89
89 90 def get_came_from(request):
90 91 came_from = safe_str(request.GET.get('came_from', ''))
91 92 parsed = urlparse.urlparse(came_from)
92 93 allowed_schemes = ['http', 'https']
93 94 default_came_from = h.route_path('home')
94 95 if parsed.scheme and parsed.scheme not in allowed_schemes:
95 96 log.error('Suspicious URL scheme detected %s for url %s' %
96 97 (parsed.scheme, parsed))
97 98 came_from = default_came_from
98 99 elif parsed.netloc and request.host != parsed.netloc:
99 100 log.error('Suspicious NETLOC detected %s for url %s server url '
100 101 'is: %s' % (parsed.netloc, parsed, request.host))
101 102 came_from = default_came_from
102 103 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
103 104 log.error('Header injection detected `%s` for url %s server url ' %
104 105 (parsed.path, parsed))
105 106 came_from = default_came_from
106 107
107 108 return came_from or default_came_from
108 109
109 110
110 111 class LoginView(BaseAppView):
111 112
112 113 def load_default_context(self):
113 114 c = self._get_local_tmpl_context()
114 115 c.came_from = get_came_from(self.request)
115 116 self._register_global_c(c)
116 117 return c
117 118
118 119 def _get_captcha_data(self):
119 120 settings = SettingsModel().get_all_settings()
120 121 private_key = settings.get('rhodecode_captcha_private_key')
121 122 public_key = settings.get('rhodecode_captcha_public_key')
122 123 active = bool(private_key)
123 124 return CaptchaData(
124 125 active=active, private_key=private_key, public_key=public_key)
125 126
126 127 @view_config(
127 128 route_name='login', request_method='GET',
128 129 renderer='rhodecode:templates/login.mako')
129 130 def login(self):
130 131 c = self.load_default_context()
131 132 auth_user = self._rhodecode_user
132 133
133 134 # redirect if already logged in
134 135 if (auth_user.is_authenticated and
135 136 not auth_user.is_default and auth_user.ip_allowed):
136 137 raise HTTPFound(c.came_from)
137 138
138 139 # check if we use headers plugin, and try to login using it.
139 140 try:
140 141 log.debug('Running PRE-AUTH for headers based authentication')
141 142 auth_info = authenticate(
142 143 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
143 144 if auth_info:
144 145 headers = _store_user_in_session(
145 146 self.session, auth_info.get('username'))
146 147 raise HTTPFound(c.came_from, headers=headers)
147 148 except UserCreationError as e:
148 149 log.error(e)
149 150 self.session.flash(e, queue='error')
150 151
151 152 return self._get_template_context(c)
152 153
153 154 @view_config(
154 155 route_name='login', request_method='POST',
155 156 renderer='rhodecode:templates/login.mako')
156 157 def login_post(self):
157 158 c = self.load_default_context()
158 159
159 160 login_form = LoginForm()()
160 161
161 162 try:
162 163 self.session.invalidate()
163 164 form_result = login_form.to_python(self.request.params)
164 165 # form checks for username/password, now we're authenticated
165 166 headers = _store_user_in_session(
166 167 self.session,
167 168 username=form_result['username'],
168 169 remember=form_result['remember'])
169 170 log.debug('Redirecting to "%s" after login.', c.came_from)
170 171
171 172 audit_user = audit_logger.UserWrap(
172 173 username=self.request.params.get('username'),
173 174 ip_addr=self.request.remote_addr)
174 175 action_data = {'user_agent': self.request.user_agent}
175 176 audit_logger.store_web(
176 177 'user.login.success', action_data=action_data,
177 178 user=audit_user, commit=True)
178 179
179 180 raise HTTPFound(c.came_from, headers=headers)
180 181 except formencode.Invalid as errors:
181 182 defaults = errors.value
182 183 # remove password from filling in form again
183 184 defaults.pop('password', None)
184 185 render_ctx = self._get_template_context(c)
185 186 render_ctx.update({
186 187 'errors': errors.error_dict,
187 188 'defaults': defaults,
188 189 })
189 190
190 191 audit_user = audit_logger.UserWrap(
191 192 username=self.request.params.get('username'),
192 193 ip_addr=self.request.remote_addr)
193 194 action_data = {'user_agent': self.request.user_agent}
194 195 audit_logger.store_web(
195 196 'user.login.failure', action_data=action_data,
196 197 user=audit_user, commit=True)
197 198 return render_ctx
198 199
199 200 except UserCreationError as e:
200 201 # headers auth or other auth functions that create users on
201 202 # the fly can throw this exception signaling that there's issue
202 203 # with user creation, explanation should be provided in
203 204 # Exception itself
204 205 self.session.flash(e, queue='error')
205 206 return self._get_template_context(c)
206 207
207 208 @CSRFRequired()
208 209 @view_config(route_name='logout', request_method='POST')
209 210 def logout(self):
210 211 auth_user = self._rhodecode_user
211 212 log.info('Deleting session for user: `%s`', auth_user)
212 213
213 214 action_data = {'user_agent': self.request.user_agent}
214 215 audit_logger.store_web(
215 216 'user.logout', action_data=action_data,
216 217 user=auth_user, commit=True)
217 218 self.session.delete()
218 219 return HTTPFound(h.route_path('home'))
219 220
220 221 @HasPermissionAnyDecorator(
221 222 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
222 223 @view_config(
223 224 route_name='register', request_method='GET',
224 225 renderer='rhodecode:templates/register.mako',)
225 226 def register(self, defaults=None, errors=None):
226 227 c = self.load_default_context()
227 228 defaults = defaults or {}
228 229 errors = errors or {}
229 230
230 231 settings = SettingsModel().get_all_settings()
231 232 register_message = settings.get('rhodecode_register_message') or ''
232 233 captcha = self._get_captcha_data()
233 234 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
234 235 .AuthUser().permissions['global']
235 236
236 237 render_ctx = self._get_template_context(c)
237 238 render_ctx.update({
238 239 'defaults': defaults,
239 240 'errors': errors,
240 241 'auto_active': auto_active,
241 242 'captcha_active': captcha.active,
242 243 'captcha_public_key': captcha.public_key,
243 244 'register_message': register_message,
244 245 })
245 246 return render_ctx
246 247
247 248 @HasPermissionAnyDecorator(
248 249 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
249 250 @view_config(
250 251 route_name='register', request_method='POST',
251 252 renderer='rhodecode:templates/register.mako')
252 253 def register_post(self):
253 254 captcha = self._get_captcha_data()
254 255 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
255 256 .AuthUser().permissions['global']
256 257
257 258 register_form = RegisterForm()()
258 259 try:
259 260 form_result = register_form.to_python(self.request.params)
260 261 form_result['active'] = auto_active
261 262
262 263 if captcha.active:
263 264 response = submit(
264 265 self.request.params.get('recaptcha_challenge_field'),
265 266 self.request.params.get('recaptcha_response_field'),
266 267 private_key=captcha.private_key,
267 268 remoteip=get_ip_addr(self.request.environ))
268 269 if not response.is_valid:
269 270 _value = form_result
270 271 _msg = _('Bad captcha')
271 272 error_dict = {'recaptcha_field': _msg}
272 273 raise formencode.Invalid(_msg, _value, None,
273 274 error_dict=error_dict)
274 275
275 276 new_user = UserModel().create_registration(form_result)
276 277 event = UserRegistered(user=new_user, session=self.session)
277 278 self.request.registry.notify(event)
278 279 self.session.flash(
279 280 _('You have successfully registered with RhodeCode'),
280 281 queue='success')
281 282 Session().commit()
282 283
283 284 redirect_ro = self.request.route_path('login')
284 285 raise HTTPFound(redirect_ro)
285 286
286 287 except formencode.Invalid as errors:
287 288 errors.value.pop('password', None)
288 289 errors.value.pop('password_confirmation', None)
289 290 return self.register(
290 291 defaults=errors.value, errors=errors.error_dict)
291 292
292 293 except UserCreationError as e:
293 294 # container auth or other auth functions that create users on
294 295 # the fly can throw this exception signaling that there's issue
295 296 # with user creation, explanation should be provided in
296 297 # Exception itself
297 298 self.session.flash(e, queue='error')
298 299 return self.register()
299 300
300 301 @view_config(
301 302 route_name='reset_password', request_method=('GET', 'POST'),
302 303 renderer='rhodecode:templates/password_reset.mako')
303 304 def password_reset(self):
304 305 captcha = self._get_captcha_data()
305 306
306 307 render_ctx = {
307 308 'captcha_active': captcha.active,
308 309 'captcha_public_key': captcha.public_key,
309 310 'defaults': {},
310 311 'errors': {},
311 312 }
312 313
313 314 # always send implicit message to prevent from discovery of
314 315 # matching emails
315 316 msg = _('If such email exists, a password reset link was sent to it.')
316 317
317 318 if self.request.POST:
318 319 if h.HasPermissionAny('hg.password_reset.disabled')():
319 320 _email = self.request.POST.get('email', '')
320 321 log.error('Failed attempt to reset password for `%s`.', _email)
321 322 self.session.flash(_('Password reset has been disabled.'),
322 323 queue='error')
323 324 return HTTPFound(self.request.route_path('reset_password'))
324 325
325 326 password_reset_form = PasswordResetForm()()
326 327 try:
327 328 form_result = password_reset_form.to_python(
328 329 self.request.params)
329 330 user_email = form_result['email']
330 331
331 332 if captcha.active:
332 333 response = submit(
333 334 self.request.params.get('recaptcha_challenge_field'),
334 335 self.request.params.get('recaptcha_response_field'),
335 336 private_key=captcha.private_key,
336 337 remoteip=get_ip_addr(self.request.environ))
337 338 if not response.is_valid:
338 339 _value = form_result
339 340 _msg = _('Bad captcha')
340 341 error_dict = {'recaptcha_field': _msg}
341 342 raise formencode.Invalid(
342 343 _msg, _value, None, error_dict=error_dict)
343 344
344 345 # Generate reset URL and send mail.
345 346 user = User.get_by_email(user_email)
346 347
347 348 # generate password reset token that expires in 10minutes
348 349 desc = 'Generated token for password reset from {}'.format(
349 350 datetime.datetime.now().isoformat())
350 351 reset_token = AuthTokenModel().create(
351 352 user, lifetime=10,
352 353 description=desc,
353 354 role=UserApiKeys.ROLE_PASSWORD_RESET)
354 355 Session().commit()
355 356
356 357 log.debug('Successfully created password recovery token')
357 358 password_reset_url = self.request.route_url(
358 359 'reset_password_confirmation',
359 360 _query={'key': reset_token.api_key})
360 361 UserModel().reset_password_link(
361 362 form_result, password_reset_url)
362 363 # Display success message and redirect.
363 364 self.session.flash(msg, queue='success')
364 365
365 366 action_data = {'email': user_email,
366 367 'user_agent': self.request.user_agent}
367 368 audit_logger.store_web(
368 369 'user.password.reset_request', action_data=action_data,
369 370 user=self._rhodecode_user, commit=True)
370 371 return HTTPFound(self.request.route_path('reset_password'))
371 372
372 373 except formencode.Invalid as errors:
373 374 render_ctx.update({
374 375 'defaults': errors.value,
375 376 'errors': errors.error_dict,
376 377 })
377 378 if not self.request.params.get('email'):
378 379 # case of empty email, we want to report that
379 380 return render_ctx
380 381
381 382 if 'recaptcha_field' in errors.error_dict:
382 383 # case of failed captcha
383 384 return render_ctx
384 385
385 386 log.debug('faking response on invalid password reset')
386 387 # make this take 2s, to prevent brute forcing.
387 388 time.sleep(2)
388 389 self.session.flash(msg, queue='success')
389 390 return HTTPFound(self.request.route_path('reset_password'))
390 391
391 392 return render_ctx
392 393
393 394 @view_config(route_name='reset_password_confirmation',
394 395 request_method='GET')
395 396 def password_reset_confirmation(self):
396 397
397 398 if self.request.GET and self.request.GET.get('key'):
398 399 # make this take 2s, to prevent brute forcing.
399 400 time.sleep(2)
400 401
401 402 token = AuthTokenModel().get_auth_token(
402 403 self.request.GET.get('key'))
403 404
404 405 # verify token is the correct role
405 406 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
406 407 log.debug('Got token with role:%s expected is %s',
407 408 getattr(token, 'role', 'EMPTY_TOKEN'),
408 409 UserApiKeys.ROLE_PASSWORD_RESET)
409 410 self.session.flash(
410 411 _('Given reset token is invalid'), queue='error')
411 412 return HTTPFound(self.request.route_path('reset_password'))
412 413
413 414 try:
414 415 owner = token.user
415 416 data = {'email': owner.email, 'token': token.api_key}
416 417 UserModel().reset_password(data)
417 418 self.session.flash(
418 419 _('Your password reset was successful, '
419 420 'a new password has been sent to your email'),
420 421 queue='success')
421 422 except Exception as e:
422 423 log.error(e)
423 424 return HTTPFound(self.request.route_path('reset_password'))
424 425
425 426 return HTTPFound(self.request.route_path('login'))
@@ -1,586 +1,587 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23
24 24 import formencode
25 import formencode.htmlfill
25 26 from pyramid.httpexceptions import HTTPFound
26 27 from pyramid.view import view_config
27 28 from pyramid.renderers import render
28 29 from pyramid.response import Response
29 30
30 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 32 from rhodecode import forms
32 33 from rhodecode.lib import helpers as h
33 34 from rhodecode.lib import audit_logger
34 35 from rhodecode.lib.ext_json import json
35 36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 from rhodecode.lib.channelstream import channelstream_request, \
37 ChannelstreamException
37 from rhodecode.lib.channelstream import (
38 channelstream_request, ChannelstreamException)
38 39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
39 40 from rhodecode.model.auth_token import AuthTokenModel
40 41 from rhodecode.model.comment import CommentsModel
41 42 from rhodecode.model.db import (
42 43 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
43 44 PullRequest)
44 45 from rhodecode.model.forms import UserForm
45 46 from rhodecode.model.meta import Session
46 47 from rhodecode.model.pull_request import PullRequestModel
47 48 from rhodecode.model.scm import RepoList
48 49 from rhodecode.model.user import UserModel
49 50 from rhodecode.model.repo import RepoModel
50 51 from rhodecode.model.validation_schema.schemas import user_schema
51 52
52 53 log = logging.getLogger(__name__)
53 54
54 55
55 56 class MyAccountView(BaseAppView, DataGridAppView):
56 57 ALLOW_SCOPED_TOKENS = False
57 58 """
58 59 This view has alternative version inside EE, if modified please take a look
59 60 in there as well.
60 61 """
61 62
62 63 def load_default_context(self):
63 64 c = self._get_local_tmpl_context()
64 65 c.user = c.auth_user.get_instance()
65 66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 67 self._register_global_c(c)
67 68 return c
68 69
69 70 @LoginRequired()
70 71 @NotAnonymous()
71 72 @view_config(
72 73 route_name='my_account_profile', request_method='GET',
73 74 renderer='rhodecode:templates/admin/my_account/my_account.mako')
74 75 def my_account_profile(self):
75 76 c = self.load_default_context()
76 77 c.active = 'profile'
77 78 return self._get_template_context(c)
78 79
79 80 @LoginRequired()
80 81 @NotAnonymous()
81 82 @view_config(
82 83 route_name='my_account_password', request_method='GET',
83 84 renderer='rhodecode:templates/admin/my_account/my_account.mako')
84 85 def my_account_password(self):
85 86 c = self.load_default_context()
86 87 c.active = 'password'
87 88 c.extern_type = c.user.extern_type
88 89
89 90 schema = user_schema.ChangePasswordSchema().bind(
90 91 username=c.user.username)
91 92
92 93 form = forms.Form(
93 94 schema,
94 95 action=h.route_path('my_account_password_update'),
95 96 buttons=(forms.buttons.save, forms.buttons.reset))
96 97
97 98 c.form = form
98 99 return self._get_template_context(c)
99 100
100 101 @LoginRequired()
101 102 @NotAnonymous()
102 103 @CSRFRequired()
103 104 @view_config(
104 105 route_name='my_account_password_update', request_method='POST',
105 106 renderer='rhodecode:templates/admin/my_account/my_account.mako')
106 107 def my_account_password_update(self):
107 108 _ = self.request.translate
108 109 c = self.load_default_context()
109 110 c.active = 'password'
110 111 c.extern_type = c.user.extern_type
111 112
112 113 schema = user_schema.ChangePasswordSchema().bind(
113 114 username=c.user.username)
114 115
115 116 form = forms.Form(
116 117 schema, buttons=(forms.buttons.save, forms.buttons.reset))
117 118
118 119 if c.extern_type != 'rhodecode':
119 120 raise HTTPFound(self.request.route_path('my_account_password'))
120 121
121 122 controls = self.request.POST.items()
122 123 try:
123 124 valid_data = form.validate(controls)
124 125 UserModel().update_user(c.user.user_id, **valid_data)
125 126 c.user.update_userdata(force_password_change=False)
126 127 Session().commit()
127 128 except forms.ValidationFailure as e:
128 129 c.form = e
129 130 return self._get_template_context(c)
130 131
131 132 except Exception:
132 133 log.exception("Exception updating password")
133 134 h.flash(_('Error occurred during update of user password'),
134 135 category='error')
135 136 else:
136 137 instance = c.auth_user.get_instance()
137 138 self.session.setdefault('rhodecode_user', {}).update(
138 139 {'password': md5(instance.password)})
139 140 self.session.save()
140 141 h.flash(_("Successfully updated password"), category='success')
141 142
142 143 raise HTTPFound(self.request.route_path('my_account_password'))
143 144
144 145 @LoginRequired()
145 146 @NotAnonymous()
146 147 @view_config(
147 148 route_name='my_account_auth_tokens', request_method='GET',
148 149 renderer='rhodecode:templates/admin/my_account/my_account.mako')
149 150 def my_account_auth_tokens(self):
150 151 _ = self.request.translate
151 152
152 153 c = self.load_default_context()
153 154 c.active = 'auth_tokens'
154 155
155 156 c.lifetime_values = [
156 157 (str(-1), _('forever')),
157 158 (str(5), _('5 minutes')),
158 159 (str(60), _('1 hour')),
159 160 (str(60 * 24), _('1 day')),
160 161 (str(60 * 24 * 30), _('1 month')),
161 162 ]
162 163 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
163 164 c.role_values = [
164 165 (x, AuthTokenModel.cls._get_role_name(x))
165 166 for x in AuthTokenModel.cls.ROLES]
166 167 c.role_options = [(c.role_values, _("Role"))]
167 168 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
168 169 c.user.user_id, show_expired=True)
169 170 return self._get_template_context(c)
170 171
171 172 def maybe_attach_token_scope(self, token):
172 173 # implemented in EE edition
173 174 pass
174 175
175 176 @LoginRequired()
176 177 @NotAnonymous()
177 178 @CSRFRequired()
178 179 @view_config(
179 180 route_name='my_account_auth_tokens_add', request_method='POST',)
180 181 def my_account_auth_tokens_add(self):
181 182 _ = self.request.translate
182 183 c = self.load_default_context()
183 184
184 185 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
185 186 description = self.request.POST.get('description')
186 187 role = self.request.POST.get('role')
187 188
188 189 token = AuthTokenModel().create(
189 190 c.user.user_id, description, lifetime, role)
190 191 token_data = token.get_api_data()
191 192
192 193 self.maybe_attach_token_scope(token)
193 194 audit_logger.store_web(
194 195 'user.edit.token.add', action_data={
195 196 'data': {'token': token_data, 'user': 'self'}},
196 197 user=self._rhodecode_user, )
197 198 Session().commit()
198 199
199 200 h.flash(_("Auth token successfully created"), category='success')
200 201 return HTTPFound(h.route_path('my_account_auth_tokens'))
201 202
202 203 @LoginRequired()
203 204 @NotAnonymous()
204 205 @CSRFRequired()
205 206 @view_config(
206 207 route_name='my_account_auth_tokens_delete', request_method='POST')
207 208 def my_account_auth_tokens_delete(self):
208 209 _ = self.request.translate
209 210 c = self.load_default_context()
210 211
211 212 del_auth_token = self.request.POST.get('del_auth_token')
212 213
213 214 if del_auth_token:
214 215 token = UserApiKeys.get_or_404(del_auth_token)
215 216 token_data = token.get_api_data()
216 217
217 218 AuthTokenModel().delete(del_auth_token, c.user.user_id)
218 219 audit_logger.store_web(
219 220 'user.edit.token.delete', action_data={
220 221 'data': {'token': token_data, 'user': 'self'}},
221 222 user=self._rhodecode_user,)
222 223 Session().commit()
223 224 h.flash(_("Auth token successfully deleted"), category='success')
224 225
225 226 return HTTPFound(h.route_path('my_account_auth_tokens'))
226 227
227 228 @LoginRequired()
228 229 @NotAnonymous()
229 230 @view_config(
230 231 route_name='my_account_emails', request_method='GET',
231 232 renderer='rhodecode:templates/admin/my_account/my_account.mako')
232 233 def my_account_emails(self):
233 234 _ = self.request.translate
234 235
235 236 c = self.load_default_context()
236 237 c.active = 'emails'
237 238
238 239 c.user_email_map = UserEmailMap.query()\
239 240 .filter(UserEmailMap.user == c.user).all()
240 241 return self._get_template_context(c)
241 242
242 243 @LoginRequired()
243 244 @NotAnonymous()
244 245 @CSRFRequired()
245 246 @view_config(
246 247 route_name='my_account_emails_add', request_method='POST')
247 248 def my_account_emails_add(self):
248 249 _ = self.request.translate
249 250 c = self.load_default_context()
250 251
251 252 email = self.request.POST.get('new_email')
252 253
253 254 try:
254 255 UserModel().add_extra_email(c.user.user_id, email)
255 256 audit_logger.store_web(
256 257 'user.edit.email.add', action_data={
257 258 'data': {'email': email, 'user': 'self'}},
258 259 user=self._rhodecode_user,)
259 260
260 261 Session().commit()
261 262 h.flash(_("Added new email address `%s` for user account") % email,
262 263 category='success')
263 264 except formencode.Invalid as error:
264 265 h.flash(h.escape(error.error_dict['email']), category='error')
265 266 except Exception:
266 267 log.exception("Exception in my_account_emails")
267 268 h.flash(_('An error occurred during email saving'),
268 269 category='error')
269 270 return HTTPFound(h.route_path('my_account_emails'))
270 271
271 272 @LoginRequired()
272 273 @NotAnonymous()
273 274 @CSRFRequired()
274 275 @view_config(
275 276 route_name='my_account_emails_delete', request_method='POST')
276 277 def my_account_emails_delete(self):
277 278 _ = self.request.translate
278 279 c = self.load_default_context()
279 280
280 281 del_email_id = self.request.POST.get('del_email_id')
281 282 if del_email_id:
282 283 email = UserEmailMap.get_or_404(del_email_id).email
283 284 UserModel().delete_extra_email(c.user.user_id, del_email_id)
284 285 audit_logger.store_web(
285 286 'user.edit.email.delete', action_data={
286 287 'data': {'email': email, 'user': 'self'}},
287 288 user=self._rhodecode_user,)
288 289 Session().commit()
289 290 h.flash(_("Email successfully deleted"),
290 291 category='success')
291 292 return HTTPFound(h.route_path('my_account_emails'))
292 293
293 294 @LoginRequired()
294 295 @NotAnonymous()
295 296 @CSRFRequired()
296 297 @view_config(
297 298 route_name='my_account_notifications_test_channelstream',
298 299 request_method='POST', renderer='json_ext')
299 300 def my_account_notifications_test_channelstream(self):
300 301 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
301 302 self._rhodecode_user.username, datetime.datetime.now())
302 303 payload = {
303 304 # 'channel': 'broadcast',
304 305 'type': 'message',
305 306 'timestamp': datetime.datetime.utcnow(),
306 307 'user': 'system',
307 308 'pm_users': [self._rhodecode_user.username],
308 309 'message': {
309 310 'message': message,
310 311 'level': 'info',
311 312 'topic': '/notifications'
312 313 }
313 314 }
314 315
315 316 registry = self.request.registry
316 317 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
317 318 channelstream_config = rhodecode_plugins.get('channelstream', {})
318 319
319 320 try:
320 321 channelstream_request(channelstream_config, [payload], '/message')
321 322 except ChannelstreamException as e:
322 323 log.exception('Failed to send channelstream data')
323 324 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
324 325 return {"response": 'Channelstream data sent. '
325 326 'You should see a new live message now.'}
326 327
327 328 def _load_my_repos_data(self, watched=False):
328 329 if watched:
329 330 admin = False
330 331 follows_repos = Session().query(UserFollowing)\
331 332 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
332 333 .options(joinedload(UserFollowing.follows_repository))\
333 334 .all()
334 335 repo_list = [x.follows_repository for x in follows_repos]
335 336 else:
336 337 admin = True
337 338 repo_list = Repository.get_all_repos(
338 339 user_id=self._rhodecode_user.user_id)
339 340 repo_list = RepoList(repo_list, perm_set=[
340 341 'repository.read', 'repository.write', 'repository.admin'])
341 342
342 343 repos_data = RepoModel().get_repos_as_dict(
343 344 repo_list=repo_list, admin=admin)
344 345 # json used to render the grid
345 346 return json.dumps(repos_data)
346 347
347 348 @LoginRequired()
348 349 @NotAnonymous()
349 350 @view_config(
350 351 route_name='my_account_repos', request_method='GET',
351 352 renderer='rhodecode:templates/admin/my_account/my_account.mako')
352 353 def my_account_repos(self):
353 354 c = self.load_default_context()
354 355 c.active = 'repos'
355 356
356 357 # json used to render the grid
357 358 c.data = self._load_my_repos_data()
358 359 return self._get_template_context(c)
359 360
360 361 @LoginRequired()
361 362 @NotAnonymous()
362 363 @view_config(
363 364 route_name='my_account_watched', request_method='GET',
364 365 renderer='rhodecode:templates/admin/my_account/my_account.mako')
365 366 def my_account_watched(self):
366 367 c = self.load_default_context()
367 368 c.active = 'watched'
368 369
369 370 # json used to render the grid
370 371 c.data = self._load_my_repos_data(watched=True)
371 372 return self._get_template_context(c)
372 373
373 374 @LoginRequired()
374 375 @NotAnonymous()
375 376 @view_config(
376 377 route_name='my_account_perms', request_method='GET',
377 378 renderer='rhodecode:templates/admin/my_account/my_account.mako')
378 379 def my_account_perms(self):
379 380 c = self.load_default_context()
380 381 c.active = 'perms'
381 382
382 383 c.perm_user = c.auth_user
383 384 return self._get_template_context(c)
384 385
385 386 @LoginRequired()
386 387 @NotAnonymous()
387 388 @view_config(
388 389 route_name='my_account_notifications', request_method='GET',
389 390 renderer='rhodecode:templates/admin/my_account/my_account.mako')
390 391 def my_notifications(self):
391 392 c = self.load_default_context()
392 393 c.active = 'notifications'
393 394
394 395 return self._get_template_context(c)
395 396
396 397 @LoginRequired()
397 398 @NotAnonymous()
398 399 @CSRFRequired()
399 400 @view_config(
400 401 route_name='my_account_notifications_toggle_visibility',
401 402 request_method='POST', renderer='json_ext')
402 403 def my_notifications_toggle_visibility(self):
403 404 user = self._rhodecode_db_user
404 405 new_status = not user.user_data.get('notification_status', True)
405 406 user.update_userdata(notification_status=new_status)
406 407 Session().commit()
407 408 return user.user_data['notification_status']
408 409
409 410 @LoginRequired()
410 411 @NotAnonymous()
411 412 @view_config(
412 413 route_name='my_account_edit',
413 414 request_method='GET',
414 415 renderer='rhodecode:templates/admin/my_account/my_account.mako')
415 416 def my_account_edit(self):
416 417 c = self.load_default_context()
417 418 c.active = 'profile_edit'
418 419
419 420 c.perm_user = c.auth_user
420 421 c.extern_type = c.user.extern_type
421 422 c.extern_name = c.user.extern_name
422 423
423 424 defaults = c.user.get_dict()
424 425
425 426 data = render('rhodecode:templates/admin/my_account/my_account.mako',
426 427 self._get_template_context(c), self.request)
427 428 html = formencode.htmlfill.render(
428 429 data,
429 430 defaults=defaults,
430 431 encoding="UTF-8",
431 432 force_defaults=False
432 433 )
433 434 return Response(html)
434 435
435 436 @LoginRequired()
436 437 @NotAnonymous()
437 438 @CSRFRequired()
438 439 @view_config(
439 440 route_name='my_account_update',
440 441 request_method='POST',
441 442 renderer='rhodecode:templates/admin/my_account/my_account.mako')
442 443 def my_account_update(self):
443 444 _ = self.request.translate
444 445 c = self.load_default_context()
445 446 c.active = 'profile_edit'
446 447
447 448 c.perm_user = c.auth_user
448 449 c.extern_type = c.user.extern_type
449 450 c.extern_name = c.user.extern_name
450 451
451 452 _form = UserForm(edit=True,
452 453 old_data={'user_id': self._rhodecode_user.user_id,
453 454 'email': self._rhodecode_user.email})()
454 455 form_result = {}
455 456 try:
456 457 post_data = dict(self.request.POST)
457 458 post_data['new_password'] = ''
458 459 post_data['password_confirmation'] = ''
459 460 form_result = _form.to_python(post_data)
460 461 # skip updating those attrs for my account
461 462 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
462 463 'new_password', 'password_confirmation']
463 464 # TODO: plugin should define if username can be updated
464 465 if c.extern_type != "rhodecode":
465 466 # forbid updating username for external accounts
466 467 skip_attrs.append('username')
467 468
468 469 UserModel().update_user(
469 470 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
470 471 **form_result)
471 472 h.flash(_('Your account was updated successfully'),
472 473 category='success')
473 474 Session().commit()
474 475
475 476 except formencode.Invalid as errors:
476 477 data = render(
477 478 'rhodecode:templates/admin/my_account/my_account.mako',
478 479 self._get_template_context(c), self.request)
479 480
480 481 html = formencode.htmlfill.render(
481 482 data,
482 483 defaults=errors.value,
483 484 errors=errors.error_dict or {},
484 485 prefix_error=False,
485 486 encoding="UTF-8",
486 487 force_defaults=False)
487 488 return Response(html)
488 489
489 490 except Exception:
490 491 log.exception("Exception updating user")
491 492 h.flash(_('Error occurred during update of user %s')
492 493 % form_result.get('username'), category='error')
493 494 raise HTTPFound(h.route_path('my_account_profile'))
494 495
495 496 raise HTTPFound(h.route_path('my_account_profile'))
496 497
497 498 def _get_pull_requests_list(self, statuses):
498 499 draw, start, limit = self._extract_chunk(self.request)
499 500 search_q, order_by, order_dir = self._extract_ordering(self.request)
500 501 _render = self.request.get_partial_renderer(
501 502 'data_table/_dt_elements.mako')
502 503
503 504 pull_requests = PullRequestModel().get_im_participating_in(
504 505 user_id=self._rhodecode_user.user_id,
505 506 statuses=statuses,
506 507 offset=start, length=limit, order_by=order_by,
507 508 order_dir=order_dir)
508 509
509 510 pull_requests_total_count = PullRequestModel().count_im_participating_in(
510 511 user_id=self._rhodecode_user.user_id, statuses=statuses)
511 512
512 513 data = []
513 514 comments_model = CommentsModel()
514 515 for pr in pull_requests:
515 516 repo_id = pr.target_repo_id
516 517 comments = comments_model.get_all_comments(
517 518 repo_id, pull_request=pr)
518 519 owned = pr.user_id == self._rhodecode_user.user_id
519 520
520 521 data.append({
521 522 'target_repo': _render('pullrequest_target_repo',
522 523 pr.target_repo.repo_name),
523 524 'name': _render('pullrequest_name',
524 525 pr.pull_request_id, pr.target_repo.repo_name,
525 526 short=True),
526 527 'name_raw': pr.pull_request_id,
527 528 'status': _render('pullrequest_status',
528 529 pr.calculated_review_status()),
529 530 'title': _render(
530 531 'pullrequest_title', pr.title, pr.description),
531 532 'description': h.escape(pr.description),
532 533 'updated_on': _render('pullrequest_updated_on',
533 534 h.datetime_to_time(pr.updated_on)),
534 535 'updated_on_raw': h.datetime_to_time(pr.updated_on),
535 536 'created_on': _render('pullrequest_updated_on',
536 537 h.datetime_to_time(pr.created_on)),
537 538 'created_on_raw': h.datetime_to_time(pr.created_on),
538 539 'author': _render('pullrequest_author',
539 540 pr.author.full_contact, ),
540 541 'author_raw': pr.author.full_name,
541 542 'comments': _render('pullrequest_comments', len(comments)),
542 543 'comments_raw': len(comments),
543 544 'closed': pr.is_closed(),
544 545 'owned': owned
545 546 })
546 547
547 548 # json used to render the grid
548 549 data = ({
549 550 'draw': draw,
550 551 'data': data,
551 552 'recordsTotal': pull_requests_total_count,
552 553 'recordsFiltered': pull_requests_total_count,
553 554 })
554 555 return data
555 556
556 557 @LoginRequired()
557 558 @NotAnonymous()
558 559 @view_config(
559 560 route_name='my_account_pullrequests',
560 561 request_method='GET',
561 562 renderer='rhodecode:templates/admin/my_account/my_account.mako')
562 563 def my_account_pullrequests(self):
563 564 c = self.load_default_context()
564 565 c.active = 'pullrequests'
565 566 req_get = self.request.GET
566 567
567 568 c.closed = str2bool(req_get.get('pr_show_closed'))
568 569
569 570 return self._get_template_context(c)
570 571
571 572 @LoginRequired()
572 573 @NotAnonymous()
573 574 @view_config(
574 575 route_name='my_account_pullrequests_data',
575 576 request_method='GET', renderer='json_ext')
576 577 def my_account_pullrequests_data(self):
577 578 req_get = self.request.GET
578 579 closed = str2bool(req_get.get('closed'))
579 580
580 581 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
581 582 if closed:
582 583 statuses += [PullRequest.STATUS_CLOSED]
583 584
584 585 data = self._get_pull_requests_list(statuses=statuses)
585 586 return data
586 587
@@ -1,78 +1,78 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPFound
24 24 from pyramid.view import view_config
25 25
26 26 from rhodecode.apps._base import RepoAppView
27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
28 CSRFRequired
27 from rhodecode.lib.auth import (
28 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.model.meta import Session
31 31 from rhodecode.model.scm import ScmModel
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class RepoCachesView(RepoAppView):
37 37 def load_default_context(self):
38 38 c = self._get_local_tmpl_context()
39 39
40 40 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
41 41 c.repo_info = self.db_repo
42 42
43 43 self._register_global_c(c)
44 44 return c
45 45
46 46 @LoginRequired()
47 47 @HasRepoPermissionAnyDecorator('repository.admin')
48 48 @view_config(
49 49 route_name='edit_repo_caches', request_method='GET',
50 50 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
51 51 def repo_caches(self):
52 52 c = self.load_default_context()
53 53 c.active = 'caches'
54 54
55 55 return self._get_template_context(c)
56 56
57 57 @LoginRequired()
58 58 @HasRepoPermissionAnyDecorator('repository.admin')
59 59 @CSRFRequired()
60 60 @view_config(
61 61 route_name='edit_repo_caches', request_method='POST')
62 62 def repo_caches_purge(self):
63 63 _ = self.request.translate
64 64 c = self.load_default_context()
65 65 c.active = 'caches'
66 66
67 67 try:
68 68 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
69 69 Session().commit()
70 70 h.flash(_('Cache invalidation successful'),
71 71 category='success')
72 72 except Exception:
73 73 log.exception("Exception during cache invalidation")
74 74 h.flash(_('An error occurred during cache invalidation'),
75 75 category='error')
76 76
77 77 raise HTTPFound(h.route_path(
78 78 'edit_repo_caches', repo_name=self.db_repo_name)) No newline at end of file
@@ -1,325 +1,324 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25 25 from pyramid.view import view_config
26 26 from pyramid.renderers import render
27 27 from pyramid.response import Response
28 28
29
30 29 from rhodecode.apps._base import RepoAppView
31 30 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
32 31 from rhodecode.lib import helpers as h
33 32 from rhodecode.lib import diffs, codeblocks
34 33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 34 from rhodecode.lib.utils import safe_str
36 35 from rhodecode.lib.utils2 import safe_unicode, str2bool
37 36 from rhodecode.lib.vcs.exceptions import (
38 37 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
39 38 NodeDoesNotExistError)
40 39 from rhodecode.model.db import Repository, ChangesetStatus
41 40
42 41 log = logging.getLogger(__name__)
43 42
44 43
45 44 class RepoCompareView(RepoAppView):
46 45 def load_default_context(self):
47 46 c = self._get_local_tmpl_context(include_app_defaults=True)
48 47
49 48 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
50 49 c.repo_info = self.db_repo
51 50 c.rhodecode_repo = self.rhodecode_vcs_repo
52 51
53 52 self._register_global_c(c)
54 53 return c
55 54
56 55 def _get_commit_or_redirect(
57 56 self, ref, ref_type, repo, redirect_after=True, partial=False):
58 57 """
59 58 This is a safe way to get a commit. If an error occurs it
60 59 redirects to a commit with a proper message. If partial is set
61 60 then it does not do redirect raise and throws an exception instead.
62 61 """
63 62 _ = self.request.translate
64 63 try:
65 64 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
66 65 except EmptyRepositoryError:
67 66 if not redirect_after:
68 67 return repo.scm_instance().EMPTY_COMMIT
69 68 h.flash(h.literal(_('There are no commits yet')),
70 69 category='warning')
71 70 if not partial:
72 71 raise HTTPFound(
73 72 h.route_path('repo_summary', repo_name=repo.repo_name))
74 73 raise HTTPBadRequest()
75 74
76 75 except RepositoryError as e:
77 76 log.exception(safe_str(e))
78 77 h.flash(safe_str(h.escape(e)), category='warning')
79 78 if not partial:
80 79 raise HTTPFound(
81 80 h.route_path('repo_summary', repo_name=repo.repo_name))
82 81 raise HTTPBadRequest()
83 82
84 83 @LoginRequired()
85 84 @HasRepoPermissionAnyDecorator(
86 85 'repository.read', 'repository.write', 'repository.admin')
87 86 @view_config(
88 87 route_name='repo_compare_select', request_method='GET',
89 88 renderer='rhodecode:templates/compare/compare_diff.mako')
90 89 def compare_select(self):
91 90 _ = self.request.translate
92 91 c = self.load_default_context()
93 92
94 93 source_repo = self.db_repo_name
95 94 target_repo = self.request.GET.get('target_repo', source_repo)
96 95 c.source_repo = Repository.get_by_repo_name(source_repo)
97 96 c.target_repo = Repository.get_by_repo_name(target_repo)
98 97
99 98 if c.source_repo is None or c.target_repo is None:
100 99 raise HTTPNotFound()
101 100
102 101 c.compare_home = True
103 102 c.commit_ranges = []
104 103 c.collapse_all_commits = False
105 104 c.diffset = None
106 105 c.limited_diff = False
107 106 c.source_ref = c.target_ref = _('Select commit')
108 107 c.source_ref_type = ""
109 108 c.target_ref_type = ""
110 109 c.commit_statuses = ChangesetStatus.STATUSES
111 110 c.preview_mode = False
112 111 c.file_path = None
113 112
114 113 return self._get_template_context(c)
115 114
116 115 @LoginRequired()
117 116 @HasRepoPermissionAnyDecorator(
118 117 'repository.read', 'repository.write', 'repository.admin')
119 118 @view_config(
120 119 route_name='repo_compare', request_method='GET',
121 120 renderer=None)
122 121 def compare(self):
123 122 _ = self.request.translate
124 123 c = self.load_default_context()
125 124
126 125 source_ref_type = self.request.matchdict['source_ref_type']
127 126 source_ref = self.request.matchdict['source_ref']
128 127 target_ref_type = self.request.matchdict['target_ref_type']
129 128 target_ref = self.request.matchdict['target_ref']
130 129
131 130 # source_ref will be evaluated in source_repo
132 131 source_repo_name = self.db_repo_name
133 132 source_path, source_id = parse_path_ref(source_ref)
134 133
135 134 # target_ref will be evaluated in target_repo
136 135 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
137 136 target_path, target_id = parse_path_ref(
138 137 target_ref, default_path=self.request.GET.get('f_path', ''))
139 138
140 139 # if merge is True
141 140 # Show what changes since the shared ancestor commit of target/source
142 141 # the source would get if it was merged with target. Only commits
143 142 # which are in target but not in source will be shown.
144 143 merge = str2bool(self.request.GET.get('merge'))
145 144 # if merge is False
146 145 # Show a raw diff of source/target refs even if no ancestor exists
147 146
148 147 # c.fulldiff disables cut_off_limit
149 148 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
150 149
151 150 c.file_path = target_path
152 151 c.commit_statuses = ChangesetStatus.STATUSES
153 152
154 153 # if partial, returns just compare_commits.html (commits log)
155 154 partial = self.request.is_xhr
156 155
157 156 # swap url for compare_diff page
158 157 c.swap_url = h.route_path(
159 158 'repo_compare',
160 159 repo_name=target_repo_name,
161 160 source_ref_type=target_ref_type,
162 161 source_ref=target_ref,
163 162 target_repo=source_repo_name,
164 163 target_ref_type=source_ref_type,
165 164 target_ref=source_ref,
166 165 _query=dict(merge=merge and '1' or '', f_path=target_path))
167 166
168 167 source_repo = Repository.get_by_repo_name(source_repo_name)
169 168 target_repo = Repository.get_by_repo_name(target_repo_name)
170 169
171 170 if source_repo is None:
172 171 log.error('Could not find the source repo: {}'
173 172 .format(source_repo_name))
174 173 h.flash(_('Could not find the source repo: `{}`')
175 174 .format(h.escape(source_repo_name)), category='error')
176 175 raise HTTPFound(
177 176 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
178 177
179 178 if target_repo is None:
180 179 log.error('Could not find the target repo: {}'
181 180 .format(source_repo_name))
182 181 h.flash(_('Could not find the target repo: `{}`')
183 182 .format(h.escape(target_repo_name)), category='error')
184 183 raise HTTPFound(
185 184 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
186 185
187 186 source_scm = source_repo.scm_instance()
188 187 target_scm = target_repo.scm_instance()
189 188
190 189 source_alias = source_scm.alias
191 190 target_alias = target_scm.alias
192 191 if source_alias != target_alias:
193 192 msg = _('The comparison of two different kinds of remote repos '
194 193 'is not available')
195 194 log.error(msg)
196 195 h.flash(msg, category='error')
197 196 raise HTTPFound(
198 197 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
199 198
200 199 source_commit = self._get_commit_or_redirect(
201 200 ref=source_id, ref_type=source_ref_type, repo=source_repo,
202 201 partial=partial)
203 202 target_commit = self._get_commit_or_redirect(
204 203 ref=target_id, ref_type=target_ref_type, repo=target_repo,
205 204 partial=partial)
206 205
207 206 c.compare_home = False
208 207 c.source_repo = source_repo
209 208 c.target_repo = target_repo
210 209 c.source_ref = source_ref
211 210 c.target_ref = target_ref
212 211 c.source_ref_type = source_ref_type
213 212 c.target_ref_type = target_ref_type
214 213
215 214 pre_load = ["author", "branch", "date", "message"]
216 215 c.ancestor = None
217 216
218 217 if c.file_path:
219 218 if source_commit == target_commit:
220 219 c.commit_ranges = []
221 220 else:
222 221 c.commit_ranges = [target_commit]
223 222 else:
224 223 try:
225 224 c.commit_ranges = source_scm.compare(
226 225 source_commit.raw_id, target_commit.raw_id,
227 226 target_scm, merge, pre_load=pre_load)
228 227 if merge:
229 228 c.ancestor = source_scm.get_common_ancestor(
230 229 source_commit.raw_id, target_commit.raw_id, target_scm)
231 230 except RepositoryRequirementError:
232 231 msg = _('Could not compare repos with different '
233 232 'large file settings')
234 233 log.error(msg)
235 234 if partial:
236 235 return Response(msg)
237 236 h.flash(msg, category='error')
238 237 raise HTTPFound(
239 238 h.route_path('repo_compare_select',
240 239 repo_name=self.db_repo_name))
241 240
242 241 c.statuses = self.db_repo.statuses(
243 242 [x.raw_id for x in c.commit_ranges])
244 243
245 244 # auto collapse if we have more than limit
246 245 collapse_limit = diffs.DiffProcessor._collapse_commits_over
247 246 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
248 247
249 248 if partial: # for PR ajax commits loader
250 249 if not c.ancestor:
251 250 return Response('') # cannot merge if there is no ancestor
252 251
253 252 html = render(
254 253 'rhodecode:templates/compare/compare_commits.mako',
255 254 self._get_template_context(c), self.request)
256 255 return Response(html)
257 256
258 257 if c.ancestor:
259 258 # case we want a simple diff without incoming commits,
260 259 # previewing what will be merged.
261 260 # Make the diff on target repo (which is known to have target_ref)
262 261 log.debug('Using ancestor %s as source_ref instead of %s'
263 262 % (c.ancestor, source_ref))
264 263 source_repo = target_repo
265 264 source_commit = target_repo.get_commit(commit_id=c.ancestor)
266 265
267 266 # diff_limit will cut off the whole diff if the limit is applied
268 267 # otherwise it will just hide the big files from the front-end
269 268 diff_limit = c.visual.cut_off_limit_diff
270 269 file_limit = c.visual.cut_off_limit_file
271 270
272 271 log.debug('calculating diff between '
273 272 'source_ref:%s and target_ref:%s for repo `%s`',
274 273 source_commit, target_commit,
275 274 safe_unicode(source_repo.scm_instance().path))
276 275
277 276 if source_commit.repository != target_commit.repository:
278 277 msg = _(
279 278 "Repositories unrelated. "
280 279 "Cannot compare commit %(commit1)s from repository %(repo1)s "
281 280 "with commit %(commit2)s from repository %(repo2)s.") % {
282 281 'commit1': h.show_id(source_commit),
283 282 'repo1': source_repo.repo_name,
284 283 'commit2': h.show_id(target_commit),
285 284 'repo2': target_repo.repo_name,
286 285 }
287 286 h.flash(msg, category='error')
288 287 raise HTTPFound(
289 288 h.route_path('repo_compare_select',
290 289 repo_name=self.db_repo_name))
291 290
292 291 txt_diff = source_repo.scm_instance().get_diff(
293 292 commit1=source_commit, commit2=target_commit,
294 293 path=target_path, path1=source_path)
295 294
296 295 diff_processor = diffs.DiffProcessor(
297 296 txt_diff, format='newdiff', diff_limit=diff_limit,
298 297 file_limit=file_limit, show_full_diff=c.fulldiff)
299 298 _parsed = diff_processor.prepare()
300 299
301 300 def _node_getter(commit):
302 301 """ Returns a function that returns a node for a commit or None """
303 302 def get_node(fname):
304 303 try:
305 304 return commit.get_node(fname)
306 305 except NodeDoesNotExistError:
307 306 return None
308 307 return get_node
309 308
310 309 diffset = codeblocks.DiffSet(
311 310 repo_name=source_repo.repo_name,
312 311 source_node_getter=_node_getter(source_commit),
313 312 target_node_getter=_node_getter(target_commit),
314 313 )
315 314 c.diffset = diffset.render_patchset(
316 315 _parsed, source_ref, target_ref)
317 316
318 317 c.preview_mode = merge
319 318 c.source_commit = source_commit
320 319 c.target_commit = target_commit
321 320
322 321 html = render(
323 322 'rhodecode:templates/compare/compare_diff.mako',
324 323 self._get_template_context(c), self.request)
325 324 return Response(html) No newline at end of file
@@ -1,256 +1,256 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
24 import formencode.htmlfill
25
26 from pyramid.httpexceptions import HTTPFound
25 27 from pyramid.view import view_config
26 28 from pyramid.renderers import render
27 29 from pyramid.response import Response
28 30
29 31 from rhodecode.apps._base import RepoAppView, DataGridAppView
30
31 32 from rhodecode.lib.auth import (
32 33 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
33 34 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
34 35 import rhodecode.lib.helpers as h
35 from rhodecode.model.db import (
36 coalesce, or_, Repository, RepoGroup, UserFollowing, User)
36 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
37 37 from rhodecode.model.repo import RepoModel
38 38 from rhodecode.model.forms import RepoForkForm
39 39 from rhodecode.model.scm import ScmModel, RepoGroupList
40 40 from rhodecode.lib.utils2 import safe_int, safe_unicode
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class RepoForksView(RepoAppView, DataGridAppView):
46 46
47 47 def load_default_context(self):
48 48 c = self._get_local_tmpl_context(include_app_defaults=True)
49 49
50 50 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
51 51 c.repo_info = self.db_repo
52 52 c.rhodecode_repo = self.rhodecode_vcs_repo
53 53
54 54 acl_groups = RepoGroupList(
55 55 RepoGroup.query().all(),
56 56 perm_set=['group.write', 'group.admin'])
57 57 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
58 58 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
59 59 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
60 60 c.landing_revs_choices = choices
61 61 c.personal_repo_group = c.rhodecode_user.personal_repo_group
62 62
63 63 self._register_global_c(c)
64 64 return c
65 65
66 66 @LoginRequired()
67 67 @HasRepoPermissionAnyDecorator(
68 68 'repository.read', 'repository.write', 'repository.admin')
69 69 @view_config(
70 70 route_name='repo_forks_show_all', request_method='GET',
71 71 renderer='rhodecode:templates/forks/forks.mako')
72 72 def repo_forks_show_all(self):
73 73 c = self.load_default_context()
74 74 return self._get_template_context(c)
75 75
76 76 @LoginRequired()
77 77 @HasRepoPermissionAnyDecorator(
78 78 'repository.read', 'repository.write', 'repository.admin')
79 79 @view_config(
80 80 route_name='repo_forks_data', request_method='GET',
81 81 renderer='json_ext', xhr=True)
82 82 def repo_forks_data(self):
83 83 _ = self.request.translate
84 84 column_map = {
85 85 'fork_name': 'repo_name',
86 86 'fork_date': 'created_on',
87 87 'last_activity': 'updated_on'
88 88 }
89 89 draw, start, limit = self._extract_chunk(self.request)
90 90 search_q, order_by, order_dir = self._extract_ordering(
91 91 self.request, column_map=column_map)
92 92
93 93 acl_check = HasRepoPermissionAny(
94 94 'repository.read', 'repository.write', 'repository.admin')
95 95 repo_id = self.db_repo.repo_id
96 96 allowed_ids = []
97 97 for f in Repository.query().filter(Repository.fork_id == repo_id):
98 98 if acl_check(f.repo_name, 'get forks check'):
99 99 allowed_ids.append(f.repo_id)
100 100
101 101 forks_data_total_count = Repository.query()\
102 102 .filter(Repository.fork_id == repo_id)\
103 103 .filter(Repository.repo_id.in_(allowed_ids))\
104 104 .count()
105 105
106 106 # json generate
107 107 base_q = Repository.query()\
108 108 .filter(Repository.fork_id == repo_id)\
109 109 .filter(Repository.repo_id.in_(allowed_ids))\
110 110
111 111 if search_q:
112 112 like_expression = u'%{}%'.format(safe_unicode(search_q))
113 113 base_q = base_q.filter(or_(
114 114 Repository.repo_name.ilike(like_expression),
115 115 Repository.description.ilike(like_expression),
116 116 ))
117 117
118 118 forks_data_total_filtered_count = base_q.count()
119 119
120 120 sort_col = getattr(Repository, order_by, None)
121 121 if sort_col:
122 122 if order_dir == 'asc':
123 123 # handle null values properly to order by NULL last
124 124 if order_by in ['last_activity']:
125 125 sort_col = coalesce(sort_col, datetime.date.max)
126 126 sort_col = sort_col.asc()
127 127 else:
128 128 # handle null values properly to order by NULL last
129 129 if order_by in ['last_activity']:
130 130 sort_col = coalesce(sort_col, datetime.date.min)
131 131 sort_col = sort_col.desc()
132 132
133 133 base_q = base_q.order_by(sort_col)
134 134 base_q = base_q.offset(start).limit(limit)
135 135
136 136 fork_list = base_q.all()
137 137
138 138 def fork_actions(fork):
139 139 url_link = h.route_path(
140 140 'repo_compare',
141 141 repo_name=fork.repo_name,
142 142 source_ref_type=self.db_repo.landing_rev[0],
143 143 source_ref=self.db_repo.landing_rev[1],
144 144 target_ref_type=self.db_repo.landing_rev[0],
145 145 target_ref=self.db_repo.landing_rev[1],
146 146 _query=dict(merge=1, target_repo=f.repo_name))
147 147 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
148 148
149 149 def fork_name(fork):
150 150 return h.link_to(fork.repo_name,
151 151 h.route_path('repo_summary', repo_name=fork.repo_name))
152 152
153 153 forks_data = []
154 154 for fork in fork_list:
155 155 forks_data.append({
156 156 "username": h.gravatar_with_user(self.request, fork.user.username),
157 157 "fork_name": fork_name(fork),
158 158 "description": fork.description,
159 159 "fork_date": h.age_component(fork.created_on, time_is_local=True),
160 160 "last_activity": h.format_date(fork.updated_on),
161 161 "action": fork_actions(fork),
162 162 })
163 163
164 164 data = ({
165 165 'draw': draw,
166 166 'data': forks_data,
167 167 'recordsTotal': forks_data_total_count,
168 168 'recordsFiltered': forks_data_total_filtered_count,
169 169 })
170 170
171 171 return data
172 172
173 173 @LoginRequired()
174 174 @NotAnonymous()
175 175 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
176 176 @HasRepoPermissionAnyDecorator(
177 177 'repository.read', 'repository.write', 'repository.admin')
178 178 @view_config(
179 179 route_name='repo_fork_new', request_method='GET',
180 180 renderer='rhodecode:templates/forks/forks.mako')
181 181 def repo_fork_new(self):
182 182 c = self.load_default_context()
183 183
184 184 defaults = RepoModel()._get_defaults(self.db_repo_name)
185 185 # alter the description to indicate a fork
186 186 defaults['description'] = (
187 187 'fork of repository: %s \n%s' % (
188 188 defaults['repo_name'], defaults['description']))
189 189 # add suffix to fork
190 190 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
191 191
192 192 data = render('rhodecode:templates/forks/fork.mako',
193 193 self._get_template_context(c), self.request)
194 194 html = formencode.htmlfill.render(
195 195 data,
196 196 defaults=defaults,
197 197 encoding="UTF-8",
198 198 force_defaults=False
199 199 )
200 200 return Response(html)
201 201
202 202 @LoginRequired()
203 203 @NotAnonymous()
204 204 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
205 205 @HasRepoPermissionAnyDecorator(
206 206 'repository.read', 'repository.write', 'repository.admin')
207 207 @CSRFRequired()
208 208 @view_config(
209 209 route_name='repo_fork_create', request_method='POST',
210 210 renderer='rhodecode:templates/forks/fork.mako')
211 211 def repo_fork_create(self):
212 212 _ = self.request.translate
213 213 c = self.load_default_context()
214 214
215 215 _form = RepoForkForm(old_data={'repo_type': self.db_repo.repo_type},
216 216 repo_groups=c.repo_groups_choices,
217 217 landing_revs=c.landing_revs_choices)()
218 218 form_result = {}
219 219 task_id = None
220 220 try:
221 221 form_result = _form.to_python(dict(self.request.POST))
222 222 # create fork is done sometimes async on celery, db transaction
223 223 # management is handled there.
224 224 task = RepoModel().create_fork(
225 225 form_result, c.rhodecode_user.user_id)
226 226 from celery.result import BaseAsyncResult
227 227 if isinstance(task, BaseAsyncResult):
228 228 task_id = task.task_id
229 229 except formencode.Invalid as errors:
230 230 c.repo_info = self.db_repo
231 231
232 232 data = render('rhodecode:templates/forks/fork.mako',
233 233 self._get_template_context(c), self.request)
234 234 html = formencode.htmlfill.render(
235 235 data,
236 236 defaults=errors.value,
237 237 errors=errors.error_dict or {},
238 238 prefix_error=False,
239 239 encoding="UTF-8",
240 240 force_defaults=False
241 241 )
242 242 return Response(html)
243 243 except Exception:
244 244 log.exception(
245 245 u'Exception while trying to fork the repository %s',
246 246 self.db_repo_name)
247 247 msg = (
248 248 _('An error occurred during repository forking %s') % (
249 249 self.db_repo_name, ))
250 250 h.flash(msg, category='error')
251 251
252 252 repo_name = form_result.get('repo_name_full', self.db_repo_name)
253 253 raise HTTPFound(
254 254 h.route_path('repo_creating',
255 255 repo_name=repo_name,
256 256 _query=dict(task_id=task_id)))
@@ -1,1193 +1,1194 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 import formencode.htmlfill
25 26 import peppercorn
26 27 from pyramid.httpexceptions import (
27 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 29 from pyramid.view import view_config
29 30 from pyramid.renderers import render
30 31
31 32 from rhodecode import events
32 33 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 34
34 35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 36 from rhodecode.lib.base import vcs_operation_context
36 37 from rhodecode.lib.ext_json import json
37 38 from rhodecode.lib.auth import (
38 39 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
39 40 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
40 41 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
41 42 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
42 43 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
43 44 from rhodecode.model.changeset_status import ChangesetStatusModel
44 45 from rhodecode.model.comment import CommentsModel
45 46 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
46 47 ChangesetComment, ChangesetStatus, Repository)
47 48 from rhodecode.model.forms import PullRequestForm
48 49 from rhodecode.model.meta import Session
49 50 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
50 51 from rhodecode.model.scm import ScmModel
51 52
52 53 log = logging.getLogger(__name__)
53 54
54 55
55 56 class RepoPullRequestsView(RepoAppView, DataGridAppView):
56 57
57 58 def load_default_context(self):
58 59 c = self._get_local_tmpl_context(include_app_defaults=True)
59 60 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
60 61 c.repo_info = self.db_repo
61 62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 64 self._register_global_c(c)
64 65 return c
65 66
66 67 def _get_pull_requests_list(
67 68 self, repo_name, source, filter_type, opened_by, statuses):
68 69
69 70 draw, start, limit = self._extract_chunk(self.request)
70 71 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 72 _render = self.request.get_partial_renderer(
72 73 'data_table/_dt_elements.mako')
73 74
74 75 # pagination
75 76
76 77 if filter_type == 'awaiting_review':
77 78 pull_requests = PullRequestModel().get_awaiting_review(
78 79 repo_name, source=source, opened_by=opened_by,
79 80 statuses=statuses, offset=start, length=limit,
80 81 order_by=order_by, order_dir=order_dir)
81 82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 83 repo_name, source=source, statuses=statuses,
83 84 opened_by=opened_by)
84 85 elif filter_type == 'awaiting_my_review':
85 86 pull_requests = PullRequestModel().get_awaiting_my_review(
86 87 repo_name, source=source, opened_by=opened_by,
87 88 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 89 offset=start, length=limit, order_by=order_by,
89 90 order_dir=order_dir)
90 91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 93 statuses=statuses, opened_by=opened_by)
93 94 else:
94 95 pull_requests = PullRequestModel().get_all(
95 96 repo_name, source=source, opened_by=opened_by,
96 97 statuses=statuses, offset=start, length=limit,
97 98 order_by=order_by, order_dir=order_dir)
98 99 pull_requests_total_count = PullRequestModel().count_all(
99 100 repo_name, source=source, statuses=statuses,
100 101 opened_by=opened_by)
101 102
102 103 data = []
103 104 comments_model = CommentsModel()
104 105 for pr in pull_requests:
105 106 comments = comments_model.get_all_comments(
106 107 self.db_repo.repo_id, pull_request=pr)
107 108
108 109 data.append({
109 110 'name': _render('pullrequest_name',
110 111 pr.pull_request_id, pr.target_repo.repo_name),
111 112 'name_raw': pr.pull_request_id,
112 113 'status': _render('pullrequest_status',
113 114 pr.calculated_review_status()),
114 115 'title': _render(
115 116 'pullrequest_title', pr.title, pr.description),
116 117 'description': h.escape(pr.description),
117 118 'updated_on': _render('pullrequest_updated_on',
118 119 h.datetime_to_time(pr.updated_on)),
119 120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 121 'created_on': _render('pullrequest_updated_on',
121 122 h.datetime_to_time(pr.created_on)),
122 123 'created_on_raw': h.datetime_to_time(pr.created_on),
123 124 'author': _render('pullrequest_author',
124 125 pr.author.full_contact, ),
125 126 'author_raw': pr.author.full_name,
126 127 'comments': _render('pullrequest_comments', len(comments)),
127 128 'comments_raw': len(comments),
128 129 'closed': pr.is_closed(),
129 130 })
130 131
131 132 data = ({
132 133 'draw': draw,
133 134 'data': data,
134 135 'recordsTotal': pull_requests_total_count,
135 136 'recordsFiltered': pull_requests_total_count,
136 137 })
137 138 return data
138 139
139 140 @LoginRequired()
140 141 @HasRepoPermissionAnyDecorator(
141 142 'repository.read', 'repository.write', 'repository.admin')
142 143 @view_config(
143 144 route_name='pullrequest_show_all', request_method='GET',
144 145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 146 def pull_request_list(self):
146 147 c = self.load_default_context()
147 148
148 149 req_get = self.request.GET
149 150 c.source = str2bool(req_get.get('source'))
150 151 c.closed = str2bool(req_get.get('closed'))
151 152 c.my = str2bool(req_get.get('my'))
152 153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154 155
155 156 c.active = 'open'
156 157 if c.my:
157 158 c.active = 'my'
158 159 if c.closed:
159 160 c.active = 'closed'
160 161 if c.awaiting_review and not c.source:
161 162 c.active = 'awaiting'
162 163 if c.source and not c.awaiting_review:
163 164 c.active = 'source'
164 165 if c.awaiting_my_review:
165 166 c.active = 'awaiting_my'
166 167
167 168 return self._get_template_context(c)
168 169
169 170 @LoginRequired()
170 171 @HasRepoPermissionAnyDecorator(
171 172 'repository.read', 'repository.write', 'repository.admin')
172 173 @view_config(
173 174 route_name='pullrequest_show_all_data', request_method='GET',
174 175 renderer='json_ext', xhr=True)
175 176 def pull_request_list_data(self):
176 177
177 178 # additional filters
178 179 req_get = self.request.GET
179 180 source = str2bool(req_get.get('source'))
180 181 closed = str2bool(req_get.get('closed'))
181 182 my = str2bool(req_get.get('my'))
182 183 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 184 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184 185
185 186 filter_type = 'awaiting_review' if awaiting_review \
186 187 else 'awaiting_my_review' if awaiting_my_review \
187 188 else None
188 189
189 190 opened_by = None
190 191 if my:
191 192 opened_by = [self._rhodecode_user.user_id]
192 193
193 194 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 195 if closed:
195 196 statuses = [PullRequest.STATUS_CLOSED]
196 197
197 198 data = self._get_pull_requests_list(
198 199 repo_name=self.db_repo_name, source=source,
199 200 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200 201
201 202 return data
202 203
203 204 def _get_pr_version(self, pull_request_id, version=None):
204 205 at_version = None
205 206
206 207 if version and version == 'latest':
207 208 pull_request_ver = PullRequest.get(pull_request_id)
208 209 pull_request_obj = pull_request_ver
209 210 _org_pull_request_obj = pull_request_obj
210 211 at_version = 'latest'
211 212 elif version:
212 213 pull_request_ver = PullRequestVersion.get_or_404(version)
213 214 pull_request_obj = pull_request_ver
214 215 _org_pull_request_obj = pull_request_ver.pull_request
215 216 at_version = pull_request_ver.pull_request_version_id
216 217 else:
217 218 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 219 pull_request_id)
219 220
220 221 pull_request_display_obj = PullRequest.get_pr_display_object(
221 222 pull_request_obj, _org_pull_request_obj)
222 223
223 224 return _org_pull_request_obj, pull_request_obj, \
224 225 pull_request_display_obj, at_version
225 226
226 227 def _get_diffset(self, source_repo_name, source_repo,
227 228 source_ref_id, target_ref_id,
228 229 target_commit, source_commit, diff_limit, fulldiff,
229 230 file_limit, display_inline_comments):
230 231
231 232 vcs_diff = PullRequestModel().get_diff(
232 233 source_repo, source_ref_id, target_ref_id)
233 234
234 235 diff_processor = diffs.DiffProcessor(
235 236 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 237 file_limit=file_limit, show_full_diff=fulldiff)
237 238
238 239 _parsed = diff_processor.prepare()
239 240
240 241 def _node_getter(commit):
241 242 def get_node(fname):
242 243 try:
243 244 return commit.get_node(fname)
244 245 except NodeDoesNotExistError:
245 246 return None
246 247
247 248 return get_node
248 249
249 250 diffset = codeblocks.DiffSet(
250 251 repo_name=self.db_repo_name,
251 252 source_repo_name=source_repo_name,
252 253 source_node_getter=_node_getter(target_commit),
253 254 target_node_getter=_node_getter(source_commit),
254 255 comments=display_inline_comments
255 256 )
256 257 diffset = diffset.render_patchset(
257 258 _parsed, target_commit.raw_id, source_commit.raw_id)
258 259
259 260 return diffset
260 261
261 262 @LoginRequired()
262 263 @HasRepoPermissionAnyDecorator(
263 264 'repository.read', 'repository.write', 'repository.admin')
264 265 @view_config(
265 266 route_name='pullrequest_show', request_method='GET',
266 267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 268 def pull_request_show(self):
268 269 pull_request_id = self.request.matchdict['pull_request_id']
269 270
270 271 c = self.load_default_context()
271 272
272 273 version = self.request.GET.get('version')
273 274 from_version = self.request.GET.get('from_version') or version
274 275 merge_checks = self.request.GET.get('merge_checks')
275 276 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276 277
277 278 (pull_request_latest,
278 279 pull_request_at_ver,
279 280 pull_request_display_obj,
280 281 at_version) = self._get_pr_version(
281 282 pull_request_id, version=version)
282 283 pr_closed = pull_request_latest.is_closed()
283 284
284 285 if pr_closed and (version or from_version):
285 286 # not allow to browse versions
286 287 raise HTTPFound(h.route_path(
287 288 'pullrequest_show', repo_name=self.db_repo_name,
288 289 pull_request_id=pull_request_id))
289 290
290 291 versions = pull_request_display_obj.versions()
291 292
292 293 c.at_version = at_version
293 294 c.at_version_num = (at_version
294 295 if at_version and at_version != 'latest'
295 296 else None)
296 297 c.at_version_pos = ChangesetComment.get_index_from_version(
297 298 c.at_version_num, versions)
298 299
299 300 (prev_pull_request_latest,
300 301 prev_pull_request_at_ver,
301 302 prev_pull_request_display_obj,
302 303 prev_at_version) = self._get_pr_version(
303 304 pull_request_id, version=from_version)
304 305
305 306 c.from_version = prev_at_version
306 307 c.from_version_num = (prev_at_version
307 308 if prev_at_version and prev_at_version != 'latest'
308 309 else None)
309 310 c.from_version_pos = ChangesetComment.get_index_from_version(
310 311 c.from_version_num, versions)
311 312
312 313 # define if we're in COMPARE mode or VIEW at version mode
313 314 compare = at_version != prev_at_version
314 315
315 316 # pull_requests repo_name we opened it against
316 317 # ie. target_repo must match
317 318 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 319 raise HTTPNotFound()
319 320
320 321 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 322 pull_request_at_ver)
322 323
323 324 c.pull_request = pull_request_display_obj
324 325 c.pull_request_latest = pull_request_latest
325 326
326 327 if compare or (at_version and not at_version == 'latest'):
327 328 c.allowed_to_change_status = False
328 329 c.allowed_to_update = False
329 330 c.allowed_to_merge = False
330 331 c.allowed_to_delete = False
331 332 c.allowed_to_comment = False
332 333 c.allowed_to_close = False
333 334 else:
334 335 can_change_status = PullRequestModel().check_user_change_status(
335 336 pull_request_at_ver, self._rhodecode_user)
336 337 c.allowed_to_change_status = can_change_status and not pr_closed
337 338
338 339 c.allowed_to_update = PullRequestModel().check_user_update(
339 340 pull_request_latest, self._rhodecode_user) and not pr_closed
340 341 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 342 pull_request_latest, self._rhodecode_user) and not pr_closed
342 343 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 344 pull_request_latest, self._rhodecode_user) and not pr_closed
344 345 c.allowed_to_comment = not pr_closed
345 346 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346 347
347 348 c.forbid_adding_reviewers = False
348 349 c.forbid_author_to_review = False
349 350 c.forbid_commit_author_to_review = False
350 351
351 352 if pull_request_latest.reviewer_data and \
352 353 'rules' in pull_request_latest.reviewer_data:
353 354 rules = pull_request_latest.reviewer_data['rules'] or {}
354 355 try:
355 356 c.forbid_adding_reviewers = rules.get(
356 357 'forbid_adding_reviewers')
357 358 c.forbid_author_to_review = rules.get(
358 359 'forbid_author_to_review')
359 360 c.forbid_commit_author_to_review = rules.get(
360 361 'forbid_commit_author_to_review')
361 362 except Exception:
362 363 pass
363 364
364 365 # check merge capabilities
365 366 _merge_check = MergeCheck.validate(
366 367 pull_request_latest, user=self._rhodecode_user)
367 368 c.pr_merge_errors = _merge_check.error_details
368 369 c.pr_merge_possible = not _merge_check.failed
369 370 c.pr_merge_message = _merge_check.merge_msg
370 371
371 372 c.pr_merge_info = MergeCheck.get_merge_conditions(pull_request_latest)
372 373
373 374 c.pull_request_review_status = _merge_check.review_status
374 375 if merge_checks:
375 376 self.request.override_renderer = \
376 377 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
377 378 return self._get_template_context(c)
378 379
379 380 comments_model = CommentsModel()
380 381
381 382 # reviewers and statuses
382 383 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
383 384 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
384 385
385 386 # GENERAL COMMENTS with versions #
386 387 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
387 388 q = q.order_by(ChangesetComment.comment_id.asc())
388 389 general_comments = q
389 390
390 391 # pick comments we want to render at current version
391 392 c.comment_versions = comments_model.aggregate_comments(
392 393 general_comments, versions, c.at_version_num)
393 394 c.comments = c.comment_versions[c.at_version_num]['until']
394 395
395 396 # INLINE COMMENTS with versions #
396 397 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
397 398 q = q.order_by(ChangesetComment.comment_id.asc())
398 399 inline_comments = q
399 400
400 401 c.inline_versions = comments_model.aggregate_comments(
401 402 inline_comments, versions, c.at_version_num, inline=True)
402 403
403 404 # inject latest version
404 405 latest_ver = PullRequest.get_pr_display_object(
405 406 pull_request_latest, pull_request_latest)
406 407
407 408 c.versions = versions + [latest_ver]
408 409
409 410 # if we use version, then do not show later comments
410 411 # than current version
411 412 display_inline_comments = collections.defaultdict(
412 413 lambda: collections.defaultdict(list))
413 414 for co in inline_comments:
414 415 if c.at_version_num:
415 416 # pick comments that are at least UPTO given version, so we
416 417 # don't render comments for higher version
417 418 should_render = co.pull_request_version_id and \
418 419 co.pull_request_version_id <= c.at_version_num
419 420 else:
420 421 # showing all, for 'latest'
421 422 should_render = True
422 423
423 424 if should_render:
424 425 display_inline_comments[co.f_path][co.line_no].append(co)
425 426
426 427 # load diff data into template context, if we use compare mode then
427 428 # diff is calculated based on changes between versions of PR
428 429
429 430 source_repo = pull_request_at_ver.source_repo
430 431 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
431 432
432 433 target_repo = pull_request_at_ver.target_repo
433 434 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
434 435
435 436 if compare:
436 437 # in compare switch the diff base to latest commit from prev version
437 438 target_ref_id = prev_pull_request_display_obj.revisions[0]
438 439
439 440 # despite opening commits for bookmarks/branches/tags, we always
440 441 # convert this to rev to prevent changes after bookmark or branch change
441 442 c.source_ref_type = 'rev'
442 443 c.source_ref = source_ref_id
443 444
444 445 c.target_ref_type = 'rev'
445 446 c.target_ref = target_ref_id
446 447
447 448 c.source_repo = source_repo
448 449 c.target_repo = target_repo
449 450
450 451 c.commit_ranges = []
451 452 source_commit = EmptyCommit()
452 453 target_commit = EmptyCommit()
453 454 c.missing_requirements = False
454 455
455 456 source_scm = source_repo.scm_instance()
456 457 target_scm = target_repo.scm_instance()
457 458
458 459 # try first shadow repo, fallback to regular repo
459 460 try:
460 461 commits_source_repo = pull_request_latest.get_shadow_repo()
461 462 except Exception:
462 463 log.debug('Failed to get shadow repo', exc_info=True)
463 464 commits_source_repo = source_scm
464 465
465 466 c.commits_source_repo = commits_source_repo
466 467 commit_cache = {}
467 468 try:
468 469 pre_load = ["author", "branch", "date", "message"]
469 470 show_revs = pull_request_at_ver.revisions
470 471 for rev in show_revs:
471 472 comm = commits_source_repo.get_commit(
472 473 commit_id=rev, pre_load=pre_load)
473 474 c.commit_ranges.append(comm)
474 475 commit_cache[comm.raw_id] = comm
475 476
476 477 # Order here matters, we first need to get target, and then
477 478 # the source
478 479 target_commit = commits_source_repo.get_commit(
479 480 commit_id=safe_str(target_ref_id))
480 481
481 482 source_commit = commits_source_repo.get_commit(
482 483 commit_id=safe_str(source_ref_id))
483 484
484 485 except CommitDoesNotExistError:
485 486 log.warning(
486 487 'Failed to get commit from `{}` repo'.format(
487 488 commits_source_repo), exc_info=True)
488 489 except RepositoryRequirementError:
489 490 log.warning(
490 491 'Failed to get all required data from repo', exc_info=True)
491 492 c.missing_requirements = True
492 493
493 494 c.ancestor = None # set it to None, to hide it from PR view
494 495
495 496 try:
496 497 ancestor_id = source_scm.get_common_ancestor(
497 498 source_commit.raw_id, target_commit.raw_id, target_scm)
498 499 c.ancestor_commit = source_scm.get_commit(ancestor_id)
499 500 except Exception:
500 501 c.ancestor_commit = None
501 502
502 503 c.statuses = source_repo.statuses(
503 504 [x.raw_id for x in c.commit_ranges])
504 505
505 506 # auto collapse if we have more than limit
506 507 collapse_limit = diffs.DiffProcessor._collapse_commits_over
507 508 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
508 509 c.compare_mode = compare
509 510
510 511 # diff_limit is the old behavior, will cut off the whole diff
511 512 # if the limit is applied otherwise will just hide the
512 513 # big files from the front-end
513 514 diff_limit = c.visual.cut_off_limit_diff
514 515 file_limit = c.visual.cut_off_limit_file
515 516
516 517 c.missing_commits = False
517 518 if (c.missing_requirements
518 519 or isinstance(source_commit, EmptyCommit)
519 520 or source_commit == target_commit):
520 521
521 522 c.missing_commits = True
522 523 else:
523 524
524 525 c.diffset = self._get_diffset(
525 526 c.source_repo.repo_name, commits_source_repo,
526 527 source_ref_id, target_ref_id,
527 528 target_commit, source_commit,
528 529 diff_limit, c.fulldiff, file_limit, display_inline_comments)
529 530
530 531 c.limited_diff = c.diffset.limited_diff
531 532
532 533 # calculate removed files that are bound to comments
533 534 comment_deleted_files = [
534 535 fname for fname in display_inline_comments
535 536 if fname not in c.diffset.file_stats]
536 537
537 538 c.deleted_files_comments = collections.defaultdict(dict)
538 539 for fname, per_line_comments in display_inline_comments.items():
539 540 if fname in comment_deleted_files:
540 541 c.deleted_files_comments[fname]['stats'] = 0
541 542 c.deleted_files_comments[fname]['comments'] = list()
542 543 for lno, comments in per_line_comments.items():
543 544 c.deleted_files_comments[fname]['comments'].extend(
544 545 comments)
545 546
546 547 # this is a hack to properly display links, when creating PR, the
547 548 # compare view and others uses different notation, and
548 549 # compare_commits.mako renders links based on the target_repo.
549 550 # We need to swap that here to generate it properly on the html side
550 551 c.target_repo = c.source_repo
551 552
552 553 c.commit_statuses = ChangesetStatus.STATUSES
553 554
554 555 c.show_version_changes = not pr_closed
555 556 if c.show_version_changes:
556 557 cur_obj = pull_request_at_ver
557 558 prev_obj = prev_pull_request_at_ver
558 559
559 560 old_commit_ids = prev_obj.revisions
560 561 new_commit_ids = cur_obj.revisions
561 562 commit_changes = PullRequestModel()._calculate_commit_id_changes(
562 563 old_commit_ids, new_commit_ids)
563 564 c.commit_changes_summary = commit_changes
564 565
565 566 # calculate the diff for commits between versions
566 567 c.commit_changes = []
567 568 mark = lambda cs, fw: list(
568 569 h.itertools.izip_longest([], cs, fillvalue=fw))
569 570 for c_type, raw_id in mark(commit_changes.added, 'a') \
570 571 + mark(commit_changes.removed, 'r') \
571 572 + mark(commit_changes.common, 'c'):
572 573
573 574 if raw_id in commit_cache:
574 575 commit = commit_cache[raw_id]
575 576 else:
576 577 try:
577 578 commit = commits_source_repo.get_commit(raw_id)
578 579 except CommitDoesNotExistError:
579 580 # in case we fail extracting still use "dummy" commit
580 581 # for display in commit diff
581 582 commit = h.AttributeDict(
582 583 {'raw_id': raw_id,
583 584 'message': 'EMPTY or MISSING COMMIT'})
584 585 c.commit_changes.append([c_type, commit])
585 586
586 587 # current user review statuses for each version
587 588 c.review_versions = {}
588 589 if self._rhodecode_user.user_id in allowed_reviewers:
589 590 for co in general_comments:
590 591 if co.author.user_id == self._rhodecode_user.user_id:
591 592 # each comment has a status change
592 593 status = co.status_change
593 594 if status:
594 595 _ver_pr = status[0].comment.pull_request_version_id
595 596 c.review_versions[_ver_pr] = status[0]
596 597
597 598 return self._get_template_context(c)
598 599
599 600 def assure_not_empty_repo(self):
600 601 _ = self.request.translate
601 602
602 603 try:
603 604 self.db_repo.scm_instance().get_commit()
604 605 except EmptyRepositoryError:
605 606 h.flash(h.literal(_('There are no commits yet')),
606 607 category='warning')
607 608 raise HTTPFound(
608 609 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
609 610
610 611 @LoginRequired()
611 612 @NotAnonymous()
612 613 @HasRepoPermissionAnyDecorator(
613 614 'repository.read', 'repository.write', 'repository.admin')
614 615 @view_config(
615 616 route_name='pullrequest_new', request_method='GET',
616 617 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
617 618 def pull_request_new(self):
618 619 _ = self.request.translate
619 620 c = self.load_default_context()
620 621
621 622 self.assure_not_empty_repo()
622 623 source_repo = self.db_repo
623 624
624 625 commit_id = self.request.GET.get('commit')
625 626 branch_ref = self.request.GET.get('branch')
626 627 bookmark_ref = self.request.GET.get('bookmark')
627 628
628 629 try:
629 630 source_repo_data = PullRequestModel().generate_repo_data(
630 631 source_repo, commit_id=commit_id,
631 632 branch=branch_ref, bookmark=bookmark_ref)
632 633 except CommitDoesNotExistError as e:
633 634 log.exception(e)
634 635 h.flash(_('Commit does not exist'), 'error')
635 636 raise HTTPFound(
636 637 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
637 638
638 639 default_target_repo = source_repo
639 640
640 641 if source_repo.parent:
641 642 parent_vcs_obj = source_repo.parent.scm_instance()
642 643 if parent_vcs_obj and not parent_vcs_obj.is_empty():
643 644 # change default if we have a parent repo
644 645 default_target_repo = source_repo.parent
645 646
646 647 target_repo_data = PullRequestModel().generate_repo_data(
647 648 default_target_repo)
648 649
649 650 selected_source_ref = source_repo_data['refs']['selected_ref']
650 651
651 652 title_source_ref = selected_source_ref.split(':', 2)[1]
652 653 c.default_title = PullRequestModel().generate_pullrequest_title(
653 654 source=source_repo.repo_name,
654 655 source_ref=title_source_ref,
655 656 target=default_target_repo.repo_name
656 657 )
657 658
658 659 c.default_repo_data = {
659 660 'source_repo_name': source_repo.repo_name,
660 661 'source_refs_json': json.dumps(source_repo_data),
661 662 'target_repo_name': default_target_repo.repo_name,
662 663 'target_refs_json': json.dumps(target_repo_data),
663 664 }
664 665 c.default_source_ref = selected_source_ref
665 666
666 667 return self._get_template_context(c)
667 668
668 669 @LoginRequired()
669 670 @NotAnonymous()
670 671 @HasRepoPermissionAnyDecorator(
671 672 'repository.read', 'repository.write', 'repository.admin')
672 673 @view_config(
673 674 route_name='pullrequest_repo_refs', request_method='GET',
674 675 renderer='json_ext', xhr=True)
675 676 def pull_request_repo_refs(self):
676 677 target_repo_name = self.request.matchdict['target_repo_name']
677 678 repo = Repository.get_by_repo_name(target_repo_name)
678 679 if not repo:
679 680 raise HTTPNotFound()
680 681 return PullRequestModel().generate_repo_data(repo)
681 682
682 683 @LoginRequired()
683 684 @NotAnonymous()
684 685 @HasRepoPermissionAnyDecorator(
685 686 'repository.read', 'repository.write', 'repository.admin')
686 687 @view_config(
687 688 route_name='pullrequest_repo_destinations', request_method='GET',
688 689 renderer='json_ext', xhr=True)
689 690 def pull_request_repo_destinations(self):
690 691 _ = self.request.translate
691 692 filter_query = self.request.GET.get('query')
692 693
693 694 query = Repository.query() \
694 695 .order_by(func.length(Repository.repo_name)) \
695 696 .filter(
696 697 or_(Repository.repo_name == self.db_repo.repo_name,
697 698 Repository.fork_id == self.db_repo.repo_id))
698 699
699 700 if filter_query:
700 701 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
701 702 query = query.filter(
702 703 Repository.repo_name.ilike(ilike_expression))
703 704
704 705 add_parent = False
705 706 if self.db_repo.parent:
706 707 if filter_query in self.db_repo.parent.repo_name:
707 708 parent_vcs_obj = self.db_repo.parent.scm_instance()
708 709 if parent_vcs_obj and not parent_vcs_obj.is_empty():
709 710 add_parent = True
710 711
711 712 limit = 20 - 1 if add_parent else 20
712 713 all_repos = query.limit(limit).all()
713 714 if add_parent:
714 715 all_repos += [self.db_repo.parent]
715 716
716 717 repos = []
717 718 for obj in ScmModel().get_repos(all_repos):
718 719 repos.append({
719 720 'id': obj['name'],
720 721 'text': obj['name'],
721 722 'type': 'repo',
722 723 'obj': obj['dbrepo']
723 724 })
724 725
725 726 data = {
726 727 'more': False,
727 728 'results': [{
728 729 'text': _('Repositories'),
729 730 'children': repos
730 731 }] if repos else []
731 732 }
732 733 return data
733 734
734 735 @LoginRequired()
735 736 @NotAnonymous()
736 737 @HasRepoPermissionAnyDecorator(
737 738 'repository.read', 'repository.write', 'repository.admin')
738 739 @CSRFRequired()
739 740 @view_config(
740 741 route_name='pullrequest_create', request_method='POST',
741 742 renderer=None)
742 743 def pull_request_create(self):
743 744 _ = self.request.translate
744 745 self.assure_not_empty_repo()
745 746
746 747 controls = peppercorn.parse(self.request.POST.items())
747 748
748 749 try:
749 750 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
750 751 except formencode.Invalid as errors:
751 752 if errors.error_dict.get('revisions'):
752 753 msg = 'Revisions: %s' % errors.error_dict['revisions']
753 754 elif errors.error_dict.get('pullrequest_title'):
754 755 msg = _('Pull request requires a title with min. 3 chars')
755 756 else:
756 757 msg = _('Error creating pull request: {}').format(errors)
757 758 log.exception(msg)
758 759 h.flash(msg, 'error')
759 760
760 761 # would rather just go back to form ...
761 762 raise HTTPFound(
762 763 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
763 764
764 765 source_repo = _form['source_repo']
765 766 source_ref = _form['source_ref']
766 767 target_repo = _form['target_repo']
767 768 target_ref = _form['target_ref']
768 769 commit_ids = _form['revisions'][::-1]
769 770
770 771 # find the ancestor for this pr
771 772 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
772 773 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
773 774
774 775 source_scm = source_db_repo.scm_instance()
775 776 target_scm = target_db_repo.scm_instance()
776 777
777 778 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
778 779 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
779 780
780 781 ancestor = source_scm.get_common_ancestor(
781 782 source_commit.raw_id, target_commit.raw_id, target_scm)
782 783
783 784 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
784 785 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
785 786
786 787 pullrequest_title = _form['pullrequest_title']
787 788 title_source_ref = source_ref.split(':', 2)[1]
788 789 if not pullrequest_title:
789 790 pullrequest_title = PullRequestModel().generate_pullrequest_title(
790 791 source=source_repo,
791 792 source_ref=title_source_ref,
792 793 target=target_repo
793 794 )
794 795
795 796 description = _form['pullrequest_desc']
796 797
797 798 get_default_reviewers_data, validate_default_reviewers = \
798 799 PullRequestModel().get_reviewer_functions()
799 800
800 801 # recalculate reviewers logic, to make sure we can validate this
801 802 reviewer_rules = get_default_reviewers_data(
802 803 self._rhodecode_db_user, source_db_repo,
803 804 source_commit, target_db_repo, target_commit)
804 805
805 806 given_reviewers = _form['review_members']
806 807 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
807 808
808 809 try:
809 810 pull_request = PullRequestModel().create(
810 811 self._rhodecode_user.user_id, source_repo, source_ref, target_repo,
811 812 target_ref, commit_ids, reviewers, pullrequest_title,
812 813 description, reviewer_rules
813 814 )
814 815 Session().commit()
815 816 h.flash(_('Successfully opened new pull request'),
816 817 category='success')
817 818 except Exception:
818 819 msg = _('Error occurred during creation of this pull request.')
819 820 log.exception(msg)
820 821 h.flash(msg, category='error')
821 822
822 823 # copy the args back to redirect
823 824 org_query = self.request.GET.mixed()
824 825 raise HTTPFound(
825 826 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
826 827 _query=org_query))
827 828
828 829 raise HTTPFound(
829 830 h.route_path('pullrequest_show', repo_name=target_repo,
830 831 pull_request_id=pull_request.pull_request_id))
831 832
832 833 @LoginRequired()
833 834 @NotAnonymous()
834 835 @HasRepoPermissionAnyDecorator(
835 836 'repository.read', 'repository.write', 'repository.admin')
836 837 @CSRFRequired()
837 838 @view_config(
838 839 route_name='pullrequest_update', request_method='POST',
839 840 renderer='json_ext')
840 841 def pull_request_update(self):
841 842 pull_request = PullRequest.get_or_404(
842 843 self.request.matchdict['pull_request_id'])
843 844
844 845 # only owner or admin can update it
845 846 allowed_to_update = PullRequestModel().check_user_update(
846 847 pull_request, self._rhodecode_user)
847 848 if allowed_to_update:
848 849 controls = peppercorn.parse(self.request.POST.items())
849 850
850 851 if 'review_members' in controls:
851 852 self._update_reviewers(
852 853 pull_request, controls['review_members'],
853 854 pull_request.reviewer_data)
854 855 elif str2bool(self.request.POST.get('update_commits', 'false')):
855 856 self._update_commits(pull_request)
856 857 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
857 858 self._edit_pull_request(pull_request)
858 859 else:
859 860 raise HTTPBadRequest()
860 861 return True
861 862 raise HTTPForbidden()
862 863
863 864 def _edit_pull_request(self, pull_request):
864 865 _ = self.request.translate
865 866 try:
866 867 PullRequestModel().edit(
867 868 pull_request, self.request.POST.get('title'),
868 869 self.request.POST.get('description'), self._rhodecode_user)
869 870 except ValueError:
870 871 msg = _(u'Cannot update closed pull requests.')
871 872 h.flash(msg, category='error')
872 873 return
873 874 else:
874 875 Session().commit()
875 876
876 877 msg = _(u'Pull request title & description updated.')
877 878 h.flash(msg, category='success')
878 879 return
879 880
880 881 def _update_commits(self, pull_request):
881 882 _ = self.request.translate
882 883 resp = PullRequestModel().update_commits(pull_request)
883 884
884 885 if resp.executed:
885 886
886 887 if resp.target_changed and resp.source_changed:
887 888 changed = 'target and source repositories'
888 889 elif resp.target_changed and not resp.source_changed:
889 890 changed = 'target repository'
890 891 elif not resp.target_changed and resp.source_changed:
891 892 changed = 'source repository'
892 893 else:
893 894 changed = 'nothing'
894 895
895 896 msg = _(
896 897 u'Pull request updated to "{source_commit_id}" with '
897 898 u'{count_added} added, {count_removed} removed commits. '
898 899 u'Source of changes: {change_source}')
899 900 msg = msg.format(
900 901 source_commit_id=pull_request.source_ref_parts.commit_id,
901 902 count_added=len(resp.changes.added),
902 903 count_removed=len(resp.changes.removed),
903 904 change_source=changed)
904 905 h.flash(msg, category='success')
905 906
906 907 channel = '/repo${}$/pr/{}'.format(
907 908 pull_request.target_repo.repo_name,
908 909 pull_request.pull_request_id)
909 910 message = msg + (
910 911 ' - <a onclick="window.location.reload()">'
911 912 '<strong>{}</strong></a>'.format(_('Reload page')))
912 913 channelstream.post_message(
913 914 channel, message, self._rhodecode_user.username,
914 915 registry=self.request.registry)
915 916 else:
916 917 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
917 918 warning_reasons = [
918 919 UpdateFailureReason.NO_CHANGE,
919 920 UpdateFailureReason.WRONG_REF_TYPE,
920 921 ]
921 922 category = 'warning' if resp.reason in warning_reasons else 'error'
922 923 h.flash(msg, category=category)
923 924
924 925 @LoginRequired()
925 926 @NotAnonymous()
926 927 @HasRepoPermissionAnyDecorator(
927 928 'repository.read', 'repository.write', 'repository.admin')
928 929 @CSRFRequired()
929 930 @view_config(
930 931 route_name='pullrequest_merge', request_method='POST',
931 932 renderer='json_ext')
932 933 def pull_request_merge(self):
933 934 """
934 935 Merge will perform a server-side merge of the specified
935 936 pull request, if the pull request is approved and mergeable.
936 937 After successful merging, the pull request is automatically
937 938 closed, with a relevant comment.
938 939 """
939 940 pull_request = PullRequest.get_or_404(
940 941 self.request.matchdict['pull_request_id'])
941 942
942 943 check = MergeCheck.validate(pull_request, self._rhodecode_db_user)
943 944 merge_possible = not check.failed
944 945
945 946 for err_type, error_msg in check.errors:
946 947 h.flash(error_msg, category=err_type)
947 948
948 949 if merge_possible:
949 950 log.debug("Pre-conditions checked, trying to merge.")
950 951 extras = vcs_operation_context(
951 952 self.request.environ, repo_name=pull_request.target_repo.repo_name,
952 953 username=self._rhodecode_db_user.username, action='push',
953 954 scm=pull_request.target_repo.repo_type)
954 955 self._merge_pull_request(
955 956 pull_request, self._rhodecode_db_user, extras)
956 957 else:
957 958 log.debug("Pre-conditions failed, NOT merging.")
958 959
959 960 raise HTTPFound(
960 961 h.route_path('pullrequest_show',
961 962 repo_name=pull_request.target_repo.repo_name,
962 963 pull_request_id=pull_request.pull_request_id))
963 964
964 965 def _merge_pull_request(self, pull_request, user, extras):
965 966 _ = self.request.translate
966 967 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
967 968
968 969 if merge_resp.executed:
969 970 log.debug("The merge was successful, closing the pull request.")
970 971 PullRequestModel().close_pull_request(
971 972 pull_request.pull_request_id, user)
972 973 Session().commit()
973 974 msg = _('Pull request was successfully merged and closed.')
974 975 h.flash(msg, category='success')
975 976 else:
976 977 log.debug(
977 978 "The merge was not successful. Merge response: %s",
978 979 merge_resp)
979 980 msg = PullRequestModel().merge_status_message(
980 981 merge_resp.failure_reason)
981 982 h.flash(msg, category='error')
982 983
983 984 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
984 985 _ = self.request.translate
985 986 get_default_reviewers_data, validate_default_reviewers = \
986 987 PullRequestModel().get_reviewer_functions()
987 988
988 989 try:
989 990 reviewers = validate_default_reviewers(review_members, reviewer_rules)
990 991 except ValueError as e:
991 992 log.error('Reviewers Validation: {}'.format(e))
992 993 h.flash(e, category='error')
993 994 return
994 995
995 996 PullRequestModel().update_reviewers(
996 997 pull_request, reviewers, self._rhodecode_user)
997 998 h.flash(_('Pull request reviewers updated.'), category='success')
998 999 Session().commit()
999 1000
1000 1001 @LoginRequired()
1001 1002 @NotAnonymous()
1002 1003 @HasRepoPermissionAnyDecorator(
1003 1004 'repository.read', 'repository.write', 'repository.admin')
1004 1005 @CSRFRequired()
1005 1006 @view_config(
1006 1007 route_name='pullrequest_delete', request_method='POST',
1007 1008 renderer='json_ext')
1008 1009 def pull_request_delete(self):
1009 1010 _ = self.request.translate
1010 1011
1011 1012 pull_request = PullRequest.get_or_404(
1012 1013 self.request.matchdict['pull_request_id'])
1013 1014
1014 1015 pr_closed = pull_request.is_closed()
1015 1016 allowed_to_delete = PullRequestModel().check_user_delete(
1016 1017 pull_request, self._rhodecode_user) and not pr_closed
1017 1018
1018 1019 # only owner can delete it !
1019 1020 if allowed_to_delete:
1020 1021 PullRequestModel().delete(pull_request, self._rhodecode_user)
1021 1022 Session().commit()
1022 1023 h.flash(_('Successfully deleted pull request'),
1023 1024 category='success')
1024 1025 raise HTTPFound(h.route_path('pullrequest_show_all',
1025 1026 repo_name=self.db_repo_name))
1026 1027
1027 1028 log.warning('user %s tried to delete pull request without access',
1028 1029 self._rhodecode_user)
1029 1030 raise HTTPNotFound()
1030 1031
1031 1032 @LoginRequired()
1032 1033 @NotAnonymous()
1033 1034 @HasRepoPermissionAnyDecorator(
1034 1035 'repository.read', 'repository.write', 'repository.admin')
1035 1036 @CSRFRequired()
1036 1037 @view_config(
1037 1038 route_name='pullrequest_comment_create', request_method='POST',
1038 1039 renderer='json_ext')
1039 1040 def pull_request_comment_create(self):
1040 1041 _ = self.request.translate
1041 1042
1042 1043 pull_request = PullRequest.get_or_404(
1043 1044 self.request.matchdict['pull_request_id'])
1044 1045 pull_request_id = pull_request.pull_request_id
1045 1046
1046 1047 if pull_request.is_closed():
1047 1048 log.debug('comment: forbidden because pull request is closed')
1048 1049 raise HTTPForbidden()
1049 1050
1050 1051 c = self.load_default_context()
1051 1052
1052 1053 status = self.request.POST.get('changeset_status', None)
1053 1054 text = self.request.POST.get('text')
1054 1055 comment_type = self.request.POST.get('comment_type')
1055 1056 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1056 1057 close_pull_request = self.request.POST.get('close_pull_request')
1057 1058
1058 1059 # the logic here should work like following, if we submit close
1059 1060 # pr comment, use `close_pull_request_with_comment` function
1060 1061 # else handle regular comment logic
1061 1062
1062 1063 if close_pull_request:
1063 1064 # only owner or admin or person with write permissions
1064 1065 allowed_to_close = PullRequestModel().check_user_update(
1065 1066 pull_request, self._rhodecode_user)
1066 1067 if not allowed_to_close:
1067 1068 log.debug('comment: forbidden because not allowed to close '
1068 1069 'pull request %s', pull_request_id)
1069 1070 raise HTTPForbidden()
1070 1071 comment, status = PullRequestModel().close_pull_request_with_comment(
1071 1072 pull_request, self._rhodecode_user, self.db_repo, message=text)
1072 1073 Session().flush()
1073 1074 events.trigger(
1074 1075 events.PullRequestCommentEvent(pull_request, comment))
1075 1076
1076 1077 else:
1077 1078 # regular comment case, could be inline, or one with status.
1078 1079 # for that one we check also permissions
1079 1080
1080 1081 allowed_to_change_status = PullRequestModel().check_user_change_status(
1081 1082 pull_request, self._rhodecode_user)
1082 1083
1083 1084 if status and allowed_to_change_status:
1084 1085 message = (_('Status change %(transition_icon)s %(status)s')
1085 1086 % {'transition_icon': '>',
1086 1087 'status': ChangesetStatus.get_status_lbl(status)})
1087 1088 text = text or message
1088 1089
1089 1090 comment = CommentsModel().create(
1090 1091 text=text,
1091 1092 repo=self.db_repo.repo_id,
1092 1093 user=self._rhodecode_user.user_id,
1093 1094 pull_request=pull_request,
1094 1095 f_path=self.request.POST.get('f_path'),
1095 1096 line_no=self.request.POST.get('line'),
1096 1097 status_change=(ChangesetStatus.get_status_lbl(status)
1097 1098 if status and allowed_to_change_status else None),
1098 1099 status_change_type=(status
1099 1100 if status and allowed_to_change_status else None),
1100 1101 comment_type=comment_type,
1101 1102 resolves_comment_id=resolves_comment_id
1102 1103 )
1103 1104
1104 1105 if allowed_to_change_status:
1105 1106 # calculate old status before we change it
1106 1107 old_calculated_status = pull_request.calculated_review_status()
1107 1108
1108 1109 # get status if set !
1109 1110 if status:
1110 1111 ChangesetStatusModel().set_status(
1111 1112 self.db_repo.repo_id,
1112 1113 status,
1113 1114 self._rhodecode_user.user_id,
1114 1115 comment,
1115 1116 pull_request=pull_request
1116 1117 )
1117 1118
1118 1119 Session().flush()
1119 1120 events.trigger(
1120 1121 events.PullRequestCommentEvent(pull_request, comment))
1121 1122
1122 1123 # we now calculate the status of pull request, and based on that
1123 1124 # calculation we set the commits status
1124 1125 calculated_status = pull_request.calculated_review_status()
1125 1126 if old_calculated_status != calculated_status:
1126 1127 PullRequestModel()._trigger_pull_request_hook(
1127 1128 pull_request, self._rhodecode_user, 'review_status_change')
1128 1129
1129 1130 Session().commit()
1130 1131
1131 1132 data = {
1132 1133 'target_id': h.safeid(h.safe_unicode(
1133 1134 self.request.POST.get('f_path'))),
1134 1135 }
1135 1136 if comment:
1136 1137 c.co = comment
1137 1138 rendered_comment = render(
1138 1139 'rhodecode:templates/changeset/changeset_comment_block.mako',
1139 1140 self._get_template_context(c), self.request)
1140 1141
1141 1142 data.update(comment.get_dict())
1142 1143 data.update({'rendered_text': rendered_comment})
1143 1144
1144 1145 return data
1145 1146
1146 1147 @LoginRequired()
1147 1148 @NotAnonymous()
1148 1149 @HasRepoPermissionAnyDecorator(
1149 1150 'repository.read', 'repository.write', 'repository.admin')
1150 1151 @CSRFRequired()
1151 1152 @view_config(
1152 1153 route_name='pullrequest_comment_delete', request_method='POST',
1153 1154 renderer='json_ext')
1154 1155 def pull_request_comment_delete(self):
1155 1156 pull_request = PullRequest.get_or_404(
1156 1157 self.request.matchdict['pull_request_id'])
1157 1158
1158 1159 comment = ChangesetComment.get_or_404(
1159 1160 self.request.matchdict['comment_id'])
1160 1161 comment_id = comment.comment_id
1161 1162
1162 1163 if pull_request.is_closed():
1163 1164 log.debug('comment: forbidden because pull request is closed')
1164 1165 raise HTTPForbidden()
1165 1166
1166 1167 if not comment:
1167 1168 log.debug('Comment with id:%s not found, skipping', comment_id)
1168 1169 # comment already deleted in another call probably
1169 1170 return True
1170 1171
1171 1172 if comment.pull_request.is_closed():
1172 1173 # don't allow deleting comments on closed pull request
1173 1174 raise HTTPForbidden()
1174 1175
1175 1176 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1176 1177 super_admin = h.HasPermissionAny('hg.admin')()
1177 1178 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1178 1179 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1179 1180 comment_repo_admin = is_repo_admin and is_repo_comment
1180 1181
1181 1182 if super_admin or comment_owner or comment_repo_admin:
1182 1183 old_calculated_status = comment.pull_request.calculated_review_status()
1183 1184 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1184 1185 Session().commit()
1185 1186 calculated_status = comment.pull_request.calculated_review_status()
1186 1187 if old_calculated_status != calculated_status:
1187 1188 PullRequestModel()._trigger_pull_request_hook(
1188 1189 comment.pull_request, self._rhodecode_user, 'review_status_change')
1189 1190 return True
1190 1191 else:
1191 1192 log.warning('No permissions for user %s to delete comment_id: %s',
1192 1193 self._rhodecode_db_user, comment_id)
1193 1194 raise HTTPNotFound()
@@ -1,113 +1,114 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import formencode
24 import formencode.htmlfill
24 25
25 26 from pyramid.httpexceptions import HTTPFound
26 27 from pyramid.view import view_config
27 28
28 29 from rhodecode.apps._base import RepoAppView
29 30 from rhodecode.lib import audit_logger
30 31 from rhodecode.lib import helpers as h
31 32 from rhodecode.lib.auth import (
32 33 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
33 34 from rhodecode.model.db import RepositoryField
34 35 from rhodecode.model.forms import RepoFieldForm
35 36 from rhodecode.model.meta import Session
36 37 from rhodecode.model.repo import RepoModel
37 38
38 39 log = logging.getLogger(__name__)
39 40
40 41
41 42 class RepoSettingsFieldsView(RepoAppView):
42 43 def load_default_context(self):
43 44 c = self._get_local_tmpl_context()
44 45
45 46 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
46 47 c.repo_info = self.db_repo
47 48
48 49 self._register_global_c(c)
49 50 return c
50 51
51 52 @LoginRequired()
52 53 @HasRepoPermissionAnyDecorator('repository.admin')
53 54 @view_config(
54 55 route_name='edit_repo_fields', request_method='GET',
55 56 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
56 57 def repo_field_edit(self):
57 58 c = self.load_default_context()
58 59
59 60 c.active = 'fields'
60 61 c.repo_fields = RepositoryField.query() \
61 62 .filter(RepositoryField.repository == self.db_repo).all()
62 63
63 64 return self._get_template_context(c)
64 65
65 66 @LoginRequired()
66 67 @HasRepoPermissionAnyDecorator('repository.admin')
67 68 @CSRFRequired()
68 69 @view_config(
69 70 route_name='edit_repo_fields_create', request_method='POST',
70 71 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
71 72 def repo_field_create(self):
72 73 _ = self.request.translate
73 74
74 75 try:
75 76 form_result = RepoFieldForm()().to_python(dict(self.request.POST))
76 77 RepoModel().add_repo_field(
77 78 self.db_repo_name,
78 79 form_result['new_field_key'],
79 80 field_type=form_result['new_field_type'],
80 81 field_value=form_result['new_field_value'],
81 82 field_label=form_result['new_field_label'],
82 83 field_desc=form_result['new_field_desc'])
83 84
84 85 Session().commit()
85 86 except Exception as e:
86 87 log.exception("Exception creating field")
87 88 msg = _('An error occurred during creation of field')
88 89 if isinstance(e, formencode.Invalid):
89 90 msg += ". " + e.msg
90 91 h.flash(msg, category='error')
91 92
92 93 raise HTTPFound(
93 94 h.route_path('edit_repo_fields', repo_name=self.db_repo_name))
94 95
95 96 @LoginRequired()
96 97 @HasRepoPermissionAnyDecorator('repository.admin')
97 98 @CSRFRequired()
98 99 @view_config(
99 100 route_name='edit_repo_fields_delete', request_method='POST',
100 101 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
101 102 def repo_field_delete(self):
102 103 _ = self.request.translate
103 104 field = RepositoryField.get_or_404(self.request.matchdict['field_id'])
104 105 try:
105 106 RepoModel().delete_repo_field(self.db_repo_name, field.field_key)
106 107 Session().commit()
107 108 except Exception:
108 109 log.exception('Exception during removal of field')
109 110 msg = _('An error occurred during removal of field')
110 111 h.flash(msg, category='error')
111 112
112 113 raise HTTPFound(
113 114 h.route_path('edit_repo_fields', repo_name=self.db_repo_name))
@@ -1,172 +1,173 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import formencode
24 import formencode.htmlfill
24 25 from pyramid.httpexceptions import HTTPFound, HTTPBadRequest
25 26 from pyramid.response import Response
26 27 from pyramid.renderers import render
27 28 from pyramid.view import view_config
28 29
29 30 from rhodecode.apps._base import RepoAppView
30 31 from rhodecode.lib import audit_logger
31 32 from rhodecode.lib import helpers as h
32 33 from rhodecode.lib.auth import (
33 34 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
34 35 from rhodecode.model.forms import RepoVcsSettingsForm
35 36 from rhodecode.model.meta import Session
36 37 from rhodecode.model.settings import VcsSettingsModel, SettingNotFound
37 38
38 39 log = logging.getLogger(__name__)
39 40
40 41
41 42 class RepoSettingsVcsView(RepoAppView):
42 43 def load_default_context(self):
43 44 c = self._get_local_tmpl_context()
44 45
45 46 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
46 47 c.repo_info = self.db_repo
47 48
48 49 self._register_global_c(c)
49 50 return c
50 51
51 52 def _vcs_form_defaults(self, repo_name):
52 53 model = VcsSettingsModel(repo=repo_name)
53 54 global_defaults = model.get_global_settings()
54 55
55 56 repo_defaults = {}
56 57 repo_defaults.update(global_defaults)
57 58 repo_defaults.update(model.get_repo_settings())
58 59
59 60 global_defaults = {
60 61 '{}_inherited'.format(k): global_defaults[k]
61 62 for k in global_defaults}
62 63
63 64 defaults = {
64 65 'inherit_global_settings': model.inherit_global_settings
65 66 }
66 67 defaults.update(global_defaults)
67 68 defaults.update(repo_defaults)
68 69 defaults.update({
69 70 'new_svn_branch': '',
70 71 'new_svn_tag': '',
71 72 })
72 73 return defaults
73 74
74 75 @LoginRequired()
75 76 @HasRepoPermissionAnyDecorator('repository.admin')
76 77 @view_config(
77 78 route_name='edit_repo_vcs', request_method='GET',
78 79 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
79 80 def repo_vcs_settings(self):
80 81 c = self.load_default_context()
81 82 model = VcsSettingsModel(repo=self.db_repo_name)
82 83
83 84 c.active = 'vcs'
84 85 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
85 86 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
86 87 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
87 88 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
88 89
89 90 defaults = self._vcs_form_defaults(self.db_repo_name)
90 91 c.inherit_global_settings = defaults['inherit_global_settings']
91 92
92 93 data = render('rhodecode:templates/admin/repos/repo_edit.mako',
93 94 self._get_template_context(c), self.request)
94 95 html = formencode.htmlfill.render(
95 96 data,
96 97 defaults=defaults,
97 98 encoding="UTF-8",
98 99 force_defaults=False
99 100 )
100 101 return Response(html)
101 102
102 103 @LoginRequired()
103 104 @HasRepoPermissionAnyDecorator('repository.admin')
104 105 @CSRFRequired()
105 106 @view_config(
106 107 route_name='edit_repo_vcs_update', request_method='POST',
107 108 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
108 109 def repo_settings_vcs_update(self):
109 110 _ = self.request.translate
110 111 c = self.load_default_context()
111 112 c.active = 'vcs'
112 113
113 114 model = VcsSettingsModel(repo=self.db_repo_name)
114 115 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
115 116 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
116 117 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
117 118 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
118 119
119 120 defaults = self._vcs_form_defaults(self.db_repo_name)
120 121 c.inherit_global_settings = defaults['inherit_global_settings']
121 122
122 123 application_form = RepoVcsSettingsForm(self.db_repo_name)()
123 124 try:
124 125 form_result = application_form.to_python(dict(self.request.POST))
125 126 except formencode.Invalid as errors:
126 127 h.flash(_("Some form inputs contain invalid data."),
127 128 category='error')
128 129
129 130 data = render('rhodecode:templates/admin/repos/repo_edit.mako',
130 131 self._get_template_context(c), self.request)
131 132 html = formencode.htmlfill.render(
132 133 data,
133 134 defaults=errors.value,
134 135 errors=errors.error_dict or {},
135 136 encoding="UTF-8",
136 137 force_defaults=False
137 138 )
138 139 return Response(html)
139 140
140 141 try:
141 142 inherit_global_settings = form_result['inherit_global_settings']
142 143 model.create_or_update_repo_settings(
143 144 form_result, inherit_global_settings=inherit_global_settings)
144 145 Session().commit()
145 146 h.flash(_('Updated VCS settings'), category='success')
146 147 except Exception:
147 148 log.exception("Exception while updating settings")
148 149 h.flash(
149 150 _('Error occurred during updating repository VCS settings'),
150 151 category='error')
151 152
152 153 raise HTTPFound(
153 154 h.route_path('edit_repo_vcs', repo_name=self.db_repo_name))
154 155
155 156 @LoginRequired()
156 157 @HasRepoPermissionAnyDecorator('repository.admin')
157 158 @CSRFRequired()
158 159 @view_config(
159 160 route_name='edit_repo_vcs_svn_pattern_delete', request_method='POST',
160 161 renderer='json_ext', xhr=True)
161 162 def repo_settings_delete_svn_pattern(self):
162 163 self.load_default_context()
163 164 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
164 165 model = VcsSettingsModel(repo=self.db_repo_name)
165 166 try:
166 167 model.delete_repo_svn_pattern(delete_pattern_id)
167 168 except SettingNotFound:
168 169 log.exception('Failed to delete SVN pattern')
169 170 raise HTTPBadRequest()
170 171
171 172 Session().commit()
172 173 return True
@@ -1,134 +1,134 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import urllib
23 23 from pyramid.view import view_config
24 24 from webhelpers.util import update_params
25 25
26 26 from rhodecode.apps._base import BaseAppView, RepoAppView
27 27 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
28 28 from rhodecode.lib.helpers import Page
29 from rhodecode.lib.utils2 import safe_str, safe_int
29 from rhodecode.lib.utils2 import safe_str
30 30 from rhodecode.lib.index import searcher_from_config
31 31 from rhodecode.model import validation_schema
32 32 from rhodecode.model.validation_schema.schemas import search_schema
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 def search(request, tmpl_context, repo_name):
38 38 searcher = searcher_from_config(request.registry.settings)
39 39 formatted_results = []
40 40 execution_time = ''
41 41
42 42 schema = search_schema.SearchParamsSchema()
43 43
44 44 search_params = {}
45 45 errors = []
46 46 try:
47 47 search_params = schema.deserialize(
48 48 dict(search_query=request.GET.get('q'),
49 49 search_type=request.GET.get('type'),
50 50 search_sort=request.GET.get('sort'),
51 51 page_limit=request.GET.get('page_limit'),
52 52 requested_page=request.GET.get('page'))
53 53 )
54 54 except validation_schema.Invalid as e:
55 55 errors = e.children
56 56
57 57 def url_generator(**kw):
58 58 q = urllib.quote(safe_str(search_query))
59 59 return update_params(
60 60 "?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
61 61
62 62 c = tmpl_context
63 63 search_query = search_params.get('search_query')
64 64 search_type = search_params.get('search_type')
65 65 search_sort = search_params.get('search_sort')
66 66 if search_params.get('search_query'):
67 67 page_limit = search_params['page_limit']
68 68 requested_page = search_params['requested_page']
69 69
70 70 try:
71 71 search_result = searcher.search(
72 72 search_query, search_type, c.auth_user, repo_name,
73 73 requested_page, page_limit, search_sort)
74 74
75 75 formatted_results = Page(
76 76 search_result['results'], page=requested_page,
77 77 item_count=search_result['count'],
78 78 items_per_page=page_limit, url=url_generator)
79 79 finally:
80 80 searcher.cleanup()
81 81
82 82 if not search_result['error']:
83 83 execution_time = '%s results (%.3f seconds)' % (
84 84 search_result['count'],
85 85 search_result['runtime'])
86 86 elif not errors:
87 87 node = schema['search_query']
88 88 errors = [
89 89 validation_schema.Invalid(node, search_result['error'])]
90 90
91 91 c.perm_user = c.auth_user
92 92 c.repo_name = repo_name
93 93 c.sort = search_sort
94 94 c.url_generator = url_generator
95 95 c.errors = errors
96 96 c.formatted_results = formatted_results
97 97 c.runtime = execution_time
98 98 c.cur_query = search_query
99 99 c.search_type = search_type
100 100 c.searcher = searcher
101 101
102 102
103 103 class SearchView(BaseAppView):
104 104 def load_default_context(self):
105 105 c = self._get_local_tmpl_context()
106 106 self._register_global_c(c)
107 107 return c
108 108
109 109 @LoginRequired()
110 110 @view_config(
111 111 route_name='search', request_method='GET',
112 112 renderer='rhodecode:templates/search/search.mako')
113 113 def search(self):
114 114 c = self.load_default_context()
115 115 search(self.request, c, repo_name=None)
116 116 return self._get_template_context(c)
117 117
118 118
119 119 class SearchRepoView(RepoAppView):
120 120 def load_default_context(self):
121 121 c = self._get_local_tmpl_context()
122 122 self._register_global_c(c)
123 123 return c
124 124
125 125 @LoginRequired()
126 126 @HasRepoPermissionAnyDecorator(
127 127 'repository.read', 'repository.write', 'repository.admin')
128 128 @view_config(
129 129 route_name='search_repo', request_method='GET',
130 130 renderer='rhodecode:templates/search/search.mako')
131 131 def search_repo(self):
132 132 c = self.load_default_context()
133 133 search(self.request, c, repo_name=self.db_repo_name)
134 134 return self._get_template_context(c)
General Comments 0
You need to be logged in to leave comments. Login now