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