##// END OF EJS Templates
users: added option to detach pull requests for users which we delete....
dan -
r4351:2d86851b default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,1381 +1,1414 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
27 from pyramid.view import view_config
28 from pyramid.renderers import render
28 from pyramid.renderers import render
29 from pyramid.response import Response
29 from pyramid.response import Response
30
30
31 from rhodecode import events
31 from rhodecode import events
32 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
32 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
34 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
34 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
35 from rhodecode.authentication.plugins import auth_rhodecode
35 from rhodecode.authentication.plugins import auth_rhodecode
36 from rhodecode.events import trigger
36 from rhodecode.events import trigger
37 from rhodecode.model.db import true, UserNotice
37 from rhodecode.model.db import true, UserNotice
38
38
39 from rhodecode.lib import audit_logger, rc_cache
39 from rhodecode.lib import audit_logger, rc_cache
40 from rhodecode.lib.exceptions import (
40 from rhodecode.lib.exceptions import (
41 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
41 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
42 UserOwnsUserGroupsException, DefaultUserException)
42 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
43 UserOwnsArtifactsException, DefaultUserException)
43 from rhodecode.lib.ext_json import json
44 from rhodecode.lib.ext_json import json
44 from rhodecode.lib.auth import (
45 from rhodecode.lib.auth import (
45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
46 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
46 from rhodecode.lib import helpers as h
47 from rhodecode.lib import helpers as h
47 from rhodecode.lib.helpers import SqlPage
48 from rhodecode.lib.helpers import SqlPage
48 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
49 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
49 from rhodecode.model.auth_token import AuthTokenModel
50 from rhodecode.model.auth_token import AuthTokenModel
50 from rhodecode.model.forms import (
51 from rhodecode.model.forms import (
51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
52 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
52 UserExtraEmailForm, UserExtraIpForm)
53 UserExtraEmailForm, UserExtraIpForm)
53 from rhodecode.model.permission import PermissionModel
54 from rhodecode.model.permission import PermissionModel
54 from rhodecode.model.repo_group import RepoGroupModel
55 from rhodecode.model.repo_group import RepoGroupModel
55 from rhodecode.model.ssh_key import SshKeyModel
56 from rhodecode.model.ssh_key import SshKeyModel
56 from rhodecode.model.user import UserModel
57 from rhodecode.model.user import UserModel
57 from rhodecode.model.user_group import UserGroupModel
58 from rhodecode.model.user_group import UserGroupModel
58 from rhodecode.model.db import (
59 from rhodecode.model.db import (
59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
60 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
60 UserApiKeys, UserSshKeys, RepoGroup)
61 UserApiKeys, UserSshKeys, RepoGroup)
61 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
62
63
63 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
64
65
65
66
66 class AdminUsersView(BaseAppView, DataGridAppView):
67 class AdminUsersView(BaseAppView, DataGridAppView):
67
68
68 def load_default_context(self):
69 def load_default_context(self):
69 c = self._get_local_tmpl_context()
70 c = self._get_local_tmpl_context()
70 return c
71 return c
71
72
72 @LoginRequired()
73 @LoginRequired()
73 @HasPermissionAllDecorator('hg.admin')
74 @HasPermissionAllDecorator('hg.admin')
74 @view_config(
75 @view_config(
75 route_name='users', request_method='GET',
76 route_name='users', request_method='GET',
76 renderer='rhodecode:templates/admin/users/users.mako')
77 renderer='rhodecode:templates/admin/users/users.mako')
77 def users_list(self):
78 def users_list(self):
78 c = self.load_default_context()
79 c = self.load_default_context()
79 return self._get_template_context(c)
80 return self._get_template_context(c)
80
81
81 @LoginRequired()
82 @LoginRequired()
82 @HasPermissionAllDecorator('hg.admin')
83 @HasPermissionAllDecorator('hg.admin')
83 @view_config(
84 @view_config(
84 # renderer defined below
85 # renderer defined below
85 route_name='users_data', request_method='GET',
86 route_name='users_data', request_method='GET',
86 renderer='json_ext', xhr=True)
87 renderer='json_ext', xhr=True)
87 def users_list_data(self):
88 def users_list_data(self):
88 self.load_default_context()
89 self.load_default_context()
89 column_map = {
90 column_map = {
90 'first_name': 'name',
91 'first_name': 'name',
91 'last_name': 'lastname',
92 'last_name': 'lastname',
92 }
93 }
93 draw, start, limit = self._extract_chunk(self.request)
94 draw, start, limit = self._extract_chunk(self.request)
94 search_q, order_by, order_dir = self._extract_ordering(
95 search_q, order_by, order_dir = self._extract_ordering(
95 self.request, column_map=column_map)
96 self.request, column_map=column_map)
96 _render = self.request.get_partial_renderer(
97 _render = self.request.get_partial_renderer(
97 'rhodecode:templates/data_table/_dt_elements.mako')
98 'rhodecode:templates/data_table/_dt_elements.mako')
98
99
99 def user_actions(user_id, username):
100 def user_actions(user_id, username):
100 return _render("user_actions", user_id, username)
101 return _render("user_actions", user_id, username)
101
102
102 users_data_total_count = User.query()\
103 users_data_total_count = User.query()\
103 .filter(User.username != User.DEFAULT_USER) \
104 .filter(User.username != User.DEFAULT_USER) \
104 .count()
105 .count()
105
106
106 users_data_total_inactive_count = User.query()\
107 users_data_total_inactive_count = User.query()\
107 .filter(User.username != User.DEFAULT_USER) \
108 .filter(User.username != User.DEFAULT_USER) \
108 .filter(User.active != true())\
109 .filter(User.active != true())\
109 .count()
110 .count()
110
111
111 # json generate
112 # json generate
112 base_q = User.query().filter(User.username != User.DEFAULT_USER)
113 base_q = User.query().filter(User.username != User.DEFAULT_USER)
113 base_inactive_q = base_q.filter(User.active != true())
114 base_inactive_q = base_q.filter(User.active != true())
114
115
115 if search_q:
116 if search_q:
116 like_expression = u'%{}%'.format(safe_unicode(search_q))
117 like_expression = u'%{}%'.format(safe_unicode(search_q))
117 base_q = base_q.filter(or_(
118 base_q = base_q.filter(or_(
118 User.username.ilike(like_expression),
119 User.username.ilike(like_expression),
119 User._email.ilike(like_expression),
120 User._email.ilike(like_expression),
120 User.name.ilike(like_expression),
121 User.name.ilike(like_expression),
121 User.lastname.ilike(like_expression),
122 User.lastname.ilike(like_expression),
122 ))
123 ))
123 base_inactive_q = base_q.filter(User.active != true())
124 base_inactive_q = base_q.filter(User.active != true())
124
125
125 users_data_total_filtered_count = base_q.count()
126 users_data_total_filtered_count = base_q.count()
126 users_data_total_filtered_inactive_count = base_inactive_q.count()
127 users_data_total_filtered_inactive_count = base_inactive_q.count()
127
128
128 sort_col = getattr(User, order_by, None)
129 sort_col = getattr(User, order_by, None)
129 if sort_col:
130 if sort_col:
130 if order_dir == 'asc':
131 if order_dir == 'asc':
131 # handle null values properly to order by NULL last
132 # handle null values properly to order by NULL last
132 if order_by in ['last_activity']:
133 if order_by in ['last_activity']:
133 sort_col = coalesce(sort_col, datetime.date.max)
134 sort_col = coalesce(sort_col, datetime.date.max)
134 sort_col = sort_col.asc()
135 sort_col = sort_col.asc()
135 else:
136 else:
136 # handle null values properly to order by NULL last
137 # handle null values properly to order by NULL last
137 if order_by in ['last_activity']:
138 if order_by in ['last_activity']:
138 sort_col = coalesce(sort_col, datetime.date.min)
139 sort_col = coalesce(sort_col, datetime.date.min)
139 sort_col = sort_col.desc()
140 sort_col = sort_col.desc()
140
141
141 base_q = base_q.order_by(sort_col)
142 base_q = base_q.order_by(sort_col)
142 base_q = base_q.offset(start).limit(limit)
143 base_q = base_q.offset(start).limit(limit)
143
144
144 users_list = base_q.all()
145 users_list = base_q.all()
145
146
146 users_data = []
147 users_data = []
147 for user in users_list:
148 for user in users_list:
148 users_data.append({
149 users_data.append({
149 "username": h.gravatar_with_user(self.request, user.username),
150 "username": h.gravatar_with_user(self.request, user.username),
150 "email": user.email,
151 "email": user.email,
151 "first_name": user.first_name,
152 "first_name": user.first_name,
152 "last_name": user.last_name,
153 "last_name": user.last_name,
153 "last_login": h.format_date(user.last_login),
154 "last_login": h.format_date(user.last_login),
154 "last_activity": h.format_date(user.last_activity),
155 "last_activity": h.format_date(user.last_activity),
155 "active": h.bool2icon(user.active),
156 "active": h.bool2icon(user.active),
156 "active_raw": user.active,
157 "active_raw": user.active,
157 "admin": h.bool2icon(user.admin),
158 "admin": h.bool2icon(user.admin),
158 "extern_type": user.extern_type,
159 "extern_type": user.extern_type,
159 "extern_name": user.extern_name,
160 "extern_name": user.extern_name,
160 "action": user_actions(user.user_id, user.username),
161 "action": user_actions(user.user_id, user.username),
161 })
162 })
162 data = ({
163 data = ({
163 'draw': draw,
164 'draw': draw,
164 'data': users_data,
165 'data': users_data,
165 'recordsTotal': users_data_total_count,
166 'recordsTotal': users_data_total_count,
166 'recordsFiltered': users_data_total_filtered_count,
167 'recordsFiltered': users_data_total_filtered_count,
167 'recordsTotalInactive': users_data_total_inactive_count,
168 'recordsTotalInactive': users_data_total_inactive_count,
168 'recordsFilteredInactive': users_data_total_filtered_inactive_count
169 'recordsFilteredInactive': users_data_total_filtered_inactive_count
169 })
170 })
170
171
171 return data
172 return data
172
173
173 def _set_personal_repo_group_template_vars(self, c_obj):
174 def _set_personal_repo_group_template_vars(self, c_obj):
174 DummyUser = AttributeDict({
175 DummyUser = AttributeDict({
175 'username': '${username}',
176 'username': '${username}',
176 'user_id': '${user_id}',
177 'user_id': '${user_id}',
177 })
178 })
178 c_obj.default_create_repo_group = RepoGroupModel() \
179 c_obj.default_create_repo_group = RepoGroupModel() \
179 .get_default_create_personal_repo_group()
180 .get_default_create_personal_repo_group()
180 c_obj.personal_repo_group_name = RepoGroupModel() \
181 c_obj.personal_repo_group_name = RepoGroupModel() \
181 .get_personal_group_name(DummyUser)
182 .get_personal_group_name(DummyUser)
182
183
183 @LoginRequired()
184 @LoginRequired()
184 @HasPermissionAllDecorator('hg.admin')
185 @HasPermissionAllDecorator('hg.admin')
185 @view_config(
186 @view_config(
186 route_name='users_new', request_method='GET',
187 route_name='users_new', request_method='GET',
187 renderer='rhodecode:templates/admin/users/user_add.mako')
188 renderer='rhodecode:templates/admin/users/user_add.mako')
188 def users_new(self):
189 def users_new(self):
189 _ = self.request.translate
190 _ = self.request.translate
190 c = self.load_default_context()
191 c = self.load_default_context()
191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
192 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
192 self._set_personal_repo_group_template_vars(c)
193 self._set_personal_repo_group_template_vars(c)
193 return self._get_template_context(c)
194 return self._get_template_context(c)
194
195
195 @LoginRequired()
196 @LoginRequired()
196 @HasPermissionAllDecorator('hg.admin')
197 @HasPermissionAllDecorator('hg.admin')
197 @CSRFRequired()
198 @CSRFRequired()
198 @view_config(
199 @view_config(
199 route_name='users_create', request_method='POST',
200 route_name='users_create', request_method='POST',
200 renderer='rhodecode:templates/admin/users/user_add.mako')
201 renderer='rhodecode:templates/admin/users/user_add.mako')
201 def users_create(self):
202 def users_create(self):
202 _ = self.request.translate
203 _ = self.request.translate
203 c = self.load_default_context()
204 c = self.load_default_context()
204 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
205 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
205 user_model = UserModel()
206 user_model = UserModel()
206 user_form = UserForm(self.request.translate)()
207 user_form = UserForm(self.request.translate)()
207 try:
208 try:
208 form_result = user_form.to_python(dict(self.request.POST))
209 form_result = user_form.to_python(dict(self.request.POST))
209 user = user_model.create(form_result)
210 user = user_model.create(form_result)
210 Session().flush()
211 Session().flush()
211 creation_data = user.get_api_data()
212 creation_data = user.get_api_data()
212 username = form_result['username']
213 username = form_result['username']
213
214
214 audit_logger.store_web(
215 audit_logger.store_web(
215 'user.create', action_data={'data': creation_data},
216 'user.create', action_data={'data': creation_data},
216 user=c.rhodecode_user)
217 user=c.rhodecode_user)
217
218
218 user_link = h.link_to(
219 user_link = h.link_to(
219 h.escape(username),
220 h.escape(username),
220 h.route_path('user_edit', user_id=user.user_id))
221 h.route_path('user_edit', user_id=user.user_id))
221 h.flash(h.literal(_('Created user %(user_link)s')
222 h.flash(h.literal(_('Created user %(user_link)s')
222 % {'user_link': user_link}), category='success')
223 % {'user_link': user_link}), category='success')
223 Session().commit()
224 Session().commit()
224 except formencode.Invalid as errors:
225 except formencode.Invalid as errors:
225 self._set_personal_repo_group_template_vars(c)
226 self._set_personal_repo_group_template_vars(c)
226 data = render(
227 data = render(
227 'rhodecode:templates/admin/users/user_add.mako',
228 'rhodecode:templates/admin/users/user_add.mako',
228 self._get_template_context(c), self.request)
229 self._get_template_context(c), self.request)
229 html = formencode.htmlfill.render(
230 html = formencode.htmlfill.render(
230 data,
231 data,
231 defaults=errors.value,
232 defaults=errors.value,
232 errors=errors.error_dict or {},
233 errors=errors.error_dict or {},
233 prefix_error=False,
234 prefix_error=False,
234 encoding="UTF-8",
235 encoding="UTF-8",
235 force_defaults=False
236 force_defaults=False
236 )
237 )
237 return Response(html)
238 return Response(html)
238 except UserCreationError as e:
239 except UserCreationError as e:
239 h.flash(e, 'error')
240 h.flash(e, 'error')
240 except Exception:
241 except Exception:
241 log.exception("Exception creation of user")
242 log.exception("Exception creation of user")
242 h.flash(_('Error occurred during creation of user %s')
243 h.flash(_('Error occurred during creation of user %s')
243 % self.request.POST.get('username'), category='error')
244 % self.request.POST.get('username'), category='error')
244 raise HTTPFound(h.route_path('users'))
245 raise HTTPFound(h.route_path('users'))
245
246
246
247
247 class UsersView(UserAppView):
248 class UsersView(UserAppView):
248 ALLOW_SCOPED_TOKENS = False
249 ALLOW_SCOPED_TOKENS = False
249 """
250 """
250 This view has alternative version inside EE, if modified please take a look
251 This view has alternative version inside EE, if modified please take a look
251 in there as well.
252 in there as well.
252 """
253 """
253
254
254 def get_auth_plugins(self):
255 def get_auth_plugins(self):
255 valid_plugins = []
256 valid_plugins = []
256 authn_registry = get_authn_registry(self.request.registry)
257 authn_registry = get_authn_registry(self.request.registry)
257 for plugin in authn_registry.get_plugins_for_authentication():
258 for plugin in authn_registry.get_plugins_for_authentication():
258 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
259 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
259 valid_plugins.append(plugin)
260 valid_plugins.append(plugin)
260 elif plugin.name == 'rhodecode':
261 elif plugin.name == 'rhodecode':
261 valid_plugins.append(plugin)
262 valid_plugins.append(plugin)
262
263
263 # extend our choices if user has set a bound plugin which isn't enabled at the
264 # extend our choices if user has set a bound plugin which isn't enabled at the
264 # moment
265 # moment
265 extern_type = self.db_user.extern_type
266 extern_type = self.db_user.extern_type
266 if extern_type not in [x.uid for x in valid_plugins]:
267 if extern_type not in [x.uid for x in valid_plugins]:
267 try:
268 try:
268 plugin = authn_registry.get_plugin_by_uid(extern_type)
269 plugin = authn_registry.get_plugin_by_uid(extern_type)
269 if plugin:
270 if plugin:
270 valid_plugins.append(plugin)
271 valid_plugins.append(plugin)
271
272
272 except Exception:
273 except Exception:
273 log.exception(
274 log.exception(
274 'Could not extend user plugins with `{}`'.format(extern_type))
275 'Could not extend user plugins with `{}`'.format(extern_type))
275 return valid_plugins
276 return valid_plugins
276
277
277 def load_default_context(self):
278 def load_default_context(self):
278 req = self.request
279 req = self.request
279
280
280 c = self._get_local_tmpl_context()
281 c = self._get_local_tmpl_context()
281 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
282 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
282 c.allowed_languages = [
283 c.allowed_languages = [
283 ('en', 'English (en)'),
284 ('en', 'English (en)'),
284 ('de', 'German (de)'),
285 ('de', 'German (de)'),
285 ('fr', 'French (fr)'),
286 ('fr', 'French (fr)'),
286 ('it', 'Italian (it)'),
287 ('it', 'Italian (it)'),
287 ('ja', 'Japanese (ja)'),
288 ('ja', 'Japanese (ja)'),
288 ('pl', 'Polish (pl)'),
289 ('pl', 'Polish (pl)'),
289 ('pt', 'Portuguese (pt)'),
290 ('pt', 'Portuguese (pt)'),
290 ('ru', 'Russian (ru)'),
291 ('ru', 'Russian (ru)'),
291 ('zh', 'Chinese (zh)'),
292 ('zh', 'Chinese (zh)'),
292 ]
293 ]
293
294
294 c.allowed_extern_types = [
295 c.allowed_extern_types = [
295 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
296 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
296 ]
297 ]
297
298
298 c.available_permissions = req.registry.settings['available_permissions']
299 c.available_permissions = req.registry.settings['available_permissions']
299 PermissionModel().set_global_permission_choices(
300 PermissionModel().set_global_permission_choices(
300 c, gettext_translator=req.translate)
301 c, gettext_translator=req.translate)
301
302
302 return c
303 return c
303
304
304 @LoginRequired()
305 @LoginRequired()
305 @HasPermissionAllDecorator('hg.admin')
306 @HasPermissionAllDecorator('hg.admin')
306 @CSRFRequired()
307 @CSRFRequired()
307 @view_config(
308 @view_config(
308 route_name='user_update', request_method='POST',
309 route_name='user_update', request_method='POST',
309 renderer='rhodecode:templates/admin/users/user_edit.mako')
310 renderer='rhodecode:templates/admin/users/user_edit.mako')
310 def user_update(self):
311 def user_update(self):
311 _ = self.request.translate
312 _ = self.request.translate
312 c = self.load_default_context()
313 c = self.load_default_context()
313
314
314 user_id = self.db_user_id
315 user_id = self.db_user_id
315 c.user = self.db_user
316 c.user = self.db_user
316
317
317 c.active = 'profile'
318 c.active = 'profile'
318 c.extern_type = c.user.extern_type
319 c.extern_type = c.user.extern_type
319 c.extern_name = c.user.extern_name
320 c.extern_name = c.user.extern_name
320 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
321 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
321 available_languages = [x[0] for x in c.allowed_languages]
322 available_languages = [x[0] for x in c.allowed_languages]
322 _form = UserForm(self.request.translate, edit=True,
323 _form = UserForm(self.request.translate, edit=True,
323 available_languages=available_languages,
324 available_languages=available_languages,
324 old_data={'user_id': user_id,
325 old_data={'user_id': user_id,
325 'email': c.user.email})()
326 'email': c.user.email})()
326 form_result = {}
327 form_result = {}
327 old_values = c.user.get_api_data()
328 old_values = c.user.get_api_data()
328 try:
329 try:
329 form_result = _form.to_python(dict(self.request.POST))
330 form_result = _form.to_python(dict(self.request.POST))
330 skip_attrs = ['extern_name']
331 skip_attrs = ['extern_name']
331 # TODO: plugin should define if username can be updated
332 # TODO: plugin should define if username can be updated
332 if c.extern_type != "rhodecode":
333 if c.extern_type != "rhodecode":
333 # forbid updating username for external accounts
334 # forbid updating username for external accounts
334 skip_attrs.append('username')
335 skip_attrs.append('username')
335
336
336 UserModel().update_user(
337 UserModel().update_user(
337 user_id, skip_attrs=skip_attrs, **form_result)
338 user_id, skip_attrs=skip_attrs, **form_result)
338
339
339 audit_logger.store_web(
340 audit_logger.store_web(
340 'user.edit', action_data={'old_data': old_values},
341 'user.edit', action_data={'old_data': old_values},
341 user=c.rhodecode_user)
342 user=c.rhodecode_user)
342
343
343 Session().commit()
344 Session().commit()
344 h.flash(_('User updated successfully'), category='success')
345 h.flash(_('User updated successfully'), category='success')
345 except formencode.Invalid as errors:
346 except formencode.Invalid as errors:
346 data = render(
347 data = render(
347 'rhodecode:templates/admin/users/user_edit.mako',
348 'rhodecode:templates/admin/users/user_edit.mako',
348 self._get_template_context(c), self.request)
349 self._get_template_context(c), self.request)
349 html = formencode.htmlfill.render(
350 html = formencode.htmlfill.render(
350 data,
351 data,
351 defaults=errors.value,
352 defaults=errors.value,
352 errors=errors.error_dict or {},
353 errors=errors.error_dict or {},
353 prefix_error=False,
354 prefix_error=False,
354 encoding="UTF-8",
355 encoding="UTF-8",
355 force_defaults=False
356 force_defaults=False
356 )
357 )
357 return Response(html)
358 return Response(html)
358 except UserCreationError as e:
359 except UserCreationError as e:
359 h.flash(e, 'error')
360 h.flash(e, 'error')
360 except Exception:
361 except Exception:
361 log.exception("Exception updating user")
362 log.exception("Exception updating user")
362 h.flash(_('Error occurred during update of user %s')
363 h.flash(_('Error occurred during update of user %s')
363 % form_result.get('username'), category='error')
364 % form_result.get('username'), category='error')
364 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
365 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
365
366
366 @LoginRequired()
367 @LoginRequired()
367 @HasPermissionAllDecorator('hg.admin')
368 @HasPermissionAllDecorator('hg.admin')
368 @CSRFRequired()
369 @CSRFRequired()
369 @view_config(
370 @view_config(
370 route_name='user_delete', request_method='POST',
371 route_name='user_delete', request_method='POST',
371 renderer='rhodecode:templates/admin/users/user_edit.mako')
372 renderer='rhodecode:templates/admin/users/user_edit.mako')
372 def user_delete(self):
373 def user_delete(self):
373 _ = self.request.translate
374 _ = self.request.translate
374 c = self.load_default_context()
375 c = self.load_default_context()
375 c.user = self.db_user
376 c.user = self.db_user
376
377
377 _repos = c.user.repositories
378 _repos = c.user.repositories
378 _repo_groups = c.user.repository_groups
379 _repo_groups = c.user.repository_groups
379 _user_groups = c.user.user_groups
380 _user_groups = c.user.user_groups
381 _pull_requests = c.user.user_pull_requests
380 _artifacts = c.user.artifacts
382 _artifacts = c.user.artifacts
381
383
382 handle_repos = None
384 handle_repos = None
383 handle_repo_groups = None
385 handle_repo_groups = None
384 handle_user_groups = None
386 handle_user_groups = None
387 handle_pull_requests = None
385 handle_artifacts = None
388 handle_artifacts = None
386
389
387 # calls for flash of handle based on handle case detach or delete
390 # calls for flash of handle based on handle case detach or delete
388 def set_handle_flash_repos():
391 def set_handle_flash_repos():
389 handle = handle_repos
392 handle = handle_repos
390 if handle == 'detach':
393 if handle == 'detach':
391 h.flash(_('Detached %s repositories') % len(_repos),
394 h.flash(_('Detached %s repositories') % len(_repos),
392 category='success')
395 category='success')
393 elif handle == 'delete':
396 elif handle == 'delete':
394 h.flash(_('Deleted %s repositories') % len(_repos),
397 h.flash(_('Deleted %s repositories') % len(_repos),
395 category='success')
398 category='success')
396
399
397 def set_handle_flash_repo_groups():
400 def set_handle_flash_repo_groups():
398 handle = handle_repo_groups
401 handle = handle_repo_groups
399 if handle == 'detach':
402 if handle == 'detach':
400 h.flash(_('Detached %s repository groups') % len(_repo_groups),
403 h.flash(_('Detached %s repository groups') % len(_repo_groups),
401 category='success')
404 category='success')
402 elif handle == 'delete':
405 elif handle == 'delete':
403 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
406 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
404 category='success')
407 category='success')
405
408
406 def set_handle_flash_user_groups():
409 def set_handle_flash_user_groups():
407 handle = handle_user_groups
410 handle = handle_user_groups
408 if handle == 'detach':
411 if handle == 'detach':
409 h.flash(_('Detached %s user groups') % len(_user_groups),
412 h.flash(_('Detached %s user groups') % len(_user_groups),
410 category='success')
413 category='success')
411 elif handle == 'delete':
414 elif handle == 'delete':
412 h.flash(_('Deleted %s user groups') % len(_user_groups),
415 h.flash(_('Deleted %s user groups') % len(_user_groups),
413 category='success')
416 category='success')
414
417
418 def set_handle_flash_pull_requests():
419 handle = handle_pull_requests
420 if handle == 'detach':
421 h.flash(_('Detached %s pull requests') % len(_pull_requests),
422 category='success')
423 elif handle == 'delete':
424 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
425 category='success')
426
415 def set_handle_flash_artifacts():
427 def set_handle_flash_artifacts():
416 handle = handle_artifacts
428 handle = handle_artifacts
417 if handle == 'detach':
429 if handle == 'detach':
418 h.flash(_('Detached %s artifacts') % len(_artifacts),
430 h.flash(_('Detached %s artifacts') % len(_artifacts),
419 category='success')
431 category='success')
420 elif handle == 'delete':
432 elif handle == 'delete':
421 h.flash(_('Deleted %s artifacts') % len(_artifacts),
433 h.flash(_('Deleted %s artifacts') % len(_artifacts),
422 category='success')
434 category='success')
423
435
436 handle_user = User.get_first_super_admin()
437 handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
438 if handle_user_id:
439 # NOTE(marcink): we get new owner for objects...
440 handle_user = User.get_or_404(handle_user_id)
441
424 if _repos and self.request.POST.get('user_repos'):
442 if _repos and self.request.POST.get('user_repos'):
425 handle_repos = self.request.POST['user_repos']
443 handle_repos = self.request.POST['user_repos']
426
444
427 if _repo_groups and self.request.POST.get('user_repo_groups'):
445 if _repo_groups and self.request.POST.get('user_repo_groups'):
428 handle_repo_groups = self.request.POST['user_repo_groups']
446 handle_repo_groups = self.request.POST['user_repo_groups']
429
447
430 if _user_groups and self.request.POST.get('user_user_groups'):
448 if _user_groups and self.request.POST.get('user_user_groups'):
431 handle_user_groups = self.request.POST['user_user_groups']
449 handle_user_groups = self.request.POST['user_user_groups']
432
450
451 if _pull_requests and self.request.POST.get('user_pull_requests'):
452 handle_pull_requests = self.request.POST['user_pull_requests']
453
433 if _artifacts and self.request.POST.get('user_artifacts'):
454 if _artifacts and self.request.POST.get('user_artifacts'):
434 handle_artifacts = self.request.POST['user_artifacts']
455 handle_artifacts = self.request.POST['user_artifacts']
435
456
436 old_values = c.user.get_api_data()
457 old_values = c.user.get_api_data()
437
458
438 try:
459 try:
439 UserModel().delete(c.user, handle_repos=handle_repos,
460
440 handle_repo_groups=handle_repo_groups,
461 UserModel().delete(
441 handle_user_groups=handle_user_groups,
462 c.user,
442 handle_artifacts=handle_artifacts)
463 handle_repos=handle_repos,
464 handle_repo_groups=handle_repo_groups,
465 handle_user_groups=handle_user_groups,
466 handle_pull_requests=handle_pull_requests,
467 handle_artifacts=handle_artifacts,
468 handle_new_owner=handle_user
469 )
443
470
444 audit_logger.store_web(
471 audit_logger.store_web(
445 'user.delete', action_data={'old_data': old_values},
472 'user.delete', action_data={'old_data': old_values},
446 user=c.rhodecode_user)
473 user=c.rhodecode_user)
447
474
448 Session().commit()
475 Session().commit()
449 set_handle_flash_repos()
476 set_handle_flash_repos()
450 set_handle_flash_repo_groups()
477 set_handle_flash_repo_groups()
451 set_handle_flash_user_groups()
478 set_handle_flash_user_groups()
479 set_handle_flash_pull_requests()
452 set_handle_flash_artifacts()
480 set_handle_flash_artifacts()
453 username = h.escape(old_values['username'])
481 username = h.escape(old_values['username'])
454 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
482 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
455 except (UserOwnsReposException, UserOwnsRepoGroupsException,
483 except (UserOwnsReposException, UserOwnsRepoGroupsException,
456 UserOwnsUserGroupsException, DefaultUserException) as e:
484 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
485 UserOwnsArtifactsException, DefaultUserException) as e:
457 h.flash(e, category='warning')
486 h.flash(e, category='warning')
458 except Exception:
487 except Exception:
459 log.exception("Exception during deletion of user")
488 log.exception("Exception during deletion of user")
460 h.flash(_('An error occurred during deletion of user'),
489 h.flash(_('An error occurred during deletion of user'),
461 category='error')
490 category='error')
462 raise HTTPFound(h.route_path('users'))
491 raise HTTPFound(h.route_path('users'))
463
492
464 @LoginRequired()
493 @LoginRequired()
465 @HasPermissionAllDecorator('hg.admin')
494 @HasPermissionAllDecorator('hg.admin')
466 @view_config(
495 @view_config(
467 route_name='user_edit', request_method='GET',
496 route_name='user_edit', request_method='GET',
468 renderer='rhodecode:templates/admin/users/user_edit.mako')
497 renderer='rhodecode:templates/admin/users/user_edit.mako')
469 def user_edit(self):
498 def user_edit(self):
470 _ = self.request.translate
499 _ = self.request.translate
471 c = self.load_default_context()
500 c = self.load_default_context()
472 c.user = self.db_user
501 c.user = self.db_user
473
502
474 c.active = 'profile'
503 c.active = 'profile'
475 c.extern_type = c.user.extern_type
504 c.extern_type = c.user.extern_type
476 c.extern_name = c.user.extern_name
505 c.extern_name = c.user.extern_name
477 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
506 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
478
507
479 defaults = c.user.get_dict()
508 defaults = c.user.get_dict()
480 defaults.update({'language': c.user.user_data.get('language')})
509 defaults.update({'language': c.user.user_data.get('language')})
481
510
482 data = render(
511 data = render(
483 'rhodecode:templates/admin/users/user_edit.mako',
512 'rhodecode:templates/admin/users/user_edit.mako',
484 self._get_template_context(c), self.request)
513 self._get_template_context(c), self.request)
485 html = formencode.htmlfill.render(
514 html = formencode.htmlfill.render(
486 data,
515 data,
487 defaults=defaults,
516 defaults=defaults,
488 encoding="UTF-8",
517 encoding="UTF-8",
489 force_defaults=False
518 force_defaults=False
490 )
519 )
491 return Response(html)
520 return Response(html)
492
521
493 @LoginRequired()
522 @LoginRequired()
494 @HasPermissionAllDecorator('hg.admin')
523 @HasPermissionAllDecorator('hg.admin')
495 @view_config(
524 @view_config(
496 route_name='user_edit_advanced', request_method='GET',
525 route_name='user_edit_advanced', request_method='GET',
497 renderer='rhodecode:templates/admin/users/user_edit.mako')
526 renderer='rhodecode:templates/admin/users/user_edit.mako')
498 def user_edit_advanced(self):
527 def user_edit_advanced(self):
499 _ = self.request.translate
528 _ = self.request.translate
500 c = self.load_default_context()
529 c = self.load_default_context()
501
530
502 user_id = self.db_user_id
531 user_id = self.db_user_id
503 c.user = self.db_user
532 c.user = self.db_user
504
533
534 c.detach_user = User.get_first_super_admin()
535 detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
536 if detach_user_id:
537 c.detach_user = User.get_or_404(detach_user_id)
538
505 c.active = 'advanced'
539 c.active = 'advanced'
506 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
540 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
507 c.personal_repo_group_name = RepoGroupModel()\
541 c.personal_repo_group_name = RepoGroupModel()\
508 .get_personal_group_name(c.user)
542 .get_personal_group_name(c.user)
509
543
510 c.user_to_review_rules = sorted(
544 c.user_to_review_rules = sorted(
511 (x.user for x in c.user.user_review_rules),
545 (x.user for x in c.user.user_review_rules),
512 key=lambda u: u.username.lower())
546 key=lambda u: u.username.lower())
513
547
514 c.first_admin = User.get_first_super_admin()
515 defaults = c.user.get_dict()
548 defaults = c.user.get_dict()
516
549
517 # Interim workaround if the user participated on any pull requests as a
550 # Interim workaround if the user participated on any pull requests as a
518 # reviewer.
551 # reviewer.
519 has_review = len(c.user.reviewer_pull_requests)
552 has_review = len(c.user.reviewer_pull_requests)
520 c.can_delete_user = not has_review
553 c.can_delete_user = not has_review
521 c.can_delete_user_message = ''
554 c.can_delete_user_message = ''
522 inactive_link = h.link_to(
555 inactive_link = h.link_to(
523 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
556 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
524 if has_review == 1:
557 if has_review == 1:
525 c.can_delete_user_message = h.literal(_(
558 c.can_delete_user_message = h.literal(_(
526 'The user participates as reviewer in {} pull request and '
559 'The user participates as reviewer in {} pull request and '
527 'cannot be deleted. \nYou can set the user to '
560 'cannot be deleted. \nYou can set the user to '
528 '"{}" instead of deleting it.').format(
561 '"{}" instead of deleting it.').format(
529 has_review, inactive_link))
562 has_review, inactive_link))
530 elif has_review:
563 elif has_review:
531 c.can_delete_user_message = h.literal(_(
564 c.can_delete_user_message = h.literal(_(
532 'The user participates as reviewer in {} pull requests and '
565 'The user participates as reviewer in {} pull requests and '
533 'cannot be deleted. \nYou can set the user to '
566 'cannot be deleted. \nYou can set the user to '
534 '"{}" instead of deleting it.').format(
567 '"{}" instead of deleting it.').format(
535 has_review, inactive_link))
568 has_review, inactive_link))
536
569
537 data = render(
570 data = render(
538 'rhodecode:templates/admin/users/user_edit.mako',
571 'rhodecode:templates/admin/users/user_edit.mako',
539 self._get_template_context(c), self.request)
572 self._get_template_context(c), self.request)
540 html = formencode.htmlfill.render(
573 html = formencode.htmlfill.render(
541 data,
574 data,
542 defaults=defaults,
575 defaults=defaults,
543 encoding="UTF-8",
576 encoding="UTF-8",
544 force_defaults=False
577 force_defaults=False
545 )
578 )
546 return Response(html)
579 return Response(html)
547
580
548 @LoginRequired()
581 @LoginRequired()
549 @HasPermissionAllDecorator('hg.admin')
582 @HasPermissionAllDecorator('hg.admin')
550 @view_config(
583 @view_config(
551 route_name='user_edit_global_perms', request_method='GET',
584 route_name='user_edit_global_perms', request_method='GET',
552 renderer='rhodecode:templates/admin/users/user_edit.mako')
585 renderer='rhodecode:templates/admin/users/user_edit.mako')
553 def user_edit_global_perms(self):
586 def user_edit_global_perms(self):
554 _ = self.request.translate
587 _ = self.request.translate
555 c = self.load_default_context()
588 c = self.load_default_context()
556 c.user = self.db_user
589 c.user = self.db_user
557
590
558 c.active = 'global_perms'
591 c.active = 'global_perms'
559
592
560 c.default_user = User.get_default_user()
593 c.default_user = User.get_default_user()
561 defaults = c.user.get_dict()
594 defaults = c.user.get_dict()
562 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
595 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
563 defaults.update(c.default_user.get_default_perms())
596 defaults.update(c.default_user.get_default_perms())
564 defaults.update(c.user.get_default_perms())
597 defaults.update(c.user.get_default_perms())
565
598
566 data = render(
599 data = render(
567 'rhodecode:templates/admin/users/user_edit.mako',
600 'rhodecode:templates/admin/users/user_edit.mako',
568 self._get_template_context(c), self.request)
601 self._get_template_context(c), self.request)
569 html = formencode.htmlfill.render(
602 html = formencode.htmlfill.render(
570 data,
603 data,
571 defaults=defaults,
604 defaults=defaults,
572 encoding="UTF-8",
605 encoding="UTF-8",
573 force_defaults=False
606 force_defaults=False
574 )
607 )
575 return Response(html)
608 return Response(html)
576
609
577 @LoginRequired()
610 @LoginRequired()
578 @HasPermissionAllDecorator('hg.admin')
611 @HasPermissionAllDecorator('hg.admin')
579 @CSRFRequired()
612 @CSRFRequired()
580 @view_config(
613 @view_config(
581 route_name='user_edit_global_perms_update', request_method='POST',
614 route_name='user_edit_global_perms_update', request_method='POST',
582 renderer='rhodecode:templates/admin/users/user_edit.mako')
615 renderer='rhodecode:templates/admin/users/user_edit.mako')
583 def user_edit_global_perms_update(self):
616 def user_edit_global_perms_update(self):
584 _ = self.request.translate
617 _ = self.request.translate
585 c = self.load_default_context()
618 c = self.load_default_context()
586
619
587 user_id = self.db_user_id
620 user_id = self.db_user_id
588 c.user = self.db_user
621 c.user = self.db_user
589
622
590 c.active = 'global_perms'
623 c.active = 'global_perms'
591 try:
624 try:
592 # first stage that verifies the checkbox
625 # first stage that verifies the checkbox
593 _form = UserIndividualPermissionsForm(self.request.translate)
626 _form = UserIndividualPermissionsForm(self.request.translate)
594 form_result = _form.to_python(dict(self.request.POST))
627 form_result = _form.to_python(dict(self.request.POST))
595 inherit_perms = form_result['inherit_default_permissions']
628 inherit_perms = form_result['inherit_default_permissions']
596 c.user.inherit_default_permissions = inherit_perms
629 c.user.inherit_default_permissions = inherit_perms
597 Session().add(c.user)
630 Session().add(c.user)
598
631
599 if not inherit_perms:
632 if not inherit_perms:
600 # only update the individual ones if we un check the flag
633 # only update the individual ones if we un check the flag
601 _form = UserPermissionsForm(
634 _form = UserPermissionsForm(
602 self.request.translate,
635 self.request.translate,
603 [x[0] for x in c.repo_create_choices],
636 [x[0] for x in c.repo_create_choices],
604 [x[0] for x in c.repo_create_on_write_choices],
637 [x[0] for x in c.repo_create_on_write_choices],
605 [x[0] for x in c.repo_group_create_choices],
638 [x[0] for x in c.repo_group_create_choices],
606 [x[0] for x in c.user_group_create_choices],
639 [x[0] for x in c.user_group_create_choices],
607 [x[0] for x in c.fork_choices],
640 [x[0] for x in c.fork_choices],
608 [x[0] for x in c.inherit_default_permission_choices])()
641 [x[0] for x in c.inherit_default_permission_choices])()
609
642
610 form_result = _form.to_python(dict(self.request.POST))
643 form_result = _form.to_python(dict(self.request.POST))
611 form_result.update({'perm_user_id': c.user.user_id})
644 form_result.update({'perm_user_id': c.user.user_id})
612
645
613 PermissionModel().update_user_permissions(form_result)
646 PermissionModel().update_user_permissions(form_result)
614
647
615 # TODO(marcink): implement global permissions
648 # TODO(marcink): implement global permissions
616 # audit_log.store_web('user.edit.permissions')
649 # audit_log.store_web('user.edit.permissions')
617
650
618 Session().commit()
651 Session().commit()
619
652
620 h.flash(_('User global permissions updated successfully'),
653 h.flash(_('User global permissions updated successfully'),
621 category='success')
654 category='success')
622
655
623 except formencode.Invalid as errors:
656 except formencode.Invalid as errors:
624 data = render(
657 data = render(
625 'rhodecode:templates/admin/users/user_edit.mako',
658 'rhodecode:templates/admin/users/user_edit.mako',
626 self._get_template_context(c), self.request)
659 self._get_template_context(c), self.request)
627 html = formencode.htmlfill.render(
660 html = formencode.htmlfill.render(
628 data,
661 data,
629 defaults=errors.value,
662 defaults=errors.value,
630 errors=errors.error_dict or {},
663 errors=errors.error_dict or {},
631 prefix_error=False,
664 prefix_error=False,
632 encoding="UTF-8",
665 encoding="UTF-8",
633 force_defaults=False
666 force_defaults=False
634 )
667 )
635 return Response(html)
668 return Response(html)
636 except Exception:
669 except Exception:
637 log.exception("Exception during permissions saving")
670 log.exception("Exception during permissions saving")
638 h.flash(_('An error occurred during permissions saving'),
671 h.flash(_('An error occurred during permissions saving'),
639 category='error')
672 category='error')
640
673
641 affected_user_ids = [user_id]
674 affected_user_ids = [user_id]
642 PermissionModel().trigger_permission_flush(affected_user_ids)
675 PermissionModel().trigger_permission_flush(affected_user_ids)
643 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
676 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
644
677
645 @LoginRequired()
678 @LoginRequired()
646 @HasPermissionAllDecorator('hg.admin')
679 @HasPermissionAllDecorator('hg.admin')
647 @CSRFRequired()
680 @CSRFRequired()
648 @view_config(
681 @view_config(
649 route_name='user_enable_force_password_reset', request_method='POST',
682 route_name='user_enable_force_password_reset', request_method='POST',
650 renderer='rhodecode:templates/admin/users/user_edit.mako')
683 renderer='rhodecode:templates/admin/users/user_edit.mako')
651 def user_enable_force_password_reset(self):
684 def user_enable_force_password_reset(self):
652 _ = self.request.translate
685 _ = self.request.translate
653 c = self.load_default_context()
686 c = self.load_default_context()
654
687
655 user_id = self.db_user_id
688 user_id = self.db_user_id
656 c.user = self.db_user
689 c.user = self.db_user
657
690
658 try:
691 try:
659 c.user.update_userdata(force_password_change=True)
692 c.user.update_userdata(force_password_change=True)
660
693
661 msg = _('Force password change enabled for user')
694 msg = _('Force password change enabled for user')
662 audit_logger.store_web('user.edit.password_reset.enabled',
695 audit_logger.store_web('user.edit.password_reset.enabled',
663 user=c.rhodecode_user)
696 user=c.rhodecode_user)
664
697
665 Session().commit()
698 Session().commit()
666 h.flash(msg, category='success')
699 h.flash(msg, category='success')
667 except Exception:
700 except Exception:
668 log.exception("Exception during password reset for user")
701 log.exception("Exception during password reset for user")
669 h.flash(_('An error occurred during password reset for user'),
702 h.flash(_('An error occurred during password reset for user'),
670 category='error')
703 category='error')
671
704
672 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
705 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
673
706
674 @LoginRequired()
707 @LoginRequired()
675 @HasPermissionAllDecorator('hg.admin')
708 @HasPermissionAllDecorator('hg.admin')
676 @CSRFRequired()
709 @CSRFRequired()
677 @view_config(
710 @view_config(
678 route_name='user_disable_force_password_reset', request_method='POST',
711 route_name='user_disable_force_password_reset', request_method='POST',
679 renderer='rhodecode:templates/admin/users/user_edit.mako')
712 renderer='rhodecode:templates/admin/users/user_edit.mako')
680 def user_disable_force_password_reset(self):
713 def user_disable_force_password_reset(self):
681 _ = self.request.translate
714 _ = self.request.translate
682 c = self.load_default_context()
715 c = self.load_default_context()
683
716
684 user_id = self.db_user_id
717 user_id = self.db_user_id
685 c.user = self.db_user
718 c.user = self.db_user
686
719
687 try:
720 try:
688 c.user.update_userdata(force_password_change=False)
721 c.user.update_userdata(force_password_change=False)
689
722
690 msg = _('Force password change disabled for user')
723 msg = _('Force password change disabled for user')
691 audit_logger.store_web(
724 audit_logger.store_web(
692 'user.edit.password_reset.disabled',
725 'user.edit.password_reset.disabled',
693 user=c.rhodecode_user)
726 user=c.rhodecode_user)
694
727
695 Session().commit()
728 Session().commit()
696 h.flash(msg, category='success')
729 h.flash(msg, category='success')
697 except Exception:
730 except Exception:
698 log.exception("Exception during password reset for user")
731 log.exception("Exception during password reset for user")
699 h.flash(_('An error occurred during password reset for user'),
732 h.flash(_('An error occurred during password reset for user'),
700 category='error')
733 category='error')
701
734
702 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
735 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
703
736
704 @LoginRequired()
737 @LoginRequired()
705 @HasPermissionAllDecorator('hg.admin')
738 @HasPermissionAllDecorator('hg.admin')
706 @CSRFRequired()
739 @CSRFRequired()
707 @view_config(
740 @view_config(
708 route_name='user_notice_dismiss', request_method='POST',
741 route_name='user_notice_dismiss', request_method='POST',
709 renderer='json_ext', xhr=True)
742 renderer='json_ext', xhr=True)
710 def user_notice_dismiss(self):
743 def user_notice_dismiss(self):
711 _ = self.request.translate
744 _ = self.request.translate
712 c = self.load_default_context()
745 c = self.load_default_context()
713
746
714 user_id = self.db_user_id
747 user_id = self.db_user_id
715 c.user = self.db_user
748 c.user = self.db_user
716 user_notice_id = safe_int(self.request.POST.get('notice_id'))
749 user_notice_id = safe_int(self.request.POST.get('notice_id'))
717 notice = UserNotice().query()\
750 notice = UserNotice().query()\
718 .filter(UserNotice.user_id == user_id)\
751 .filter(UserNotice.user_id == user_id)\
719 .filter(UserNotice.user_notice_id == user_notice_id)\
752 .filter(UserNotice.user_notice_id == user_notice_id)\
720 .scalar()
753 .scalar()
721 read = False
754 read = False
722 if notice:
755 if notice:
723 notice.notice_read = True
756 notice.notice_read = True
724 Session().add(notice)
757 Session().add(notice)
725 Session().commit()
758 Session().commit()
726 read = True
759 read = True
727
760
728 return {'notice': user_notice_id, 'read': read}
761 return {'notice': user_notice_id, 'read': read}
729
762
730 @LoginRequired()
763 @LoginRequired()
731 @HasPermissionAllDecorator('hg.admin')
764 @HasPermissionAllDecorator('hg.admin')
732 @CSRFRequired()
765 @CSRFRequired()
733 @view_config(
766 @view_config(
734 route_name='user_create_personal_repo_group', request_method='POST',
767 route_name='user_create_personal_repo_group', request_method='POST',
735 renderer='rhodecode:templates/admin/users/user_edit.mako')
768 renderer='rhodecode:templates/admin/users/user_edit.mako')
736 def user_create_personal_repo_group(self):
769 def user_create_personal_repo_group(self):
737 """
770 """
738 Create personal repository group for this user
771 Create personal repository group for this user
739 """
772 """
740 from rhodecode.model.repo_group import RepoGroupModel
773 from rhodecode.model.repo_group import RepoGroupModel
741
774
742 _ = self.request.translate
775 _ = self.request.translate
743 c = self.load_default_context()
776 c = self.load_default_context()
744
777
745 user_id = self.db_user_id
778 user_id = self.db_user_id
746 c.user = self.db_user
779 c.user = self.db_user
747
780
748 personal_repo_group = RepoGroup.get_user_personal_repo_group(
781 personal_repo_group = RepoGroup.get_user_personal_repo_group(
749 c.user.user_id)
782 c.user.user_id)
750 if personal_repo_group:
783 if personal_repo_group:
751 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
784 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
752
785
753 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
786 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
754 named_personal_group = RepoGroup.get_by_group_name(
787 named_personal_group = RepoGroup.get_by_group_name(
755 personal_repo_group_name)
788 personal_repo_group_name)
756 try:
789 try:
757
790
758 if named_personal_group and named_personal_group.user_id == c.user.user_id:
791 if named_personal_group and named_personal_group.user_id == c.user.user_id:
759 # migrate the same named group, and mark it as personal
792 # migrate the same named group, and mark it as personal
760 named_personal_group.personal = True
793 named_personal_group.personal = True
761 Session().add(named_personal_group)
794 Session().add(named_personal_group)
762 Session().commit()
795 Session().commit()
763 msg = _('Linked repository group `%s` as personal' % (
796 msg = _('Linked repository group `%s` as personal' % (
764 personal_repo_group_name,))
797 personal_repo_group_name,))
765 h.flash(msg, category='success')
798 h.flash(msg, category='success')
766 elif not named_personal_group:
799 elif not named_personal_group:
767 RepoGroupModel().create_personal_repo_group(c.user)
800 RepoGroupModel().create_personal_repo_group(c.user)
768
801
769 msg = _('Created repository group `%s`' % (
802 msg = _('Created repository group `%s`' % (
770 personal_repo_group_name,))
803 personal_repo_group_name,))
771 h.flash(msg, category='success')
804 h.flash(msg, category='success')
772 else:
805 else:
773 msg = _('Repository group `%s` is already taken' % (
806 msg = _('Repository group `%s` is already taken' % (
774 personal_repo_group_name,))
807 personal_repo_group_name,))
775 h.flash(msg, category='warning')
808 h.flash(msg, category='warning')
776 except Exception:
809 except Exception:
777 log.exception("Exception during repository group creation")
810 log.exception("Exception during repository group creation")
778 msg = _(
811 msg = _(
779 'An error occurred during repository group creation for user')
812 'An error occurred during repository group creation for user')
780 h.flash(msg, category='error')
813 h.flash(msg, category='error')
781 Session().rollback()
814 Session().rollback()
782
815
783 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
816 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
784
817
785 @LoginRequired()
818 @LoginRequired()
786 @HasPermissionAllDecorator('hg.admin')
819 @HasPermissionAllDecorator('hg.admin')
787 @view_config(
820 @view_config(
788 route_name='edit_user_auth_tokens', request_method='GET',
821 route_name='edit_user_auth_tokens', request_method='GET',
789 renderer='rhodecode:templates/admin/users/user_edit.mako')
822 renderer='rhodecode:templates/admin/users/user_edit.mako')
790 def auth_tokens(self):
823 def auth_tokens(self):
791 _ = self.request.translate
824 _ = self.request.translate
792 c = self.load_default_context()
825 c = self.load_default_context()
793 c.user = self.db_user
826 c.user = self.db_user
794
827
795 c.active = 'auth_tokens'
828 c.active = 'auth_tokens'
796
829
797 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
830 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
798 c.role_values = [
831 c.role_values = [
799 (x, AuthTokenModel.cls._get_role_name(x))
832 (x, AuthTokenModel.cls._get_role_name(x))
800 for x in AuthTokenModel.cls.ROLES]
833 for x in AuthTokenModel.cls.ROLES]
801 c.role_options = [(c.role_values, _("Role"))]
834 c.role_options = [(c.role_values, _("Role"))]
802 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
835 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
803 c.user.user_id, show_expired=True)
836 c.user.user_id, show_expired=True)
804 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
837 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
805 return self._get_template_context(c)
838 return self._get_template_context(c)
806
839
807 @LoginRequired()
840 @LoginRequired()
808 @HasPermissionAllDecorator('hg.admin')
841 @HasPermissionAllDecorator('hg.admin')
809 @view_config(
842 @view_config(
810 route_name='edit_user_auth_tokens_view', request_method='POST',
843 route_name='edit_user_auth_tokens_view', request_method='POST',
811 renderer='json_ext', xhr=True)
844 renderer='json_ext', xhr=True)
812 def auth_tokens_view(self):
845 def auth_tokens_view(self):
813 _ = self.request.translate
846 _ = self.request.translate
814 c = self.load_default_context()
847 c = self.load_default_context()
815 c.user = self.db_user
848 c.user = self.db_user
816
849
817 auth_token_id = self.request.POST.get('auth_token_id')
850 auth_token_id = self.request.POST.get('auth_token_id')
818
851
819 if auth_token_id:
852 if auth_token_id:
820 token = UserApiKeys.get_or_404(auth_token_id)
853 token = UserApiKeys.get_or_404(auth_token_id)
821
854
822 return {
855 return {
823 'auth_token': token.api_key
856 'auth_token': token.api_key
824 }
857 }
825
858
826 def maybe_attach_token_scope(self, token):
859 def maybe_attach_token_scope(self, token):
827 # implemented in EE edition
860 # implemented in EE edition
828 pass
861 pass
829
862
830 @LoginRequired()
863 @LoginRequired()
831 @HasPermissionAllDecorator('hg.admin')
864 @HasPermissionAllDecorator('hg.admin')
832 @CSRFRequired()
865 @CSRFRequired()
833 @view_config(
866 @view_config(
834 route_name='edit_user_auth_tokens_add', request_method='POST')
867 route_name='edit_user_auth_tokens_add', request_method='POST')
835 def auth_tokens_add(self):
868 def auth_tokens_add(self):
836 _ = self.request.translate
869 _ = self.request.translate
837 c = self.load_default_context()
870 c = self.load_default_context()
838
871
839 user_id = self.db_user_id
872 user_id = self.db_user_id
840 c.user = self.db_user
873 c.user = self.db_user
841
874
842 user_data = c.user.get_api_data()
875 user_data = c.user.get_api_data()
843 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
876 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
844 description = self.request.POST.get('description')
877 description = self.request.POST.get('description')
845 role = self.request.POST.get('role')
878 role = self.request.POST.get('role')
846
879
847 token = UserModel().add_auth_token(
880 token = UserModel().add_auth_token(
848 user=c.user.user_id,
881 user=c.user.user_id,
849 lifetime_minutes=lifetime, role=role, description=description,
882 lifetime_minutes=lifetime, role=role, description=description,
850 scope_callback=self.maybe_attach_token_scope)
883 scope_callback=self.maybe_attach_token_scope)
851 token_data = token.get_api_data()
884 token_data = token.get_api_data()
852
885
853 audit_logger.store_web(
886 audit_logger.store_web(
854 'user.edit.token.add', action_data={
887 'user.edit.token.add', action_data={
855 'data': {'token': token_data, 'user': user_data}},
888 'data': {'token': token_data, 'user': user_data}},
856 user=self._rhodecode_user, )
889 user=self._rhodecode_user, )
857 Session().commit()
890 Session().commit()
858
891
859 h.flash(_("Auth token successfully created"), category='success')
892 h.flash(_("Auth token successfully created"), category='success')
860 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
893 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
861
894
862 @LoginRequired()
895 @LoginRequired()
863 @HasPermissionAllDecorator('hg.admin')
896 @HasPermissionAllDecorator('hg.admin')
864 @CSRFRequired()
897 @CSRFRequired()
865 @view_config(
898 @view_config(
866 route_name='edit_user_auth_tokens_delete', request_method='POST')
899 route_name='edit_user_auth_tokens_delete', request_method='POST')
867 def auth_tokens_delete(self):
900 def auth_tokens_delete(self):
868 _ = self.request.translate
901 _ = self.request.translate
869 c = self.load_default_context()
902 c = self.load_default_context()
870
903
871 user_id = self.db_user_id
904 user_id = self.db_user_id
872 c.user = self.db_user
905 c.user = self.db_user
873
906
874 user_data = c.user.get_api_data()
907 user_data = c.user.get_api_data()
875
908
876 del_auth_token = self.request.POST.get('del_auth_token')
909 del_auth_token = self.request.POST.get('del_auth_token')
877
910
878 if del_auth_token:
911 if del_auth_token:
879 token = UserApiKeys.get_or_404(del_auth_token)
912 token = UserApiKeys.get_or_404(del_auth_token)
880 token_data = token.get_api_data()
913 token_data = token.get_api_data()
881
914
882 AuthTokenModel().delete(del_auth_token, c.user.user_id)
915 AuthTokenModel().delete(del_auth_token, c.user.user_id)
883 audit_logger.store_web(
916 audit_logger.store_web(
884 'user.edit.token.delete', action_data={
917 'user.edit.token.delete', action_data={
885 'data': {'token': token_data, 'user': user_data}},
918 'data': {'token': token_data, 'user': user_data}},
886 user=self._rhodecode_user,)
919 user=self._rhodecode_user,)
887 Session().commit()
920 Session().commit()
888 h.flash(_("Auth token successfully deleted"), category='success')
921 h.flash(_("Auth token successfully deleted"), category='success')
889
922
890 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
923 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
891
924
892 @LoginRequired()
925 @LoginRequired()
893 @HasPermissionAllDecorator('hg.admin')
926 @HasPermissionAllDecorator('hg.admin')
894 @view_config(
927 @view_config(
895 route_name='edit_user_ssh_keys', request_method='GET',
928 route_name='edit_user_ssh_keys', request_method='GET',
896 renderer='rhodecode:templates/admin/users/user_edit.mako')
929 renderer='rhodecode:templates/admin/users/user_edit.mako')
897 def ssh_keys(self):
930 def ssh_keys(self):
898 _ = self.request.translate
931 _ = self.request.translate
899 c = self.load_default_context()
932 c = self.load_default_context()
900 c.user = self.db_user
933 c.user = self.db_user
901
934
902 c.active = 'ssh_keys'
935 c.active = 'ssh_keys'
903 c.default_key = self.request.GET.get('default_key')
936 c.default_key = self.request.GET.get('default_key')
904 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
937 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
905 return self._get_template_context(c)
938 return self._get_template_context(c)
906
939
907 @LoginRequired()
940 @LoginRequired()
908 @HasPermissionAllDecorator('hg.admin')
941 @HasPermissionAllDecorator('hg.admin')
909 @view_config(
942 @view_config(
910 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
943 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
911 renderer='rhodecode:templates/admin/users/user_edit.mako')
944 renderer='rhodecode:templates/admin/users/user_edit.mako')
912 def ssh_keys_generate_keypair(self):
945 def ssh_keys_generate_keypair(self):
913 _ = self.request.translate
946 _ = self.request.translate
914 c = self.load_default_context()
947 c = self.load_default_context()
915
948
916 c.user = self.db_user
949 c.user = self.db_user
917
950
918 c.active = 'ssh_keys_generate'
951 c.active = 'ssh_keys_generate'
919 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
952 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
920 private_format = self.request.GET.get('private_format') \
953 private_format = self.request.GET.get('private_format') \
921 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
954 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
922 c.private, c.public = SshKeyModel().generate_keypair(
955 c.private, c.public = SshKeyModel().generate_keypair(
923 comment=comment, private_format=private_format)
956 comment=comment, private_format=private_format)
924
957
925 return self._get_template_context(c)
958 return self._get_template_context(c)
926
959
927 @LoginRequired()
960 @LoginRequired()
928 @HasPermissionAllDecorator('hg.admin')
961 @HasPermissionAllDecorator('hg.admin')
929 @CSRFRequired()
962 @CSRFRequired()
930 @view_config(
963 @view_config(
931 route_name='edit_user_ssh_keys_add', request_method='POST')
964 route_name='edit_user_ssh_keys_add', request_method='POST')
932 def ssh_keys_add(self):
965 def ssh_keys_add(self):
933 _ = self.request.translate
966 _ = self.request.translate
934 c = self.load_default_context()
967 c = self.load_default_context()
935
968
936 user_id = self.db_user_id
969 user_id = self.db_user_id
937 c.user = self.db_user
970 c.user = self.db_user
938
971
939 user_data = c.user.get_api_data()
972 user_data = c.user.get_api_data()
940 key_data = self.request.POST.get('key_data')
973 key_data = self.request.POST.get('key_data')
941 description = self.request.POST.get('description')
974 description = self.request.POST.get('description')
942
975
943 fingerprint = 'unknown'
976 fingerprint = 'unknown'
944 try:
977 try:
945 if not key_data:
978 if not key_data:
946 raise ValueError('Please add a valid public key')
979 raise ValueError('Please add a valid public key')
947
980
948 key = SshKeyModel().parse_key(key_data.strip())
981 key = SshKeyModel().parse_key(key_data.strip())
949 fingerprint = key.hash_md5()
982 fingerprint = key.hash_md5()
950
983
951 ssh_key = SshKeyModel().create(
984 ssh_key = SshKeyModel().create(
952 c.user.user_id, fingerprint, key.keydata, description)
985 c.user.user_id, fingerprint, key.keydata, description)
953 ssh_key_data = ssh_key.get_api_data()
986 ssh_key_data = ssh_key.get_api_data()
954
987
955 audit_logger.store_web(
988 audit_logger.store_web(
956 'user.edit.ssh_key.add', action_data={
989 'user.edit.ssh_key.add', action_data={
957 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
990 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
958 user=self._rhodecode_user, )
991 user=self._rhodecode_user, )
959 Session().commit()
992 Session().commit()
960
993
961 # Trigger an event on change of keys.
994 # Trigger an event on change of keys.
962 trigger(SshKeyFileChangeEvent(), self.request.registry)
995 trigger(SshKeyFileChangeEvent(), self.request.registry)
963
996
964 h.flash(_("Ssh Key successfully created"), category='success')
997 h.flash(_("Ssh Key successfully created"), category='success')
965
998
966 except IntegrityError:
999 except IntegrityError:
967 log.exception("Exception during ssh key saving")
1000 log.exception("Exception during ssh key saving")
968 err = 'Such key with fingerprint `{}` already exists, ' \
1001 err = 'Such key with fingerprint `{}` already exists, ' \
969 'please use a different one'.format(fingerprint)
1002 'please use a different one'.format(fingerprint)
970 h.flash(_('An error occurred during ssh key saving: {}').format(err),
1003 h.flash(_('An error occurred during ssh key saving: {}').format(err),
971 category='error')
1004 category='error')
972 except Exception as e:
1005 except Exception as e:
973 log.exception("Exception during ssh key saving")
1006 log.exception("Exception during ssh key saving")
974 h.flash(_('An error occurred during ssh key saving: {}').format(e),
1007 h.flash(_('An error occurred during ssh key saving: {}').format(e),
975 category='error')
1008 category='error')
976
1009
977 return HTTPFound(
1010 return HTTPFound(
978 h.route_path('edit_user_ssh_keys', user_id=user_id))
1011 h.route_path('edit_user_ssh_keys', user_id=user_id))
979
1012
980 @LoginRequired()
1013 @LoginRequired()
981 @HasPermissionAllDecorator('hg.admin')
1014 @HasPermissionAllDecorator('hg.admin')
982 @CSRFRequired()
1015 @CSRFRequired()
983 @view_config(
1016 @view_config(
984 route_name='edit_user_ssh_keys_delete', request_method='POST')
1017 route_name='edit_user_ssh_keys_delete', request_method='POST')
985 def ssh_keys_delete(self):
1018 def ssh_keys_delete(self):
986 _ = self.request.translate
1019 _ = self.request.translate
987 c = self.load_default_context()
1020 c = self.load_default_context()
988
1021
989 user_id = self.db_user_id
1022 user_id = self.db_user_id
990 c.user = self.db_user
1023 c.user = self.db_user
991
1024
992 user_data = c.user.get_api_data()
1025 user_data = c.user.get_api_data()
993
1026
994 del_ssh_key = self.request.POST.get('del_ssh_key')
1027 del_ssh_key = self.request.POST.get('del_ssh_key')
995
1028
996 if del_ssh_key:
1029 if del_ssh_key:
997 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
1030 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
998 ssh_key_data = ssh_key.get_api_data()
1031 ssh_key_data = ssh_key.get_api_data()
999
1032
1000 SshKeyModel().delete(del_ssh_key, c.user.user_id)
1033 SshKeyModel().delete(del_ssh_key, c.user.user_id)
1001 audit_logger.store_web(
1034 audit_logger.store_web(
1002 'user.edit.ssh_key.delete', action_data={
1035 'user.edit.ssh_key.delete', action_data={
1003 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
1036 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
1004 user=self._rhodecode_user,)
1037 user=self._rhodecode_user,)
1005 Session().commit()
1038 Session().commit()
1006 # Trigger an event on change of keys.
1039 # Trigger an event on change of keys.
1007 trigger(SshKeyFileChangeEvent(), self.request.registry)
1040 trigger(SshKeyFileChangeEvent(), self.request.registry)
1008 h.flash(_("Ssh key successfully deleted"), category='success')
1041 h.flash(_("Ssh key successfully deleted"), category='success')
1009
1042
1010 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
1043 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
1011
1044
1012 @LoginRequired()
1045 @LoginRequired()
1013 @HasPermissionAllDecorator('hg.admin')
1046 @HasPermissionAllDecorator('hg.admin')
1014 @view_config(
1047 @view_config(
1015 route_name='edit_user_emails', request_method='GET',
1048 route_name='edit_user_emails', request_method='GET',
1016 renderer='rhodecode:templates/admin/users/user_edit.mako')
1049 renderer='rhodecode:templates/admin/users/user_edit.mako')
1017 def emails(self):
1050 def emails(self):
1018 _ = self.request.translate
1051 _ = self.request.translate
1019 c = self.load_default_context()
1052 c = self.load_default_context()
1020 c.user = self.db_user
1053 c.user = self.db_user
1021
1054
1022 c.active = 'emails'
1055 c.active = 'emails'
1023 c.user_email_map = UserEmailMap.query() \
1056 c.user_email_map = UserEmailMap.query() \
1024 .filter(UserEmailMap.user == c.user).all()
1057 .filter(UserEmailMap.user == c.user).all()
1025
1058
1026 return self._get_template_context(c)
1059 return self._get_template_context(c)
1027
1060
1028 @LoginRequired()
1061 @LoginRequired()
1029 @HasPermissionAllDecorator('hg.admin')
1062 @HasPermissionAllDecorator('hg.admin')
1030 @CSRFRequired()
1063 @CSRFRequired()
1031 @view_config(
1064 @view_config(
1032 route_name='edit_user_emails_add', request_method='POST')
1065 route_name='edit_user_emails_add', request_method='POST')
1033 def emails_add(self):
1066 def emails_add(self):
1034 _ = self.request.translate
1067 _ = self.request.translate
1035 c = self.load_default_context()
1068 c = self.load_default_context()
1036
1069
1037 user_id = self.db_user_id
1070 user_id = self.db_user_id
1038 c.user = self.db_user
1071 c.user = self.db_user
1039
1072
1040 email = self.request.POST.get('new_email')
1073 email = self.request.POST.get('new_email')
1041 user_data = c.user.get_api_data()
1074 user_data = c.user.get_api_data()
1042 try:
1075 try:
1043
1076
1044 form = UserExtraEmailForm(self.request.translate)()
1077 form = UserExtraEmailForm(self.request.translate)()
1045 data = form.to_python({'email': email})
1078 data = form.to_python({'email': email})
1046 email = data['email']
1079 email = data['email']
1047
1080
1048 UserModel().add_extra_email(c.user.user_id, email)
1081 UserModel().add_extra_email(c.user.user_id, email)
1049 audit_logger.store_web(
1082 audit_logger.store_web(
1050 'user.edit.email.add',
1083 'user.edit.email.add',
1051 action_data={'email': email, 'user': user_data},
1084 action_data={'email': email, 'user': user_data},
1052 user=self._rhodecode_user)
1085 user=self._rhodecode_user)
1053 Session().commit()
1086 Session().commit()
1054 h.flash(_("Added new email address `%s` for user account") % email,
1087 h.flash(_("Added new email address `%s` for user account") % email,
1055 category='success')
1088 category='success')
1056 except formencode.Invalid as error:
1089 except formencode.Invalid as error:
1057 h.flash(h.escape(error.error_dict['email']), category='error')
1090 h.flash(h.escape(error.error_dict['email']), category='error')
1058 except IntegrityError:
1091 except IntegrityError:
1059 log.warning("Email %s already exists", email)
1092 log.warning("Email %s already exists", email)
1060 h.flash(_('Email `{}` is already registered for another user.').format(email),
1093 h.flash(_('Email `{}` is already registered for another user.').format(email),
1061 category='error')
1094 category='error')
1062 except Exception:
1095 except Exception:
1063 log.exception("Exception during email saving")
1096 log.exception("Exception during email saving")
1064 h.flash(_('An error occurred during email saving'),
1097 h.flash(_('An error occurred during email saving'),
1065 category='error')
1098 category='error')
1066 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1099 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1067
1100
1068 @LoginRequired()
1101 @LoginRequired()
1069 @HasPermissionAllDecorator('hg.admin')
1102 @HasPermissionAllDecorator('hg.admin')
1070 @CSRFRequired()
1103 @CSRFRequired()
1071 @view_config(
1104 @view_config(
1072 route_name='edit_user_emails_delete', request_method='POST')
1105 route_name='edit_user_emails_delete', request_method='POST')
1073 def emails_delete(self):
1106 def emails_delete(self):
1074 _ = self.request.translate
1107 _ = self.request.translate
1075 c = self.load_default_context()
1108 c = self.load_default_context()
1076
1109
1077 user_id = self.db_user_id
1110 user_id = self.db_user_id
1078 c.user = self.db_user
1111 c.user = self.db_user
1079
1112
1080 email_id = self.request.POST.get('del_email_id')
1113 email_id = self.request.POST.get('del_email_id')
1081 user_model = UserModel()
1114 user_model = UserModel()
1082
1115
1083 email = UserEmailMap.query().get(email_id).email
1116 email = UserEmailMap.query().get(email_id).email
1084 user_data = c.user.get_api_data()
1117 user_data = c.user.get_api_data()
1085 user_model.delete_extra_email(c.user.user_id, email_id)
1118 user_model.delete_extra_email(c.user.user_id, email_id)
1086 audit_logger.store_web(
1119 audit_logger.store_web(
1087 'user.edit.email.delete',
1120 'user.edit.email.delete',
1088 action_data={'email': email, 'user': user_data},
1121 action_data={'email': email, 'user': user_data},
1089 user=self._rhodecode_user)
1122 user=self._rhodecode_user)
1090 Session().commit()
1123 Session().commit()
1091 h.flash(_("Removed email address from user account"),
1124 h.flash(_("Removed email address from user account"),
1092 category='success')
1125 category='success')
1093 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1126 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1094
1127
1095 @LoginRequired()
1128 @LoginRequired()
1096 @HasPermissionAllDecorator('hg.admin')
1129 @HasPermissionAllDecorator('hg.admin')
1097 @view_config(
1130 @view_config(
1098 route_name='edit_user_ips', request_method='GET',
1131 route_name='edit_user_ips', request_method='GET',
1099 renderer='rhodecode:templates/admin/users/user_edit.mako')
1132 renderer='rhodecode:templates/admin/users/user_edit.mako')
1100 def ips(self):
1133 def ips(self):
1101 _ = self.request.translate
1134 _ = self.request.translate
1102 c = self.load_default_context()
1135 c = self.load_default_context()
1103 c.user = self.db_user
1136 c.user = self.db_user
1104
1137
1105 c.active = 'ips'
1138 c.active = 'ips'
1106 c.user_ip_map = UserIpMap.query() \
1139 c.user_ip_map = UserIpMap.query() \
1107 .filter(UserIpMap.user == c.user).all()
1140 .filter(UserIpMap.user == c.user).all()
1108
1141
1109 c.inherit_default_ips = c.user.inherit_default_permissions
1142 c.inherit_default_ips = c.user.inherit_default_permissions
1110 c.default_user_ip_map = UserIpMap.query() \
1143 c.default_user_ip_map = UserIpMap.query() \
1111 .filter(UserIpMap.user == User.get_default_user()).all()
1144 .filter(UserIpMap.user == User.get_default_user()).all()
1112
1145
1113 return self._get_template_context(c)
1146 return self._get_template_context(c)
1114
1147
1115 @LoginRequired()
1148 @LoginRequired()
1116 @HasPermissionAllDecorator('hg.admin')
1149 @HasPermissionAllDecorator('hg.admin')
1117 @CSRFRequired()
1150 @CSRFRequired()
1118 @view_config(
1151 @view_config(
1119 route_name='edit_user_ips_add', request_method='POST')
1152 route_name='edit_user_ips_add', request_method='POST')
1120 # NOTE(marcink): this view is allowed for default users, as we can
1153 # NOTE(marcink): this view is allowed for default users, as we can
1121 # edit their IP white list
1154 # edit their IP white list
1122 def ips_add(self):
1155 def ips_add(self):
1123 _ = self.request.translate
1156 _ = self.request.translate
1124 c = self.load_default_context()
1157 c = self.load_default_context()
1125
1158
1126 user_id = self.db_user_id
1159 user_id = self.db_user_id
1127 c.user = self.db_user
1160 c.user = self.db_user
1128
1161
1129 user_model = UserModel()
1162 user_model = UserModel()
1130 desc = self.request.POST.get('description')
1163 desc = self.request.POST.get('description')
1131 try:
1164 try:
1132 ip_list = user_model.parse_ip_range(
1165 ip_list = user_model.parse_ip_range(
1133 self.request.POST.get('new_ip'))
1166 self.request.POST.get('new_ip'))
1134 except Exception as e:
1167 except Exception as e:
1135 ip_list = []
1168 ip_list = []
1136 log.exception("Exception during ip saving")
1169 log.exception("Exception during ip saving")
1137 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1170 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1138 category='error')
1171 category='error')
1139 added = []
1172 added = []
1140 user_data = c.user.get_api_data()
1173 user_data = c.user.get_api_data()
1141 for ip in ip_list:
1174 for ip in ip_list:
1142 try:
1175 try:
1143 form = UserExtraIpForm(self.request.translate)()
1176 form = UserExtraIpForm(self.request.translate)()
1144 data = form.to_python({'ip': ip})
1177 data = form.to_python({'ip': ip})
1145 ip = data['ip']
1178 ip = data['ip']
1146
1179
1147 user_model.add_extra_ip(c.user.user_id, ip, desc)
1180 user_model.add_extra_ip(c.user.user_id, ip, desc)
1148 audit_logger.store_web(
1181 audit_logger.store_web(
1149 'user.edit.ip.add',
1182 'user.edit.ip.add',
1150 action_data={'ip': ip, 'user': user_data},
1183 action_data={'ip': ip, 'user': user_data},
1151 user=self._rhodecode_user)
1184 user=self._rhodecode_user)
1152 Session().commit()
1185 Session().commit()
1153 added.append(ip)
1186 added.append(ip)
1154 except formencode.Invalid as error:
1187 except formencode.Invalid as error:
1155 msg = error.error_dict['ip']
1188 msg = error.error_dict['ip']
1156 h.flash(msg, category='error')
1189 h.flash(msg, category='error')
1157 except Exception:
1190 except Exception:
1158 log.exception("Exception during ip saving")
1191 log.exception("Exception during ip saving")
1159 h.flash(_('An error occurred during ip saving'),
1192 h.flash(_('An error occurred during ip saving'),
1160 category='error')
1193 category='error')
1161 if added:
1194 if added:
1162 h.flash(
1195 h.flash(
1163 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1196 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1164 category='success')
1197 category='success')
1165 if 'default_user' in self.request.POST:
1198 if 'default_user' in self.request.POST:
1166 # case for editing global IP list we do it for 'DEFAULT' user
1199 # case for editing global IP list we do it for 'DEFAULT' user
1167 raise HTTPFound(h.route_path('admin_permissions_ips'))
1200 raise HTTPFound(h.route_path('admin_permissions_ips'))
1168 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1201 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1169
1202
1170 @LoginRequired()
1203 @LoginRequired()
1171 @HasPermissionAllDecorator('hg.admin')
1204 @HasPermissionAllDecorator('hg.admin')
1172 @CSRFRequired()
1205 @CSRFRequired()
1173 @view_config(
1206 @view_config(
1174 route_name='edit_user_ips_delete', request_method='POST')
1207 route_name='edit_user_ips_delete', request_method='POST')
1175 # NOTE(marcink): this view is allowed for default users, as we can
1208 # NOTE(marcink): this view is allowed for default users, as we can
1176 # edit their IP white list
1209 # edit their IP white list
1177 def ips_delete(self):
1210 def ips_delete(self):
1178 _ = self.request.translate
1211 _ = self.request.translate
1179 c = self.load_default_context()
1212 c = self.load_default_context()
1180
1213
1181 user_id = self.db_user_id
1214 user_id = self.db_user_id
1182 c.user = self.db_user
1215 c.user = self.db_user
1183
1216
1184 ip_id = self.request.POST.get('del_ip_id')
1217 ip_id = self.request.POST.get('del_ip_id')
1185 user_model = UserModel()
1218 user_model = UserModel()
1186 user_data = c.user.get_api_data()
1219 user_data = c.user.get_api_data()
1187 ip = UserIpMap.query().get(ip_id).ip_addr
1220 ip = UserIpMap.query().get(ip_id).ip_addr
1188 user_model.delete_extra_ip(c.user.user_id, ip_id)
1221 user_model.delete_extra_ip(c.user.user_id, ip_id)
1189 audit_logger.store_web(
1222 audit_logger.store_web(
1190 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1223 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1191 user=self._rhodecode_user)
1224 user=self._rhodecode_user)
1192 Session().commit()
1225 Session().commit()
1193 h.flash(_("Removed ip address from user whitelist"), category='success')
1226 h.flash(_("Removed ip address from user whitelist"), category='success')
1194
1227
1195 if 'default_user' in self.request.POST:
1228 if 'default_user' in self.request.POST:
1196 # case for editing global IP list we do it for 'DEFAULT' user
1229 # case for editing global IP list we do it for 'DEFAULT' user
1197 raise HTTPFound(h.route_path('admin_permissions_ips'))
1230 raise HTTPFound(h.route_path('admin_permissions_ips'))
1198 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1231 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1199
1232
1200 @LoginRequired()
1233 @LoginRequired()
1201 @HasPermissionAllDecorator('hg.admin')
1234 @HasPermissionAllDecorator('hg.admin')
1202 @view_config(
1235 @view_config(
1203 route_name='edit_user_groups_management', request_method='GET',
1236 route_name='edit_user_groups_management', request_method='GET',
1204 renderer='rhodecode:templates/admin/users/user_edit.mako')
1237 renderer='rhodecode:templates/admin/users/user_edit.mako')
1205 def groups_management(self):
1238 def groups_management(self):
1206 c = self.load_default_context()
1239 c = self.load_default_context()
1207 c.user = self.db_user
1240 c.user = self.db_user
1208 c.data = c.user.group_member
1241 c.data = c.user.group_member
1209
1242
1210 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1243 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1211 for group in c.user.group_member]
1244 for group in c.user.group_member]
1212 c.groups = json.dumps(groups)
1245 c.groups = json.dumps(groups)
1213 c.active = 'groups'
1246 c.active = 'groups'
1214
1247
1215 return self._get_template_context(c)
1248 return self._get_template_context(c)
1216
1249
1217 @LoginRequired()
1250 @LoginRequired()
1218 @HasPermissionAllDecorator('hg.admin')
1251 @HasPermissionAllDecorator('hg.admin')
1219 @CSRFRequired()
1252 @CSRFRequired()
1220 @view_config(
1253 @view_config(
1221 route_name='edit_user_groups_management_updates', request_method='POST')
1254 route_name='edit_user_groups_management_updates', request_method='POST')
1222 def groups_management_updates(self):
1255 def groups_management_updates(self):
1223 _ = self.request.translate
1256 _ = self.request.translate
1224 c = self.load_default_context()
1257 c = self.load_default_context()
1225
1258
1226 user_id = self.db_user_id
1259 user_id = self.db_user_id
1227 c.user = self.db_user
1260 c.user = self.db_user
1228
1261
1229 user_groups = set(self.request.POST.getall('users_group_id'))
1262 user_groups = set(self.request.POST.getall('users_group_id'))
1230 user_groups_objects = []
1263 user_groups_objects = []
1231
1264
1232 for ugid in user_groups:
1265 for ugid in user_groups:
1233 user_groups_objects.append(
1266 user_groups_objects.append(
1234 UserGroupModel().get_group(safe_int(ugid)))
1267 UserGroupModel().get_group(safe_int(ugid)))
1235 user_group_model = UserGroupModel()
1268 user_group_model = UserGroupModel()
1236 added_to_groups, removed_from_groups = \
1269 added_to_groups, removed_from_groups = \
1237 user_group_model.change_groups(c.user, user_groups_objects)
1270 user_group_model.change_groups(c.user, user_groups_objects)
1238
1271
1239 user_data = c.user.get_api_data()
1272 user_data = c.user.get_api_data()
1240 for user_group_id in added_to_groups:
1273 for user_group_id in added_to_groups:
1241 user_group = UserGroup.get(user_group_id)
1274 user_group = UserGroup.get(user_group_id)
1242 old_values = user_group.get_api_data()
1275 old_values = user_group.get_api_data()
1243 audit_logger.store_web(
1276 audit_logger.store_web(
1244 'user_group.edit.member.add',
1277 'user_group.edit.member.add',
1245 action_data={'user': user_data, 'old_data': old_values},
1278 action_data={'user': user_data, 'old_data': old_values},
1246 user=self._rhodecode_user)
1279 user=self._rhodecode_user)
1247
1280
1248 for user_group_id in removed_from_groups:
1281 for user_group_id in removed_from_groups:
1249 user_group = UserGroup.get(user_group_id)
1282 user_group = UserGroup.get(user_group_id)
1250 old_values = user_group.get_api_data()
1283 old_values = user_group.get_api_data()
1251 audit_logger.store_web(
1284 audit_logger.store_web(
1252 'user_group.edit.member.delete',
1285 'user_group.edit.member.delete',
1253 action_data={'user': user_data, 'old_data': old_values},
1286 action_data={'user': user_data, 'old_data': old_values},
1254 user=self._rhodecode_user)
1287 user=self._rhodecode_user)
1255
1288
1256 Session().commit()
1289 Session().commit()
1257 c.active = 'user_groups_management'
1290 c.active = 'user_groups_management'
1258 h.flash(_("Groups successfully changed"), category='success')
1291 h.flash(_("Groups successfully changed"), category='success')
1259
1292
1260 return HTTPFound(h.route_path(
1293 return HTTPFound(h.route_path(
1261 'edit_user_groups_management', user_id=user_id))
1294 'edit_user_groups_management', user_id=user_id))
1262
1295
1263 @LoginRequired()
1296 @LoginRequired()
1264 @HasPermissionAllDecorator('hg.admin')
1297 @HasPermissionAllDecorator('hg.admin')
1265 @view_config(
1298 @view_config(
1266 route_name='edit_user_audit_logs', request_method='GET',
1299 route_name='edit_user_audit_logs', request_method='GET',
1267 renderer='rhodecode:templates/admin/users/user_edit.mako')
1300 renderer='rhodecode:templates/admin/users/user_edit.mako')
1268 def user_audit_logs(self):
1301 def user_audit_logs(self):
1269 _ = self.request.translate
1302 _ = self.request.translate
1270 c = self.load_default_context()
1303 c = self.load_default_context()
1271 c.user = self.db_user
1304 c.user = self.db_user
1272
1305
1273 c.active = 'audit'
1306 c.active = 'audit'
1274
1307
1275 p = safe_int(self.request.GET.get('page', 1), 1)
1308 p = safe_int(self.request.GET.get('page', 1), 1)
1276
1309
1277 filter_term = self.request.GET.get('filter')
1310 filter_term = self.request.GET.get('filter')
1278 user_log = UserModel().get_user_log(c.user, filter_term)
1311 user_log = UserModel().get_user_log(c.user, filter_term)
1279
1312
1280 def url_generator(page_num):
1313 def url_generator(page_num):
1281 query_params = {
1314 query_params = {
1282 'page': page_num
1315 'page': page_num
1283 }
1316 }
1284 if filter_term:
1317 if filter_term:
1285 query_params['filter'] = filter_term
1318 query_params['filter'] = filter_term
1286 return self.request.current_route_path(_query=query_params)
1319 return self.request.current_route_path(_query=query_params)
1287
1320
1288 c.audit_logs = SqlPage(
1321 c.audit_logs = SqlPage(
1289 user_log, page=p, items_per_page=10, url_maker=url_generator)
1322 user_log, page=p, items_per_page=10, url_maker=url_generator)
1290 c.filter_term = filter_term
1323 c.filter_term = filter_term
1291 return self._get_template_context(c)
1324 return self._get_template_context(c)
1292
1325
1293 @LoginRequired()
1326 @LoginRequired()
1294 @HasPermissionAllDecorator('hg.admin')
1327 @HasPermissionAllDecorator('hg.admin')
1295 @view_config(
1328 @view_config(
1296 route_name='edit_user_audit_logs_download', request_method='GET',
1329 route_name='edit_user_audit_logs_download', request_method='GET',
1297 renderer='string')
1330 renderer='string')
1298 def user_audit_logs_download(self):
1331 def user_audit_logs_download(self):
1299 _ = self.request.translate
1332 _ = self.request.translate
1300 c = self.load_default_context()
1333 c = self.load_default_context()
1301 c.user = self.db_user
1334 c.user = self.db_user
1302
1335
1303 user_log = UserModel().get_user_log(c.user, filter_term=None)
1336 user_log = UserModel().get_user_log(c.user, filter_term=None)
1304
1337
1305 audit_log_data = {}
1338 audit_log_data = {}
1306 for entry in user_log:
1339 for entry in user_log:
1307 audit_log_data[entry.user_log_id] = entry.get_dict()
1340 audit_log_data[entry.user_log_id] = entry.get_dict()
1308
1341
1309 response = Response(json.dumps(audit_log_data, indent=4))
1342 response = Response(json.dumps(audit_log_data, indent=4))
1310 response.content_disposition = str(
1343 response.content_disposition = str(
1311 'attachment; filename=%s' % 'user_{}_audit_logs.json'.format(c.user.user_id))
1344 'attachment; filename=%s' % 'user_{}_audit_logs.json'.format(c.user.user_id))
1312 response.content_type = 'application/json'
1345 response.content_type = 'application/json'
1313
1346
1314 return response
1347 return response
1315
1348
1316 @LoginRequired()
1349 @LoginRequired()
1317 @HasPermissionAllDecorator('hg.admin')
1350 @HasPermissionAllDecorator('hg.admin')
1318 @view_config(
1351 @view_config(
1319 route_name='edit_user_perms_summary', request_method='GET',
1352 route_name='edit_user_perms_summary', request_method='GET',
1320 renderer='rhodecode:templates/admin/users/user_edit.mako')
1353 renderer='rhodecode:templates/admin/users/user_edit.mako')
1321 def user_perms_summary(self):
1354 def user_perms_summary(self):
1322 _ = self.request.translate
1355 _ = self.request.translate
1323 c = self.load_default_context()
1356 c = self.load_default_context()
1324 c.user = self.db_user
1357 c.user = self.db_user
1325
1358
1326 c.active = 'perms_summary'
1359 c.active = 'perms_summary'
1327 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1360 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1328
1361
1329 return self._get_template_context(c)
1362 return self._get_template_context(c)
1330
1363
1331 @LoginRequired()
1364 @LoginRequired()
1332 @HasPermissionAllDecorator('hg.admin')
1365 @HasPermissionAllDecorator('hg.admin')
1333 @view_config(
1366 @view_config(
1334 route_name='edit_user_perms_summary_json', request_method='GET',
1367 route_name='edit_user_perms_summary_json', request_method='GET',
1335 renderer='json_ext')
1368 renderer='json_ext')
1336 def user_perms_summary_json(self):
1369 def user_perms_summary_json(self):
1337 self.load_default_context()
1370 self.load_default_context()
1338 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1371 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1339
1372
1340 return perm_user.permissions
1373 return perm_user.permissions
1341
1374
1342 @LoginRequired()
1375 @LoginRequired()
1343 @HasPermissionAllDecorator('hg.admin')
1376 @HasPermissionAllDecorator('hg.admin')
1344 @view_config(
1377 @view_config(
1345 route_name='edit_user_caches', request_method='GET',
1378 route_name='edit_user_caches', request_method='GET',
1346 renderer='rhodecode:templates/admin/users/user_edit.mako')
1379 renderer='rhodecode:templates/admin/users/user_edit.mako')
1347 def user_caches(self):
1380 def user_caches(self):
1348 _ = self.request.translate
1381 _ = self.request.translate
1349 c = self.load_default_context()
1382 c = self.load_default_context()
1350 c.user = self.db_user
1383 c.user = self.db_user
1351
1384
1352 c.active = 'caches'
1385 c.active = 'caches'
1353 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1386 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1354
1387
1355 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1388 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1356 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1389 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1357 c.backend = c.region.backend
1390 c.backend = c.region.backend
1358 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1391 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1359
1392
1360 return self._get_template_context(c)
1393 return self._get_template_context(c)
1361
1394
1362 @LoginRequired()
1395 @LoginRequired()
1363 @HasPermissionAllDecorator('hg.admin')
1396 @HasPermissionAllDecorator('hg.admin')
1364 @CSRFRequired()
1397 @CSRFRequired()
1365 @view_config(
1398 @view_config(
1366 route_name='edit_user_caches_update', request_method='POST')
1399 route_name='edit_user_caches_update', request_method='POST')
1367 def user_caches_update(self):
1400 def user_caches_update(self):
1368 _ = self.request.translate
1401 _ = self.request.translate
1369 c = self.load_default_context()
1402 c = self.load_default_context()
1370 c.user = self.db_user
1403 c.user = self.db_user
1371
1404
1372 c.active = 'caches'
1405 c.active = 'caches'
1373 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1406 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1374
1407
1375 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1408 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1376 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1409 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1377
1410
1378 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1411 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1379
1412
1380 return HTTPFound(h.route_path(
1413 return HTTPFound(h.route_path(
1381 'edit_user_caches', user_id=c.user.user_id))
1414 'edit_user_caches', user_id=c.user.user_id))
@@ -1,175 +1,179 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 Set of custom exceptions used in RhodeCode
22 Set of custom exceptions used in RhodeCode
23 """
23 """
24
24
25 from webob.exc import HTTPClientError
25 from webob.exc import HTTPClientError
26 from pyramid.httpexceptions import HTTPBadGateway
26 from pyramid.httpexceptions import HTTPBadGateway
27
27
28
28
29 class LdapUsernameError(Exception):
29 class LdapUsernameError(Exception):
30 pass
30 pass
31
31
32
32
33 class LdapPasswordError(Exception):
33 class LdapPasswordError(Exception):
34 pass
34 pass
35
35
36
36
37 class LdapConnectionError(Exception):
37 class LdapConnectionError(Exception):
38 pass
38 pass
39
39
40
40
41 class LdapImportError(Exception):
41 class LdapImportError(Exception):
42 pass
42 pass
43
43
44
44
45 class DefaultUserException(Exception):
45 class DefaultUserException(Exception):
46 pass
46 pass
47
47
48
48
49 class UserOwnsReposException(Exception):
49 class UserOwnsReposException(Exception):
50 pass
50 pass
51
51
52
52
53 class UserOwnsRepoGroupsException(Exception):
53 class UserOwnsRepoGroupsException(Exception):
54 pass
54 pass
55
55
56
56
57 class UserOwnsUserGroupsException(Exception):
57 class UserOwnsUserGroupsException(Exception):
58 pass
58 pass
59
59
60
60
61 class UserOwnsPullRequestsException(Exception):
62 pass
63
64
61 class UserOwnsArtifactsException(Exception):
65 class UserOwnsArtifactsException(Exception):
62 pass
66 pass
63
67
64
68
65 class UserGroupAssignedException(Exception):
69 class UserGroupAssignedException(Exception):
66 pass
70 pass
67
71
68
72
69 class StatusChangeOnClosedPullRequestError(Exception):
73 class StatusChangeOnClosedPullRequestError(Exception):
70 pass
74 pass
71
75
72
76
73 class AttachedForksError(Exception):
77 class AttachedForksError(Exception):
74 pass
78 pass
75
79
76
80
77 class AttachedPullRequestsError(Exception):
81 class AttachedPullRequestsError(Exception):
78 pass
82 pass
79
83
80
84
81 class RepoGroupAssignmentError(Exception):
85 class RepoGroupAssignmentError(Exception):
82 pass
86 pass
83
87
84
88
85 class NonRelativePathError(Exception):
89 class NonRelativePathError(Exception):
86 pass
90 pass
87
91
88
92
89 class HTTPRequirementError(HTTPClientError):
93 class HTTPRequirementError(HTTPClientError):
90 title = explanation = 'Repository Requirement Missing'
94 title = explanation = 'Repository Requirement Missing'
91 reason = None
95 reason = None
92
96
93 def __init__(self, message, *args, **kwargs):
97 def __init__(self, message, *args, **kwargs):
94 self.title = self.explanation = message
98 self.title = self.explanation = message
95 super(HTTPRequirementError, self).__init__(*args, **kwargs)
99 super(HTTPRequirementError, self).__init__(*args, **kwargs)
96 self.args = (message, )
100 self.args = (message, )
97
101
98
102
99 class HTTPLockedRC(HTTPClientError):
103 class HTTPLockedRC(HTTPClientError):
100 """
104 """
101 Special Exception For locked Repos in RhodeCode, the return code can
105 Special Exception For locked Repos in RhodeCode, the return code can
102 be overwritten by _code keyword argument passed into constructors
106 be overwritten by _code keyword argument passed into constructors
103 """
107 """
104 code = 423
108 code = 423
105 title = explanation = 'Repository Locked'
109 title = explanation = 'Repository Locked'
106 reason = None
110 reason = None
107
111
108 def __init__(self, message, *args, **kwargs):
112 def __init__(self, message, *args, **kwargs):
109 from rhodecode import CONFIG
113 from rhodecode import CONFIG
110 from rhodecode.lib.utils2 import safe_int
114 from rhodecode.lib.utils2 import safe_int
111 _code = CONFIG.get('lock_ret_code')
115 _code = CONFIG.get('lock_ret_code')
112 self.code = safe_int(_code, self.code)
116 self.code = safe_int(_code, self.code)
113 self.title = self.explanation = message
117 self.title = self.explanation = message
114 super(HTTPLockedRC, self).__init__(*args, **kwargs)
118 super(HTTPLockedRC, self).__init__(*args, **kwargs)
115 self.args = (message, )
119 self.args = (message, )
116
120
117
121
118 class HTTPBranchProtected(HTTPClientError):
122 class HTTPBranchProtected(HTTPClientError):
119 """
123 """
120 Special Exception For Indicating that branch is protected in RhodeCode, the
124 Special Exception For Indicating that branch is protected in RhodeCode, the
121 return code can be overwritten by _code keyword argument passed into constructors
125 return code can be overwritten by _code keyword argument passed into constructors
122 """
126 """
123 code = 403
127 code = 403
124 title = explanation = 'Branch Protected'
128 title = explanation = 'Branch Protected'
125 reason = None
129 reason = None
126
130
127 def __init__(self, message, *args, **kwargs):
131 def __init__(self, message, *args, **kwargs):
128 self.title = self.explanation = message
132 self.title = self.explanation = message
129 super(HTTPBranchProtected, self).__init__(*args, **kwargs)
133 super(HTTPBranchProtected, self).__init__(*args, **kwargs)
130 self.args = (message, )
134 self.args = (message, )
131
135
132
136
133 class IMCCommitError(Exception):
137 class IMCCommitError(Exception):
134 pass
138 pass
135
139
136
140
137 class UserCreationError(Exception):
141 class UserCreationError(Exception):
138 pass
142 pass
139
143
140
144
141 class NotAllowedToCreateUserError(Exception):
145 class NotAllowedToCreateUserError(Exception):
142 pass
146 pass
143
147
144
148
145 class RepositoryCreationError(Exception):
149 class RepositoryCreationError(Exception):
146 pass
150 pass
147
151
148
152
149 class VCSServerUnavailable(HTTPBadGateway):
153 class VCSServerUnavailable(HTTPBadGateway):
150 """ HTTP Exception class for VCS Server errors """
154 """ HTTP Exception class for VCS Server errors """
151 code = 502
155 code = 502
152 title = 'VCS Server Error'
156 title = 'VCS Server Error'
153 causes = [
157 causes = [
154 'VCS Server is not running',
158 'VCS Server is not running',
155 'Incorrect vcs.server=host:port',
159 'Incorrect vcs.server=host:port',
156 'Incorrect vcs.server.protocol',
160 'Incorrect vcs.server.protocol',
157 ]
161 ]
158
162
159 def __init__(self, message=''):
163 def __init__(self, message=''):
160 self.explanation = 'Could not connect to VCS Server'
164 self.explanation = 'Could not connect to VCS Server'
161 if message:
165 if message:
162 self.explanation += ': ' + message
166 self.explanation += ': ' + message
163 super(VCSServerUnavailable, self).__init__()
167 super(VCSServerUnavailable, self).__init__()
164
168
165
169
166 class ArtifactMetadataDuplicate(ValueError):
170 class ArtifactMetadataDuplicate(ValueError):
167
171
168 def __init__(self, *args, **kwargs):
172 def __init__(self, *args, **kwargs):
169 self.err_section = kwargs.pop('err_section', None)
173 self.err_section = kwargs.pop('err_section', None)
170 self.err_key = kwargs.pop('err_key', None)
174 self.err_key = kwargs.pop('err_key', None)
171 super(ArtifactMetadataDuplicate, self).__init__(*args, **kwargs)
175 super(ArtifactMetadataDuplicate, self).__init__(*args, **kwargs)
172
176
173
177
174 class ArtifactMetadataBadValueType(ValueError):
178 class ArtifactMetadataBadValueType(ValueError):
175 pass
179 pass
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,2067 +1,2072 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import os
29 import os
30
30
31 import datetime
31 import datetime
32 import urllib
32 import urllib
33 import collections
33 import collections
34
34
35 from pyramid import compat
35 from pyramid import compat
36 from pyramid.threadlocal import get_current_request
36 from pyramid.threadlocal import get_current_request
37
37
38 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.lib.vcs.nodes import FileNode
39 from rhodecode.translation import lazy_ugettext
39 from rhodecode.translation import lazy_ugettext
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
41 from rhodecode.lib import audit_logger
41 from rhodecode.lib import audit_logger
42 from rhodecode.lib.compat import OrderedDict
42 from rhodecode.lib.compat import OrderedDict
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.markup_renderer import (
44 from rhodecode.lib.markup_renderer import (
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
46 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe, AttributeDict, safe_int
46 from rhodecode.lib.utils2 import (
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
48 get_current_rhodecode_user)
47 from rhodecode.lib.vcs.backends.base import (
49 from rhodecode.lib.vcs.backends.base import (
48 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
49 TargetRefMissing, SourceRefMissing)
51 TargetRefMissing, SourceRefMissing)
50 from rhodecode.lib.vcs.conf import settings as vcs_settings
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
51 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
52 CommitDoesNotExistError, EmptyRepositoryError)
54 CommitDoesNotExistError, EmptyRepositoryError)
53 from rhodecode.model import BaseModel
55 from rhodecode.model import BaseModel
54 from rhodecode.model.changeset_status import ChangesetStatusModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
55 from rhodecode.model.comment import CommentsModel
57 from rhodecode.model.comment import CommentsModel
56 from rhodecode.model.db import (
58 from rhodecode.model.db import (
57 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
58 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
59 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
60 from rhodecode.model.notification import NotificationModel, \
62 from rhodecode.model.notification import NotificationModel, \
61 EmailNotificationModel
63 EmailNotificationModel
62 from rhodecode.model.scm import ScmModel
64 from rhodecode.model.scm import ScmModel
63 from rhodecode.model.settings import VcsSettingsModel
65 from rhodecode.model.settings import VcsSettingsModel
64
66
65
67
66 log = logging.getLogger(__name__)
68 log = logging.getLogger(__name__)
67
69
68
70
69 # Data structure to hold the response data when updating commits during a pull
71 # Data structure to hold the response data when updating commits during a pull
70 # request update.
72 # request update.
71 class UpdateResponse(object):
73 class UpdateResponse(object):
72
74
73 def __init__(self, executed, reason, new, old, common_ancestor_id,
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
74 commit_changes, source_changed, target_changed):
76 commit_changes, source_changed, target_changed):
75
77
76 self.executed = executed
78 self.executed = executed
77 self.reason = reason
79 self.reason = reason
78 self.new = new
80 self.new = new
79 self.old = old
81 self.old = old
80 self.common_ancestor_id = common_ancestor_id
82 self.common_ancestor_id = common_ancestor_id
81 self.changes = commit_changes
83 self.changes = commit_changes
82 self.source_changed = source_changed
84 self.source_changed = source_changed
83 self.target_changed = target_changed
85 self.target_changed = target_changed
84
86
85
87
86 def get_diff_info(
88 def get_diff_info(
87 source_repo, source_ref, target_repo, target_ref, get_authors=False,
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
88 get_commit_authors=True):
90 get_commit_authors=True):
89 """
91 """
90 Calculates detailed diff information for usage in preview of creation of a pull-request.
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
91 This is also used for default reviewers logic
93 This is also used for default reviewers logic
92 """
94 """
93
95
94 source_scm = source_repo.scm_instance()
96 source_scm = source_repo.scm_instance()
95 target_scm = target_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
96
98
97 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
98 if not ancestor_id:
100 if not ancestor_id:
99 raise ValueError(
101 raise ValueError(
100 'cannot calculate diff info without a common ancestor. '
102 'cannot calculate diff info without a common ancestor. '
101 'Make sure both repositories are related, and have a common forking commit.')
103 'Make sure both repositories are related, and have a common forking commit.')
102
104
103 # case here is that want a simple diff without incoming commits,
105 # case here is that want a simple diff without incoming commits,
104 # previewing what will be merged based only on commits in the source.
106 # previewing what will be merged based only on commits in the source.
105 log.debug('Using ancestor %s as source_ref instead of %s',
107 log.debug('Using ancestor %s as source_ref instead of %s',
106 ancestor_id, source_ref)
108 ancestor_id, source_ref)
107
109
108 # source of changes now is the common ancestor
110 # source of changes now is the common ancestor
109 source_commit = source_scm.get_commit(commit_id=ancestor_id)
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
110 # target commit becomes the source ref as it is the last commit
112 # target commit becomes the source ref as it is the last commit
111 # for diff generation this logic gives proper diff
113 # for diff generation this logic gives proper diff
112 target_commit = source_scm.get_commit(commit_id=source_ref)
114 target_commit = source_scm.get_commit(commit_id=source_ref)
113
115
114 vcs_diff = \
116 vcs_diff = \
115 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
116 ignore_whitespace=False, context=3)
118 ignore_whitespace=False, context=3)
117
119
118 diff_processor = diffs.DiffProcessor(
120 diff_processor = diffs.DiffProcessor(
119 vcs_diff, format='newdiff', diff_limit=None,
121 vcs_diff, format='newdiff', diff_limit=None,
120 file_limit=None, show_full_diff=True)
122 file_limit=None, show_full_diff=True)
121
123
122 _parsed = diff_processor.prepare()
124 _parsed = diff_processor.prepare()
123
125
124 all_files = []
126 all_files = []
125 all_files_changes = []
127 all_files_changes = []
126 changed_lines = {}
128 changed_lines = {}
127 stats = [0, 0]
129 stats = [0, 0]
128 for f in _parsed:
130 for f in _parsed:
129 all_files.append(f['filename'])
131 all_files.append(f['filename'])
130 all_files_changes.append({
132 all_files_changes.append({
131 'filename': f['filename'],
133 'filename': f['filename'],
132 'stats': f['stats']
134 'stats': f['stats']
133 })
135 })
134 stats[0] += f['stats']['added']
136 stats[0] += f['stats']['added']
135 stats[1] += f['stats']['deleted']
137 stats[1] += f['stats']['deleted']
136
138
137 changed_lines[f['filename']] = []
139 changed_lines[f['filename']] = []
138 if len(f['chunks']) < 2:
140 if len(f['chunks']) < 2:
139 continue
141 continue
140 # first line is "context" information
142 # first line is "context" information
141 for chunks in f['chunks'][1:]:
143 for chunks in f['chunks'][1:]:
142 for chunk in chunks['lines']:
144 for chunk in chunks['lines']:
143 if chunk['action'] not in ('del', 'mod'):
145 if chunk['action'] not in ('del', 'mod'):
144 continue
146 continue
145 changed_lines[f['filename']].append(chunk['old_lineno'])
147 changed_lines[f['filename']].append(chunk['old_lineno'])
146
148
147 commit_authors = []
149 commit_authors = []
148 user_counts = {}
150 user_counts = {}
149 email_counts = {}
151 email_counts = {}
150 author_counts = {}
152 author_counts = {}
151 _commit_cache = {}
153 _commit_cache = {}
152
154
153 commits = []
155 commits = []
154 if get_commit_authors:
156 if get_commit_authors:
155 commits = target_scm.compare(
157 commits = target_scm.compare(
156 target_ref, source_ref, source_scm, merge=True,
158 target_ref, source_ref, source_scm, merge=True,
157 pre_load=["author"])
159 pre_load=["author"])
158
160
159 for commit in commits:
161 for commit in commits:
160 user = User.get_from_cs_author(commit.author)
162 user = User.get_from_cs_author(commit.author)
161 if user and user not in commit_authors:
163 if user and user not in commit_authors:
162 commit_authors.append(user)
164 commit_authors.append(user)
163
165
164 # lines
166 # lines
165 if get_authors:
167 if get_authors:
166 target_commit = source_repo.get_commit(ancestor_id)
168 target_commit = source_repo.get_commit(ancestor_id)
167
169
168 for fname, lines in changed_lines.items():
170 for fname, lines in changed_lines.items():
169 try:
171 try:
170 node = target_commit.get_node(fname)
172 node = target_commit.get_node(fname)
171 except Exception:
173 except Exception:
172 continue
174 continue
173
175
174 if not isinstance(node, FileNode):
176 if not isinstance(node, FileNode):
175 continue
177 continue
176
178
177 for annotation in node.annotate:
179 for annotation in node.annotate:
178 line_no, commit_id, get_commit_func, line_text = annotation
180 line_no, commit_id, get_commit_func, line_text = annotation
179 if line_no in lines:
181 if line_no in lines:
180 if commit_id not in _commit_cache:
182 if commit_id not in _commit_cache:
181 _commit_cache[commit_id] = get_commit_func()
183 _commit_cache[commit_id] = get_commit_func()
182 commit = _commit_cache[commit_id]
184 commit = _commit_cache[commit_id]
183 author = commit.author
185 author = commit.author
184 email = commit.author_email
186 email = commit.author_email
185 user = User.get_from_cs_author(author)
187 user = User.get_from_cs_author(author)
186 if user:
188 if user:
187 user_counts[user] = user_counts.get(user, 0) + 1
189 user_counts[user] = user_counts.get(user, 0) + 1
188 author_counts[author] = author_counts.get(author, 0) + 1
190 author_counts[author] = author_counts.get(author, 0) + 1
189 email_counts[email] = email_counts.get(email, 0) + 1
191 email_counts[email] = email_counts.get(email, 0) + 1
190
192
191 return {
193 return {
192 'commits': commits,
194 'commits': commits,
193 'files': all_files_changes,
195 'files': all_files_changes,
194 'stats': stats,
196 'stats': stats,
195 'ancestor': ancestor_id,
197 'ancestor': ancestor_id,
196 # original authors of modified files
198 # original authors of modified files
197 'original_authors': {
199 'original_authors': {
198 'users': user_counts,
200 'users': user_counts,
199 'authors': author_counts,
201 'authors': author_counts,
200 'emails': email_counts,
202 'emails': email_counts,
201 },
203 },
202 'commit_authors': commit_authors
204 'commit_authors': commit_authors
203 }
205 }
204
206
205
207
206 class PullRequestModel(BaseModel):
208 class PullRequestModel(BaseModel):
207
209
208 cls = PullRequest
210 cls = PullRequest
209
211
210 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
212 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
211
213
212 UPDATE_STATUS_MESSAGES = {
214 UPDATE_STATUS_MESSAGES = {
213 UpdateFailureReason.NONE: lazy_ugettext(
215 UpdateFailureReason.NONE: lazy_ugettext(
214 'Pull request update successful.'),
216 'Pull request update successful.'),
215 UpdateFailureReason.UNKNOWN: lazy_ugettext(
217 UpdateFailureReason.UNKNOWN: lazy_ugettext(
216 'Pull request update failed because of an unknown error.'),
218 'Pull request update failed because of an unknown error.'),
217 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
219 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
218 'No update needed because the source and target have not changed.'),
220 'No update needed because the source and target have not changed.'),
219 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
221 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
220 'Pull request cannot be updated because the reference type is '
222 'Pull request cannot be updated because the reference type is '
221 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
223 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
222 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
224 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
223 'This pull request cannot be updated because the target '
225 'This pull request cannot be updated because the target '
224 'reference is missing.'),
226 'reference is missing.'),
225 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
227 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
226 'This pull request cannot be updated because the source '
228 'This pull request cannot be updated because the source '
227 'reference is missing.'),
229 'reference is missing.'),
228 }
230 }
229 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
231 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
230 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
232 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
231
233
232 def __get_pull_request(self, pull_request):
234 def __get_pull_request(self, pull_request):
233 return self._get_instance((
235 return self._get_instance((
234 PullRequest, PullRequestVersion), pull_request)
236 PullRequest, PullRequestVersion), pull_request)
235
237
236 def _check_perms(self, perms, pull_request, user, api=False):
238 def _check_perms(self, perms, pull_request, user, api=False):
237 if not api:
239 if not api:
238 return h.HasRepoPermissionAny(*perms)(
240 return h.HasRepoPermissionAny(*perms)(
239 user=user, repo_name=pull_request.target_repo.repo_name)
241 user=user, repo_name=pull_request.target_repo.repo_name)
240 else:
242 else:
241 return h.HasRepoPermissionAnyApi(*perms)(
243 return h.HasRepoPermissionAnyApi(*perms)(
242 user=user, repo_name=pull_request.target_repo.repo_name)
244 user=user, repo_name=pull_request.target_repo.repo_name)
243
245
244 def check_user_read(self, pull_request, user, api=False):
246 def check_user_read(self, pull_request, user, api=False):
245 _perms = ('repository.admin', 'repository.write', 'repository.read',)
247 _perms = ('repository.admin', 'repository.write', 'repository.read',)
246 return self._check_perms(_perms, pull_request, user, api)
248 return self._check_perms(_perms, pull_request, user, api)
247
249
248 def check_user_merge(self, pull_request, user, api=False):
250 def check_user_merge(self, pull_request, user, api=False):
249 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
251 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
250 return self._check_perms(_perms, pull_request, user, api)
252 return self._check_perms(_perms, pull_request, user, api)
251
253
252 def check_user_update(self, pull_request, user, api=False):
254 def check_user_update(self, pull_request, user, api=False):
253 owner = user.user_id == pull_request.user_id
255 owner = user.user_id == pull_request.user_id
254 return self.check_user_merge(pull_request, user, api) or owner
256 return self.check_user_merge(pull_request, user, api) or owner
255
257
256 def check_user_delete(self, pull_request, user):
258 def check_user_delete(self, pull_request, user):
257 owner = user.user_id == pull_request.user_id
259 owner = user.user_id == pull_request.user_id
258 _perms = ('repository.admin',)
260 _perms = ('repository.admin',)
259 return self._check_perms(_perms, pull_request, user) or owner
261 return self._check_perms(_perms, pull_request, user) or owner
260
262
261 def check_user_change_status(self, pull_request, user, api=False):
263 def check_user_change_status(self, pull_request, user, api=False):
262 reviewer = user.user_id in [x.user_id for x in
264 reviewer = user.user_id in [x.user_id for x in
263 pull_request.reviewers]
265 pull_request.reviewers]
264 return self.check_user_update(pull_request, user, api) or reviewer
266 return self.check_user_update(pull_request, user, api) or reviewer
265
267
266 def check_user_comment(self, pull_request, user):
268 def check_user_comment(self, pull_request, user):
267 owner = user.user_id == pull_request.user_id
269 owner = user.user_id == pull_request.user_id
268 return self.check_user_read(pull_request, user) or owner
270 return self.check_user_read(pull_request, user) or owner
269
271
270 def get(self, pull_request):
272 def get(self, pull_request):
271 return self.__get_pull_request(pull_request)
273 return self.__get_pull_request(pull_request)
272
274
273 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
275 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
274 statuses=None, opened_by=None, order_by=None,
276 statuses=None, opened_by=None, order_by=None,
275 order_dir='desc', only_created=False):
277 order_dir='desc', only_created=False):
276 repo = None
278 repo = None
277 if repo_name:
279 if repo_name:
278 repo = self._get_repo(repo_name)
280 repo = self._get_repo(repo_name)
279
281
280 q = PullRequest.query()
282 q = PullRequest.query()
281
283
282 if search_q:
284 if search_q:
283 like_expression = u'%{}%'.format(safe_unicode(search_q))
285 like_expression = u'%{}%'.format(safe_unicode(search_q))
284 q = q.join(User)
286 q = q.join(User)
285 q = q.filter(or_(
287 q = q.filter(or_(
286 cast(PullRequest.pull_request_id, String).ilike(like_expression),
288 cast(PullRequest.pull_request_id, String).ilike(like_expression),
287 User.username.ilike(like_expression),
289 User.username.ilike(like_expression),
288 PullRequest.title.ilike(like_expression),
290 PullRequest.title.ilike(like_expression),
289 PullRequest.description.ilike(like_expression),
291 PullRequest.description.ilike(like_expression),
290 ))
292 ))
291
293
292 # source or target
294 # source or target
293 if repo and source:
295 if repo and source:
294 q = q.filter(PullRequest.source_repo == repo)
296 q = q.filter(PullRequest.source_repo == repo)
295 elif repo:
297 elif repo:
296 q = q.filter(PullRequest.target_repo == repo)
298 q = q.filter(PullRequest.target_repo == repo)
297
299
298 # closed,opened
300 # closed,opened
299 if statuses:
301 if statuses:
300 q = q.filter(PullRequest.status.in_(statuses))
302 q = q.filter(PullRequest.status.in_(statuses))
301
303
302 # opened by filter
304 # opened by filter
303 if opened_by:
305 if opened_by:
304 q = q.filter(PullRequest.user_id.in_(opened_by))
306 q = q.filter(PullRequest.user_id.in_(opened_by))
305
307
306 # only get those that are in "created" state
308 # only get those that are in "created" state
307 if only_created:
309 if only_created:
308 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
310 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
309
311
310 if order_by:
312 if order_by:
311 order_map = {
313 order_map = {
312 'name_raw': PullRequest.pull_request_id,
314 'name_raw': PullRequest.pull_request_id,
313 'id': PullRequest.pull_request_id,
315 'id': PullRequest.pull_request_id,
314 'title': PullRequest.title,
316 'title': PullRequest.title,
315 'updated_on_raw': PullRequest.updated_on,
317 'updated_on_raw': PullRequest.updated_on,
316 'target_repo': PullRequest.target_repo_id
318 'target_repo': PullRequest.target_repo_id
317 }
319 }
318 if order_dir == 'asc':
320 if order_dir == 'asc':
319 q = q.order_by(order_map[order_by].asc())
321 q = q.order_by(order_map[order_by].asc())
320 else:
322 else:
321 q = q.order_by(order_map[order_by].desc())
323 q = q.order_by(order_map[order_by].desc())
322
324
323 return q
325 return q
324
326
325 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
327 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
326 opened_by=None):
328 opened_by=None):
327 """
329 """
328 Count the number of pull requests for a specific repository.
330 Count the number of pull requests for a specific repository.
329
331
330 :param repo_name: target or source repo
332 :param repo_name: target or source repo
331 :param search_q: filter by text
333 :param search_q: filter by text
332 :param source: boolean flag to specify if repo_name refers to source
334 :param source: boolean flag to specify if repo_name refers to source
333 :param statuses: list of pull request statuses
335 :param statuses: list of pull request statuses
334 :param opened_by: author user of the pull request
336 :param opened_by: author user of the pull request
335 :returns: int number of pull requests
337 :returns: int number of pull requests
336 """
338 """
337 q = self._prepare_get_all_query(
339 q = self._prepare_get_all_query(
338 repo_name, search_q=search_q, source=source, statuses=statuses,
340 repo_name, search_q=search_q, source=source, statuses=statuses,
339 opened_by=opened_by)
341 opened_by=opened_by)
340
342
341 return q.count()
343 return q.count()
342
344
343 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
345 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
344 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
346 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
345 """
347 """
346 Get all pull requests for a specific repository.
348 Get all pull requests for a specific repository.
347
349
348 :param repo_name: target or source repo
350 :param repo_name: target or source repo
349 :param search_q: filter by text
351 :param search_q: filter by text
350 :param source: boolean flag to specify if repo_name refers to source
352 :param source: boolean flag to specify if repo_name refers to source
351 :param statuses: list of pull request statuses
353 :param statuses: list of pull request statuses
352 :param opened_by: author user of the pull request
354 :param opened_by: author user of the pull request
353 :param offset: pagination offset
355 :param offset: pagination offset
354 :param length: length of returned list
356 :param length: length of returned list
355 :param order_by: order of the returned list
357 :param order_by: order of the returned list
356 :param order_dir: 'asc' or 'desc' ordering direction
358 :param order_dir: 'asc' or 'desc' ordering direction
357 :returns: list of pull requests
359 :returns: list of pull requests
358 """
360 """
359 q = self._prepare_get_all_query(
361 q = self._prepare_get_all_query(
360 repo_name, search_q=search_q, source=source, statuses=statuses,
362 repo_name, search_q=search_q, source=source, statuses=statuses,
361 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
363 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
362
364
363 if length:
365 if length:
364 pull_requests = q.limit(length).offset(offset).all()
366 pull_requests = q.limit(length).offset(offset).all()
365 else:
367 else:
366 pull_requests = q.all()
368 pull_requests = q.all()
367
369
368 return pull_requests
370 return pull_requests
369
371
370 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
372 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
371 opened_by=None):
373 opened_by=None):
372 """
374 """
373 Count the number of pull requests for a specific repository that are
375 Count the number of pull requests for a specific repository that are
374 awaiting review.
376 awaiting review.
375
377
376 :param repo_name: target or source repo
378 :param repo_name: target or source repo
377 :param search_q: filter by text
379 :param search_q: filter by text
378 :param source: boolean flag to specify if repo_name refers to source
380 :param source: boolean flag to specify if repo_name refers to source
379 :param statuses: list of pull request statuses
381 :param statuses: list of pull request statuses
380 :param opened_by: author user of the pull request
382 :param opened_by: author user of the pull request
381 :returns: int number of pull requests
383 :returns: int number of pull requests
382 """
384 """
383 pull_requests = self.get_awaiting_review(
385 pull_requests = self.get_awaiting_review(
384 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
386 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
385
387
386 return len(pull_requests)
388 return len(pull_requests)
387
389
388 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
390 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
389 opened_by=None, offset=0, length=None,
391 opened_by=None, offset=0, length=None,
390 order_by=None, order_dir='desc'):
392 order_by=None, order_dir='desc'):
391 """
393 """
392 Get all pull requests for a specific repository that are awaiting
394 Get all pull requests for a specific repository that are awaiting
393 review.
395 review.
394
396
395 :param repo_name: target or source repo
397 :param repo_name: target or source repo
396 :param search_q: filter by text
398 :param search_q: filter by text
397 :param source: boolean flag to specify if repo_name refers to source
399 :param source: boolean flag to specify if repo_name refers to source
398 :param statuses: list of pull request statuses
400 :param statuses: list of pull request statuses
399 :param opened_by: author user of the pull request
401 :param opened_by: author user of the pull request
400 :param offset: pagination offset
402 :param offset: pagination offset
401 :param length: length of returned list
403 :param length: length of returned list
402 :param order_by: order of the returned list
404 :param order_by: order of the returned list
403 :param order_dir: 'asc' or 'desc' ordering direction
405 :param order_dir: 'asc' or 'desc' ordering direction
404 :returns: list of pull requests
406 :returns: list of pull requests
405 """
407 """
406 pull_requests = self.get_all(
408 pull_requests = self.get_all(
407 repo_name, search_q=search_q, source=source, statuses=statuses,
409 repo_name, search_q=search_q, source=source, statuses=statuses,
408 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
410 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
409
411
410 _filtered_pull_requests = []
412 _filtered_pull_requests = []
411 for pr in pull_requests:
413 for pr in pull_requests:
412 status = pr.calculated_review_status()
414 status = pr.calculated_review_status()
413 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
415 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
414 ChangesetStatus.STATUS_UNDER_REVIEW]:
416 ChangesetStatus.STATUS_UNDER_REVIEW]:
415 _filtered_pull_requests.append(pr)
417 _filtered_pull_requests.append(pr)
416 if length:
418 if length:
417 return _filtered_pull_requests[offset:offset+length]
419 return _filtered_pull_requests[offset:offset+length]
418 else:
420 else:
419 return _filtered_pull_requests
421 return _filtered_pull_requests
420
422
421 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
423 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
422 opened_by=None, user_id=None):
424 opened_by=None, user_id=None):
423 """
425 """
424 Count the number of pull requests for a specific repository that are
426 Count the number of pull requests for a specific repository that are
425 awaiting review from a specific user.
427 awaiting review from a specific user.
426
428
427 :param repo_name: target or source repo
429 :param repo_name: target or source repo
428 :param search_q: filter by text
430 :param search_q: filter by text
429 :param source: boolean flag to specify if repo_name refers to source
431 :param source: boolean flag to specify if repo_name refers to source
430 :param statuses: list of pull request statuses
432 :param statuses: list of pull request statuses
431 :param opened_by: author user of the pull request
433 :param opened_by: author user of the pull request
432 :param user_id: reviewer user of the pull request
434 :param user_id: reviewer user of the pull request
433 :returns: int number of pull requests
435 :returns: int number of pull requests
434 """
436 """
435 pull_requests = self.get_awaiting_my_review(
437 pull_requests = self.get_awaiting_my_review(
436 repo_name, search_q=search_q, source=source, statuses=statuses,
438 repo_name, search_q=search_q, source=source, statuses=statuses,
437 opened_by=opened_by, user_id=user_id)
439 opened_by=opened_by, user_id=user_id)
438
440
439 return len(pull_requests)
441 return len(pull_requests)
440
442
441 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
443 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
442 opened_by=None, user_id=None, offset=0,
444 opened_by=None, user_id=None, offset=0,
443 length=None, order_by=None, order_dir='desc'):
445 length=None, order_by=None, order_dir='desc'):
444 """
446 """
445 Get all pull requests for a specific repository that are awaiting
447 Get all pull requests for a specific repository that are awaiting
446 review from a specific user.
448 review from a specific user.
447
449
448 :param repo_name: target or source repo
450 :param repo_name: target or source repo
449 :param search_q: filter by text
451 :param search_q: filter by text
450 :param source: boolean flag to specify if repo_name refers to source
452 :param source: boolean flag to specify if repo_name refers to source
451 :param statuses: list of pull request statuses
453 :param statuses: list of pull request statuses
452 :param opened_by: author user of the pull request
454 :param opened_by: author user of the pull request
453 :param user_id: reviewer user of the pull request
455 :param user_id: reviewer user of the pull request
454 :param offset: pagination offset
456 :param offset: pagination offset
455 :param length: length of returned list
457 :param length: length of returned list
456 :param order_by: order of the returned list
458 :param order_by: order of the returned list
457 :param order_dir: 'asc' or 'desc' ordering direction
459 :param order_dir: 'asc' or 'desc' ordering direction
458 :returns: list of pull requests
460 :returns: list of pull requests
459 """
461 """
460 pull_requests = self.get_all(
462 pull_requests = self.get_all(
461 repo_name, search_q=search_q, source=source, statuses=statuses,
463 repo_name, search_q=search_q, source=source, statuses=statuses,
462 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
464 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
463
465
464 _my = PullRequestModel().get_not_reviewed(user_id)
466 _my = PullRequestModel().get_not_reviewed(user_id)
465 my_participation = []
467 my_participation = []
466 for pr in pull_requests:
468 for pr in pull_requests:
467 if pr in _my:
469 if pr in _my:
468 my_participation.append(pr)
470 my_participation.append(pr)
469 _filtered_pull_requests = my_participation
471 _filtered_pull_requests = my_participation
470 if length:
472 if length:
471 return _filtered_pull_requests[offset:offset+length]
473 return _filtered_pull_requests[offset:offset+length]
472 else:
474 else:
473 return _filtered_pull_requests
475 return _filtered_pull_requests
474
476
475 def get_not_reviewed(self, user_id):
477 def get_not_reviewed(self, user_id):
476 return [
478 return [
477 x.pull_request for x in PullRequestReviewers.query().filter(
479 x.pull_request for x in PullRequestReviewers.query().filter(
478 PullRequestReviewers.user_id == user_id).all()
480 PullRequestReviewers.user_id == user_id).all()
479 ]
481 ]
480
482
481 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
483 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
482 order_by=None, order_dir='desc'):
484 order_by=None, order_dir='desc'):
483 q = PullRequest.query()
485 q = PullRequest.query()
484 if user_id:
486 if user_id:
485 reviewers_subquery = Session().query(
487 reviewers_subquery = Session().query(
486 PullRequestReviewers.pull_request_id).filter(
488 PullRequestReviewers.pull_request_id).filter(
487 PullRequestReviewers.user_id == user_id).subquery()
489 PullRequestReviewers.user_id == user_id).subquery()
488 user_filter = or_(
490 user_filter = or_(
489 PullRequest.user_id == user_id,
491 PullRequest.user_id == user_id,
490 PullRequest.pull_request_id.in_(reviewers_subquery)
492 PullRequest.pull_request_id.in_(reviewers_subquery)
491 )
493 )
492 q = PullRequest.query().filter(user_filter)
494 q = PullRequest.query().filter(user_filter)
493
495
494 # closed,opened
496 # closed,opened
495 if statuses:
497 if statuses:
496 q = q.filter(PullRequest.status.in_(statuses))
498 q = q.filter(PullRequest.status.in_(statuses))
497
499
498 if query:
500 if query:
499 like_expression = u'%{}%'.format(safe_unicode(query))
501 like_expression = u'%{}%'.format(safe_unicode(query))
500 q = q.join(User)
502 q = q.join(User)
501 q = q.filter(or_(
503 q = q.filter(or_(
502 cast(PullRequest.pull_request_id, String).ilike(like_expression),
504 cast(PullRequest.pull_request_id, String).ilike(like_expression),
503 User.username.ilike(like_expression),
505 User.username.ilike(like_expression),
504 PullRequest.title.ilike(like_expression),
506 PullRequest.title.ilike(like_expression),
505 PullRequest.description.ilike(like_expression),
507 PullRequest.description.ilike(like_expression),
506 ))
508 ))
507 if order_by:
509 if order_by:
508 order_map = {
510 order_map = {
509 'name_raw': PullRequest.pull_request_id,
511 'name_raw': PullRequest.pull_request_id,
510 'title': PullRequest.title,
512 'title': PullRequest.title,
511 'updated_on_raw': PullRequest.updated_on,
513 'updated_on_raw': PullRequest.updated_on,
512 'target_repo': PullRequest.target_repo_id
514 'target_repo': PullRequest.target_repo_id
513 }
515 }
514 if order_dir == 'asc':
516 if order_dir == 'asc':
515 q = q.order_by(order_map[order_by].asc())
517 q = q.order_by(order_map[order_by].asc())
516 else:
518 else:
517 q = q.order_by(order_map[order_by].desc())
519 q = q.order_by(order_map[order_by].desc())
518
520
519 return q
521 return q
520
522
521 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
523 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
522 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
524 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
523 return q.count()
525 return q.count()
524
526
525 def get_im_participating_in(
527 def get_im_participating_in(
526 self, user_id=None, statuses=None, query='', offset=0,
528 self, user_id=None, statuses=None, query='', offset=0,
527 length=None, order_by=None, order_dir='desc'):
529 length=None, order_by=None, order_dir='desc'):
528 """
530 """
529 Get all Pull requests that i'm participating in, or i have opened
531 Get all Pull requests that i'm participating in, or i have opened
530 """
532 """
531
533
532 q = self._prepare_participating_query(
534 q = self._prepare_participating_query(
533 user_id, statuses=statuses, query=query, order_by=order_by,
535 user_id, statuses=statuses, query=query, order_by=order_by,
534 order_dir=order_dir)
536 order_dir=order_dir)
535
537
536 if length:
538 if length:
537 pull_requests = q.limit(length).offset(offset).all()
539 pull_requests = q.limit(length).offset(offset).all()
538 else:
540 else:
539 pull_requests = q.all()
541 pull_requests = q.all()
540
542
541 return pull_requests
543 return pull_requests
542
544
543 def get_versions(self, pull_request):
545 def get_versions(self, pull_request):
544 """
546 """
545 returns version of pull request sorted by ID descending
547 returns version of pull request sorted by ID descending
546 """
548 """
547 return PullRequestVersion.query()\
549 return PullRequestVersion.query()\
548 .filter(PullRequestVersion.pull_request == pull_request)\
550 .filter(PullRequestVersion.pull_request == pull_request)\
549 .order_by(PullRequestVersion.pull_request_version_id.asc())\
551 .order_by(PullRequestVersion.pull_request_version_id.asc())\
550 .all()
552 .all()
551
553
552 def get_pr_version(self, pull_request_id, version=None):
554 def get_pr_version(self, pull_request_id, version=None):
553 at_version = None
555 at_version = None
554
556
555 if version and version == 'latest':
557 if version and version == 'latest':
556 pull_request_ver = PullRequest.get(pull_request_id)
558 pull_request_ver = PullRequest.get(pull_request_id)
557 pull_request_obj = pull_request_ver
559 pull_request_obj = pull_request_ver
558 _org_pull_request_obj = pull_request_obj
560 _org_pull_request_obj = pull_request_obj
559 at_version = 'latest'
561 at_version = 'latest'
560 elif version:
562 elif version:
561 pull_request_ver = PullRequestVersion.get_or_404(version)
563 pull_request_ver = PullRequestVersion.get_or_404(version)
562 pull_request_obj = pull_request_ver
564 pull_request_obj = pull_request_ver
563 _org_pull_request_obj = pull_request_ver.pull_request
565 _org_pull_request_obj = pull_request_ver.pull_request
564 at_version = pull_request_ver.pull_request_version_id
566 at_version = pull_request_ver.pull_request_version_id
565 else:
567 else:
566 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
568 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
567 pull_request_id)
569 pull_request_id)
568
570
569 pull_request_display_obj = PullRequest.get_pr_display_object(
571 pull_request_display_obj = PullRequest.get_pr_display_object(
570 pull_request_obj, _org_pull_request_obj)
572 pull_request_obj, _org_pull_request_obj)
571
573
572 return _org_pull_request_obj, pull_request_obj, \
574 return _org_pull_request_obj, pull_request_obj, \
573 pull_request_display_obj, at_version
575 pull_request_display_obj, at_version
574
576
575 def create(self, created_by, source_repo, source_ref, target_repo,
577 def create(self, created_by, source_repo, source_ref, target_repo,
576 target_ref, revisions, reviewers, title, description=None,
578 target_ref, revisions, reviewers, title, description=None,
577 common_ancestor_id=None,
579 common_ancestor_id=None,
578 description_renderer=None,
580 description_renderer=None,
579 reviewer_data=None, translator=None, auth_user=None):
581 reviewer_data=None, translator=None, auth_user=None):
580 translator = translator or get_current_request().translate
582 translator = translator or get_current_request().translate
581
583
582 created_by_user = self._get_user(created_by)
584 created_by_user = self._get_user(created_by)
583 auth_user = auth_user or created_by_user.AuthUser()
585 auth_user = auth_user or created_by_user.AuthUser()
584 source_repo = self._get_repo(source_repo)
586 source_repo = self._get_repo(source_repo)
585 target_repo = self._get_repo(target_repo)
587 target_repo = self._get_repo(target_repo)
586
588
587 pull_request = PullRequest()
589 pull_request = PullRequest()
588 pull_request.source_repo = source_repo
590 pull_request.source_repo = source_repo
589 pull_request.source_ref = source_ref
591 pull_request.source_ref = source_ref
590 pull_request.target_repo = target_repo
592 pull_request.target_repo = target_repo
591 pull_request.target_ref = target_ref
593 pull_request.target_ref = target_ref
592 pull_request.revisions = revisions
594 pull_request.revisions = revisions
593 pull_request.title = title
595 pull_request.title = title
594 pull_request.description = description
596 pull_request.description = description
595 pull_request.description_renderer = description_renderer
597 pull_request.description_renderer = description_renderer
596 pull_request.author = created_by_user
598 pull_request.author = created_by_user
597 pull_request.reviewer_data = reviewer_data
599 pull_request.reviewer_data = reviewer_data
598 pull_request.pull_request_state = pull_request.STATE_CREATING
600 pull_request.pull_request_state = pull_request.STATE_CREATING
599 pull_request.common_ancestor_id = common_ancestor_id
601 pull_request.common_ancestor_id = common_ancestor_id
600
602
601 Session().add(pull_request)
603 Session().add(pull_request)
602 Session().flush()
604 Session().flush()
603
605
604 reviewer_ids = set()
606 reviewer_ids = set()
605 # members / reviewers
607 # members / reviewers
606 for reviewer_object in reviewers:
608 for reviewer_object in reviewers:
607 user_id, reasons, mandatory, rules = reviewer_object
609 user_id, reasons, mandatory, rules = reviewer_object
608 user = self._get_user(user_id)
610 user = self._get_user(user_id)
609
611
610 # skip duplicates
612 # skip duplicates
611 if user.user_id in reviewer_ids:
613 if user.user_id in reviewer_ids:
612 continue
614 continue
613
615
614 reviewer_ids.add(user.user_id)
616 reviewer_ids.add(user.user_id)
615
617
616 reviewer = PullRequestReviewers()
618 reviewer = PullRequestReviewers()
617 reviewer.user = user
619 reviewer.user = user
618 reviewer.pull_request = pull_request
620 reviewer.pull_request = pull_request
619 reviewer.reasons = reasons
621 reviewer.reasons = reasons
620 reviewer.mandatory = mandatory
622 reviewer.mandatory = mandatory
621
623
622 # NOTE(marcink): pick only first rule for now
624 # NOTE(marcink): pick only first rule for now
623 rule_id = list(rules)[0] if rules else None
625 rule_id = list(rules)[0] if rules else None
624 rule = RepoReviewRule.get(rule_id) if rule_id else None
626 rule = RepoReviewRule.get(rule_id) if rule_id else None
625 if rule:
627 if rule:
626 review_group = rule.user_group_vote_rule(user_id)
628 review_group = rule.user_group_vote_rule(user_id)
627 # we check if this particular reviewer is member of a voting group
629 # we check if this particular reviewer is member of a voting group
628 if review_group:
630 if review_group:
629 # NOTE(marcink):
631 # NOTE(marcink):
630 # can be that user is member of more but we pick the first same,
632 # can be that user is member of more but we pick the first same,
631 # same as default reviewers algo
633 # same as default reviewers algo
632 review_group = review_group[0]
634 review_group = review_group[0]
633
635
634 rule_data = {
636 rule_data = {
635 'rule_name':
637 'rule_name':
636 rule.review_rule_name,
638 rule.review_rule_name,
637 'rule_user_group_entry_id':
639 'rule_user_group_entry_id':
638 review_group.repo_review_rule_users_group_id,
640 review_group.repo_review_rule_users_group_id,
639 'rule_user_group_name':
641 'rule_user_group_name':
640 review_group.users_group.users_group_name,
642 review_group.users_group.users_group_name,
641 'rule_user_group_members':
643 'rule_user_group_members':
642 [x.user.username for x in review_group.users_group.members],
644 [x.user.username for x in review_group.users_group.members],
643 'rule_user_group_members_id':
645 'rule_user_group_members_id':
644 [x.user.user_id for x in review_group.users_group.members],
646 [x.user.user_id for x in review_group.users_group.members],
645 }
647 }
646 # e.g {'vote_rule': -1, 'mandatory': True}
648 # e.g {'vote_rule': -1, 'mandatory': True}
647 rule_data.update(review_group.rule_data())
649 rule_data.update(review_group.rule_data())
648
650
649 reviewer.rule_data = rule_data
651 reviewer.rule_data = rule_data
650
652
651 Session().add(reviewer)
653 Session().add(reviewer)
652 Session().flush()
654 Session().flush()
653
655
654 # Set approval status to "Under Review" for all commits which are
656 # Set approval status to "Under Review" for all commits which are
655 # part of this pull request.
657 # part of this pull request.
656 ChangesetStatusModel().set_status(
658 ChangesetStatusModel().set_status(
657 repo=target_repo,
659 repo=target_repo,
658 status=ChangesetStatus.STATUS_UNDER_REVIEW,
660 status=ChangesetStatus.STATUS_UNDER_REVIEW,
659 user=created_by_user,
661 user=created_by_user,
660 pull_request=pull_request
662 pull_request=pull_request
661 )
663 )
662 # we commit early at this point. This has to do with a fact
664 # we commit early at this point. This has to do with a fact
663 # that before queries do some row-locking. And because of that
665 # that before queries do some row-locking. And because of that
664 # we need to commit and finish transaction before below validate call
666 # we need to commit and finish transaction before below validate call
665 # that for large repos could be long resulting in long row locks
667 # that for large repos could be long resulting in long row locks
666 Session().commit()
668 Session().commit()
667
669
668 # prepare workspace, and run initial merge simulation. Set state during that
670 # prepare workspace, and run initial merge simulation. Set state during that
669 # operation
671 # operation
670 pull_request = PullRequest.get(pull_request.pull_request_id)
672 pull_request = PullRequest.get(pull_request.pull_request_id)
671
673
672 # set as merging, for merge simulation, and if finished to created so we mark
674 # set as merging, for merge simulation, and if finished to created so we mark
673 # simulation is working fine
675 # simulation is working fine
674 with pull_request.set_state(PullRequest.STATE_MERGING,
676 with pull_request.set_state(PullRequest.STATE_MERGING,
675 final_state=PullRequest.STATE_CREATED) as state_obj:
677 final_state=PullRequest.STATE_CREATED) as state_obj:
676 MergeCheck.validate(
678 MergeCheck.validate(
677 pull_request, auth_user=auth_user, translator=translator)
679 pull_request, auth_user=auth_user, translator=translator)
678
680
679 self.notify_reviewers(pull_request, reviewer_ids)
681 self.notify_reviewers(pull_request, reviewer_ids)
680 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
682 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
681
683
682 creation_data = pull_request.get_api_data(with_merge_state=False)
684 creation_data = pull_request.get_api_data(with_merge_state=False)
683 self._log_audit_action(
685 self._log_audit_action(
684 'repo.pull_request.create', {'data': creation_data},
686 'repo.pull_request.create', {'data': creation_data},
685 auth_user, pull_request)
687 auth_user, pull_request)
686
688
687 return pull_request
689 return pull_request
688
690
689 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
691 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
690 pull_request = self.__get_pull_request(pull_request)
692 pull_request = self.__get_pull_request(pull_request)
691 target_scm = pull_request.target_repo.scm_instance()
693 target_scm = pull_request.target_repo.scm_instance()
692 if action == 'create':
694 if action == 'create':
693 trigger_hook = hooks_utils.trigger_create_pull_request_hook
695 trigger_hook = hooks_utils.trigger_create_pull_request_hook
694 elif action == 'merge':
696 elif action == 'merge':
695 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
697 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
696 elif action == 'close':
698 elif action == 'close':
697 trigger_hook = hooks_utils.trigger_close_pull_request_hook
699 trigger_hook = hooks_utils.trigger_close_pull_request_hook
698 elif action == 'review_status_change':
700 elif action == 'review_status_change':
699 trigger_hook = hooks_utils.trigger_review_pull_request_hook
701 trigger_hook = hooks_utils.trigger_review_pull_request_hook
700 elif action == 'update':
702 elif action == 'update':
701 trigger_hook = hooks_utils.trigger_update_pull_request_hook
703 trigger_hook = hooks_utils.trigger_update_pull_request_hook
702 elif action == 'comment':
704 elif action == 'comment':
703 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
705 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
704 else:
706 else:
705 return
707 return
706
708
707 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
709 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
708 pull_request, action, trigger_hook)
710 pull_request, action, trigger_hook)
709 trigger_hook(
711 trigger_hook(
710 username=user.username,
712 username=user.username,
711 repo_name=pull_request.target_repo.repo_name,
713 repo_name=pull_request.target_repo.repo_name,
712 repo_type=target_scm.alias,
714 repo_type=target_scm.alias,
713 pull_request=pull_request,
715 pull_request=pull_request,
714 data=data)
716 data=data)
715
717
716 def _get_commit_ids(self, pull_request):
718 def _get_commit_ids(self, pull_request):
717 """
719 """
718 Return the commit ids of the merged pull request.
720 Return the commit ids of the merged pull request.
719
721
720 This method is not dealing correctly yet with the lack of autoupdates
722 This method is not dealing correctly yet with the lack of autoupdates
721 nor with the implicit target updates.
723 nor with the implicit target updates.
722 For example: if a commit in the source repo is already in the target it
724 For example: if a commit in the source repo is already in the target it
723 will be reported anyways.
725 will be reported anyways.
724 """
726 """
725 merge_rev = pull_request.merge_rev
727 merge_rev = pull_request.merge_rev
726 if merge_rev is None:
728 if merge_rev is None:
727 raise ValueError('This pull request was not merged yet')
729 raise ValueError('This pull request was not merged yet')
728
730
729 commit_ids = list(pull_request.revisions)
731 commit_ids = list(pull_request.revisions)
730 if merge_rev not in commit_ids:
732 if merge_rev not in commit_ids:
731 commit_ids.append(merge_rev)
733 commit_ids.append(merge_rev)
732
734
733 return commit_ids
735 return commit_ids
734
736
735 def merge_repo(self, pull_request, user, extras):
737 def merge_repo(self, pull_request, user, extras):
736 log.debug("Merging pull request %s", pull_request.pull_request_id)
738 log.debug("Merging pull request %s", pull_request.pull_request_id)
737 extras['user_agent'] = 'internal-merge'
739 extras['user_agent'] = 'internal-merge'
738 merge_state = self._merge_pull_request(pull_request, user, extras)
740 merge_state = self._merge_pull_request(pull_request, user, extras)
739 if merge_state.executed:
741 if merge_state.executed:
740 log.debug("Merge was successful, updating the pull request comments.")
742 log.debug("Merge was successful, updating the pull request comments.")
741 self._comment_and_close_pr(pull_request, user, merge_state)
743 self._comment_and_close_pr(pull_request, user, merge_state)
742
744
743 self._log_audit_action(
745 self._log_audit_action(
744 'repo.pull_request.merge',
746 'repo.pull_request.merge',
745 {'merge_state': merge_state.__dict__},
747 {'merge_state': merge_state.__dict__},
746 user, pull_request)
748 user, pull_request)
747
749
748 else:
750 else:
749 log.warn("Merge failed, not updating the pull request.")
751 log.warn("Merge failed, not updating the pull request.")
750 return merge_state
752 return merge_state
751
753
752 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
754 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
753 target_vcs = pull_request.target_repo.scm_instance()
755 target_vcs = pull_request.target_repo.scm_instance()
754 source_vcs = pull_request.source_repo.scm_instance()
756 source_vcs = pull_request.source_repo.scm_instance()
755
757
756 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
758 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
757 pr_id=pull_request.pull_request_id,
759 pr_id=pull_request.pull_request_id,
758 pr_title=pull_request.title,
760 pr_title=pull_request.title,
759 source_repo=source_vcs.name,
761 source_repo=source_vcs.name,
760 source_ref_name=pull_request.source_ref_parts.name,
762 source_ref_name=pull_request.source_ref_parts.name,
761 target_repo=target_vcs.name,
763 target_repo=target_vcs.name,
762 target_ref_name=pull_request.target_ref_parts.name,
764 target_ref_name=pull_request.target_ref_parts.name,
763 )
765 )
764
766
765 workspace_id = self._workspace_id(pull_request)
767 workspace_id = self._workspace_id(pull_request)
766 repo_id = pull_request.target_repo.repo_id
768 repo_id = pull_request.target_repo.repo_id
767 use_rebase = self._use_rebase_for_merging(pull_request)
769 use_rebase = self._use_rebase_for_merging(pull_request)
768 close_branch = self._close_branch_before_merging(pull_request)
770 close_branch = self._close_branch_before_merging(pull_request)
769 user_name = self._user_name_for_merging(pull_request, user)
771 user_name = self._user_name_for_merging(pull_request, user)
770
772
771 target_ref = self._refresh_reference(
773 target_ref = self._refresh_reference(
772 pull_request.target_ref_parts, target_vcs)
774 pull_request.target_ref_parts, target_vcs)
773
775
774 callback_daemon, extras = prepare_callback_daemon(
776 callback_daemon, extras = prepare_callback_daemon(
775 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
777 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
776 host=vcs_settings.HOOKS_HOST,
778 host=vcs_settings.HOOKS_HOST,
777 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
779 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
778
780
779 with callback_daemon:
781 with callback_daemon:
780 # TODO: johbo: Implement a clean way to run a config_override
782 # TODO: johbo: Implement a clean way to run a config_override
781 # for a single call.
783 # for a single call.
782 target_vcs.config.set(
784 target_vcs.config.set(
783 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
785 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
784
786
785 merge_state = target_vcs.merge(
787 merge_state = target_vcs.merge(
786 repo_id, workspace_id, target_ref, source_vcs,
788 repo_id, workspace_id, target_ref, source_vcs,
787 pull_request.source_ref_parts,
789 pull_request.source_ref_parts,
788 user_name=user_name, user_email=user.email,
790 user_name=user_name, user_email=user.email,
789 message=message, use_rebase=use_rebase,
791 message=message, use_rebase=use_rebase,
790 close_branch=close_branch)
792 close_branch=close_branch)
791 return merge_state
793 return merge_state
792
794
793 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
795 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
794 pull_request.merge_rev = merge_state.merge_ref.commit_id
796 pull_request.merge_rev = merge_state.merge_ref.commit_id
795 pull_request.updated_on = datetime.datetime.now()
797 pull_request.updated_on = datetime.datetime.now()
796 close_msg = close_msg or 'Pull request merged and closed'
798 close_msg = close_msg or 'Pull request merged and closed'
797
799
798 CommentsModel().create(
800 CommentsModel().create(
799 text=safe_unicode(close_msg),
801 text=safe_unicode(close_msg),
800 repo=pull_request.target_repo.repo_id,
802 repo=pull_request.target_repo.repo_id,
801 user=user.user_id,
803 user=user.user_id,
802 pull_request=pull_request.pull_request_id,
804 pull_request=pull_request.pull_request_id,
803 f_path=None,
805 f_path=None,
804 line_no=None,
806 line_no=None,
805 closing_pr=True
807 closing_pr=True
806 )
808 )
807
809
808 Session().add(pull_request)
810 Session().add(pull_request)
809 Session().flush()
811 Session().flush()
810 # TODO: paris: replace invalidation with less radical solution
812 # TODO: paris: replace invalidation with less radical solution
811 ScmModel().mark_for_invalidation(
813 ScmModel().mark_for_invalidation(
812 pull_request.target_repo.repo_name)
814 pull_request.target_repo.repo_name)
813 self.trigger_pull_request_hook(pull_request, user, 'merge')
815 self.trigger_pull_request_hook(pull_request, user, 'merge')
814
816
815 def has_valid_update_type(self, pull_request):
817 def has_valid_update_type(self, pull_request):
816 source_ref_type = pull_request.source_ref_parts.type
818 source_ref_type = pull_request.source_ref_parts.type
817 return source_ref_type in self.REF_TYPES
819 return source_ref_type in self.REF_TYPES
818
820
819 def get_flow_commits(self, pull_request):
821 def get_flow_commits(self, pull_request):
820
822
821 # source repo
823 # source repo
822 source_ref_name = pull_request.source_ref_parts.name
824 source_ref_name = pull_request.source_ref_parts.name
823 source_ref_type = pull_request.source_ref_parts.type
825 source_ref_type = pull_request.source_ref_parts.type
824 source_ref_id = pull_request.source_ref_parts.commit_id
826 source_ref_id = pull_request.source_ref_parts.commit_id
825 source_repo = pull_request.source_repo.scm_instance()
827 source_repo = pull_request.source_repo.scm_instance()
826
828
827 try:
829 try:
828 if source_ref_type in self.REF_TYPES:
830 if source_ref_type in self.REF_TYPES:
829 source_commit = source_repo.get_commit(source_ref_name)
831 source_commit = source_repo.get_commit(source_ref_name)
830 else:
832 else:
831 source_commit = source_repo.get_commit(source_ref_id)
833 source_commit = source_repo.get_commit(source_ref_id)
832 except CommitDoesNotExistError:
834 except CommitDoesNotExistError:
833 raise SourceRefMissing()
835 raise SourceRefMissing()
834
836
835 # target repo
837 # target repo
836 target_ref_name = pull_request.target_ref_parts.name
838 target_ref_name = pull_request.target_ref_parts.name
837 target_ref_type = pull_request.target_ref_parts.type
839 target_ref_type = pull_request.target_ref_parts.type
838 target_ref_id = pull_request.target_ref_parts.commit_id
840 target_ref_id = pull_request.target_ref_parts.commit_id
839 target_repo = pull_request.target_repo.scm_instance()
841 target_repo = pull_request.target_repo.scm_instance()
840
842
841 try:
843 try:
842 if target_ref_type in self.REF_TYPES:
844 if target_ref_type in self.REF_TYPES:
843 target_commit = target_repo.get_commit(target_ref_name)
845 target_commit = target_repo.get_commit(target_ref_name)
844 else:
846 else:
845 target_commit = target_repo.get_commit(target_ref_id)
847 target_commit = target_repo.get_commit(target_ref_id)
846 except CommitDoesNotExistError:
848 except CommitDoesNotExistError:
847 raise TargetRefMissing()
849 raise TargetRefMissing()
848
850
849 return source_commit, target_commit
851 return source_commit, target_commit
850
852
851 def update_commits(self, pull_request, updating_user):
853 def update_commits(self, pull_request, updating_user):
852 """
854 """
853 Get the updated list of commits for the pull request
855 Get the updated list of commits for the pull request
854 and return the new pull request version and the list
856 and return the new pull request version and the list
855 of commits processed by this update action
857 of commits processed by this update action
856
858
857 updating_user is the user_object who triggered the update
859 updating_user is the user_object who triggered the update
858 """
860 """
859 pull_request = self.__get_pull_request(pull_request)
861 pull_request = self.__get_pull_request(pull_request)
860 source_ref_type = pull_request.source_ref_parts.type
862 source_ref_type = pull_request.source_ref_parts.type
861 source_ref_name = pull_request.source_ref_parts.name
863 source_ref_name = pull_request.source_ref_parts.name
862 source_ref_id = pull_request.source_ref_parts.commit_id
864 source_ref_id = pull_request.source_ref_parts.commit_id
863
865
864 target_ref_type = pull_request.target_ref_parts.type
866 target_ref_type = pull_request.target_ref_parts.type
865 target_ref_name = pull_request.target_ref_parts.name
867 target_ref_name = pull_request.target_ref_parts.name
866 target_ref_id = pull_request.target_ref_parts.commit_id
868 target_ref_id = pull_request.target_ref_parts.commit_id
867
869
868 if not self.has_valid_update_type(pull_request):
870 if not self.has_valid_update_type(pull_request):
869 log.debug("Skipping update of pull request %s due to ref type: %s",
871 log.debug("Skipping update of pull request %s due to ref type: %s",
870 pull_request, source_ref_type)
872 pull_request, source_ref_type)
871 return UpdateResponse(
873 return UpdateResponse(
872 executed=False,
874 executed=False,
873 reason=UpdateFailureReason.WRONG_REF_TYPE,
875 reason=UpdateFailureReason.WRONG_REF_TYPE,
874 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
876 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
875 source_changed=False, target_changed=False)
877 source_changed=False, target_changed=False)
876
878
877 try:
879 try:
878 source_commit, target_commit = self.get_flow_commits(pull_request)
880 source_commit, target_commit = self.get_flow_commits(pull_request)
879 except SourceRefMissing:
881 except SourceRefMissing:
880 return UpdateResponse(
882 return UpdateResponse(
881 executed=False,
883 executed=False,
882 reason=UpdateFailureReason.MISSING_SOURCE_REF,
884 reason=UpdateFailureReason.MISSING_SOURCE_REF,
883 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
885 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
884 source_changed=False, target_changed=False)
886 source_changed=False, target_changed=False)
885 except TargetRefMissing:
887 except TargetRefMissing:
886 return UpdateResponse(
888 return UpdateResponse(
887 executed=False,
889 executed=False,
888 reason=UpdateFailureReason.MISSING_TARGET_REF,
890 reason=UpdateFailureReason.MISSING_TARGET_REF,
889 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
891 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
890 source_changed=False, target_changed=False)
892 source_changed=False, target_changed=False)
891
893
892 source_changed = source_ref_id != source_commit.raw_id
894 source_changed = source_ref_id != source_commit.raw_id
893 target_changed = target_ref_id != target_commit.raw_id
895 target_changed = target_ref_id != target_commit.raw_id
894
896
895 if not (source_changed or target_changed):
897 if not (source_changed or target_changed):
896 log.debug("Nothing changed in pull request %s", pull_request)
898 log.debug("Nothing changed in pull request %s", pull_request)
897 return UpdateResponse(
899 return UpdateResponse(
898 executed=False,
900 executed=False,
899 reason=UpdateFailureReason.NO_CHANGE,
901 reason=UpdateFailureReason.NO_CHANGE,
900 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
902 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
901 source_changed=target_changed, target_changed=source_changed)
903 source_changed=target_changed, target_changed=source_changed)
902
904
903 change_in_found = 'target repo' if target_changed else 'source repo'
905 change_in_found = 'target repo' if target_changed else 'source repo'
904 log.debug('Updating pull request because of change in %s detected',
906 log.debug('Updating pull request because of change in %s detected',
905 change_in_found)
907 change_in_found)
906
908
907 # Finally there is a need for an update, in case of source change
909 # Finally there is a need for an update, in case of source change
908 # we create a new version, else just an update
910 # we create a new version, else just an update
909 if source_changed:
911 if source_changed:
910 pull_request_version = self._create_version_from_snapshot(pull_request)
912 pull_request_version = self._create_version_from_snapshot(pull_request)
911 self._link_comments_to_version(pull_request_version)
913 self._link_comments_to_version(pull_request_version)
912 else:
914 else:
913 try:
915 try:
914 ver = pull_request.versions[-1]
916 ver = pull_request.versions[-1]
915 except IndexError:
917 except IndexError:
916 ver = None
918 ver = None
917
919
918 pull_request.pull_request_version_id = \
920 pull_request.pull_request_version_id = \
919 ver.pull_request_version_id if ver else None
921 ver.pull_request_version_id if ver else None
920 pull_request_version = pull_request
922 pull_request_version = pull_request
921
923
922 source_repo = pull_request.source_repo.scm_instance()
924 source_repo = pull_request.source_repo.scm_instance()
923 target_repo = pull_request.target_repo.scm_instance()
925 target_repo = pull_request.target_repo.scm_instance()
924
926
925 # re-compute commit ids
927 # re-compute commit ids
926 old_commit_ids = pull_request.revisions
928 old_commit_ids = pull_request.revisions
927 pre_load = ["author", "date", "message", "branch"]
929 pre_load = ["author", "date", "message", "branch"]
928 commit_ranges = target_repo.compare(
930 commit_ranges = target_repo.compare(
929 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
931 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
930 pre_load=pre_load)
932 pre_load=pre_load)
931
933
932 target_ref = target_commit.raw_id
934 target_ref = target_commit.raw_id
933 source_ref = source_commit.raw_id
935 source_ref = source_commit.raw_id
934 ancestor_commit_id = target_repo.get_common_ancestor(
936 ancestor_commit_id = target_repo.get_common_ancestor(
935 target_ref, source_ref, source_repo)
937 target_ref, source_ref, source_repo)
936
938
937 if not ancestor_commit_id:
939 if not ancestor_commit_id:
938 raise ValueError(
940 raise ValueError(
939 'cannot calculate diff info without a common ancestor. '
941 'cannot calculate diff info without a common ancestor. '
940 'Make sure both repositories are related, and have a common forking commit.')
942 'Make sure both repositories are related, and have a common forking commit.')
941
943
942 pull_request.common_ancestor_id = ancestor_commit_id
944 pull_request.common_ancestor_id = ancestor_commit_id
943
945
944 pull_request.source_ref = '%s:%s:%s' % (
946 pull_request.source_ref = '%s:%s:%s' % (
945 source_ref_type, source_ref_name, source_commit.raw_id)
947 source_ref_type, source_ref_name, source_commit.raw_id)
946 pull_request.target_ref = '%s:%s:%s' % (
948 pull_request.target_ref = '%s:%s:%s' % (
947 target_ref_type, target_ref_name, ancestor_commit_id)
949 target_ref_type, target_ref_name, ancestor_commit_id)
948
950
949 pull_request.revisions = [
951 pull_request.revisions = [
950 commit.raw_id for commit in reversed(commit_ranges)]
952 commit.raw_id for commit in reversed(commit_ranges)]
951 pull_request.updated_on = datetime.datetime.now()
953 pull_request.updated_on = datetime.datetime.now()
952 Session().add(pull_request)
954 Session().add(pull_request)
953 new_commit_ids = pull_request.revisions
955 new_commit_ids = pull_request.revisions
954
956
955 old_diff_data, new_diff_data = self._generate_update_diffs(
957 old_diff_data, new_diff_data = self._generate_update_diffs(
956 pull_request, pull_request_version)
958 pull_request, pull_request_version)
957
959
958 # calculate commit and file changes
960 # calculate commit and file changes
959 commit_changes = self._calculate_commit_id_changes(
961 commit_changes = self._calculate_commit_id_changes(
960 old_commit_ids, new_commit_ids)
962 old_commit_ids, new_commit_ids)
961 file_changes = self._calculate_file_changes(
963 file_changes = self._calculate_file_changes(
962 old_diff_data, new_diff_data)
964 old_diff_data, new_diff_data)
963
965
964 # set comments as outdated if DIFFS changed
966 # set comments as outdated if DIFFS changed
965 CommentsModel().outdate_comments(
967 CommentsModel().outdate_comments(
966 pull_request, old_diff_data=old_diff_data,
968 pull_request, old_diff_data=old_diff_data,
967 new_diff_data=new_diff_data)
969 new_diff_data=new_diff_data)
968
970
969 valid_commit_changes = (commit_changes.added or commit_changes.removed)
971 valid_commit_changes = (commit_changes.added or commit_changes.removed)
970 file_node_changes = (
972 file_node_changes = (
971 file_changes.added or file_changes.modified or file_changes.removed)
973 file_changes.added or file_changes.modified or file_changes.removed)
972 pr_has_changes = valid_commit_changes or file_node_changes
974 pr_has_changes = valid_commit_changes or file_node_changes
973
975
974 # Add an automatic comment to the pull request, in case
976 # Add an automatic comment to the pull request, in case
975 # anything has changed
977 # anything has changed
976 if pr_has_changes:
978 if pr_has_changes:
977 update_comment = CommentsModel().create(
979 update_comment = CommentsModel().create(
978 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
980 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
979 repo=pull_request.target_repo,
981 repo=pull_request.target_repo,
980 user=pull_request.author,
982 user=pull_request.author,
981 pull_request=pull_request,
983 pull_request=pull_request,
982 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
984 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
983
985
984 # Update status to "Under Review" for added commits
986 # Update status to "Under Review" for added commits
985 for commit_id in commit_changes.added:
987 for commit_id in commit_changes.added:
986 ChangesetStatusModel().set_status(
988 ChangesetStatusModel().set_status(
987 repo=pull_request.source_repo,
989 repo=pull_request.source_repo,
988 status=ChangesetStatus.STATUS_UNDER_REVIEW,
990 status=ChangesetStatus.STATUS_UNDER_REVIEW,
989 comment=update_comment,
991 comment=update_comment,
990 user=pull_request.author,
992 user=pull_request.author,
991 pull_request=pull_request,
993 pull_request=pull_request,
992 revision=commit_id)
994 revision=commit_id)
993
995
994 # send update email to users
996 # send update email to users
995 try:
997 try:
996 self.notify_users(pull_request=pull_request, updating_user=updating_user,
998 self.notify_users(pull_request=pull_request, updating_user=updating_user,
997 ancestor_commit_id=ancestor_commit_id,
999 ancestor_commit_id=ancestor_commit_id,
998 commit_changes=commit_changes,
1000 commit_changes=commit_changes,
999 file_changes=file_changes)
1001 file_changes=file_changes)
1000 except Exception:
1002 except Exception:
1001 log.exception('Failed to send email notification to users')
1003 log.exception('Failed to send email notification to users')
1002
1004
1003 log.debug(
1005 log.debug(
1004 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1006 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1005 'removed_ids: %s', pull_request.pull_request_id,
1007 'removed_ids: %s', pull_request.pull_request_id,
1006 commit_changes.added, commit_changes.common, commit_changes.removed)
1008 commit_changes.added, commit_changes.common, commit_changes.removed)
1007 log.debug(
1009 log.debug(
1008 'Updated pull request with the following file changes: %s',
1010 'Updated pull request with the following file changes: %s',
1009 file_changes)
1011 file_changes)
1010
1012
1011 log.info(
1013 log.info(
1012 "Updated pull request %s from commit %s to commit %s, "
1014 "Updated pull request %s from commit %s to commit %s, "
1013 "stored new version %s of this pull request.",
1015 "stored new version %s of this pull request.",
1014 pull_request.pull_request_id, source_ref_id,
1016 pull_request.pull_request_id, source_ref_id,
1015 pull_request.source_ref_parts.commit_id,
1017 pull_request.source_ref_parts.commit_id,
1016 pull_request_version.pull_request_version_id)
1018 pull_request_version.pull_request_version_id)
1017 Session().commit()
1019 Session().commit()
1018 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1020 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1019
1021
1020 return UpdateResponse(
1022 return UpdateResponse(
1021 executed=True, reason=UpdateFailureReason.NONE,
1023 executed=True, reason=UpdateFailureReason.NONE,
1022 old=pull_request, new=pull_request_version,
1024 old=pull_request, new=pull_request_version,
1023 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1025 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1024 source_changed=source_changed, target_changed=target_changed)
1026 source_changed=source_changed, target_changed=target_changed)
1025
1027
1026 def _create_version_from_snapshot(self, pull_request):
1028 def _create_version_from_snapshot(self, pull_request):
1027 version = PullRequestVersion()
1029 version = PullRequestVersion()
1028 version.title = pull_request.title
1030 version.title = pull_request.title
1029 version.description = pull_request.description
1031 version.description = pull_request.description
1030 version.status = pull_request.status
1032 version.status = pull_request.status
1031 version.pull_request_state = pull_request.pull_request_state
1033 version.pull_request_state = pull_request.pull_request_state
1032 version.created_on = datetime.datetime.now()
1034 version.created_on = datetime.datetime.now()
1033 version.updated_on = pull_request.updated_on
1035 version.updated_on = pull_request.updated_on
1034 version.user_id = pull_request.user_id
1036 version.user_id = pull_request.user_id
1035 version.source_repo = pull_request.source_repo
1037 version.source_repo = pull_request.source_repo
1036 version.source_ref = pull_request.source_ref
1038 version.source_ref = pull_request.source_ref
1037 version.target_repo = pull_request.target_repo
1039 version.target_repo = pull_request.target_repo
1038 version.target_ref = pull_request.target_ref
1040 version.target_ref = pull_request.target_ref
1039
1041
1040 version._last_merge_source_rev = pull_request._last_merge_source_rev
1042 version._last_merge_source_rev = pull_request._last_merge_source_rev
1041 version._last_merge_target_rev = pull_request._last_merge_target_rev
1043 version._last_merge_target_rev = pull_request._last_merge_target_rev
1042 version.last_merge_status = pull_request.last_merge_status
1044 version.last_merge_status = pull_request.last_merge_status
1043 version.last_merge_metadata = pull_request.last_merge_metadata
1045 version.last_merge_metadata = pull_request.last_merge_metadata
1044 version.shadow_merge_ref = pull_request.shadow_merge_ref
1046 version.shadow_merge_ref = pull_request.shadow_merge_ref
1045 version.merge_rev = pull_request.merge_rev
1047 version.merge_rev = pull_request.merge_rev
1046 version.reviewer_data = pull_request.reviewer_data
1048 version.reviewer_data = pull_request.reviewer_data
1047
1049
1048 version.revisions = pull_request.revisions
1050 version.revisions = pull_request.revisions
1049 version.common_ancestor_id = pull_request.common_ancestor_id
1051 version.common_ancestor_id = pull_request.common_ancestor_id
1050 version.pull_request = pull_request
1052 version.pull_request = pull_request
1051 Session().add(version)
1053 Session().add(version)
1052 Session().flush()
1054 Session().flush()
1053
1055
1054 return version
1056 return version
1055
1057
1056 def _generate_update_diffs(self, pull_request, pull_request_version):
1058 def _generate_update_diffs(self, pull_request, pull_request_version):
1057
1059
1058 diff_context = (
1060 diff_context = (
1059 self.DIFF_CONTEXT +
1061 self.DIFF_CONTEXT +
1060 CommentsModel.needed_extra_diff_context())
1062 CommentsModel.needed_extra_diff_context())
1061 hide_whitespace_changes = False
1063 hide_whitespace_changes = False
1062 source_repo = pull_request_version.source_repo
1064 source_repo = pull_request_version.source_repo
1063 source_ref_id = pull_request_version.source_ref_parts.commit_id
1065 source_ref_id = pull_request_version.source_ref_parts.commit_id
1064 target_ref_id = pull_request_version.target_ref_parts.commit_id
1066 target_ref_id = pull_request_version.target_ref_parts.commit_id
1065 old_diff = self._get_diff_from_pr_or_version(
1067 old_diff = self._get_diff_from_pr_or_version(
1066 source_repo, source_ref_id, target_ref_id,
1068 source_repo, source_ref_id, target_ref_id,
1067 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1069 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1068
1070
1069 source_repo = pull_request.source_repo
1071 source_repo = pull_request.source_repo
1070 source_ref_id = pull_request.source_ref_parts.commit_id
1072 source_ref_id = pull_request.source_ref_parts.commit_id
1071 target_ref_id = pull_request.target_ref_parts.commit_id
1073 target_ref_id = pull_request.target_ref_parts.commit_id
1072
1074
1073 new_diff = self._get_diff_from_pr_or_version(
1075 new_diff = self._get_diff_from_pr_or_version(
1074 source_repo, source_ref_id, target_ref_id,
1076 source_repo, source_ref_id, target_ref_id,
1075 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1077 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1076
1078
1077 old_diff_data = diffs.DiffProcessor(old_diff)
1079 old_diff_data = diffs.DiffProcessor(old_diff)
1078 old_diff_data.prepare()
1080 old_diff_data.prepare()
1079 new_diff_data = diffs.DiffProcessor(new_diff)
1081 new_diff_data = diffs.DiffProcessor(new_diff)
1080 new_diff_data.prepare()
1082 new_diff_data.prepare()
1081
1083
1082 return old_diff_data, new_diff_data
1084 return old_diff_data, new_diff_data
1083
1085
1084 def _link_comments_to_version(self, pull_request_version):
1086 def _link_comments_to_version(self, pull_request_version):
1085 """
1087 """
1086 Link all unlinked comments of this pull request to the given version.
1088 Link all unlinked comments of this pull request to the given version.
1087
1089
1088 :param pull_request_version: The `PullRequestVersion` to which
1090 :param pull_request_version: The `PullRequestVersion` to which
1089 the comments shall be linked.
1091 the comments shall be linked.
1090
1092
1091 """
1093 """
1092 pull_request = pull_request_version.pull_request
1094 pull_request = pull_request_version.pull_request
1093 comments = ChangesetComment.query()\
1095 comments = ChangesetComment.query()\
1094 .filter(
1096 .filter(
1095 # TODO: johbo: Should we query for the repo at all here?
1097 # TODO: johbo: Should we query for the repo at all here?
1096 # Pending decision on how comments of PRs are to be related
1098 # Pending decision on how comments of PRs are to be related
1097 # to either the source repo, the target repo or no repo at all.
1099 # to either the source repo, the target repo or no repo at all.
1098 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1100 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1099 ChangesetComment.pull_request == pull_request,
1101 ChangesetComment.pull_request == pull_request,
1100 ChangesetComment.pull_request_version == None)\
1102 ChangesetComment.pull_request_version == None)\
1101 .order_by(ChangesetComment.comment_id.asc())
1103 .order_by(ChangesetComment.comment_id.asc())
1102
1104
1103 # TODO: johbo: Find out why this breaks if it is done in a bulk
1105 # TODO: johbo: Find out why this breaks if it is done in a bulk
1104 # operation.
1106 # operation.
1105 for comment in comments:
1107 for comment in comments:
1106 comment.pull_request_version_id = (
1108 comment.pull_request_version_id = (
1107 pull_request_version.pull_request_version_id)
1109 pull_request_version.pull_request_version_id)
1108 Session().add(comment)
1110 Session().add(comment)
1109
1111
1110 def _calculate_commit_id_changes(self, old_ids, new_ids):
1112 def _calculate_commit_id_changes(self, old_ids, new_ids):
1111 added = [x for x in new_ids if x not in old_ids]
1113 added = [x for x in new_ids if x not in old_ids]
1112 common = [x for x in new_ids if x in old_ids]
1114 common = [x for x in new_ids if x in old_ids]
1113 removed = [x for x in old_ids if x not in new_ids]
1115 removed = [x for x in old_ids if x not in new_ids]
1114 total = new_ids
1116 total = new_ids
1115 return ChangeTuple(added, common, removed, total)
1117 return ChangeTuple(added, common, removed, total)
1116
1118
1117 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1119 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1118
1120
1119 old_files = OrderedDict()
1121 old_files = OrderedDict()
1120 for diff_data in old_diff_data.parsed_diff:
1122 for diff_data in old_diff_data.parsed_diff:
1121 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1123 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1122
1124
1123 added_files = []
1125 added_files = []
1124 modified_files = []
1126 modified_files = []
1125 removed_files = []
1127 removed_files = []
1126 for diff_data in new_diff_data.parsed_diff:
1128 for diff_data in new_diff_data.parsed_diff:
1127 new_filename = diff_data['filename']
1129 new_filename = diff_data['filename']
1128 new_hash = md5_safe(diff_data['raw_diff'])
1130 new_hash = md5_safe(diff_data['raw_diff'])
1129
1131
1130 old_hash = old_files.get(new_filename)
1132 old_hash = old_files.get(new_filename)
1131 if not old_hash:
1133 if not old_hash:
1132 # file is not present in old diff, we have to figure out from parsed diff
1134 # file is not present in old diff, we have to figure out from parsed diff
1133 # operation ADD/REMOVE
1135 # operation ADD/REMOVE
1134 operations_dict = diff_data['stats']['ops']
1136 operations_dict = diff_data['stats']['ops']
1135 if diffs.DEL_FILENODE in operations_dict:
1137 if diffs.DEL_FILENODE in operations_dict:
1136 removed_files.append(new_filename)
1138 removed_files.append(new_filename)
1137 else:
1139 else:
1138 added_files.append(new_filename)
1140 added_files.append(new_filename)
1139 else:
1141 else:
1140 if new_hash != old_hash:
1142 if new_hash != old_hash:
1141 modified_files.append(new_filename)
1143 modified_files.append(new_filename)
1142 # now remove a file from old, since we have seen it already
1144 # now remove a file from old, since we have seen it already
1143 del old_files[new_filename]
1145 del old_files[new_filename]
1144
1146
1145 # removed files is when there are present in old, but not in NEW,
1147 # removed files is when there are present in old, but not in NEW,
1146 # since we remove old files that are present in new diff, left-overs
1148 # since we remove old files that are present in new diff, left-overs
1147 # if any should be the removed files
1149 # if any should be the removed files
1148 removed_files.extend(old_files.keys())
1150 removed_files.extend(old_files.keys())
1149
1151
1150 return FileChangeTuple(added_files, modified_files, removed_files)
1152 return FileChangeTuple(added_files, modified_files, removed_files)
1151
1153
1152 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1154 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1153 """
1155 """
1154 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1156 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1155 so it's always looking the same disregarding on which default
1157 so it's always looking the same disregarding on which default
1156 renderer system is using.
1158 renderer system is using.
1157
1159
1158 :param ancestor_commit_id: ancestor raw_id
1160 :param ancestor_commit_id: ancestor raw_id
1159 :param changes: changes named tuple
1161 :param changes: changes named tuple
1160 :param file_changes: file changes named tuple
1162 :param file_changes: file changes named tuple
1161
1163
1162 """
1164 """
1163 new_status = ChangesetStatus.get_status_lbl(
1165 new_status = ChangesetStatus.get_status_lbl(
1164 ChangesetStatus.STATUS_UNDER_REVIEW)
1166 ChangesetStatus.STATUS_UNDER_REVIEW)
1165
1167
1166 changed_files = (
1168 changed_files = (
1167 file_changes.added + file_changes.modified + file_changes.removed)
1169 file_changes.added + file_changes.modified + file_changes.removed)
1168
1170
1169 params = {
1171 params = {
1170 'under_review_label': new_status,
1172 'under_review_label': new_status,
1171 'added_commits': changes.added,
1173 'added_commits': changes.added,
1172 'removed_commits': changes.removed,
1174 'removed_commits': changes.removed,
1173 'changed_files': changed_files,
1175 'changed_files': changed_files,
1174 'added_files': file_changes.added,
1176 'added_files': file_changes.added,
1175 'modified_files': file_changes.modified,
1177 'modified_files': file_changes.modified,
1176 'removed_files': file_changes.removed,
1178 'removed_files': file_changes.removed,
1177 'ancestor_commit_id': ancestor_commit_id
1179 'ancestor_commit_id': ancestor_commit_id
1178 }
1180 }
1179 renderer = RstTemplateRenderer()
1181 renderer = RstTemplateRenderer()
1180 return renderer.render('pull_request_update.mako', **params)
1182 return renderer.render('pull_request_update.mako', **params)
1181
1183
1182 def edit(self, pull_request, title, description, description_renderer, user):
1184 def edit(self, pull_request, title, description, description_renderer, user):
1183 pull_request = self.__get_pull_request(pull_request)
1185 pull_request = self.__get_pull_request(pull_request)
1184 old_data = pull_request.get_api_data(with_merge_state=False)
1186 old_data = pull_request.get_api_data(with_merge_state=False)
1185 if pull_request.is_closed():
1187 if pull_request.is_closed():
1186 raise ValueError('This pull request is closed')
1188 raise ValueError('This pull request is closed')
1187 if title:
1189 if title:
1188 pull_request.title = title
1190 pull_request.title = title
1189 pull_request.description = description
1191 pull_request.description = description
1190 pull_request.updated_on = datetime.datetime.now()
1192 pull_request.updated_on = datetime.datetime.now()
1191 pull_request.description_renderer = description_renderer
1193 pull_request.description_renderer = description_renderer
1192 Session().add(pull_request)
1194 Session().add(pull_request)
1193 self._log_audit_action(
1195 self._log_audit_action(
1194 'repo.pull_request.edit', {'old_data': old_data},
1196 'repo.pull_request.edit', {'old_data': old_data},
1195 user, pull_request)
1197 user, pull_request)
1196
1198
1197 def update_reviewers(self, pull_request, reviewer_data, user):
1199 def update_reviewers(self, pull_request, reviewer_data, user):
1198 """
1200 """
1199 Update the reviewers in the pull request
1201 Update the reviewers in the pull request
1200
1202
1201 :param pull_request: the pr to update
1203 :param pull_request: the pr to update
1202 :param reviewer_data: list of tuples
1204 :param reviewer_data: list of tuples
1203 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1205 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1204 """
1206 """
1205 pull_request = self.__get_pull_request(pull_request)
1207 pull_request = self.__get_pull_request(pull_request)
1206 if pull_request.is_closed():
1208 if pull_request.is_closed():
1207 raise ValueError('This pull request is closed')
1209 raise ValueError('This pull request is closed')
1208
1210
1209 reviewers = {}
1211 reviewers = {}
1210 for user_id, reasons, mandatory, rules in reviewer_data:
1212 for user_id, reasons, mandatory, rules in reviewer_data:
1211 if isinstance(user_id, (int, compat.string_types)):
1213 if isinstance(user_id, (int, compat.string_types)):
1212 user_id = self._get_user(user_id).user_id
1214 user_id = self._get_user(user_id).user_id
1213 reviewers[user_id] = {
1215 reviewers[user_id] = {
1214 'reasons': reasons, 'mandatory': mandatory}
1216 'reasons': reasons, 'mandatory': mandatory}
1215
1217
1216 reviewers_ids = set(reviewers.keys())
1218 reviewers_ids = set(reviewers.keys())
1217 current_reviewers = PullRequestReviewers.query()\
1219 current_reviewers = PullRequestReviewers.query()\
1218 .filter(PullRequestReviewers.pull_request ==
1220 .filter(PullRequestReviewers.pull_request ==
1219 pull_request).all()
1221 pull_request).all()
1220 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1222 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1221
1223
1222 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1224 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1223 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1225 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1224
1226
1225 log.debug("Adding %s reviewers", ids_to_add)
1227 log.debug("Adding %s reviewers", ids_to_add)
1226 log.debug("Removing %s reviewers", ids_to_remove)
1228 log.debug("Removing %s reviewers", ids_to_remove)
1227 changed = False
1229 changed = False
1228 added_audit_reviewers = []
1230 added_audit_reviewers = []
1229 removed_audit_reviewers = []
1231 removed_audit_reviewers = []
1230
1232
1231 for uid in ids_to_add:
1233 for uid in ids_to_add:
1232 changed = True
1234 changed = True
1233 _usr = self._get_user(uid)
1235 _usr = self._get_user(uid)
1234 reviewer = PullRequestReviewers()
1236 reviewer = PullRequestReviewers()
1235 reviewer.user = _usr
1237 reviewer.user = _usr
1236 reviewer.pull_request = pull_request
1238 reviewer.pull_request = pull_request
1237 reviewer.reasons = reviewers[uid]['reasons']
1239 reviewer.reasons = reviewers[uid]['reasons']
1238 # NOTE(marcink): mandatory shouldn't be changed now
1240 # NOTE(marcink): mandatory shouldn't be changed now
1239 # reviewer.mandatory = reviewers[uid]['reasons']
1241 # reviewer.mandatory = reviewers[uid]['reasons']
1240 Session().add(reviewer)
1242 Session().add(reviewer)
1241 added_audit_reviewers.append(reviewer.get_dict())
1243 added_audit_reviewers.append(reviewer.get_dict())
1242
1244
1243 for uid in ids_to_remove:
1245 for uid in ids_to_remove:
1244 changed = True
1246 changed = True
1245 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1247 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1246 # that prevents and fixes cases that we added the same reviewer twice.
1248 # that prevents and fixes cases that we added the same reviewer twice.
1247 # this CAN happen due to the lack of DB checks
1249 # this CAN happen due to the lack of DB checks
1248 reviewers = PullRequestReviewers.query()\
1250 reviewers = PullRequestReviewers.query()\
1249 .filter(PullRequestReviewers.user_id == uid,
1251 .filter(PullRequestReviewers.user_id == uid,
1250 PullRequestReviewers.pull_request == pull_request)\
1252 PullRequestReviewers.pull_request == pull_request)\
1251 .all()
1253 .all()
1252
1254
1253 for obj in reviewers:
1255 for obj in reviewers:
1254 added_audit_reviewers.append(obj.get_dict())
1256 added_audit_reviewers.append(obj.get_dict())
1255 Session().delete(obj)
1257 Session().delete(obj)
1256
1258
1257 if changed:
1259 if changed:
1258 Session().expire_all()
1260 Session().expire_all()
1259 pull_request.updated_on = datetime.datetime.now()
1261 pull_request.updated_on = datetime.datetime.now()
1260 Session().add(pull_request)
1262 Session().add(pull_request)
1261
1263
1262 # finally store audit logs
1264 # finally store audit logs
1263 for user_data in added_audit_reviewers:
1265 for user_data in added_audit_reviewers:
1264 self._log_audit_action(
1266 self._log_audit_action(
1265 'repo.pull_request.reviewer.add', {'data': user_data},
1267 'repo.pull_request.reviewer.add', {'data': user_data},
1266 user, pull_request)
1268 user, pull_request)
1267 for user_data in removed_audit_reviewers:
1269 for user_data in removed_audit_reviewers:
1268 self._log_audit_action(
1270 self._log_audit_action(
1269 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1271 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1270 user, pull_request)
1272 user, pull_request)
1271
1273
1272 self.notify_reviewers(pull_request, ids_to_add)
1274 self.notify_reviewers(pull_request, ids_to_add)
1273 return ids_to_add, ids_to_remove
1275 return ids_to_add, ids_to_remove
1274
1276
1275 def get_url(self, pull_request, request=None, permalink=False):
1277 def get_url(self, pull_request, request=None, permalink=False):
1276 if not request:
1278 if not request:
1277 request = get_current_request()
1279 request = get_current_request()
1278
1280
1279 if permalink:
1281 if permalink:
1280 return request.route_url(
1282 return request.route_url(
1281 'pull_requests_global',
1283 'pull_requests_global',
1282 pull_request_id=pull_request.pull_request_id,)
1284 pull_request_id=pull_request.pull_request_id,)
1283 else:
1285 else:
1284 return request.route_url('pullrequest_show',
1286 return request.route_url('pullrequest_show',
1285 repo_name=safe_str(pull_request.target_repo.repo_name),
1287 repo_name=safe_str(pull_request.target_repo.repo_name),
1286 pull_request_id=pull_request.pull_request_id,)
1288 pull_request_id=pull_request.pull_request_id,)
1287
1289
1288 def get_shadow_clone_url(self, pull_request, request=None):
1290 def get_shadow_clone_url(self, pull_request, request=None):
1289 """
1291 """
1290 Returns qualified url pointing to the shadow repository. If this pull
1292 Returns qualified url pointing to the shadow repository. If this pull
1291 request is closed there is no shadow repository and ``None`` will be
1293 request is closed there is no shadow repository and ``None`` will be
1292 returned.
1294 returned.
1293 """
1295 """
1294 if pull_request.is_closed():
1296 if pull_request.is_closed():
1295 return None
1297 return None
1296 else:
1298 else:
1297 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1299 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1298 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1300 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1299
1301
1300 def notify_reviewers(self, pull_request, reviewers_ids):
1302 def notify_reviewers(self, pull_request, reviewers_ids):
1301 # notification to reviewers
1303 # notification to reviewers
1302 if not reviewers_ids:
1304 if not reviewers_ids:
1303 return
1305 return
1304
1306
1305 log.debug('Notify following reviewers about pull-request %s', reviewers_ids)
1307 log.debug('Notify following reviewers about pull-request %s', reviewers_ids)
1306
1308
1307 pull_request_obj = pull_request
1309 pull_request_obj = pull_request
1308 # get the current participants of this pull request
1310 # get the current participants of this pull request
1309 recipients = reviewers_ids
1311 recipients = reviewers_ids
1310 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1312 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1311
1313
1312 pr_source_repo = pull_request_obj.source_repo
1314 pr_source_repo = pull_request_obj.source_repo
1313 pr_target_repo = pull_request_obj.target_repo
1315 pr_target_repo = pull_request_obj.target_repo
1314
1316
1315 pr_url = h.route_url('pullrequest_show',
1317 pr_url = h.route_url('pullrequest_show',
1316 repo_name=pr_target_repo.repo_name,
1318 repo_name=pr_target_repo.repo_name,
1317 pull_request_id=pull_request_obj.pull_request_id,)
1319 pull_request_id=pull_request_obj.pull_request_id,)
1318
1320
1319 # set some variables for email notification
1321 # set some variables for email notification
1320 pr_target_repo_url = h.route_url(
1322 pr_target_repo_url = h.route_url(
1321 'repo_summary', repo_name=pr_target_repo.repo_name)
1323 'repo_summary', repo_name=pr_target_repo.repo_name)
1322
1324
1323 pr_source_repo_url = h.route_url(
1325 pr_source_repo_url = h.route_url(
1324 'repo_summary', repo_name=pr_source_repo.repo_name)
1326 'repo_summary', repo_name=pr_source_repo.repo_name)
1325
1327
1326 # pull request specifics
1328 # pull request specifics
1327 pull_request_commits = [
1329 pull_request_commits = [
1328 (x.raw_id, x.message)
1330 (x.raw_id, x.message)
1329 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1331 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1330
1332
1331 kwargs = {
1333 kwargs = {
1332 'user': pull_request.author,
1334 'user': pull_request.author,
1333 'pull_request': pull_request_obj,
1335 'pull_request': pull_request_obj,
1334 'pull_request_commits': pull_request_commits,
1336 'pull_request_commits': pull_request_commits,
1335
1337
1336 'pull_request_target_repo': pr_target_repo,
1338 'pull_request_target_repo': pr_target_repo,
1337 'pull_request_target_repo_url': pr_target_repo_url,
1339 'pull_request_target_repo_url': pr_target_repo_url,
1338
1340
1339 'pull_request_source_repo': pr_source_repo,
1341 'pull_request_source_repo': pr_source_repo,
1340 'pull_request_source_repo_url': pr_source_repo_url,
1342 'pull_request_source_repo_url': pr_source_repo_url,
1341
1343
1342 'pull_request_url': pr_url,
1344 'pull_request_url': pr_url,
1343 }
1345 }
1344
1346
1345 # pre-generate the subject for notification itself
1347 # pre-generate the subject for notification itself
1346 (subject,
1348 (subject,
1347 _h, _e, # we don't care about those
1349 _h, _e, # we don't care about those
1348 body_plaintext) = EmailNotificationModel().render_email(
1350 body_plaintext) = EmailNotificationModel().render_email(
1349 notification_type, **kwargs)
1351 notification_type, **kwargs)
1350
1352
1351 # create notification objects, and emails
1353 # create notification objects, and emails
1352 NotificationModel().create(
1354 NotificationModel().create(
1353 created_by=pull_request.author,
1355 created_by=pull_request.author,
1354 notification_subject=subject,
1356 notification_subject=subject,
1355 notification_body=body_plaintext,
1357 notification_body=body_plaintext,
1356 notification_type=notification_type,
1358 notification_type=notification_type,
1357 recipients=recipients,
1359 recipients=recipients,
1358 email_kwargs=kwargs,
1360 email_kwargs=kwargs,
1359 )
1361 )
1360
1362
1361 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1363 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1362 commit_changes, file_changes):
1364 commit_changes, file_changes):
1363
1365
1364 updating_user_id = updating_user.user_id
1366 updating_user_id = updating_user.user_id
1365 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1367 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1366 # NOTE(marcink): send notification to all other users except to
1368 # NOTE(marcink): send notification to all other users except to
1367 # person who updated the PR
1369 # person who updated the PR
1368 recipients = reviewers.difference(set([updating_user_id]))
1370 recipients = reviewers.difference(set([updating_user_id]))
1369
1371
1370 log.debug('Notify following recipients about pull-request update %s', recipients)
1372 log.debug('Notify following recipients about pull-request update %s', recipients)
1371
1373
1372 pull_request_obj = pull_request
1374 pull_request_obj = pull_request
1373
1375
1374 # send email about the update
1376 # send email about the update
1375 changed_files = (
1377 changed_files = (
1376 file_changes.added + file_changes.modified + file_changes.removed)
1378 file_changes.added + file_changes.modified + file_changes.removed)
1377
1379
1378 pr_source_repo = pull_request_obj.source_repo
1380 pr_source_repo = pull_request_obj.source_repo
1379 pr_target_repo = pull_request_obj.target_repo
1381 pr_target_repo = pull_request_obj.target_repo
1380
1382
1381 pr_url = h.route_url('pullrequest_show',
1383 pr_url = h.route_url('pullrequest_show',
1382 repo_name=pr_target_repo.repo_name,
1384 repo_name=pr_target_repo.repo_name,
1383 pull_request_id=pull_request_obj.pull_request_id,)
1385 pull_request_id=pull_request_obj.pull_request_id,)
1384
1386
1385 # set some variables for email notification
1387 # set some variables for email notification
1386 pr_target_repo_url = h.route_url(
1388 pr_target_repo_url = h.route_url(
1387 'repo_summary', repo_name=pr_target_repo.repo_name)
1389 'repo_summary', repo_name=pr_target_repo.repo_name)
1388
1390
1389 pr_source_repo_url = h.route_url(
1391 pr_source_repo_url = h.route_url(
1390 'repo_summary', repo_name=pr_source_repo.repo_name)
1392 'repo_summary', repo_name=pr_source_repo.repo_name)
1391
1393
1392 email_kwargs = {
1394 email_kwargs = {
1393 'date': datetime.datetime.now(),
1395 'date': datetime.datetime.now(),
1394 'updating_user': updating_user,
1396 'updating_user': updating_user,
1395
1397
1396 'pull_request': pull_request_obj,
1398 'pull_request': pull_request_obj,
1397
1399
1398 'pull_request_target_repo': pr_target_repo,
1400 'pull_request_target_repo': pr_target_repo,
1399 'pull_request_target_repo_url': pr_target_repo_url,
1401 'pull_request_target_repo_url': pr_target_repo_url,
1400
1402
1401 'pull_request_source_repo': pr_source_repo,
1403 'pull_request_source_repo': pr_source_repo,
1402 'pull_request_source_repo_url': pr_source_repo_url,
1404 'pull_request_source_repo_url': pr_source_repo_url,
1403
1405
1404 'pull_request_url': pr_url,
1406 'pull_request_url': pr_url,
1405
1407
1406 'ancestor_commit_id': ancestor_commit_id,
1408 'ancestor_commit_id': ancestor_commit_id,
1407 'added_commits': commit_changes.added,
1409 'added_commits': commit_changes.added,
1408 'removed_commits': commit_changes.removed,
1410 'removed_commits': commit_changes.removed,
1409 'changed_files': changed_files,
1411 'changed_files': changed_files,
1410 'added_files': file_changes.added,
1412 'added_files': file_changes.added,
1411 'modified_files': file_changes.modified,
1413 'modified_files': file_changes.modified,
1412 'removed_files': file_changes.removed,
1414 'removed_files': file_changes.removed,
1413 }
1415 }
1414
1416
1415 (subject,
1417 (subject,
1416 _h, _e, # we don't care about those
1418 _h, _e, # we don't care about those
1417 body_plaintext) = EmailNotificationModel().render_email(
1419 body_plaintext) = EmailNotificationModel().render_email(
1418 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1420 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1419
1421
1420 # create notification objects, and emails
1422 # create notification objects, and emails
1421 NotificationModel().create(
1423 NotificationModel().create(
1422 created_by=updating_user,
1424 created_by=updating_user,
1423 notification_subject=subject,
1425 notification_subject=subject,
1424 notification_body=body_plaintext,
1426 notification_body=body_plaintext,
1425 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1427 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1426 recipients=recipients,
1428 recipients=recipients,
1427 email_kwargs=email_kwargs,
1429 email_kwargs=email_kwargs,
1428 )
1430 )
1429
1431
1430 def delete(self, pull_request, user):
1432 def delete(self, pull_request, user=None):
1433 if not user:
1434 user = getattr(get_current_rhodecode_user(), 'username', None)
1435
1431 pull_request = self.__get_pull_request(pull_request)
1436 pull_request = self.__get_pull_request(pull_request)
1432 old_data = pull_request.get_api_data(with_merge_state=False)
1437 old_data = pull_request.get_api_data(with_merge_state=False)
1433 self._cleanup_merge_workspace(pull_request)
1438 self._cleanup_merge_workspace(pull_request)
1434 self._log_audit_action(
1439 self._log_audit_action(
1435 'repo.pull_request.delete', {'old_data': old_data},
1440 'repo.pull_request.delete', {'old_data': old_data},
1436 user, pull_request)
1441 user, pull_request)
1437 Session().delete(pull_request)
1442 Session().delete(pull_request)
1438
1443
1439 def close_pull_request(self, pull_request, user):
1444 def close_pull_request(self, pull_request, user):
1440 pull_request = self.__get_pull_request(pull_request)
1445 pull_request = self.__get_pull_request(pull_request)
1441 self._cleanup_merge_workspace(pull_request)
1446 self._cleanup_merge_workspace(pull_request)
1442 pull_request.status = PullRequest.STATUS_CLOSED
1447 pull_request.status = PullRequest.STATUS_CLOSED
1443 pull_request.updated_on = datetime.datetime.now()
1448 pull_request.updated_on = datetime.datetime.now()
1444 Session().add(pull_request)
1449 Session().add(pull_request)
1445 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1450 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1446
1451
1447 pr_data = pull_request.get_api_data(with_merge_state=False)
1452 pr_data = pull_request.get_api_data(with_merge_state=False)
1448 self._log_audit_action(
1453 self._log_audit_action(
1449 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1454 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1450
1455
1451 def close_pull_request_with_comment(
1456 def close_pull_request_with_comment(
1452 self, pull_request, user, repo, message=None, auth_user=None):
1457 self, pull_request, user, repo, message=None, auth_user=None):
1453
1458
1454 pull_request_review_status = pull_request.calculated_review_status()
1459 pull_request_review_status = pull_request.calculated_review_status()
1455
1460
1456 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1461 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1457 # approved only if we have voting consent
1462 # approved only if we have voting consent
1458 status = ChangesetStatus.STATUS_APPROVED
1463 status = ChangesetStatus.STATUS_APPROVED
1459 else:
1464 else:
1460 status = ChangesetStatus.STATUS_REJECTED
1465 status = ChangesetStatus.STATUS_REJECTED
1461 status_lbl = ChangesetStatus.get_status_lbl(status)
1466 status_lbl = ChangesetStatus.get_status_lbl(status)
1462
1467
1463 default_message = (
1468 default_message = (
1464 'Closing with status change {transition_icon} {status}.'
1469 'Closing with status change {transition_icon} {status}.'
1465 ).format(transition_icon='>', status=status_lbl)
1470 ).format(transition_icon='>', status=status_lbl)
1466 text = message or default_message
1471 text = message or default_message
1467
1472
1468 # create a comment, and link it to new status
1473 # create a comment, and link it to new status
1469 comment = CommentsModel().create(
1474 comment = CommentsModel().create(
1470 text=text,
1475 text=text,
1471 repo=repo.repo_id,
1476 repo=repo.repo_id,
1472 user=user.user_id,
1477 user=user.user_id,
1473 pull_request=pull_request.pull_request_id,
1478 pull_request=pull_request.pull_request_id,
1474 status_change=status_lbl,
1479 status_change=status_lbl,
1475 status_change_type=status,
1480 status_change_type=status,
1476 closing_pr=True,
1481 closing_pr=True,
1477 auth_user=auth_user,
1482 auth_user=auth_user,
1478 )
1483 )
1479
1484
1480 # calculate old status before we change it
1485 # calculate old status before we change it
1481 old_calculated_status = pull_request.calculated_review_status()
1486 old_calculated_status = pull_request.calculated_review_status()
1482 ChangesetStatusModel().set_status(
1487 ChangesetStatusModel().set_status(
1483 repo.repo_id,
1488 repo.repo_id,
1484 status,
1489 status,
1485 user.user_id,
1490 user.user_id,
1486 comment=comment,
1491 comment=comment,
1487 pull_request=pull_request.pull_request_id
1492 pull_request=pull_request.pull_request_id
1488 )
1493 )
1489
1494
1490 Session().flush()
1495 Session().flush()
1491
1496
1492 self.trigger_pull_request_hook(pull_request, user, 'comment',
1497 self.trigger_pull_request_hook(pull_request, user, 'comment',
1493 data={'comment': comment})
1498 data={'comment': comment})
1494
1499
1495 # we now calculate the status of pull request again, and based on that
1500 # we now calculate the status of pull request again, and based on that
1496 # calculation trigger status change. This might happen in cases
1501 # calculation trigger status change. This might happen in cases
1497 # that non-reviewer admin closes a pr, which means his vote doesn't
1502 # that non-reviewer admin closes a pr, which means his vote doesn't
1498 # change the status, while if he's a reviewer this might change it.
1503 # change the status, while if he's a reviewer this might change it.
1499 calculated_status = pull_request.calculated_review_status()
1504 calculated_status = pull_request.calculated_review_status()
1500 if old_calculated_status != calculated_status:
1505 if old_calculated_status != calculated_status:
1501 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1506 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1502 data={'status': calculated_status})
1507 data={'status': calculated_status})
1503
1508
1504 # finally close the PR
1509 # finally close the PR
1505 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1510 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1506
1511
1507 return comment, status
1512 return comment, status
1508
1513
1509 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1514 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1510 _ = translator or get_current_request().translate
1515 _ = translator or get_current_request().translate
1511
1516
1512 if not self._is_merge_enabled(pull_request):
1517 if not self._is_merge_enabled(pull_request):
1513 return None, False, _('Server-side pull request merging is disabled.')
1518 return None, False, _('Server-side pull request merging is disabled.')
1514
1519
1515 if pull_request.is_closed():
1520 if pull_request.is_closed():
1516 return None, False, _('This pull request is closed.')
1521 return None, False, _('This pull request is closed.')
1517
1522
1518 merge_possible, msg = self._check_repo_requirements(
1523 merge_possible, msg = self._check_repo_requirements(
1519 target=pull_request.target_repo, source=pull_request.source_repo,
1524 target=pull_request.target_repo, source=pull_request.source_repo,
1520 translator=_)
1525 translator=_)
1521 if not merge_possible:
1526 if not merge_possible:
1522 return None, merge_possible, msg
1527 return None, merge_possible, msg
1523
1528
1524 try:
1529 try:
1525 merge_response = self._try_merge(
1530 merge_response = self._try_merge(
1526 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1531 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1527 log.debug("Merge response: %s", merge_response)
1532 log.debug("Merge response: %s", merge_response)
1528 return merge_response, merge_response.possible, merge_response.merge_status_message
1533 return merge_response, merge_response.possible, merge_response.merge_status_message
1529 except NotImplementedError:
1534 except NotImplementedError:
1530 return None, False, _('Pull request merging is not supported.')
1535 return None, False, _('Pull request merging is not supported.')
1531
1536
1532 def _check_repo_requirements(self, target, source, translator):
1537 def _check_repo_requirements(self, target, source, translator):
1533 """
1538 """
1534 Check if `target` and `source` have compatible requirements.
1539 Check if `target` and `source` have compatible requirements.
1535
1540
1536 Currently this is just checking for largefiles.
1541 Currently this is just checking for largefiles.
1537 """
1542 """
1538 _ = translator
1543 _ = translator
1539 target_has_largefiles = self._has_largefiles(target)
1544 target_has_largefiles = self._has_largefiles(target)
1540 source_has_largefiles = self._has_largefiles(source)
1545 source_has_largefiles = self._has_largefiles(source)
1541 merge_possible = True
1546 merge_possible = True
1542 message = u''
1547 message = u''
1543
1548
1544 if target_has_largefiles != source_has_largefiles:
1549 if target_has_largefiles != source_has_largefiles:
1545 merge_possible = False
1550 merge_possible = False
1546 if source_has_largefiles:
1551 if source_has_largefiles:
1547 message = _(
1552 message = _(
1548 'Target repository large files support is disabled.')
1553 'Target repository large files support is disabled.')
1549 else:
1554 else:
1550 message = _(
1555 message = _(
1551 'Source repository large files support is disabled.')
1556 'Source repository large files support is disabled.')
1552
1557
1553 return merge_possible, message
1558 return merge_possible, message
1554
1559
1555 def _has_largefiles(self, repo):
1560 def _has_largefiles(self, repo):
1556 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1561 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1557 'extensions', 'largefiles')
1562 'extensions', 'largefiles')
1558 return largefiles_ui and largefiles_ui[0].active
1563 return largefiles_ui and largefiles_ui[0].active
1559
1564
1560 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1565 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1561 """
1566 """
1562 Try to merge the pull request and return the merge status.
1567 Try to merge the pull request and return the merge status.
1563 """
1568 """
1564 log.debug(
1569 log.debug(
1565 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1570 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1566 pull_request.pull_request_id, force_shadow_repo_refresh)
1571 pull_request.pull_request_id, force_shadow_repo_refresh)
1567 target_vcs = pull_request.target_repo.scm_instance()
1572 target_vcs = pull_request.target_repo.scm_instance()
1568 # Refresh the target reference.
1573 # Refresh the target reference.
1569 try:
1574 try:
1570 target_ref = self._refresh_reference(
1575 target_ref = self._refresh_reference(
1571 pull_request.target_ref_parts, target_vcs)
1576 pull_request.target_ref_parts, target_vcs)
1572 except CommitDoesNotExistError:
1577 except CommitDoesNotExistError:
1573 merge_state = MergeResponse(
1578 merge_state = MergeResponse(
1574 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1579 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1575 metadata={'target_ref': pull_request.target_ref_parts})
1580 metadata={'target_ref': pull_request.target_ref_parts})
1576 return merge_state
1581 return merge_state
1577
1582
1578 target_locked = pull_request.target_repo.locked
1583 target_locked = pull_request.target_repo.locked
1579 if target_locked and target_locked[0]:
1584 if target_locked and target_locked[0]:
1580 locked_by = 'user:{}'.format(target_locked[0])
1585 locked_by = 'user:{}'.format(target_locked[0])
1581 log.debug("The target repository is locked by %s.", locked_by)
1586 log.debug("The target repository is locked by %s.", locked_by)
1582 merge_state = MergeResponse(
1587 merge_state = MergeResponse(
1583 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1588 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1584 metadata={'locked_by': locked_by})
1589 metadata={'locked_by': locked_by})
1585 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1590 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1586 pull_request, target_ref):
1591 pull_request, target_ref):
1587 log.debug("Refreshing the merge status of the repository.")
1592 log.debug("Refreshing the merge status of the repository.")
1588 merge_state = self._refresh_merge_state(
1593 merge_state = self._refresh_merge_state(
1589 pull_request, target_vcs, target_ref)
1594 pull_request, target_vcs, target_ref)
1590 else:
1595 else:
1591 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1596 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1592 metadata = {
1597 metadata = {
1593 'unresolved_files': '',
1598 'unresolved_files': '',
1594 'target_ref': pull_request.target_ref_parts,
1599 'target_ref': pull_request.target_ref_parts,
1595 'source_ref': pull_request.source_ref_parts,
1600 'source_ref': pull_request.source_ref_parts,
1596 }
1601 }
1597 if pull_request.last_merge_metadata:
1602 if pull_request.last_merge_metadata:
1598 metadata.update(pull_request.last_merge_metadata)
1603 metadata.update(pull_request.last_merge_metadata)
1599
1604
1600 if not possible and target_ref.type == 'branch':
1605 if not possible and target_ref.type == 'branch':
1601 # NOTE(marcink): case for mercurial multiple heads on branch
1606 # NOTE(marcink): case for mercurial multiple heads on branch
1602 heads = target_vcs._heads(target_ref.name)
1607 heads = target_vcs._heads(target_ref.name)
1603 if len(heads) != 1:
1608 if len(heads) != 1:
1604 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1609 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1605 metadata.update({
1610 metadata.update({
1606 'heads': heads
1611 'heads': heads
1607 })
1612 })
1608
1613
1609 merge_state = MergeResponse(
1614 merge_state = MergeResponse(
1610 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1615 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1611
1616
1612 return merge_state
1617 return merge_state
1613
1618
1614 def _refresh_reference(self, reference, vcs_repository):
1619 def _refresh_reference(self, reference, vcs_repository):
1615 if reference.type in self.UPDATABLE_REF_TYPES:
1620 if reference.type in self.UPDATABLE_REF_TYPES:
1616 name_or_id = reference.name
1621 name_or_id = reference.name
1617 else:
1622 else:
1618 name_or_id = reference.commit_id
1623 name_or_id = reference.commit_id
1619
1624
1620 refreshed_commit = vcs_repository.get_commit(name_or_id)
1625 refreshed_commit = vcs_repository.get_commit(name_or_id)
1621 refreshed_reference = Reference(
1626 refreshed_reference = Reference(
1622 reference.type, reference.name, refreshed_commit.raw_id)
1627 reference.type, reference.name, refreshed_commit.raw_id)
1623 return refreshed_reference
1628 return refreshed_reference
1624
1629
1625 def _needs_merge_state_refresh(self, pull_request, target_reference):
1630 def _needs_merge_state_refresh(self, pull_request, target_reference):
1626 return not(
1631 return not(
1627 pull_request.revisions and
1632 pull_request.revisions and
1628 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1633 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1629 target_reference.commit_id == pull_request._last_merge_target_rev)
1634 target_reference.commit_id == pull_request._last_merge_target_rev)
1630
1635
1631 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1636 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1632 workspace_id = self._workspace_id(pull_request)
1637 workspace_id = self._workspace_id(pull_request)
1633 source_vcs = pull_request.source_repo.scm_instance()
1638 source_vcs = pull_request.source_repo.scm_instance()
1634 repo_id = pull_request.target_repo.repo_id
1639 repo_id = pull_request.target_repo.repo_id
1635 use_rebase = self._use_rebase_for_merging(pull_request)
1640 use_rebase = self._use_rebase_for_merging(pull_request)
1636 close_branch = self._close_branch_before_merging(pull_request)
1641 close_branch = self._close_branch_before_merging(pull_request)
1637 merge_state = target_vcs.merge(
1642 merge_state = target_vcs.merge(
1638 repo_id, workspace_id,
1643 repo_id, workspace_id,
1639 target_reference, source_vcs, pull_request.source_ref_parts,
1644 target_reference, source_vcs, pull_request.source_ref_parts,
1640 dry_run=True, use_rebase=use_rebase,
1645 dry_run=True, use_rebase=use_rebase,
1641 close_branch=close_branch)
1646 close_branch=close_branch)
1642
1647
1643 # Do not store the response if there was an unknown error.
1648 # Do not store the response if there was an unknown error.
1644 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1649 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1645 pull_request._last_merge_source_rev = \
1650 pull_request._last_merge_source_rev = \
1646 pull_request.source_ref_parts.commit_id
1651 pull_request.source_ref_parts.commit_id
1647 pull_request._last_merge_target_rev = target_reference.commit_id
1652 pull_request._last_merge_target_rev = target_reference.commit_id
1648 pull_request.last_merge_status = merge_state.failure_reason
1653 pull_request.last_merge_status = merge_state.failure_reason
1649 pull_request.last_merge_metadata = merge_state.metadata
1654 pull_request.last_merge_metadata = merge_state.metadata
1650
1655
1651 pull_request.shadow_merge_ref = merge_state.merge_ref
1656 pull_request.shadow_merge_ref = merge_state.merge_ref
1652 Session().add(pull_request)
1657 Session().add(pull_request)
1653 Session().commit()
1658 Session().commit()
1654
1659
1655 return merge_state
1660 return merge_state
1656
1661
1657 def _workspace_id(self, pull_request):
1662 def _workspace_id(self, pull_request):
1658 workspace_id = 'pr-%s' % pull_request.pull_request_id
1663 workspace_id = 'pr-%s' % pull_request.pull_request_id
1659 return workspace_id
1664 return workspace_id
1660
1665
1661 def generate_repo_data(self, repo, commit_id=None, branch=None,
1666 def generate_repo_data(self, repo, commit_id=None, branch=None,
1662 bookmark=None, translator=None):
1667 bookmark=None, translator=None):
1663 from rhodecode.model.repo import RepoModel
1668 from rhodecode.model.repo import RepoModel
1664
1669
1665 all_refs, selected_ref = \
1670 all_refs, selected_ref = \
1666 self._get_repo_pullrequest_sources(
1671 self._get_repo_pullrequest_sources(
1667 repo.scm_instance(), commit_id=commit_id,
1672 repo.scm_instance(), commit_id=commit_id,
1668 branch=branch, bookmark=bookmark, translator=translator)
1673 branch=branch, bookmark=bookmark, translator=translator)
1669
1674
1670 refs_select2 = []
1675 refs_select2 = []
1671 for element in all_refs:
1676 for element in all_refs:
1672 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1677 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1673 refs_select2.append({'text': element[1], 'children': children})
1678 refs_select2.append({'text': element[1], 'children': children})
1674
1679
1675 return {
1680 return {
1676 'user': {
1681 'user': {
1677 'user_id': repo.user.user_id,
1682 'user_id': repo.user.user_id,
1678 'username': repo.user.username,
1683 'username': repo.user.username,
1679 'firstname': repo.user.first_name,
1684 'firstname': repo.user.first_name,
1680 'lastname': repo.user.last_name,
1685 'lastname': repo.user.last_name,
1681 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1686 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1682 },
1687 },
1683 'name': repo.repo_name,
1688 'name': repo.repo_name,
1684 'link': RepoModel().get_url(repo),
1689 'link': RepoModel().get_url(repo),
1685 'description': h.chop_at_smart(repo.description_safe, '\n'),
1690 'description': h.chop_at_smart(repo.description_safe, '\n'),
1686 'refs': {
1691 'refs': {
1687 'all_refs': all_refs,
1692 'all_refs': all_refs,
1688 'selected_ref': selected_ref,
1693 'selected_ref': selected_ref,
1689 'select2_refs': refs_select2
1694 'select2_refs': refs_select2
1690 }
1695 }
1691 }
1696 }
1692
1697
1693 def generate_pullrequest_title(self, source, source_ref, target):
1698 def generate_pullrequest_title(self, source, source_ref, target):
1694 return u'{source}#{at_ref} to {target}'.format(
1699 return u'{source}#{at_ref} to {target}'.format(
1695 source=source,
1700 source=source,
1696 at_ref=source_ref,
1701 at_ref=source_ref,
1697 target=target,
1702 target=target,
1698 )
1703 )
1699
1704
1700 def _cleanup_merge_workspace(self, pull_request):
1705 def _cleanup_merge_workspace(self, pull_request):
1701 # Merging related cleanup
1706 # Merging related cleanup
1702 repo_id = pull_request.target_repo.repo_id
1707 repo_id = pull_request.target_repo.repo_id
1703 target_scm = pull_request.target_repo.scm_instance()
1708 target_scm = pull_request.target_repo.scm_instance()
1704 workspace_id = self._workspace_id(pull_request)
1709 workspace_id = self._workspace_id(pull_request)
1705
1710
1706 try:
1711 try:
1707 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1712 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1708 except NotImplementedError:
1713 except NotImplementedError:
1709 pass
1714 pass
1710
1715
1711 def _get_repo_pullrequest_sources(
1716 def _get_repo_pullrequest_sources(
1712 self, repo, commit_id=None, branch=None, bookmark=None,
1717 self, repo, commit_id=None, branch=None, bookmark=None,
1713 translator=None):
1718 translator=None):
1714 """
1719 """
1715 Return a structure with repo's interesting commits, suitable for
1720 Return a structure with repo's interesting commits, suitable for
1716 the selectors in pullrequest controller
1721 the selectors in pullrequest controller
1717
1722
1718 :param commit_id: a commit that must be in the list somehow
1723 :param commit_id: a commit that must be in the list somehow
1719 and selected by default
1724 and selected by default
1720 :param branch: a branch that must be in the list and selected
1725 :param branch: a branch that must be in the list and selected
1721 by default - even if closed
1726 by default - even if closed
1722 :param bookmark: a bookmark that must be in the list and selected
1727 :param bookmark: a bookmark that must be in the list and selected
1723 """
1728 """
1724 _ = translator or get_current_request().translate
1729 _ = translator or get_current_request().translate
1725
1730
1726 commit_id = safe_str(commit_id) if commit_id else None
1731 commit_id = safe_str(commit_id) if commit_id else None
1727 branch = safe_unicode(branch) if branch else None
1732 branch = safe_unicode(branch) if branch else None
1728 bookmark = safe_unicode(bookmark) if bookmark else None
1733 bookmark = safe_unicode(bookmark) if bookmark else None
1729
1734
1730 selected = None
1735 selected = None
1731
1736
1732 # order matters: first source that has commit_id in it will be selected
1737 # order matters: first source that has commit_id in it will be selected
1733 sources = []
1738 sources = []
1734 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1739 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1735 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1740 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1736
1741
1737 if commit_id:
1742 if commit_id:
1738 ref_commit = (h.short_id(commit_id), commit_id)
1743 ref_commit = (h.short_id(commit_id), commit_id)
1739 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1744 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1740
1745
1741 sources.append(
1746 sources.append(
1742 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1747 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1743 )
1748 )
1744
1749
1745 groups = []
1750 groups = []
1746
1751
1747 for group_key, ref_list, group_name, match in sources:
1752 for group_key, ref_list, group_name, match in sources:
1748 group_refs = []
1753 group_refs = []
1749 for ref_name, ref_id in ref_list:
1754 for ref_name, ref_id in ref_list:
1750 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1755 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1751 group_refs.append((ref_key, ref_name))
1756 group_refs.append((ref_key, ref_name))
1752
1757
1753 if not selected:
1758 if not selected:
1754 if set([commit_id, match]) & set([ref_id, ref_name]):
1759 if set([commit_id, match]) & set([ref_id, ref_name]):
1755 selected = ref_key
1760 selected = ref_key
1756
1761
1757 if group_refs:
1762 if group_refs:
1758 groups.append((group_refs, group_name))
1763 groups.append((group_refs, group_name))
1759
1764
1760 if not selected:
1765 if not selected:
1761 ref = commit_id or branch or bookmark
1766 ref = commit_id or branch or bookmark
1762 if ref:
1767 if ref:
1763 raise CommitDoesNotExistError(
1768 raise CommitDoesNotExistError(
1764 u'No commit refs could be found matching: {}'.format(ref))
1769 u'No commit refs could be found matching: {}'.format(ref))
1765 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1770 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1766 selected = u'branch:{}:{}'.format(
1771 selected = u'branch:{}:{}'.format(
1767 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1772 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1768 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1773 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1769 )
1774 )
1770 elif repo.commit_ids:
1775 elif repo.commit_ids:
1771 # make the user select in this case
1776 # make the user select in this case
1772 selected = None
1777 selected = None
1773 else:
1778 else:
1774 raise EmptyRepositoryError()
1779 raise EmptyRepositoryError()
1775 return groups, selected
1780 return groups, selected
1776
1781
1777 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1782 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1778 hide_whitespace_changes, diff_context):
1783 hide_whitespace_changes, diff_context):
1779
1784
1780 return self._get_diff_from_pr_or_version(
1785 return self._get_diff_from_pr_or_version(
1781 source_repo, source_ref_id, target_ref_id,
1786 source_repo, source_ref_id, target_ref_id,
1782 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1787 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1783
1788
1784 def _get_diff_from_pr_or_version(
1789 def _get_diff_from_pr_or_version(
1785 self, source_repo, source_ref_id, target_ref_id,
1790 self, source_repo, source_ref_id, target_ref_id,
1786 hide_whitespace_changes, diff_context):
1791 hide_whitespace_changes, diff_context):
1787
1792
1788 target_commit = source_repo.get_commit(
1793 target_commit = source_repo.get_commit(
1789 commit_id=safe_str(target_ref_id))
1794 commit_id=safe_str(target_ref_id))
1790 source_commit = source_repo.get_commit(
1795 source_commit = source_repo.get_commit(
1791 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1796 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1792 if isinstance(source_repo, Repository):
1797 if isinstance(source_repo, Repository):
1793 vcs_repo = source_repo.scm_instance()
1798 vcs_repo = source_repo.scm_instance()
1794 else:
1799 else:
1795 vcs_repo = source_repo
1800 vcs_repo = source_repo
1796
1801
1797 # TODO: johbo: In the context of an update, we cannot reach
1802 # TODO: johbo: In the context of an update, we cannot reach
1798 # the old commit anymore with our normal mechanisms. It needs
1803 # the old commit anymore with our normal mechanisms. It needs
1799 # some sort of special support in the vcs layer to avoid this
1804 # some sort of special support in the vcs layer to avoid this
1800 # workaround.
1805 # workaround.
1801 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1806 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1802 vcs_repo.alias == 'git'):
1807 vcs_repo.alias == 'git'):
1803 source_commit.raw_id = safe_str(source_ref_id)
1808 source_commit.raw_id = safe_str(source_ref_id)
1804
1809
1805 log.debug('calculating diff between '
1810 log.debug('calculating diff between '
1806 'source_ref:%s and target_ref:%s for repo `%s`',
1811 'source_ref:%s and target_ref:%s for repo `%s`',
1807 target_ref_id, source_ref_id,
1812 target_ref_id, source_ref_id,
1808 safe_unicode(vcs_repo.path))
1813 safe_unicode(vcs_repo.path))
1809
1814
1810 vcs_diff = vcs_repo.get_diff(
1815 vcs_diff = vcs_repo.get_diff(
1811 commit1=target_commit, commit2=source_commit,
1816 commit1=target_commit, commit2=source_commit,
1812 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1817 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1813 return vcs_diff
1818 return vcs_diff
1814
1819
1815 def _is_merge_enabled(self, pull_request):
1820 def _is_merge_enabled(self, pull_request):
1816 return self._get_general_setting(
1821 return self._get_general_setting(
1817 pull_request, 'rhodecode_pr_merge_enabled')
1822 pull_request, 'rhodecode_pr_merge_enabled')
1818
1823
1819 def _use_rebase_for_merging(self, pull_request):
1824 def _use_rebase_for_merging(self, pull_request):
1820 repo_type = pull_request.target_repo.repo_type
1825 repo_type = pull_request.target_repo.repo_type
1821 if repo_type == 'hg':
1826 if repo_type == 'hg':
1822 return self._get_general_setting(
1827 return self._get_general_setting(
1823 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1828 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1824 elif repo_type == 'git':
1829 elif repo_type == 'git':
1825 return self._get_general_setting(
1830 return self._get_general_setting(
1826 pull_request, 'rhodecode_git_use_rebase_for_merging')
1831 pull_request, 'rhodecode_git_use_rebase_for_merging')
1827
1832
1828 return False
1833 return False
1829
1834
1830 def _user_name_for_merging(self, pull_request, user):
1835 def _user_name_for_merging(self, pull_request, user):
1831 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1836 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1832 if env_user_name_attr and hasattr(user, env_user_name_attr):
1837 if env_user_name_attr and hasattr(user, env_user_name_attr):
1833 user_name_attr = env_user_name_attr
1838 user_name_attr = env_user_name_attr
1834 else:
1839 else:
1835 user_name_attr = 'short_contact'
1840 user_name_attr = 'short_contact'
1836
1841
1837 user_name = getattr(user, user_name_attr)
1842 user_name = getattr(user, user_name_attr)
1838 return user_name
1843 return user_name
1839
1844
1840 def _close_branch_before_merging(self, pull_request):
1845 def _close_branch_before_merging(self, pull_request):
1841 repo_type = pull_request.target_repo.repo_type
1846 repo_type = pull_request.target_repo.repo_type
1842 if repo_type == 'hg':
1847 if repo_type == 'hg':
1843 return self._get_general_setting(
1848 return self._get_general_setting(
1844 pull_request, 'rhodecode_hg_close_branch_before_merging')
1849 pull_request, 'rhodecode_hg_close_branch_before_merging')
1845 elif repo_type == 'git':
1850 elif repo_type == 'git':
1846 return self._get_general_setting(
1851 return self._get_general_setting(
1847 pull_request, 'rhodecode_git_close_branch_before_merging')
1852 pull_request, 'rhodecode_git_close_branch_before_merging')
1848
1853
1849 return False
1854 return False
1850
1855
1851 def _get_general_setting(self, pull_request, settings_key, default=False):
1856 def _get_general_setting(self, pull_request, settings_key, default=False):
1852 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1857 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1853 settings = settings_model.get_general_settings()
1858 settings = settings_model.get_general_settings()
1854 return settings.get(settings_key, default)
1859 return settings.get(settings_key, default)
1855
1860
1856 def _log_audit_action(self, action, action_data, user, pull_request):
1861 def _log_audit_action(self, action, action_data, user, pull_request):
1857 audit_logger.store(
1862 audit_logger.store(
1858 action=action,
1863 action=action,
1859 action_data=action_data,
1864 action_data=action_data,
1860 user=user,
1865 user=user,
1861 repo=pull_request.target_repo)
1866 repo=pull_request.target_repo)
1862
1867
1863 def get_reviewer_functions(self):
1868 def get_reviewer_functions(self):
1864 """
1869 """
1865 Fetches functions for validation and fetching default reviewers.
1870 Fetches functions for validation and fetching default reviewers.
1866 If available we use the EE package, else we fallback to CE
1871 If available we use the EE package, else we fallback to CE
1867 package functions
1872 package functions
1868 """
1873 """
1869 try:
1874 try:
1870 from rc_reviewers.utils import get_default_reviewers_data
1875 from rc_reviewers.utils import get_default_reviewers_data
1871 from rc_reviewers.utils import validate_default_reviewers
1876 from rc_reviewers.utils import validate_default_reviewers
1872 except ImportError:
1877 except ImportError:
1873 from rhodecode.apps.repository.utils import get_default_reviewers_data
1878 from rhodecode.apps.repository.utils import get_default_reviewers_data
1874 from rhodecode.apps.repository.utils import validate_default_reviewers
1879 from rhodecode.apps.repository.utils import validate_default_reviewers
1875
1880
1876 return get_default_reviewers_data, validate_default_reviewers
1881 return get_default_reviewers_data, validate_default_reviewers
1877
1882
1878
1883
1879 class MergeCheck(object):
1884 class MergeCheck(object):
1880 """
1885 """
1881 Perform Merge Checks and returns a check object which stores information
1886 Perform Merge Checks and returns a check object which stores information
1882 about merge errors, and merge conditions
1887 about merge errors, and merge conditions
1883 """
1888 """
1884 TODO_CHECK = 'todo'
1889 TODO_CHECK = 'todo'
1885 PERM_CHECK = 'perm'
1890 PERM_CHECK = 'perm'
1886 REVIEW_CHECK = 'review'
1891 REVIEW_CHECK = 'review'
1887 MERGE_CHECK = 'merge'
1892 MERGE_CHECK = 'merge'
1888 WIP_CHECK = 'wip'
1893 WIP_CHECK = 'wip'
1889
1894
1890 def __init__(self):
1895 def __init__(self):
1891 self.review_status = None
1896 self.review_status = None
1892 self.merge_possible = None
1897 self.merge_possible = None
1893 self.merge_msg = ''
1898 self.merge_msg = ''
1894 self.merge_response = None
1899 self.merge_response = None
1895 self.failed = None
1900 self.failed = None
1896 self.errors = []
1901 self.errors = []
1897 self.error_details = OrderedDict()
1902 self.error_details = OrderedDict()
1898 self.source_commit = AttributeDict()
1903 self.source_commit = AttributeDict()
1899 self.target_commit = AttributeDict()
1904 self.target_commit = AttributeDict()
1900
1905
1901 def __repr__(self):
1906 def __repr__(self):
1902 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
1907 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
1903 self.merge_possible, self.failed, self.errors)
1908 self.merge_possible, self.failed, self.errors)
1904
1909
1905 def push_error(self, error_type, message, error_key, details):
1910 def push_error(self, error_type, message, error_key, details):
1906 self.failed = True
1911 self.failed = True
1907 self.errors.append([error_type, message])
1912 self.errors.append([error_type, message])
1908 self.error_details[error_key] = dict(
1913 self.error_details[error_key] = dict(
1909 details=details,
1914 details=details,
1910 error_type=error_type,
1915 error_type=error_type,
1911 message=message
1916 message=message
1912 )
1917 )
1913
1918
1914 @classmethod
1919 @classmethod
1915 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1920 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1916 force_shadow_repo_refresh=False):
1921 force_shadow_repo_refresh=False):
1917 _ = translator
1922 _ = translator
1918 merge_check = cls()
1923 merge_check = cls()
1919
1924
1920 # title has WIP:
1925 # title has WIP:
1921 if pull_request.work_in_progress:
1926 if pull_request.work_in_progress:
1922 log.debug("MergeCheck: cannot merge, title has wip: marker.")
1927 log.debug("MergeCheck: cannot merge, title has wip: marker.")
1923
1928
1924 msg = _('WIP marker in title prevents from accidental merge.')
1929 msg = _('WIP marker in title prevents from accidental merge.')
1925 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
1930 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
1926 if fail_early:
1931 if fail_early:
1927 return merge_check
1932 return merge_check
1928
1933
1929 # permissions to merge
1934 # permissions to merge
1930 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
1935 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
1931 if not user_allowed_to_merge:
1936 if not user_allowed_to_merge:
1932 log.debug("MergeCheck: cannot merge, approval is pending.")
1937 log.debug("MergeCheck: cannot merge, approval is pending.")
1933
1938
1934 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1939 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1935 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1940 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1936 if fail_early:
1941 if fail_early:
1937 return merge_check
1942 return merge_check
1938
1943
1939 # permission to merge into the target branch
1944 # permission to merge into the target branch
1940 target_commit_id = pull_request.target_ref_parts.commit_id
1945 target_commit_id = pull_request.target_ref_parts.commit_id
1941 if pull_request.target_ref_parts.type == 'branch':
1946 if pull_request.target_ref_parts.type == 'branch':
1942 branch_name = pull_request.target_ref_parts.name
1947 branch_name = pull_request.target_ref_parts.name
1943 else:
1948 else:
1944 # for mercurial we can always figure out the branch from the commit
1949 # for mercurial we can always figure out the branch from the commit
1945 # in case of bookmark
1950 # in case of bookmark
1946 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1951 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1947 branch_name = target_commit.branch
1952 branch_name = target_commit.branch
1948
1953
1949 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1954 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1950 pull_request.target_repo.repo_name, branch_name)
1955 pull_request.target_repo.repo_name, branch_name)
1951 if branch_perm and branch_perm == 'branch.none':
1956 if branch_perm and branch_perm == 'branch.none':
1952 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1957 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1953 branch_name, rule)
1958 branch_name, rule)
1954 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1959 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1955 if fail_early:
1960 if fail_early:
1956 return merge_check
1961 return merge_check
1957
1962
1958 # review status, must be always present
1963 # review status, must be always present
1959 review_status = pull_request.calculated_review_status()
1964 review_status = pull_request.calculated_review_status()
1960 merge_check.review_status = review_status
1965 merge_check.review_status = review_status
1961
1966
1962 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1967 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1963 if not status_approved:
1968 if not status_approved:
1964 log.debug("MergeCheck: cannot merge, approval is pending.")
1969 log.debug("MergeCheck: cannot merge, approval is pending.")
1965
1970
1966 msg = _('Pull request reviewer approval is pending.')
1971 msg = _('Pull request reviewer approval is pending.')
1967
1972
1968 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1973 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1969
1974
1970 if fail_early:
1975 if fail_early:
1971 return merge_check
1976 return merge_check
1972
1977
1973 # left over TODOs
1978 # left over TODOs
1974 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1979 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1975 if todos:
1980 if todos:
1976 log.debug("MergeCheck: cannot merge, {} "
1981 log.debug("MergeCheck: cannot merge, {} "
1977 "unresolved TODOs left.".format(len(todos)))
1982 "unresolved TODOs left.".format(len(todos)))
1978
1983
1979 if len(todos) == 1:
1984 if len(todos) == 1:
1980 msg = _('Cannot merge, {} TODO still not resolved.').format(
1985 msg = _('Cannot merge, {} TODO still not resolved.').format(
1981 len(todos))
1986 len(todos))
1982 else:
1987 else:
1983 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1988 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1984 len(todos))
1989 len(todos))
1985
1990
1986 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1991 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1987
1992
1988 if fail_early:
1993 if fail_early:
1989 return merge_check
1994 return merge_check
1990
1995
1991 # merge possible, here is the filesystem simulation + shadow repo
1996 # merge possible, here is the filesystem simulation + shadow repo
1992 merge_response, merge_status, msg = PullRequestModel().merge_status(
1997 merge_response, merge_status, msg = PullRequestModel().merge_status(
1993 pull_request, translator=translator,
1998 pull_request, translator=translator,
1994 force_shadow_repo_refresh=force_shadow_repo_refresh)
1999 force_shadow_repo_refresh=force_shadow_repo_refresh)
1995
2000
1996 merge_check.merge_possible = merge_status
2001 merge_check.merge_possible = merge_status
1997 merge_check.merge_msg = msg
2002 merge_check.merge_msg = msg
1998 merge_check.merge_response = merge_response
2003 merge_check.merge_response = merge_response
1999
2004
2000 source_ref_id = pull_request.source_ref_parts.commit_id
2005 source_ref_id = pull_request.source_ref_parts.commit_id
2001 target_ref_id = pull_request.target_ref_parts.commit_id
2006 target_ref_id = pull_request.target_ref_parts.commit_id
2002
2007
2003 try:
2008 try:
2004 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2009 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2005 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2010 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2006 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2011 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2007 merge_check.source_commit.current_raw_id = source_commit.raw_id
2012 merge_check.source_commit.current_raw_id = source_commit.raw_id
2008 merge_check.source_commit.previous_raw_id = source_ref_id
2013 merge_check.source_commit.previous_raw_id = source_ref_id
2009
2014
2010 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2015 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2011 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2016 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2012 merge_check.target_commit.current_raw_id = target_commit.raw_id
2017 merge_check.target_commit.current_raw_id = target_commit.raw_id
2013 merge_check.target_commit.previous_raw_id = target_ref_id
2018 merge_check.target_commit.previous_raw_id = target_ref_id
2014 except (SourceRefMissing, TargetRefMissing):
2019 except (SourceRefMissing, TargetRefMissing):
2015 pass
2020 pass
2016
2021
2017 if not merge_status:
2022 if not merge_status:
2018 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2023 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2019 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2024 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2020
2025
2021 if fail_early:
2026 if fail_early:
2022 return merge_check
2027 return merge_check
2023
2028
2024 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2029 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2025 return merge_check
2030 return merge_check
2026
2031
2027 @classmethod
2032 @classmethod
2028 def get_merge_conditions(cls, pull_request, translator):
2033 def get_merge_conditions(cls, pull_request, translator):
2029 _ = translator
2034 _ = translator
2030 merge_details = {}
2035 merge_details = {}
2031
2036
2032 model = PullRequestModel()
2037 model = PullRequestModel()
2033 use_rebase = model._use_rebase_for_merging(pull_request)
2038 use_rebase = model._use_rebase_for_merging(pull_request)
2034
2039
2035 if use_rebase:
2040 if use_rebase:
2036 merge_details['merge_strategy'] = dict(
2041 merge_details['merge_strategy'] = dict(
2037 details={},
2042 details={},
2038 message=_('Merge strategy: rebase')
2043 message=_('Merge strategy: rebase')
2039 )
2044 )
2040 else:
2045 else:
2041 merge_details['merge_strategy'] = dict(
2046 merge_details['merge_strategy'] = dict(
2042 details={},
2047 details={},
2043 message=_('Merge strategy: explicit merge commit')
2048 message=_('Merge strategy: explicit merge commit')
2044 )
2049 )
2045
2050
2046 close_branch = model._close_branch_before_merging(pull_request)
2051 close_branch = model._close_branch_before_merging(pull_request)
2047 if close_branch:
2052 if close_branch:
2048 repo_type = pull_request.target_repo.repo_type
2053 repo_type = pull_request.target_repo.repo_type
2049 close_msg = ''
2054 close_msg = ''
2050 if repo_type == 'hg':
2055 if repo_type == 'hg':
2051 close_msg = _('Source branch will be closed after merge.')
2056 close_msg = _('Source branch will be closed after merge.')
2052 elif repo_type == 'git':
2057 elif repo_type == 'git':
2053 close_msg = _('Source branch will be deleted after merge.')
2058 close_msg = _('Source branch will be deleted after merge.')
2054
2059
2055 merge_details['close_branch'] = dict(
2060 merge_details['close_branch'] = dict(
2056 details={},
2061 details={},
2057 message=close_msg
2062 message=close_msg
2058 )
2063 )
2059
2064
2060 return merge_details
2065 return merge_details
2061
2066
2062
2067
2063 ChangeTuple = collections.namedtuple(
2068 ChangeTuple = collections.namedtuple(
2064 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2069 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2065
2070
2066 FileChangeTuple = collections.namedtuple(
2071 FileChangeTuple = collections.namedtuple(
2067 'FileChangeTuple', ['added', 'modified', 'removed'])
2072 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,1011 +1,1051 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 users model for RhodeCode
22 users model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import datetime
27 import datetime
28 import ipaddress
28 import ipaddress
29
29
30 from pyramid.threadlocal import get_current_request
30 from pyramid.threadlocal import get_current_request
31 from sqlalchemy.exc import DatabaseError
31 from sqlalchemy.exc import DatabaseError
32
32
33 from rhodecode import events
33 from rhodecode import events
34 from rhodecode.lib.user_log_filter import user_log_filter
34 from rhodecode.lib.user_log_filter import user_log_filter
35 from rhodecode.lib.utils2 import (
35 from rhodecode.lib.utils2 import (
36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
37 AttributeDict, str2bool)
37 AttributeDict, str2bool)
38 from rhodecode.lib.exceptions import (
38 from rhodecode.lib.exceptions import (
39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
40 UserOwnsUserGroupsException, NotAllowedToCreateUserError, UserOwnsArtifactsException)
40 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
41 UserOwnsPullRequestsException, UserOwnsArtifactsException)
41 from rhodecode.lib.caching_query import FromCache
42 from rhodecode.lib.caching_query import FromCache
42 from rhodecode.model import BaseModel
43 from rhodecode.model import BaseModel
43 from rhodecode.model.auth_token import AuthTokenModel
44 from rhodecode.model.db import (
44 from rhodecode.model.db import (
45 _hash_key, true, false, or_, joinedload, User, UserToPerm,
45 _hash_key, true, false, or_, joinedload, User, UserToPerm,
46 UserEmailMap, UserIpMap, UserLog)
46 UserEmailMap, UserIpMap, UserLog)
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.auth_token import AuthTokenModel
48 from rhodecode.model.repo_group import RepoGroupModel
49 from rhodecode.model.repo_group import RepoGroupModel
49
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 class UserModel(BaseModel):
54 class UserModel(BaseModel):
55 cls = User
55 cls = User
56
56
57 def get(self, user_id, cache=False):
57 def get(self, user_id, cache=False):
58 user = self.sa.query(User)
58 user = self.sa.query(User)
59 if cache:
59 if cache:
60 user = user.options(
60 user = user.options(
61 FromCache("sql_cache_short", "get_user_%s" % user_id))
61 FromCache("sql_cache_short", "get_user_%s" % user_id))
62 return user.get(user_id)
62 return user.get(user_id)
63
63
64 def get_user(self, user):
64 def get_user(self, user):
65 return self._get_user(user)
65 return self._get_user(user)
66
66
67 def _serialize_user(self, user):
67 def _serialize_user(self, user):
68 import rhodecode.lib.helpers as h
68 import rhodecode.lib.helpers as h
69
69
70 return {
70 return {
71 'id': user.user_id,
71 'id': user.user_id,
72 'first_name': user.first_name,
72 'first_name': user.first_name,
73 'last_name': user.last_name,
73 'last_name': user.last_name,
74 'username': user.username,
74 'username': user.username,
75 'email': user.email,
75 'email': user.email,
76 'icon_link': h.gravatar_url(user.email, 30),
76 'icon_link': h.gravatar_url(user.email, 30),
77 'profile_link': h.link_to_user(user),
77 'profile_link': h.link_to_user(user),
78 'value_display': h.escape(h.person(user)),
78 'value_display': h.escape(h.person(user)),
79 'value': user.username,
79 'value': user.username,
80 'value_type': 'user',
80 'value_type': 'user',
81 'active': user.active,
81 'active': user.active,
82 }
82 }
83
83
84 def get_users(self, name_contains=None, limit=20, only_active=True):
84 def get_users(self, name_contains=None, limit=20, only_active=True):
85
85
86 query = self.sa.query(User)
86 query = self.sa.query(User)
87 if only_active:
87 if only_active:
88 query = query.filter(User.active == true())
88 query = query.filter(User.active == true())
89
89
90 if name_contains:
90 if name_contains:
91 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
91 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
92 query = query.filter(
92 query = query.filter(
93 or_(
93 or_(
94 User.name.ilike(ilike_expression),
94 User.name.ilike(ilike_expression),
95 User.lastname.ilike(ilike_expression),
95 User.lastname.ilike(ilike_expression),
96 User.username.ilike(ilike_expression)
96 User.username.ilike(ilike_expression)
97 )
97 )
98 )
98 )
99 query = query.limit(limit)
99 query = query.limit(limit)
100 users = query.all()
100 users = query.all()
101
101
102 _users = [
102 _users = [
103 self._serialize_user(user) for user in users
103 self._serialize_user(user) for user in users
104 ]
104 ]
105 return _users
105 return _users
106
106
107 def get_by_username(self, username, cache=False, case_insensitive=False):
107 def get_by_username(self, username, cache=False, case_insensitive=False):
108
108
109 if case_insensitive:
109 if case_insensitive:
110 user = self.sa.query(User).filter(User.username.ilike(username))
110 user = self.sa.query(User).filter(User.username.ilike(username))
111 else:
111 else:
112 user = self.sa.query(User)\
112 user = self.sa.query(User)\
113 .filter(User.username == username)
113 .filter(User.username == username)
114 if cache:
114 if cache:
115 name_key = _hash_key(username)
115 name_key = _hash_key(username)
116 user = user.options(
116 user = user.options(
117 FromCache("sql_cache_short", "get_user_%s" % name_key))
117 FromCache("sql_cache_short", "get_user_%s" % name_key))
118 return user.scalar()
118 return user.scalar()
119
119
120 def get_by_email(self, email, cache=False, case_insensitive=False):
120 def get_by_email(self, email, cache=False, case_insensitive=False):
121 return User.get_by_email(email, case_insensitive, cache)
121 return User.get_by_email(email, case_insensitive, cache)
122
122
123 def get_by_auth_token(self, auth_token, cache=False):
123 def get_by_auth_token(self, auth_token, cache=False):
124 return User.get_by_auth_token(auth_token, cache)
124 return User.get_by_auth_token(auth_token, cache)
125
125
126 def get_active_user_count(self, cache=False):
126 def get_active_user_count(self, cache=False):
127 qry = User.query().filter(
127 qry = User.query().filter(
128 User.active == true()).filter(
128 User.active == true()).filter(
129 User.username != User.DEFAULT_USER)
129 User.username != User.DEFAULT_USER)
130 if cache:
130 if cache:
131 qry = qry.options(
131 qry = qry.options(
132 FromCache("sql_cache_short", "get_active_users"))
132 FromCache("sql_cache_short", "get_active_users"))
133 return qry.count()
133 return qry.count()
134
134
135 def create(self, form_data, cur_user=None):
135 def create(self, form_data, cur_user=None):
136 if not cur_user:
136 if not cur_user:
137 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
137 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
138
138
139 user_data = {
139 user_data = {
140 'username': form_data['username'],
140 'username': form_data['username'],
141 'password': form_data['password'],
141 'password': form_data['password'],
142 'email': form_data['email'],
142 'email': form_data['email'],
143 'firstname': form_data['firstname'],
143 'firstname': form_data['firstname'],
144 'lastname': form_data['lastname'],
144 'lastname': form_data['lastname'],
145 'active': form_data['active'],
145 'active': form_data['active'],
146 'extern_type': form_data['extern_type'],
146 'extern_type': form_data['extern_type'],
147 'extern_name': form_data['extern_name'],
147 'extern_name': form_data['extern_name'],
148 'admin': False,
148 'admin': False,
149 'cur_user': cur_user
149 'cur_user': cur_user
150 }
150 }
151
151
152 if 'create_repo_group' in form_data:
152 if 'create_repo_group' in form_data:
153 user_data['create_repo_group'] = str2bool(
153 user_data['create_repo_group'] = str2bool(
154 form_data.get('create_repo_group'))
154 form_data.get('create_repo_group'))
155
155
156 try:
156 try:
157 if form_data.get('password_change'):
157 if form_data.get('password_change'):
158 user_data['force_password_change'] = True
158 user_data['force_password_change'] = True
159 return UserModel().create_or_update(**user_data)
159 return UserModel().create_or_update(**user_data)
160 except Exception:
160 except Exception:
161 log.error(traceback.format_exc())
161 log.error(traceback.format_exc())
162 raise
162 raise
163
163
164 def update_user(self, user, skip_attrs=None, **kwargs):
164 def update_user(self, user, skip_attrs=None, **kwargs):
165 from rhodecode.lib.auth import get_crypt_password
165 from rhodecode.lib.auth import get_crypt_password
166
166
167 user = self._get_user(user)
167 user = self._get_user(user)
168 if user.username == User.DEFAULT_USER:
168 if user.username == User.DEFAULT_USER:
169 raise DefaultUserException(
169 raise DefaultUserException(
170 "You can't edit this user (`%(username)s`) since it's "
170 "You can't edit this user (`%(username)s`) since it's "
171 "crucial for entire application" % {
171 "crucial for entire application" % {
172 'username': user.username})
172 'username': user.username})
173
173
174 # first store only defaults
174 # first store only defaults
175 user_attrs = {
175 user_attrs = {
176 'updating_user_id': user.user_id,
176 'updating_user_id': user.user_id,
177 'username': user.username,
177 'username': user.username,
178 'password': user.password,
178 'password': user.password,
179 'email': user.email,
179 'email': user.email,
180 'firstname': user.name,
180 'firstname': user.name,
181 'lastname': user.lastname,
181 'lastname': user.lastname,
182 'description': user.description,
182 'description': user.description,
183 'active': user.active,
183 'active': user.active,
184 'admin': user.admin,
184 'admin': user.admin,
185 'extern_name': user.extern_name,
185 'extern_name': user.extern_name,
186 'extern_type': user.extern_type,
186 'extern_type': user.extern_type,
187 'language': user.user_data.get('language')
187 'language': user.user_data.get('language')
188 }
188 }
189
189
190 # in case there's new_password, that comes from form, use it to
190 # in case there's new_password, that comes from form, use it to
191 # store password
191 # store password
192 if kwargs.get('new_password'):
192 if kwargs.get('new_password'):
193 kwargs['password'] = kwargs['new_password']
193 kwargs['password'] = kwargs['new_password']
194
194
195 # cleanups, my_account password change form
195 # cleanups, my_account password change form
196 kwargs.pop('current_password', None)
196 kwargs.pop('current_password', None)
197 kwargs.pop('new_password', None)
197 kwargs.pop('new_password', None)
198
198
199 # cleanups, user edit password change form
199 # cleanups, user edit password change form
200 kwargs.pop('password_confirmation', None)
200 kwargs.pop('password_confirmation', None)
201 kwargs.pop('password_change', None)
201 kwargs.pop('password_change', None)
202
202
203 # create repo group on user creation
203 # create repo group on user creation
204 kwargs.pop('create_repo_group', None)
204 kwargs.pop('create_repo_group', None)
205
205
206 # legacy forms send name, which is the firstname
206 # legacy forms send name, which is the firstname
207 firstname = kwargs.pop('name', None)
207 firstname = kwargs.pop('name', None)
208 if firstname:
208 if firstname:
209 kwargs['firstname'] = firstname
209 kwargs['firstname'] = firstname
210
210
211 for k, v in kwargs.items():
211 for k, v in kwargs.items():
212 # skip if we don't want to update this
212 # skip if we don't want to update this
213 if skip_attrs and k in skip_attrs:
213 if skip_attrs and k in skip_attrs:
214 continue
214 continue
215
215
216 user_attrs[k] = v
216 user_attrs[k] = v
217
217
218 try:
218 try:
219 return self.create_or_update(**user_attrs)
219 return self.create_or_update(**user_attrs)
220 except Exception:
220 except Exception:
221 log.error(traceback.format_exc())
221 log.error(traceback.format_exc())
222 raise
222 raise
223
223
224 def create_or_update(
224 def create_or_update(
225 self, username, password, email, firstname='', lastname='',
225 self, username, password, email, firstname='', lastname='',
226 active=True, admin=False, extern_type=None, extern_name=None,
226 active=True, admin=False, extern_type=None, extern_name=None,
227 cur_user=None, plugin=None, force_password_change=False,
227 cur_user=None, plugin=None, force_password_change=False,
228 allow_to_create_user=True, create_repo_group=None,
228 allow_to_create_user=True, create_repo_group=None,
229 updating_user_id=None, language=None, description='',
229 updating_user_id=None, language=None, description='',
230 strict_creation_check=True):
230 strict_creation_check=True):
231 """
231 """
232 Creates a new instance if not found, or updates current one
232 Creates a new instance if not found, or updates current one
233
233
234 :param username:
234 :param username:
235 :param password:
235 :param password:
236 :param email:
236 :param email:
237 :param firstname:
237 :param firstname:
238 :param lastname:
238 :param lastname:
239 :param active:
239 :param active:
240 :param admin:
240 :param admin:
241 :param extern_type:
241 :param extern_type:
242 :param extern_name:
242 :param extern_name:
243 :param cur_user:
243 :param cur_user:
244 :param plugin: optional plugin this method was called from
244 :param plugin: optional plugin this method was called from
245 :param force_password_change: toggles new or existing user flag
245 :param force_password_change: toggles new or existing user flag
246 for password change
246 for password change
247 :param allow_to_create_user: Defines if the method can actually create
247 :param allow_to_create_user: Defines if the method can actually create
248 new users
248 new users
249 :param create_repo_group: Defines if the method should also
249 :param create_repo_group: Defines if the method should also
250 create an repo group with user name, and owner
250 create an repo group with user name, and owner
251 :param updating_user_id: if we set it up this is the user we want to
251 :param updating_user_id: if we set it up this is the user we want to
252 update this allows to editing username.
252 update this allows to editing username.
253 :param language: language of user from interface.
253 :param language: language of user from interface.
254 :param description: user description
254 :param description: user description
255 :param strict_creation_check: checks for allowed creation license wise etc.
255 :param strict_creation_check: checks for allowed creation license wise etc.
256
256
257 :returns: new User object with injected `is_new_user` attribute.
257 :returns: new User object with injected `is_new_user` attribute.
258 """
258 """
259
259
260 if not cur_user:
260 if not cur_user:
261 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
261 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
262
262
263 from rhodecode.lib.auth import (
263 from rhodecode.lib.auth import (
264 get_crypt_password, check_password, generate_auth_token)
264 get_crypt_password, check_password)
265 from rhodecode.lib.hooks_base import (
265 from rhodecode.lib.hooks_base import (
266 log_create_user, check_allowed_create_user)
266 log_create_user, check_allowed_create_user)
267
267
268 def _password_change(new_user, password):
268 def _password_change(new_user, password):
269 old_password = new_user.password or ''
269 old_password = new_user.password or ''
270 # empty password
270 # empty password
271 if not old_password:
271 if not old_password:
272 return False
272 return False
273
273
274 # password check is only needed for RhodeCode internal auth calls
274 # password check is only needed for RhodeCode internal auth calls
275 # in case it's a plugin we don't care
275 # in case it's a plugin we don't care
276 if not plugin:
276 if not plugin:
277
277
278 # first check if we gave crypted password back, and if it
278 # first check if we gave crypted password back, and if it
279 # matches it's not password change
279 # matches it's not password change
280 if new_user.password == password:
280 if new_user.password == password:
281 return False
281 return False
282
282
283 password_match = check_password(password, old_password)
283 password_match = check_password(password, old_password)
284 if not password_match:
284 if not password_match:
285 return True
285 return True
286
286
287 return False
287 return False
288
288
289 # read settings on default personal repo group creation
289 # read settings on default personal repo group creation
290 if create_repo_group is None:
290 if create_repo_group is None:
291 default_create_repo_group = RepoGroupModel()\
291 default_create_repo_group = RepoGroupModel()\
292 .get_default_create_personal_repo_group()
292 .get_default_create_personal_repo_group()
293 create_repo_group = default_create_repo_group
293 create_repo_group = default_create_repo_group
294
294
295 user_data = {
295 user_data = {
296 'username': username,
296 'username': username,
297 'password': password,
297 'password': password,
298 'email': email,
298 'email': email,
299 'firstname': firstname,
299 'firstname': firstname,
300 'lastname': lastname,
300 'lastname': lastname,
301 'active': active,
301 'active': active,
302 'admin': admin
302 'admin': admin
303 }
303 }
304
304
305 if updating_user_id:
305 if updating_user_id:
306 log.debug('Checking for existing account in RhodeCode '
306 log.debug('Checking for existing account in RhodeCode '
307 'database with user_id `%s` ', updating_user_id)
307 'database with user_id `%s` ', updating_user_id)
308 user = User.get(updating_user_id)
308 user = User.get(updating_user_id)
309 else:
309 else:
310 log.debug('Checking for existing account in RhodeCode '
310 log.debug('Checking for existing account in RhodeCode '
311 'database with username `%s` ', username)
311 'database with username `%s` ', username)
312 user = User.get_by_username(username, case_insensitive=True)
312 user = User.get_by_username(username, case_insensitive=True)
313
313
314 if user is None:
314 if user is None:
315 # we check internal flag if this method is actually allowed to
315 # we check internal flag if this method is actually allowed to
316 # create new user
316 # create new user
317 if not allow_to_create_user:
317 if not allow_to_create_user:
318 msg = ('Method wants to create new user, but it is not '
318 msg = ('Method wants to create new user, but it is not '
319 'allowed to do so')
319 'allowed to do so')
320 log.warning(msg)
320 log.warning(msg)
321 raise NotAllowedToCreateUserError(msg)
321 raise NotAllowedToCreateUserError(msg)
322
322
323 log.debug('Creating new user %s', username)
323 log.debug('Creating new user %s', username)
324
324
325 # only if we create user that is active
325 # only if we create user that is active
326 new_active_user = active
326 new_active_user = active
327 if new_active_user and strict_creation_check:
327 if new_active_user and strict_creation_check:
328 # raises UserCreationError if it's not allowed for any reason to
328 # raises UserCreationError if it's not allowed for any reason to
329 # create new active user, this also executes pre-create hooks
329 # create new active user, this also executes pre-create hooks
330 check_allowed_create_user(user_data, cur_user, strict_check=True)
330 check_allowed_create_user(user_data, cur_user, strict_check=True)
331 events.trigger(events.UserPreCreate(user_data))
331 events.trigger(events.UserPreCreate(user_data))
332 new_user = User()
332 new_user = User()
333 edit = False
333 edit = False
334 else:
334 else:
335 log.debug('updating user `%s`', username)
335 log.debug('updating user `%s`', username)
336 events.trigger(events.UserPreUpdate(user, user_data))
336 events.trigger(events.UserPreUpdate(user, user_data))
337 new_user = user
337 new_user = user
338 edit = True
338 edit = True
339
339
340 # we're not allowed to edit default user
340 # we're not allowed to edit default user
341 if user.username == User.DEFAULT_USER:
341 if user.username == User.DEFAULT_USER:
342 raise DefaultUserException(
342 raise DefaultUserException(
343 "You can't edit this user (`%(username)s`) since it's "
343 "You can't edit this user (`%(username)s`) since it's "
344 "crucial for entire application"
344 "crucial for entire application"
345 % {'username': user.username})
345 % {'username': user.username})
346
346
347 # inject special attribute that will tell us if User is new or old
347 # inject special attribute that will tell us if User is new or old
348 new_user.is_new_user = not edit
348 new_user.is_new_user = not edit
349 # for users that didn's specify auth type, we use RhodeCode built in
349 # for users that didn's specify auth type, we use RhodeCode built in
350 from rhodecode.authentication.plugins import auth_rhodecode
350 from rhodecode.authentication.plugins import auth_rhodecode
351 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
351 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
352 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
352 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
353
353
354 try:
354 try:
355 new_user.username = username
355 new_user.username = username
356 new_user.admin = admin
356 new_user.admin = admin
357 new_user.email = email
357 new_user.email = email
358 new_user.active = active
358 new_user.active = active
359 new_user.extern_name = safe_unicode(extern_name)
359 new_user.extern_name = safe_unicode(extern_name)
360 new_user.extern_type = safe_unicode(extern_type)
360 new_user.extern_type = safe_unicode(extern_type)
361 new_user.name = firstname
361 new_user.name = firstname
362 new_user.lastname = lastname
362 new_user.lastname = lastname
363 new_user.description = description
363 new_user.description = description
364
364
365 # set password only if creating an user or password is changed
365 # set password only if creating an user or password is changed
366 if not edit or _password_change(new_user, password):
366 if not edit or _password_change(new_user, password):
367 reason = 'new password' if edit else 'new user'
367 reason = 'new password' if edit else 'new user'
368 log.debug('Updating password reason=>%s', reason)
368 log.debug('Updating password reason=>%s', reason)
369 new_user.password = get_crypt_password(password) if password else None
369 new_user.password = get_crypt_password(password) if password else None
370
370
371 if force_password_change:
371 if force_password_change:
372 new_user.update_userdata(force_password_change=True)
372 new_user.update_userdata(force_password_change=True)
373 if language:
373 if language:
374 new_user.update_userdata(language=language)
374 new_user.update_userdata(language=language)
375 new_user.update_userdata(notification_status=True)
375 new_user.update_userdata(notification_status=True)
376
376
377 self.sa.add(new_user)
377 self.sa.add(new_user)
378
378
379 if not edit and create_repo_group:
379 if not edit and create_repo_group:
380 RepoGroupModel().create_personal_repo_group(
380 RepoGroupModel().create_personal_repo_group(
381 new_user, commit_early=False)
381 new_user, commit_early=False)
382
382
383 if not edit:
383 if not edit:
384 # add the RSS token
384 # add the RSS token
385 self.add_auth_token(
385 self.add_auth_token(
386 user=username, lifetime_minutes=-1,
386 user=username, lifetime_minutes=-1,
387 role=self.auth_token_role.ROLE_FEED,
387 role=self.auth_token_role.ROLE_FEED,
388 description=u'Generated feed token')
388 description=u'Generated feed token')
389
389
390 kwargs = new_user.get_dict()
390 kwargs = new_user.get_dict()
391 # backward compat, require api_keys present
391 # backward compat, require api_keys present
392 kwargs['api_keys'] = kwargs['auth_tokens']
392 kwargs['api_keys'] = kwargs['auth_tokens']
393 log_create_user(created_by=cur_user, **kwargs)
393 log_create_user(created_by=cur_user, **kwargs)
394 events.trigger(events.UserPostCreate(user_data))
394 events.trigger(events.UserPostCreate(user_data))
395 return new_user
395 return new_user
396 except (DatabaseError,):
396 except (DatabaseError,):
397 log.error(traceback.format_exc())
397 log.error(traceback.format_exc())
398 raise
398 raise
399
399
400 def create_registration(self, form_data,
400 def create_registration(self, form_data,
401 extern_name='rhodecode', extern_type='rhodecode'):
401 extern_name='rhodecode', extern_type='rhodecode'):
402 from rhodecode.model.notification import NotificationModel
402 from rhodecode.model.notification import NotificationModel
403 from rhodecode.model.notification import EmailNotificationModel
403 from rhodecode.model.notification import EmailNotificationModel
404
404
405 try:
405 try:
406 form_data['admin'] = False
406 form_data['admin'] = False
407 form_data['extern_name'] = extern_name
407 form_data['extern_name'] = extern_name
408 form_data['extern_type'] = extern_type
408 form_data['extern_type'] = extern_type
409 new_user = self.create(form_data)
409 new_user = self.create(form_data)
410
410
411 self.sa.add(new_user)
411 self.sa.add(new_user)
412 self.sa.flush()
412 self.sa.flush()
413
413
414 user_data = new_user.get_dict()
414 user_data = new_user.get_dict()
415 user_data.update({
415 user_data.update({
416 'first_name': user_data.get('firstname'),
416 'first_name': user_data.get('firstname'),
417 'last_name': user_data.get('lastname'),
417 'last_name': user_data.get('lastname'),
418 })
418 })
419 kwargs = {
419 kwargs = {
420 # use SQLALCHEMY safe dump of user data
420 # use SQLALCHEMY safe dump of user data
421 'user': AttributeDict(user_data),
421 'user': AttributeDict(user_data),
422 'date': datetime.datetime.now()
422 'date': datetime.datetime.now()
423 }
423 }
424 notification_type = EmailNotificationModel.TYPE_REGISTRATION
424 notification_type = EmailNotificationModel.TYPE_REGISTRATION
425 # pre-generate the subject for notification itself
425 # pre-generate the subject for notification itself
426 (subject,
426 (subject,
427 _h, _e, # we don't care about those
427 _h, _e, # we don't care about those
428 body_plaintext) = EmailNotificationModel().render_email(
428 body_plaintext) = EmailNotificationModel().render_email(
429 notification_type, **kwargs)
429 notification_type, **kwargs)
430
430
431 # create notification objects, and emails
431 # create notification objects, and emails
432 NotificationModel().create(
432 NotificationModel().create(
433 created_by=new_user,
433 created_by=new_user,
434 notification_subject=subject,
434 notification_subject=subject,
435 notification_body=body_plaintext,
435 notification_body=body_plaintext,
436 notification_type=notification_type,
436 notification_type=notification_type,
437 recipients=None, # all admins
437 recipients=None, # all admins
438 email_kwargs=kwargs,
438 email_kwargs=kwargs,
439 )
439 )
440
440
441 return new_user
441 return new_user
442 except Exception:
442 except Exception:
443 log.error(traceback.format_exc())
443 log.error(traceback.format_exc())
444 raise
444 raise
445
445
446 def _handle_user_repos(self, username, repositories, handle_mode=None):
446 def _handle_user_repos(self, username, repositories, handle_user,
447 _superadmin = self.cls.get_first_super_admin()
447 handle_mode=None):
448
448 left_overs = True
449 left_overs = True
449
450
450 from rhodecode.model.repo import RepoModel
451 from rhodecode.model.repo import RepoModel
451
452
452 if handle_mode == 'detach':
453 if handle_mode == 'detach':
453 for obj in repositories:
454 for obj in repositories:
454 obj.user = _superadmin
455 obj.user = handle_user
455 # set description we know why we super admin now owns
456 # set description we know why we super admin now owns
456 # additional repositories that were orphaned !
457 # additional repositories that were orphaned !
457 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
458 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
458 self.sa.add(obj)
459 self.sa.add(obj)
459 left_overs = False
460 left_overs = False
460 elif handle_mode == 'delete':
461 elif handle_mode == 'delete':
461 for obj in repositories:
462 for obj in repositories:
462 RepoModel().delete(obj, forks='detach')
463 RepoModel().delete(obj, forks='detach')
463 left_overs = False
464 left_overs = False
464
465
465 # if nothing is done we have left overs left
466 # if nothing is done we have left overs left
466 return left_overs
467 return left_overs
467
468
468 def _handle_user_repo_groups(self, username, repository_groups,
469 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
469 handle_mode=None):
470 handle_mode=None):
470 _superadmin = self.cls.get_first_super_admin()
471
471 left_overs = True
472 left_overs = True
472
473
473 from rhodecode.model.repo_group import RepoGroupModel
474 from rhodecode.model.repo_group import RepoGroupModel
474
475
475 if handle_mode == 'detach':
476 if handle_mode == 'detach':
476 for r in repository_groups:
477 for r in repository_groups:
477 r.user = _superadmin
478 r.user = handle_user
478 # set description we know why we super admin now owns
479 # set description we know why we super admin now owns
479 # additional repositories that were orphaned !
480 # additional repositories that were orphaned !
480 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
481 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
481 r.personal = False
482 r.personal = False
482 self.sa.add(r)
483 self.sa.add(r)
483 left_overs = False
484 left_overs = False
484 elif handle_mode == 'delete':
485 elif handle_mode == 'delete':
485 for r in repository_groups:
486 for r in repository_groups:
486 RepoGroupModel().delete(r)
487 RepoGroupModel().delete(r)
487 left_overs = False
488 left_overs = False
488
489
489 # if nothing is done we have left overs left
490 # if nothing is done we have left overs left
490 return left_overs
491 return left_overs
491
492
492 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
493 def _handle_user_user_groups(self, username, user_groups, handle_user,
493 _superadmin = self.cls.get_first_super_admin()
494 handle_mode=None):
495
494 left_overs = True
496 left_overs = True
495
497
496 from rhodecode.model.user_group import UserGroupModel
498 from rhodecode.model.user_group import UserGroupModel
497
499
498 if handle_mode == 'detach':
500 if handle_mode == 'detach':
499 for r in user_groups:
501 for r in user_groups:
500 for user_user_group_to_perm in r.user_user_group_to_perm:
502 for user_user_group_to_perm in r.user_user_group_to_perm:
501 if user_user_group_to_perm.user.username == username:
503 if user_user_group_to_perm.user.username == username:
502 user_user_group_to_perm.user = _superadmin
504 user_user_group_to_perm.user = handle_user
503 r.user = _superadmin
505 r.user = handle_user
504 # set description we know why we super admin now owns
506 # set description we know why we super admin now owns
505 # additional repositories that were orphaned !
507 # additional repositories that were orphaned !
506 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
508 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
507 self.sa.add(r)
509 self.sa.add(r)
508 left_overs = False
510 left_overs = False
509 elif handle_mode == 'delete':
511 elif handle_mode == 'delete':
510 for r in user_groups:
512 for r in user_groups:
511 UserGroupModel().delete(r)
513 UserGroupModel().delete(r)
512 left_overs = False
514 left_overs = False
513
515
514 # if nothing is done we have left overs left
516 # if nothing is done we have left overs left
515 return left_overs
517 return left_overs
516
518
517 def _handle_user_artifacts(self, username, artifacts, handle_mode=None):
519 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
518 _superadmin = self.cls.get_first_super_admin()
520 handle_mode=None):
521 left_overs = True
522
523 from rhodecode.model.pull_request import PullRequestModel
524
525 if handle_mode == 'detach':
526 for pr in pull_requests:
527 pr.user_id = handle_user.user_id
528 # set description we know why we super admin now owns
529 # additional repositories that were orphaned !
530 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
531 self.sa.add(pr)
532 left_overs = False
533 elif handle_mode == 'delete':
534 for pr in pull_requests:
535 PullRequestModel().delete(pr)
536
537 left_overs = False
538
539 # if nothing is done we have left overs left
540 return left_overs
541
542 def _handle_user_artifacts(self, username, artifacts, handle_user,
543 handle_mode=None):
544
519 left_overs = True
545 left_overs = True
520
546
521 if handle_mode == 'detach':
547 if handle_mode == 'detach':
522 for a in artifacts:
548 for a in artifacts:
523 a.upload_user = _superadmin
549 a.upload_user = handle_user
524 # set description we know why we super admin now owns
550 # set description we know why we super admin now owns
525 # additional artifacts that were orphaned !
551 # additional artifacts that were orphaned !
526 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
552 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
527 self.sa.add(a)
553 self.sa.add(a)
528 left_overs = False
554 left_overs = False
529 elif handle_mode == 'delete':
555 elif handle_mode == 'delete':
530 from rhodecode.apps.file_store import utils as store_utils
556 from rhodecode.apps.file_store import utils as store_utils
531 storage = store_utils.get_file_storage(self.request.registry.settings)
557 request = get_current_request()
558 storage = store_utils.get_file_storage(request.registry.settings)
532 for a in artifacts:
559 for a in artifacts:
533 file_uid = a.file_uid
560 file_uid = a.file_uid
534 storage.delete(file_uid)
561 storage.delete(file_uid)
535 self.sa.delete(a)
562 self.sa.delete(a)
536
563
537 left_overs = False
564 left_overs = False
538
565
539 # if nothing is done we have left overs left
566 # if nothing is done we have left overs left
540 return left_overs
567 return left_overs
541
568
542 def delete(self, user, cur_user=None, handle_repos=None,
569 def delete(self, user, cur_user=None, handle_repos=None,
543 handle_repo_groups=None, handle_user_groups=None, handle_artifacts=None):
570 handle_repo_groups=None, handle_user_groups=None,
571 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
544 from rhodecode.lib.hooks_base import log_delete_user
572 from rhodecode.lib.hooks_base import log_delete_user
545
573
546 if not cur_user:
574 if not cur_user:
547 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
575 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
576
548 user = self._get_user(user)
577 user = self._get_user(user)
549
578
550 try:
579 try:
551 if user.username == User.DEFAULT_USER:
580 if user.username == User.DEFAULT_USER:
552 raise DefaultUserException(
581 raise DefaultUserException(
553 u"You can't remove this user since it's"
582 u"You can't remove this user since it's"
554 u" crucial for entire application")
583 u" crucial for entire application")
584 handle_user = handle_new_owner or self.cls.get_first_super_admin()
585 log.debug('New detached objects owner %s', handle_user)
555
586
556 left_overs = self._handle_user_repos(
587 left_overs = self._handle_user_repos(
557 user.username, user.repositories, handle_repos)
588 user.username, user.repositories, handle_user, handle_repos)
558 if left_overs and user.repositories:
589 if left_overs and user.repositories:
559 repos = [x.repo_name for x in user.repositories]
590 repos = [x.repo_name for x in user.repositories]
560 raise UserOwnsReposException(
591 raise UserOwnsReposException(
561 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
592 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
562 u'removed. Switch owners or remove those repositories:%(list_repos)s'
593 u'removed. Switch owners or remove those repositories:%(list_repos)s'
563 % {'username': user.username, 'len_repos': len(repos),
594 % {'username': user.username, 'len_repos': len(repos),
564 'list_repos': ', '.join(repos)})
595 'list_repos': ', '.join(repos)})
565
596
566 left_overs = self._handle_user_repo_groups(
597 left_overs = self._handle_user_repo_groups(
567 user.username, user.repository_groups, handle_repo_groups)
598 user.username, user.repository_groups, handle_user, handle_repo_groups)
568 if left_overs and user.repository_groups:
599 if left_overs and user.repository_groups:
569 repo_groups = [x.group_name for x in user.repository_groups]
600 repo_groups = [x.group_name for x in user.repository_groups]
570 raise UserOwnsRepoGroupsException(
601 raise UserOwnsRepoGroupsException(
571 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
602 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
572 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
603 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
573 % {'username': user.username, 'len_repo_groups': len(repo_groups),
604 % {'username': user.username, 'len_repo_groups': len(repo_groups),
574 'list_repo_groups': ', '.join(repo_groups)})
605 'list_repo_groups': ', '.join(repo_groups)})
575
606
576 left_overs = self._handle_user_user_groups(
607 left_overs = self._handle_user_user_groups(
577 user.username, user.user_groups, handle_user_groups)
608 user.username, user.user_groups, handle_user, handle_user_groups)
578 if left_overs and user.user_groups:
609 if left_overs and user.user_groups:
579 user_groups = [x.users_group_name for x in user.user_groups]
610 user_groups = [x.users_group_name for x in user.user_groups]
580 raise UserOwnsUserGroupsException(
611 raise UserOwnsUserGroupsException(
581 u'user "%s" still owns %s user groups and cannot be '
612 u'user "%s" still owns %s user groups and cannot be '
582 u'removed. Switch owners or remove those user groups:%s'
613 u'removed. Switch owners or remove those user groups:%s'
583 % (user.username, len(user_groups), ', '.join(user_groups)))
614 % (user.username, len(user_groups), ', '.join(user_groups)))
584
615
616 left_overs = self._handle_user_pull_requests(
617 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
618 if left_overs and user.user_pull_requests:
619 pull_requests = ['!{}'.format(x.pull_request_id) for x in user.user_pull_requests]
620 raise UserOwnsPullRequestsException(
621 u'user "%s" still owns %s pull requests and cannot be '
622 u'removed. Switch owners or remove those pull requests:%s'
623 % (user.username, len(pull_requests), ', '.join(pull_requests)))
624
585 left_overs = self._handle_user_artifacts(
625 left_overs = self._handle_user_artifacts(
586 user.username, user.artifacts, handle_artifacts)
626 user.username, user.artifacts, handle_user, handle_artifacts)
587 if left_overs and user.artifacts:
627 if left_overs and user.artifacts:
588 artifacts = [x.file_uid for x in user.artifacts]
628 artifacts = [x.file_uid for x in user.artifacts]
589 raise UserOwnsArtifactsException(
629 raise UserOwnsArtifactsException(
590 u'user "%s" still owns %s artifacts and cannot be '
630 u'user "%s" still owns %s artifacts and cannot be '
591 u'removed. Switch owners or remove those artifacts:%s'
631 u'removed. Switch owners or remove those artifacts:%s'
592 % (user.username, len(artifacts), ', '.join(artifacts)))
632 % (user.username, len(artifacts), ', '.join(artifacts)))
593
633
594 user_data = user.get_dict() # fetch user data before expire
634 user_data = user.get_dict() # fetch user data before expire
595
635
596 # we might change the user data with detach/delete, make sure
636 # we might change the user data with detach/delete, make sure
597 # the object is marked as expired before actually deleting !
637 # the object is marked as expired before actually deleting !
598 self.sa.expire(user)
638 self.sa.expire(user)
599 self.sa.delete(user)
639 self.sa.delete(user)
600
640
601 log_delete_user(deleted_by=cur_user, **user_data)
641 log_delete_user(deleted_by=cur_user, **user_data)
602 except Exception:
642 except Exception:
603 log.error(traceback.format_exc())
643 log.error(traceback.format_exc())
604 raise
644 raise
605
645
606 def reset_password_link(self, data, pwd_reset_url):
646 def reset_password_link(self, data, pwd_reset_url):
607 from rhodecode.lib.celerylib import tasks, run_task
647 from rhodecode.lib.celerylib import tasks, run_task
608 from rhodecode.model.notification import EmailNotificationModel
648 from rhodecode.model.notification import EmailNotificationModel
609 user_email = data['email']
649 user_email = data['email']
610 try:
650 try:
611 user = User.get_by_email(user_email)
651 user = User.get_by_email(user_email)
612 if user:
652 if user:
613 log.debug('password reset user found %s', user)
653 log.debug('password reset user found %s', user)
614
654
615 email_kwargs = {
655 email_kwargs = {
616 'password_reset_url': pwd_reset_url,
656 'password_reset_url': pwd_reset_url,
617 'user': user,
657 'user': user,
618 'email': user_email,
658 'email': user_email,
619 'date': datetime.datetime.now(),
659 'date': datetime.datetime.now(),
620 'first_admin_email': User.get_first_super_admin().email
660 'first_admin_email': User.get_first_super_admin().email
621 }
661 }
622
662
623 (subject, headers, email_body,
663 (subject, headers, email_body,
624 email_body_plaintext) = EmailNotificationModel().render_email(
664 email_body_plaintext) = EmailNotificationModel().render_email(
625 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
665 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
626
666
627 recipients = [user_email]
667 recipients = [user_email]
628
668
629 action_logger_generic(
669 action_logger_generic(
630 'sending password reset email to user: {}'.format(
670 'sending password reset email to user: {}'.format(
631 user), namespace='security.password_reset')
671 user), namespace='security.password_reset')
632
672
633 run_task(tasks.send_email, recipients, subject,
673 run_task(tasks.send_email, recipients, subject,
634 email_body_plaintext, email_body)
674 email_body_plaintext, email_body)
635
675
636 else:
676 else:
637 log.debug("password reset email %s not found", user_email)
677 log.debug("password reset email %s not found", user_email)
638 except Exception:
678 except Exception:
639 log.error(traceback.format_exc())
679 log.error(traceback.format_exc())
640 return False
680 return False
641
681
642 return True
682 return True
643
683
644 def reset_password(self, data):
684 def reset_password(self, data):
645 from rhodecode.lib.celerylib import tasks, run_task
685 from rhodecode.lib.celerylib import tasks, run_task
646 from rhodecode.model.notification import EmailNotificationModel
686 from rhodecode.model.notification import EmailNotificationModel
647 from rhodecode.lib import auth
687 from rhodecode.lib import auth
648 user_email = data['email']
688 user_email = data['email']
649 pre_db = True
689 pre_db = True
650 try:
690 try:
651 user = User.get_by_email(user_email)
691 user = User.get_by_email(user_email)
652 new_passwd = auth.PasswordGenerator().gen_password(
692 new_passwd = auth.PasswordGenerator().gen_password(
653 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
693 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
654 if user:
694 if user:
655 user.password = auth.get_crypt_password(new_passwd)
695 user.password = auth.get_crypt_password(new_passwd)
656 # also force this user to reset his password !
696 # also force this user to reset his password !
657 user.update_userdata(force_password_change=True)
697 user.update_userdata(force_password_change=True)
658
698
659 Session().add(user)
699 Session().add(user)
660
700
661 # now delete the token in question
701 # now delete the token in question
662 UserApiKeys = AuthTokenModel.cls
702 UserApiKeys = AuthTokenModel.cls
663 UserApiKeys().query().filter(
703 UserApiKeys().query().filter(
664 UserApiKeys.api_key == data['token']).delete()
704 UserApiKeys.api_key == data['token']).delete()
665
705
666 Session().commit()
706 Session().commit()
667 log.info('successfully reset password for `%s`', user_email)
707 log.info('successfully reset password for `%s`', user_email)
668
708
669 if new_passwd is None:
709 if new_passwd is None:
670 raise Exception('unable to generate new password')
710 raise Exception('unable to generate new password')
671
711
672 pre_db = False
712 pre_db = False
673
713
674 email_kwargs = {
714 email_kwargs = {
675 'new_password': new_passwd,
715 'new_password': new_passwd,
676 'user': user,
716 'user': user,
677 'email': user_email,
717 'email': user_email,
678 'date': datetime.datetime.now(),
718 'date': datetime.datetime.now(),
679 'first_admin_email': User.get_first_super_admin().email
719 'first_admin_email': User.get_first_super_admin().email
680 }
720 }
681
721
682 (subject, headers, email_body,
722 (subject, headers, email_body,
683 email_body_plaintext) = EmailNotificationModel().render_email(
723 email_body_plaintext) = EmailNotificationModel().render_email(
684 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
724 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
685 **email_kwargs)
725 **email_kwargs)
686
726
687 recipients = [user_email]
727 recipients = [user_email]
688
728
689 action_logger_generic(
729 action_logger_generic(
690 'sent new password to user: {} with email: {}'.format(
730 'sent new password to user: {} with email: {}'.format(
691 user, user_email), namespace='security.password_reset')
731 user, user_email), namespace='security.password_reset')
692
732
693 run_task(tasks.send_email, recipients, subject,
733 run_task(tasks.send_email, recipients, subject,
694 email_body_plaintext, email_body)
734 email_body_plaintext, email_body)
695
735
696 except Exception:
736 except Exception:
697 log.error('Failed to update user password')
737 log.error('Failed to update user password')
698 log.error(traceback.format_exc())
738 log.error(traceback.format_exc())
699 if pre_db:
739 if pre_db:
700 # we rollback only if local db stuff fails. If it goes into
740 # we rollback only if local db stuff fails. If it goes into
701 # run_task, we're pass rollback state this wouldn't work then
741 # run_task, we're pass rollback state this wouldn't work then
702 Session().rollback()
742 Session().rollback()
703
743
704 return True
744 return True
705
745
706 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
746 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
707 """
747 """
708 Fetches auth_user by user_id,or api_key if present.
748 Fetches auth_user by user_id,or api_key if present.
709 Fills auth_user attributes with those taken from database.
749 Fills auth_user attributes with those taken from database.
710 Additionally set's is_authenitated if lookup fails
750 Additionally set's is_authenitated if lookup fails
711 present in database
751 present in database
712
752
713 :param auth_user: instance of user to set attributes
753 :param auth_user: instance of user to set attributes
714 :param user_id: user id to fetch by
754 :param user_id: user id to fetch by
715 :param api_key: api key to fetch by
755 :param api_key: api key to fetch by
716 :param username: username to fetch by
756 :param username: username to fetch by
717 """
757 """
718 def token_obfuscate(token):
758 def token_obfuscate(token):
719 if token:
759 if token:
720 return token[:4] + "****"
760 return token[:4] + "****"
721
761
722 if user_id is None and api_key is None and username is None:
762 if user_id is None and api_key is None and username is None:
723 raise Exception('You need to pass user_id, api_key or username')
763 raise Exception('You need to pass user_id, api_key or username')
724
764
725 log.debug(
765 log.debug(
726 'AuthUser: fill data execution based on: '
766 'AuthUser: fill data execution based on: '
727 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
767 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
728 try:
768 try:
729 dbuser = None
769 dbuser = None
730 if user_id:
770 if user_id:
731 dbuser = self.get(user_id)
771 dbuser = self.get(user_id)
732 elif api_key:
772 elif api_key:
733 dbuser = self.get_by_auth_token(api_key)
773 dbuser = self.get_by_auth_token(api_key)
734 elif username:
774 elif username:
735 dbuser = self.get_by_username(username)
775 dbuser = self.get_by_username(username)
736
776
737 if not dbuser:
777 if not dbuser:
738 log.warning(
778 log.warning(
739 'Unable to lookup user by id:%s api_key:%s username:%s',
779 'Unable to lookup user by id:%s api_key:%s username:%s',
740 user_id, token_obfuscate(api_key), username)
780 user_id, token_obfuscate(api_key), username)
741 return False
781 return False
742 if not dbuser.active:
782 if not dbuser.active:
743 log.debug('User `%s:%s` is inactive, skipping fill data',
783 log.debug('User `%s:%s` is inactive, skipping fill data',
744 username, user_id)
784 username, user_id)
745 return False
785 return False
746
786
747 log.debug('AuthUser: filling found user:%s data', dbuser)
787 log.debug('AuthUser: filling found user:%s data', dbuser)
748
788
749 attrs = {
789 attrs = {
750 'user_id': dbuser.user_id,
790 'user_id': dbuser.user_id,
751 'username': dbuser.username,
791 'username': dbuser.username,
752 'name': dbuser.name,
792 'name': dbuser.name,
753 'first_name': dbuser.first_name,
793 'first_name': dbuser.first_name,
754 'firstname': dbuser.firstname,
794 'firstname': dbuser.firstname,
755 'last_name': dbuser.last_name,
795 'last_name': dbuser.last_name,
756 'lastname': dbuser.lastname,
796 'lastname': dbuser.lastname,
757 'admin': dbuser.admin,
797 'admin': dbuser.admin,
758 'active': dbuser.active,
798 'active': dbuser.active,
759
799
760 'email': dbuser.email,
800 'email': dbuser.email,
761 'emails': dbuser.emails_cached(),
801 'emails': dbuser.emails_cached(),
762 'short_contact': dbuser.short_contact,
802 'short_contact': dbuser.short_contact,
763 'full_contact': dbuser.full_contact,
803 'full_contact': dbuser.full_contact,
764 'full_name': dbuser.full_name,
804 'full_name': dbuser.full_name,
765 'full_name_or_username': dbuser.full_name_or_username,
805 'full_name_or_username': dbuser.full_name_or_username,
766
806
767 '_api_key': dbuser._api_key,
807 '_api_key': dbuser._api_key,
768 '_user_data': dbuser._user_data,
808 '_user_data': dbuser._user_data,
769
809
770 'created_on': dbuser.created_on,
810 'created_on': dbuser.created_on,
771 'extern_name': dbuser.extern_name,
811 'extern_name': dbuser.extern_name,
772 'extern_type': dbuser.extern_type,
812 'extern_type': dbuser.extern_type,
773
813
774 'inherit_default_permissions': dbuser.inherit_default_permissions,
814 'inherit_default_permissions': dbuser.inherit_default_permissions,
775
815
776 'language': dbuser.language,
816 'language': dbuser.language,
777 'last_activity': dbuser.last_activity,
817 'last_activity': dbuser.last_activity,
778 'last_login': dbuser.last_login,
818 'last_login': dbuser.last_login,
779 'password': dbuser.password,
819 'password': dbuser.password,
780 }
820 }
781 auth_user.__dict__.update(attrs)
821 auth_user.__dict__.update(attrs)
782 except Exception:
822 except Exception:
783 log.error(traceback.format_exc())
823 log.error(traceback.format_exc())
784 auth_user.is_authenticated = False
824 auth_user.is_authenticated = False
785 return False
825 return False
786
826
787 return True
827 return True
788
828
789 def has_perm(self, user, perm):
829 def has_perm(self, user, perm):
790 perm = self._get_perm(perm)
830 perm = self._get_perm(perm)
791 user = self._get_user(user)
831 user = self._get_user(user)
792
832
793 return UserToPerm.query().filter(UserToPerm.user == user)\
833 return UserToPerm.query().filter(UserToPerm.user == user)\
794 .filter(UserToPerm.permission == perm).scalar() is not None
834 .filter(UserToPerm.permission == perm).scalar() is not None
795
835
796 def grant_perm(self, user, perm):
836 def grant_perm(self, user, perm):
797 """
837 """
798 Grant user global permissions
838 Grant user global permissions
799
839
800 :param user:
840 :param user:
801 :param perm:
841 :param perm:
802 """
842 """
803 user = self._get_user(user)
843 user = self._get_user(user)
804 perm = self._get_perm(perm)
844 perm = self._get_perm(perm)
805 # if this permission is already granted skip it
845 # if this permission is already granted skip it
806 _perm = UserToPerm.query()\
846 _perm = UserToPerm.query()\
807 .filter(UserToPerm.user == user)\
847 .filter(UserToPerm.user == user)\
808 .filter(UserToPerm.permission == perm)\
848 .filter(UserToPerm.permission == perm)\
809 .scalar()
849 .scalar()
810 if _perm:
850 if _perm:
811 return
851 return
812 new = UserToPerm()
852 new = UserToPerm()
813 new.user = user
853 new.user = user
814 new.permission = perm
854 new.permission = perm
815 self.sa.add(new)
855 self.sa.add(new)
816 return new
856 return new
817
857
818 def revoke_perm(self, user, perm):
858 def revoke_perm(self, user, perm):
819 """
859 """
820 Revoke users global permissions
860 Revoke users global permissions
821
861
822 :param user:
862 :param user:
823 :param perm:
863 :param perm:
824 """
864 """
825 user = self._get_user(user)
865 user = self._get_user(user)
826 perm = self._get_perm(perm)
866 perm = self._get_perm(perm)
827
867
828 obj = UserToPerm.query()\
868 obj = UserToPerm.query()\
829 .filter(UserToPerm.user == user)\
869 .filter(UserToPerm.user == user)\
830 .filter(UserToPerm.permission == perm)\
870 .filter(UserToPerm.permission == perm)\
831 .scalar()
871 .scalar()
832 if obj:
872 if obj:
833 self.sa.delete(obj)
873 self.sa.delete(obj)
834
874
835 def add_extra_email(self, user, email):
875 def add_extra_email(self, user, email):
836 """
876 """
837 Adds email address to UserEmailMap
877 Adds email address to UserEmailMap
838
878
839 :param user:
879 :param user:
840 :param email:
880 :param email:
841 """
881 """
842
882
843 user = self._get_user(user)
883 user = self._get_user(user)
844
884
845 obj = UserEmailMap()
885 obj = UserEmailMap()
846 obj.user = user
886 obj.user = user
847 obj.email = email
887 obj.email = email
848 self.sa.add(obj)
888 self.sa.add(obj)
849 return obj
889 return obj
850
890
851 def delete_extra_email(self, user, email_id):
891 def delete_extra_email(self, user, email_id):
852 """
892 """
853 Removes email address from UserEmailMap
893 Removes email address from UserEmailMap
854
894
855 :param user:
895 :param user:
856 :param email_id:
896 :param email_id:
857 """
897 """
858 user = self._get_user(user)
898 user = self._get_user(user)
859 obj = UserEmailMap.query().get(email_id)
899 obj = UserEmailMap.query().get(email_id)
860 if obj and obj.user_id == user.user_id:
900 if obj and obj.user_id == user.user_id:
861 self.sa.delete(obj)
901 self.sa.delete(obj)
862
902
863 def parse_ip_range(self, ip_range):
903 def parse_ip_range(self, ip_range):
864 ip_list = []
904 ip_list = []
865
905
866 def make_unique(value):
906 def make_unique(value):
867 seen = []
907 seen = []
868 return [c for c in value if not (c in seen or seen.append(c))]
908 return [c for c in value if not (c in seen or seen.append(c))]
869
909
870 # firsts split by commas
910 # firsts split by commas
871 for ip_range in ip_range.split(','):
911 for ip_range in ip_range.split(','):
872 if not ip_range:
912 if not ip_range:
873 continue
913 continue
874 ip_range = ip_range.strip()
914 ip_range = ip_range.strip()
875 if '-' in ip_range:
915 if '-' in ip_range:
876 start_ip, end_ip = ip_range.split('-', 1)
916 start_ip, end_ip = ip_range.split('-', 1)
877 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
917 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
878 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
918 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
879 parsed_ip_range = []
919 parsed_ip_range = []
880
920
881 for index in xrange(int(start_ip), int(end_ip) + 1):
921 for index in range(int(start_ip), int(end_ip) + 1):
882 new_ip = ipaddress.ip_address(index)
922 new_ip = ipaddress.ip_address(index)
883 parsed_ip_range.append(str(new_ip))
923 parsed_ip_range.append(str(new_ip))
884 ip_list.extend(parsed_ip_range)
924 ip_list.extend(parsed_ip_range)
885 else:
925 else:
886 ip_list.append(ip_range)
926 ip_list.append(ip_range)
887
927
888 return make_unique(ip_list)
928 return make_unique(ip_list)
889
929
890 def add_extra_ip(self, user, ip, description=None):
930 def add_extra_ip(self, user, ip, description=None):
891 """
931 """
892 Adds ip address to UserIpMap
932 Adds ip address to UserIpMap
893
933
894 :param user:
934 :param user:
895 :param ip:
935 :param ip:
896 """
936 """
897
937
898 user = self._get_user(user)
938 user = self._get_user(user)
899 obj = UserIpMap()
939 obj = UserIpMap()
900 obj.user = user
940 obj.user = user
901 obj.ip_addr = ip
941 obj.ip_addr = ip
902 obj.description = description
942 obj.description = description
903 self.sa.add(obj)
943 self.sa.add(obj)
904 return obj
944 return obj
905
945
906 auth_token_role = AuthTokenModel.cls
946 auth_token_role = AuthTokenModel.cls
907
947
908 def add_auth_token(self, user, lifetime_minutes, role, description=u'',
948 def add_auth_token(self, user, lifetime_minutes, role, description=u'',
909 scope_callback=None):
949 scope_callback=None):
910 """
950 """
911 Add AuthToken for user.
951 Add AuthToken for user.
912
952
913 :param user: username/user_id
953 :param user: username/user_id
914 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
954 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
915 :param role: one of AuthTokenModel.cls.ROLE_*
955 :param role: one of AuthTokenModel.cls.ROLE_*
916 :param description: optional string description
956 :param description: optional string description
917 """
957 """
918
958
919 token = AuthTokenModel().create(
959 token = AuthTokenModel().create(
920 user, description, lifetime_minutes, role)
960 user, description, lifetime_minutes, role)
921 if scope_callback and callable(scope_callback):
961 if scope_callback and callable(scope_callback):
922 # call the callback if we provide, used to attach scope for EE edition
962 # call the callback if we provide, used to attach scope for EE edition
923 scope_callback(token)
963 scope_callback(token)
924 return token
964 return token
925
965
926 def delete_extra_ip(self, user, ip_id):
966 def delete_extra_ip(self, user, ip_id):
927 """
967 """
928 Removes ip address from UserIpMap
968 Removes ip address from UserIpMap
929
969
930 :param user:
970 :param user:
931 :param ip_id:
971 :param ip_id:
932 """
972 """
933 user = self._get_user(user)
973 user = self._get_user(user)
934 obj = UserIpMap.query().get(ip_id)
974 obj = UserIpMap.query().get(ip_id)
935 if obj and obj.user_id == user.user_id:
975 if obj and obj.user_id == user.user_id:
936 self.sa.delete(obj)
976 self.sa.delete(obj)
937
977
938 def get_accounts_in_creation_order(self, current_user=None):
978 def get_accounts_in_creation_order(self, current_user=None):
939 """
979 """
940 Get accounts in order of creation for deactivation for license limits
980 Get accounts in order of creation for deactivation for license limits
941
981
942 pick currently logged in user, and append to the list in position 0
982 pick currently logged in user, and append to the list in position 0
943 pick all super-admins in order of creation date and add it to the list
983 pick all super-admins in order of creation date and add it to the list
944 pick all other accounts in order of creation and add it to the list.
984 pick all other accounts in order of creation and add it to the list.
945
985
946 Based on that list, the last accounts can be disabled as they are
986 Based on that list, the last accounts can be disabled as they are
947 created at the end and don't include any of the super admins as well
987 created at the end and don't include any of the super admins as well
948 as the current user.
988 as the current user.
949
989
950 :param current_user: optionally current user running this operation
990 :param current_user: optionally current user running this operation
951 """
991 """
952
992
953 if not current_user:
993 if not current_user:
954 current_user = get_current_rhodecode_user()
994 current_user = get_current_rhodecode_user()
955 active_super_admins = [
995 active_super_admins = [
956 x.user_id for x in User.query()
996 x.user_id for x in User.query()
957 .filter(User.user_id != current_user.user_id)
997 .filter(User.user_id != current_user.user_id)
958 .filter(User.active == true())
998 .filter(User.active == true())
959 .filter(User.admin == true())
999 .filter(User.admin == true())
960 .order_by(User.created_on.asc())]
1000 .order_by(User.created_on.asc())]
961
1001
962 active_regular_users = [
1002 active_regular_users = [
963 x.user_id for x in User.query()
1003 x.user_id for x in User.query()
964 .filter(User.user_id != current_user.user_id)
1004 .filter(User.user_id != current_user.user_id)
965 .filter(User.active == true())
1005 .filter(User.active == true())
966 .filter(User.admin == false())
1006 .filter(User.admin == false())
967 .order_by(User.created_on.asc())]
1007 .order_by(User.created_on.asc())]
968
1008
969 list_of_accounts = [current_user.user_id]
1009 list_of_accounts = [current_user.user_id]
970 list_of_accounts += active_super_admins
1010 list_of_accounts += active_super_admins
971 list_of_accounts += active_regular_users
1011 list_of_accounts += active_regular_users
972
1012
973 return list_of_accounts
1013 return list_of_accounts
974
1014
975 def deactivate_last_users(self, expected_users, current_user=None):
1015 def deactivate_last_users(self, expected_users, current_user=None):
976 """
1016 """
977 Deactivate accounts that are over the license limits.
1017 Deactivate accounts that are over the license limits.
978 Algorithm of which accounts to disabled is based on the formula:
1018 Algorithm of which accounts to disabled is based on the formula:
979
1019
980 Get current user, then super admins in creation order, then regular
1020 Get current user, then super admins in creation order, then regular
981 active users in creation order.
1021 active users in creation order.
982
1022
983 Using that list we mark all accounts from the end of it as inactive.
1023 Using that list we mark all accounts from the end of it as inactive.
984 This way we block only latest created accounts.
1024 This way we block only latest created accounts.
985
1025
986 :param expected_users: list of users in special order, we deactivate
1026 :param expected_users: list of users in special order, we deactivate
987 the end N amount of users from that list
1027 the end N amount of users from that list
988 """
1028 """
989
1029
990 list_of_accounts = self.get_accounts_in_creation_order(
1030 list_of_accounts = self.get_accounts_in_creation_order(
991 current_user=current_user)
1031 current_user=current_user)
992
1032
993 for acc_id in list_of_accounts[expected_users + 1:]:
1033 for acc_id in list_of_accounts[expected_users + 1:]:
994 user = User.get(acc_id)
1034 user = User.get(acc_id)
995 log.info('Deactivating account %s for license unlock', user)
1035 log.info('Deactivating account %s for license unlock', user)
996 user.active = False
1036 user.active = False
997 Session().add(user)
1037 Session().add(user)
998 Session().commit()
1038 Session().commit()
999
1039
1000 return
1040 return
1001
1041
1002 def get_user_log(self, user, filter_term):
1042 def get_user_log(self, user, filter_term):
1003 user_log = UserLog.query()\
1043 user_log = UserLog.query()\
1004 .filter(or_(UserLog.user_id == user.user_id,
1044 .filter(or_(UserLog.user_id == user.user_id,
1005 UserLog.username == user.username))\
1045 UserLog.username == user.username))\
1006 .options(joinedload(UserLog.user))\
1046 .options(joinedload(UserLog.user))\
1007 .options(joinedload(UserLog.repository))\
1047 .options(joinedload(UserLog.repository))\
1008 .order_by(UserLog.action_date.desc())
1048 .order_by(UserLog.action_date.desc())
1009
1049
1010 user_log = user_log_filter(user_log, filter_term)
1050 user_log = user_log_filter(user_log, filter_term)
1011 return user_log
1051 return user_log
@@ -1,198 +1,211 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2
2
3 <%
3 <%
4 elems = [
4 elems = [
5 (_('User ID'), c.user.user_id, '', ''),
5 (_('User ID'), c.user.user_id, '', ''),
6 (_('Created on'), h.format_date(c.user.created_on), '', ''),
6 (_('Created on'), h.format_date(c.user.created_on), '', ''),
7 (_('Source of Record'), c.user.extern_type, '', ''),
7 (_('Source of Record'), c.user.extern_type, '', ''),
8
8
9 (_('Last login'), c.user.last_login or '-', '', ''),
9 (_('Last login'), c.user.last_login or '-', '', ''),
10 (_('Last activity'), c.user.last_activity, '', ''),
10 (_('Last activity'), c.user.last_activity, '', ''),
11
11
12 (_('Repositories'), len(c.user.repositories), '', [x.repo_name for x in c.user.repositories]),
12 (_('Repositories'), len(c.user.repositories), '', [x.repo_name for x in c.user.repositories]),
13 (_('Repository groups'), len(c.user.repository_groups), '', [x.group_name for x in c.user.repository_groups]),
13 (_('Repository groups'), len(c.user.repository_groups), '', [x.group_name for x in c.user.repository_groups]),
14 (_('User groups'), len(c.user.user_groups), '', [x.users_group_name for x in c.user.user_groups]),
14 (_('User groups'), len(c.user.user_groups), '', [x.users_group_name for x in c.user.user_groups]),
15
15
16 (_('Owned Artifacts'), len(c.user.artifacts), '', [x.file_uid for x in c.user.artifacts]),
16 (_('Owned Artifacts'), len(c.user.artifacts), '', [x.file_uid for x in c.user.artifacts]),
17
17
18 (_('Reviewer of pull requests'), len(c.user.reviewer_pull_requests), '', ['Pull Request #{}'.format(x.pull_request.pull_request_id) for x in c.user.reviewer_pull_requests]),
18 (_('Reviewer of pull requests'), len(c.user.reviewer_pull_requests), '', ['Pull Request #{}'.format(x.pull_request.pull_request_id) for x in c.user.reviewer_pull_requests]),
19 (_('Assigned to review rules'), len(c.user_to_review_rules), '', [x for x in c.user_to_review_rules]),
19 (_('Assigned to review rules'), len(c.user_to_review_rules), '', [x for x in c.user_to_review_rules]),
20
20
21 (_('Member of User groups'), len(c.user.group_member), '', [x.users_group.users_group_name for x in c.user.group_member]),
21 (_('Member of User groups'), len(c.user.group_member), '', [x.users_group.users_group_name for x in c.user.group_member]),
22 (_('Force password change'), c.user.user_data.get('force_password_change', 'False'), '', ''),
22 (_('Force password change'), c.user.user_data.get('force_password_change', 'False'), '', ''),
23 ]
23 ]
24 %>
24 %>
25
25
26 <div class="panel panel-default">
26 <div class="panel panel-default">
27 <div class="panel-heading">
27 <div class="panel-heading">
28 <h3 class="panel-title">
28 <h3 class="panel-title">
29 ${base.gravatar_with_user(c.user.username, 16, tooltip=False, _class='pull-left')}
29 ${base.gravatar_with_user(c.user.username, 16, tooltip=False, _class='pull-left')}
30 &nbsp;- ${_('Access Permissions')}
30 &nbsp;- ${_('Access Permissions')}
31 </h3>
31 </h3>
32 </div>
32 </div>
33 <div class="panel-body">
33 <div class="panel-body">
34 <table class="rctable">
34 <table class="rctable">
35 <tr>
35 <tr>
36 <th>Name</th>
36 <th>Name</th>
37 <th>Value</th>
37 <th>Value</th>
38 <th>Action</th>
38 <th>Action</th>
39 </tr>
39 </tr>
40 % for elem in elems:
40 % for elem in elems:
41 ${base.tr_info_entry(elem)}
41 ${base.tr_info_entry(elem)}
42 % endfor
42 % endfor
43 </table>
43 </table>
44 </div>
44 </div>
45 </div>
45 </div>
46
46
47 <div class="panel panel-default">
47 <div class="panel panel-default">
48 <div class="panel-heading">
48 <div class="panel-heading">
49 <h3 class="panel-title">${_('Force Password Reset')}</h3>
49 <h3 class="panel-title">${_('Force Password Reset')}</h3>
50 </div>
50 </div>
51 <div class="panel-body">
51 <div class="panel-body">
52 ${h.secure_form(h.route_path('user_disable_force_password_reset', user_id=c.user.user_id), request=request)}
52 ${h.secure_form(h.route_path('user_disable_force_password_reset', user_id=c.user.user_id), request=request)}
53 <div class="field">
53 <div class="field">
54 <button class="btn btn-default" type="submit">
54 <button class="btn btn-default" type="submit">
55 <i class="icon-unlock"></i> ${_('Disable forced password reset')}
55 <i class="icon-unlock"></i> ${_('Disable forced password reset')}
56 </button>
56 </button>
57 </div>
57 </div>
58 <div class="field">
58 <div class="field">
59 <span class="help-block">
59 <span class="help-block">
60 ${_("Clear the forced password change flag.")}
60 ${_("Clear the forced password change flag.")}
61 </span>
61 </span>
62 </div>
62 </div>
63 ${h.end_form()}
63 ${h.end_form()}
64
64
65 ${h.secure_form(h.route_path('user_enable_force_password_reset', user_id=c.user.user_id), request=request)}
65 ${h.secure_form(h.route_path('user_enable_force_password_reset', user_id=c.user.user_id), request=request)}
66 <div class="field">
66 <div class="field">
67 <button class="btn btn-default" type="submit" onclick="return confirm('${_('Confirm to enable forced password change')}');">
67 <button class="btn btn-default" type="submit" onclick="return confirm('${_('Confirm to enable forced password change')}');">
68 <i class="icon-lock"></i> ${_('Enable forced password reset')}
68 <i class="icon-lock"></i> ${_('Enable forced password reset')}
69 </button>
69 </button>
70 </div>
70 </div>
71 <div class="field">
71 <div class="field">
72 <span class="help-block">
72 <span class="help-block">
73 ${_("When this is enabled user will have to change they password when they next use RhodeCode system. This will also forbid vcs operations until someone makes a password change in the web interface")}
73 ${_("When this is enabled user will have to change they password when they next use RhodeCode system. This will also forbid vcs operations until someone makes a password change in the web interface")}
74 </span>
74 </span>
75 </div>
75 </div>
76 ${h.end_form()}
76 ${h.end_form()}
77
77
78 </div>
78 </div>
79 </div>
79 </div>
80
80
81 <div class="panel panel-default">
81 <div class="panel panel-default">
82 <div class="panel-heading">
82 <div class="panel-heading">
83 <h3 class="panel-title">${_('Personal Repository Group')}</h3>
83 <h3 class="panel-title">${_('Personal Repository Group')}</h3>
84 </div>
84 </div>
85 <div class="panel-body">
85 <div class="panel-body">
86 ${h.secure_form(h.route_path('user_create_personal_repo_group', user_id=c.user.user_id), request=request)}
86 ${h.secure_form(h.route_path('user_create_personal_repo_group', user_id=c.user.user_id), request=request)}
87
87
88 %if c.personal_repo_group:
88 %if c.personal_repo_group:
89 <div class="panel-body-title-text">${_('Users personal repository group')} : ${h.link_to(c.personal_repo_group.group_name, h.route_path('repo_group_home', repo_group_name=c.personal_repo_group.group_name))}</div>
89 <div class="panel-body-title-text">${_('Users personal repository group')} : ${h.link_to(c.personal_repo_group.group_name, h.route_path('repo_group_home', repo_group_name=c.personal_repo_group.group_name))}</div>
90 %else:
90 %else:
91 <div class="panel-body-title-text">
91 <div class="panel-body-title-text">
92 ${_('This user currently does not have a personal repository group')}
92 ${_('This user currently does not have a personal repository group')}
93 <br/>
93 <br/>
94 ${_('New group will be created at: `/%(path)s`') % {'path': c.personal_repo_group_name}}
94 ${_('New group will be created at: `/%(path)s`') % {'path': c.personal_repo_group_name}}
95 </div>
95 </div>
96 %endif
96 %endif
97 <button class="btn btn-default" type="submit" ${'disabled="disabled"' if c.personal_repo_group else ''}>
97 <button class="btn btn-default" type="submit" ${'disabled="disabled"' if c.personal_repo_group else ''}>
98 <i class="icon-repo-group"></i>
98 <i class="icon-repo-group"></i>
99 ${_('Create personal repository group')}
99 ${_('Create personal repository group')}
100 </button>
100 </button>
101 ${h.end_form()}
101 ${h.end_form()}
102 </div>
102 </div>
103 </div>
103 </div>
104
104
105
105
106 <div class="panel panel-danger">
106 <div class="panel panel-danger">
107 <div class="panel-heading">
107 <div class="panel-heading">
108 <h3 class="panel-title">${_('Delete User')}</h3>
108 <h3 class="panel-title">${_('Delete User')}</h3>
109 </div>
109 </div>
110 <div class="panel-body">
110 <div class="panel-body">
111 ${h.secure_form(h.route_path('user_delete', user_id=c.user.user_id), request=request)}
111 ${h.secure_form(h.route_path('user_delete', user_id=c.user.user_id), request=request)}
112
112
113 <table class="display rctable">
113 <table class="display rctable">
114 <tr>
114 <tr>
115 <td>
115 <td>
116 ${_ungettext('This user owns %s repository.', 'This user owns %s repositories.', len(c.user.repositories)) % len(c.user.repositories)}
116 ${_ungettext('This user owns %s repository.', 'This user owns %s repositories.', len(c.user.repositories)) % len(c.user.repositories)}
117 </td>
117 </td>
118 <td>
118 <td>
119 <input type="radio" id="user_repos_1" name="user_repos" value="detach" checked="checked" ${'disabled=1' if len(c.user.repositories) == 0 else ''} /> <label for="user_repos_1">${_('Detach repositories')}</label>
119 <input type="radio" id="user_repos_1" name="user_repos" value="detach" checked="checked" ${'disabled=1' if len(c.user.repositories) == 0 else ''} /> <label for="user_repos_1">${_('Detach repositories')}</label>
120 </td>
120 </td>
121 <td>
121 <td>
122 <input type="radio" id="user_repos_2" name="user_repos" value="delete" ${'disabled=1' if len(c.user.repositories) == 0 else ''} /> <label for="user_repos_2">${_('Delete repositories')}</label>
122 <input type="radio" id="user_repos_2" name="user_repos" value="delete" ${'disabled=1' if len(c.user.repositories) == 0 else ''} /> <label for="user_repos_2">${_('Delete repositories')}</label>
123 </td>
123 </td>
124 </tr>
124 </tr>
125
125
126 <tr>
126 <tr>
127 <td>
127 <td>
128 ${_ungettext('This user owns %s repository group.', 'This user owns %s repository groups.', len(c.user.repository_groups)) % len(c.user.repository_groups)}
128 ${_ungettext('This user owns %s repository group.', 'This user owns %s repository groups.', len(c.user.repository_groups)) % len(c.user.repository_groups)}
129 </td>
129 </td>
130 <td>
130 <td>
131 <input type="radio" id="user_repo_groups_1" name="user_repo_groups" value="detach" checked="checked" ${'disabled=1' if len(c.user.repository_groups) == 0 else ''} /> <label for="user_repo_groups_1">${_('Detach repository groups')}</label>
131 <input type="radio" id="user_repo_groups_1" name="user_repo_groups" value="detach" checked="checked" ${'disabled=1' if len(c.user.repository_groups) == 0 else ''} /> <label for="user_repo_groups_1">${_('Detach repository groups')}</label>
132 </td>
132 </td>
133 <td>
133 <td>
134 <input type="radio" id="user_repo_groups_2" name="user_repo_groups" value="delete" ${'disabled=1' if len(c.user.repository_groups) == 0 else ''}/> <label for="user_repo_groups_2">${_('Delete repositories')}</label>
134 <input type="radio" id="user_repo_groups_2" name="user_repo_groups" value="delete" ${'disabled=1' if len(c.user.repository_groups) == 0 else ''}/> <label for="user_repo_groups_2">${_('Delete repositories')}</label>
135 </td>
135 </td>
136 </tr>
136 </tr>
137
137
138 <tr>
138 <tr>
139 <td>
139 <td>
140 ${_ungettext('This user owns %s user group.', 'This user owns %s user groups.', len(c.user.user_groups)) % len(c.user.user_groups)}
140 ${_ungettext('This user owns %s user group.', 'This user owns %s user groups.', len(c.user.user_groups)) % len(c.user.user_groups)}
141 </td>
141 </td>
142 <td>
142 <td>
143 <input type="radio" id="user_user_groups_1" name="user_user_groups" value="detach" checked="checked" ${'disabled=1' if len(c.user.user_groups) == 0 else ''}/> <label for="user_user_groups_1">${_('Detach user groups')}</label>
143 <input type="radio" id="user_user_groups_1" name="user_user_groups" value="detach" checked="checked" ${'disabled=1' if len(c.user.user_groups) == 0 else ''}/> <label for="user_user_groups_1">${_('Detach user groups')}</label>
144 </td>
144 </td>
145 <td>
145 <td>
146 <input type="radio" id="user_user_groups_2" name="user_user_groups" value="delete" ${'disabled=1' if len(c.user.user_groups) == 0 else ''}/> <label for="user_user_groups_2">${_('Delete repositories')}</label>
146 <input type="radio" id="user_user_groups_2" name="user_user_groups" value="delete" ${'disabled=1' if len(c.user.user_groups) == 0 else ''}/> <label for="user_user_groups_2">${_('Delete repositories')}</label>
147 </td>
147 </td>
148 </tr>
148 </tr>
149
149
150 <tr>
150 <tr>
151 <td>
151 <td>
152 ${_ungettext('This user owns %s pull request.', 'This user owns %s pull requests.', len(c.user.user_pull_requests)) % len(c.user.user_pull_requests)}
153 </td>
154 <td>
155 <input type="radio" id="user_pull_requests_1" name="user_pull_requests" value="detach" checked="checked" ${'disabled=1' if len(c.user.user_pull_requests) == 0 else ''}/> <label for="user_pull_requests_1">${_('Detach pull requests')}</label>
156 </td>
157 <td>
158 <input type="radio" id="user_pull_requests_2" name="user_pull_requests" value="delete" ${'disabled=1' if len(c.user.user_pull_requests) == 0 else ''}/> <label for="user_pull_requests_2">${_('Delete pull requests')}</label>
159 </td>
160 </tr>
161
162 <tr>
163 <td>
152 ${_ungettext('This user owns %s artifact.', 'This user owns %s artifacts.', len(c.user.artifacts)) % len(c.user.artifacts)}
164 ${_ungettext('This user owns %s artifact.', 'This user owns %s artifacts.', len(c.user.artifacts)) % len(c.user.artifacts)}
153 </td>
165 </td>
154 <td>
166 <td>
155 <input type="radio" id="user_artifacts_1" name="user_artifacts" value="detach" checked="checked" ${'disabled=1' if len(c.user.artifacts) == 0 else ''}/> <label for="user_artifacts_1">${_('Detach Artifacts')}</label>
167 <input type="radio" id="user_artifacts_1" name="user_artifacts" value="detach" checked="checked" ${'disabled=1' if len(c.user.artifacts) == 0 else ''}/> <label for="user_artifacts_1">${_('Detach Artifacts')}</label>
156 </td>
168 </td>
157 <td>
169 <td>
158 <input type="radio" id="user_artifacts_2" name="user_artifacts" value="delete" ${'disabled=1' if len(c.user.artifacts) == 0 else ''}/> <label for="user_artifacts_2">${_('Delete Artifacts')}</label>
170 <input type="radio" id="user_artifacts_2" name="user_artifacts" value="delete" ${'disabled=1' if len(c.user.artifacts) == 0 else ''}/> <label for="user_artifacts_2">${_('Delete Artifacts')}</label>
159 </td>
171 </td>
160 </tr>
172 </tr>
161
173
162 </table>
174 </table>
163 <div style="margin: 0 0 20px 0" class="fake-space"></div>
175 <div style="margin: 0 0 20px 0" class="fake-space"></div>
164 <div class="pull-left">
176 <div class="pull-left">
165 % if len(c.user.repositories) > 0 or len(c.user.repository_groups) > 0 or len(c.user.user_groups) > 0:
177 % if len(c.user.repositories) > 0 or len(c.user.repository_groups) > 0 or len(c.user.user_groups) > 0:
166 % endif
178 % endif
167
179
168 <span style="padding: 0 5px 0 0">${_('New owner for detached objects')}:</span>
180 <span style="padding: 0 5px 0 0">${_('New owner for detached objects')}:</span>
169 <div class="pull-right">${base.gravatar_with_user(c.first_admin.email, 16)}</div>
181 <div class="pull-right">${base.gravatar_with_user(c.detach_user.email, 16, tooltip=True)}</div>
182 <input type="hidden" name="detach_user_id" value="${c.detach_user.user_id}">
170 </div>
183 </div>
171 <div style="clear: both">
184 <div style="clear: both">
172
185
173 <div>
186 <div>
174 <p class="help-block">
187 <p class="help-block">
175 ${_("When selecting the detach option, the depending objects owned by this user will be assigned to the above user.")}
188 ${_("When selecting the detach option, the depending objects owned by this user will be assigned to the above user.")}
176 <br/>
189 <br/>
177 ${_("The delete option will delete the user and all his owned objects!")}
190 ${_("The delete option will delete the user and all his owned objects!")}
178 </p>
191 </p>
179 </div>
192 </div>
180
193
181 % if c.can_delete_user_message:
194 % if c.can_delete_user_message:
182 <p class="pre-formatting">${c.can_delete_user_message}</p>
195 <p class="pre-formatting">${c.can_delete_user_message}</p>
183 % endif
196 % endif
184 </div>
197 </div>
185
198
186 <div style="margin: 0 0 20px 0" class="fake-space"></div>
199 <div style="margin: 0 0 20px 0" class="fake-space"></div>
187
200
188 <div class="field">
201 <div class="field">
189 <button class="btn btn-small btn-danger" type="submit"
202 <input class="btn btn-small btn-danger" id="remove_user" name="remove_user"
190 onclick="return confirm('${_('Confirm to delete this user: %s') % c.user.username}');"
203 onclick="submitConfirm(event, this, _gettext('Confirm to delete this user'), _gettext('Confirm Delete'), '${c.user.username}')"
191 ${"disabled" if not c.can_delete_user else ""}>
204 ${("disabled=1" if not c.can_delete_user else "")}
192 ${_('Delete this user')}
205 type="submit" value="${_('Delete this user')}"
193 </button>
206 >
194 </div>
207 </div>
195
208
196 ${h.end_form()}
209 ${h.end_form()}
197 </div>
210 </div>
198 </div>
211 </div>
General Comments 0
You need to be logged in to leave comments. Login now