##// END OF EJS Templates
views: fixed some view names for better usage in view whitelist access
marcink -
r1944:5ee1b12e default
parent child Browse files
Show More
@@ -1,412 +1,412 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 peppercorn
26 26
27 27 from pyramid.httpexceptions import HTTPNotFound, HTTPForbidden, HTTPFound
28 28 from pyramid.view import view_config
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31
32 32 from rhodecode.apps._base import BaseAppView
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
35 35 from rhodecode.lib.utils2 import time_to_datetime
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
38 38 from rhodecode.model.gist import GistModel
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode.model.db import Gist, User, or_
41 41 from rhodecode.model import validation_schema
42 42 from rhodecode.model.validation_schema.schemas import gist_schema
43 43
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class GistView(BaseAppView):
49 49
50 50 def load_default_context(self):
51 51 _ = self.request.translate
52 52 c = self._get_local_tmpl_context()
53 53 c.user = c.auth_user.get_instance()
54 54
55 55 c.lifetime_values = [
56 56 (-1, _('forever')),
57 57 (5, _('5 minutes')),
58 58 (60, _('1 hour')),
59 59 (60 * 24, _('1 day')),
60 60 (60 * 24 * 30, _('1 month')),
61 61 ]
62 62
63 63 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
64 64 c.acl_options = [
65 65 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
66 66 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
67 67 ]
68 68
69 69 self._register_global_c(c)
70 70 return c
71 71
72 72 @LoginRequired()
73 73 @view_config(
74 74 route_name='gists_show', request_method='GET',
75 75 renderer='rhodecode:templates/admin/gists/index.mako')
76 76 def gist_show_all(self):
77 77 c = self.load_default_context()
78 78
79 79 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
80 80 c.show_private = self.request.GET.get('private') and not_default_user
81 81 c.show_public = self.request.GET.get('public') and not_default_user
82 82 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
83 83
84 84 gists = _gists = Gist().query()\
85 85 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
86 86 .order_by(Gist.created_on.desc())
87 87
88 88 c.active = 'public'
89 89 # MY private
90 90 if c.show_private and not c.show_public:
91 91 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
92 92 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
93 93 c.active = 'my_private'
94 94 # MY public
95 95 elif c.show_public and not c.show_private:
96 96 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
97 97 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
98 98 c.active = 'my_public'
99 99 # MY public+private
100 100 elif c.show_private and c.show_public:
101 101 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
102 102 Gist.gist_type == Gist.GIST_PRIVATE))\
103 103 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
104 104 c.active = 'my_all'
105 105 # Show all by super-admin
106 106 elif c.show_all:
107 107 c.active = 'all'
108 108 gists = _gists
109 109
110 110 # default show ALL public gists
111 111 if not c.show_public and not c.show_private and not c.show_all:
112 112 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
113 113 c.active = 'public'
114 114
115 115 _render = self.request.get_partial_renderer(
116 116 'data_table/_dt_elements.mako')
117 117
118 118 data = []
119 119
120 120 for gist in gists:
121 121 data.append({
122 122 'created_on': _render('gist_created', gist.created_on),
123 123 'created_on_raw': gist.created_on,
124 124 'type': _render('gist_type', gist.gist_type),
125 125 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
126 126 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
127 127 'author_raw': h.escape(gist.owner.full_contact),
128 128 'expires': _render('gist_expires', gist.gist_expires),
129 129 'description': _render('gist_description', gist.gist_description)
130 130 })
131 131 c.data = json.dumps(data)
132 132
133 133 return self._get_template_context(c)
134 134
135 135 @LoginRequired()
136 136 @NotAnonymous()
137 137 @view_config(
138 138 route_name='gists_new', request_method='GET',
139 139 renderer='rhodecode:templates/admin/gists/new.mako')
140 140 def gist_new(self):
141 141 c = self.load_default_context()
142 142 return self._get_template_context(c)
143 143
144 144 @LoginRequired()
145 145 @NotAnonymous()
146 146 @CSRFRequired()
147 147 @view_config(
148 148 route_name='gists_create', request_method='POST',
149 149 renderer='rhodecode:templates/admin/gists/new.mako')
150 150 def gist_create(self):
151 151 _ = self.request.translate
152 152 c = self.load_default_context()
153 153
154 154 data = dict(self.request.POST)
155 155 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
156 156 data['nodes'] = [{
157 157 'filename': data['filename'],
158 158 'content': data.get('content'),
159 159 'mimetype': data.get('mimetype') # None is autodetect
160 160 }]
161 161
162 162 data['gist_type'] = (
163 163 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
164 164 data['gist_acl_level'] = (
165 165 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
166 166
167 167 schema = gist_schema.GistSchema().bind(
168 168 lifetime_options=[x[0] for x in c.lifetime_values])
169 169
170 170 try:
171 171
172 172 schema_data = schema.deserialize(data)
173 173 # convert to safer format with just KEYs so we sure no duplicates
174 174 schema_data['nodes'] = gist_schema.sequence_to_nodes(
175 175 schema_data['nodes'])
176 176
177 177 gist = GistModel().create(
178 178 gist_id=schema_data['gistid'], # custom access id not real ID
179 179 description=schema_data['description'],
180 180 owner=self._rhodecode_user.user_id,
181 181 gist_mapping=schema_data['nodes'],
182 182 gist_type=schema_data['gist_type'],
183 183 lifetime=schema_data['lifetime'],
184 184 gist_acl_level=schema_data['gist_acl_level']
185 185 )
186 186 Session().commit()
187 187 new_gist_id = gist.gist_access_id
188 188 except validation_schema.Invalid as errors:
189 189 defaults = data
190 190 errors = errors.asdict()
191 191
192 192 if 'nodes.0.content' in errors:
193 193 errors['content'] = errors['nodes.0.content']
194 194 del errors['nodes.0.content']
195 195 if 'nodes.0.filename' in errors:
196 196 errors['filename'] = errors['nodes.0.filename']
197 197 del errors['nodes.0.filename']
198 198
199 199 data = render('rhodecode:templates/admin/gists/new.mako',
200 200 self._get_template_context(c), self.request)
201 201 html = formencode.htmlfill.render(
202 202 data,
203 203 defaults=defaults,
204 204 errors=errors,
205 205 prefix_error=False,
206 206 encoding="UTF-8",
207 207 force_defaults=False
208 208 )
209 209 return Response(html)
210 210
211 211 except Exception:
212 212 log.exception("Exception while trying to create a gist")
213 213 h.flash(_('Error occurred during gist creation'), category='error')
214 214 raise HTTPFound(h.route_url('gists_new'))
215 215 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
216 216
217 217 @LoginRequired()
218 218 @NotAnonymous()
219 219 @CSRFRequired()
220 220 @view_config(
221 221 route_name='gist_delete', request_method='POST')
222 222 def gist_delete(self):
223 223 _ = self.request.translate
224 224 gist_id = self.request.matchdict['gist_id']
225 225
226 226 c = self.load_default_context()
227 227 c.gist = Gist.get_or_404(gist_id)
228 228
229 229 owner = c.gist.gist_owner == self._rhodecode_user.user_id
230 230 if not (h.HasPermissionAny('hg.admin')() or owner):
231 231 log.warning('Deletion of Gist was forbidden '
232 232 'by unauthorized user: `%s`', self._rhodecode_user)
233 233 raise HTTPNotFound()
234 234
235 235 GistModel().delete(c.gist)
236 236 Session().commit()
237 237 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
238 238
239 239 raise HTTPFound(h.route_url('gists_show'))
240 240
241 241 def _get_gist(self, gist_id):
242 242
243 243 gist = Gist.get_or_404(gist_id)
244 244
245 245 # Check if this gist is expired
246 246 if gist.gist_expires != -1:
247 247 if time.time() > gist.gist_expires:
248 248 log.error(
249 249 'Gist expired at %s', time_to_datetime(gist.gist_expires))
250 250 raise HTTPNotFound()
251 251
252 252 # check if this gist requires a login
253 253 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
254 254 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
255 255 log.error("Anonymous user %s tried to access protected gist `%s`",
256 256 self._rhodecode_user, gist_id)
257 257 raise HTTPNotFound()
258 258 return gist
259 259
260 260 @LoginRequired()
261 261 @view_config(
262 262 route_name='gist_show', request_method='GET',
263 263 renderer='rhodecode:templates/admin/gists/show.mako')
264 264 @view_config(
265 265 route_name='gist_show_rev', request_method='GET',
266 266 renderer='rhodecode:templates/admin/gists/show.mako')
267 267 @view_config(
268 268 route_name='gist_show_formatted', request_method='GET',
269 269 renderer=None)
270 270 @view_config(
271 271 route_name='gist_show_formatted_path', request_method='GET',
272 272 renderer=None)
273 def show(self):
273 def gist_show(self):
274 274 gist_id = self.request.matchdict['gist_id']
275 275
276 276 # TODO(marcink): expose those via matching dict
277 277 revision = self.request.matchdict.get('revision', 'tip')
278 278 f_path = self.request.matchdict.get('f_path', None)
279 279 return_format = self.request.matchdict.get('format')
280 280
281 281 c = self.load_default_context()
282 282 c.gist = self._get_gist(gist_id)
283 283 c.render = not self.request.GET.get('no-render', False)
284 284
285 285 try:
286 286 c.file_last_commit, c.files = GistModel().get_gist_files(
287 287 gist_id, revision=revision)
288 288 except VCSError:
289 289 log.exception("Exception in gist show")
290 290 raise HTTPNotFound()
291 291
292 292 if return_format == 'raw':
293 293 content = '\n\n'.join([f.content for f in c.files
294 294 if (f_path is None or f.path == f_path)])
295 295 response = Response(content)
296 296 response.content_type = 'text/plain'
297 297 return response
298 298
299 299 return self._get_template_context(c)
300 300
301 301 @LoginRequired()
302 302 @NotAnonymous()
303 303 @view_config(
304 304 route_name='gist_edit', request_method='GET',
305 305 renderer='rhodecode:templates/admin/gists/edit.mako')
306 306 def gist_edit(self):
307 307 _ = self.request.translate
308 308 gist_id = self.request.matchdict['gist_id']
309 309 c = self.load_default_context()
310 310 c.gist = self._get_gist(gist_id)
311 311
312 312 owner = c.gist.gist_owner == self._rhodecode_user.user_id
313 313 if not (h.HasPermissionAny('hg.admin')() or owner):
314 314 raise HTTPNotFound()
315 315
316 316 try:
317 317 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
318 318 except VCSError:
319 319 log.exception("Exception in gist edit")
320 320 raise HTTPNotFound()
321 321
322 322 if c.gist.gist_expires == -1:
323 323 expiry = _('never')
324 324 else:
325 325 # this cannot use timeago, since it's used in select2 as a value
326 326 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
327 327
328 328 c.lifetime_values.append(
329 329 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
330 330 )
331 331
332 332 return self._get_template_context(c)
333 333
334 334 @LoginRequired()
335 335 @NotAnonymous()
336 336 @CSRFRequired()
337 337 @view_config(
338 338 route_name='gist_update', request_method='POST',
339 339 renderer='rhodecode:templates/admin/gists/edit.mako')
340 340 def gist_update(self):
341 341 _ = self.request.translate
342 342 gist_id = self.request.matchdict['gist_id']
343 343 c = self.load_default_context()
344 344 c.gist = self._get_gist(gist_id)
345 345
346 346 owner = c.gist.gist_owner == self._rhodecode_user.user_id
347 347 if not (h.HasPermissionAny('hg.admin')() or owner):
348 348 raise HTTPNotFound()
349 349
350 350 data = peppercorn.parse(self.request.POST.items())
351 351
352 352 schema = gist_schema.GistSchema()
353 353 schema = schema.bind(
354 354 # '0' is special value to leave lifetime untouched
355 355 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
356 356 )
357 357
358 358 try:
359 359 schema_data = schema.deserialize(data)
360 360 # convert to safer format with just KEYs so we sure no duplicates
361 361 schema_data['nodes'] = gist_schema.sequence_to_nodes(
362 362 schema_data['nodes'])
363 363
364 364 GistModel().update(
365 365 gist=c.gist,
366 366 description=schema_data['description'],
367 367 owner=c.gist.owner,
368 368 gist_mapping=schema_data['nodes'],
369 369 lifetime=schema_data['lifetime'],
370 370 gist_acl_level=schema_data['gist_acl_level']
371 371 )
372 372
373 373 Session().commit()
374 374 h.flash(_('Successfully updated gist content'), category='success')
375 375 except NodeNotChangedError:
376 376 # raised if nothing was changed in repo itself. We anyway then
377 377 # store only DB stuff for gist
378 378 Session().commit()
379 379 h.flash(_('Successfully updated gist data'), category='success')
380 380 except validation_schema.Invalid as errors:
381 381 errors = errors.asdict()
382 382 h.flash(_('Error occurred during update of gist {}: {}').format(
383 383 gist_id, errors), category='error')
384 384 except Exception:
385 385 log.exception("Exception in gist edit")
386 386 h.flash(_('Error occurred during update of gist %s') % gist_id,
387 387 category='error')
388 388
389 389 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
390 390
391 391 @LoginRequired()
392 392 @NotAnonymous()
393 393 @view_config(
394 394 route_name='gist_edit_check_revision', request_method='GET',
395 395 renderer='json_ext')
396 396 def gist_edit_check_revision(self):
397 397 _ = self.request.translate
398 398 gist_id = self.request.matchdict['gist_id']
399 399 c = self.load_default_context()
400 400 c.gist = self._get_gist(gist_id)
401 401
402 402 last_rev = c.gist.scm_instance().get_commit()
403 403 success = True
404 404 revision = self.request.GET.get('revision')
405 405
406 406 if revision != last_rev.raw_id:
407 407 log.error('Last revision %s is different then submitted %s'
408 408 % (revision, last_rev))
409 409 # our gist has newer version than we
410 410 success = False
411 411
412 412 return {'success': success}
@@ -1,126 +1,126 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
22 22 from rhodecode.apps._base import ADMIN_PREFIX
23 23
24 24
25 25 def includeme(config):
26 26
27 27 config.add_route(
28 28 name='my_account_profile',
29 29 pattern=ADMIN_PREFIX + '/my_account/profile')
30 30
31 31 # my account edit details
32 32 config.add_route(
33 33 name='my_account_edit',
34 34 pattern=ADMIN_PREFIX + '/my_account/edit')
35 35 config.add_route(
36 36 name='my_account_update',
37 37 pattern=ADMIN_PREFIX + '/my_account/update')
38 38
39 39 # my account password
40 40 config.add_route(
41 41 name='my_account_password',
42 42 pattern=ADMIN_PREFIX + '/my_account/password')
43 43
44 44 config.add_route(
45 45 name='my_account_password_update',
46 pattern=ADMIN_PREFIX + '/my_account/password')
46 pattern=ADMIN_PREFIX + '/my_account/password/update')
47 47
48 48 # my account tokens
49 49 config.add_route(
50 50 name='my_account_auth_tokens',
51 51 pattern=ADMIN_PREFIX + '/my_account/auth_tokens')
52 52 config.add_route(
53 53 name='my_account_auth_tokens_add',
54 54 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new')
55 55 config.add_route(
56 56 name='my_account_auth_tokens_delete',
57 57 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete')
58 58
59 59 # my account emails
60 60 config.add_route(
61 61 name='my_account_emails',
62 62 pattern=ADMIN_PREFIX + '/my_account/emails')
63 63 config.add_route(
64 64 name='my_account_emails_add',
65 65 pattern=ADMIN_PREFIX + '/my_account/emails/new')
66 66 config.add_route(
67 67 name='my_account_emails_delete',
68 68 pattern=ADMIN_PREFIX + '/my_account/emails/delete')
69 69
70 70 config.add_route(
71 71 name='my_account_repos',
72 72 pattern=ADMIN_PREFIX + '/my_account/repos')
73 73
74 74 config.add_route(
75 75 name='my_account_watched',
76 76 pattern=ADMIN_PREFIX + '/my_account/watched')
77 77
78 78 config.add_route(
79 79 name='my_account_perms',
80 80 pattern=ADMIN_PREFIX + '/my_account/perms')
81 81
82 82 config.add_route(
83 83 name='my_account_notifications',
84 84 pattern=ADMIN_PREFIX + '/my_account/notifications')
85 85
86 86 config.add_route(
87 87 name='my_account_notifications_toggle_visibility',
88 88 pattern=ADMIN_PREFIX + '/my_account/toggle_visibility')
89 89
90 90 # my account pull requests
91 91 config.add_route(
92 92 name='my_account_pullrequests',
93 93 pattern=ADMIN_PREFIX + '/my_account/pull_requests')
94 94 config.add_route(
95 95 name='my_account_pullrequests_data',
96 96 pattern=ADMIN_PREFIX + '/my_account/pull_requests/data')
97 97
98 98 # notifications
99 99 config.add_route(
100 100 name='notifications_show_all',
101 101 pattern=ADMIN_PREFIX + '/notifications')
102 102
103 103 # notifications
104 104 config.add_route(
105 105 name='notifications_mark_all_read',
106 106 pattern=ADMIN_PREFIX + '/notifications/mark_all_read')
107 107
108 108 config.add_route(
109 109 name='notifications_show',
110 110 pattern=ADMIN_PREFIX + '/notifications/{notification_id}')
111 111
112 112 config.add_route(
113 113 name='notifications_update',
114 114 pattern=ADMIN_PREFIX + '/notifications/{notification_id}/update')
115 115
116 116 config.add_route(
117 117 name='notifications_delete',
118 118 pattern=ADMIN_PREFIX + '/notifications/{notification_id}/delete')
119 119
120 120 # channelstream test
121 121 config.add_route(
122 122 name='my_account_notifications_test_channelstream',
123 123 pattern=ADMIN_PREFIX + '/my_account/test_channelstream')
124 124
125 125 # Scan module for configuration decorators.
126 126 config.scan()
@@ -1,138 +1,144 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-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 pytest
22 22 import mock
23 23
24 24 from rhodecode.apps._base import ADMIN_PREFIX
25 25 from rhodecode.lib import helpers as h
26 26 from rhodecode.lib.auth import check_password
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.model.user import UserModel
29 29 from rhodecode.tests import assert_session_flash
30 30 from rhodecode.tests.fixture import Fixture, TestController, error_function
31 31
32 32 fixture = Fixture()
33 33
34 34
35 35 def route_path(name, **kwargs):
36 36 return {
37 37 'home': '/',
38 38 'my_account_password':
39 39 ADMIN_PREFIX + '/my_account/password',
40 'my_account_password_update':
41 ADMIN_PREFIX + '/my_account/password/update',
40 42 }[name].format(**kwargs)
41 43
42 44
43 45 test_user_1 = 'testme'
44 46 test_user_1_password = '0jd83nHNS/d23n'
45 47
46 48
47 49 class TestMyAccountPassword(TestController):
48 50 def test_valid_change_password(self, user_util):
49 51 new_password = 'my_new_valid_password'
50 52 user = user_util.create_user(password=test_user_1_password)
51 53 self.log_user(user.username, test_user_1_password)
52 54
53 55 form_data = [
54 56 ('current_password', test_user_1_password),
55 57 ('__start__', 'new_password:mapping'),
56 58 ('new_password', new_password),
57 59 ('new_password-confirm', new_password),
58 60 ('__end__', 'new_password:mapping'),
59 61 ('csrf_token', self.csrf_token),
60 62 ]
61 response = self.app.post(route_path('my_account_password'), form_data).follow()
63 response = self.app.post(
64 route_path('my_account_password_update'), form_data).follow()
62 65 assert 'Successfully updated password' in response
63 66
64 67 # check_password depends on user being in session
65 68 Session().add(user)
66 69 try:
67 70 assert check_password(new_password, user.password)
68 71 finally:
69 72 Session().expunge(user)
70 73
71 74 @pytest.mark.parametrize('current_pw, new_pw, confirm_pw', [
72 75 ('', 'abcdef123', 'abcdef123'),
73 76 ('wrong_pw', 'abcdef123', 'abcdef123'),
74 77 (test_user_1_password, test_user_1_password, test_user_1_password),
75 78 (test_user_1_password, '', ''),
76 79 (test_user_1_password, 'abcdef123', ''),
77 80 (test_user_1_password, '', 'abcdef123'),
78 81 (test_user_1_password, 'not_the', 'same_pw'),
79 82 (test_user_1_password, 'short', 'short'),
80 83 ])
81 84 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
82 85 user_util):
83 86 user = user_util.create_user(password=test_user_1_password)
84 87 self.log_user(user.username, test_user_1_password)
85 88
86 89 form_data = [
87 90 ('current_password', current_pw),
88 91 ('__start__', 'new_password:mapping'),
89 92 ('new_password', new_pw),
90 93 ('new_password-confirm', confirm_pw),
91 94 ('__end__', 'new_password:mapping'),
92 95 ('csrf_token', self.csrf_token),
93 96 ]
94 response = self.app.post(route_path('my_account_password'), form_data)
97 response = self.app.post(
98 route_path('my_account_password_update'), form_data)
95 99
96 100 assert_response = response.assert_response()
97 101 assert assert_response.get_elements('.error-block')
98 102
99 103 @mock.patch.object(UserModel, 'update_user', error_function)
100 104 def test_invalid_change_password_exception(self, user_util):
101 105 user = user_util.create_user(password=test_user_1_password)
102 106 self.log_user(user.username, test_user_1_password)
103 107
104 108 form_data = [
105 109 ('current_password', test_user_1_password),
106 110 ('__start__', 'new_password:mapping'),
107 111 ('new_password', '123456'),
108 112 ('new_password-confirm', '123456'),
109 113 ('__end__', 'new_password:mapping'),
110 114 ('csrf_token', self.csrf_token),
111 115 ]
112 response = self.app.post(route_path('my_account_password'), form_data)
116 response = self.app.post(
117 route_path('my_account_password_update'), form_data)
113 118 assert_session_flash(
114 119 response, 'Error occurred during update of user password')
115 120
116 121 def test_password_is_updated_in_session_on_password_change(self, user_util):
117 122 old_password = 'abcdef123'
118 123 new_password = 'abcdef124'
119 124
120 125 user = user_util.create_user(password=old_password)
121 126 session = self.log_user(user.username, old_password)
122 127 old_password_hash = session['password']
123 128
124 129 form_data = [
125 130 ('current_password', old_password),
126 131 ('__start__', 'new_password:mapping'),
127 132 ('new_password', new_password),
128 133 ('new_password-confirm', new_password),
129 134 ('__end__', 'new_password:mapping'),
130 135 ('csrf_token', self.csrf_token),
131 136 ]
132 self.app.post(route_path('my_account_password'), form_data)
137 self.app.post(
138 route_path('my_account_password_update'), form_data)
133 139
134 140 response = self.app.get(route_path('home'))
135 141 session = response.get_session_from_response()
136 142 new_password_hash = session['rhodecode_user']['password']
137 143
138 144 assert old_password_hash != new_password_hash No newline at end of file
@@ -1,584 +1,586 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 25 from pyramid.httpexceptions import HTTPFound
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 31 from rhodecode import forms
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib import audit_logger
34 34 from rhodecode.lib.ext_json import json
35 35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 36 from rhodecode.lib.channelstream import channelstream_request, \
37 37 ChannelstreamException
38 38 from rhodecode.lib.utils2 import safe_int, md5, str2bool
39 39 from rhodecode.model.auth_token import AuthTokenModel
40 40 from rhodecode.model.comment import CommentsModel
41 41 from rhodecode.model.db import (
42 42 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
43 43 PullRequest)
44 44 from rhodecode.model.forms import UserForm
45 45 from rhodecode.model.meta import Session
46 46 from rhodecode.model.pull_request import PullRequestModel
47 47 from rhodecode.model.scm import RepoList
48 48 from rhodecode.model.user import UserModel
49 49 from rhodecode.model.repo import RepoModel
50 50 from rhodecode.model.validation_schema.schemas import user_schema
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class MyAccountView(BaseAppView, DataGridAppView):
56 56 ALLOW_SCOPED_TOKENS = False
57 57 """
58 58 This view has alternative version inside EE, if modified please take a look
59 59 in there as well.
60 60 """
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context()
64 64 c.user = c.auth_user.get_instance()
65 65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 66 self._register_global_c(c)
67 67 return c
68 68
69 69 @LoginRequired()
70 70 @NotAnonymous()
71 71 @view_config(
72 72 route_name='my_account_profile', request_method='GET',
73 73 renderer='rhodecode:templates/admin/my_account/my_account.mako')
74 74 def my_account_profile(self):
75 75 c = self.load_default_context()
76 76 c.active = 'profile'
77 77 return self._get_template_context(c)
78 78
79 79 @LoginRequired()
80 80 @NotAnonymous()
81 81 @view_config(
82 82 route_name='my_account_password', request_method='GET',
83 83 renderer='rhodecode:templates/admin/my_account/my_account.mako')
84 84 def my_account_password(self):
85 85 c = self.load_default_context()
86 86 c.active = 'password'
87 87 c.extern_type = c.user.extern_type
88 88
89 89 schema = user_schema.ChangePasswordSchema().bind(
90 90 username=c.user.username)
91 91
92 92 form = forms.Form(
93 schema, buttons=(forms.buttons.save, forms.buttons.reset))
93 schema,
94 action=h.route_path('my_account_password_update'),
95 buttons=(forms.buttons.save, forms.buttons.reset))
94 96
95 97 c.form = form
96 98 return self._get_template_context(c)
97 99
98 100 @LoginRequired()
99 101 @NotAnonymous()
100 102 @CSRFRequired()
101 103 @view_config(
102 route_name='my_account_password', request_method='POST',
104 route_name='my_account_password_update', request_method='POST',
103 105 renderer='rhodecode:templates/admin/my_account/my_account.mako')
104 106 def my_account_password_update(self):
105 107 _ = self.request.translate
106 108 c = self.load_default_context()
107 109 c.active = 'password'
108 110 c.extern_type = c.user.extern_type
109 111
110 112 schema = user_schema.ChangePasswordSchema().bind(
111 113 username=c.user.username)
112 114
113 115 form = forms.Form(
114 116 schema, buttons=(forms.buttons.save, forms.buttons.reset))
115 117
116 118 if c.extern_type != 'rhodecode':
117 119 raise HTTPFound(self.request.route_path('my_account_password'))
118 120
119 121 controls = self.request.POST.items()
120 122 try:
121 123 valid_data = form.validate(controls)
122 124 UserModel().update_user(c.user.user_id, **valid_data)
123 125 c.user.update_userdata(force_password_change=False)
124 126 Session().commit()
125 127 except forms.ValidationFailure as e:
126 128 c.form = e
127 129 return self._get_template_context(c)
128 130
129 131 except Exception:
130 132 log.exception("Exception updating password")
131 133 h.flash(_('Error occurred during update of user password'),
132 134 category='error')
133 135 else:
134 136 instance = c.auth_user.get_instance()
135 137 self.session.setdefault('rhodecode_user', {}).update(
136 138 {'password': md5(instance.password)})
137 139 self.session.save()
138 140 h.flash(_("Successfully updated password"), category='success')
139 141
140 142 raise HTTPFound(self.request.route_path('my_account_password'))
141 143
142 144 @LoginRequired()
143 145 @NotAnonymous()
144 146 @view_config(
145 147 route_name='my_account_auth_tokens', request_method='GET',
146 148 renderer='rhodecode:templates/admin/my_account/my_account.mako')
147 149 def my_account_auth_tokens(self):
148 150 _ = self.request.translate
149 151
150 152 c = self.load_default_context()
151 153 c.active = 'auth_tokens'
152 154
153 155 c.lifetime_values = [
154 156 (str(-1), _('forever')),
155 157 (str(5), _('5 minutes')),
156 158 (str(60), _('1 hour')),
157 159 (str(60 * 24), _('1 day')),
158 160 (str(60 * 24 * 30), _('1 month')),
159 161 ]
160 162 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
161 163 c.role_values = [
162 164 (x, AuthTokenModel.cls._get_role_name(x))
163 165 for x in AuthTokenModel.cls.ROLES]
164 166 c.role_options = [(c.role_values, _("Role"))]
165 167 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
166 168 c.user.user_id, show_expired=True)
167 169 return self._get_template_context(c)
168 170
169 171 def maybe_attach_token_scope(self, token):
170 172 # implemented in EE edition
171 173 pass
172 174
173 175 @LoginRequired()
174 176 @NotAnonymous()
175 177 @CSRFRequired()
176 178 @view_config(
177 179 route_name='my_account_auth_tokens_add', request_method='POST',)
178 180 def my_account_auth_tokens_add(self):
179 181 _ = self.request.translate
180 182 c = self.load_default_context()
181 183
182 184 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
183 185 description = self.request.POST.get('description')
184 186 role = self.request.POST.get('role')
185 187
186 188 token = AuthTokenModel().create(
187 189 c.user.user_id, description, lifetime, role)
188 190 token_data = token.get_api_data()
189 191
190 192 self.maybe_attach_token_scope(token)
191 193 audit_logger.store_web(
192 194 'user.edit.token.add', action_data={
193 195 'data': {'token': token_data, 'user': 'self'}},
194 196 user=self._rhodecode_user, )
195 197 Session().commit()
196 198
197 199 h.flash(_("Auth token successfully created"), category='success')
198 200 return HTTPFound(h.route_path('my_account_auth_tokens'))
199 201
200 202 @LoginRequired()
201 203 @NotAnonymous()
202 204 @CSRFRequired()
203 205 @view_config(
204 206 route_name='my_account_auth_tokens_delete', request_method='POST')
205 207 def my_account_auth_tokens_delete(self):
206 208 _ = self.request.translate
207 209 c = self.load_default_context()
208 210
209 211 del_auth_token = self.request.POST.get('del_auth_token')
210 212
211 213 if del_auth_token:
212 214 token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
213 215 token_data = token.get_api_data()
214 216
215 217 AuthTokenModel().delete(del_auth_token, c.user.user_id)
216 218 audit_logger.store_web(
217 219 'user.edit.token.delete', action_data={
218 220 'data': {'token': token_data, 'user': 'self'}},
219 221 user=self._rhodecode_user,)
220 222 Session().commit()
221 223 h.flash(_("Auth token successfully deleted"), category='success')
222 224
223 225 return HTTPFound(h.route_path('my_account_auth_tokens'))
224 226
225 227 @LoginRequired()
226 228 @NotAnonymous()
227 229 @view_config(
228 230 route_name='my_account_emails', request_method='GET',
229 231 renderer='rhodecode:templates/admin/my_account/my_account.mako')
230 232 def my_account_emails(self):
231 233 _ = self.request.translate
232 234
233 235 c = self.load_default_context()
234 236 c.active = 'emails'
235 237
236 238 c.user_email_map = UserEmailMap.query()\
237 239 .filter(UserEmailMap.user == c.user).all()
238 240 return self._get_template_context(c)
239 241
240 242 @LoginRequired()
241 243 @NotAnonymous()
242 244 @CSRFRequired()
243 245 @view_config(
244 246 route_name='my_account_emails_add', request_method='POST')
245 247 def my_account_emails_add(self):
246 248 _ = self.request.translate
247 249 c = self.load_default_context()
248 250
249 251 email = self.request.POST.get('new_email')
250 252
251 253 try:
252 254 UserModel().add_extra_email(c.user.user_id, email)
253 255 audit_logger.store_web(
254 256 'user.edit.email.add', action_data={
255 257 'data': {'email': email, 'user': 'self'}},
256 258 user=self._rhodecode_user,)
257 259
258 260 Session().commit()
259 261 h.flash(_("Added new email address `%s` for user account") % email,
260 262 category='success')
261 263 except formencode.Invalid as error:
262 264 h.flash(h.escape(error.error_dict['email']), category='error')
263 265 except Exception:
264 266 log.exception("Exception in my_account_emails")
265 267 h.flash(_('An error occurred during email saving'),
266 268 category='error')
267 269 return HTTPFound(h.route_path('my_account_emails'))
268 270
269 271 @LoginRequired()
270 272 @NotAnonymous()
271 273 @CSRFRequired()
272 274 @view_config(
273 275 route_name='my_account_emails_delete', request_method='POST')
274 276 def my_account_emails_delete(self):
275 277 _ = self.request.translate
276 278 c = self.load_default_context()
277 279
278 280 del_email_id = self.request.POST.get('del_email_id')
279 281 if del_email_id:
280 282 email = UserEmailMap.get_or_404(del_email_id, pyramid_exc=True).email
281 283 UserModel().delete_extra_email(c.user.user_id, del_email_id)
282 284 audit_logger.store_web(
283 285 'user.edit.email.delete', action_data={
284 286 'data': {'email': email, 'user': 'self'}},
285 287 user=self._rhodecode_user,)
286 288 Session().commit()
287 289 h.flash(_("Email successfully deleted"),
288 290 category='success')
289 291 return HTTPFound(h.route_path('my_account_emails'))
290 292
291 293 @LoginRequired()
292 294 @NotAnonymous()
293 295 @CSRFRequired()
294 296 @view_config(
295 297 route_name='my_account_notifications_test_channelstream',
296 298 request_method='POST', renderer='json_ext')
297 299 def my_account_notifications_test_channelstream(self):
298 300 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
299 301 self._rhodecode_user.username, datetime.datetime.now())
300 302 payload = {
301 303 # 'channel': 'broadcast',
302 304 'type': 'message',
303 305 'timestamp': datetime.datetime.utcnow(),
304 306 'user': 'system',
305 307 'pm_users': [self._rhodecode_user.username],
306 308 'message': {
307 309 'message': message,
308 310 'level': 'info',
309 311 'topic': '/notifications'
310 312 }
311 313 }
312 314
313 315 registry = self.request.registry
314 316 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
315 317 channelstream_config = rhodecode_plugins.get('channelstream', {})
316 318
317 319 try:
318 320 channelstream_request(channelstream_config, [payload], '/message')
319 321 except ChannelstreamException as e:
320 322 log.exception('Failed to send channelstream data')
321 323 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
322 324 return {"response": 'Channelstream data sent. '
323 325 'You should see a new live message now.'}
324 326
325 327 def _load_my_repos_data(self, watched=False):
326 328 if watched:
327 329 admin = False
328 330 follows_repos = Session().query(UserFollowing)\
329 331 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
330 332 .options(joinedload(UserFollowing.follows_repository))\
331 333 .all()
332 334 repo_list = [x.follows_repository for x in follows_repos]
333 335 else:
334 336 admin = True
335 337 repo_list = Repository.get_all_repos(
336 338 user_id=self._rhodecode_user.user_id)
337 339 repo_list = RepoList(repo_list, perm_set=[
338 340 'repository.read', 'repository.write', 'repository.admin'])
339 341
340 342 repos_data = RepoModel().get_repos_as_dict(
341 343 repo_list=repo_list, admin=admin)
342 344 # json used to render the grid
343 345 return json.dumps(repos_data)
344 346
345 347 @LoginRequired()
346 348 @NotAnonymous()
347 349 @view_config(
348 350 route_name='my_account_repos', request_method='GET',
349 351 renderer='rhodecode:templates/admin/my_account/my_account.mako')
350 352 def my_account_repos(self):
351 353 c = self.load_default_context()
352 354 c.active = 'repos'
353 355
354 356 # json used to render the grid
355 357 c.data = self._load_my_repos_data()
356 358 return self._get_template_context(c)
357 359
358 360 @LoginRequired()
359 361 @NotAnonymous()
360 362 @view_config(
361 363 route_name='my_account_watched', request_method='GET',
362 364 renderer='rhodecode:templates/admin/my_account/my_account.mako')
363 365 def my_account_watched(self):
364 366 c = self.load_default_context()
365 367 c.active = 'watched'
366 368
367 369 # json used to render the grid
368 370 c.data = self._load_my_repos_data(watched=True)
369 371 return self._get_template_context(c)
370 372
371 373 @LoginRequired()
372 374 @NotAnonymous()
373 375 @view_config(
374 376 route_name='my_account_perms', request_method='GET',
375 377 renderer='rhodecode:templates/admin/my_account/my_account.mako')
376 378 def my_account_perms(self):
377 379 c = self.load_default_context()
378 380 c.active = 'perms'
379 381
380 382 c.perm_user = c.auth_user
381 383 return self._get_template_context(c)
382 384
383 385 @LoginRequired()
384 386 @NotAnonymous()
385 387 @view_config(
386 388 route_name='my_account_notifications', request_method='GET',
387 389 renderer='rhodecode:templates/admin/my_account/my_account.mako')
388 390 def my_notifications(self):
389 391 c = self.load_default_context()
390 392 c.active = 'notifications'
391 393
392 394 return self._get_template_context(c)
393 395
394 396 @LoginRequired()
395 397 @NotAnonymous()
396 398 @CSRFRequired()
397 399 @view_config(
398 400 route_name='my_account_notifications_toggle_visibility',
399 401 request_method='POST', renderer='json_ext')
400 402 def my_notifications_toggle_visibility(self):
401 403 user = self._rhodecode_db_user
402 404 new_status = not user.user_data.get('notification_status', True)
403 405 user.update_userdata(notification_status=new_status)
404 406 Session().commit()
405 407 return user.user_data['notification_status']
406 408
407 409 @LoginRequired()
408 410 @NotAnonymous()
409 411 @view_config(
410 412 route_name='my_account_edit',
411 413 request_method='GET',
412 414 renderer='rhodecode:templates/admin/my_account/my_account.mako')
413 415 def my_account_edit(self):
414 416 c = self.load_default_context()
415 417 c.active = 'profile_edit'
416 418
417 419 c.perm_user = c.auth_user
418 420 c.extern_type = c.user.extern_type
419 421 c.extern_name = c.user.extern_name
420 422
421 423 defaults = c.user.get_dict()
422 424
423 425 data = render('rhodecode:templates/admin/my_account/my_account.mako',
424 426 self._get_template_context(c), self.request)
425 427 html = formencode.htmlfill.render(
426 428 data,
427 429 defaults=defaults,
428 430 encoding="UTF-8",
429 431 force_defaults=False
430 432 )
431 433 return Response(html)
432 434
433 435 @LoginRequired()
434 436 @NotAnonymous()
435 437 @CSRFRequired()
436 438 @view_config(
437 439 route_name='my_account_update',
438 440 request_method='POST',
439 441 renderer='rhodecode:templates/admin/my_account/my_account.mako')
440 442 def my_account_update(self):
441 443 _ = self.request.translate
442 444 c = self.load_default_context()
443 445 c.active = 'profile_edit'
444 446
445 447 c.perm_user = c.auth_user
446 448 c.extern_type = c.user.extern_type
447 449 c.extern_name = c.user.extern_name
448 450
449 451 _form = UserForm(edit=True,
450 452 old_data={'user_id': self._rhodecode_user.user_id,
451 453 'email': self._rhodecode_user.email})()
452 454 form_result = {}
453 455 try:
454 456 post_data = dict(self.request.POST)
455 457 post_data['new_password'] = ''
456 458 post_data['password_confirmation'] = ''
457 459 form_result = _form.to_python(post_data)
458 460 # skip updating those attrs for my account
459 461 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
460 462 'new_password', 'password_confirmation']
461 463 # TODO: plugin should define if username can be updated
462 464 if c.extern_type != "rhodecode":
463 465 # forbid updating username for external accounts
464 466 skip_attrs.append('username')
465 467
466 468 UserModel().update_user(
467 469 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
468 470 **form_result)
469 471 h.flash(_('Your account was updated successfully'),
470 472 category='success')
471 473 Session().commit()
472 474
473 475 except formencode.Invalid as errors:
474 476 data = render(
475 477 'rhodecode:templates/admin/my_account/my_account.mako',
476 478 self._get_template_context(c), self.request)
477 479
478 480 html = formencode.htmlfill.render(
479 481 data,
480 482 defaults=errors.value,
481 483 errors=errors.error_dict or {},
482 484 prefix_error=False,
483 485 encoding="UTF-8",
484 486 force_defaults=False)
485 487 return Response(html)
486 488
487 489 except Exception:
488 490 log.exception("Exception updating user")
489 491 h.flash(_('Error occurred during update of user %s')
490 492 % form_result.get('username'), category='error')
491 493 raise HTTPFound(h.route_path('my_account_profile'))
492 494
493 495 raise HTTPFound(h.route_path('my_account_profile'))
494 496
495 497 def _get_pull_requests_list(self, statuses):
496 498 draw, start, limit = self._extract_chunk(self.request)
497 499 search_q, order_by, order_dir = self._extract_ordering(self.request)
498 500 _render = self.request.get_partial_renderer(
499 501 'data_table/_dt_elements.mako')
500 502
501 503 pull_requests = PullRequestModel().get_im_participating_in(
502 504 user_id=self._rhodecode_user.user_id,
503 505 statuses=statuses,
504 506 offset=start, length=limit, order_by=order_by,
505 507 order_dir=order_dir)
506 508
507 509 pull_requests_total_count = PullRequestModel().count_im_participating_in(
508 510 user_id=self._rhodecode_user.user_id, statuses=statuses)
509 511
510 512 data = []
511 513 comments_model = CommentsModel()
512 514 for pr in pull_requests:
513 515 repo_id = pr.target_repo_id
514 516 comments = comments_model.get_all_comments(
515 517 repo_id, pull_request=pr)
516 518 owned = pr.user_id == self._rhodecode_user.user_id
517 519
518 520 data.append({
519 521 'target_repo': _render('pullrequest_target_repo',
520 522 pr.target_repo.repo_name),
521 523 'name': _render('pullrequest_name',
522 524 pr.pull_request_id, pr.target_repo.repo_name,
523 525 short=True),
524 526 'name_raw': pr.pull_request_id,
525 527 'status': _render('pullrequest_status',
526 528 pr.calculated_review_status()),
527 529 'title': _render(
528 530 'pullrequest_title', pr.title, pr.description),
529 531 'description': h.escape(pr.description),
530 532 'updated_on': _render('pullrequest_updated_on',
531 533 h.datetime_to_time(pr.updated_on)),
532 534 'updated_on_raw': h.datetime_to_time(pr.updated_on),
533 535 'created_on': _render('pullrequest_updated_on',
534 536 h.datetime_to_time(pr.created_on)),
535 537 'created_on_raw': h.datetime_to_time(pr.created_on),
536 538 'author': _render('pullrequest_author',
537 539 pr.author.full_contact, ),
538 540 'author_raw': pr.author.full_name,
539 541 'comments': _render('pullrequest_comments', len(comments)),
540 542 'comments_raw': len(comments),
541 543 'closed': pr.is_closed(),
542 544 'owned': owned
543 545 })
544 546
545 547 # json used to render the grid
546 548 data = ({
547 549 'draw': draw,
548 550 'data': data,
549 551 'recordsTotal': pull_requests_total_count,
550 552 'recordsFiltered': pull_requests_total_count,
551 553 })
552 554 return data
553 555
554 556 @LoginRequired()
555 557 @NotAnonymous()
556 558 @view_config(
557 559 route_name='my_account_pullrequests',
558 560 request_method='GET',
559 561 renderer='rhodecode:templates/admin/my_account/my_account.mako')
560 562 def my_account_pullrequests(self):
561 563 c = self.load_default_context()
562 564 c.active = 'pullrequests'
563 565 req_get = self.request.GET
564 566
565 567 c.closed = str2bool(req_get.get('pr_show_closed'))
566 568
567 569 return self._get_template_context(c)
568 570
569 571 @LoginRequired()
570 572 @NotAnonymous()
571 573 @view_config(
572 574 route_name='my_account_pullrequests_data',
573 575 request_method='GET', renderer='json_ext')
574 576 def my_account_pullrequests_data(self):
575 577 req_get = self.request.GET
576 578 closed = str2bool(req_get.get('closed'))
577 579
578 580 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
579 581 if closed:
580 582 statuses += [PullRequest.STATUS_CLOSED]
581 583
582 584 data = self._get_pull_requests_list(statuses=statuses)
583 585 return data
584 586
@@ -1,367 +1,370 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 string
23 23
24 24 from pyramid.view import view_config
25 25
26 26 from beaker.cache import cache_region
27 27
28 28
29 29 from rhodecode.controllers import utils
30 30
31 31 from rhodecode.apps._base import RepoAppView
32 32 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
33 33 from rhodecode.lib import caches, helpers as h
34 34 from rhodecode.lib.helpers import RepoPage
35 35 from rhodecode.lib.utils2 import safe_str, safe_int
36 36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 37 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.vcs.backends.base import EmptyCommit
40 40 from rhodecode.lib.vcs.exceptions import CommitError, EmptyRepositoryError
41 41 from rhodecode.model.db import Statistics, CacheKey, User
42 42 from rhodecode.model.meta import Session
43 43 from rhodecode.model.repo import ReadmeFinder
44 44 from rhodecode.model.scm import ScmModel
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class RepoSummaryView(RepoAppView):
50 50
51 51 def load_default_context(self):
52 52 c = self._get_local_tmpl_context(include_app_defaults=True)
53 53
54 54 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
55 55 c.repo_info = self.db_repo
56 56 c.rhodecode_repo = None
57 57 if not c.repository_requirements_missing:
58 58 c.rhodecode_repo = self.rhodecode_vcs_repo
59 59
60 60 self._register_global_c(c)
61 61 return c
62 62
63 63 def _get_readme_data(self, db_repo, default_renderer):
64 64 repo_name = db_repo.repo_name
65 65 log.debug('Looking for README file')
66 66
67 67 @cache_region('long_term')
68 68 def _generate_readme(cache_key):
69 69 readme_data = None
70 70 readme_node = None
71 71 readme_filename = None
72 72 commit = self._get_landing_commit_or_none(db_repo)
73 73 if commit:
74 74 log.debug("Searching for a README file.")
75 75 readme_node = ReadmeFinder(default_renderer).search(commit)
76 76 if readme_node:
77 77 relative_url = h.route_path(
78 78 'repo_file_raw', repo_name=repo_name,
79 79 commit_id=commit.raw_id, f_path=readme_node.path)
80 80 readme_data = self._render_readme_or_none(
81 81 commit, readme_node, relative_url)
82 82 readme_filename = readme_node.path
83 83 return readme_data, readme_filename
84 84
85 85 invalidator_context = CacheKey.repo_context_cache(
86 86 _generate_readme, repo_name, CacheKey.CACHE_TYPE_README)
87 87
88 88 with invalidator_context as context:
89 89 context.invalidate()
90 90 computed = context.compute()
91 91
92 92 return computed
93 93
94 94 def _get_landing_commit_or_none(self, db_repo):
95 95 log.debug("Getting the landing commit.")
96 96 try:
97 97 commit = db_repo.get_landing_commit()
98 98 if not isinstance(commit, EmptyCommit):
99 99 return commit
100 100 else:
101 101 log.debug("Repository is empty, no README to render.")
102 102 except CommitError:
103 103 log.exception(
104 104 "Problem getting commit when trying to render the README.")
105 105
106 106 def _render_readme_or_none(self, commit, readme_node, relative_url):
107 107 log.debug(
108 108 'Found README file `%s` rendering...', readme_node.path)
109 109 renderer = MarkupRenderer()
110 110 try:
111 111 html_source = renderer.render(
112 112 readme_node.content, filename=readme_node.path)
113 113 if relative_url:
114 114 return relative_links(html_source, relative_url)
115 115 return html_source
116 116 except Exception:
117 117 log.exception(
118 118 "Exception while trying to render the README")
119 119
120 120 def _load_commits_context(self, c):
121 121 p = safe_int(self.request.GET.get('page'), 1)
122 122 size = safe_int(self.request.GET.get('size'), 10)
123 123
124 124 def url_generator(**kw):
125 125 query_params = {
126 126 'size': size
127 127 }
128 128 query_params.update(kw)
129 129 return h.route_path(
130 130 'repo_summary_commits',
131 131 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
132 132
133 133 pre_load = ['author', 'branch', 'date', 'message']
134 134 try:
135 135 collection = self.rhodecode_vcs_repo.get_commits(pre_load=pre_load)
136 136 except EmptyRepositoryError:
137 137 collection = self.rhodecode_vcs_repo
138 138
139 139 c.repo_commits = RepoPage(
140 140 collection, page=p, items_per_page=size, url=url_generator)
141 141 page_ids = [x.raw_id for x in c.repo_commits]
142 142 c.comments = self.db_repo.get_comments(page_ids)
143 143 c.statuses = self.db_repo.statuses(page_ids)
144 144
145 145 @LoginRequired()
146 146 @HasRepoPermissionAnyDecorator(
147 147 'repository.read', 'repository.write', 'repository.admin')
148 148 @view_config(
149 149 route_name='repo_summary_commits', request_method='GET',
150 150 renderer='rhodecode:templates/summary/summary_commits.mako')
151 151 def summary_commits(self):
152 152 c = self.load_default_context()
153 153 self._load_commits_context(c)
154 154 return self._get_template_context(c)
155 155
156 156 @LoginRequired()
157 157 @HasRepoPermissionAnyDecorator(
158 158 'repository.read', 'repository.write', 'repository.admin')
159 159 @view_config(
160 160 route_name='repo_summary', request_method='GET',
161 161 renderer='rhodecode:templates/summary/summary.mako')
162 162 @view_config(
163 163 route_name='repo_summary_slash', request_method='GET',
164 164 renderer='rhodecode:templates/summary/summary.mako')
165 @view_config(
166 route_name='repo_summary_explicit', request_method='GET',
167 renderer='rhodecode:templates/summary/summary.mako')
165 168 def summary(self):
166 169 c = self.load_default_context()
167 170
168 171 # Prepare the clone URL
169 172 username = ''
170 173 if self._rhodecode_user.username != User.DEFAULT_USER:
171 174 username = safe_str(self._rhodecode_user.username)
172 175
173 176 _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl
174 177 if '{repo}' in _def_clone_uri:
175 178 _def_clone_uri_by_id = _def_clone_uri.replace(
176 179 '{repo}', '_{repoid}')
177 180 elif '{repoid}' in _def_clone_uri:
178 181 _def_clone_uri_by_id = _def_clone_uri.replace(
179 182 '_{repoid}', '{repo}')
180 183
181 184 c.clone_repo_url = self.db_repo.clone_url(
182 185 user=username, uri_tmpl=_def_clone_uri)
183 186 c.clone_repo_url_id = self.db_repo.clone_url(
184 187 user=username, uri_tmpl=_def_clone_uri_by_id)
185 188
186 189 # If enabled, get statistics data
187 190
188 191 c.show_stats = bool(self.db_repo.enable_statistics)
189 192
190 193 stats = Session().query(Statistics) \
191 194 .filter(Statistics.repository == self.db_repo) \
192 195 .scalar()
193 196
194 197 c.stats_percentage = 0
195 198
196 199 if stats and stats.languages:
197 200 c.no_data = False is self.db_repo.enable_statistics
198 201 lang_stats_d = json.loads(stats.languages)
199 202
200 203 # Sort first by decreasing count and second by the file extension,
201 204 # so we have a consistent output.
202 205 lang_stats_items = sorted(lang_stats_d.iteritems(),
203 206 key=lambda k: (-k[1], k[0]))[:10]
204 207 lang_stats = [(x, {"count": y,
205 208 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
206 209 for x, y in lang_stats_items]
207 210
208 211 c.trending_languages = json.dumps(lang_stats)
209 212 else:
210 213 c.no_data = True
211 214 c.trending_languages = json.dumps({})
212 215
213 216 scm_model = ScmModel()
214 217 c.enable_downloads = self.db_repo.enable_downloads
215 218 c.repository_followers = scm_model.get_followers(self.db_repo)
216 219 c.repository_forks = scm_model.get_forks(self.db_repo)
217 220 c.repository_is_user_following = scm_model.is_following_repo(
218 221 self.db_repo_name, self._rhodecode_user.user_id)
219 222
220 223 # first interaction with the VCS instance after here...
221 224 if c.repository_requirements_missing:
222 225 self.request.override_renderer = \
223 226 'rhodecode:templates/summary/missing_requirements.mako'
224 227 return self._get_template_context(c)
225 228
226 229 c.readme_data, c.readme_file = \
227 230 self._get_readme_data(self.db_repo, c.visual.default_renderer)
228 231
229 232 # loads the summary commits template context
230 233 self._load_commits_context(c)
231 234
232 235 return self._get_template_context(c)
233 236
234 237 def get_request_commit_id(self):
235 238 return self.request.matchdict['commit_id']
236 239
237 240 @LoginRequired()
238 241 @HasRepoPermissionAnyDecorator(
239 242 'repository.read', 'repository.write', 'repository.admin')
240 243 @view_config(
241 244 route_name='repo_stats', request_method='GET',
242 245 renderer='json_ext')
243 246 def repo_stats(self):
244 247 commit_id = self.get_request_commit_id()
245 248
246 249 _namespace = caches.get_repo_namespace_key(
247 250 caches.SUMMARY_STATS, self.db_repo_name)
248 251 show_stats = bool(self.db_repo.enable_statistics)
249 252 cache_manager = caches.get_cache_manager(
250 253 'repo_cache_long', _namespace)
251 254 _cache_key = caches.compute_key_from_params(
252 255 self.db_repo_name, commit_id, show_stats)
253 256
254 257 def compute_stats():
255 258 code_stats = {}
256 259 size = 0
257 260 try:
258 261 scm_instance = self.db_repo.scm_instance()
259 262 commit = scm_instance.get_commit(commit_id)
260 263
261 264 for node in commit.get_filenodes_generator():
262 265 size += node.size
263 266 if not show_stats:
264 267 continue
265 268 ext = string.lower(node.extension)
266 269 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
267 270 if ext_info:
268 271 if ext in code_stats:
269 272 code_stats[ext]['count'] += 1
270 273 else:
271 274 code_stats[ext] = {"count": 1, "desc": ext_info}
272 275 except EmptyRepositoryError:
273 276 pass
274 277 return {'size': h.format_byte_size_binary(size),
275 278 'code_stats': code_stats}
276 279
277 280 stats = cache_manager.get(_cache_key, createfunc=compute_stats)
278 281 return stats
279 282
280 283 @LoginRequired()
281 284 @HasRepoPermissionAnyDecorator(
282 285 'repository.read', 'repository.write', 'repository.admin')
283 286 @view_config(
284 287 route_name='repo_refs_data', request_method='GET',
285 288 renderer='json_ext')
286 289 def repo_refs_data(self):
287 290 _ = self.request.translate
288 291 self.load_default_context()
289 292
290 293 repo = self.rhodecode_vcs_repo
291 294 refs_to_create = [
292 295 (_("Branch"), repo.branches, 'branch'),
293 296 (_("Tag"), repo.tags, 'tag'),
294 297 (_("Bookmark"), repo.bookmarks, 'book'),
295 298 ]
296 299 res = self._create_reference_data(
297 300 repo, self.db_repo_name, refs_to_create)
298 301 data = {
299 302 'more': False,
300 303 'results': res
301 304 }
302 305 return data
303 306
304 307 @LoginRequired()
305 308 @HasRepoPermissionAnyDecorator(
306 309 'repository.read', 'repository.write', 'repository.admin')
307 310 @view_config(
308 311 route_name='repo_refs_changelog_data', request_method='GET',
309 312 renderer='json_ext')
310 313 def repo_refs_changelog_data(self):
311 314 _ = self.request.translate
312 315 self.load_default_context()
313 316
314 317 repo = self.rhodecode_vcs_repo
315 318
316 319 refs_to_create = [
317 320 (_("Branches"), repo.branches, 'branch'),
318 321 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
319 322 # TODO: enable when vcs can handle bookmarks filters
320 323 # (_("Bookmarks"), repo.bookmarks, "book"),
321 324 ]
322 325 res = self._create_reference_data(
323 326 repo, self.db_repo_name, refs_to_create)
324 327 data = {
325 328 'more': False,
326 329 'results': res
327 330 }
328 331 return data
329 332
330 333 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
331 334 format_ref_id = utils.get_format_ref_id(repo)
332 335
333 336 result = []
334 337 for title, refs, ref_type in refs_to_create:
335 338 if refs:
336 339 result.append({
337 340 'text': title,
338 341 'children': self._create_reference_items(
339 342 repo, full_repo_name, refs, ref_type,
340 343 format_ref_id),
341 344 })
342 345 return result
343 346
344 347 def _create_reference_items(self, repo, full_repo_name, refs, ref_type,
345 348 format_ref_id):
346 349 result = []
347 350 is_svn = h.is_svn(repo)
348 351 for ref_name, raw_id in refs.iteritems():
349 352 files_url = self._create_files_url(
350 353 repo, full_repo_name, ref_name, raw_id, is_svn)
351 354 result.append({
352 355 'text': ref_name,
353 356 'id': format_ref_id(ref_name, raw_id),
354 357 'raw_id': raw_id,
355 358 'type': ref_type,
356 359 'files_url': files_url,
357 360 })
358 361 return result
359 362
360 363 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
361 364 use_commit_id = '/' in ref_name or is_svn
362 365 return h.route_path(
363 366 'repo_files',
364 367 repo_name=full_repo_name,
365 368 f_path=ref_name if is_svn else '',
366 369 commit_id=raw_id if use_commit_id else ref_name,
367 370 _query=dict(at=ref_name))
@@ -1,53 +1,53 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
23 23 from pyramid.httpexceptions import HTTPNotFound
24 24 from pyramid.view import view_config
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.lib.auth import LoginRequired, NotAnonymous
28 28
29 29 from rhodecode.model.db import User
30 30 from rhodecode.model.user import UserModel
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 class UserProfileView(BaseAppView):
36 36
37 37 @LoginRequired()
38 38 @NotAnonymous()
39 39 @view_config(
40 40 route_name='user_profile', request_method='GET',
41 41 renderer='rhodecode:templates/users/user.mako')
42 def login(self):
42 def user_profile(self):
43 43 # register local template context
44 44 c = self._get_local_tmpl_context()
45 45 c.active = 'user_profile'
46 46
47 47 username = self.request.matchdict.get('username')
48 48
49 49 c.user = UserModel().get_by_username(username)
50 50 if not c.user or c.user.username == User.DEFAULT_USER:
51 51 raise HTTPNotFound()
52 52
53 53 return self._get_template_context(c)
General Comments 0
You need to be logged in to leave comments. Login now