##// 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
@@ -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
461 UserModel().delete(
462 c.user,
463 handle_repos=handle_repos,
440 handle_repo_groups=handle_repo_groups,
464 handle_repo_groups=handle_repo_groups,
441 handle_user_groups=handle_user_groups,
465 handle_user_groups=handle_user_groups,
442 handle_artifacts=handle_artifacts)
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,5593 +1,5594 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 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import string
28 import string
29 import hashlib
29 import hashlib
30 import logging
30 import logging
31 import datetime
31 import datetime
32 import uuid
32 import uuid
33 import warnings
33 import warnings
34 import ipaddress
34 import ipaddress
35 import functools
35 import functools
36 import traceback
36 import traceback
37 import collections
37 import collections
38
38
39 from sqlalchemy import (
39 from sqlalchemy import (
40 or_, and_, not_, func, cast, TypeDecorator, event,
40 or_, and_, not_, func, cast, TypeDecorator, event,
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 Text, Float, PickleType, BigInteger)
43 Text, Float, PickleType, BigInteger)
44 from sqlalchemy.sql.expression import true, false, case
44 from sqlalchemy.sql.expression import true, false, case
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 from sqlalchemy.orm import (
46 from sqlalchemy.orm import (
47 relationship, joinedload, class_mapper, validates, aliased)
47 relationship, joinedload, class_mapper, validates, aliased)
48 from sqlalchemy.ext.declarative import declared_attr
48 from sqlalchemy.ext.declarative import declared_attr
49 from sqlalchemy.ext.hybrid import hybrid_property
49 from sqlalchemy.ext.hybrid import hybrid_property
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
51 from sqlalchemy.dialects.mysql import LONGTEXT
51 from sqlalchemy.dialects.mysql import LONGTEXT
52 from zope.cachedescriptors.property import Lazy as LazyProperty
52 from zope.cachedescriptors.property import Lazy as LazyProperty
53 from pyramid import compat
53 from pyramid import compat
54 from pyramid.threadlocal import get_current_request
54 from pyramid.threadlocal import get_current_request
55 from webhelpers2.text import remove_formatting
55 from webhelpers2.text import remove_formatting
56
56
57 from rhodecode.translation import _
57 from rhodecode.translation import _
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
60 from rhodecode.lib.utils2 import (
60 from rhodecode.lib.utils2 import (
61 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
61 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
62 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
63 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
64 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
64 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
65 JsonRaw
65 JsonRaw
66 from rhodecode.lib.ext_json import json
66 from rhodecode.lib.ext_json import json
67 from rhodecode.lib.caching_query import FromCache
67 from rhodecode.lib.caching_query import FromCache
68 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
68 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
69 from rhodecode.lib.encrypt2 import Encryptor
69 from rhodecode.lib.encrypt2 import Encryptor
70 from rhodecode.lib.exceptions import (
70 from rhodecode.lib.exceptions import (
71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 from rhodecode.model.meta import Base, Session
72 from rhodecode.model.meta import Base, Session
73
73
74 URL_SEP = '/'
74 URL_SEP = '/'
75 log = logging.getLogger(__name__)
75 log = logging.getLogger(__name__)
76
76
77 # =============================================================================
77 # =============================================================================
78 # BASE CLASSES
78 # BASE CLASSES
79 # =============================================================================
79 # =============================================================================
80
80
81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 # beaker.session.secret if first is not set.
82 # beaker.session.secret if first is not set.
83 # and initialized at environment.py
83 # and initialized at environment.py
84 ENCRYPTION_KEY = None
84 ENCRYPTION_KEY = None
85
85
86 # used to sort permissions by types, '#' used here is not allowed to be in
86 # used to sort permissions by types, '#' used here is not allowed to be in
87 # usernames, and it's very early in sorted string.printable table.
87 # usernames, and it's very early in sorted string.printable table.
88 PERMISSION_TYPE_SORT = {
88 PERMISSION_TYPE_SORT = {
89 'admin': '####',
89 'admin': '####',
90 'write': '###',
90 'write': '###',
91 'read': '##',
91 'read': '##',
92 'none': '#',
92 'none': '#',
93 }
93 }
94
94
95
95
96 def display_user_sort(obj):
96 def display_user_sort(obj):
97 """
97 """
98 Sort function used to sort permissions in .permissions() function of
98 Sort function used to sort permissions in .permissions() function of
99 Repository, RepoGroup, UserGroup. Also it put the default user in front
99 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 of all other resources
100 of all other resources
101 """
101 """
102
102
103 if obj.username == User.DEFAULT_USER:
103 if obj.username == User.DEFAULT_USER:
104 return '#####'
104 return '#####'
105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 return prefix + obj.username
106 return prefix + obj.username
107
107
108
108
109 def display_user_group_sort(obj):
109 def display_user_group_sort(obj):
110 """
110 """
111 Sort function used to sort permissions in .permissions() function of
111 Sort function used to sort permissions in .permissions() function of
112 Repository, RepoGroup, UserGroup. Also it put the default user in front
112 Repository, RepoGroup, UserGroup. Also it put the default user in front
113 of all other resources
113 of all other resources
114 """
114 """
115
115
116 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
116 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
117 return prefix + obj.users_group_name
117 return prefix + obj.users_group_name
118
118
119
119
120 def _hash_key(k):
120 def _hash_key(k):
121 return sha1_safe(k)
121 return sha1_safe(k)
122
122
123
123
124 def in_filter_generator(qry, items, limit=500):
124 def in_filter_generator(qry, items, limit=500):
125 """
125 """
126 Splits IN() into multiple with OR
126 Splits IN() into multiple with OR
127 e.g.::
127 e.g.::
128 cnt = Repository.query().filter(
128 cnt = Repository.query().filter(
129 or_(
129 or_(
130 *in_filter_generator(Repository.repo_id, range(100000))
130 *in_filter_generator(Repository.repo_id, range(100000))
131 )).count()
131 )).count()
132 """
132 """
133 if not items:
133 if not items:
134 # empty list will cause empty query which might cause security issues
134 # empty list will cause empty query which might cause security issues
135 # this can lead to hidden unpleasant results
135 # this can lead to hidden unpleasant results
136 items = [-1]
136 items = [-1]
137
137
138 parts = []
138 parts = []
139 for chunk in xrange(0, len(items), limit):
139 for chunk in xrange(0, len(items), limit):
140 parts.append(
140 parts.append(
141 qry.in_(items[chunk: chunk + limit])
141 qry.in_(items[chunk: chunk + limit])
142 )
142 )
143
143
144 return parts
144 return parts
145
145
146
146
147 base_table_args = {
147 base_table_args = {
148 'extend_existing': True,
148 'extend_existing': True,
149 'mysql_engine': 'InnoDB',
149 'mysql_engine': 'InnoDB',
150 'mysql_charset': 'utf8',
150 'mysql_charset': 'utf8',
151 'sqlite_autoincrement': True
151 'sqlite_autoincrement': True
152 }
152 }
153
153
154
154
155 class EncryptedTextValue(TypeDecorator):
155 class EncryptedTextValue(TypeDecorator):
156 """
156 """
157 Special column for encrypted long text data, use like::
157 Special column for encrypted long text data, use like::
158
158
159 value = Column("encrypted_value", EncryptedValue(), nullable=False)
159 value = Column("encrypted_value", EncryptedValue(), nullable=False)
160
160
161 This column is intelligent so if value is in unencrypted form it return
161 This column is intelligent so if value is in unencrypted form it return
162 unencrypted form, but on save it always encrypts
162 unencrypted form, but on save it always encrypts
163 """
163 """
164 impl = Text
164 impl = Text
165
165
166 def process_bind_param(self, value, dialect):
166 def process_bind_param(self, value, dialect):
167 """
167 """
168 Setter for storing value
168 Setter for storing value
169 """
169 """
170 import rhodecode
170 import rhodecode
171 if not value:
171 if not value:
172 return value
172 return value
173
173
174 # protect against double encrypting if values is already encrypted
174 # protect against double encrypting if values is already encrypted
175 if value.startswith('enc$aes$') \
175 if value.startswith('enc$aes$') \
176 or value.startswith('enc$aes_hmac$') \
176 or value.startswith('enc$aes_hmac$') \
177 or value.startswith('enc2$'):
177 or value.startswith('enc2$'):
178 raise ValueError('value needs to be in unencrypted format, '
178 raise ValueError('value needs to be in unencrypted format, '
179 'ie. not starting with enc$ or enc2$')
179 'ie. not starting with enc$ or enc2$')
180
180
181 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
181 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
182 if algo == 'aes':
182 if algo == 'aes':
183 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
183 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
184 elif algo == 'fernet':
184 elif algo == 'fernet':
185 return Encryptor(ENCRYPTION_KEY).encrypt(value)
185 return Encryptor(ENCRYPTION_KEY).encrypt(value)
186 else:
186 else:
187 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
187 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
188
188
189 def process_result_value(self, value, dialect):
189 def process_result_value(self, value, dialect):
190 """
190 """
191 Getter for retrieving value
191 Getter for retrieving value
192 """
192 """
193
193
194 import rhodecode
194 import rhodecode
195 if not value:
195 if not value:
196 return value
196 return value
197
197
198 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
198 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
199 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
199 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
200 if algo == 'aes':
200 if algo == 'aes':
201 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
201 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
202 elif algo == 'fernet':
202 elif algo == 'fernet':
203 return Encryptor(ENCRYPTION_KEY).decrypt(value)
203 return Encryptor(ENCRYPTION_KEY).decrypt(value)
204 else:
204 else:
205 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
205 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
206 return decrypted_data
206 return decrypted_data
207
207
208
208
209 class BaseModel(object):
209 class BaseModel(object):
210 """
210 """
211 Base Model for all classes
211 Base Model for all classes
212 """
212 """
213
213
214 @classmethod
214 @classmethod
215 def _get_keys(cls):
215 def _get_keys(cls):
216 """return column names for this model """
216 """return column names for this model """
217 return class_mapper(cls).c.keys()
217 return class_mapper(cls).c.keys()
218
218
219 def get_dict(self):
219 def get_dict(self):
220 """
220 """
221 return dict with keys and values corresponding
221 return dict with keys and values corresponding
222 to this model data """
222 to this model data """
223
223
224 d = {}
224 d = {}
225 for k in self._get_keys():
225 for k in self._get_keys():
226 d[k] = getattr(self, k)
226 d[k] = getattr(self, k)
227
227
228 # also use __json__() if present to get additional fields
228 # also use __json__() if present to get additional fields
229 _json_attr = getattr(self, '__json__', None)
229 _json_attr = getattr(self, '__json__', None)
230 if _json_attr:
230 if _json_attr:
231 # update with attributes from __json__
231 # update with attributes from __json__
232 if callable(_json_attr):
232 if callable(_json_attr):
233 _json_attr = _json_attr()
233 _json_attr = _json_attr()
234 for k, val in _json_attr.iteritems():
234 for k, val in _json_attr.iteritems():
235 d[k] = val
235 d[k] = val
236 return d
236 return d
237
237
238 def get_appstruct(self):
238 def get_appstruct(self):
239 """return list with keys and values tuples corresponding
239 """return list with keys and values tuples corresponding
240 to this model data """
240 to this model data """
241
241
242 lst = []
242 lst = []
243 for k in self._get_keys():
243 for k in self._get_keys():
244 lst.append((k, getattr(self, k),))
244 lst.append((k, getattr(self, k),))
245 return lst
245 return lst
246
246
247 def populate_obj(self, populate_dict):
247 def populate_obj(self, populate_dict):
248 """populate model with data from given populate_dict"""
248 """populate model with data from given populate_dict"""
249
249
250 for k in self._get_keys():
250 for k in self._get_keys():
251 if k in populate_dict:
251 if k in populate_dict:
252 setattr(self, k, populate_dict[k])
252 setattr(self, k, populate_dict[k])
253
253
254 @classmethod
254 @classmethod
255 def query(cls):
255 def query(cls):
256 return Session().query(cls)
256 return Session().query(cls)
257
257
258 @classmethod
258 @classmethod
259 def get(cls, id_):
259 def get(cls, id_):
260 if id_:
260 if id_:
261 return cls.query().get(id_)
261 return cls.query().get(id_)
262
262
263 @classmethod
263 @classmethod
264 def get_or_404(cls, id_):
264 def get_or_404(cls, id_):
265 from pyramid.httpexceptions import HTTPNotFound
265 from pyramid.httpexceptions import HTTPNotFound
266
266
267 try:
267 try:
268 id_ = int(id_)
268 id_ = int(id_)
269 except (TypeError, ValueError):
269 except (TypeError, ValueError):
270 raise HTTPNotFound()
270 raise HTTPNotFound()
271
271
272 res = cls.query().get(id_)
272 res = cls.query().get(id_)
273 if not res:
273 if not res:
274 raise HTTPNotFound()
274 raise HTTPNotFound()
275 return res
275 return res
276
276
277 @classmethod
277 @classmethod
278 def getAll(cls):
278 def getAll(cls):
279 # deprecated and left for backward compatibility
279 # deprecated and left for backward compatibility
280 return cls.get_all()
280 return cls.get_all()
281
281
282 @classmethod
282 @classmethod
283 def get_all(cls):
283 def get_all(cls):
284 return cls.query().all()
284 return cls.query().all()
285
285
286 @classmethod
286 @classmethod
287 def delete(cls, id_):
287 def delete(cls, id_):
288 obj = cls.query().get(id_)
288 obj = cls.query().get(id_)
289 Session().delete(obj)
289 Session().delete(obj)
290
290
291 @classmethod
291 @classmethod
292 def identity_cache(cls, session, attr_name, value):
292 def identity_cache(cls, session, attr_name, value):
293 exist_in_session = []
293 exist_in_session = []
294 for (item_cls, pkey), instance in session.identity_map.items():
294 for (item_cls, pkey), instance in session.identity_map.items():
295 if cls == item_cls and getattr(instance, attr_name) == value:
295 if cls == item_cls and getattr(instance, attr_name) == value:
296 exist_in_session.append(instance)
296 exist_in_session.append(instance)
297 if exist_in_session:
297 if exist_in_session:
298 if len(exist_in_session) == 1:
298 if len(exist_in_session) == 1:
299 return exist_in_session[0]
299 return exist_in_session[0]
300 log.exception(
300 log.exception(
301 'multiple objects with attr %s and '
301 'multiple objects with attr %s and '
302 'value %s found with same name: %r',
302 'value %s found with same name: %r',
303 attr_name, value, exist_in_session)
303 attr_name, value, exist_in_session)
304
304
305 def __repr__(self):
305 def __repr__(self):
306 if hasattr(self, '__unicode__'):
306 if hasattr(self, '__unicode__'):
307 # python repr needs to return str
307 # python repr needs to return str
308 try:
308 try:
309 return safe_str(self.__unicode__())
309 return safe_str(self.__unicode__())
310 except UnicodeDecodeError:
310 except UnicodeDecodeError:
311 pass
311 pass
312 return '<DB:%s>' % (self.__class__.__name__)
312 return '<DB:%s>' % (self.__class__.__name__)
313
313
314
314
315 class RhodeCodeSetting(Base, BaseModel):
315 class RhodeCodeSetting(Base, BaseModel):
316 __tablename__ = 'rhodecode_settings'
316 __tablename__ = 'rhodecode_settings'
317 __table_args__ = (
317 __table_args__ = (
318 UniqueConstraint('app_settings_name'),
318 UniqueConstraint('app_settings_name'),
319 base_table_args
319 base_table_args
320 )
320 )
321
321
322 SETTINGS_TYPES = {
322 SETTINGS_TYPES = {
323 'str': safe_str,
323 'str': safe_str,
324 'int': safe_int,
324 'int': safe_int,
325 'unicode': safe_unicode,
325 'unicode': safe_unicode,
326 'bool': str2bool,
326 'bool': str2bool,
327 'list': functools.partial(aslist, sep=',')
327 'list': functools.partial(aslist, sep=',')
328 }
328 }
329 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
329 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
330 GLOBAL_CONF_KEY = 'app_settings'
330 GLOBAL_CONF_KEY = 'app_settings'
331
331
332 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
332 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
333 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
333 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
334 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
334 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
335 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
335 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
336
336
337 def __init__(self, key='', val='', type='unicode'):
337 def __init__(self, key='', val='', type='unicode'):
338 self.app_settings_name = key
338 self.app_settings_name = key
339 self.app_settings_type = type
339 self.app_settings_type = type
340 self.app_settings_value = val
340 self.app_settings_value = val
341
341
342 @validates('_app_settings_value')
342 @validates('_app_settings_value')
343 def validate_settings_value(self, key, val):
343 def validate_settings_value(self, key, val):
344 assert type(val) == unicode
344 assert type(val) == unicode
345 return val
345 return val
346
346
347 @hybrid_property
347 @hybrid_property
348 def app_settings_value(self):
348 def app_settings_value(self):
349 v = self._app_settings_value
349 v = self._app_settings_value
350 _type = self.app_settings_type
350 _type = self.app_settings_type
351 if _type:
351 if _type:
352 _type = self.app_settings_type.split('.')[0]
352 _type = self.app_settings_type.split('.')[0]
353 # decode the encrypted value
353 # decode the encrypted value
354 if 'encrypted' in self.app_settings_type:
354 if 'encrypted' in self.app_settings_type:
355 cipher = EncryptedTextValue()
355 cipher = EncryptedTextValue()
356 v = safe_unicode(cipher.process_result_value(v, None))
356 v = safe_unicode(cipher.process_result_value(v, None))
357
357
358 converter = self.SETTINGS_TYPES.get(_type) or \
358 converter = self.SETTINGS_TYPES.get(_type) or \
359 self.SETTINGS_TYPES['unicode']
359 self.SETTINGS_TYPES['unicode']
360 return converter(v)
360 return converter(v)
361
361
362 @app_settings_value.setter
362 @app_settings_value.setter
363 def app_settings_value(self, val):
363 def app_settings_value(self, val):
364 """
364 """
365 Setter that will always make sure we use unicode in app_settings_value
365 Setter that will always make sure we use unicode in app_settings_value
366
366
367 :param val:
367 :param val:
368 """
368 """
369 val = safe_unicode(val)
369 val = safe_unicode(val)
370 # encode the encrypted value
370 # encode the encrypted value
371 if 'encrypted' in self.app_settings_type:
371 if 'encrypted' in self.app_settings_type:
372 cipher = EncryptedTextValue()
372 cipher = EncryptedTextValue()
373 val = safe_unicode(cipher.process_bind_param(val, None))
373 val = safe_unicode(cipher.process_bind_param(val, None))
374 self._app_settings_value = val
374 self._app_settings_value = val
375
375
376 @hybrid_property
376 @hybrid_property
377 def app_settings_type(self):
377 def app_settings_type(self):
378 return self._app_settings_type
378 return self._app_settings_type
379
379
380 @app_settings_type.setter
380 @app_settings_type.setter
381 def app_settings_type(self, val):
381 def app_settings_type(self, val):
382 if val.split('.')[0] not in self.SETTINGS_TYPES:
382 if val.split('.')[0] not in self.SETTINGS_TYPES:
383 raise Exception('type must be one of %s got %s'
383 raise Exception('type must be one of %s got %s'
384 % (self.SETTINGS_TYPES.keys(), val))
384 % (self.SETTINGS_TYPES.keys(), val))
385 self._app_settings_type = val
385 self._app_settings_type = val
386
386
387 @classmethod
387 @classmethod
388 def get_by_prefix(cls, prefix):
388 def get_by_prefix(cls, prefix):
389 return RhodeCodeSetting.query()\
389 return RhodeCodeSetting.query()\
390 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
390 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
391 .all()
391 .all()
392
392
393 def __unicode__(self):
393 def __unicode__(self):
394 return u"<%s('%s:%s[%s]')>" % (
394 return u"<%s('%s:%s[%s]')>" % (
395 self.__class__.__name__,
395 self.__class__.__name__,
396 self.app_settings_name, self.app_settings_value,
396 self.app_settings_name, self.app_settings_value,
397 self.app_settings_type
397 self.app_settings_type
398 )
398 )
399
399
400
400
401 class RhodeCodeUi(Base, BaseModel):
401 class RhodeCodeUi(Base, BaseModel):
402 __tablename__ = 'rhodecode_ui'
402 __tablename__ = 'rhodecode_ui'
403 __table_args__ = (
403 __table_args__ = (
404 UniqueConstraint('ui_key'),
404 UniqueConstraint('ui_key'),
405 base_table_args
405 base_table_args
406 )
406 )
407
407
408 HOOK_REPO_SIZE = 'changegroup.repo_size'
408 HOOK_REPO_SIZE = 'changegroup.repo_size'
409 # HG
409 # HG
410 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
410 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
411 HOOK_PULL = 'outgoing.pull_logger'
411 HOOK_PULL = 'outgoing.pull_logger'
412 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
412 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
413 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
413 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
414 HOOK_PUSH = 'changegroup.push_logger'
414 HOOK_PUSH = 'changegroup.push_logger'
415 HOOK_PUSH_KEY = 'pushkey.key_push'
415 HOOK_PUSH_KEY = 'pushkey.key_push'
416
416
417 HOOKS_BUILTIN = [
417 HOOKS_BUILTIN = [
418 HOOK_PRE_PULL,
418 HOOK_PRE_PULL,
419 HOOK_PULL,
419 HOOK_PULL,
420 HOOK_PRE_PUSH,
420 HOOK_PRE_PUSH,
421 HOOK_PRETX_PUSH,
421 HOOK_PRETX_PUSH,
422 HOOK_PUSH,
422 HOOK_PUSH,
423 HOOK_PUSH_KEY,
423 HOOK_PUSH_KEY,
424 ]
424 ]
425
425
426 # TODO: johbo: Unify way how hooks are configured for git and hg,
426 # TODO: johbo: Unify way how hooks are configured for git and hg,
427 # git part is currently hardcoded.
427 # git part is currently hardcoded.
428
428
429 # SVN PATTERNS
429 # SVN PATTERNS
430 SVN_BRANCH_ID = 'vcs_svn_branch'
430 SVN_BRANCH_ID = 'vcs_svn_branch'
431 SVN_TAG_ID = 'vcs_svn_tag'
431 SVN_TAG_ID = 'vcs_svn_tag'
432
432
433 ui_id = Column(
433 ui_id = Column(
434 "ui_id", Integer(), nullable=False, unique=True, default=None,
434 "ui_id", Integer(), nullable=False, unique=True, default=None,
435 primary_key=True)
435 primary_key=True)
436 ui_section = Column(
436 ui_section = Column(
437 "ui_section", String(255), nullable=True, unique=None, default=None)
437 "ui_section", String(255), nullable=True, unique=None, default=None)
438 ui_key = Column(
438 ui_key = Column(
439 "ui_key", String(255), nullable=True, unique=None, default=None)
439 "ui_key", String(255), nullable=True, unique=None, default=None)
440 ui_value = Column(
440 ui_value = Column(
441 "ui_value", String(255), nullable=True, unique=None, default=None)
441 "ui_value", String(255), nullable=True, unique=None, default=None)
442 ui_active = Column(
442 ui_active = Column(
443 "ui_active", Boolean(), nullable=True, unique=None, default=True)
443 "ui_active", Boolean(), nullable=True, unique=None, default=True)
444
444
445 def __repr__(self):
445 def __repr__(self):
446 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
446 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
447 self.ui_key, self.ui_value)
447 self.ui_key, self.ui_value)
448
448
449
449
450 class RepoRhodeCodeSetting(Base, BaseModel):
450 class RepoRhodeCodeSetting(Base, BaseModel):
451 __tablename__ = 'repo_rhodecode_settings'
451 __tablename__ = 'repo_rhodecode_settings'
452 __table_args__ = (
452 __table_args__ = (
453 UniqueConstraint(
453 UniqueConstraint(
454 'app_settings_name', 'repository_id',
454 'app_settings_name', 'repository_id',
455 name='uq_repo_rhodecode_setting_name_repo_id'),
455 name='uq_repo_rhodecode_setting_name_repo_id'),
456 base_table_args
456 base_table_args
457 )
457 )
458
458
459 repository_id = Column(
459 repository_id = Column(
460 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
460 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
461 nullable=False)
461 nullable=False)
462 app_settings_id = Column(
462 app_settings_id = Column(
463 "app_settings_id", Integer(), nullable=False, unique=True,
463 "app_settings_id", Integer(), nullable=False, unique=True,
464 default=None, primary_key=True)
464 default=None, primary_key=True)
465 app_settings_name = Column(
465 app_settings_name = Column(
466 "app_settings_name", String(255), nullable=True, unique=None,
466 "app_settings_name", String(255), nullable=True, unique=None,
467 default=None)
467 default=None)
468 _app_settings_value = Column(
468 _app_settings_value = Column(
469 "app_settings_value", String(4096), nullable=True, unique=None,
469 "app_settings_value", String(4096), nullable=True, unique=None,
470 default=None)
470 default=None)
471 _app_settings_type = Column(
471 _app_settings_type = Column(
472 "app_settings_type", String(255), nullable=True, unique=None,
472 "app_settings_type", String(255), nullable=True, unique=None,
473 default=None)
473 default=None)
474
474
475 repository = relationship('Repository')
475 repository = relationship('Repository')
476
476
477 def __init__(self, repository_id, key='', val='', type='unicode'):
477 def __init__(self, repository_id, key='', val='', type='unicode'):
478 self.repository_id = repository_id
478 self.repository_id = repository_id
479 self.app_settings_name = key
479 self.app_settings_name = key
480 self.app_settings_type = type
480 self.app_settings_type = type
481 self.app_settings_value = val
481 self.app_settings_value = val
482
482
483 @validates('_app_settings_value')
483 @validates('_app_settings_value')
484 def validate_settings_value(self, key, val):
484 def validate_settings_value(self, key, val):
485 assert type(val) == unicode
485 assert type(val) == unicode
486 return val
486 return val
487
487
488 @hybrid_property
488 @hybrid_property
489 def app_settings_value(self):
489 def app_settings_value(self):
490 v = self._app_settings_value
490 v = self._app_settings_value
491 type_ = self.app_settings_type
491 type_ = self.app_settings_type
492 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
492 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
493 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
493 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
494 return converter(v)
494 return converter(v)
495
495
496 @app_settings_value.setter
496 @app_settings_value.setter
497 def app_settings_value(self, val):
497 def app_settings_value(self, val):
498 """
498 """
499 Setter that will always make sure we use unicode in app_settings_value
499 Setter that will always make sure we use unicode in app_settings_value
500
500
501 :param val:
501 :param val:
502 """
502 """
503 self._app_settings_value = safe_unicode(val)
503 self._app_settings_value = safe_unicode(val)
504
504
505 @hybrid_property
505 @hybrid_property
506 def app_settings_type(self):
506 def app_settings_type(self):
507 return self._app_settings_type
507 return self._app_settings_type
508
508
509 @app_settings_type.setter
509 @app_settings_type.setter
510 def app_settings_type(self, val):
510 def app_settings_type(self, val):
511 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
511 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
512 if val not in SETTINGS_TYPES:
512 if val not in SETTINGS_TYPES:
513 raise Exception('type must be one of %s got %s'
513 raise Exception('type must be one of %s got %s'
514 % (SETTINGS_TYPES.keys(), val))
514 % (SETTINGS_TYPES.keys(), val))
515 self._app_settings_type = val
515 self._app_settings_type = val
516
516
517 def __unicode__(self):
517 def __unicode__(self):
518 return u"<%s('%s:%s:%s[%s]')>" % (
518 return u"<%s('%s:%s:%s[%s]')>" % (
519 self.__class__.__name__, self.repository.repo_name,
519 self.__class__.__name__, self.repository.repo_name,
520 self.app_settings_name, self.app_settings_value,
520 self.app_settings_name, self.app_settings_value,
521 self.app_settings_type
521 self.app_settings_type
522 )
522 )
523
523
524
524
525 class RepoRhodeCodeUi(Base, BaseModel):
525 class RepoRhodeCodeUi(Base, BaseModel):
526 __tablename__ = 'repo_rhodecode_ui'
526 __tablename__ = 'repo_rhodecode_ui'
527 __table_args__ = (
527 __table_args__ = (
528 UniqueConstraint(
528 UniqueConstraint(
529 'repository_id', 'ui_section', 'ui_key',
529 'repository_id', 'ui_section', 'ui_key',
530 name='uq_repo_rhodecode_ui_repository_id_section_key'),
530 name='uq_repo_rhodecode_ui_repository_id_section_key'),
531 base_table_args
531 base_table_args
532 )
532 )
533
533
534 repository_id = Column(
534 repository_id = Column(
535 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
535 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
536 nullable=False)
536 nullable=False)
537 ui_id = Column(
537 ui_id = Column(
538 "ui_id", Integer(), nullable=False, unique=True, default=None,
538 "ui_id", Integer(), nullable=False, unique=True, default=None,
539 primary_key=True)
539 primary_key=True)
540 ui_section = Column(
540 ui_section = Column(
541 "ui_section", String(255), nullable=True, unique=None, default=None)
541 "ui_section", String(255), nullable=True, unique=None, default=None)
542 ui_key = Column(
542 ui_key = Column(
543 "ui_key", String(255), nullable=True, unique=None, default=None)
543 "ui_key", String(255), nullable=True, unique=None, default=None)
544 ui_value = Column(
544 ui_value = Column(
545 "ui_value", String(255), nullable=True, unique=None, default=None)
545 "ui_value", String(255), nullable=True, unique=None, default=None)
546 ui_active = Column(
546 ui_active = Column(
547 "ui_active", Boolean(), nullable=True, unique=None, default=True)
547 "ui_active", Boolean(), nullable=True, unique=None, default=True)
548
548
549 repository = relationship('Repository')
549 repository = relationship('Repository')
550
550
551 def __repr__(self):
551 def __repr__(self):
552 return '<%s[%s:%s]%s=>%s]>' % (
552 return '<%s[%s:%s]%s=>%s]>' % (
553 self.__class__.__name__, self.repository.repo_name,
553 self.__class__.__name__, self.repository.repo_name,
554 self.ui_section, self.ui_key, self.ui_value)
554 self.ui_section, self.ui_key, self.ui_value)
555
555
556
556
557 class User(Base, BaseModel):
557 class User(Base, BaseModel):
558 __tablename__ = 'users'
558 __tablename__ = 'users'
559 __table_args__ = (
559 __table_args__ = (
560 UniqueConstraint('username'), UniqueConstraint('email'),
560 UniqueConstraint('username'), UniqueConstraint('email'),
561 Index('u_username_idx', 'username'),
561 Index('u_username_idx', 'username'),
562 Index('u_email_idx', 'email'),
562 Index('u_email_idx', 'email'),
563 base_table_args
563 base_table_args
564 )
564 )
565
565
566 DEFAULT_USER = 'default'
566 DEFAULT_USER = 'default'
567 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
567 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
568 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
568 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
569
569
570 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
570 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
571 username = Column("username", String(255), nullable=True, unique=None, default=None)
571 username = Column("username", String(255), nullable=True, unique=None, default=None)
572 password = Column("password", String(255), nullable=True, unique=None, default=None)
572 password = Column("password", String(255), nullable=True, unique=None, default=None)
573 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
573 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
574 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
574 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
575 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
575 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
576 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
576 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
577 _email = Column("email", String(255), nullable=True, unique=None, default=None)
577 _email = Column("email", String(255), nullable=True, unique=None, default=None)
578 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
578 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
579 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
579 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
580 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
580 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
581
581
582 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
582 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
583 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
583 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
584 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
584 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
585 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
585 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
586 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
586 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
587 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
587 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
588
588
589 user_log = relationship('UserLog')
589 user_log = relationship('UserLog')
590 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
590 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
591
591
592 repositories = relationship('Repository')
592 repositories = relationship('Repository')
593 repository_groups = relationship('RepoGroup')
593 repository_groups = relationship('RepoGroup')
594 user_groups = relationship('UserGroup')
594 user_groups = relationship('UserGroup')
595
595
596 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
596 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
597 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
597 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
598
598
599 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
599 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
600 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
600 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
601 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
601 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
602
602
603 group_member = relationship('UserGroupMember', cascade='all')
603 group_member = relationship('UserGroupMember', cascade='all')
604
604
605 notifications = relationship('UserNotification', cascade='all')
605 notifications = relationship('UserNotification', cascade='all')
606 # notifications assigned to this user
606 # notifications assigned to this user
607 user_created_notifications = relationship('Notification', cascade='all')
607 user_created_notifications = relationship('Notification', cascade='all')
608 # comments created by this user
608 # comments created by this user
609 user_comments = relationship('ChangesetComment', cascade='all')
609 user_comments = relationship('ChangesetComment', cascade='all')
610 # user profile extra info
610 # user profile extra info
611 user_emails = relationship('UserEmailMap', cascade='all')
611 user_emails = relationship('UserEmailMap', cascade='all')
612 user_ip_map = relationship('UserIpMap', cascade='all')
612 user_ip_map = relationship('UserIpMap', cascade='all')
613 user_auth_tokens = relationship('UserApiKeys', cascade='all')
613 user_auth_tokens = relationship('UserApiKeys', cascade='all')
614 user_ssh_keys = relationship('UserSshKeys', cascade='all')
614 user_ssh_keys = relationship('UserSshKeys', cascade='all')
615
615
616 # gists
616 # gists
617 user_gists = relationship('Gist', cascade='all')
617 user_gists = relationship('Gist', cascade='all')
618 # user pull requests
618 # user pull requests
619 user_pull_requests = relationship('PullRequest', cascade='all')
619 user_pull_requests = relationship('PullRequest', cascade='all')
620
620 # external identities
621 # external identities
621 external_identities = relationship(
622 external_identities = relationship(
622 'ExternalIdentity',
623 'ExternalIdentity',
623 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
624 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
624 cascade='all')
625 cascade='all')
625 # review rules
626 # review rules
626 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
627 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
627
628
628 # artifacts owned
629 # artifacts owned
629 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
630 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
630
631
631 # no cascade, set NULL
632 # no cascade, set NULL
632 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
633 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
633
634
634 def __unicode__(self):
635 def __unicode__(self):
635 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
636 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
636 self.user_id, self.username)
637 self.user_id, self.username)
637
638
638 @hybrid_property
639 @hybrid_property
639 def email(self):
640 def email(self):
640 return self._email
641 return self._email
641
642
642 @email.setter
643 @email.setter
643 def email(self, val):
644 def email(self, val):
644 self._email = val.lower() if val else None
645 self._email = val.lower() if val else None
645
646
646 @hybrid_property
647 @hybrid_property
647 def first_name(self):
648 def first_name(self):
648 from rhodecode.lib import helpers as h
649 from rhodecode.lib import helpers as h
649 if self.name:
650 if self.name:
650 return h.escape(self.name)
651 return h.escape(self.name)
651 return self.name
652 return self.name
652
653
653 @hybrid_property
654 @hybrid_property
654 def last_name(self):
655 def last_name(self):
655 from rhodecode.lib import helpers as h
656 from rhodecode.lib import helpers as h
656 if self.lastname:
657 if self.lastname:
657 return h.escape(self.lastname)
658 return h.escape(self.lastname)
658 return self.lastname
659 return self.lastname
659
660
660 @hybrid_property
661 @hybrid_property
661 def api_key(self):
662 def api_key(self):
662 """
663 """
663 Fetch if exist an auth-token with role ALL connected to this user
664 Fetch if exist an auth-token with role ALL connected to this user
664 """
665 """
665 user_auth_token = UserApiKeys.query()\
666 user_auth_token = UserApiKeys.query()\
666 .filter(UserApiKeys.user_id == self.user_id)\
667 .filter(UserApiKeys.user_id == self.user_id)\
667 .filter(or_(UserApiKeys.expires == -1,
668 .filter(or_(UserApiKeys.expires == -1,
668 UserApiKeys.expires >= time.time()))\
669 UserApiKeys.expires >= time.time()))\
669 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
670 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
670 if user_auth_token:
671 if user_auth_token:
671 user_auth_token = user_auth_token.api_key
672 user_auth_token = user_auth_token.api_key
672
673
673 return user_auth_token
674 return user_auth_token
674
675
675 @api_key.setter
676 @api_key.setter
676 def api_key(self, val):
677 def api_key(self, val):
677 # don't allow to set API key this is deprecated for now
678 # don't allow to set API key this is deprecated for now
678 self._api_key = None
679 self._api_key = None
679
680
680 @property
681 @property
681 def reviewer_pull_requests(self):
682 def reviewer_pull_requests(self):
682 return PullRequestReviewers.query() \
683 return PullRequestReviewers.query() \
683 .options(joinedload(PullRequestReviewers.pull_request)) \
684 .options(joinedload(PullRequestReviewers.pull_request)) \
684 .filter(PullRequestReviewers.user_id == self.user_id) \
685 .filter(PullRequestReviewers.user_id == self.user_id) \
685 .all()
686 .all()
686
687
687 @property
688 @property
688 def firstname(self):
689 def firstname(self):
689 # alias for future
690 # alias for future
690 return self.name
691 return self.name
691
692
692 @property
693 @property
693 def emails(self):
694 def emails(self):
694 other = UserEmailMap.query()\
695 other = UserEmailMap.query()\
695 .filter(UserEmailMap.user == self) \
696 .filter(UserEmailMap.user == self) \
696 .order_by(UserEmailMap.email_id.asc()) \
697 .order_by(UserEmailMap.email_id.asc()) \
697 .all()
698 .all()
698 return [self.email] + [x.email for x in other]
699 return [self.email] + [x.email for x in other]
699
700
700 def emails_cached(self):
701 def emails_cached(self):
701 emails = UserEmailMap.query()\
702 emails = UserEmailMap.query()\
702 .filter(UserEmailMap.user == self) \
703 .filter(UserEmailMap.user == self) \
703 .order_by(UserEmailMap.email_id.asc())
704 .order_by(UserEmailMap.email_id.asc())
704
705
705 emails = emails.options(
706 emails = emails.options(
706 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
707 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
707 )
708 )
708
709
709 return [self.email] + [x.email for x in emails]
710 return [self.email] + [x.email for x in emails]
710
711
711 @property
712 @property
712 def auth_tokens(self):
713 def auth_tokens(self):
713 auth_tokens = self.get_auth_tokens()
714 auth_tokens = self.get_auth_tokens()
714 return [x.api_key for x in auth_tokens]
715 return [x.api_key for x in auth_tokens]
715
716
716 def get_auth_tokens(self):
717 def get_auth_tokens(self):
717 return UserApiKeys.query()\
718 return UserApiKeys.query()\
718 .filter(UserApiKeys.user == self)\
719 .filter(UserApiKeys.user == self)\
719 .order_by(UserApiKeys.user_api_key_id.asc())\
720 .order_by(UserApiKeys.user_api_key_id.asc())\
720 .all()
721 .all()
721
722
722 @LazyProperty
723 @LazyProperty
723 def feed_token(self):
724 def feed_token(self):
724 return self.get_feed_token()
725 return self.get_feed_token()
725
726
726 def get_feed_token(self, cache=True):
727 def get_feed_token(self, cache=True):
727 feed_tokens = UserApiKeys.query()\
728 feed_tokens = UserApiKeys.query()\
728 .filter(UserApiKeys.user == self)\
729 .filter(UserApiKeys.user == self)\
729 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
730 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
730 if cache:
731 if cache:
731 feed_tokens = feed_tokens.options(
732 feed_tokens = feed_tokens.options(
732 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
733 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
733
734
734 feed_tokens = feed_tokens.all()
735 feed_tokens = feed_tokens.all()
735 if feed_tokens:
736 if feed_tokens:
736 return feed_tokens[0].api_key
737 return feed_tokens[0].api_key
737 return 'NO_FEED_TOKEN_AVAILABLE'
738 return 'NO_FEED_TOKEN_AVAILABLE'
738
739
739 @LazyProperty
740 @LazyProperty
740 def artifact_token(self):
741 def artifact_token(self):
741 return self.get_artifact_token()
742 return self.get_artifact_token()
742
743
743 def get_artifact_token(self, cache=True):
744 def get_artifact_token(self, cache=True):
744 artifacts_tokens = UserApiKeys.query()\
745 artifacts_tokens = UserApiKeys.query()\
745 .filter(UserApiKeys.user == self)\
746 .filter(UserApiKeys.user == self)\
746 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
747 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
747 if cache:
748 if cache:
748 artifacts_tokens = artifacts_tokens.options(
749 artifacts_tokens = artifacts_tokens.options(
749 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
750 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
750
751
751 artifacts_tokens = artifacts_tokens.all()
752 artifacts_tokens = artifacts_tokens.all()
752 if artifacts_tokens:
753 if artifacts_tokens:
753 return artifacts_tokens[0].api_key
754 return artifacts_tokens[0].api_key
754 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
755 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
755
756
756 @classmethod
757 @classmethod
757 def get(cls, user_id, cache=False):
758 def get(cls, user_id, cache=False):
758 if not user_id:
759 if not user_id:
759 return
760 return
760
761
761 user = cls.query()
762 user = cls.query()
762 if cache:
763 if cache:
763 user = user.options(
764 user = user.options(
764 FromCache("sql_cache_short", "get_users_%s" % user_id))
765 FromCache("sql_cache_short", "get_users_%s" % user_id))
765 return user.get(user_id)
766 return user.get(user_id)
766
767
767 @classmethod
768 @classmethod
768 def extra_valid_auth_tokens(cls, user, role=None):
769 def extra_valid_auth_tokens(cls, user, role=None):
769 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
770 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
770 .filter(or_(UserApiKeys.expires == -1,
771 .filter(or_(UserApiKeys.expires == -1,
771 UserApiKeys.expires >= time.time()))
772 UserApiKeys.expires >= time.time()))
772 if role:
773 if role:
773 tokens = tokens.filter(or_(UserApiKeys.role == role,
774 tokens = tokens.filter(or_(UserApiKeys.role == role,
774 UserApiKeys.role == UserApiKeys.ROLE_ALL))
775 UserApiKeys.role == UserApiKeys.ROLE_ALL))
775 return tokens.all()
776 return tokens.all()
776
777
777 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
778 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
778 from rhodecode.lib import auth
779 from rhodecode.lib import auth
779
780
780 log.debug('Trying to authenticate user: %s via auth-token, '
781 log.debug('Trying to authenticate user: %s via auth-token, '
781 'and roles: %s', self, roles)
782 'and roles: %s', self, roles)
782
783
783 if not auth_token:
784 if not auth_token:
784 return False
785 return False
785
786
786 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
787 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
787 tokens_q = UserApiKeys.query()\
788 tokens_q = UserApiKeys.query()\
788 .filter(UserApiKeys.user_id == self.user_id)\
789 .filter(UserApiKeys.user_id == self.user_id)\
789 .filter(or_(UserApiKeys.expires == -1,
790 .filter(or_(UserApiKeys.expires == -1,
790 UserApiKeys.expires >= time.time()))
791 UserApiKeys.expires >= time.time()))
791
792
792 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
793 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
793
794
794 crypto_backend = auth.crypto_backend()
795 crypto_backend = auth.crypto_backend()
795 enc_token_map = {}
796 enc_token_map = {}
796 plain_token_map = {}
797 plain_token_map = {}
797 for token in tokens_q:
798 for token in tokens_q:
798 if token.api_key.startswith(crypto_backend.ENC_PREF):
799 if token.api_key.startswith(crypto_backend.ENC_PREF):
799 enc_token_map[token.api_key] = token
800 enc_token_map[token.api_key] = token
800 else:
801 else:
801 plain_token_map[token.api_key] = token
802 plain_token_map[token.api_key] = token
802 log.debug(
803 log.debug(
803 'Found %s plain and %s encrypted tokens to check for authentication for this user',
804 'Found %s plain and %s encrypted tokens to check for authentication for this user',
804 len(plain_token_map), len(enc_token_map))
805 len(plain_token_map), len(enc_token_map))
805
806
806 # plain token match comes first
807 # plain token match comes first
807 match = plain_token_map.get(auth_token)
808 match = plain_token_map.get(auth_token)
808
809
809 # check encrypted tokens now
810 # check encrypted tokens now
810 if not match:
811 if not match:
811 for token_hash, token in enc_token_map.items():
812 for token_hash, token in enc_token_map.items():
812 # NOTE(marcink): this is expensive to calculate, but most secure
813 # NOTE(marcink): this is expensive to calculate, but most secure
813 if crypto_backend.hash_check(auth_token, token_hash):
814 if crypto_backend.hash_check(auth_token, token_hash):
814 match = token
815 match = token
815 break
816 break
816
817
817 if match:
818 if match:
818 log.debug('Found matching token %s', match)
819 log.debug('Found matching token %s', match)
819 if match.repo_id:
820 if match.repo_id:
820 log.debug('Found scope, checking for scope match of token %s', match)
821 log.debug('Found scope, checking for scope match of token %s', match)
821 if match.repo_id == scope_repo_id:
822 if match.repo_id == scope_repo_id:
822 return True
823 return True
823 else:
824 else:
824 log.debug(
825 log.debug(
825 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
826 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
826 'and calling scope is:%s, skipping further checks',
827 'and calling scope is:%s, skipping further checks',
827 match.repo, scope_repo_id)
828 match.repo, scope_repo_id)
828 return False
829 return False
829 else:
830 else:
830 return True
831 return True
831
832
832 return False
833 return False
833
834
834 @property
835 @property
835 def ip_addresses(self):
836 def ip_addresses(self):
836 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
837 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
837 return [x.ip_addr for x in ret]
838 return [x.ip_addr for x in ret]
838
839
839 @property
840 @property
840 def username_and_name(self):
841 def username_and_name(self):
841 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
842 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
842
843
843 @property
844 @property
844 def username_or_name_or_email(self):
845 def username_or_name_or_email(self):
845 full_name = self.full_name if self.full_name is not ' ' else None
846 full_name = self.full_name if self.full_name is not ' ' else None
846 return self.username or full_name or self.email
847 return self.username or full_name or self.email
847
848
848 @property
849 @property
849 def full_name(self):
850 def full_name(self):
850 return '%s %s' % (self.first_name, self.last_name)
851 return '%s %s' % (self.first_name, self.last_name)
851
852
852 @property
853 @property
853 def full_name_or_username(self):
854 def full_name_or_username(self):
854 return ('%s %s' % (self.first_name, self.last_name)
855 return ('%s %s' % (self.first_name, self.last_name)
855 if (self.first_name and self.last_name) else self.username)
856 if (self.first_name and self.last_name) else self.username)
856
857
857 @property
858 @property
858 def full_contact(self):
859 def full_contact(self):
859 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
860 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
860
861
861 @property
862 @property
862 def short_contact(self):
863 def short_contact(self):
863 return '%s %s' % (self.first_name, self.last_name)
864 return '%s %s' % (self.first_name, self.last_name)
864
865
865 @property
866 @property
866 def is_admin(self):
867 def is_admin(self):
867 return self.admin
868 return self.admin
868
869
869 @property
870 @property
870 def language(self):
871 def language(self):
871 return self.user_data.get('language')
872 return self.user_data.get('language')
872
873
873 def AuthUser(self, **kwargs):
874 def AuthUser(self, **kwargs):
874 """
875 """
875 Returns instance of AuthUser for this user
876 Returns instance of AuthUser for this user
876 """
877 """
877 from rhodecode.lib.auth import AuthUser
878 from rhodecode.lib.auth import AuthUser
878 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
879 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
879
880
880 @hybrid_property
881 @hybrid_property
881 def user_data(self):
882 def user_data(self):
882 if not self._user_data:
883 if not self._user_data:
883 return {}
884 return {}
884
885
885 try:
886 try:
886 return json.loads(self._user_data)
887 return json.loads(self._user_data)
887 except TypeError:
888 except TypeError:
888 return {}
889 return {}
889
890
890 @user_data.setter
891 @user_data.setter
891 def user_data(self, val):
892 def user_data(self, val):
892 if not isinstance(val, dict):
893 if not isinstance(val, dict):
893 raise Exception('user_data must be dict, got %s' % type(val))
894 raise Exception('user_data must be dict, got %s' % type(val))
894 try:
895 try:
895 self._user_data = json.dumps(val)
896 self._user_data = json.dumps(val)
896 except Exception:
897 except Exception:
897 log.error(traceback.format_exc())
898 log.error(traceback.format_exc())
898
899
899 @classmethod
900 @classmethod
900 def get_by_username(cls, username, case_insensitive=False,
901 def get_by_username(cls, username, case_insensitive=False,
901 cache=False, identity_cache=False):
902 cache=False, identity_cache=False):
902 session = Session()
903 session = Session()
903
904
904 if case_insensitive:
905 if case_insensitive:
905 q = cls.query().filter(
906 q = cls.query().filter(
906 func.lower(cls.username) == func.lower(username))
907 func.lower(cls.username) == func.lower(username))
907 else:
908 else:
908 q = cls.query().filter(cls.username == username)
909 q = cls.query().filter(cls.username == username)
909
910
910 if cache:
911 if cache:
911 if identity_cache:
912 if identity_cache:
912 val = cls.identity_cache(session, 'username', username)
913 val = cls.identity_cache(session, 'username', username)
913 if val:
914 if val:
914 return val
915 return val
915 else:
916 else:
916 cache_key = "get_user_by_name_%s" % _hash_key(username)
917 cache_key = "get_user_by_name_%s" % _hash_key(username)
917 q = q.options(
918 q = q.options(
918 FromCache("sql_cache_short", cache_key))
919 FromCache("sql_cache_short", cache_key))
919
920
920 return q.scalar()
921 return q.scalar()
921
922
922 @classmethod
923 @classmethod
923 def get_by_auth_token(cls, auth_token, cache=False):
924 def get_by_auth_token(cls, auth_token, cache=False):
924 q = UserApiKeys.query()\
925 q = UserApiKeys.query()\
925 .filter(UserApiKeys.api_key == auth_token)\
926 .filter(UserApiKeys.api_key == auth_token)\
926 .filter(or_(UserApiKeys.expires == -1,
927 .filter(or_(UserApiKeys.expires == -1,
927 UserApiKeys.expires >= time.time()))
928 UserApiKeys.expires >= time.time()))
928 if cache:
929 if cache:
929 q = q.options(
930 q = q.options(
930 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
931 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
931
932
932 match = q.first()
933 match = q.first()
933 if match:
934 if match:
934 return match.user
935 return match.user
935
936
936 @classmethod
937 @classmethod
937 def get_by_email(cls, email, case_insensitive=False, cache=False):
938 def get_by_email(cls, email, case_insensitive=False, cache=False):
938
939
939 if case_insensitive:
940 if case_insensitive:
940 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
941 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
941
942
942 else:
943 else:
943 q = cls.query().filter(cls.email == email)
944 q = cls.query().filter(cls.email == email)
944
945
945 email_key = _hash_key(email)
946 email_key = _hash_key(email)
946 if cache:
947 if cache:
947 q = q.options(
948 q = q.options(
948 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
949 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
949
950
950 ret = q.scalar()
951 ret = q.scalar()
951 if ret is None:
952 if ret is None:
952 q = UserEmailMap.query()
953 q = UserEmailMap.query()
953 # try fetching in alternate email map
954 # try fetching in alternate email map
954 if case_insensitive:
955 if case_insensitive:
955 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
956 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
956 else:
957 else:
957 q = q.filter(UserEmailMap.email == email)
958 q = q.filter(UserEmailMap.email == email)
958 q = q.options(joinedload(UserEmailMap.user))
959 q = q.options(joinedload(UserEmailMap.user))
959 if cache:
960 if cache:
960 q = q.options(
961 q = q.options(
961 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
962 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
962 ret = getattr(q.scalar(), 'user', None)
963 ret = getattr(q.scalar(), 'user', None)
963
964
964 return ret
965 return ret
965
966
966 @classmethod
967 @classmethod
967 def get_from_cs_author(cls, author):
968 def get_from_cs_author(cls, author):
968 """
969 """
969 Tries to get User objects out of commit author string
970 Tries to get User objects out of commit author string
970
971
971 :param author:
972 :param author:
972 """
973 """
973 from rhodecode.lib.helpers import email, author_name
974 from rhodecode.lib.helpers import email, author_name
974 # Valid email in the attribute passed, see if they're in the system
975 # Valid email in the attribute passed, see if they're in the system
975 _email = email(author)
976 _email = email(author)
976 if _email:
977 if _email:
977 user = cls.get_by_email(_email, case_insensitive=True)
978 user = cls.get_by_email(_email, case_insensitive=True)
978 if user:
979 if user:
979 return user
980 return user
980 # Maybe we can match by username?
981 # Maybe we can match by username?
981 _author = author_name(author)
982 _author = author_name(author)
982 user = cls.get_by_username(_author, case_insensitive=True)
983 user = cls.get_by_username(_author, case_insensitive=True)
983 if user:
984 if user:
984 return user
985 return user
985
986
986 def update_userdata(self, **kwargs):
987 def update_userdata(self, **kwargs):
987 usr = self
988 usr = self
988 old = usr.user_data
989 old = usr.user_data
989 old.update(**kwargs)
990 old.update(**kwargs)
990 usr.user_data = old
991 usr.user_data = old
991 Session().add(usr)
992 Session().add(usr)
992 log.debug('updated userdata with %s', kwargs)
993 log.debug('updated userdata with %s', kwargs)
993
994
994 def update_lastlogin(self):
995 def update_lastlogin(self):
995 """Update user lastlogin"""
996 """Update user lastlogin"""
996 self.last_login = datetime.datetime.now()
997 self.last_login = datetime.datetime.now()
997 Session().add(self)
998 Session().add(self)
998 log.debug('updated user %s lastlogin', self.username)
999 log.debug('updated user %s lastlogin', self.username)
999
1000
1000 def update_password(self, new_password):
1001 def update_password(self, new_password):
1001 from rhodecode.lib.auth import get_crypt_password
1002 from rhodecode.lib.auth import get_crypt_password
1002
1003
1003 self.password = get_crypt_password(new_password)
1004 self.password = get_crypt_password(new_password)
1004 Session().add(self)
1005 Session().add(self)
1005
1006
1006 @classmethod
1007 @classmethod
1007 def get_first_super_admin(cls):
1008 def get_first_super_admin(cls):
1008 user = User.query()\
1009 user = User.query()\
1009 .filter(User.admin == true()) \
1010 .filter(User.admin == true()) \
1010 .order_by(User.user_id.asc()) \
1011 .order_by(User.user_id.asc()) \
1011 .first()
1012 .first()
1012
1013
1013 if user is None:
1014 if user is None:
1014 raise Exception('FATAL: Missing administrative account!')
1015 raise Exception('FATAL: Missing administrative account!')
1015 return user
1016 return user
1016
1017
1017 @classmethod
1018 @classmethod
1018 def get_all_super_admins(cls, only_active=False):
1019 def get_all_super_admins(cls, only_active=False):
1019 """
1020 """
1020 Returns all admin accounts sorted by username
1021 Returns all admin accounts sorted by username
1021 """
1022 """
1022 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1023 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1023 if only_active:
1024 if only_active:
1024 qry = qry.filter(User.active == true())
1025 qry = qry.filter(User.active == true())
1025 return qry.all()
1026 return qry.all()
1026
1027
1027 @classmethod
1028 @classmethod
1028 def get_all_user_ids(cls, only_active=True):
1029 def get_all_user_ids(cls, only_active=True):
1029 """
1030 """
1030 Returns all users IDs
1031 Returns all users IDs
1031 """
1032 """
1032 qry = Session().query(User.user_id)
1033 qry = Session().query(User.user_id)
1033
1034
1034 if only_active:
1035 if only_active:
1035 qry = qry.filter(User.active == true())
1036 qry = qry.filter(User.active == true())
1036 return [x.user_id for x in qry]
1037 return [x.user_id for x in qry]
1037
1038
1038 @classmethod
1039 @classmethod
1039 def get_default_user(cls, cache=False, refresh=False):
1040 def get_default_user(cls, cache=False, refresh=False):
1040 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1041 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1041 if user is None:
1042 if user is None:
1042 raise Exception('FATAL: Missing default account!')
1043 raise Exception('FATAL: Missing default account!')
1043 if refresh:
1044 if refresh:
1044 # The default user might be based on outdated state which
1045 # The default user might be based on outdated state which
1045 # has been loaded from the cache.
1046 # has been loaded from the cache.
1046 # A call to refresh() ensures that the
1047 # A call to refresh() ensures that the
1047 # latest state from the database is used.
1048 # latest state from the database is used.
1048 Session().refresh(user)
1049 Session().refresh(user)
1049 return user
1050 return user
1050
1051
1051 @classmethod
1052 @classmethod
1052 def get_default_user_id(cls):
1053 def get_default_user_id(cls):
1053 import rhodecode
1054 import rhodecode
1054 return rhodecode.CONFIG['default_user_id']
1055 return rhodecode.CONFIG['default_user_id']
1055
1056
1056 def _get_default_perms(self, user, suffix=''):
1057 def _get_default_perms(self, user, suffix=''):
1057 from rhodecode.model.permission import PermissionModel
1058 from rhodecode.model.permission import PermissionModel
1058 return PermissionModel().get_default_perms(user.user_perms, suffix)
1059 return PermissionModel().get_default_perms(user.user_perms, suffix)
1059
1060
1060 def get_default_perms(self, suffix=''):
1061 def get_default_perms(self, suffix=''):
1061 return self._get_default_perms(self, suffix)
1062 return self._get_default_perms(self, suffix)
1062
1063
1063 def get_api_data(self, include_secrets=False, details='full'):
1064 def get_api_data(self, include_secrets=False, details='full'):
1064 """
1065 """
1065 Common function for generating user related data for API
1066 Common function for generating user related data for API
1066
1067
1067 :param include_secrets: By default secrets in the API data will be replaced
1068 :param include_secrets: By default secrets in the API data will be replaced
1068 by a placeholder value to prevent exposing this data by accident. In case
1069 by a placeholder value to prevent exposing this data by accident. In case
1069 this data shall be exposed, set this flag to ``True``.
1070 this data shall be exposed, set this flag to ``True``.
1070
1071
1071 :param details: details can be 'basic|full' basic gives only a subset of
1072 :param details: details can be 'basic|full' basic gives only a subset of
1072 the available user information that includes user_id, name and emails.
1073 the available user information that includes user_id, name and emails.
1073 """
1074 """
1074 user = self
1075 user = self
1075 user_data = self.user_data
1076 user_data = self.user_data
1076 data = {
1077 data = {
1077 'user_id': user.user_id,
1078 'user_id': user.user_id,
1078 'username': user.username,
1079 'username': user.username,
1079 'firstname': user.name,
1080 'firstname': user.name,
1080 'lastname': user.lastname,
1081 'lastname': user.lastname,
1081 'description': user.description,
1082 'description': user.description,
1082 'email': user.email,
1083 'email': user.email,
1083 'emails': user.emails,
1084 'emails': user.emails,
1084 }
1085 }
1085 if details == 'basic':
1086 if details == 'basic':
1086 return data
1087 return data
1087
1088
1088 auth_token_length = 40
1089 auth_token_length = 40
1089 auth_token_replacement = '*' * auth_token_length
1090 auth_token_replacement = '*' * auth_token_length
1090
1091
1091 extras = {
1092 extras = {
1092 'auth_tokens': [auth_token_replacement],
1093 'auth_tokens': [auth_token_replacement],
1093 'active': user.active,
1094 'active': user.active,
1094 'admin': user.admin,
1095 'admin': user.admin,
1095 'extern_type': user.extern_type,
1096 'extern_type': user.extern_type,
1096 'extern_name': user.extern_name,
1097 'extern_name': user.extern_name,
1097 'last_login': user.last_login,
1098 'last_login': user.last_login,
1098 'last_activity': user.last_activity,
1099 'last_activity': user.last_activity,
1099 'ip_addresses': user.ip_addresses,
1100 'ip_addresses': user.ip_addresses,
1100 'language': user_data.get('language')
1101 'language': user_data.get('language')
1101 }
1102 }
1102 data.update(extras)
1103 data.update(extras)
1103
1104
1104 if include_secrets:
1105 if include_secrets:
1105 data['auth_tokens'] = user.auth_tokens
1106 data['auth_tokens'] = user.auth_tokens
1106 return data
1107 return data
1107
1108
1108 def __json__(self):
1109 def __json__(self):
1109 data = {
1110 data = {
1110 'full_name': self.full_name,
1111 'full_name': self.full_name,
1111 'full_name_or_username': self.full_name_or_username,
1112 'full_name_or_username': self.full_name_or_username,
1112 'short_contact': self.short_contact,
1113 'short_contact': self.short_contact,
1113 'full_contact': self.full_contact,
1114 'full_contact': self.full_contact,
1114 }
1115 }
1115 data.update(self.get_api_data())
1116 data.update(self.get_api_data())
1116 return data
1117 return data
1117
1118
1118
1119
1119 class UserApiKeys(Base, BaseModel):
1120 class UserApiKeys(Base, BaseModel):
1120 __tablename__ = 'user_api_keys'
1121 __tablename__ = 'user_api_keys'
1121 __table_args__ = (
1122 __table_args__ = (
1122 Index('uak_api_key_idx', 'api_key'),
1123 Index('uak_api_key_idx', 'api_key'),
1123 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1124 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1124 base_table_args
1125 base_table_args
1125 )
1126 )
1126 __mapper_args__ = {}
1127 __mapper_args__ = {}
1127
1128
1128 # ApiKey role
1129 # ApiKey role
1129 ROLE_ALL = 'token_role_all'
1130 ROLE_ALL = 'token_role_all'
1130 ROLE_HTTP = 'token_role_http'
1131 ROLE_HTTP = 'token_role_http'
1131 ROLE_VCS = 'token_role_vcs'
1132 ROLE_VCS = 'token_role_vcs'
1132 ROLE_API = 'token_role_api'
1133 ROLE_API = 'token_role_api'
1133 ROLE_FEED = 'token_role_feed'
1134 ROLE_FEED = 'token_role_feed'
1134 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1135 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1135 ROLE_PASSWORD_RESET = 'token_password_reset'
1136 ROLE_PASSWORD_RESET = 'token_password_reset'
1136
1137
1137 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1138 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1138
1139
1139 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1140 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1140 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1141 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1141 api_key = Column("api_key", String(255), nullable=False, unique=True)
1142 api_key = Column("api_key", String(255), nullable=False, unique=True)
1142 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1143 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1143 expires = Column('expires', Float(53), nullable=False)
1144 expires = Column('expires', Float(53), nullable=False)
1144 role = Column('role', String(255), nullable=True)
1145 role = Column('role', String(255), nullable=True)
1145 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1146 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1146
1147
1147 # scope columns
1148 # scope columns
1148 repo_id = Column(
1149 repo_id = Column(
1149 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1150 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1150 nullable=True, unique=None, default=None)
1151 nullable=True, unique=None, default=None)
1151 repo = relationship('Repository', lazy='joined')
1152 repo = relationship('Repository', lazy='joined')
1152
1153
1153 repo_group_id = Column(
1154 repo_group_id = Column(
1154 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1155 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1155 nullable=True, unique=None, default=None)
1156 nullable=True, unique=None, default=None)
1156 repo_group = relationship('RepoGroup', lazy='joined')
1157 repo_group = relationship('RepoGroup', lazy='joined')
1157
1158
1158 user = relationship('User', lazy='joined')
1159 user = relationship('User', lazy='joined')
1159
1160
1160 def __unicode__(self):
1161 def __unicode__(self):
1161 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1162 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1162
1163
1163 def __json__(self):
1164 def __json__(self):
1164 data = {
1165 data = {
1165 'auth_token': self.api_key,
1166 'auth_token': self.api_key,
1166 'role': self.role,
1167 'role': self.role,
1167 'scope': self.scope_humanized,
1168 'scope': self.scope_humanized,
1168 'expired': self.expired
1169 'expired': self.expired
1169 }
1170 }
1170 return data
1171 return data
1171
1172
1172 def get_api_data(self, include_secrets=False):
1173 def get_api_data(self, include_secrets=False):
1173 data = self.__json__()
1174 data = self.__json__()
1174 if include_secrets:
1175 if include_secrets:
1175 return data
1176 return data
1176 else:
1177 else:
1177 data['auth_token'] = self.token_obfuscated
1178 data['auth_token'] = self.token_obfuscated
1178 return data
1179 return data
1179
1180
1180 @hybrid_property
1181 @hybrid_property
1181 def description_safe(self):
1182 def description_safe(self):
1182 from rhodecode.lib import helpers as h
1183 from rhodecode.lib import helpers as h
1183 return h.escape(self.description)
1184 return h.escape(self.description)
1184
1185
1185 @property
1186 @property
1186 def expired(self):
1187 def expired(self):
1187 if self.expires == -1:
1188 if self.expires == -1:
1188 return False
1189 return False
1189 return time.time() > self.expires
1190 return time.time() > self.expires
1190
1191
1191 @classmethod
1192 @classmethod
1192 def _get_role_name(cls, role):
1193 def _get_role_name(cls, role):
1193 return {
1194 return {
1194 cls.ROLE_ALL: _('all'),
1195 cls.ROLE_ALL: _('all'),
1195 cls.ROLE_HTTP: _('http/web interface'),
1196 cls.ROLE_HTTP: _('http/web interface'),
1196 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1197 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1197 cls.ROLE_API: _('api calls'),
1198 cls.ROLE_API: _('api calls'),
1198 cls.ROLE_FEED: _('feed access'),
1199 cls.ROLE_FEED: _('feed access'),
1199 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1200 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1200 }.get(role, role)
1201 }.get(role, role)
1201
1202
1202 @property
1203 @property
1203 def role_humanized(self):
1204 def role_humanized(self):
1204 return self._get_role_name(self.role)
1205 return self._get_role_name(self.role)
1205
1206
1206 def _get_scope(self):
1207 def _get_scope(self):
1207 if self.repo:
1208 if self.repo:
1208 return 'Repository: {}'.format(self.repo.repo_name)
1209 return 'Repository: {}'.format(self.repo.repo_name)
1209 if self.repo_group:
1210 if self.repo_group:
1210 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1211 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1211 return 'Global'
1212 return 'Global'
1212
1213
1213 @property
1214 @property
1214 def scope_humanized(self):
1215 def scope_humanized(self):
1215 return self._get_scope()
1216 return self._get_scope()
1216
1217
1217 @property
1218 @property
1218 def token_obfuscated(self):
1219 def token_obfuscated(self):
1219 if self.api_key:
1220 if self.api_key:
1220 return self.api_key[:4] + "****"
1221 return self.api_key[:4] + "****"
1221
1222
1222
1223
1223 class UserEmailMap(Base, BaseModel):
1224 class UserEmailMap(Base, BaseModel):
1224 __tablename__ = 'user_email_map'
1225 __tablename__ = 'user_email_map'
1225 __table_args__ = (
1226 __table_args__ = (
1226 Index('uem_email_idx', 'email'),
1227 Index('uem_email_idx', 'email'),
1227 UniqueConstraint('email'),
1228 UniqueConstraint('email'),
1228 base_table_args
1229 base_table_args
1229 )
1230 )
1230 __mapper_args__ = {}
1231 __mapper_args__ = {}
1231
1232
1232 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1233 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1233 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1234 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1234 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1235 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1235 user = relationship('User', lazy='joined')
1236 user = relationship('User', lazy='joined')
1236
1237
1237 @validates('_email')
1238 @validates('_email')
1238 def validate_email(self, key, email):
1239 def validate_email(self, key, email):
1239 # check if this email is not main one
1240 # check if this email is not main one
1240 main_email = Session().query(User).filter(User.email == email).scalar()
1241 main_email = Session().query(User).filter(User.email == email).scalar()
1241 if main_email is not None:
1242 if main_email is not None:
1242 raise AttributeError('email %s is present is user table' % email)
1243 raise AttributeError('email %s is present is user table' % email)
1243 return email
1244 return email
1244
1245
1245 @hybrid_property
1246 @hybrid_property
1246 def email(self):
1247 def email(self):
1247 return self._email
1248 return self._email
1248
1249
1249 @email.setter
1250 @email.setter
1250 def email(self, val):
1251 def email(self, val):
1251 self._email = val.lower() if val else None
1252 self._email = val.lower() if val else None
1252
1253
1253
1254
1254 class UserIpMap(Base, BaseModel):
1255 class UserIpMap(Base, BaseModel):
1255 __tablename__ = 'user_ip_map'
1256 __tablename__ = 'user_ip_map'
1256 __table_args__ = (
1257 __table_args__ = (
1257 UniqueConstraint('user_id', 'ip_addr'),
1258 UniqueConstraint('user_id', 'ip_addr'),
1258 base_table_args
1259 base_table_args
1259 )
1260 )
1260 __mapper_args__ = {}
1261 __mapper_args__ = {}
1261
1262
1262 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1263 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1263 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1264 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1264 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1265 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1265 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1266 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1266 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1267 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1267 user = relationship('User', lazy='joined')
1268 user = relationship('User', lazy='joined')
1268
1269
1269 @hybrid_property
1270 @hybrid_property
1270 def description_safe(self):
1271 def description_safe(self):
1271 from rhodecode.lib import helpers as h
1272 from rhodecode.lib import helpers as h
1272 return h.escape(self.description)
1273 return h.escape(self.description)
1273
1274
1274 @classmethod
1275 @classmethod
1275 def _get_ip_range(cls, ip_addr):
1276 def _get_ip_range(cls, ip_addr):
1276 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1277 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1277 return [str(net.network_address), str(net.broadcast_address)]
1278 return [str(net.network_address), str(net.broadcast_address)]
1278
1279
1279 def __json__(self):
1280 def __json__(self):
1280 return {
1281 return {
1281 'ip_addr': self.ip_addr,
1282 'ip_addr': self.ip_addr,
1282 'ip_range': self._get_ip_range(self.ip_addr),
1283 'ip_range': self._get_ip_range(self.ip_addr),
1283 }
1284 }
1284
1285
1285 def __unicode__(self):
1286 def __unicode__(self):
1286 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1287 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1287 self.user_id, self.ip_addr)
1288 self.user_id, self.ip_addr)
1288
1289
1289
1290
1290 class UserSshKeys(Base, BaseModel):
1291 class UserSshKeys(Base, BaseModel):
1291 __tablename__ = 'user_ssh_keys'
1292 __tablename__ = 'user_ssh_keys'
1292 __table_args__ = (
1293 __table_args__ = (
1293 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1294 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1294
1295
1295 UniqueConstraint('ssh_key_fingerprint'),
1296 UniqueConstraint('ssh_key_fingerprint'),
1296
1297
1297 base_table_args
1298 base_table_args
1298 )
1299 )
1299 __mapper_args__ = {}
1300 __mapper_args__ = {}
1300
1301
1301 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1302 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1302 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1303 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1303 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1304 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1304
1305
1305 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1306 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1306
1307
1307 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1308 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1308 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1309 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1309 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1310 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1310
1311
1311 user = relationship('User', lazy='joined')
1312 user = relationship('User', lazy='joined')
1312
1313
1313 def __json__(self):
1314 def __json__(self):
1314 data = {
1315 data = {
1315 'ssh_fingerprint': self.ssh_key_fingerprint,
1316 'ssh_fingerprint': self.ssh_key_fingerprint,
1316 'description': self.description,
1317 'description': self.description,
1317 'created_on': self.created_on
1318 'created_on': self.created_on
1318 }
1319 }
1319 return data
1320 return data
1320
1321
1321 def get_api_data(self):
1322 def get_api_data(self):
1322 data = self.__json__()
1323 data = self.__json__()
1323 return data
1324 return data
1324
1325
1325
1326
1326 class UserLog(Base, BaseModel):
1327 class UserLog(Base, BaseModel):
1327 __tablename__ = 'user_logs'
1328 __tablename__ = 'user_logs'
1328 __table_args__ = (
1329 __table_args__ = (
1329 base_table_args,
1330 base_table_args,
1330 )
1331 )
1331
1332
1332 VERSION_1 = 'v1'
1333 VERSION_1 = 'v1'
1333 VERSION_2 = 'v2'
1334 VERSION_2 = 'v2'
1334 VERSIONS = [VERSION_1, VERSION_2]
1335 VERSIONS = [VERSION_1, VERSION_2]
1335
1336
1336 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1337 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1337 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1338 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1338 username = Column("username", String(255), nullable=True, unique=None, default=None)
1339 username = Column("username", String(255), nullable=True, unique=None, default=None)
1339 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1340 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1340 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1341 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1341 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1342 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1342 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1343 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1343 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1344 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1344
1345
1345 version = Column("version", String(255), nullable=True, default=VERSION_1)
1346 version = Column("version", String(255), nullable=True, default=VERSION_1)
1346 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1347 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1347 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1348 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1348
1349
1349 def __unicode__(self):
1350 def __unicode__(self):
1350 return u"<%s('id:%s:%s')>" % (
1351 return u"<%s('id:%s:%s')>" % (
1351 self.__class__.__name__, self.repository_name, self.action)
1352 self.__class__.__name__, self.repository_name, self.action)
1352
1353
1353 def __json__(self):
1354 def __json__(self):
1354 return {
1355 return {
1355 'user_id': self.user_id,
1356 'user_id': self.user_id,
1356 'username': self.username,
1357 'username': self.username,
1357 'repository_id': self.repository_id,
1358 'repository_id': self.repository_id,
1358 'repository_name': self.repository_name,
1359 'repository_name': self.repository_name,
1359 'user_ip': self.user_ip,
1360 'user_ip': self.user_ip,
1360 'action_date': self.action_date,
1361 'action_date': self.action_date,
1361 'action': self.action,
1362 'action': self.action,
1362 }
1363 }
1363
1364
1364 @hybrid_property
1365 @hybrid_property
1365 def entry_id(self):
1366 def entry_id(self):
1366 return self.user_log_id
1367 return self.user_log_id
1367
1368
1368 @property
1369 @property
1369 def action_as_day(self):
1370 def action_as_day(self):
1370 return datetime.date(*self.action_date.timetuple()[:3])
1371 return datetime.date(*self.action_date.timetuple()[:3])
1371
1372
1372 user = relationship('User')
1373 user = relationship('User')
1373 repository = relationship('Repository', cascade='')
1374 repository = relationship('Repository', cascade='')
1374
1375
1375
1376
1376 class UserGroup(Base, BaseModel):
1377 class UserGroup(Base, BaseModel):
1377 __tablename__ = 'users_groups'
1378 __tablename__ = 'users_groups'
1378 __table_args__ = (
1379 __table_args__ = (
1379 base_table_args,
1380 base_table_args,
1380 )
1381 )
1381
1382
1382 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1383 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1383 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1384 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1384 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1385 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1385 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1386 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1386 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1387 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1387 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1388 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1388 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1389 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1389 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1390 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1390
1391
1391 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1392 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1392 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1393 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1393 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1394 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1394 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1395 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1395 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1396 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1396 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1397 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1397
1398
1398 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1399 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1399 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1400 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1400
1401
1401 @classmethod
1402 @classmethod
1402 def _load_group_data(cls, column):
1403 def _load_group_data(cls, column):
1403 if not column:
1404 if not column:
1404 return {}
1405 return {}
1405
1406
1406 try:
1407 try:
1407 return json.loads(column) or {}
1408 return json.loads(column) or {}
1408 except TypeError:
1409 except TypeError:
1409 return {}
1410 return {}
1410
1411
1411 @hybrid_property
1412 @hybrid_property
1412 def description_safe(self):
1413 def description_safe(self):
1413 from rhodecode.lib import helpers as h
1414 from rhodecode.lib import helpers as h
1414 return h.escape(self.user_group_description)
1415 return h.escape(self.user_group_description)
1415
1416
1416 @hybrid_property
1417 @hybrid_property
1417 def group_data(self):
1418 def group_data(self):
1418 return self._load_group_data(self._group_data)
1419 return self._load_group_data(self._group_data)
1419
1420
1420 @group_data.expression
1421 @group_data.expression
1421 def group_data(self, **kwargs):
1422 def group_data(self, **kwargs):
1422 return self._group_data
1423 return self._group_data
1423
1424
1424 @group_data.setter
1425 @group_data.setter
1425 def group_data(self, val):
1426 def group_data(self, val):
1426 try:
1427 try:
1427 self._group_data = json.dumps(val)
1428 self._group_data = json.dumps(val)
1428 except Exception:
1429 except Exception:
1429 log.error(traceback.format_exc())
1430 log.error(traceback.format_exc())
1430
1431
1431 @classmethod
1432 @classmethod
1432 def _load_sync(cls, group_data):
1433 def _load_sync(cls, group_data):
1433 if group_data:
1434 if group_data:
1434 return group_data.get('extern_type')
1435 return group_data.get('extern_type')
1435
1436
1436 @property
1437 @property
1437 def sync(self):
1438 def sync(self):
1438 return self._load_sync(self.group_data)
1439 return self._load_sync(self.group_data)
1439
1440
1440 def __unicode__(self):
1441 def __unicode__(self):
1441 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1442 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1442 self.users_group_id,
1443 self.users_group_id,
1443 self.users_group_name)
1444 self.users_group_name)
1444
1445
1445 @classmethod
1446 @classmethod
1446 def get_by_group_name(cls, group_name, cache=False,
1447 def get_by_group_name(cls, group_name, cache=False,
1447 case_insensitive=False):
1448 case_insensitive=False):
1448 if case_insensitive:
1449 if case_insensitive:
1449 q = cls.query().filter(func.lower(cls.users_group_name) ==
1450 q = cls.query().filter(func.lower(cls.users_group_name) ==
1450 func.lower(group_name))
1451 func.lower(group_name))
1451
1452
1452 else:
1453 else:
1453 q = cls.query().filter(cls.users_group_name == group_name)
1454 q = cls.query().filter(cls.users_group_name == group_name)
1454 if cache:
1455 if cache:
1455 q = q.options(
1456 q = q.options(
1456 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1457 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1457 return q.scalar()
1458 return q.scalar()
1458
1459
1459 @classmethod
1460 @classmethod
1460 def get(cls, user_group_id, cache=False):
1461 def get(cls, user_group_id, cache=False):
1461 if not user_group_id:
1462 if not user_group_id:
1462 return
1463 return
1463
1464
1464 user_group = cls.query()
1465 user_group = cls.query()
1465 if cache:
1466 if cache:
1466 user_group = user_group.options(
1467 user_group = user_group.options(
1467 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1468 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1468 return user_group.get(user_group_id)
1469 return user_group.get(user_group_id)
1469
1470
1470 def permissions(self, with_admins=True, with_owner=True,
1471 def permissions(self, with_admins=True, with_owner=True,
1471 expand_from_user_groups=False):
1472 expand_from_user_groups=False):
1472 """
1473 """
1473 Permissions for user groups
1474 Permissions for user groups
1474 """
1475 """
1475 _admin_perm = 'usergroup.admin'
1476 _admin_perm = 'usergroup.admin'
1476
1477
1477 owner_row = []
1478 owner_row = []
1478 if with_owner:
1479 if with_owner:
1479 usr = AttributeDict(self.user.get_dict())
1480 usr = AttributeDict(self.user.get_dict())
1480 usr.owner_row = True
1481 usr.owner_row = True
1481 usr.permission = _admin_perm
1482 usr.permission = _admin_perm
1482 owner_row.append(usr)
1483 owner_row.append(usr)
1483
1484
1484 super_admin_ids = []
1485 super_admin_ids = []
1485 super_admin_rows = []
1486 super_admin_rows = []
1486 if with_admins:
1487 if with_admins:
1487 for usr in User.get_all_super_admins():
1488 for usr in User.get_all_super_admins():
1488 super_admin_ids.append(usr.user_id)
1489 super_admin_ids.append(usr.user_id)
1489 # if this admin is also owner, don't double the record
1490 # if this admin is also owner, don't double the record
1490 if usr.user_id == owner_row[0].user_id:
1491 if usr.user_id == owner_row[0].user_id:
1491 owner_row[0].admin_row = True
1492 owner_row[0].admin_row = True
1492 else:
1493 else:
1493 usr = AttributeDict(usr.get_dict())
1494 usr = AttributeDict(usr.get_dict())
1494 usr.admin_row = True
1495 usr.admin_row = True
1495 usr.permission = _admin_perm
1496 usr.permission = _admin_perm
1496 super_admin_rows.append(usr)
1497 super_admin_rows.append(usr)
1497
1498
1498 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1499 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1499 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1500 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1500 joinedload(UserUserGroupToPerm.user),
1501 joinedload(UserUserGroupToPerm.user),
1501 joinedload(UserUserGroupToPerm.permission),)
1502 joinedload(UserUserGroupToPerm.permission),)
1502
1503
1503 # get owners and admins and permissions. We do a trick of re-writing
1504 # get owners and admins and permissions. We do a trick of re-writing
1504 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1505 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1505 # has a global reference and changing one object propagates to all
1506 # has a global reference and changing one object propagates to all
1506 # others. This means if admin is also an owner admin_row that change
1507 # others. This means if admin is also an owner admin_row that change
1507 # would propagate to both objects
1508 # would propagate to both objects
1508 perm_rows = []
1509 perm_rows = []
1509 for _usr in q.all():
1510 for _usr in q.all():
1510 usr = AttributeDict(_usr.user.get_dict())
1511 usr = AttributeDict(_usr.user.get_dict())
1511 # if this user is also owner/admin, mark as duplicate record
1512 # if this user is also owner/admin, mark as duplicate record
1512 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1513 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1513 usr.duplicate_perm = True
1514 usr.duplicate_perm = True
1514 usr.permission = _usr.permission.permission_name
1515 usr.permission = _usr.permission.permission_name
1515 perm_rows.append(usr)
1516 perm_rows.append(usr)
1516
1517
1517 # filter the perm rows by 'default' first and then sort them by
1518 # filter the perm rows by 'default' first and then sort them by
1518 # admin,write,read,none permissions sorted again alphabetically in
1519 # admin,write,read,none permissions sorted again alphabetically in
1519 # each group
1520 # each group
1520 perm_rows = sorted(perm_rows, key=display_user_sort)
1521 perm_rows = sorted(perm_rows, key=display_user_sort)
1521
1522
1522 user_groups_rows = []
1523 user_groups_rows = []
1523 if expand_from_user_groups:
1524 if expand_from_user_groups:
1524 for ug in self.permission_user_groups(with_members=True):
1525 for ug in self.permission_user_groups(with_members=True):
1525 for user_data in ug.members:
1526 for user_data in ug.members:
1526 user_groups_rows.append(user_data)
1527 user_groups_rows.append(user_data)
1527
1528
1528 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1529 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1529
1530
1530 def permission_user_groups(self, with_members=False):
1531 def permission_user_groups(self, with_members=False):
1531 q = UserGroupUserGroupToPerm.query()\
1532 q = UserGroupUserGroupToPerm.query()\
1532 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1533 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1533 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1534 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1534 joinedload(UserGroupUserGroupToPerm.target_user_group),
1535 joinedload(UserGroupUserGroupToPerm.target_user_group),
1535 joinedload(UserGroupUserGroupToPerm.permission),)
1536 joinedload(UserGroupUserGroupToPerm.permission),)
1536
1537
1537 perm_rows = []
1538 perm_rows = []
1538 for _user_group in q.all():
1539 for _user_group in q.all():
1539 entry = AttributeDict(_user_group.user_group.get_dict())
1540 entry = AttributeDict(_user_group.user_group.get_dict())
1540 entry.permission = _user_group.permission.permission_name
1541 entry.permission = _user_group.permission.permission_name
1541 if with_members:
1542 if with_members:
1542 entry.members = [x.user.get_dict()
1543 entry.members = [x.user.get_dict()
1543 for x in _user_group.user_group.members]
1544 for x in _user_group.user_group.members]
1544 perm_rows.append(entry)
1545 perm_rows.append(entry)
1545
1546
1546 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1547 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1547 return perm_rows
1548 return perm_rows
1548
1549
1549 def _get_default_perms(self, user_group, suffix=''):
1550 def _get_default_perms(self, user_group, suffix=''):
1550 from rhodecode.model.permission import PermissionModel
1551 from rhodecode.model.permission import PermissionModel
1551 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1552 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1552
1553
1553 def get_default_perms(self, suffix=''):
1554 def get_default_perms(self, suffix=''):
1554 return self._get_default_perms(self, suffix)
1555 return self._get_default_perms(self, suffix)
1555
1556
1556 def get_api_data(self, with_group_members=True, include_secrets=False):
1557 def get_api_data(self, with_group_members=True, include_secrets=False):
1557 """
1558 """
1558 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1559 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1559 basically forwarded.
1560 basically forwarded.
1560
1561
1561 """
1562 """
1562 user_group = self
1563 user_group = self
1563 data = {
1564 data = {
1564 'users_group_id': user_group.users_group_id,
1565 'users_group_id': user_group.users_group_id,
1565 'group_name': user_group.users_group_name,
1566 'group_name': user_group.users_group_name,
1566 'group_description': user_group.user_group_description,
1567 'group_description': user_group.user_group_description,
1567 'active': user_group.users_group_active,
1568 'active': user_group.users_group_active,
1568 'owner': user_group.user.username,
1569 'owner': user_group.user.username,
1569 'sync': user_group.sync,
1570 'sync': user_group.sync,
1570 'owner_email': user_group.user.email,
1571 'owner_email': user_group.user.email,
1571 }
1572 }
1572
1573
1573 if with_group_members:
1574 if with_group_members:
1574 users = []
1575 users = []
1575 for user in user_group.members:
1576 for user in user_group.members:
1576 user = user.user
1577 user = user.user
1577 users.append(user.get_api_data(include_secrets=include_secrets))
1578 users.append(user.get_api_data(include_secrets=include_secrets))
1578 data['users'] = users
1579 data['users'] = users
1579
1580
1580 return data
1581 return data
1581
1582
1582
1583
1583 class UserGroupMember(Base, BaseModel):
1584 class UserGroupMember(Base, BaseModel):
1584 __tablename__ = 'users_groups_members'
1585 __tablename__ = 'users_groups_members'
1585 __table_args__ = (
1586 __table_args__ = (
1586 base_table_args,
1587 base_table_args,
1587 )
1588 )
1588
1589
1589 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1590 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1590 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1591 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1591 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1592 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1592
1593
1593 user = relationship('User', lazy='joined')
1594 user = relationship('User', lazy='joined')
1594 users_group = relationship('UserGroup')
1595 users_group = relationship('UserGroup')
1595
1596
1596 def __init__(self, gr_id='', u_id=''):
1597 def __init__(self, gr_id='', u_id=''):
1597 self.users_group_id = gr_id
1598 self.users_group_id = gr_id
1598 self.user_id = u_id
1599 self.user_id = u_id
1599
1600
1600
1601
1601 class RepositoryField(Base, BaseModel):
1602 class RepositoryField(Base, BaseModel):
1602 __tablename__ = 'repositories_fields'
1603 __tablename__ = 'repositories_fields'
1603 __table_args__ = (
1604 __table_args__ = (
1604 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1605 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1605 base_table_args,
1606 base_table_args,
1606 )
1607 )
1607
1608
1608 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1609 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1609
1610
1610 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1611 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1611 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1612 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1612 field_key = Column("field_key", String(250))
1613 field_key = Column("field_key", String(250))
1613 field_label = Column("field_label", String(1024), nullable=False)
1614 field_label = Column("field_label", String(1024), nullable=False)
1614 field_value = Column("field_value", String(10000), nullable=False)
1615 field_value = Column("field_value", String(10000), nullable=False)
1615 field_desc = Column("field_desc", String(1024), nullable=False)
1616 field_desc = Column("field_desc", String(1024), nullable=False)
1616 field_type = Column("field_type", String(255), nullable=False, unique=None)
1617 field_type = Column("field_type", String(255), nullable=False, unique=None)
1617 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1618 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1618
1619
1619 repository = relationship('Repository')
1620 repository = relationship('Repository')
1620
1621
1621 @property
1622 @property
1622 def field_key_prefixed(self):
1623 def field_key_prefixed(self):
1623 return 'ex_%s' % self.field_key
1624 return 'ex_%s' % self.field_key
1624
1625
1625 @classmethod
1626 @classmethod
1626 def un_prefix_key(cls, key):
1627 def un_prefix_key(cls, key):
1627 if key.startswith(cls.PREFIX):
1628 if key.startswith(cls.PREFIX):
1628 return key[len(cls.PREFIX):]
1629 return key[len(cls.PREFIX):]
1629 return key
1630 return key
1630
1631
1631 @classmethod
1632 @classmethod
1632 def get_by_key_name(cls, key, repo):
1633 def get_by_key_name(cls, key, repo):
1633 row = cls.query()\
1634 row = cls.query()\
1634 .filter(cls.repository == repo)\
1635 .filter(cls.repository == repo)\
1635 .filter(cls.field_key == key).scalar()
1636 .filter(cls.field_key == key).scalar()
1636 return row
1637 return row
1637
1638
1638
1639
1639 class Repository(Base, BaseModel):
1640 class Repository(Base, BaseModel):
1640 __tablename__ = 'repositories'
1641 __tablename__ = 'repositories'
1641 __table_args__ = (
1642 __table_args__ = (
1642 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1643 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1643 base_table_args,
1644 base_table_args,
1644 )
1645 )
1645 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1646 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1646 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1647 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1647 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1648 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1648
1649
1649 STATE_CREATED = 'repo_state_created'
1650 STATE_CREATED = 'repo_state_created'
1650 STATE_PENDING = 'repo_state_pending'
1651 STATE_PENDING = 'repo_state_pending'
1651 STATE_ERROR = 'repo_state_error'
1652 STATE_ERROR = 'repo_state_error'
1652
1653
1653 LOCK_AUTOMATIC = 'lock_auto'
1654 LOCK_AUTOMATIC = 'lock_auto'
1654 LOCK_API = 'lock_api'
1655 LOCK_API = 'lock_api'
1655 LOCK_WEB = 'lock_web'
1656 LOCK_WEB = 'lock_web'
1656 LOCK_PULL = 'lock_pull'
1657 LOCK_PULL = 'lock_pull'
1657
1658
1658 NAME_SEP = URL_SEP
1659 NAME_SEP = URL_SEP
1659
1660
1660 repo_id = Column(
1661 repo_id = Column(
1661 "repo_id", Integer(), nullable=False, unique=True, default=None,
1662 "repo_id", Integer(), nullable=False, unique=True, default=None,
1662 primary_key=True)
1663 primary_key=True)
1663 _repo_name = Column(
1664 _repo_name = Column(
1664 "repo_name", Text(), nullable=False, default=None)
1665 "repo_name", Text(), nullable=False, default=None)
1665 repo_name_hash = Column(
1666 repo_name_hash = Column(
1666 "repo_name_hash", String(255), nullable=False, unique=True)
1667 "repo_name_hash", String(255), nullable=False, unique=True)
1667 repo_state = Column("repo_state", String(255), nullable=True)
1668 repo_state = Column("repo_state", String(255), nullable=True)
1668
1669
1669 clone_uri = Column(
1670 clone_uri = Column(
1670 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1671 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1671 default=None)
1672 default=None)
1672 push_uri = Column(
1673 push_uri = Column(
1673 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1674 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1674 default=None)
1675 default=None)
1675 repo_type = Column(
1676 repo_type = Column(
1676 "repo_type", String(255), nullable=False, unique=False, default=None)
1677 "repo_type", String(255), nullable=False, unique=False, default=None)
1677 user_id = Column(
1678 user_id = Column(
1678 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1679 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1679 unique=False, default=None)
1680 unique=False, default=None)
1680 private = Column(
1681 private = Column(
1681 "private", Boolean(), nullable=True, unique=None, default=None)
1682 "private", Boolean(), nullable=True, unique=None, default=None)
1682 archived = Column(
1683 archived = Column(
1683 "archived", Boolean(), nullable=True, unique=None, default=None)
1684 "archived", Boolean(), nullable=True, unique=None, default=None)
1684 enable_statistics = Column(
1685 enable_statistics = Column(
1685 "statistics", Boolean(), nullable=True, unique=None, default=True)
1686 "statistics", Boolean(), nullable=True, unique=None, default=True)
1686 enable_downloads = Column(
1687 enable_downloads = Column(
1687 "downloads", Boolean(), nullable=True, unique=None, default=True)
1688 "downloads", Boolean(), nullable=True, unique=None, default=True)
1688 description = Column(
1689 description = Column(
1689 "description", String(10000), nullable=True, unique=None, default=None)
1690 "description", String(10000), nullable=True, unique=None, default=None)
1690 created_on = Column(
1691 created_on = Column(
1691 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1692 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1692 default=datetime.datetime.now)
1693 default=datetime.datetime.now)
1693 updated_on = Column(
1694 updated_on = Column(
1694 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1695 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1695 default=datetime.datetime.now)
1696 default=datetime.datetime.now)
1696 _landing_revision = Column(
1697 _landing_revision = Column(
1697 "landing_revision", String(255), nullable=False, unique=False,
1698 "landing_revision", String(255), nullable=False, unique=False,
1698 default=None)
1699 default=None)
1699 enable_locking = Column(
1700 enable_locking = Column(
1700 "enable_locking", Boolean(), nullable=False, unique=None,
1701 "enable_locking", Boolean(), nullable=False, unique=None,
1701 default=False)
1702 default=False)
1702 _locked = Column(
1703 _locked = Column(
1703 "locked", String(255), nullable=True, unique=False, default=None)
1704 "locked", String(255), nullable=True, unique=False, default=None)
1704 _changeset_cache = Column(
1705 _changeset_cache = Column(
1705 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1706 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1706
1707
1707 fork_id = Column(
1708 fork_id = Column(
1708 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1709 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1709 nullable=True, unique=False, default=None)
1710 nullable=True, unique=False, default=None)
1710 group_id = Column(
1711 group_id = Column(
1711 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1712 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1712 unique=False, default=None)
1713 unique=False, default=None)
1713
1714
1714 user = relationship('User', lazy='joined')
1715 user = relationship('User', lazy='joined')
1715 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1716 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1716 group = relationship('RepoGroup', lazy='joined')
1717 group = relationship('RepoGroup', lazy='joined')
1717 repo_to_perm = relationship(
1718 repo_to_perm = relationship(
1718 'UserRepoToPerm', cascade='all',
1719 'UserRepoToPerm', cascade='all',
1719 order_by='UserRepoToPerm.repo_to_perm_id')
1720 order_by='UserRepoToPerm.repo_to_perm_id')
1720 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1721 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1721 stats = relationship('Statistics', cascade='all', uselist=False)
1722 stats = relationship('Statistics', cascade='all', uselist=False)
1722
1723
1723 followers = relationship(
1724 followers = relationship(
1724 'UserFollowing',
1725 'UserFollowing',
1725 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1726 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1726 cascade='all')
1727 cascade='all')
1727 extra_fields = relationship(
1728 extra_fields = relationship(
1728 'RepositoryField', cascade="all, delete-orphan")
1729 'RepositoryField', cascade="all, delete-orphan")
1729 logs = relationship('UserLog')
1730 logs = relationship('UserLog')
1730 comments = relationship(
1731 comments = relationship(
1731 'ChangesetComment', cascade="all, delete-orphan")
1732 'ChangesetComment', cascade="all, delete-orphan")
1732 pull_requests_source = relationship(
1733 pull_requests_source = relationship(
1733 'PullRequest',
1734 'PullRequest',
1734 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1735 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1735 cascade="all, delete-orphan")
1736 cascade="all, delete-orphan")
1736 pull_requests_target = relationship(
1737 pull_requests_target = relationship(
1737 'PullRequest',
1738 'PullRequest',
1738 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1739 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1739 cascade="all, delete-orphan")
1740 cascade="all, delete-orphan")
1740 ui = relationship('RepoRhodeCodeUi', cascade="all")
1741 ui = relationship('RepoRhodeCodeUi', cascade="all")
1741 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1742 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1742 integrations = relationship('Integration', cascade="all, delete-orphan")
1743 integrations = relationship('Integration', cascade="all, delete-orphan")
1743
1744
1744 scoped_tokens = relationship('UserApiKeys', cascade="all")
1745 scoped_tokens = relationship('UserApiKeys', cascade="all")
1745
1746
1746 # no cascade, set NULL
1747 # no cascade, set NULL
1747 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1748 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1748
1749
1749 def __unicode__(self):
1750 def __unicode__(self):
1750 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1751 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1751 safe_unicode(self.repo_name))
1752 safe_unicode(self.repo_name))
1752
1753
1753 @hybrid_property
1754 @hybrid_property
1754 def description_safe(self):
1755 def description_safe(self):
1755 from rhodecode.lib import helpers as h
1756 from rhodecode.lib import helpers as h
1756 return h.escape(self.description)
1757 return h.escape(self.description)
1757
1758
1758 @hybrid_property
1759 @hybrid_property
1759 def landing_rev(self):
1760 def landing_rev(self):
1760 # always should return [rev_type, rev]
1761 # always should return [rev_type, rev]
1761 if self._landing_revision:
1762 if self._landing_revision:
1762 _rev_info = self._landing_revision.split(':')
1763 _rev_info = self._landing_revision.split(':')
1763 if len(_rev_info) < 2:
1764 if len(_rev_info) < 2:
1764 _rev_info.insert(0, 'rev')
1765 _rev_info.insert(0, 'rev')
1765 return [_rev_info[0], _rev_info[1]]
1766 return [_rev_info[0], _rev_info[1]]
1766 return [None, None]
1767 return [None, None]
1767
1768
1768 @landing_rev.setter
1769 @landing_rev.setter
1769 def landing_rev(self, val):
1770 def landing_rev(self, val):
1770 if ':' not in val:
1771 if ':' not in val:
1771 raise ValueError('value must be delimited with `:` and consist '
1772 raise ValueError('value must be delimited with `:` and consist '
1772 'of <rev_type>:<rev>, got %s instead' % val)
1773 'of <rev_type>:<rev>, got %s instead' % val)
1773 self._landing_revision = val
1774 self._landing_revision = val
1774
1775
1775 @hybrid_property
1776 @hybrid_property
1776 def locked(self):
1777 def locked(self):
1777 if self._locked:
1778 if self._locked:
1778 user_id, timelocked, reason = self._locked.split(':')
1779 user_id, timelocked, reason = self._locked.split(':')
1779 lock_values = int(user_id), timelocked, reason
1780 lock_values = int(user_id), timelocked, reason
1780 else:
1781 else:
1781 lock_values = [None, None, None]
1782 lock_values = [None, None, None]
1782 return lock_values
1783 return lock_values
1783
1784
1784 @locked.setter
1785 @locked.setter
1785 def locked(self, val):
1786 def locked(self, val):
1786 if val and isinstance(val, (list, tuple)):
1787 if val and isinstance(val, (list, tuple)):
1787 self._locked = ':'.join(map(str, val))
1788 self._locked = ':'.join(map(str, val))
1788 else:
1789 else:
1789 self._locked = None
1790 self._locked = None
1790
1791
1791 @classmethod
1792 @classmethod
1792 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1793 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1793 from rhodecode.lib.vcs.backends.base import EmptyCommit
1794 from rhodecode.lib.vcs.backends.base import EmptyCommit
1794 dummy = EmptyCommit().__json__()
1795 dummy = EmptyCommit().__json__()
1795 if not changeset_cache_raw:
1796 if not changeset_cache_raw:
1796 dummy['source_repo_id'] = repo_id
1797 dummy['source_repo_id'] = repo_id
1797 return json.loads(json.dumps(dummy))
1798 return json.loads(json.dumps(dummy))
1798
1799
1799 try:
1800 try:
1800 return json.loads(changeset_cache_raw)
1801 return json.loads(changeset_cache_raw)
1801 except TypeError:
1802 except TypeError:
1802 return dummy
1803 return dummy
1803 except Exception:
1804 except Exception:
1804 log.error(traceback.format_exc())
1805 log.error(traceback.format_exc())
1805 return dummy
1806 return dummy
1806
1807
1807 @hybrid_property
1808 @hybrid_property
1808 def changeset_cache(self):
1809 def changeset_cache(self):
1809 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1810 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1810
1811
1811 @changeset_cache.setter
1812 @changeset_cache.setter
1812 def changeset_cache(self, val):
1813 def changeset_cache(self, val):
1813 try:
1814 try:
1814 self._changeset_cache = json.dumps(val)
1815 self._changeset_cache = json.dumps(val)
1815 except Exception:
1816 except Exception:
1816 log.error(traceback.format_exc())
1817 log.error(traceback.format_exc())
1817
1818
1818 @hybrid_property
1819 @hybrid_property
1819 def repo_name(self):
1820 def repo_name(self):
1820 return self._repo_name
1821 return self._repo_name
1821
1822
1822 @repo_name.setter
1823 @repo_name.setter
1823 def repo_name(self, value):
1824 def repo_name(self, value):
1824 self._repo_name = value
1825 self._repo_name = value
1825 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1826 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1826
1827
1827 @classmethod
1828 @classmethod
1828 def normalize_repo_name(cls, repo_name):
1829 def normalize_repo_name(cls, repo_name):
1829 """
1830 """
1830 Normalizes os specific repo_name to the format internally stored inside
1831 Normalizes os specific repo_name to the format internally stored inside
1831 database using URL_SEP
1832 database using URL_SEP
1832
1833
1833 :param cls:
1834 :param cls:
1834 :param repo_name:
1835 :param repo_name:
1835 """
1836 """
1836 return cls.NAME_SEP.join(repo_name.split(os.sep))
1837 return cls.NAME_SEP.join(repo_name.split(os.sep))
1837
1838
1838 @classmethod
1839 @classmethod
1839 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1840 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1840 session = Session()
1841 session = Session()
1841 q = session.query(cls).filter(cls.repo_name == repo_name)
1842 q = session.query(cls).filter(cls.repo_name == repo_name)
1842
1843
1843 if cache:
1844 if cache:
1844 if identity_cache:
1845 if identity_cache:
1845 val = cls.identity_cache(session, 'repo_name', repo_name)
1846 val = cls.identity_cache(session, 'repo_name', repo_name)
1846 if val:
1847 if val:
1847 return val
1848 return val
1848 else:
1849 else:
1849 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1850 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1850 q = q.options(
1851 q = q.options(
1851 FromCache("sql_cache_short", cache_key))
1852 FromCache("sql_cache_short", cache_key))
1852
1853
1853 return q.scalar()
1854 return q.scalar()
1854
1855
1855 @classmethod
1856 @classmethod
1856 def get_by_id_or_repo_name(cls, repoid):
1857 def get_by_id_or_repo_name(cls, repoid):
1857 if isinstance(repoid, (int, long)):
1858 if isinstance(repoid, (int, long)):
1858 try:
1859 try:
1859 repo = cls.get(repoid)
1860 repo = cls.get(repoid)
1860 except ValueError:
1861 except ValueError:
1861 repo = None
1862 repo = None
1862 else:
1863 else:
1863 repo = cls.get_by_repo_name(repoid)
1864 repo = cls.get_by_repo_name(repoid)
1864 return repo
1865 return repo
1865
1866
1866 @classmethod
1867 @classmethod
1867 def get_by_full_path(cls, repo_full_path):
1868 def get_by_full_path(cls, repo_full_path):
1868 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1869 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1869 repo_name = cls.normalize_repo_name(repo_name)
1870 repo_name = cls.normalize_repo_name(repo_name)
1870 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1871 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1871
1872
1872 @classmethod
1873 @classmethod
1873 def get_repo_forks(cls, repo_id):
1874 def get_repo_forks(cls, repo_id):
1874 return cls.query().filter(Repository.fork_id == repo_id)
1875 return cls.query().filter(Repository.fork_id == repo_id)
1875
1876
1876 @classmethod
1877 @classmethod
1877 def base_path(cls):
1878 def base_path(cls):
1878 """
1879 """
1879 Returns base path when all repos are stored
1880 Returns base path when all repos are stored
1880
1881
1881 :param cls:
1882 :param cls:
1882 """
1883 """
1883 q = Session().query(RhodeCodeUi)\
1884 q = Session().query(RhodeCodeUi)\
1884 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1885 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1885 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1886 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1886 return q.one().ui_value
1887 return q.one().ui_value
1887
1888
1888 @classmethod
1889 @classmethod
1889 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1890 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1890 case_insensitive=True, archived=False):
1891 case_insensitive=True, archived=False):
1891 q = Repository.query()
1892 q = Repository.query()
1892
1893
1893 if not archived:
1894 if not archived:
1894 q = q.filter(Repository.archived.isnot(true()))
1895 q = q.filter(Repository.archived.isnot(true()))
1895
1896
1896 if not isinstance(user_id, Optional):
1897 if not isinstance(user_id, Optional):
1897 q = q.filter(Repository.user_id == user_id)
1898 q = q.filter(Repository.user_id == user_id)
1898
1899
1899 if not isinstance(group_id, Optional):
1900 if not isinstance(group_id, Optional):
1900 q = q.filter(Repository.group_id == group_id)
1901 q = q.filter(Repository.group_id == group_id)
1901
1902
1902 if case_insensitive:
1903 if case_insensitive:
1903 q = q.order_by(func.lower(Repository.repo_name))
1904 q = q.order_by(func.lower(Repository.repo_name))
1904 else:
1905 else:
1905 q = q.order_by(Repository.repo_name)
1906 q = q.order_by(Repository.repo_name)
1906
1907
1907 return q.all()
1908 return q.all()
1908
1909
1909 @property
1910 @property
1910 def repo_uid(self):
1911 def repo_uid(self):
1911 return '_{}'.format(self.repo_id)
1912 return '_{}'.format(self.repo_id)
1912
1913
1913 @property
1914 @property
1914 def forks(self):
1915 def forks(self):
1915 """
1916 """
1916 Return forks of this repo
1917 Return forks of this repo
1917 """
1918 """
1918 return Repository.get_repo_forks(self.repo_id)
1919 return Repository.get_repo_forks(self.repo_id)
1919
1920
1920 @property
1921 @property
1921 def parent(self):
1922 def parent(self):
1922 """
1923 """
1923 Returns fork parent
1924 Returns fork parent
1924 """
1925 """
1925 return self.fork
1926 return self.fork
1926
1927
1927 @property
1928 @property
1928 def just_name(self):
1929 def just_name(self):
1929 return self.repo_name.split(self.NAME_SEP)[-1]
1930 return self.repo_name.split(self.NAME_SEP)[-1]
1930
1931
1931 @property
1932 @property
1932 def groups_with_parents(self):
1933 def groups_with_parents(self):
1933 groups = []
1934 groups = []
1934 if self.group is None:
1935 if self.group is None:
1935 return groups
1936 return groups
1936
1937
1937 cur_gr = self.group
1938 cur_gr = self.group
1938 groups.insert(0, cur_gr)
1939 groups.insert(0, cur_gr)
1939 while 1:
1940 while 1:
1940 gr = getattr(cur_gr, 'parent_group', None)
1941 gr = getattr(cur_gr, 'parent_group', None)
1941 cur_gr = cur_gr.parent_group
1942 cur_gr = cur_gr.parent_group
1942 if gr is None:
1943 if gr is None:
1943 break
1944 break
1944 groups.insert(0, gr)
1945 groups.insert(0, gr)
1945
1946
1946 return groups
1947 return groups
1947
1948
1948 @property
1949 @property
1949 def groups_and_repo(self):
1950 def groups_and_repo(self):
1950 return self.groups_with_parents, self
1951 return self.groups_with_parents, self
1951
1952
1952 @LazyProperty
1953 @LazyProperty
1953 def repo_path(self):
1954 def repo_path(self):
1954 """
1955 """
1955 Returns base full path for that repository means where it actually
1956 Returns base full path for that repository means where it actually
1956 exists on a filesystem
1957 exists on a filesystem
1957 """
1958 """
1958 q = Session().query(RhodeCodeUi).filter(
1959 q = Session().query(RhodeCodeUi).filter(
1959 RhodeCodeUi.ui_key == self.NAME_SEP)
1960 RhodeCodeUi.ui_key == self.NAME_SEP)
1960 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1961 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1961 return q.one().ui_value
1962 return q.one().ui_value
1962
1963
1963 @property
1964 @property
1964 def repo_full_path(self):
1965 def repo_full_path(self):
1965 p = [self.repo_path]
1966 p = [self.repo_path]
1966 # we need to split the name by / since this is how we store the
1967 # we need to split the name by / since this is how we store the
1967 # names in the database, but that eventually needs to be converted
1968 # names in the database, but that eventually needs to be converted
1968 # into a valid system path
1969 # into a valid system path
1969 p += self.repo_name.split(self.NAME_SEP)
1970 p += self.repo_name.split(self.NAME_SEP)
1970 return os.path.join(*map(safe_unicode, p))
1971 return os.path.join(*map(safe_unicode, p))
1971
1972
1972 @property
1973 @property
1973 def cache_keys(self):
1974 def cache_keys(self):
1974 """
1975 """
1975 Returns associated cache keys for that repo
1976 Returns associated cache keys for that repo
1976 """
1977 """
1977 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
1978 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
1978 repo_id=self.repo_id)
1979 repo_id=self.repo_id)
1979 return CacheKey.query()\
1980 return CacheKey.query()\
1980 .filter(CacheKey.cache_args == invalidation_namespace)\
1981 .filter(CacheKey.cache_args == invalidation_namespace)\
1981 .order_by(CacheKey.cache_key)\
1982 .order_by(CacheKey.cache_key)\
1982 .all()
1983 .all()
1983
1984
1984 @property
1985 @property
1985 def cached_diffs_relative_dir(self):
1986 def cached_diffs_relative_dir(self):
1986 """
1987 """
1987 Return a relative to the repository store path of cached diffs
1988 Return a relative to the repository store path of cached diffs
1988 used for safe display for users, who shouldn't know the absolute store
1989 used for safe display for users, who shouldn't know the absolute store
1989 path
1990 path
1990 """
1991 """
1991 return os.path.join(
1992 return os.path.join(
1992 os.path.dirname(self.repo_name),
1993 os.path.dirname(self.repo_name),
1993 self.cached_diffs_dir.split(os.path.sep)[-1])
1994 self.cached_diffs_dir.split(os.path.sep)[-1])
1994
1995
1995 @property
1996 @property
1996 def cached_diffs_dir(self):
1997 def cached_diffs_dir(self):
1997 path = self.repo_full_path
1998 path = self.repo_full_path
1998 return os.path.join(
1999 return os.path.join(
1999 os.path.dirname(path),
2000 os.path.dirname(path),
2000 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2001 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2001
2002
2002 def cached_diffs(self):
2003 def cached_diffs(self):
2003 diff_cache_dir = self.cached_diffs_dir
2004 diff_cache_dir = self.cached_diffs_dir
2004 if os.path.isdir(diff_cache_dir):
2005 if os.path.isdir(diff_cache_dir):
2005 return os.listdir(diff_cache_dir)
2006 return os.listdir(diff_cache_dir)
2006 return []
2007 return []
2007
2008
2008 def shadow_repos(self):
2009 def shadow_repos(self):
2009 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2010 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2010 return [
2011 return [
2011 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2012 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2012 if x.startswith(shadow_repos_pattern)]
2013 if x.startswith(shadow_repos_pattern)]
2013
2014
2014 def get_new_name(self, repo_name):
2015 def get_new_name(self, repo_name):
2015 """
2016 """
2016 returns new full repository name based on assigned group and new new
2017 returns new full repository name based on assigned group and new new
2017
2018
2018 :param group_name:
2019 :param group_name:
2019 """
2020 """
2020 path_prefix = self.group.full_path_splitted if self.group else []
2021 path_prefix = self.group.full_path_splitted if self.group else []
2021 return self.NAME_SEP.join(path_prefix + [repo_name])
2022 return self.NAME_SEP.join(path_prefix + [repo_name])
2022
2023
2023 @property
2024 @property
2024 def _config(self):
2025 def _config(self):
2025 """
2026 """
2026 Returns db based config object.
2027 Returns db based config object.
2027 """
2028 """
2028 from rhodecode.lib.utils import make_db_config
2029 from rhodecode.lib.utils import make_db_config
2029 return make_db_config(clear_session=False, repo=self)
2030 return make_db_config(clear_session=False, repo=self)
2030
2031
2031 def permissions(self, with_admins=True, with_owner=True,
2032 def permissions(self, with_admins=True, with_owner=True,
2032 expand_from_user_groups=False):
2033 expand_from_user_groups=False):
2033 """
2034 """
2034 Permissions for repositories
2035 Permissions for repositories
2035 """
2036 """
2036 _admin_perm = 'repository.admin'
2037 _admin_perm = 'repository.admin'
2037
2038
2038 owner_row = []
2039 owner_row = []
2039 if with_owner:
2040 if with_owner:
2040 usr = AttributeDict(self.user.get_dict())
2041 usr = AttributeDict(self.user.get_dict())
2041 usr.owner_row = True
2042 usr.owner_row = True
2042 usr.permission = _admin_perm
2043 usr.permission = _admin_perm
2043 usr.permission_id = None
2044 usr.permission_id = None
2044 owner_row.append(usr)
2045 owner_row.append(usr)
2045
2046
2046 super_admin_ids = []
2047 super_admin_ids = []
2047 super_admin_rows = []
2048 super_admin_rows = []
2048 if with_admins:
2049 if with_admins:
2049 for usr in User.get_all_super_admins():
2050 for usr in User.get_all_super_admins():
2050 super_admin_ids.append(usr.user_id)
2051 super_admin_ids.append(usr.user_id)
2051 # if this admin is also owner, don't double the record
2052 # if this admin is also owner, don't double the record
2052 if usr.user_id == owner_row[0].user_id:
2053 if usr.user_id == owner_row[0].user_id:
2053 owner_row[0].admin_row = True
2054 owner_row[0].admin_row = True
2054 else:
2055 else:
2055 usr = AttributeDict(usr.get_dict())
2056 usr = AttributeDict(usr.get_dict())
2056 usr.admin_row = True
2057 usr.admin_row = True
2057 usr.permission = _admin_perm
2058 usr.permission = _admin_perm
2058 usr.permission_id = None
2059 usr.permission_id = None
2059 super_admin_rows.append(usr)
2060 super_admin_rows.append(usr)
2060
2061
2061 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2062 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2062 q = q.options(joinedload(UserRepoToPerm.repository),
2063 q = q.options(joinedload(UserRepoToPerm.repository),
2063 joinedload(UserRepoToPerm.user),
2064 joinedload(UserRepoToPerm.user),
2064 joinedload(UserRepoToPerm.permission),)
2065 joinedload(UserRepoToPerm.permission),)
2065
2066
2066 # get owners and admins and permissions. We do a trick of re-writing
2067 # get owners and admins and permissions. We do a trick of re-writing
2067 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2068 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2068 # has a global reference and changing one object propagates to all
2069 # has a global reference and changing one object propagates to all
2069 # others. This means if admin is also an owner admin_row that change
2070 # others. This means if admin is also an owner admin_row that change
2070 # would propagate to both objects
2071 # would propagate to both objects
2071 perm_rows = []
2072 perm_rows = []
2072 for _usr in q.all():
2073 for _usr in q.all():
2073 usr = AttributeDict(_usr.user.get_dict())
2074 usr = AttributeDict(_usr.user.get_dict())
2074 # if this user is also owner/admin, mark as duplicate record
2075 # if this user is also owner/admin, mark as duplicate record
2075 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2076 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2076 usr.duplicate_perm = True
2077 usr.duplicate_perm = True
2077 # also check if this permission is maybe used by branch_permissions
2078 # also check if this permission is maybe used by branch_permissions
2078 if _usr.branch_perm_entry:
2079 if _usr.branch_perm_entry:
2079 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2080 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2080
2081
2081 usr.permission = _usr.permission.permission_name
2082 usr.permission = _usr.permission.permission_name
2082 usr.permission_id = _usr.repo_to_perm_id
2083 usr.permission_id = _usr.repo_to_perm_id
2083 perm_rows.append(usr)
2084 perm_rows.append(usr)
2084
2085
2085 # filter the perm rows by 'default' first and then sort them by
2086 # filter the perm rows by 'default' first and then sort them by
2086 # admin,write,read,none permissions sorted again alphabetically in
2087 # admin,write,read,none permissions sorted again alphabetically in
2087 # each group
2088 # each group
2088 perm_rows = sorted(perm_rows, key=display_user_sort)
2089 perm_rows = sorted(perm_rows, key=display_user_sort)
2089
2090
2090 user_groups_rows = []
2091 user_groups_rows = []
2091 if expand_from_user_groups:
2092 if expand_from_user_groups:
2092 for ug in self.permission_user_groups(with_members=True):
2093 for ug in self.permission_user_groups(with_members=True):
2093 for user_data in ug.members:
2094 for user_data in ug.members:
2094 user_groups_rows.append(user_data)
2095 user_groups_rows.append(user_data)
2095
2096
2096 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2097 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2097
2098
2098 def permission_user_groups(self, with_members=True):
2099 def permission_user_groups(self, with_members=True):
2099 q = UserGroupRepoToPerm.query()\
2100 q = UserGroupRepoToPerm.query()\
2100 .filter(UserGroupRepoToPerm.repository == self)
2101 .filter(UserGroupRepoToPerm.repository == self)
2101 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2102 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2102 joinedload(UserGroupRepoToPerm.users_group),
2103 joinedload(UserGroupRepoToPerm.users_group),
2103 joinedload(UserGroupRepoToPerm.permission),)
2104 joinedload(UserGroupRepoToPerm.permission),)
2104
2105
2105 perm_rows = []
2106 perm_rows = []
2106 for _user_group in q.all():
2107 for _user_group in q.all():
2107 entry = AttributeDict(_user_group.users_group.get_dict())
2108 entry = AttributeDict(_user_group.users_group.get_dict())
2108 entry.permission = _user_group.permission.permission_name
2109 entry.permission = _user_group.permission.permission_name
2109 if with_members:
2110 if with_members:
2110 entry.members = [x.user.get_dict()
2111 entry.members = [x.user.get_dict()
2111 for x in _user_group.users_group.members]
2112 for x in _user_group.users_group.members]
2112 perm_rows.append(entry)
2113 perm_rows.append(entry)
2113
2114
2114 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2115 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2115 return perm_rows
2116 return perm_rows
2116
2117
2117 def get_api_data(self, include_secrets=False):
2118 def get_api_data(self, include_secrets=False):
2118 """
2119 """
2119 Common function for generating repo api data
2120 Common function for generating repo api data
2120
2121
2121 :param include_secrets: See :meth:`User.get_api_data`.
2122 :param include_secrets: See :meth:`User.get_api_data`.
2122
2123
2123 """
2124 """
2124 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2125 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2125 # move this methods on models level.
2126 # move this methods on models level.
2126 from rhodecode.model.settings import SettingsModel
2127 from rhodecode.model.settings import SettingsModel
2127 from rhodecode.model.repo import RepoModel
2128 from rhodecode.model.repo import RepoModel
2128
2129
2129 repo = self
2130 repo = self
2130 _user_id, _time, _reason = self.locked
2131 _user_id, _time, _reason = self.locked
2131
2132
2132 data = {
2133 data = {
2133 'repo_id': repo.repo_id,
2134 'repo_id': repo.repo_id,
2134 'repo_name': repo.repo_name,
2135 'repo_name': repo.repo_name,
2135 'repo_type': repo.repo_type,
2136 'repo_type': repo.repo_type,
2136 'clone_uri': repo.clone_uri or '',
2137 'clone_uri': repo.clone_uri or '',
2137 'push_uri': repo.push_uri or '',
2138 'push_uri': repo.push_uri or '',
2138 'url': RepoModel().get_url(self),
2139 'url': RepoModel().get_url(self),
2139 'private': repo.private,
2140 'private': repo.private,
2140 'created_on': repo.created_on,
2141 'created_on': repo.created_on,
2141 'description': repo.description_safe,
2142 'description': repo.description_safe,
2142 'landing_rev': repo.landing_rev,
2143 'landing_rev': repo.landing_rev,
2143 'owner': repo.user.username,
2144 'owner': repo.user.username,
2144 'fork_of': repo.fork.repo_name if repo.fork else None,
2145 'fork_of': repo.fork.repo_name if repo.fork else None,
2145 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2146 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2146 'enable_statistics': repo.enable_statistics,
2147 'enable_statistics': repo.enable_statistics,
2147 'enable_locking': repo.enable_locking,
2148 'enable_locking': repo.enable_locking,
2148 'enable_downloads': repo.enable_downloads,
2149 'enable_downloads': repo.enable_downloads,
2149 'last_changeset': repo.changeset_cache,
2150 'last_changeset': repo.changeset_cache,
2150 'locked_by': User.get(_user_id).get_api_data(
2151 'locked_by': User.get(_user_id).get_api_data(
2151 include_secrets=include_secrets) if _user_id else None,
2152 include_secrets=include_secrets) if _user_id else None,
2152 'locked_date': time_to_datetime(_time) if _time else None,
2153 'locked_date': time_to_datetime(_time) if _time else None,
2153 'lock_reason': _reason if _reason else None,
2154 'lock_reason': _reason if _reason else None,
2154 }
2155 }
2155
2156
2156 # TODO: mikhail: should be per-repo settings here
2157 # TODO: mikhail: should be per-repo settings here
2157 rc_config = SettingsModel().get_all_settings()
2158 rc_config = SettingsModel().get_all_settings()
2158 repository_fields = str2bool(
2159 repository_fields = str2bool(
2159 rc_config.get('rhodecode_repository_fields'))
2160 rc_config.get('rhodecode_repository_fields'))
2160 if repository_fields:
2161 if repository_fields:
2161 for f in self.extra_fields:
2162 for f in self.extra_fields:
2162 data[f.field_key_prefixed] = f.field_value
2163 data[f.field_key_prefixed] = f.field_value
2163
2164
2164 return data
2165 return data
2165
2166
2166 @classmethod
2167 @classmethod
2167 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2168 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2168 if not lock_time:
2169 if not lock_time:
2169 lock_time = time.time()
2170 lock_time = time.time()
2170 if not lock_reason:
2171 if not lock_reason:
2171 lock_reason = cls.LOCK_AUTOMATIC
2172 lock_reason = cls.LOCK_AUTOMATIC
2172 repo.locked = [user_id, lock_time, lock_reason]
2173 repo.locked = [user_id, lock_time, lock_reason]
2173 Session().add(repo)
2174 Session().add(repo)
2174 Session().commit()
2175 Session().commit()
2175
2176
2176 @classmethod
2177 @classmethod
2177 def unlock(cls, repo):
2178 def unlock(cls, repo):
2178 repo.locked = None
2179 repo.locked = None
2179 Session().add(repo)
2180 Session().add(repo)
2180 Session().commit()
2181 Session().commit()
2181
2182
2182 @classmethod
2183 @classmethod
2183 def getlock(cls, repo):
2184 def getlock(cls, repo):
2184 return repo.locked
2185 return repo.locked
2185
2186
2186 def is_user_lock(self, user_id):
2187 def is_user_lock(self, user_id):
2187 if self.lock[0]:
2188 if self.lock[0]:
2188 lock_user_id = safe_int(self.lock[0])
2189 lock_user_id = safe_int(self.lock[0])
2189 user_id = safe_int(user_id)
2190 user_id = safe_int(user_id)
2190 # both are ints, and they are equal
2191 # both are ints, and they are equal
2191 return all([lock_user_id, user_id]) and lock_user_id == user_id
2192 return all([lock_user_id, user_id]) and lock_user_id == user_id
2192
2193
2193 return False
2194 return False
2194
2195
2195 def get_locking_state(self, action, user_id, only_when_enabled=True):
2196 def get_locking_state(self, action, user_id, only_when_enabled=True):
2196 """
2197 """
2197 Checks locking on this repository, if locking is enabled and lock is
2198 Checks locking on this repository, if locking is enabled and lock is
2198 present returns a tuple of make_lock, locked, locked_by.
2199 present returns a tuple of make_lock, locked, locked_by.
2199 make_lock can have 3 states None (do nothing) True, make lock
2200 make_lock can have 3 states None (do nothing) True, make lock
2200 False release lock, This value is later propagated to hooks, which
2201 False release lock, This value is later propagated to hooks, which
2201 do the locking. Think about this as signals passed to hooks what to do.
2202 do the locking. Think about this as signals passed to hooks what to do.
2202
2203
2203 """
2204 """
2204 # TODO: johbo: This is part of the business logic and should be moved
2205 # TODO: johbo: This is part of the business logic and should be moved
2205 # into the RepositoryModel.
2206 # into the RepositoryModel.
2206
2207
2207 if action not in ('push', 'pull'):
2208 if action not in ('push', 'pull'):
2208 raise ValueError("Invalid action value: %s" % repr(action))
2209 raise ValueError("Invalid action value: %s" % repr(action))
2209
2210
2210 # defines if locked error should be thrown to user
2211 # defines if locked error should be thrown to user
2211 currently_locked = False
2212 currently_locked = False
2212 # defines if new lock should be made, tri-state
2213 # defines if new lock should be made, tri-state
2213 make_lock = None
2214 make_lock = None
2214 repo = self
2215 repo = self
2215 user = User.get(user_id)
2216 user = User.get(user_id)
2216
2217
2217 lock_info = repo.locked
2218 lock_info = repo.locked
2218
2219
2219 if repo and (repo.enable_locking or not only_when_enabled):
2220 if repo and (repo.enable_locking or not only_when_enabled):
2220 if action == 'push':
2221 if action == 'push':
2221 # check if it's already locked !, if it is compare users
2222 # check if it's already locked !, if it is compare users
2222 locked_by_user_id = lock_info[0]
2223 locked_by_user_id = lock_info[0]
2223 if user.user_id == locked_by_user_id:
2224 if user.user_id == locked_by_user_id:
2224 log.debug(
2225 log.debug(
2225 'Got `push` action from user %s, now unlocking', user)
2226 'Got `push` action from user %s, now unlocking', user)
2226 # unlock if we have push from user who locked
2227 # unlock if we have push from user who locked
2227 make_lock = False
2228 make_lock = False
2228 else:
2229 else:
2229 # we're not the same user who locked, ban with
2230 # we're not the same user who locked, ban with
2230 # code defined in settings (default is 423 HTTP Locked) !
2231 # code defined in settings (default is 423 HTTP Locked) !
2231 log.debug('Repo %s is currently locked by %s', repo, user)
2232 log.debug('Repo %s is currently locked by %s', repo, user)
2232 currently_locked = True
2233 currently_locked = True
2233 elif action == 'pull':
2234 elif action == 'pull':
2234 # [0] user [1] date
2235 # [0] user [1] date
2235 if lock_info[0] and lock_info[1]:
2236 if lock_info[0] and lock_info[1]:
2236 log.debug('Repo %s is currently locked by %s', repo, user)
2237 log.debug('Repo %s is currently locked by %s', repo, user)
2237 currently_locked = True
2238 currently_locked = True
2238 else:
2239 else:
2239 log.debug('Setting lock on repo %s by %s', repo, user)
2240 log.debug('Setting lock on repo %s by %s', repo, user)
2240 make_lock = True
2241 make_lock = True
2241
2242
2242 else:
2243 else:
2243 log.debug('Repository %s do not have locking enabled', repo)
2244 log.debug('Repository %s do not have locking enabled', repo)
2244
2245
2245 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2246 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2246 make_lock, currently_locked, lock_info)
2247 make_lock, currently_locked, lock_info)
2247
2248
2248 from rhodecode.lib.auth import HasRepoPermissionAny
2249 from rhodecode.lib.auth import HasRepoPermissionAny
2249 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2250 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2250 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2251 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2251 # if we don't have at least write permission we cannot make a lock
2252 # if we don't have at least write permission we cannot make a lock
2252 log.debug('lock state reset back to FALSE due to lack '
2253 log.debug('lock state reset back to FALSE due to lack '
2253 'of at least read permission')
2254 'of at least read permission')
2254 make_lock = False
2255 make_lock = False
2255
2256
2256 return make_lock, currently_locked, lock_info
2257 return make_lock, currently_locked, lock_info
2257
2258
2258 @property
2259 @property
2259 def last_commit_cache_update_diff(self):
2260 def last_commit_cache_update_diff(self):
2260 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2261 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2261
2262
2262 @classmethod
2263 @classmethod
2263 def _load_commit_change(cls, last_commit_cache):
2264 def _load_commit_change(cls, last_commit_cache):
2264 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2265 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2265 empty_date = datetime.datetime.fromtimestamp(0)
2266 empty_date = datetime.datetime.fromtimestamp(0)
2266 date_latest = last_commit_cache.get('date', empty_date)
2267 date_latest = last_commit_cache.get('date', empty_date)
2267 try:
2268 try:
2268 return parse_datetime(date_latest)
2269 return parse_datetime(date_latest)
2269 except Exception:
2270 except Exception:
2270 return empty_date
2271 return empty_date
2271
2272
2272 @property
2273 @property
2273 def last_commit_change(self):
2274 def last_commit_change(self):
2274 return self._load_commit_change(self.changeset_cache)
2275 return self._load_commit_change(self.changeset_cache)
2275
2276
2276 @property
2277 @property
2277 def last_db_change(self):
2278 def last_db_change(self):
2278 return self.updated_on
2279 return self.updated_on
2279
2280
2280 @property
2281 @property
2281 def clone_uri_hidden(self):
2282 def clone_uri_hidden(self):
2282 clone_uri = self.clone_uri
2283 clone_uri = self.clone_uri
2283 if clone_uri:
2284 if clone_uri:
2284 import urlobject
2285 import urlobject
2285 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2286 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2286 if url_obj.password:
2287 if url_obj.password:
2287 clone_uri = url_obj.with_password('*****')
2288 clone_uri = url_obj.with_password('*****')
2288 return clone_uri
2289 return clone_uri
2289
2290
2290 @property
2291 @property
2291 def push_uri_hidden(self):
2292 def push_uri_hidden(self):
2292 push_uri = self.push_uri
2293 push_uri = self.push_uri
2293 if push_uri:
2294 if push_uri:
2294 import urlobject
2295 import urlobject
2295 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2296 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2296 if url_obj.password:
2297 if url_obj.password:
2297 push_uri = url_obj.with_password('*****')
2298 push_uri = url_obj.with_password('*****')
2298 return push_uri
2299 return push_uri
2299
2300
2300 def clone_url(self, **override):
2301 def clone_url(self, **override):
2301 from rhodecode.model.settings import SettingsModel
2302 from rhodecode.model.settings import SettingsModel
2302
2303
2303 uri_tmpl = None
2304 uri_tmpl = None
2304 if 'with_id' in override:
2305 if 'with_id' in override:
2305 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2306 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2306 del override['with_id']
2307 del override['with_id']
2307
2308
2308 if 'uri_tmpl' in override:
2309 if 'uri_tmpl' in override:
2309 uri_tmpl = override['uri_tmpl']
2310 uri_tmpl = override['uri_tmpl']
2310 del override['uri_tmpl']
2311 del override['uri_tmpl']
2311
2312
2312 ssh = False
2313 ssh = False
2313 if 'ssh' in override:
2314 if 'ssh' in override:
2314 ssh = True
2315 ssh = True
2315 del override['ssh']
2316 del override['ssh']
2316
2317
2317 # we didn't override our tmpl from **overrides
2318 # we didn't override our tmpl from **overrides
2318 request = get_current_request()
2319 request = get_current_request()
2319 if not uri_tmpl:
2320 if not uri_tmpl:
2320 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2321 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2321 rc_config = request.call_context.rc_config
2322 rc_config = request.call_context.rc_config
2322 else:
2323 else:
2323 rc_config = SettingsModel().get_all_settings(cache=True)
2324 rc_config = SettingsModel().get_all_settings(cache=True)
2324
2325
2325 if ssh:
2326 if ssh:
2326 uri_tmpl = rc_config.get(
2327 uri_tmpl = rc_config.get(
2327 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2328 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2328
2329
2329 else:
2330 else:
2330 uri_tmpl = rc_config.get(
2331 uri_tmpl = rc_config.get(
2331 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2332 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2332
2333
2333 return get_clone_url(request=request,
2334 return get_clone_url(request=request,
2334 uri_tmpl=uri_tmpl,
2335 uri_tmpl=uri_tmpl,
2335 repo_name=self.repo_name,
2336 repo_name=self.repo_name,
2336 repo_id=self.repo_id,
2337 repo_id=self.repo_id,
2337 repo_type=self.repo_type,
2338 repo_type=self.repo_type,
2338 **override)
2339 **override)
2339
2340
2340 def set_state(self, state):
2341 def set_state(self, state):
2341 self.repo_state = state
2342 self.repo_state = state
2342 Session().add(self)
2343 Session().add(self)
2343 #==========================================================================
2344 #==========================================================================
2344 # SCM PROPERTIES
2345 # SCM PROPERTIES
2345 #==========================================================================
2346 #==========================================================================
2346
2347
2347 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2348 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2348 return get_commit_safe(
2349 return get_commit_safe(
2349 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2350 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2350 maybe_unreachable=maybe_unreachable)
2351 maybe_unreachable=maybe_unreachable)
2351
2352
2352 def get_changeset(self, rev=None, pre_load=None):
2353 def get_changeset(self, rev=None, pre_load=None):
2353 warnings.warn("Use get_commit", DeprecationWarning)
2354 warnings.warn("Use get_commit", DeprecationWarning)
2354 commit_id = None
2355 commit_id = None
2355 commit_idx = None
2356 commit_idx = None
2356 if isinstance(rev, compat.string_types):
2357 if isinstance(rev, compat.string_types):
2357 commit_id = rev
2358 commit_id = rev
2358 else:
2359 else:
2359 commit_idx = rev
2360 commit_idx = rev
2360 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2361 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2361 pre_load=pre_load)
2362 pre_load=pre_load)
2362
2363
2363 def get_landing_commit(self):
2364 def get_landing_commit(self):
2364 """
2365 """
2365 Returns landing commit, or if that doesn't exist returns the tip
2366 Returns landing commit, or if that doesn't exist returns the tip
2366 """
2367 """
2367 _rev_type, _rev = self.landing_rev
2368 _rev_type, _rev = self.landing_rev
2368 commit = self.get_commit(_rev)
2369 commit = self.get_commit(_rev)
2369 if isinstance(commit, EmptyCommit):
2370 if isinstance(commit, EmptyCommit):
2370 return self.get_commit()
2371 return self.get_commit()
2371 return commit
2372 return commit
2372
2373
2373 def flush_commit_cache(self):
2374 def flush_commit_cache(self):
2374 self.update_commit_cache(cs_cache={'raw_id':'0'})
2375 self.update_commit_cache(cs_cache={'raw_id':'0'})
2375 self.update_commit_cache()
2376 self.update_commit_cache()
2376
2377
2377 def update_commit_cache(self, cs_cache=None, config=None):
2378 def update_commit_cache(self, cs_cache=None, config=None):
2378 """
2379 """
2379 Update cache of last commit for repository
2380 Update cache of last commit for repository
2380 cache_keys should be::
2381 cache_keys should be::
2381
2382
2382 source_repo_id
2383 source_repo_id
2383 short_id
2384 short_id
2384 raw_id
2385 raw_id
2385 revision
2386 revision
2386 parents
2387 parents
2387 message
2388 message
2388 date
2389 date
2389 author
2390 author
2390 updated_on
2391 updated_on
2391
2392
2392 """
2393 """
2393 from rhodecode.lib.vcs.backends.base import BaseChangeset
2394 from rhodecode.lib.vcs.backends.base import BaseChangeset
2394 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2395 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2395 empty_date = datetime.datetime.fromtimestamp(0)
2396 empty_date = datetime.datetime.fromtimestamp(0)
2396
2397
2397 if cs_cache is None:
2398 if cs_cache is None:
2398 # use no-cache version here
2399 # use no-cache version here
2399 try:
2400 try:
2400 scm_repo = self.scm_instance(cache=False, config=config)
2401 scm_repo = self.scm_instance(cache=False, config=config)
2401 except VCSError:
2402 except VCSError:
2402 scm_repo = None
2403 scm_repo = None
2403 empty = scm_repo is None or scm_repo.is_empty()
2404 empty = scm_repo is None or scm_repo.is_empty()
2404
2405
2405 if not empty:
2406 if not empty:
2406 cs_cache = scm_repo.get_commit(
2407 cs_cache = scm_repo.get_commit(
2407 pre_load=["author", "date", "message", "parents", "branch"])
2408 pre_load=["author", "date", "message", "parents", "branch"])
2408 else:
2409 else:
2409 cs_cache = EmptyCommit()
2410 cs_cache = EmptyCommit()
2410
2411
2411 if isinstance(cs_cache, BaseChangeset):
2412 if isinstance(cs_cache, BaseChangeset):
2412 cs_cache = cs_cache.__json__()
2413 cs_cache = cs_cache.__json__()
2413
2414
2414 def is_outdated(new_cs_cache):
2415 def is_outdated(new_cs_cache):
2415 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2416 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2416 new_cs_cache['revision'] != self.changeset_cache['revision']):
2417 new_cs_cache['revision'] != self.changeset_cache['revision']):
2417 return True
2418 return True
2418 return False
2419 return False
2419
2420
2420 # check if we have maybe already latest cached revision
2421 # check if we have maybe already latest cached revision
2421 if is_outdated(cs_cache) or not self.changeset_cache:
2422 if is_outdated(cs_cache) or not self.changeset_cache:
2422 _current_datetime = datetime.datetime.utcnow()
2423 _current_datetime = datetime.datetime.utcnow()
2423 last_change = cs_cache.get('date') or _current_datetime
2424 last_change = cs_cache.get('date') or _current_datetime
2424 # we check if last update is newer than the new value
2425 # we check if last update is newer than the new value
2425 # if yes, we use the current timestamp instead. Imagine you get
2426 # if yes, we use the current timestamp instead. Imagine you get
2426 # old commit pushed 1y ago, we'd set last update 1y to ago.
2427 # old commit pushed 1y ago, we'd set last update 1y to ago.
2427 last_change_timestamp = datetime_to_time(last_change)
2428 last_change_timestamp = datetime_to_time(last_change)
2428 current_timestamp = datetime_to_time(last_change)
2429 current_timestamp = datetime_to_time(last_change)
2429 if last_change_timestamp > current_timestamp and not empty:
2430 if last_change_timestamp > current_timestamp and not empty:
2430 cs_cache['date'] = _current_datetime
2431 cs_cache['date'] = _current_datetime
2431
2432
2432 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2433 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2433 cs_cache['updated_on'] = time.time()
2434 cs_cache['updated_on'] = time.time()
2434 self.changeset_cache = cs_cache
2435 self.changeset_cache = cs_cache
2435 self.updated_on = last_change
2436 self.updated_on = last_change
2436 Session().add(self)
2437 Session().add(self)
2437 Session().commit()
2438 Session().commit()
2438
2439
2439 else:
2440 else:
2440 if empty:
2441 if empty:
2441 cs_cache = EmptyCommit().__json__()
2442 cs_cache = EmptyCommit().__json__()
2442 else:
2443 else:
2443 cs_cache = self.changeset_cache
2444 cs_cache = self.changeset_cache
2444
2445
2445 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2446 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2446
2447
2447 cs_cache['updated_on'] = time.time()
2448 cs_cache['updated_on'] = time.time()
2448 self.changeset_cache = cs_cache
2449 self.changeset_cache = cs_cache
2449 self.updated_on = _date_latest
2450 self.updated_on = _date_latest
2450 Session().add(self)
2451 Session().add(self)
2451 Session().commit()
2452 Session().commit()
2452
2453
2453 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2454 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2454 self.repo_name, cs_cache, _date_latest)
2455 self.repo_name, cs_cache, _date_latest)
2455
2456
2456 @property
2457 @property
2457 def tip(self):
2458 def tip(self):
2458 return self.get_commit('tip')
2459 return self.get_commit('tip')
2459
2460
2460 @property
2461 @property
2461 def author(self):
2462 def author(self):
2462 return self.tip.author
2463 return self.tip.author
2463
2464
2464 @property
2465 @property
2465 def last_change(self):
2466 def last_change(self):
2466 return self.scm_instance().last_change
2467 return self.scm_instance().last_change
2467
2468
2468 def get_comments(self, revisions=None):
2469 def get_comments(self, revisions=None):
2469 """
2470 """
2470 Returns comments for this repository grouped by revisions
2471 Returns comments for this repository grouped by revisions
2471
2472
2472 :param revisions: filter query by revisions only
2473 :param revisions: filter query by revisions only
2473 """
2474 """
2474 cmts = ChangesetComment.query()\
2475 cmts = ChangesetComment.query()\
2475 .filter(ChangesetComment.repo == self)
2476 .filter(ChangesetComment.repo == self)
2476 if revisions:
2477 if revisions:
2477 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2478 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2478 grouped = collections.defaultdict(list)
2479 grouped = collections.defaultdict(list)
2479 for cmt in cmts.all():
2480 for cmt in cmts.all():
2480 grouped[cmt.revision].append(cmt)
2481 grouped[cmt.revision].append(cmt)
2481 return grouped
2482 return grouped
2482
2483
2483 def statuses(self, revisions=None):
2484 def statuses(self, revisions=None):
2484 """
2485 """
2485 Returns statuses for this repository
2486 Returns statuses for this repository
2486
2487
2487 :param revisions: list of revisions to get statuses for
2488 :param revisions: list of revisions to get statuses for
2488 """
2489 """
2489 statuses = ChangesetStatus.query()\
2490 statuses = ChangesetStatus.query()\
2490 .filter(ChangesetStatus.repo == self)\
2491 .filter(ChangesetStatus.repo == self)\
2491 .filter(ChangesetStatus.version == 0)
2492 .filter(ChangesetStatus.version == 0)
2492
2493
2493 if revisions:
2494 if revisions:
2494 # Try doing the filtering in chunks to avoid hitting limits
2495 # Try doing the filtering in chunks to avoid hitting limits
2495 size = 500
2496 size = 500
2496 status_results = []
2497 status_results = []
2497 for chunk in xrange(0, len(revisions), size):
2498 for chunk in xrange(0, len(revisions), size):
2498 status_results += statuses.filter(
2499 status_results += statuses.filter(
2499 ChangesetStatus.revision.in_(
2500 ChangesetStatus.revision.in_(
2500 revisions[chunk: chunk+size])
2501 revisions[chunk: chunk+size])
2501 ).all()
2502 ).all()
2502 else:
2503 else:
2503 status_results = statuses.all()
2504 status_results = statuses.all()
2504
2505
2505 grouped = {}
2506 grouped = {}
2506
2507
2507 # maybe we have open new pullrequest without a status?
2508 # maybe we have open new pullrequest without a status?
2508 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2509 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2509 status_lbl = ChangesetStatus.get_status_lbl(stat)
2510 status_lbl = ChangesetStatus.get_status_lbl(stat)
2510 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2511 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2511 for rev in pr.revisions:
2512 for rev in pr.revisions:
2512 pr_id = pr.pull_request_id
2513 pr_id = pr.pull_request_id
2513 pr_repo = pr.target_repo.repo_name
2514 pr_repo = pr.target_repo.repo_name
2514 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2515 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2515
2516
2516 for stat in status_results:
2517 for stat in status_results:
2517 pr_id = pr_repo = None
2518 pr_id = pr_repo = None
2518 if stat.pull_request:
2519 if stat.pull_request:
2519 pr_id = stat.pull_request.pull_request_id
2520 pr_id = stat.pull_request.pull_request_id
2520 pr_repo = stat.pull_request.target_repo.repo_name
2521 pr_repo = stat.pull_request.target_repo.repo_name
2521 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2522 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2522 pr_id, pr_repo]
2523 pr_id, pr_repo]
2523 return grouped
2524 return grouped
2524
2525
2525 # ==========================================================================
2526 # ==========================================================================
2526 # SCM CACHE INSTANCE
2527 # SCM CACHE INSTANCE
2527 # ==========================================================================
2528 # ==========================================================================
2528
2529
2529 def scm_instance(self, **kwargs):
2530 def scm_instance(self, **kwargs):
2530 import rhodecode
2531 import rhodecode
2531
2532
2532 # Passing a config will not hit the cache currently only used
2533 # Passing a config will not hit the cache currently only used
2533 # for repo2dbmapper
2534 # for repo2dbmapper
2534 config = kwargs.pop('config', None)
2535 config = kwargs.pop('config', None)
2535 cache = kwargs.pop('cache', None)
2536 cache = kwargs.pop('cache', None)
2536 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2537 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2537 if vcs_full_cache is not None:
2538 if vcs_full_cache is not None:
2538 # allows override global config
2539 # allows override global config
2539 full_cache = vcs_full_cache
2540 full_cache = vcs_full_cache
2540 else:
2541 else:
2541 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2542 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2542 # if cache is NOT defined use default global, else we have a full
2543 # if cache is NOT defined use default global, else we have a full
2543 # control over cache behaviour
2544 # control over cache behaviour
2544 if cache is None and full_cache and not config:
2545 if cache is None and full_cache and not config:
2545 log.debug('Initializing pure cached instance for %s', self.repo_path)
2546 log.debug('Initializing pure cached instance for %s', self.repo_path)
2546 return self._get_instance_cached()
2547 return self._get_instance_cached()
2547
2548
2548 # cache here is sent to the "vcs server"
2549 # cache here is sent to the "vcs server"
2549 return self._get_instance(cache=bool(cache), config=config)
2550 return self._get_instance(cache=bool(cache), config=config)
2550
2551
2551 def _get_instance_cached(self):
2552 def _get_instance_cached(self):
2552 from rhodecode.lib import rc_cache
2553 from rhodecode.lib import rc_cache
2553
2554
2554 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2555 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2555 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2556 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2556 repo_id=self.repo_id)
2557 repo_id=self.repo_id)
2557 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2558 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2558
2559
2559 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2560 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2560 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2561 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2561 return self._get_instance(repo_state_uid=_cache_state_uid)
2562 return self._get_instance(repo_state_uid=_cache_state_uid)
2562
2563
2563 # we must use thread scoped cache here,
2564 # we must use thread scoped cache here,
2564 # because each thread of gevent needs it's own not shared connection and cache
2565 # because each thread of gevent needs it's own not shared connection and cache
2565 # we also alter `args` so the cache key is individual for every green thread.
2566 # we also alter `args` so the cache key is individual for every green thread.
2566 inv_context_manager = rc_cache.InvalidationContext(
2567 inv_context_manager = rc_cache.InvalidationContext(
2567 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2568 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2568 thread_scoped=True)
2569 thread_scoped=True)
2569 with inv_context_manager as invalidation_context:
2570 with inv_context_manager as invalidation_context:
2570 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2571 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2571 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2572 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2572
2573
2573 # re-compute and store cache if we get invalidate signal
2574 # re-compute and store cache if we get invalidate signal
2574 if invalidation_context.should_invalidate():
2575 if invalidation_context.should_invalidate():
2575 instance = get_instance_cached.refresh(*args)
2576 instance = get_instance_cached.refresh(*args)
2576 else:
2577 else:
2577 instance = get_instance_cached(*args)
2578 instance = get_instance_cached(*args)
2578
2579
2579 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2580 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2580 return instance
2581 return instance
2581
2582
2582 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2583 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2583 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2584 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2584 self.repo_type, self.repo_path, cache)
2585 self.repo_type, self.repo_path, cache)
2585 config = config or self._config
2586 config = config or self._config
2586 custom_wire = {
2587 custom_wire = {
2587 'cache': cache, # controls the vcs.remote cache
2588 'cache': cache, # controls the vcs.remote cache
2588 'repo_state_uid': repo_state_uid
2589 'repo_state_uid': repo_state_uid
2589 }
2590 }
2590 repo = get_vcs_instance(
2591 repo = get_vcs_instance(
2591 repo_path=safe_str(self.repo_full_path),
2592 repo_path=safe_str(self.repo_full_path),
2592 config=config,
2593 config=config,
2593 with_wire=custom_wire,
2594 with_wire=custom_wire,
2594 create=False,
2595 create=False,
2595 _vcs_alias=self.repo_type)
2596 _vcs_alias=self.repo_type)
2596 if repo is not None:
2597 if repo is not None:
2597 repo.count() # cache rebuild
2598 repo.count() # cache rebuild
2598 return repo
2599 return repo
2599
2600
2600 def get_shadow_repository_path(self, workspace_id):
2601 def get_shadow_repository_path(self, workspace_id):
2601 from rhodecode.lib.vcs.backends.base import BaseRepository
2602 from rhodecode.lib.vcs.backends.base import BaseRepository
2602 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2603 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2603 self.repo_full_path, self.repo_id, workspace_id)
2604 self.repo_full_path, self.repo_id, workspace_id)
2604 return shadow_repo_path
2605 return shadow_repo_path
2605
2606
2606 def __json__(self):
2607 def __json__(self):
2607 return {'landing_rev': self.landing_rev}
2608 return {'landing_rev': self.landing_rev}
2608
2609
2609 def get_dict(self):
2610 def get_dict(self):
2610
2611
2611 # Since we transformed `repo_name` to a hybrid property, we need to
2612 # Since we transformed `repo_name` to a hybrid property, we need to
2612 # keep compatibility with the code which uses `repo_name` field.
2613 # keep compatibility with the code which uses `repo_name` field.
2613
2614
2614 result = super(Repository, self).get_dict()
2615 result = super(Repository, self).get_dict()
2615 result['repo_name'] = result.pop('_repo_name', None)
2616 result['repo_name'] = result.pop('_repo_name', None)
2616 return result
2617 return result
2617
2618
2618
2619
2619 class RepoGroup(Base, BaseModel):
2620 class RepoGroup(Base, BaseModel):
2620 __tablename__ = 'groups'
2621 __tablename__ = 'groups'
2621 __table_args__ = (
2622 __table_args__ = (
2622 UniqueConstraint('group_name', 'group_parent_id'),
2623 UniqueConstraint('group_name', 'group_parent_id'),
2623 base_table_args,
2624 base_table_args,
2624 )
2625 )
2625 __mapper_args__ = {'order_by': 'group_name'}
2626 __mapper_args__ = {'order_by': 'group_name'}
2626
2627
2627 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2628 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2628
2629
2629 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2630 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2630 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2631 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2631 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2632 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2632 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2633 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2633 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2634 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2634 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2635 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2635 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2636 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2636 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2637 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2637 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2638 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2638 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2639 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2639 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2640 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2640
2641
2641 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2642 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2642 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2643 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2643 parent_group = relationship('RepoGroup', remote_side=group_id)
2644 parent_group = relationship('RepoGroup', remote_side=group_id)
2644 user = relationship('User')
2645 user = relationship('User')
2645 integrations = relationship('Integration', cascade="all, delete-orphan")
2646 integrations = relationship('Integration', cascade="all, delete-orphan")
2646
2647
2647 # no cascade, set NULL
2648 # no cascade, set NULL
2648 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2649 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2649
2650
2650 def __init__(self, group_name='', parent_group=None):
2651 def __init__(self, group_name='', parent_group=None):
2651 self.group_name = group_name
2652 self.group_name = group_name
2652 self.parent_group = parent_group
2653 self.parent_group = parent_group
2653
2654
2654 def __unicode__(self):
2655 def __unicode__(self):
2655 return u"<%s('id:%s:%s')>" % (
2656 return u"<%s('id:%s:%s')>" % (
2656 self.__class__.__name__, self.group_id, self.group_name)
2657 self.__class__.__name__, self.group_id, self.group_name)
2657
2658
2658 @hybrid_property
2659 @hybrid_property
2659 def group_name(self):
2660 def group_name(self):
2660 return self._group_name
2661 return self._group_name
2661
2662
2662 @group_name.setter
2663 @group_name.setter
2663 def group_name(self, value):
2664 def group_name(self, value):
2664 self._group_name = value
2665 self._group_name = value
2665 self.group_name_hash = self.hash_repo_group_name(value)
2666 self.group_name_hash = self.hash_repo_group_name(value)
2666
2667
2667 @classmethod
2668 @classmethod
2668 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2669 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2669 from rhodecode.lib.vcs.backends.base import EmptyCommit
2670 from rhodecode.lib.vcs.backends.base import EmptyCommit
2670 dummy = EmptyCommit().__json__()
2671 dummy = EmptyCommit().__json__()
2671 if not changeset_cache_raw:
2672 if not changeset_cache_raw:
2672 dummy['source_repo_id'] = repo_id
2673 dummy['source_repo_id'] = repo_id
2673 return json.loads(json.dumps(dummy))
2674 return json.loads(json.dumps(dummy))
2674
2675
2675 try:
2676 try:
2676 return json.loads(changeset_cache_raw)
2677 return json.loads(changeset_cache_raw)
2677 except TypeError:
2678 except TypeError:
2678 return dummy
2679 return dummy
2679 except Exception:
2680 except Exception:
2680 log.error(traceback.format_exc())
2681 log.error(traceback.format_exc())
2681 return dummy
2682 return dummy
2682
2683
2683 @hybrid_property
2684 @hybrid_property
2684 def changeset_cache(self):
2685 def changeset_cache(self):
2685 return self._load_changeset_cache('', self._changeset_cache)
2686 return self._load_changeset_cache('', self._changeset_cache)
2686
2687
2687 @changeset_cache.setter
2688 @changeset_cache.setter
2688 def changeset_cache(self, val):
2689 def changeset_cache(self, val):
2689 try:
2690 try:
2690 self._changeset_cache = json.dumps(val)
2691 self._changeset_cache = json.dumps(val)
2691 except Exception:
2692 except Exception:
2692 log.error(traceback.format_exc())
2693 log.error(traceback.format_exc())
2693
2694
2694 @validates('group_parent_id')
2695 @validates('group_parent_id')
2695 def validate_group_parent_id(self, key, val):
2696 def validate_group_parent_id(self, key, val):
2696 """
2697 """
2697 Check cycle references for a parent group to self
2698 Check cycle references for a parent group to self
2698 """
2699 """
2699 if self.group_id and val:
2700 if self.group_id and val:
2700 assert val != self.group_id
2701 assert val != self.group_id
2701
2702
2702 return val
2703 return val
2703
2704
2704 @hybrid_property
2705 @hybrid_property
2705 def description_safe(self):
2706 def description_safe(self):
2706 from rhodecode.lib import helpers as h
2707 from rhodecode.lib import helpers as h
2707 return h.escape(self.group_description)
2708 return h.escape(self.group_description)
2708
2709
2709 @classmethod
2710 @classmethod
2710 def hash_repo_group_name(cls, repo_group_name):
2711 def hash_repo_group_name(cls, repo_group_name):
2711 val = remove_formatting(repo_group_name)
2712 val = remove_formatting(repo_group_name)
2712 val = safe_str(val).lower()
2713 val = safe_str(val).lower()
2713 chars = []
2714 chars = []
2714 for c in val:
2715 for c in val:
2715 if c not in string.ascii_letters:
2716 if c not in string.ascii_letters:
2716 c = str(ord(c))
2717 c = str(ord(c))
2717 chars.append(c)
2718 chars.append(c)
2718
2719
2719 return ''.join(chars)
2720 return ''.join(chars)
2720
2721
2721 @classmethod
2722 @classmethod
2722 def _generate_choice(cls, repo_group):
2723 def _generate_choice(cls, repo_group):
2723 from webhelpers2.html import literal as _literal
2724 from webhelpers2.html import literal as _literal
2724 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2725 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2725 return repo_group.group_id, _name(repo_group.full_path_splitted)
2726 return repo_group.group_id, _name(repo_group.full_path_splitted)
2726
2727
2727 @classmethod
2728 @classmethod
2728 def groups_choices(cls, groups=None, show_empty_group=True):
2729 def groups_choices(cls, groups=None, show_empty_group=True):
2729 if not groups:
2730 if not groups:
2730 groups = cls.query().all()
2731 groups = cls.query().all()
2731
2732
2732 repo_groups = []
2733 repo_groups = []
2733 if show_empty_group:
2734 if show_empty_group:
2734 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2735 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2735
2736
2736 repo_groups.extend([cls._generate_choice(x) for x in groups])
2737 repo_groups.extend([cls._generate_choice(x) for x in groups])
2737
2738
2738 repo_groups = sorted(
2739 repo_groups = sorted(
2739 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2740 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2740 return repo_groups
2741 return repo_groups
2741
2742
2742 @classmethod
2743 @classmethod
2743 def url_sep(cls):
2744 def url_sep(cls):
2744 return URL_SEP
2745 return URL_SEP
2745
2746
2746 @classmethod
2747 @classmethod
2747 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2748 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2748 if case_insensitive:
2749 if case_insensitive:
2749 gr = cls.query().filter(func.lower(cls.group_name)
2750 gr = cls.query().filter(func.lower(cls.group_name)
2750 == func.lower(group_name))
2751 == func.lower(group_name))
2751 else:
2752 else:
2752 gr = cls.query().filter(cls.group_name == group_name)
2753 gr = cls.query().filter(cls.group_name == group_name)
2753 if cache:
2754 if cache:
2754 name_key = _hash_key(group_name)
2755 name_key = _hash_key(group_name)
2755 gr = gr.options(
2756 gr = gr.options(
2756 FromCache("sql_cache_short", "get_group_%s" % name_key))
2757 FromCache("sql_cache_short", "get_group_%s" % name_key))
2757 return gr.scalar()
2758 return gr.scalar()
2758
2759
2759 @classmethod
2760 @classmethod
2760 def get_user_personal_repo_group(cls, user_id):
2761 def get_user_personal_repo_group(cls, user_id):
2761 user = User.get(user_id)
2762 user = User.get(user_id)
2762 if user.username == User.DEFAULT_USER:
2763 if user.username == User.DEFAULT_USER:
2763 return None
2764 return None
2764
2765
2765 return cls.query()\
2766 return cls.query()\
2766 .filter(cls.personal == true()) \
2767 .filter(cls.personal == true()) \
2767 .filter(cls.user == user) \
2768 .filter(cls.user == user) \
2768 .order_by(cls.group_id.asc()) \
2769 .order_by(cls.group_id.asc()) \
2769 .first()
2770 .first()
2770
2771
2771 @classmethod
2772 @classmethod
2772 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2773 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2773 case_insensitive=True):
2774 case_insensitive=True):
2774 q = RepoGroup.query()
2775 q = RepoGroup.query()
2775
2776
2776 if not isinstance(user_id, Optional):
2777 if not isinstance(user_id, Optional):
2777 q = q.filter(RepoGroup.user_id == user_id)
2778 q = q.filter(RepoGroup.user_id == user_id)
2778
2779
2779 if not isinstance(group_id, Optional):
2780 if not isinstance(group_id, Optional):
2780 q = q.filter(RepoGroup.group_parent_id == group_id)
2781 q = q.filter(RepoGroup.group_parent_id == group_id)
2781
2782
2782 if case_insensitive:
2783 if case_insensitive:
2783 q = q.order_by(func.lower(RepoGroup.group_name))
2784 q = q.order_by(func.lower(RepoGroup.group_name))
2784 else:
2785 else:
2785 q = q.order_by(RepoGroup.group_name)
2786 q = q.order_by(RepoGroup.group_name)
2786 return q.all()
2787 return q.all()
2787
2788
2788 @property
2789 @property
2789 def parents(self, parents_recursion_limit=10):
2790 def parents(self, parents_recursion_limit=10):
2790 groups = []
2791 groups = []
2791 if self.parent_group is None:
2792 if self.parent_group is None:
2792 return groups
2793 return groups
2793 cur_gr = self.parent_group
2794 cur_gr = self.parent_group
2794 groups.insert(0, cur_gr)
2795 groups.insert(0, cur_gr)
2795 cnt = 0
2796 cnt = 0
2796 while 1:
2797 while 1:
2797 cnt += 1
2798 cnt += 1
2798 gr = getattr(cur_gr, 'parent_group', None)
2799 gr = getattr(cur_gr, 'parent_group', None)
2799 cur_gr = cur_gr.parent_group
2800 cur_gr = cur_gr.parent_group
2800 if gr is None:
2801 if gr is None:
2801 break
2802 break
2802 if cnt == parents_recursion_limit:
2803 if cnt == parents_recursion_limit:
2803 # this will prevent accidental infinit loops
2804 # this will prevent accidental infinit loops
2804 log.error('more than %s parents found for group %s, stopping '
2805 log.error('more than %s parents found for group %s, stopping '
2805 'recursive parent fetching', parents_recursion_limit, self)
2806 'recursive parent fetching', parents_recursion_limit, self)
2806 break
2807 break
2807
2808
2808 groups.insert(0, gr)
2809 groups.insert(0, gr)
2809 return groups
2810 return groups
2810
2811
2811 @property
2812 @property
2812 def last_commit_cache_update_diff(self):
2813 def last_commit_cache_update_diff(self):
2813 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2814 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2814
2815
2815 @classmethod
2816 @classmethod
2816 def _load_commit_change(cls, last_commit_cache):
2817 def _load_commit_change(cls, last_commit_cache):
2817 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2818 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2818 empty_date = datetime.datetime.fromtimestamp(0)
2819 empty_date = datetime.datetime.fromtimestamp(0)
2819 date_latest = last_commit_cache.get('date', empty_date)
2820 date_latest = last_commit_cache.get('date', empty_date)
2820 try:
2821 try:
2821 return parse_datetime(date_latest)
2822 return parse_datetime(date_latest)
2822 except Exception:
2823 except Exception:
2823 return empty_date
2824 return empty_date
2824
2825
2825 @property
2826 @property
2826 def last_commit_change(self):
2827 def last_commit_change(self):
2827 return self._load_commit_change(self.changeset_cache)
2828 return self._load_commit_change(self.changeset_cache)
2828
2829
2829 @property
2830 @property
2830 def last_db_change(self):
2831 def last_db_change(self):
2831 return self.updated_on
2832 return self.updated_on
2832
2833
2833 @property
2834 @property
2834 def children(self):
2835 def children(self):
2835 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2836 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2836
2837
2837 @property
2838 @property
2838 def name(self):
2839 def name(self):
2839 return self.group_name.split(RepoGroup.url_sep())[-1]
2840 return self.group_name.split(RepoGroup.url_sep())[-1]
2840
2841
2841 @property
2842 @property
2842 def full_path(self):
2843 def full_path(self):
2843 return self.group_name
2844 return self.group_name
2844
2845
2845 @property
2846 @property
2846 def full_path_splitted(self):
2847 def full_path_splitted(self):
2847 return self.group_name.split(RepoGroup.url_sep())
2848 return self.group_name.split(RepoGroup.url_sep())
2848
2849
2849 @property
2850 @property
2850 def repositories(self):
2851 def repositories(self):
2851 return Repository.query()\
2852 return Repository.query()\
2852 .filter(Repository.group == self)\
2853 .filter(Repository.group == self)\
2853 .order_by(Repository.repo_name)
2854 .order_by(Repository.repo_name)
2854
2855
2855 @property
2856 @property
2856 def repositories_recursive_count(self):
2857 def repositories_recursive_count(self):
2857 cnt = self.repositories.count()
2858 cnt = self.repositories.count()
2858
2859
2859 def children_count(group):
2860 def children_count(group):
2860 cnt = 0
2861 cnt = 0
2861 for child in group.children:
2862 for child in group.children:
2862 cnt += child.repositories.count()
2863 cnt += child.repositories.count()
2863 cnt += children_count(child)
2864 cnt += children_count(child)
2864 return cnt
2865 return cnt
2865
2866
2866 return cnt + children_count(self)
2867 return cnt + children_count(self)
2867
2868
2868 def _recursive_objects(self, include_repos=True, include_groups=True):
2869 def _recursive_objects(self, include_repos=True, include_groups=True):
2869 all_ = []
2870 all_ = []
2870
2871
2871 def _get_members(root_gr):
2872 def _get_members(root_gr):
2872 if include_repos:
2873 if include_repos:
2873 for r in root_gr.repositories:
2874 for r in root_gr.repositories:
2874 all_.append(r)
2875 all_.append(r)
2875 childs = root_gr.children.all()
2876 childs = root_gr.children.all()
2876 if childs:
2877 if childs:
2877 for gr in childs:
2878 for gr in childs:
2878 if include_groups:
2879 if include_groups:
2879 all_.append(gr)
2880 all_.append(gr)
2880 _get_members(gr)
2881 _get_members(gr)
2881
2882
2882 root_group = []
2883 root_group = []
2883 if include_groups:
2884 if include_groups:
2884 root_group = [self]
2885 root_group = [self]
2885
2886
2886 _get_members(self)
2887 _get_members(self)
2887 return root_group + all_
2888 return root_group + all_
2888
2889
2889 def recursive_groups_and_repos(self):
2890 def recursive_groups_and_repos(self):
2890 """
2891 """
2891 Recursive return all groups, with repositories in those groups
2892 Recursive return all groups, with repositories in those groups
2892 """
2893 """
2893 return self._recursive_objects()
2894 return self._recursive_objects()
2894
2895
2895 def recursive_groups(self):
2896 def recursive_groups(self):
2896 """
2897 """
2897 Returns all children groups for this group including children of children
2898 Returns all children groups for this group including children of children
2898 """
2899 """
2899 return self._recursive_objects(include_repos=False)
2900 return self._recursive_objects(include_repos=False)
2900
2901
2901 def recursive_repos(self):
2902 def recursive_repos(self):
2902 """
2903 """
2903 Returns all children repositories for this group
2904 Returns all children repositories for this group
2904 """
2905 """
2905 return self._recursive_objects(include_groups=False)
2906 return self._recursive_objects(include_groups=False)
2906
2907
2907 def get_new_name(self, group_name):
2908 def get_new_name(self, group_name):
2908 """
2909 """
2909 returns new full group name based on parent and new name
2910 returns new full group name based on parent and new name
2910
2911
2911 :param group_name:
2912 :param group_name:
2912 """
2913 """
2913 path_prefix = (self.parent_group.full_path_splitted if
2914 path_prefix = (self.parent_group.full_path_splitted if
2914 self.parent_group else [])
2915 self.parent_group else [])
2915 return RepoGroup.url_sep().join(path_prefix + [group_name])
2916 return RepoGroup.url_sep().join(path_prefix + [group_name])
2916
2917
2917 def update_commit_cache(self, config=None):
2918 def update_commit_cache(self, config=None):
2918 """
2919 """
2919 Update cache of last commit for newest repository inside this repository group.
2920 Update cache of last commit for newest repository inside this repository group.
2920 cache_keys should be::
2921 cache_keys should be::
2921
2922
2922 source_repo_id
2923 source_repo_id
2923 short_id
2924 short_id
2924 raw_id
2925 raw_id
2925 revision
2926 revision
2926 parents
2927 parents
2927 message
2928 message
2928 date
2929 date
2929 author
2930 author
2930
2931
2931 """
2932 """
2932 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2933 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2933 empty_date = datetime.datetime.fromtimestamp(0)
2934 empty_date = datetime.datetime.fromtimestamp(0)
2934
2935
2935 def repo_groups_and_repos(root_gr):
2936 def repo_groups_and_repos(root_gr):
2936 for _repo in root_gr.repositories:
2937 for _repo in root_gr.repositories:
2937 yield _repo
2938 yield _repo
2938 for child_group in root_gr.children.all():
2939 for child_group in root_gr.children.all():
2939 yield child_group
2940 yield child_group
2940
2941
2941 latest_repo_cs_cache = {}
2942 latest_repo_cs_cache = {}
2942 for obj in repo_groups_and_repos(self):
2943 for obj in repo_groups_and_repos(self):
2943 repo_cs_cache = obj.changeset_cache
2944 repo_cs_cache = obj.changeset_cache
2944 date_latest = latest_repo_cs_cache.get('date', empty_date)
2945 date_latest = latest_repo_cs_cache.get('date', empty_date)
2945 date_current = repo_cs_cache.get('date', empty_date)
2946 date_current = repo_cs_cache.get('date', empty_date)
2946 current_timestamp = datetime_to_time(parse_datetime(date_latest))
2947 current_timestamp = datetime_to_time(parse_datetime(date_latest))
2947 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
2948 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
2948 latest_repo_cs_cache = repo_cs_cache
2949 latest_repo_cs_cache = repo_cs_cache
2949 if hasattr(obj, 'repo_id'):
2950 if hasattr(obj, 'repo_id'):
2950 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
2951 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
2951 else:
2952 else:
2952 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
2953 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
2953
2954
2954 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
2955 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
2955
2956
2956 latest_repo_cs_cache['updated_on'] = time.time()
2957 latest_repo_cs_cache['updated_on'] = time.time()
2957 self.changeset_cache = latest_repo_cs_cache
2958 self.changeset_cache = latest_repo_cs_cache
2958 self.updated_on = _date_latest
2959 self.updated_on = _date_latest
2959 Session().add(self)
2960 Session().add(self)
2960 Session().commit()
2961 Session().commit()
2961
2962
2962 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
2963 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
2963 self.group_name, latest_repo_cs_cache, _date_latest)
2964 self.group_name, latest_repo_cs_cache, _date_latest)
2964
2965
2965 def permissions(self, with_admins=True, with_owner=True,
2966 def permissions(self, with_admins=True, with_owner=True,
2966 expand_from_user_groups=False):
2967 expand_from_user_groups=False):
2967 """
2968 """
2968 Permissions for repository groups
2969 Permissions for repository groups
2969 """
2970 """
2970 _admin_perm = 'group.admin'
2971 _admin_perm = 'group.admin'
2971
2972
2972 owner_row = []
2973 owner_row = []
2973 if with_owner:
2974 if with_owner:
2974 usr = AttributeDict(self.user.get_dict())
2975 usr = AttributeDict(self.user.get_dict())
2975 usr.owner_row = True
2976 usr.owner_row = True
2976 usr.permission = _admin_perm
2977 usr.permission = _admin_perm
2977 owner_row.append(usr)
2978 owner_row.append(usr)
2978
2979
2979 super_admin_ids = []
2980 super_admin_ids = []
2980 super_admin_rows = []
2981 super_admin_rows = []
2981 if with_admins:
2982 if with_admins:
2982 for usr in User.get_all_super_admins():
2983 for usr in User.get_all_super_admins():
2983 super_admin_ids.append(usr.user_id)
2984 super_admin_ids.append(usr.user_id)
2984 # if this admin is also owner, don't double the record
2985 # if this admin is also owner, don't double the record
2985 if usr.user_id == owner_row[0].user_id:
2986 if usr.user_id == owner_row[0].user_id:
2986 owner_row[0].admin_row = True
2987 owner_row[0].admin_row = True
2987 else:
2988 else:
2988 usr = AttributeDict(usr.get_dict())
2989 usr = AttributeDict(usr.get_dict())
2989 usr.admin_row = True
2990 usr.admin_row = True
2990 usr.permission = _admin_perm
2991 usr.permission = _admin_perm
2991 super_admin_rows.append(usr)
2992 super_admin_rows.append(usr)
2992
2993
2993 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2994 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2994 q = q.options(joinedload(UserRepoGroupToPerm.group),
2995 q = q.options(joinedload(UserRepoGroupToPerm.group),
2995 joinedload(UserRepoGroupToPerm.user),
2996 joinedload(UserRepoGroupToPerm.user),
2996 joinedload(UserRepoGroupToPerm.permission),)
2997 joinedload(UserRepoGroupToPerm.permission),)
2997
2998
2998 # get owners and admins and permissions. We do a trick of re-writing
2999 # get owners and admins and permissions. We do a trick of re-writing
2999 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3000 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3000 # has a global reference and changing one object propagates to all
3001 # has a global reference and changing one object propagates to all
3001 # others. This means if admin is also an owner admin_row that change
3002 # others. This means if admin is also an owner admin_row that change
3002 # would propagate to both objects
3003 # would propagate to both objects
3003 perm_rows = []
3004 perm_rows = []
3004 for _usr in q.all():
3005 for _usr in q.all():
3005 usr = AttributeDict(_usr.user.get_dict())
3006 usr = AttributeDict(_usr.user.get_dict())
3006 # if this user is also owner/admin, mark as duplicate record
3007 # if this user is also owner/admin, mark as duplicate record
3007 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3008 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3008 usr.duplicate_perm = True
3009 usr.duplicate_perm = True
3009 usr.permission = _usr.permission.permission_name
3010 usr.permission = _usr.permission.permission_name
3010 perm_rows.append(usr)
3011 perm_rows.append(usr)
3011
3012
3012 # filter the perm rows by 'default' first and then sort them by
3013 # filter the perm rows by 'default' first and then sort them by
3013 # admin,write,read,none permissions sorted again alphabetically in
3014 # admin,write,read,none permissions sorted again alphabetically in
3014 # each group
3015 # each group
3015 perm_rows = sorted(perm_rows, key=display_user_sort)
3016 perm_rows = sorted(perm_rows, key=display_user_sort)
3016
3017
3017 user_groups_rows = []
3018 user_groups_rows = []
3018 if expand_from_user_groups:
3019 if expand_from_user_groups:
3019 for ug in self.permission_user_groups(with_members=True):
3020 for ug in self.permission_user_groups(with_members=True):
3020 for user_data in ug.members:
3021 for user_data in ug.members:
3021 user_groups_rows.append(user_data)
3022 user_groups_rows.append(user_data)
3022
3023
3023 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3024 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3024
3025
3025 def permission_user_groups(self, with_members=False):
3026 def permission_user_groups(self, with_members=False):
3026 q = UserGroupRepoGroupToPerm.query()\
3027 q = UserGroupRepoGroupToPerm.query()\
3027 .filter(UserGroupRepoGroupToPerm.group == self)
3028 .filter(UserGroupRepoGroupToPerm.group == self)
3028 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3029 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3029 joinedload(UserGroupRepoGroupToPerm.users_group),
3030 joinedload(UserGroupRepoGroupToPerm.users_group),
3030 joinedload(UserGroupRepoGroupToPerm.permission),)
3031 joinedload(UserGroupRepoGroupToPerm.permission),)
3031
3032
3032 perm_rows = []
3033 perm_rows = []
3033 for _user_group in q.all():
3034 for _user_group in q.all():
3034 entry = AttributeDict(_user_group.users_group.get_dict())
3035 entry = AttributeDict(_user_group.users_group.get_dict())
3035 entry.permission = _user_group.permission.permission_name
3036 entry.permission = _user_group.permission.permission_name
3036 if with_members:
3037 if with_members:
3037 entry.members = [x.user.get_dict()
3038 entry.members = [x.user.get_dict()
3038 for x in _user_group.users_group.members]
3039 for x in _user_group.users_group.members]
3039 perm_rows.append(entry)
3040 perm_rows.append(entry)
3040
3041
3041 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3042 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3042 return perm_rows
3043 return perm_rows
3043
3044
3044 def get_api_data(self):
3045 def get_api_data(self):
3045 """
3046 """
3046 Common function for generating api data
3047 Common function for generating api data
3047
3048
3048 """
3049 """
3049 group = self
3050 group = self
3050 data = {
3051 data = {
3051 'group_id': group.group_id,
3052 'group_id': group.group_id,
3052 'group_name': group.group_name,
3053 'group_name': group.group_name,
3053 'group_description': group.description_safe,
3054 'group_description': group.description_safe,
3054 'parent_group': group.parent_group.group_name if group.parent_group else None,
3055 'parent_group': group.parent_group.group_name if group.parent_group else None,
3055 'repositories': [x.repo_name for x in group.repositories],
3056 'repositories': [x.repo_name for x in group.repositories],
3056 'owner': group.user.username,
3057 'owner': group.user.username,
3057 }
3058 }
3058 return data
3059 return data
3059
3060
3060 def get_dict(self):
3061 def get_dict(self):
3061 # Since we transformed `group_name` to a hybrid property, we need to
3062 # Since we transformed `group_name` to a hybrid property, we need to
3062 # keep compatibility with the code which uses `group_name` field.
3063 # keep compatibility with the code which uses `group_name` field.
3063 result = super(RepoGroup, self).get_dict()
3064 result = super(RepoGroup, self).get_dict()
3064 result['group_name'] = result.pop('_group_name', None)
3065 result['group_name'] = result.pop('_group_name', None)
3065 return result
3066 return result
3066
3067
3067
3068
3068 class Permission(Base, BaseModel):
3069 class Permission(Base, BaseModel):
3069 __tablename__ = 'permissions'
3070 __tablename__ = 'permissions'
3070 __table_args__ = (
3071 __table_args__ = (
3071 Index('p_perm_name_idx', 'permission_name'),
3072 Index('p_perm_name_idx', 'permission_name'),
3072 base_table_args,
3073 base_table_args,
3073 )
3074 )
3074
3075
3075 PERMS = [
3076 PERMS = [
3076 ('hg.admin', _('RhodeCode Super Administrator')),
3077 ('hg.admin', _('RhodeCode Super Administrator')),
3077
3078
3078 ('repository.none', _('Repository no access')),
3079 ('repository.none', _('Repository no access')),
3079 ('repository.read', _('Repository read access')),
3080 ('repository.read', _('Repository read access')),
3080 ('repository.write', _('Repository write access')),
3081 ('repository.write', _('Repository write access')),
3081 ('repository.admin', _('Repository admin access')),
3082 ('repository.admin', _('Repository admin access')),
3082
3083
3083 ('group.none', _('Repository group no access')),
3084 ('group.none', _('Repository group no access')),
3084 ('group.read', _('Repository group read access')),
3085 ('group.read', _('Repository group read access')),
3085 ('group.write', _('Repository group write access')),
3086 ('group.write', _('Repository group write access')),
3086 ('group.admin', _('Repository group admin access')),
3087 ('group.admin', _('Repository group admin access')),
3087
3088
3088 ('usergroup.none', _('User group no access')),
3089 ('usergroup.none', _('User group no access')),
3089 ('usergroup.read', _('User group read access')),
3090 ('usergroup.read', _('User group read access')),
3090 ('usergroup.write', _('User group write access')),
3091 ('usergroup.write', _('User group write access')),
3091 ('usergroup.admin', _('User group admin access')),
3092 ('usergroup.admin', _('User group admin access')),
3092
3093
3093 ('branch.none', _('Branch no permissions')),
3094 ('branch.none', _('Branch no permissions')),
3094 ('branch.merge', _('Branch access by web merge')),
3095 ('branch.merge', _('Branch access by web merge')),
3095 ('branch.push', _('Branch access by push')),
3096 ('branch.push', _('Branch access by push')),
3096 ('branch.push_force', _('Branch access by push with force')),
3097 ('branch.push_force', _('Branch access by push with force')),
3097
3098
3098 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3099 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3099 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3100 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3100
3101
3101 ('hg.usergroup.create.false', _('User Group creation disabled')),
3102 ('hg.usergroup.create.false', _('User Group creation disabled')),
3102 ('hg.usergroup.create.true', _('User Group creation enabled')),
3103 ('hg.usergroup.create.true', _('User Group creation enabled')),
3103
3104
3104 ('hg.create.none', _('Repository creation disabled')),
3105 ('hg.create.none', _('Repository creation disabled')),
3105 ('hg.create.repository', _('Repository creation enabled')),
3106 ('hg.create.repository', _('Repository creation enabled')),
3106 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3107 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3107 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3108 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3108
3109
3109 ('hg.fork.none', _('Repository forking disabled')),
3110 ('hg.fork.none', _('Repository forking disabled')),
3110 ('hg.fork.repository', _('Repository forking enabled')),
3111 ('hg.fork.repository', _('Repository forking enabled')),
3111
3112
3112 ('hg.register.none', _('Registration disabled')),
3113 ('hg.register.none', _('Registration disabled')),
3113 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3114 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3114 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3115 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3115
3116
3116 ('hg.password_reset.enabled', _('Password reset enabled')),
3117 ('hg.password_reset.enabled', _('Password reset enabled')),
3117 ('hg.password_reset.hidden', _('Password reset hidden')),
3118 ('hg.password_reset.hidden', _('Password reset hidden')),
3118 ('hg.password_reset.disabled', _('Password reset disabled')),
3119 ('hg.password_reset.disabled', _('Password reset disabled')),
3119
3120
3120 ('hg.extern_activate.manual', _('Manual activation of external account')),
3121 ('hg.extern_activate.manual', _('Manual activation of external account')),
3121 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3122 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3122
3123
3123 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3124 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3124 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3125 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3125 ]
3126 ]
3126
3127
3127 # definition of system default permissions for DEFAULT user, created on
3128 # definition of system default permissions for DEFAULT user, created on
3128 # system setup
3129 # system setup
3129 DEFAULT_USER_PERMISSIONS = [
3130 DEFAULT_USER_PERMISSIONS = [
3130 # object perms
3131 # object perms
3131 'repository.read',
3132 'repository.read',
3132 'group.read',
3133 'group.read',
3133 'usergroup.read',
3134 'usergroup.read',
3134 # branch, for backward compat we need same value as before so forced pushed
3135 # branch, for backward compat we need same value as before so forced pushed
3135 'branch.push_force',
3136 'branch.push_force',
3136 # global
3137 # global
3137 'hg.create.repository',
3138 'hg.create.repository',
3138 'hg.repogroup.create.false',
3139 'hg.repogroup.create.false',
3139 'hg.usergroup.create.false',
3140 'hg.usergroup.create.false',
3140 'hg.create.write_on_repogroup.true',
3141 'hg.create.write_on_repogroup.true',
3141 'hg.fork.repository',
3142 'hg.fork.repository',
3142 'hg.register.manual_activate',
3143 'hg.register.manual_activate',
3143 'hg.password_reset.enabled',
3144 'hg.password_reset.enabled',
3144 'hg.extern_activate.auto',
3145 'hg.extern_activate.auto',
3145 'hg.inherit_default_perms.true',
3146 'hg.inherit_default_perms.true',
3146 ]
3147 ]
3147
3148
3148 # defines which permissions are more important higher the more important
3149 # defines which permissions are more important higher the more important
3149 # Weight defines which permissions are more important.
3150 # Weight defines which permissions are more important.
3150 # The higher number the more important.
3151 # The higher number the more important.
3151 PERM_WEIGHTS = {
3152 PERM_WEIGHTS = {
3152 'repository.none': 0,
3153 'repository.none': 0,
3153 'repository.read': 1,
3154 'repository.read': 1,
3154 'repository.write': 3,
3155 'repository.write': 3,
3155 'repository.admin': 4,
3156 'repository.admin': 4,
3156
3157
3157 'group.none': 0,
3158 'group.none': 0,
3158 'group.read': 1,
3159 'group.read': 1,
3159 'group.write': 3,
3160 'group.write': 3,
3160 'group.admin': 4,
3161 'group.admin': 4,
3161
3162
3162 'usergroup.none': 0,
3163 'usergroup.none': 0,
3163 'usergroup.read': 1,
3164 'usergroup.read': 1,
3164 'usergroup.write': 3,
3165 'usergroup.write': 3,
3165 'usergroup.admin': 4,
3166 'usergroup.admin': 4,
3166
3167
3167 'branch.none': 0,
3168 'branch.none': 0,
3168 'branch.merge': 1,
3169 'branch.merge': 1,
3169 'branch.push': 3,
3170 'branch.push': 3,
3170 'branch.push_force': 4,
3171 'branch.push_force': 4,
3171
3172
3172 'hg.repogroup.create.false': 0,
3173 'hg.repogroup.create.false': 0,
3173 'hg.repogroup.create.true': 1,
3174 'hg.repogroup.create.true': 1,
3174
3175
3175 'hg.usergroup.create.false': 0,
3176 'hg.usergroup.create.false': 0,
3176 'hg.usergroup.create.true': 1,
3177 'hg.usergroup.create.true': 1,
3177
3178
3178 'hg.fork.none': 0,
3179 'hg.fork.none': 0,
3179 'hg.fork.repository': 1,
3180 'hg.fork.repository': 1,
3180 'hg.create.none': 0,
3181 'hg.create.none': 0,
3181 'hg.create.repository': 1
3182 'hg.create.repository': 1
3182 }
3183 }
3183
3184
3184 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3185 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3185 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3186 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3186 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3187 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3187
3188
3188 def __unicode__(self):
3189 def __unicode__(self):
3189 return u"<%s('%s:%s')>" % (
3190 return u"<%s('%s:%s')>" % (
3190 self.__class__.__name__, self.permission_id, self.permission_name
3191 self.__class__.__name__, self.permission_id, self.permission_name
3191 )
3192 )
3192
3193
3193 @classmethod
3194 @classmethod
3194 def get_by_key(cls, key):
3195 def get_by_key(cls, key):
3195 return cls.query().filter(cls.permission_name == key).scalar()
3196 return cls.query().filter(cls.permission_name == key).scalar()
3196
3197
3197 @classmethod
3198 @classmethod
3198 def get_default_repo_perms(cls, user_id, repo_id=None):
3199 def get_default_repo_perms(cls, user_id, repo_id=None):
3199 q = Session().query(UserRepoToPerm, Repository, Permission)\
3200 q = Session().query(UserRepoToPerm, Repository, Permission)\
3200 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3201 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3201 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3202 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3202 .filter(UserRepoToPerm.user_id == user_id)
3203 .filter(UserRepoToPerm.user_id == user_id)
3203 if repo_id:
3204 if repo_id:
3204 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3205 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3205 return q.all()
3206 return q.all()
3206
3207
3207 @classmethod
3208 @classmethod
3208 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3209 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3209 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3210 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3210 .join(
3211 .join(
3211 Permission,
3212 Permission,
3212 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3213 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3213 .join(
3214 .join(
3214 UserRepoToPerm,
3215 UserRepoToPerm,
3215 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3216 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3216 .filter(UserRepoToPerm.user_id == user_id)
3217 .filter(UserRepoToPerm.user_id == user_id)
3217
3218
3218 if repo_id:
3219 if repo_id:
3219 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3220 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3220 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3221 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3221
3222
3222 @classmethod
3223 @classmethod
3223 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3224 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3224 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3225 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3225 .join(
3226 .join(
3226 Permission,
3227 Permission,
3227 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3228 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3228 .join(
3229 .join(
3229 Repository,
3230 Repository,
3230 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3231 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3231 .join(
3232 .join(
3232 UserGroup,
3233 UserGroup,
3233 UserGroupRepoToPerm.users_group_id ==
3234 UserGroupRepoToPerm.users_group_id ==
3234 UserGroup.users_group_id)\
3235 UserGroup.users_group_id)\
3235 .join(
3236 .join(
3236 UserGroupMember,
3237 UserGroupMember,
3237 UserGroupRepoToPerm.users_group_id ==
3238 UserGroupRepoToPerm.users_group_id ==
3238 UserGroupMember.users_group_id)\
3239 UserGroupMember.users_group_id)\
3239 .filter(
3240 .filter(
3240 UserGroupMember.user_id == user_id,
3241 UserGroupMember.user_id == user_id,
3241 UserGroup.users_group_active == true())
3242 UserGroup.users_group_active == true())
3242 if repo_id:
3243 if repo_id:
3243 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3244 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3244 return q.all()
3245 return q.all()
3245
3246
3246 @classmethod
3247 @classmethod
3247 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3248 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3248 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3249 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3249 .join(
3250 .join(
3250 Permission,
3251 Permission,
3251 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3252 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3252 .join(
3253 .join(
3253 UserGroupRepoToPerm,
3254 UserGroupRepoToPerm,
3254 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3255 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3255 .join(
3256 .join(
3256 UserGroup,
3257 UserGroup,
3257 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3258 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3258 .join(
3259 .join(
3259 UserGroupMember,
3260 UserGroupMember,
3260 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3261 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3261 .filter(
3262 .filter(
3262 UserGroupMember.user_id == user_id,
3263 UserGroupMember.user_id == user_id,
3263 UserGroup.users_group_active == true())
3264 UserGroup.users_group_active == true())
3264
3265
3265 if repo_id:
3266 if repo_id:
3266 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3267 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3267 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3268 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3268
3269
3269 @classmethod
3270 @classmethod
3270 def get_default_group_perms(cls, user_id, repo_group_id=None):
3271 def get_default_group_perms(cls, user_id, repo_group_id=None):
3271 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3272 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3272 .join(
3273 .join(
3273 Permission,
3274 Permission,
3274 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3275 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3275 .join(
3276 .join(
3276 RepoGroup,
3277 RepoGroup,
3277 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3278 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3278 .filter(UserRepoGroupToPerm.user_id == user_id)
3279 .filter(UserRepoGroupToPerm.user_id == user_id)
3279 if repo_group_id:
3280 if repo_group_id:
3280 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3281 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3281 return q.all()
3282 return q.all()
3282
3283
3283 @classmethod
3284 @classmethod
3284 def get_default_group_perms_from_user_group(
3285 def get_default_group_perms_from_user_group(
3285 cls, user_id, repo_group_id=None):
3286 cls, user_id, repo_group_id=None):
3286 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3287 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3287 .join(
3288 .join(
3288 Permission,
3289 Permission,
3289 UserGroupRepoGroupToPerm.permission_id ==
3290 UserGroupRepoGroupToPerm.permission_id ==
3290 Permission.permission_id)\
3291 Permission.permission_id)\
3291 .join(
3292 .join(
3292 RepoGroup,
3293 RepoGroup,
3293 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3294 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3294 .join(
3295 .join(
3295 UserGroup,
3296 UserGroup,
3296 UserGroupRepoGroupToPerm.users_group_id ==
3297 UserGroupRepoGroupToPerm.users_group_id ==
3297 UserGroup.users_group_id)\
3298 UserGroup.users_group_id)\
3298 .join(
3299 .join(
3299 UserGroupMember,
3300 UserGroupMember,
3300 UserGroupRepoGroupToPerm.users_group_id ==
3301 UserGroupRepoGroupToPerm.users_group_id ==
3301 UserGroupMember.users_group_id)\
3302 UserGroupMember.users_group_id)\
3302 .filter(
3303 .filter(
3303 UserGroupMember.user_id == user_id,
3304 UserGroupMember.user_id == user_id,
3304 UserGroup.users_group_active == true())
3305 UserGroup.users_group_active == true())
3305 if repo_group_id:
3306 if repo_group_id:
3306 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3307 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3307 return q.all()
3308 return q.all()
3308
3309
3309 @classmethod
3310 @classmethod
3310 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3311 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3311 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3312 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3312 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3313 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3313 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3314 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3314 .filter(UserUserGroupToPerm.user_id == user_id)
3315 .filter(UserUserGroupToPerm.user_id == user_id)
3315 if user_group_id:
3316 if user_group_id:
3316 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3317 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3317 return q.all()
3318 return q.all()
3318
3319
3319 @classmethod
3320 @classmethod
3320 def get_default_user_group_perms_from_user_group(
3321 def get_default_user_group_perms_from_user_group(
3321 cls, user_id, user_group_id=None):
3322 cls, user_id, user_group_id=None):
3322 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3323 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3323 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3324 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3324 .join(
3325 .join(
3325 Permission,
3326 Permission,
3326 UserGroupUserGroupToPerm.permission_id ==
3327 UserGroupUserGroupToPerm.permission_id ==
3327 Permission.permission_id)\
3328 Permission.permission_id)\
3328 .join(
3329 .join(
3329 TargetUserGroup,
3330 TargetUserGroup,
3330 UserGroupUserGroupToPerm.target_user_group_id ==
3331 UserGroupUserGroupToPerm.target_user_group_id ==
3331 TargetUserGroup.users_group_id)\
3332 TargetUserGroup.users_group_id)\
3332 .join(
3333 .join(
3333 UserGroup,
3334 UserGroup,
3334 UserGroupUserGroupToPerm.user_group_id ==
3335 UserGroupUserGroupToPerm.user_group_id ==
3335 UserGroup.users_group_id)\
3336 UserGroup.users_group_id)\
3336 .join(
3337 .join(
3337 UserGroupMember,
3338 UserGroupMember,
3338 UserGroupUserGroupToPerm.user_group_id ==
3339 UserGroupUserGroupToPerm.user_group_id ==
3339 UserGroupMember.users_group_id)\
3340 UserGroupMember.users_group_id)\
3340 .filter(
3341 .filter(
3341 UserGroupMember.user_id == user_id,
3342 UserGroupMember.user_id == user_id,
3342 UserGroup.users_group_active == true())
3343 UserGroup.users_group_active == true())
3343 if user_group_id:
3344 if user_group_id:
3344 q = q.filter(
3345 q = q.filter(
3345 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3346 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3346
3347
3347 return q.all()
3348 return q.all()
3348
3349
3349
3350
3350 class UserRepoToPerm(Base, BaseModel):
3351 class UserRepoToPerm(Base, BaseModel):
3351 __tablename__ = 'repo_to_perm'
3352 __tablename__ = 'repo_to_perm'
3352 __table_args__ = (
3353 __table_args__ = (
3353 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3354 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3354 base_table_args
3355 base_table_args
3355 )
3356 )
3356
3357
3357 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3358 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3358 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3359 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3359 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3360 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3360 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3361 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3361
3362
3362 user = relationship('User')
3363 user = relationship('User')
3363 repository = relationship('Repository')
3364 repository = relationship('Repository')
3364 permission = relationship('Permission')
3365 permission = relationship('Permission')
3365
3366
3366 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3367 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3367
3368
3368 @classmethod
3369 @classmethod
3369 def create(cls, user, repository, permission):
3370 def create(cls, user, repository, permission):
3370 n = cls()
3371 n = cls()
3371 n.user = user
3372 n.user = user
3372 n.repository = repository
3373 n.repository = repository
3373 n.permission = permission
3374 n.permission = permission
3374 Session().add(n)
3375 Session().add(n)
3375 return n
3376 return n
3376
3377
3377 def __unicode__(self):
3378 def __unicode__(self):
3378 return u'<%s => %s >' % (self.user, self.repository)
3379 return u'<%s => %s >' % (self.user, self.repository)
3379
3380
3380
3381
3381 class UserUserGroupToPerm(Base, BaseModel):
3382 class UserUserGroupToPerm(Base, BaseModel):
3382 __tablename__ = 'user_user_group_to_perm'
3383 __tablename__ = 'user_user_group_to_perm'
3383 __table_args__ = (
3384 __table_args__ = (
3384 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3385 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3385 base_table_args
3386 base_table_args
3386 )
3387 )
3387
3388
3388 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3389 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3389 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3390 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3390 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3391 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3391 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3392 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3392
3393
3393 user = relationship('User')
3394 user = relationship('User')
3394 user_group = relationship('UserGroup')
3395 user_group = relationship('UserGroup')
3395 permission = relationship('Permission')
3396 permission = relationship('Permission')
3396
3397
3397 @classmethod
3398 @classmethod
3398 def create(cls, user, user_group, permission):
3399 def create(cls, user, user_group, permission):
3399 n = cls()
3400 n = cls()
3400 n.user = user
3401 n.user = user
3401 n.user_group = user_group
3402 n.user_group = user_group
3402 n.permission = permission
3403 n.permission = permission
3403 Session().add(n)
3404 Session().add(n)
3404 return n
3405 return n
3405
3406
3406 def __unicode__(self):
3407 def __unicode__(self):
3407 return u'<%s => %s >' % (self.user, self.user_group)
3408 return u'<%s => %s >' % (self.user, self.user_group)
3408
3409
3409
3410
3410 class UserToPerm(Base, BaseModel):
3411 class UserToPerm(Base, BaseModel):
3411 __tablename__ = 'user_to_perm'
3412 __tablename__ = 'user_to_perm'
3412 __table_args__ = (
3413 __table_args__ = (
3413 UniqueConstraint('user_id', 'permission_id'),
3414 UniqueConstraint('user_id', 'permission_id'),
3414 base_table_args
3415 base_table_args
3415 )
3416 )
3416
3417
3417 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3418 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3418 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3419 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3419 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3420 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3420
3421
3421 user = relationship('User')
3422 user = relationship('User')
3422 permission = relationship('Permission', lazy='joined')
3423 permission = relationship('Permission', lazy='joined')
3423
3424
3424 def __unicode__(self):
3425 def __unicode__(self):
3425 return u'<%s => %s >' % (self.user, self.permission)
3426 return u'<%s => %s >' % (self.user, self.permission)
3426
3427
3427
3428
3428 class UserGroupRepoToPerm(Base, BaseModel):
3429 class UserGroupRepoToPerm(Base, BaseModel):
3429 __tablename__ = 'users_group_repo_to_perm'
3430 __tablename__ = 'users_group_repo_to_perm'
3430 __table_args__ = (
3431 __table_args__ = (
3431 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3432 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3432 base_table_args
3433 base_table_args
3433 )
3434 )
3434
3435
3435 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3436 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3436 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3437 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3437 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3438 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3438 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3439 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3439
3440
3440 users_group = relationship('UserGroup')
3441 users_group = relationship('UserGroup')
3441 permission = relationship('Permission')
3442 permission = relationship('Permission')
3442 repository = relationship('Repository')
3443 repository = relationship('Repository')
3443 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3444 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3444
3445
3445 @classmethod
3446 @classmethod
3446 def create(cls, users_group, repository, permission):
3447 def create(cls, users_group, repository, permission):
3447 n = cls()
3448 n = cls()
3448 n.users_group = users_group
3449 n.users_group = users_group
3449 n.repository = repository
3450 n.repository = repository
3450 n.permission = permission
3451 n.permission = permission
3451 Session().add(n)
3452 Session().add(n)
3452 return n
3453 return n
3453
3454
3454 def __unicode__(self):
3455 def __unicode__(self):
3455 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3456 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3456
3457
3457
3458
3458 class UserGroupUserGroupToPerm(Base, BaseModel):
3459 class UserGroupUserGroupToPerm(Base, BaseModel):
3459 __tablename__ = 'user_group_user_group_to_perm'
3460 __tablename__ = 'user_group_user_group_to_perm'
3460 __table_args__ = (
3461 __table_args__ = (
3461 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3462 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3462 CheckConstraint('target_user_group_id != user_group_id'),
3463 CheckConstraint('target_user_group_id != user_group_id'),
3463 base_table_args
3464 base_table_args
3464 )
3465 )
3465
3466
3466 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3467 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3467 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3468 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3468 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3469 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3469 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3470 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3470
3471
3471 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3472 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3472 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3473 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3473 permission = relationship('Permission')
3474 permission = relationship('Permission')
3474
3475
3475 @classmethod
3476 @classmethod
3476 def create(cls, target_user_group, user_group, permission):
3477 def create(cls, target_user_group, user_group, permission):
3477 n = cls()
3478 n = cls()
3478 n.target_user_group = target_user_group
3479 n.target_user_group = target_user_group
3479 n.user_group = user_group
3480 n.user_group = user_group
3480 n.permission = permission
3481 n.permission = permission
3481 Session().add(n)
3482 Session().add(n)
3482 return n
3483 return n
3483
3484
3484 def __unicode__(self):
3485 def __unicode__(self):
3485 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3486 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3486
3487
3487
3488
3488 class UserGroupToPerm(Base, BaseModel):
3489 class UserGroupToPerm(Base, BaseModel):
3489 __tablename__ = 'users_group_to_perm'
3490 __tablename__ = 'users_group_to_perm'
3490 __table_args__ = (
3491 __table_args__ = (
3491 UniqueConstraint('users_group_id', 'permission_id',),
3492 UniqueConstraint('users_group_id', 'permission_id',),
3492 base_table_args
3493 base_table_args
3493 )
3494 )
3494
3495
3495 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3496 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3496 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3497 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3497 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3498 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3498
3499
3499 users_group = relationship('UserGroup')
3500 users_group = relationship('UserGroup')
3500 permission = relationship('Permission')
3501 permission = relationship('Permission')
3501
3502
3502
3503
3503 class UserRepoGroupToPerm(Base, BaseModel):
3504 class UserRepoGroupToPerm(Base, BaseModel):
3504 __tablename__ = 'user_repo_group_to_perm'
3505 __tablename__ = 'user_repo_group_to_perm'
3505 __table_args__ = (
3506 __table_args__ = (
3506 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3507 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3507 base_table_args
3508 base_table_args
3508 )
3509 )
3509
3510
3510 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3511 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3511 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3512 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3512 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3513 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3513 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3514 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3514
3515
3515 user = relationship('User')
3516 user = relationship('User')
3516 group = relationship('RepoGroup')
3517 group = relationship('RepoGroup')
3517 permission = relationship('Permission')
3518 permission = relationship('Permission')
3518
3519
3519 @classmethod
3520 @classmethod
3520 def create(cls, user, repository_group, permission):
3521 def create(cls, user, repository_group, permission):
3521 n = cls()
3522 n = cls()
3522 n.user = user
3523 n.user = user
3523 n.group = repository_group
3524 n.group = repository_group
3524 n.permission = permission
3525 n.permission = permission
3525 Session().add(n)
3526 Session().add(n)
3526 return n
3527 return n
3527
3528
3528
3529
3529 class UserGroupRepoGroupToPerm(Base, BaseModel):
3530 class UserGroupRepoGroupToPerm(Base, BaseModel):
3530 __tablename__ = 'users_group_repo_group_to_perm'
3531 __tablename__ = 'users_group_repo_group_to_perm'
3531 __table_args__ = (
3532 __table_args__ = (
3532 UniqueConstraint('users_group_id', 'group_id'),
3533 UniqueConstraint('users_group_id', 'group_id'),
3533 base_table_args
3534 base_table_args
3534 )
3535 )
3535
3536
3536 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3537 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3537 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3538 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3538 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3539 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3539 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3540 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3540
3541
3541 users_group = relationship('UserGroup')
3542 users_group = relationship('UserGroup')
3542 permission = relationship('Permission')
3543 permission = relationship('Permission')
3543 group = relationship('RepoGroup')
3544 group = relationship('RepoGroup')
3544
3545
3545 @classmethod
3546 @classmethod
3546 def create(cls, user_group, repository_group, permission):
3547 def create(cls, user_group, repository_group, permission):
3547 n = cls()
3548 n = cls()
3548 n.users_group = user_group
3549 n.users_group = user_group
3549 n.group = repository_group
3550 n.group = repository_group
3550 n.permission = permission
3551 n.permission = permission
3551 Session().add(n)
3552 Session().add(n)
3552 return n
3553 return n
3553
3554
3554 def __unicode__(self):
3555 def __unicode__(self):
3555 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3556 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3556
3557
3557
3558
3558 class Statistics(Base, BaseModel):
3559 class Statistics(Base, BaseModel):
3559 __tablename__ = 'statistics'
3560 __tablename__ = 'statistics'
3560 __table_args__ = (
3561 __table_args__ = (
3561 base_table_args
3562 base_table_args
3562 )
3563 )
3563
3564
3564 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3565 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3565 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3566 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3566 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3567 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3567 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3568 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3568 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3569 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3569 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3570 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3570
3571
3571 repository = relationship('Repository', single_parent=True)
3572 repository = relationship('Repository', single_parent=True)
3572
3573
3573
3574
3574 class UserFollowing(Base, BaseModel):
3575 class UserFollowing(Base, BaseModel):
3575 __tablename__ = 'user_followings'
3576 __tablename__ = 'user_followings'
3576 __table_args__ = (
3577 __table_args__ = (
3577 UniqueConstraint('user_id', 'follows_repository_id'),
3578 UniqueConstraint('user_id', 'follows_repository_id'),
3578 UniqueConstraint('user_id', 'follows_user_id'),
3579 UniqueConstraint('user_id', 'follows_user_id'),
3579 base_table_args
3580 base_table_args
3580 )
3581 )
3581
3582
3582 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3583 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3583 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3584 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3584 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3585 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3585 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3586 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3586 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3587 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3587
3588
3588 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3589 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3589
3590
3590 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3591 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3591 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3592 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3592
3593
3593 @classmethod
3594 @classmethod
3594 def get_repo_followers(cls, repo_id):
3595 def get_repo_followers(cls, repo_id):
3595 return cls.query().filter(cls.follows_repo_id == repo_id)
3596 return cls.query().filter(cls.follows_repo_id == repo_id)
3596
3597
3597
3598
3598 class CacheKey(Base, BaseModel):
3599 class CacheKey(Base, BaseModel):
3599 __tablename__ = 'cache_invalidation'
3600 __tablename__ = 'cache_invalidation'
3600 __table_args__ = (
3601 __table_args__ = (
3601 UniqueConstraint('cache_key'),
3602 UniqueConstraint('cache_key'),
3602 Index('key_idx', 'cache_key'),
3603 Index('key_idx', 'cache_key'),
3603 base_table_args,
3604 base_table_args,
3604 )
3605 )
3605
3606
3606 CACHE_TYPE_FEED = 'FEED'
3607 CACHE_TYPE_FEED = 'FEED'
3607
3608
3608 # namespaces used to register process/thread aware caches
3609 # namespaces used to register process/thread aware caches
3609 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3610 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3610 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3611 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3611
3612
3612 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3613 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3613 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3614 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3614 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3615 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3615 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3616 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3616 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3617 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3617
3618
3618 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3619 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3619 self.cache_key = cache_key
3620 self.cache_key = cache_key
3620 self.cache_args = cache_args
3621 self.cache_args = cache_args
3621 self.cache_active = False
3622 self.cache_active = False
3622 # first key should be same for all entries, since all workers should share it
3623 # first key should be same for all entries, since all workers should share it
3623 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3624 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3624
3625
3625 def __unicode__(self):
3626 def __unicode__(self):
3626 return u"<%s('%s:%s[%s]')>" % (
3627 return u"<%s('%s:%s[%s]')>" % (
3627 self.__class__.__name__,
3628 self.__class__.__name__,
3628 self.cache_id, self.cache_key, self.cache_active)
3629 self.cache_id, self.cache_key, self.cache_active)
3629
3630
3630 def _cache_key_partition(self):
3631 def _cache_key_partition(self):
3631 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3632 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3632 return prefix, repo_name, suffix
3633 return prefix, repo_name, suffix
3633
3634
3634 def get_prefix(self):
3635 def get_prefix(self):
3635 """
3636 """
3636 Try to extract prefix from existing cache key. The key could consist
3637 Try to extract prefix from existing cache key. The key could consist
3637 of prefix, repo_name, suffix
3638 of prefix, repo_name, suffix
3638 """
3639 """
3639 # this returns prefix, repo_name, suffix
3640 # this returns prefix, repo_name, suffix
3640 return self._cache_key_partition()[0]
3641 return self._cache_key_partition()[0]
3641
3642
3642 def get_suffix(self):
3643 def get_suffix(self):
3643 """
3644 """
3644 get suffix that might have been used in _get_cache_key to
3645 get suffix that might have been used in _get_cache_key to
3645 generate self.cache_key. Only used for informational purposes
3646 generate self.cache_key. Only used for informational purposes
3646 in repo_edit.mako.
3647 in repo_edit.mako.
3647 """
3648 """
3648 # prefix, repo_name, suffix
3649 # prefix, repo_name, suffix
3649 return self._cache_key_partition()[2]
3650 return self._cache_key_partition()[2]
3650
3651
3651 @classmethod
3652 @classmethod
3652 def generate_new_state_uid(cls, based_on=None):
3653 def generate_new_state_uid(cls, based_on=None):
3653 if based_on:
3654 if based_on:
3654 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3655 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3655 else:
3656 else:
3656 return str(uuid.uuid4())
3657 return str(uuid.uuid4())
3657
3658
3658 @classmethod
3659 @classmethod
3659 def delete_all_cache(cls):
3660 def delete_all_cache(cls):
3660 """
3661 """
3661 Delete all cache keys from database.
3662 Delete all cache keys from database.
3662 Should only be run when all instances are down and all entries
3663 Should only be run when all instances are down and all entries
3663 thus stale.
3664 thus stale.
3664 """
3665 """
3665 cls.query().delete()
3666 cls.query().delete()
3666 Session().commit()
3667 Session().commit()
3667
3668
3668 @classmethod
3669 @classmethod
3669 def set_invalidate(cls, cache_uid, delete=False):
3670 def set_invalidate(cls, cache_uid, delete=False):
3670 """
3671 """
3671 Mark all caches of a repo as invalid in the database.
3672 Mark all caches of a repo as invalid in the database.
3672 """
3673 """
3673
3674
3674 try:
3675 try:
3675 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3676 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3676 if delete:
3677 if delete:
3677 qry.delete()
3678 qry.delete()
3678 log.debug('cache objects deleted for cache args %s',
3679 log.debug('cache objects deleted for cache args %s',
3679 safe_str(cache_uid))
3680 safe_str(cache_uid))
3680 else:
3681 else:
3681 qry.update({"cache_active": False,
3682 qry.update({"cache_active": False,
3682 "cache_state_uid": cls.generate_new_state_uid()})
3683 "cache_state_uid": cls.generate_new_state_uid()})
3683 log.debug('cache objects marked as invalid for cache args %s',
3684 log.debug('cache objects marked as invalid for cache args %s',
3684 safe_str(cache_uid))
3685 safe_str(cache_uid))
3685
3686
3686 Session().commit()
3687 Session().commit()
3687 except Exception:
3688 except Exception:
3688 log.exception(
3689 log.exception(
3689 'Cache key invalidation failed for cache args %s',
3690 'Cache key invalidation failed for cache args %s',
3690 safe_str(cache_uid))
3691 safe_str(cache_uid))
3691 Session().rollback()
3692 Session().rollback()
3692
3693
3693 @classmethod
3694 @classmethod
3694 def get_active_cache(cls, cache_key):
3695 def get_active_cache(cls, cache_key):
3695 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3696 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3696 if inv_obj:
3697 if inv_obj:
3697 return inv_obj
3698 return inv_obj
3698 return None
3699 return None
3699
3700
3700 @classmethod
3701 @classmethod
3701 def get_namespace_map(cls, namespace):
3702 def get_namespace_map(cls, namespace):
3702 return {
3703 return {
3703 x.cache_key: x
3704 x.cache_key: x
3704 for x in cls.query().filter(cls.cache_args == namespace)}
3705 for x in cls.query().filter(cls.cache_args == namespace)}
3705
3706
3706
3707
3707 class ChangesetComment(Base, BaseModel):
3708 class ChangesetComment(Base, BaseModel):
3708 __tablename__ = 'changeset_comments'
3709 __tablename__ = 'changeset_comments'
3709 __table_args__ = (
3710 __table_args__ = (
3710 Index('cc_revision_idx', 'revision'),
3711 Index('cc_revision_idx', 'revision'),
3711 base_table_args,
3712 base_table_args,
3712 )
3713 )
3713
3714
3714 COMMENT_OUTDATED = u'comment_outdated'
3715 COMMENT_OUTDATED = u'comment_outdated'
3715 COMMENT_TYPE_NOTE = u'note'
3716 COMMENT_TYPE_NOTE = u'note'
3716 COMMENT_TYPE_TODO = u'todo'
3717 COMMENT_TYPE_TODO = u'todo'
3717 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3718 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3718
3719
3719 OP_IMMUTABLE = u'immutable'
3720 OP_IMMUTABLE = u'immutable'
3720 OP_CHANGEABLE = u'changeable'
3721 OP_CHANGEABLE = u'changeable'
3721
3722
3722 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3723 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3723 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3724 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3724 revision = Column('revision', String(40), nullable=True)
3725 revision = Column('revision', String(40), nullable=True)
3725 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3726 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3726 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3727 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3727 line_no = Column('line_no', Unicode(10), nullable=True)
3728 line_no = Column('line_no', Unicode(10), nullable=True)
3728 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3729 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3729 f_path = Column('f_path', Unicode(1000), nullable=True)
3730 f_path = Column('f_path', Unicode(1000), nullable=True)
3730 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3731 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3731 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3732 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3732 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3733 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3733 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3734 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3734 renderer = Column('renderer', Unicode(64), nullable=True)
3735 renderer = Column('renderer', Unicode(64), nullable=True)
3735 display_state = Column('display_state', Unicode(128), nullable=True)
3736 display_state = Column('display_state', Unicode(128), nullable=True)
3736 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3737 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3737
3738
3738 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3739 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3739 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3740 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3740
3741
3741 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3742 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3742 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3743 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3743
3744
3744 author = relationship('User', lazy='joined')
3745 author = relationship('User', lazy='joined')
3745 repo = relationship('Repository')
3746 repo = relationship('Repository')
3746 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3747 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3747 pull_request = relationship('PullRequest', lazy='joined')
3748 pull_request = relationship('PullRequest', lazy='joined')
3748 pull_request_version = relationship('PullRequestVersion')
3749 pull_request_version = relationship('PullRequestVersion')
3749
3750
3750 @classmethod
3751 @classmethod
3751 def get_users(cls, revision=None, pull_request_id=None):
3752 def get_users(cls, revision=None, pull_request_id=None):
3752 """
3753 """
3753 Returns user associated with this ChangesetComment. ie those
3754 Returns user associated with this ChangesetComment. ie those
3754 who actually commented
3755 who actually commented
3755
3756
3756 :param cls:
3757 :param cls:
3757 :param revision:
3758 :param revision:
3758 """
3759 """
3759 q = Session().query(User)\
3760 q = Session().query(User)\
3760 .join(ChangesetComment.author)
3761 .join(ChangesetComment.author)
3761 if revision:
3762 if revision:
3762 q = q.filter(cls.revision == revision)
3763 q = q.filter(cls.revision == revision)
3763 elif pull_request_id:
3764 elif pull_request_id:
3764 q = q.filter(cls.pull_request_id == pull_request_id)
3765 q = q.filter(cls.pull_request_id == pull_request_id)
3765 return q.all()
3766 return q.all()
3766
3767
3767 @classmethod
3768 @classmethod
3768 def get_index_from_version(cls, pr_version, versions):
3769 def get_index_from_version(cls, pr_version, versions):
3769 num_versions = [x.pull_request_version_id for x in versions]
3770 num_versions = [x.pull_request_version_id for x in versions]
3770 try:
3771 try:
3771 return num_versions.index(pr_version) +1
3772 return num_versions.index(pr_version) +1
3772 except (IndexError, ValueError):
3773 except (IndexError, ValueError):
3773 return
3774 return
3774
3775
3775 @property
3776 @property
3776 def outdated(self):
3777 def outdated(self):
3777 return self.display_state == self.COMMENT_OUTDATED
3778 return self.display_state == self.COMMENT_OUTDATED
3778
3779
3779 @property
3780 @property
3780 def immutable(self):
3781 def immutable(self):
3781 return self.immutable_state == self.OP_IMMUTABLE
3782 return self.immutable_state == self.OP_IMMUTABLE
3782
3783
3783 def outdated_at_version(self, version):
3784 def outdated_at_version(self, version):
3784 """
3785 """
3785 Checks if comment is outdated for given pull request version
3786 Checks if comment is outdated for given pull request version
3786 """
3787 """
3787 return self.outdated and self.pull_request_version_id != version
3788 return self.outdated and self.pull_request_version_id != version
3788
3789
3789 def older_than_version(self, version):
3790 def older_than_version(self, version):
3790 """
3791 """
3791 Checks if comment is made from previous version than given
3792 Checks if comment is made from previous version than given
3792 """
3793 """
3793 if version is None:
3794 if version is None:
3794 return self.pull_request_version_id is not None
3795 return self.pull_request_version_id is not None
3795
3796
3796 return self.pull_request_version_id < version
3797 return self.pull_request_version_id < version
3797
3798
3798 @property
3799 @property
3799 def resolved(self):
3800 def resolved(self):
3800 return self.resolved_by[0] if self.resolved_by else None
3801 return self.resolved_by[0] if self.resolved_by else None
3801
3802
3802 @property
3803 @property
3803 def is_todo(self):
3804 def is_todo(self):
3804 return self.comment_type == self.COMMENT_TYPE_TODO
3805 return self.comment_type == self.COMMENT_TYPE_TODO
3805
3806
3806 @property
3807 @property
3807 def is_inline(self):
3808 def is_inline(self):
3808 return self.line_no and self.f_path
3809 return self.line_no and self.f_path
3809
3810
3810 def get_index_version(self, versions):
3811 def get_index_version(self, versions):
3811 return self.get_index_from_version(
3812 return self.get_index_from_version(
3812 self.pull_request_version_id, versions)
3813 self.pull_request_version_id, versions)
3813
3814
3814 def __repr__(self):
3815 def __repr__(self):
3815 if self.comment_id:
3816 if self.comment_id:
3816 return '<DB:Comment #%s>' % self.comment_id
3817 return '<DB:Comment #%s>' % self.comment_id
3817 else:
3818 else:
3818 return '<DB:Comment at %#x>' % id(self)
3819 return '<DB:Comment at %#x>' % id(self)
3819
3820
3820 def get_api_data(self):
3821 def get_api_data(self):
3821 comment = self
3822 comment = self
3822 data = {
3823 data = {
3823 'comment_id': comment.comment_id,
3824 'comment_id': comment.comment_id,
3824 'comment_type': comment.comment_type,
3825 'comment_type': comment.comment_type,
3825 'comment_text': comment.text,
3826 'comment_text': comment.text,
3826 'comment_status': comment.status_change,
3827 'comment_status': comment.status_change,
3827 'comment_f_path': comment.f_path,
3828 'comment_f_path': comment.f_path,
3828 'comment_lineno': comment.line_no,
3829 'comment_lineno': comment.line_no,
3829 'comment_author': comment.author,
3830 'comment_author': comment.author,
3830 'comment_created_on': comment.created_on,
3831 'comment_created_on': comment.created_on,
3831 'comment_resolved_by': self.resolved,
3832 'comment_resolved_by': self.resolved,
3832 'comment_commit_id': comment.revision,
3833 'comment_commit_id': comment.revision,
3833 'comment_pull_request_id': comment.pull_request_id,
3834 'comment_pull_request_id': comment.pull_request_id,
3834 }
3835 }
3835 return data
3836 return data
3836
3837
3837 def __json__(self):
3838 def __json__(self):
3838 data = dict()
3839 data = dict()
3839 data.update(self.get_api_data())
3840 data.update(self.get_api_data())
3840 return data
3841 return data
3841
3842
3842
3843
3843 class ChangesetStatus(Base, BaseModel):
3844 class ChangesetStatus(Base, BaseModel):
3844 __tablename__ = 'changeset_statuses'
3845 __tablename__ = 'changeset_statuses'
3845 __table_args__ = (
3846 __table_args__ = (
3846 Index('cs_revision_idx', 'revision'),
3847 Index('cs_revision_idx', 'revision'),
3847 Index('cs_version_idx', 'version'),
3848 Index('cs_version_idx', 'version'),
3848 UniqueConstraint('repo_id', 'revision', 'version'),
3849 UniqueConstraint('repo_id', 'revision', 'version'),
3849 base_table_args
3850 base_table_args
3850 )
3851 )
3851
3852
3852 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3853 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3853 STATUS_APPROVED = 'approved'
3854 STATUS_APPROVED = 'approved'
3854 STATUS_REJECTED = 'rejected'
3855 STATUS_REJECTED = 'rejected'
3855 STATUS_UNDER_REVIEW = 'under_review'
3856 STATUS_UNDER_REVIEW = 'under_review'
3856
3857
3857 STATUSES = [
3858 STATUSES = [
3858 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3859 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3859 (STATUS_APPROVED, _("Approved")),
3860 (STATUS_APPROVED, _("Approved")),
3860 (STATUS_REJECTED, _("Rejected")),
3861 (STATUS_REJECTED, _("Rejected")),
3861 (STATUS_UNDER_REVIEW, _("Under Review")),
3862 (STATUS_UNDER_REVIEW, _("Under Review")),
3862 ]
3863 ]
3863
3864
3864 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3865 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3865 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3866 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3866 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3867 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3867 revision = Column('revision', String(40), nullable=False)
3868 revision = Column('revision', String(40), nullable=False)
3868 status = Column('status', String(128), nullable=False, default=DEFAULT)
3869 status = Column('status', String(128), nullable=False, default=DEFAULT)
3869 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3870 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3870 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3871 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3871 version = Column('version', Integer(), nullable=False, default=0)
3872 version = Column('version', Integer(), nullable=False, default=0)
3872 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3873 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3873
3874
3874 author = relationship('User', lazy='joined')
3875 author = relationship('User', lazy='joined')
3875 repo = relationship('Repository')
3876 repo = relationship('Repository')
3876 comment = relationship('ChangesetComment', lazy='joined')
3877 comment = relationship('ChangesetComment', lazy='joined')
3877 pull_request = relationship('PullRequest', lazy='joined')
3878 pull_request = relationship('PullRequest', lazy='joined')
3878
3879
3879 def __unicode__(self):
3880 def __unicode__(self):
3880 return u"<%s('%s[v%s]:%s')>" % (
3881 return u"<%s('%s[v%s]:%s')>" % (
3881 self.__class__.__name__,
3882 self.__class__.__name__,
3882 self.status, self.version, self.author
3883 self.status, self.version, self.author
3883 )
3884 )
3884
3885
3885 @classmethod
3886 @classmethod
3886 def get_status_lbl(cls, value):
3887 def get_status_lbl(cls, value):
3887 return dict(cls.STATUSES).get(value)
3888 return dict(cls.STATUSES).get(value)
3888
3889
3889 @property
3890 @property
3890 def status_lbl(self):
3891 def status_lbl(self):
3891 return ChangesetStatus.get_status_lbl(self.status)
3892 return ChangesetStatus.get_status_lbl(self.status)
3892
3893
3893 def get_api_data(self):
3894 def get_api_data(self):
3894 status = self
3895 status = self
3895 data = {
3896 data = {
3896 'status_id': status.changeset_status_id,
3897 'status_id': status.changeset_status_id,
3897 'status': status.status,
3898 'status': status.status,
3898 }
3899 }
3899 return data
3900 return data
3900
3901
3901 def __json__(self):
3902 def __json__(self):
3902 data = dict()
3903 data = dict()
3903 data.update(self.get_api_data())
3904 data.update(self.get_api_data())
3904 return data
3905 return data
3905
3906
3906
3907
3907 class _SetState(object):
3908 class _SetState(object):
3908 """
3909 """
3909 Context processor allowing changing state for sensitive operation such as
3910 Context processor allowing changing state for sensitive operation such as
3910 pull request update or merge
3911 pull request update or merge
3911 """
3912 """
3912
3913
3913 def __init__(self, pull_request, pr_state, back_state=None):
3914 def __init__(self, pull_request, pr_state, back_state=None):
3914 self._pr = pull_request
3915 self._pr = pull_request
3915 self._org_state = back_state or pull_request.pull_request_state
3916 self._org_state = back_state or pull_request.pull_request_state
3916 self._pr_state = pr_state
3917 self._pr_state = pr_state
3917 self._current_state = None
3918 self._current_state = None
3918
3919
3919 def __enter__(self):
3920 def __enter__(self):
3920 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
3921 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
3921 self._pr, self._pr_state)
3922 self._pr, self._pr_state)
3922 self.set_pr_state(self._pr_state)
3923 self.set_pr_state(self._pr_state)
3923 return self
3924 return self
3924
3925
3925 def __exit__(self, exc_type, exc_val, exc_tb):
3926 def __exit__(self, exc_type, exc_val, exc_tb):
3926 if exc_val is not None:
3927 if exc_val is not None:
3927 log.error(traceback.format_exc(exc_tb))
3928 log.error(traceback.format_exc(exc_tb))
3928 return None
3929 return None
3929
3930
3930 self.set_pr_state(self._org_state)
3931 self.set_pr_state(self._org_state)
3931 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
3932 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
3932 self._pr, self._org_state)
3933 self._pr, self._org_state)
3933
3934
3934 @property
3935 @property
3935 def state(self):
3936 def state(self):
3936 return self._current_state
3937 return self._current_state
3937
3938
3938 def set_pr_state(self, pr_state):
3939 def set_pr_state(self, pr_state):
3939 try:
3940 try:
3940 self._pr.pull_request_state = pr_state
3941 self._pr.pull_request_state = pr_state
3941 Session().add(self._pr)
3942 Session().add(self._pr)
3942 Session().commit()
3943 Session().commit()
3943 self._current_state = pr_state
3944 self._current_state = pr_state
3944 except Exception:
3945 except Exception:
3945 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
3946 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
3946 raise
3947 raise
3947
3948
3948
3949
3949 class _PullRequestBase(BaseModel):
3950 class _PullRequestBase(BaseModel):
3950 """
3951 """
3951 Common attributes of pull request and version entries.
3952 Common attributes of pull request and version entries.
3952 """
3953 """
3953
3954
3954 # .status values
3955 # .status values
3955 STATUS_NEW = u'new'
3956 STATUS_NEW = u'new'
3956 STATUS_OPEN = u'open'
3957 STATUS_OPEN = u'open'
3957 STATUS_CLOSED = u'closed'
3958 STATUS_CLOSED = u'closed'
3958
3959
3959 # available states
3960 # available states
3960 STATE_CREATING = u'creating'
3961 STATE_CREATING = u'creating'
3961 STATE_UPDATING = u'updating'
3962 STATE_UPDATING = u'updating'
3962 STATE_MERGING = u'merging'
3963 STATE_MERGING = u'merging'
3963 STATE_CREATED = u'created'
3964 STATE_CREATED = u'created'
3964
3965
3965 title = Column('title', Unicode(255), nullable=True)
3966 title = Column('title', Unicode(255), nullable=True)
3966 description = Column(
3967 description = Column(
3967 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3968 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3968 nullable=True)
3969 nullable=True)
3969 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
3970 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
3970
3971
3971 # new/open/closed status of pull request (not approve/reject/etc)
3972 # new/open/closed status of pull request (not approve/reject/etc)
3972 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3973 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3973 created_on = Column(
3974 created_on = Column(
3974 'created_on', DateTime(timezone=False), nullable=False,
3975 'created_on', DateTime(timezone=False), nullable=False,
3975 default=datetime.datetime.now)
3976 default=datetime.datetime.now)
3976 updated_on = Column(
3977 updated_on = Column(
3977 'updated_on', DateTime(timezone=False), nullable=False,
3978 'updated_on', DateTime(timezone=False), nullable=False,
3978 default=datetime.datetime.now)
3979 default=datetime.datetime.now)
3979
3980
3980 pull_request_state = Column("pull_request_state", String(255), nullable=True)
3981 pull_request_state = Column("pull_request_state", String(255), nullable=True)
3981
3982
3982 @declared_attr
3983 @declared_attr
3983 def user_id(cls):
3984 def user_id(cls):
3984 return Column(
3985 return Column(
3985 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3986 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3986 unique=None)
3987 unique=None)
3987
3988
3988 # 500 revisions max
3989 # 500 revisions max
3989 _revisions = Column(
3990 _revisions = Column(
3990 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3991 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3991
3992
3992 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
3993 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
3993
3994
3994 @declared_attr
3995 @declared_attr
3995 def source_repo_id(cls):
3996 def source_repo_id(cls):
3996 # TODO: dan: rename column to source_repo_id
3997 # TODO: dan: rename column to source_repo_id
3997 return Column(
3998 return Column(
3998 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3999 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3999 nullable=False)
4000 nullable=False)
4000
4001
4001 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4002 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4002
4003
4003 @hybrid_property
4004 @hybrid_property
4004 def source_ref(self):
4005 def source_ref(self):
4005 return self._source_ref
4006 return self._source_ref
4006
4007
4007 @source_ref.setter
4008 @source_ref.setter
4008 def source_ref(self, val):
4009 def source_ref(self, val):
4009 parts = (val or '').split(':')
4010 parts = (val or '').split(':')
4010 if len(parts) != 3:
4011 if len(parts) != 3:
4011 raise ValueError(
4012 raise ValueError(
4012 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4013 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4013 self._source_ref = safe_unicode(val)
4014 self._source_ref = safe_unicode(val)
4014
4015
4015 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4016 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4016
4017
4017 @hybrid_property
4018 @hybrid_property
4018 def target_ref(self):
4019 def target_ref(self):
4019 return self._target_ref
4020 return self._target_ref
4020
4021
4021 @target_ref.setter
4022 @target_ref.setter
4022 def target_ref(self, val):
4023 def target_ref(self, val):
4023 parts = (val or '').split(':')
4024 parts = (val or '').split(':')
4024 if len(parts) != 3:
4025 if len(parts) != 3:
4025 raise ValueError(
4026 raise ValueError(
4026 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4027 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4027 self._target_ref = safe_unicode(val)
4028 self._target_ref = safe_unicode(val)
4028
4029
4029 @declared_attr
4030 @declared_attr
4030 def target_repo_id(cls):
4031 def target_repo_id(cls):
4031 # TODO: dan: rename column to target_repo_id
4032 # TODO: dan: rename column to target_repo_id
4032 return Column(
4033 return Column(
4033 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4034 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4034 nullable=False)
4035 nullable=False)
4035
4036
4036 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4037 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4037
4038
4038 # TODO: dan: rename column to last_merge_source_rev
4039 # TODO: dan: rename column to last_merge_source_rev
4039 _last_merge_source_rev = Column(
4040 _last_merge_source_rev = Column(
4040 'last_merge_org_rev', String(40), nullable=True)
4041 'last_merge_org_rev', String(40), nullable=True)
4041 # TODO: dan: rename column to last_merge_target_rev
4042 # TODO: dan: rename column to last_merge_target_rev
4042 _last_merge_target_rev = Column(
4043 _last_merge_target_rev = Column(
4043 'last_merge_other_rev', String(40), nullable=True)
4044 'last_merge_other_rev', String(40), nullable=True)
4044 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4045 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4045 last_merge_metadata = Column(
4046 last_merge_metadata = Column(
4046 'last_merge_metadata', MutationObj.as_mutable(
4047 'last_merge_metadata', MutationObj.as_mutable(
4047 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4048 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4048
4049
4049 merge_rev = Column('merge_rev', String(40), nullable=True)
4050 merge_rev = Column('merge_rev', String(40), nullable=True)
4050
4051
4051 reviewer_data = Column(
4052 reviewer_data = Column(
4052 'reviewer_data_json', MutationObj.as_mutable(
4053 'reviewer_data_json', MutationObj.as_mutable(
4053 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4054 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4054
4055
4055 @property
4056 @property
4056 def reviewer_data_json(self):
4057 def reviewer_data_json(self):
4057 return json.dumps(self.reviewer_data)
4058 return json.dumps(self.reviewer_data)
4058
4059
4059 @property
4060 @property
4060 def work_in_progress(self):
4061 def work_in_progress(self):
4061 """checks if pull request is work in progress by checking the title"""
4062 """checks if pull request is work in progress by checking the title"""
4062 title = self.title.upper()
4063 title = self.title.upper()
4063 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4064 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4064 return True
4065 return True
4065 return False
4066 return False
4066
4067
4067 @hybrid_property
4068 @hybrid_property
4068 def description_safe(self):
4069 def description_safe(self):
4069 from rhodecode.lib import helpers as h
4070 from rhodecode.lib import helpers as h
4070 return h.escape(self.description)
4071 return h.escape(self.description)
4071
4072
4072 @hybrid_property
4073 @hybrid_property
4073 def revisions(self):
4074 def revisions(self):
4074 return self._revisions.split(':') if self._revisions else []
4075 return self._revisions.split(':') if self._revisions else []
4075
4076
4076 @revisions.setter
4077 @revisions.setter
4077 def revisions(self, val):
4078 def revisions(self, val):
4078 self._revisions = u':'.join(val)
4079 self._revisions = u':'.join(val)
4079
4080
4080 @hybrid_property
4081 @hybrid_property
4081 def last_merge_status(self):
4082 def last_merge_status(self):
4082 return safe_int(self._last_merge_status)
4083 return safe_int(self._last_merge_status)
4083
4084
4084 @last_merge_status.setter
4085 @last_merge_status.setter
4085 def last_merge_status(self, val):
4086 def last_merge_status(self, val):
4086 self._last_merge_status = val
4087 self._last_merge_status = val
4087
4088
4088 @declared_attr
4089 @declared_attr
4089 def author(cls):
4090 def author(cls):
4090 return relationship('User', lazy='joined')
4091 return relationship('User', lazy='joined')
4091
4092
4092 @declared_attr
4093 @declared_attr
4093 def source_repo(cls):
4094 def source_repo(cls):
4094 return relationship(
4095 return relationship(
4095 'Repository',
4096 'Repository',
4096 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4097 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4097
4098
4098 @property
4099 @property
4099 def source_ref_parts(self):
4100 def source_ref_parts(self):
4100 return self.unicode_to_reference(self.source_ref)
4101 return self.unicode_to_reference(self.source_ref)
4101
4102
4102 @declared_attr
4103 @declared_attr
4103 def target_repo(cls):
4104 def target_repo(cls):
4104 return relationship(
4105 return relationship(
4105 'Repository',
4106 'Repository',
4106 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4107 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4107
4108
4108 @property
4109 @property
4109 def target_ref_parts(self):
4110 def target_ref_parts(self):
4110 return self.unicode_to_reference(self.target_ref)
4111 return self.unicode_to_reference(self.target_ref)
4111
4112
4112 @property
4113 @property
4113 def shadow_merge_ref(self):
4114 def shadow_merge_ref(self):
4114 return self.unicode_to_reference(self._shadow_merge_ref)
4115 return self.unicode_to_reference(self._shadow_merge_ref)
4115
4116
4116 @shadow_merge_ref.setter
4117 @shadow_merge_ref.setter
4117 def shadow_merge_ref(self, ref):
4118 def shadow_merge_ref(self, ref):
4118 self._shadow_merge_ref = self.reference_to_unicode(ref)
4119 self._shadow_merge_ref = self.reference_to_unicode(ref)
4119
4120
4120 @staticmethod
4121 @staticmethod
4121 def unicode_to_reference(raw):
4122 def unicode_to_reference(raw):
4122 """
4123 """
4123 Convert a unicode (or string) to a reference object.
4124 Convert a unicode (or string) to a reference object.
4124 If unicode evaluates to False it returns None.
4125 If unicode evaluates to False it returns None.
4125 """
4126 """
4126 if raw:
4127 if raw:
4127 refs = raw.split(':')
4128 refs = raw.split(':')
4128 return Reference(*refs)
4129 return Reference(*refs)
4129 else:
4130 else:
4130 return None
4131 return None
4131
4132
4132 @staticmethod
4133 @staticmethod
4133 def reference_to_unicode(ref):
4134 def reference_to_unicode(ref):
4134 """
4135 """
4135 Convert a reference object to unicode.
4136 Convert a reference object to unicode.
4136 If reference is None it returns None.
4137 If reference is None it returns None.
4137 """
4138 """
4138 if ref:
4139 if ref:
4139 return u':'.join(ref)
4140 return u':'.join(ref)
4140 else:
4141 else:
4141 return None
4142 return None
4142
4143
4143 def get_api_data(self, with_merge_state=True):
4144 def get_api_data(self, with_merge_state=True):
4144 from rhodecode.model.pull_request import PullRequestModel
4145 from rhodecode.model.pull_request import PullRequestModel
4145
4146
4146 pull_request = self
4147 pull_request = self
4147 if with_merge_state:
4148 if with_merge_state:
4148 merge_response, merge_status, msg = \
4149 merge_response, merge_status, msg = \
4149 PullRequestModel().merge_status(pull_request)
4150 PullRequestModel().merge_status(pull_request)
4150 merge_state = {
4151 merge_state = {
4151 'status': merge_status,
4152 'status': merge_status,
4152 'message': safe_unicode(msg),
4153 'message': safe_unicode(msg),
4153 }
4154 }
4154 else:
4155 else:
4155 merge_state = {'status': 'not_available',
4156 merge_state = {'status': 'not_available',
4156 'message': 'not_available'}
4157 'message': 'not_available'}
4157
4158
4158 merge_data = {
4159 merge_data = {
4159 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4160 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4160 'reference': (
4161 'reference': (
4161 pull_request.shadow_merge_ref._asdict()
4162 pull_request.shadow_merge_ref._asdict()
4162 if pull_request.shadow_merge_ref else None),
4163 if pull_request.shadow_merge_ref else None),
4163 }
4164 }
4164
4165
4165 data = {
4166 data = {
4166 'pull_request_id': pull_request.pull_request_id,
4167 'pull_request_id': pull_request.pull_request_id,
4167 'url': PullRequestModel().get_url(pull_request),
4168 'url': PullRequestModel().get_url(pull_request),
4168 'title': pull_request.title,
4169 'title': pull_request.title,
4169 'description': pull_request.description,
4170 'description': pull_request.description,
4170 'status': pull_request.status,
4171 'status': pull_request.status,
4171 'state': pull_request.pull_request_state,
4172 'state': pull_request.pull_request_state,
4172 'created_on': pull_request.created_on,
4173 'created_on': pull_request.created_on,
4173 'updated_on': pull_request.updated_on,
4174 'updated_on': pull_request.updated_on,
4174 'commit_ids': pull_request.revisions,
4175 'commit_ids': pull_request.revisions,
4175 'review_status': pull_request.calculated_review_status(),
4176 'review_status': pull_request.calculated_review_status(),
4176 'mergeable': merge_state,
4177 'mergeable': merge_state,
4177 'source': {
4178 'source': {
4178 'clone_url': pull_request.source_repo.clone_url(),
4179 'clone_url': pull_request.source_repo.clone_url(),
4179 'repository': pull_request.source_repo.repo_name,
4180 'repository': pull_request.source_repo.repo_name,
4180 'reference': {
4181 'reference': {
4181 'name': pull_request.source_ref_parts.name,
4182 'name': pull_request.source_ref_parts.name,
4182 'type': pull_request.source_ref_parts.type,
4183 'type': pull_request.source_ref_parts.type,
4183 'commit_id': pull_request.source_ref_parts.commit_id,
4184 'commit_id': pull_request.source_ref_parts.commit_id,
4184 },
4185 },
4185 },
4186 },
4186 'target': {
4187 'target': {
4187 'clone_url': pull_request.target_repo.clone_url(),
4188 'clone_url': pull_request.target_repo.clone_url(),
4188 'repository': pull_request.target_repo.repo_name,
4189 'repository': pull_request.target_repo.repo_name,
4189 'reference': {
4190 'reference': {
4190 'name': pull_request.target_ref_parts.name,
4191 'name': pull_request.target_ref_parts.name,
4191 'type': pull_request.target_ref_parts.type,
4192 'type': pull_request.target_ref_parts.type,
4192 'commit_id': pull_request.target_ref_parts.commit_id,
4193 'commit_id': pull_request.target_ref_parts.commit_id,
4193 },
4194 },
4194 },
4195 },
4195 'merge': merge_data,
4196 'merge': merge_data,
4196 'author': pull_request.author.get_api_data(include_secrets=False,
4197 'author': pull_request.author.get_api_data(include_secrets=False,
4197 details='basic'),
4198 details='basic'),
4198 'reviewers': [
4199 'reviewers': [
4199 {
4200 {
4200 'user': reviewer.get_api_data(include_secrets=False,
4201 'user': reviewer.get_api_data(include_secrets=False,
4201 details='basic'),
4202 details='basic'),
4202 'reasons': reasons,
4203 'reasons': reasons,
4203 'review_status': st[0][1].status if st else 'not_reviewed',
4204 'review_status': st[0][1].status if st else 'not_reviewed',
4204 }
4205 }
4205 for obj, reviewer, reasons, mandatory, st in
4206 for obj, reviewer, reasons, mandatory, st in
4206 pull_request.reviewers_statuses()
4207 pull_request.reviewers_statuses()
4207 ]
4208 ]
4208 }
4209 }
4209
4210
4210 return data
4211 return data
4211
4212
4212 def set_state(self, pull_request_state, final_state=None):
4213 def set_state(self, pull_request_state, final_state=None):
4213 """
4214 """
4214 # goes from initial state to updating to initial state.
4215 # goes from initial state to updating to initial state.
4215 # initial state can be changed by specifying back_state=
4216 # initial state can be changed by specifying back_state=
4216 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4217 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4217 pull_request.merge()
4218 pull_request.merge()
4218
4219
4219 :param pull_request_state:
4220 :param pull_request_state:
4220 :param final_state:
4221 :param final_state:
4221
4222
4222 """
4223 """
4223
4224
4224 return _SetState(self, pull_request_state, back_state=final_state)
4225 return _SetState(self, pull_request_state, back_state=final_state)
4225
4226
4226
4227
4227 class PullRequest(Base, _PullRequestBase):
4228 class PullRequest(Base, _PullRequestBase):
4228 __tablename__ = 'pull_requests'
4229 __tablename__ = 'pull_requests'
4229 __table_args__ = (
4230 __table_args__ = (
4230 base_table_args,
4231 base_table_args,
4231 )
4232 )
4232
4233
4233 pull_request_id = Column(
4234 pull_request_id = Column(
4234 'pull_request_id', Integer(), nullable=False, primary_key=True)
4235 'pull_request_id', Integer(), nullable=False, primary_key=True)
4235
4236
4236 def __repr__(self):
4237 def __repr__(self):
4237 if self.pull_request_id:
4238 if self.pull_request_id:
4238 return '<DB:PullRequest #%s>' % self.pull_request_id
4239 return '<DB:PullRequest #%s>' % self.pull_request_id
4239 else:
4240 else:
4240 return '<DB:PullRequest at %#x>' % id(self)
4241 return '<DB:PullRequest at %#x>' % id(self)
4241
4242
4242 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4243 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4243 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4244 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4244 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4245 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4245 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4246 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4246 lazy='dynamic')
4247 lazy='dynamic')
4247
4248
4248 @classmethod
4249 @classmethod
4249 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4250 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4250 internal_methods=None):
4251 internal_methods=None):
4251
4252
4252 class PullRequestDisplay(object):
4253 class PullRequestDisplay(object):
4253 """
4254 """
4254 Special object wrapper for showing PullRequest data via Versions
4255 Special object wrapper for showing PullRequest data via Versions
4255 It mimics PR object as close as possible. This is read only object
4256 It mimics PR object as close as possible. This is read only object
4256 just for display
4257 just for display
4257 """
4258 """
4258
4259
4259 def __init__(self, attrs, internal=None):
4260 def __init__(self, attrs, internal=None):
4260 self.attrs = attrs
4261 self.attrs = attrs
4261 # internal have priority over the given ones via attrs
4262 # internal have priority over the given ones via attrs
4262 self.internal = internal or ['versions']
4263 self.internal = internal or ['versions']
4263
4264
4264 def __getattr__(self, item):
4265 def __getattr__(self, item):
4265 if item in self.internal:
4266 if item in self.internal:
4266 return getattr(self, item)
4267 return getattr(self, item)
4267 try:
4268 try:
4268 return self.attrs[item]
4269 return self.attrs[item]
4269 except KeyError:
4270 except KeyError:
4270 raise AttributeError(
4271 raise AttributeError(
4271 '%s object has no attribute %s' % (self, item))
4272 '%s object has no attribute %s' % (self, item))
4272
4273
4273 def __repr__(self):
4274 def __repr__(self):
4274 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4275 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4275
4276
4276 def versions(self):
4277 def versions(self):
4277 return pull_request_obj.versions.order_by(
4278 return pull_request_obj.versions.order_by(
4278 PullRequestVersion.pull_request_version_id).all()
4279 PullRequestVersion.pull_request_version_id).all()
4279
4280
4280 def is_closed(self):
4281 def is_closed(self):
4281 return pull_request_obj.is_closed()
4282 return pull_request_obj.is_closed()
4282
4283
4283 def is_state_changing(self):
4284 def is_state_changing(self):
4284 return pull_request_obj.is_state_changing()
4285 return pull_request_obj.is_state_changing()
4285
4286
4286 @property
4287 @property
4287 def pull_request_version_id(self):
4288 def pull_request_version_id(self):
4288 return getattr(pull_request_obj, 'pull_request_version_id', None)
4289 return getattr(pull_request_obj, 'pull_request_version_id', None)
4289
4290
4290 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4291 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4291
4292
4292 attrs.author = StrictAttributeDict(
4293 attrs.author = StrictAttributeDict(
4293 pull_request_obj.author.get_api_data())
4294 pull_request_obj.author.get_api_data())
4294 if pull_request_obj.target_repo:
4295 if pull_request_obj.target_repo:
4295 attrs.target_repo = StrictAttributeDict(
4296 attrs.target_repo = StrictAttributeDict(
4296 pull_request_obj.target_repo.get_api_data())
4297 pull_request_obj.target_repo.get_api_data())
4297 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4298 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4298
4299
4299 if pull_request_obj.source_repo:
4300 if pull_request_obj.source_repo:
4300 attrs.source_repo = StrictAttributeDict(
4301 attrs.source_repo = StrictAttributeDict(
4301 pull_request_obj.source_repo.get_api_data())
4302 pull_request_obj.source_repo.get_api_data())
4302 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4303 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4303
4304
4304 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4305 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4305 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4306 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4306 attrs.revisions = pull_request_obj.revisions
4307 attrs.revisions = pull_request_obj.revisions
4307 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4308 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4308 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4309 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4309 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4310 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4310 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4311 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4311
4312
4312 return PullRequestDisplay(attrs, internal=internal_methods)
4313 return PullRequestDisplay(attrs, internal=internal_methods)
4313
4314
4314 def is_closed(self):
4315 def is_closed(self):
4315 return self.status == self.STATUS_CLOSED
4316 return self.status == self.STATUS_CLOSED
4316
4317
4317 def is_state_changing(self):
4318 def is_state_changing(self):
4318 return self.pull_request_state != PullRequest.STATE_CREATED
4319 return self.pull_request_state != PullRequest.STATE_CREATED
4319
4320
4320 def __json__(self):
4321 def __json__(self):
4321 return {
4322 return {
4322 'revisions': self.revisions,
4323 'revisions': self.revisions,
4323 'versions': self.versions_count
4324 'versions': self.versions_count
4324 }
4325 }
4325
4326
4326 def calculated_review_status(self):
4327 def calculated_review_status(self):
4327 from rhodecode.model.changeset_status import ChangesetStatusModel
4328 from rhodecode.model.changeset_status import ChangesetStatusModel
4328 return ChangesetStatusModel().calculated_review_status(self)
4329 return ChangesetStatusModel().calculated_review_status(self)
4329
4330
4330 def reviewers_statuses(self):
4331 def reviewers_statuses(self):
4331 from rhodecode.model.changeset_status import ChangesetStatusModel
4332 from rhodecode.model.changeset_status import ChangesetStatusModel
4332 return ChangesetStatusModel().reviewers_statuses(self)
4333 return ChangesetStatusModel().reviewers_statuses(self)
4333
4334
4334 @property
4335 @property
4335 def workspace_id(self):
4336 def workspace_id(self):
4336 from rhodecode.model.pull_request import PullRequestModel
4337 from rhodecode.model.pull_request import PullRequestModel
4337 return PullRequestModel()._workspace_id(self)
4338 return PullRequestModel()._workspace_id(self)
4338
4339
4339 def get_shadow_repo(self):
4340 def get_shadow_repo(self):
4340 workspace_id = self.workspace_id
4341 workspace_id = self.workspace_id
4341 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4342 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4342 if os.path.isdir(shadow_repository_path):
4343 if os.path.isdir(shadow_repository_path):
4343 vcs_obj = self.target_repo.scm_instance()
4344 vcs_obj = self.target_repo.scm_instance()
4344 return vcs_obj.get_shadow_instance(shadow_repository_path)
4345 return vcs_obj.get_shadow_instance(shadow_repository_path)
4345
4346
4346 @property
4347 @property
4347 def versions_count(self):
4348 def versions_count(self):
4348 """
4349 """
4349 return number of versions this PR have, e.g a PR that once been
4350 return number of versions this PR have, e.g a PR that once been
4350 updated will have 2 versions
4351 updated will have 2 versions
4351 """
4352 """
4352 return self.versions.count() + 1
4353 return self.versions.count() + 1
4353
4354
4354
4355
4355 class PullRequestVersion(Base, _PullRequestBase):
4356 class PullRequestVersion(Base, _PullRequestBase):
4356 __tablename__ = 'pull_request_versions'
4357 __tablename__ = 'pull_request_versions'
4357 __table_args__ = (
4358 __table_args__ = (
4358 base_table_args,
4359 base_table_args,
4359 )
4360 )
4360
4361
4361 pull_request_version_id = Column(
4362 pull_request_version_id = Column(
4362 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4363 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4363 pull_request_id = Column(
4364 pull_request_id = Column(
4364 'pull_request_id', Integer(),
4365 'pull_request_id', Integer(),
4365 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4366 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4366 pull_request = relationship('PullRequest')
4367 pull_request = relationship('PullRequest')
4367
4368
4368 def __repr__(self):
4369 def __repr__(self):
4369 if self.pull_request_version_id:
4370 if self.pull_request_version_id:
4370 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4371 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4371 else:
4372 else:
4372 return '<DB:PullRequestVersion at %#x>' % id(self)
4373 return '<DB:PullRequestVersion at %#x>' % id(self)
4373
4374
4374 @property
4375 @property
4375 def reviewers(self):
4376 def reviewers(self):
4376 return self.pull_request.reviewers
4377 return self.pull_request.reviewers
4377
4378
4378 @property
4379 @property
4379 def versions(self):
4380 def versions(self):
4380 return self.pull_request.versions
4381 return self.pull_request.versions
4381
4382
4382 def is_closed(self):
4383 def is_closed(self):
4383 # calculate from original
4384 # calculate from original
4384 return self.pull_request.status == self.STATUS_CLOSED
4385 return self.pull_request.status == self.STATUS_CLOSED
4385
4386
4386 def is_state_changing(self):
4387 def is_state_changing(self):
4387 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4388 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4388
4389
4389 def calculated_review_status(self):
4390 def calculated_review_status(self):
4390 return self.pull_request.calculated_review_status()
4391 return self.pull_request.calculated_review_status()
4391
4392
4392 def reviewers_statuses(self):
4393 def reviewers_statuses(self):
4393 return self.pull_request.reviewers_statuses()
4394 return self.pull_request.reviewers_statuses()
4394
4395
4395
4396
4396 class PullRequestReviewers(Base, BaseModel):
4397 class PullRequestReviewers(Base, BaseModel):
4397 __tablename__ = 'pull_request_reviewers'
4398 __tablename__ = 'pull_request_reviewers'
4398 __table_args__ = (
4399 __table_args__ = (
4399 base_table_args,
4400 base_table_args,
4400 )
4401 )
4401
4402
4402 @hybrid_property
4403 @hybrid_property
4403 def reasons(self):
4404 def reasons(self):
4404 if not self._reasons:
4405 if not self._reasons:
4405 return []
4406 return []
4406 return self._reasons
4407 return self._reasons
4407
4408
4408 @reasons.setter
4409 @reasons.setter
4409 def reasons(self, val):
4410 def reasons(self, val):
4410 val = val or []
4411 val = val or []
4411 if any(not isinstance(x, compat.string_types) for x in val):
4412 if any(not isinstance(x, compat.string_types) for x in val):
4412 raise Exception('invalid reasons type, must be list of strings')
4413 raise Exception('invalid reasons type, must be list of strings')
4413 self._reasons = val
4414 self._reasons = val
4414
4415
4415 pull_requests_reviewers_id = Column(
4416 pull_requests_reviewers_id = Column(
4416 'pull_requests_reviewers_id', Integer(), nullable=False,
4417 'pull_requests_reviewers_id', Integer(), nullable=False,
4417 primary_key=True)
4418 primary_key=True)
4418 pull_request_id = Column(
4419 pull_request_id = Column(
4419 "pull_request_id", Integer(),
4420 "pull_request_id", Integer(),
4420 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4421 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4421 user_id = Column(
4422 user_id = Column(
4422 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4423 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4423 _reasons = Column(
4424 _reasons = Column(
4424 'reason', MutationList.as_mutable(
4425 'reason', MutationList.as_mutable(
4425 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4426 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4426
4427
4427 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4428 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4428 user = relationship('User')
4429 user = relationship('User')
4429 pull_request = relationship('PullRequest')
4430 pull_request = relationship('PullRequest')
4430
4431
4431 rule_data = Column(
4432 rule_data = Column(
4432 'rule_data_json',
4433 'rule_data_json',
4433 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4434 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4434
4435
4435 def rule_user_group_data(self):
4436 def rule_user_group_data(self):
4436 """
4437 """
4437 Returns the voting user group rule data for this reviewer
4438 Returns the voting user group rule data for this reviewer
4438 """
4439 """
4439
4440
4440 if self.rule_data and 'vote_rule' in self.rule_data:
4441 if self.rule_data and 'vote_rule' in self.rule_data:
4441 user_group_data = {}
4442 user_group_data = {}
4442 if 'rule_user_group_entry_id' in self.rule_data:
4443 if 'rule_user_group_entry_id' in self.rule_data:
4443 # means a group with voting rules !
4444 # means a group with voting rules !
4444 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4445 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4445 user_group_data['name'] = self.rule_data['rule_name']
4446 user_group_data['name'] = self.rule_data['rule_name']
4446 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4447 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4447
4448
4448 return user_group_data
4449 return user_group_data
4449
4450
4450 def __unicode__(self):
4451 def __unicode__(self):
4451 return u"<%s('id:%s')>" % (self.__class__.__name__,
4452 return u"<%s('id:%s')>" % (self.__class__.__name__,
4452 self.pull_requests_reviewers_id)
4453 self.pull_requests_reviewers_id)
4453
4454
4454
4455
4455 class Notification(Base, BaseModel):
4456 class Notification(Base, BaseModel):
4456 __tablename__ = 'notifications'
4457 __tablename__ = 'notifications'
4457 __table_args__ = (
4458 __table_args__ = (
4458 Index('notification_type_idx', 'type'),
4459 Index('notification_type_idx', 'type'),
4459 base_table_args,
4460 base_table_args,
4460 )
4461 )
4461
4462
4462 TYPE_CHANGESET_COMMENT = u'cs_comment'
4463 TYPE_CHANGESET_COMMENT = u'cs_comment'
4463 TYPE_MESSAGE = u'message'
4464 TYPE_MESSAGE = u'message'
4464 TYPE_MENTION = u'mention'
4465 TYPE_MENTION = u'mention'
4465 TYPE_REGISTRATION = u'registration'
4466 TYPE_REGISTRATION = u'registration'
4466 TYPE_PULL_REQUEST = u'pull_request'
4467 TYPE_PULL_REQUEST = u'pull_request'
4467 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4468 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4468 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4469 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4469
4470
4470 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4471 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4471 subject = Column('subject', Unicode(512), nullable=True)
4472 subject = Column('subject', Unicode(512), nullable=True)
4472 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4473 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4473 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4474 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4474 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4475 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4475 type_ = Column('type', Unicode(255))
4476 type_ = Column('type', Unicode(255))
4476
4477
4477 created_by_user = relationship('User')
4478 created_by_user = relationship('User')
4478 notifications_to_users = relationship('UserNotification', lazy='joined',
4479 notifications_to_users = relationship('UserNotification', lazy='joined',
4479 cascade="all, delete-orphan")
4480 cascade="all, delete-orphan")
4480
4481
4481 @property
4482 @property
4482 def recipients(self):
4483 def recipients(self):
4483 return [x.user for x in UserNotification.query()\
4484 return [x.user for x in UserNotification.query()\
4484 .filter(UserNotification.notification == self)\
4485 .filter(UserNotification.notification == self)\
4485 .order_by(UserNotification.user_id.asc()).all()]
4486 .order_by(UserNotification.user_id.asc()).all()]
4486
4487
4487 @classmethod
4488 @classmethod
4488 def create(cls, created_by, subject, body, recipients, type_=None):
4489 def create(cls, created_by, subject, body, recipients, type_=None):
4489 if type_ is None:
4490 if type_ is None:
4490 type_ = Notification.TYPE_MESSAGE
4491 type_ = Notification.TYPE_MESSAGE
4491
4492
4492 notification = cls()
4493 notification = cls()
4493 notification.created_by_user = created_by
4494 notification.created_by_user = created_by
4494 notification.subject = subject
4495 notification.subject = subject
4495 notification.body = body
4496 notification.body = body
4496 notification.type_ = type_
4497 notification.type_ = type_
4497 notification.created_on = datetime.datetime.now()
4498 notification.created_on = datetime.datetime.now()
4498
4499
4499 # For each recipient link the created notification to his account
4500 # For each recipient link the created notification to his account
4500 for u in recipients:
4501 for u in recipients:
4501 assoc = UserNotification()
4502 assoc = UserNotification()
4502 assoc.user_id = u.user_id
4503 assoc.user_id = u.user_id
4503 assoc.notification = notification
4504 assoc.notification = notification
4504
4505
4505 # if created_by is inside recipients mark his notification
4506 # if created_by is inside recipients mark his notification
4506 # as read
4507 # as read
4507 if u.user_id == created_by.user_id:
4508 if u.user_id == created_by.user_id:
4508 assoc.read = True
4509 assoc.read = True
4509 Session().add(assoc)
4510 Session().add(assoc)
4510
4511
4511 Session().add(notification)
4512 Session().add(notification)
4512
4513
4513 return notification
4514 return notification
4514
4515
4515
4516
4516 class UserNotification(Base, BaseModel):
4517 class UserNotification(Base, BaseModel):
4517 __tablename__ = 'user_to_notification'
4518 __tablename__ = 'user_to_notification'
4518 __table_args__ = (
4519 __table_args__ = (
4519 UniqueConstraint('user_id', 'notification_id'),
4520 UniqueConstraint('user_id', 'notification_id'),
4520 base_table_args
4521 base_table_args
4521 )
4522 )
4522
4523
4523 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4524 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4524 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4525 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4525 read = Column('read', Boolean, default=False)
4526 read = Column('read', Boolean, default=False)
4526 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4527 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4527
4528
4528 user = relationship('User', lazy="joined")
4529 user = relationship('User', lazy="joined")
4529 notification = relationship('Notification', lazy="joined",
4530 notification = relationship('Notification', lazy="joined",
4530 order_by=lambda: Notification.created_on.desc(),)
4531 order_by=lambda: Notification.created_on.desc(),)
4531
4532
4532 def mark_as_read(self):
4533 def mark_as_read(self):
4533 self.read = True
4534 self.read = True
4534 Session().add(self)
4535 Session().add(self)
4535
4536
4536
4537
4537 class UserNotice(Base, BaseModel):
4538 class UserNotice(Base, BaseModel):
4538 __tablename__ = 'user_notices'
4539 __tablename__ = 'user_notices'
4539 __table_args__ = (
4540 __table_args__ = (
4540 base_table_args
4541 base_table_args
4541 )
4542 )
4542
4543
4543 NOTIFICATION_TYPE_MESSAGE = 'message'
4544 NOTIFICATION_TYPE_MESSAGE = 'message'
4544 NOTIFICATION_TYPE_NOTICE = 'notice'
4545 NOTIFICATION_TYPE_NOTICE = 'notice'
4545
4546
4546 NOTIFICATION_LEVEL_INFO = 'info'
4547 NOTIFICATION_LEVEL_INFO = 'info'
4547 NOTIFICATION_LEVEL_WARNING = 'warning'
4548 NOTIFICATION_LEVEL_WARNING = 'warning'
4548 NOTIFICATION_LEVEL_ERROR = 'error'
4549 NOTIFICATION_LEVEL_ERROR = 'error'
4549
4550
4550 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4551 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4551
4552
4552 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4553 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4553 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4554 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4554
4555
4555 notice_read = Column('notice_read', Boolean, default=False)
4556 notice_read = Column('notice_read', Boolean, default=False)
4556
4557
4557 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4558 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4558 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4559 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4559
4560
4560 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4561 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4561 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4562 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4562
4563
4563 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4564 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4564 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4565 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4565
4566
4566 @classmethod
4567 @classmethod
4567 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4568 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4568
4569
4569 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4570 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4570 cls.NOTIFICATION_LEVEL_WARNING,
4571 cls.NOTIFICATION_LEVEL_WARNING,
4571 cls.NOTIFICATION_LEVEL_INFO]:
4572 cls.NOTIFICATION_LEVEL_INFO]:
4572 return
4573 return
4573
4574
4574 from rhodecode.model.user import UserModel
4575 from rhodecode.model.user import UserModel
4575 user = UserModel().get_user(user)
4576 user = UserModel().get_user(user)
4576
4577
4577 new_notice = UserNotice()
4578 new_notice = UserNotice()
4578 if not allow_duplicate:
4579 if not allow_duplicate:
4579 existing_msg = UserNotice().query() \
4580 existing_msg = UserNotice().query() \
4580 .filter(UserNotice.user == user) \
4581 .filter(UserNotice.user == user) \
4581 .filter(UserNotice.notice_body == body) \
4582 .filter(UserNotice.notice_body == body) \
4582 .filter(UserNotice.notice_read == false()) \
4583 .filter(UserNotice.notice_read == false()) \
4583 .scalar()
4584 .scalar()
4584 if existing_msg:
4585 if existing_msg:
4585 log.warning('Ignoring duplicate notice for user %s', user)
4586 log.warning('Ignoring duplicate notice for user %s', user)
4586 return
4587 return
4587
4588
4588 new_notice.user = user
4589 new_notice.user = user
4589 new_notice.notice_subject = subject
4590 new_notice.notice_subject = subject
4590 new_notice.notice_body = body
4591 new_notice.notice_body = body
4591 new_notice.notification_level = notice_level
4592 new_notice.notification_level = notice_level
4592 Session().add(new_notice)
4593 Session().add(new_notice)
4593 Session().commit()
4594 Session().commit()
4594
4595
4595
4596
4596 class Gist(Base, BaseModel):
4597 class Gist(Base, BaseModel):
4597 __tablename__ = 'gists'
4598 __tablename__ = 'gists'
4598 __table_args__ = (
4599 __table_args__ = (
4599 Index('g_gist_access_id_idx', 'gist_access_id'),
4600 Index('g_gist_access_id_idx', 'gist_access_id'),
4600 Index('g_created_on_idx', 'created_on'),
4601 Index('g_created_on_idx', 'created_on'),
4601 base_table_args
4602 base_table_args
4602 )
4603 )
4603
4604
4604 GIST_PUBLIC = u'public'
4605 GIST_PUBLIC = u'public'
4605 GIST_PRIVATE = u'private'
4606 GIST_PRIVATE = u'private'
4606 DEFAULT_FILENAME = u'gistfile1.txt'
4607 DEFAULT_FILENAME = u'gistfile1.txt'
4607
4608
4608 ACL_LEVEL_PUBLIC = u'acl_public'
4609 ACL_LEVEL_PUBLIC = u'acl_public'
4609 ACL_LEVEL_PRIVATE = u'acl_private'
4610 ACL_LEVEL_PRIVATE = u'acl_private'
4610
4611
4611 gist_id = Column('gist_id', Integer(), primary_key=True)
4612 gist_id = Column('gist_id', Integer(), primary_key=True)
4612 gist_access_id = Column('gist_access_id', Unicode(250))
4613 gist_access_id = Column('gist_access_id', Unicode(250))
4613 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4614 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4614 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4615 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4615 gist_expires = Column('gist_expires', Float(53), nullable=False)
4616 gist_expires = Column('gist_expires', Float(53), nullable=False)
4616 gist_type = Column('gist_type', Unicode(128), nullable=False)
4617 gist_type = Column('gist_type', Unicode(128), nullable=False)
4617 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4618 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4618 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4619 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4619 acl_level = Column('acl_level', Unicode(128), nullable=True)
4620 acl_level = Column('acl_level', Unicode(128), nullable=True)
4620
4621
4621 owner = relationship('User')
4622 owner = relationship('User')
4622
4623
4623 def __repr__(self):
4624 def __repr__(self):
4624 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4625 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4625
4626
4626 @hybrid_property
4627 @hybrid_property
4627 def description_safe(self):
4628 def description_safe(self):
4628 from rhodecode.lib import helpers as h
4629 from rhodecode.lib import helpers as h
4629 return h.escape(self.gist_description)
4630 return h.escape(self.gist_description)
4630
4631
4631 @classmethod
4632 @classmethod
4632 def get_or_404(cls, id_):
4633 def get_or_404(cls, id_):
4633 from pyramid.httpexceptions import HTTPNotFound
4634 from pyramid.httpexceptions import HTTPNotFound
4634
4635
4635 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4636 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4636 if not res:
4637 if not res:
4637 raise HTTPNotFound()
4638 raise HTTPNotFound()
4638 return res
4639 return res
4639
4640
4640 @classmethod
4641 @classmethod
4641 def get_by_access_id(cls, gist_access_id):
4642 def get_by_access_id(cls, gist_access_id):
4642 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4643 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4643
4644
4644 def gist_url(self):
4645 def gist_url(self):
4645 from rhodecode.model.gist import GistModel
4646 from rhodecode.model.gist import GistModel
4646 return GistModel().get_url(self)
4647 return GistModel().get_url(self)
4647
4648
4648 @classmethod
4649 @classmethod
4649 def base_path(cls):
4650 def base_path(cls):
4650 """
4651 """
4651 Returns base path when all gists are stored
4652 Returns base path when all gists are stored
4652
4653
4653 :param cls:
4654 :param cls:
4654 """
4655 """
4655 from rhodecode.model.gist import GIST_STORE_LOC
4656 from rhodecode.model.gist import GIST_STORE_LOC
4656 q = Session().query(RhodeCodeUi)\
4657 q = Session().query(RhodeCodeUi)\
4657 .filter(RhodeCodeUi.ui_key == URL_SEP)
4658 .filter(RhodeCodeUi.ui_key == URL_SEP)
4658 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4659 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4659 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4660 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4660
4661
4661 def get_api_data(self):
4662 def get_api_data(self):
4662 """
4663 """
4663 Common function for generating gist related data for API
4664 Common function for generating gist related data for API
4664 """
4665 """
4665 gist = self
4666 gist = self
4666 data = {
4667 data = {
4667 'gist_id': gist.gist_id,
4668 'gist_id': gist.gist_id,
4668 'type': gist.gist_type,
4669 'type': gist.gist_type,
4669 'access_id': gist.gist_access_id,
4670 'access_id': gist.gist_access_id,
4670 'description': gist.gist_description,
4671 'description': gist.gist_description,
4671 'url': gist.gist_url(),
4672 'url': gist.gist_url(),
4672 'expires': gist.gist_expires,
4673 'expires': gist.gist_expires,
4673 'created_on': gist.created_on,
4674 'created_on': gist.created_on,
4674 'modified_at': gist.modified_at,
4675 'modified_at': gist.modified_at,
4675 'content': None,
4676 'content': None,
4676 'acl_level': gist.acl_level,
4677 'acl_level': gist.acl_level,
4677 }
4678 }
4678 return data
4679 return data
4679
4680
4680 def __json__(self):
4681 def __json__(self):
4681 data = dict(
4682 data = dict(
4682 )
4683 )
4683 data.update(self.get_api_data())
4684 data.update(self.get_api_data())
4684 return data
4685 return data
4685 # SCM functions
4686 # SCM functions
4686
4687
4687 def scm_instance(self, **kwargs):
4688 def scm_instance(self, **kwargs):
4688 """
4689 """
4689 Get an instance of VCS Repository
4690 Get an instance of VCS Repository
4690
4691
4691 :param kwargs:
4692 :param kwargs:
4692 """
4693 """
4693 from rhodecode.model.gist import GistModel
4694 from rhodecode.model.gist import GistModel
4694 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4695 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4695 return get_vcs_instance(
4696 return get_vcs_instance(
4696 repo_path=safe_str(full_repo_path), create=False,
4697 repo_path=safe_str(full_repo_path), create=False,
4697 _vcs_alias=GistModel.vcs_backend)
4698 _vcs_alias=GistModel.vcs_backend)
4698
4699
4699
4700
4700 class ExternalIdentity(Base, BaseModel):
4701 class ExternalIdentity(Base, BaseModel):
4701 __tablename__ = 'external_identities'
4702 __tablename__ = 'external_identities'
4702 __table_args__ = (
4703 __table_args__ = (
4703 Index('local_user_id_idx', 'local_user_id'),
4704 Index('local_user_id_idx', 'local_user_id'),
4704 Index('external_id_idx', 'external_id'),
4705 Index('external_id_idx', 'external_id'),
4705 base_table_args
4706 base_table_args
4706 )
4707 )
4707
4708
4708 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4709 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4709 external_username = Column('external_username', Unicode(1024), default=u'')
4710 external_username = Column('external_username', Unicode(1024), default=u'')
4710 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4711 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4711 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4712 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4712 access_token = Column('access_token', String(1024), default=u'')
4713 access_token = Column('access_token', String(1024), default=u'')
4713 alt_token = Column('alt_token', String(1024), default=u'')
4714 alt_token = Column('alt_token', String(1024), default=u'')
4714 token_secret = Column('token_secret', String(1024), default=u'')
4715 token_secret = Column('token_secret', String(1024), default=u'')
4715
4716
4716 @classmethod
4717 @classmethod
4717 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4718 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4718 """
4719 """
4719 Returns ExternalIdentity instance based on search params
4720 Returns ExternalIdentity instance based on search params
4720
4721
4721 :param external_id:
4722 :param external_id:
4722 :param provider_name:
4723 :param provider_name:
4723 :return: ExternalIdentity
4724 :return: ExternalIdentity
4724 """
4725 """
4725 query = cls.query()
4726 query = cls.query()
4726 query = query.filter(cls.external_id == external_id)
4727 query = query.filter(cls.external_id == external_id)
4727 query = query.filter(cls.provider_name == provider_name)
4728 query = query.filter(cls.provider_name == provider_name)
4728 if local_user_id:
4729 if local_user_id:
4729 query = query.filter(cls.local_user_id == local_user_id)
4730 query = query.filter(cls.local_user_id == local_user_id)
4730 return query.first()
4731 return query.first()
4731
4732
4732 @classmethod
4733 @classmethod
4733 def user_by_external_id_and_provider(cls, external_id, provider_name):
4734 def user_by_external_id_and_provider(cls, external_id, provider_name):
4734 """
4735 """
4735 Returns User instance based on search params
4736 Returns User instance based on search params
4736
4737
4737 :param external_id:
4738 :param external_id:
4738 :param provider_name:
4739 :param provider_name:
4739 :return: User
4740 :return: User
4740 """
4741 """
4741 query = User.query()
4742 query = User.query()
4742 query = query.filter(cls.external_id == external_id)
4743 query = query.filter(cls.external_id == external_id)
4743 query = query.filter(cls.provider_name == provider_name)
4744 query = query.filter(cls.provider_name == provider_name)
4744 query = query.filter(User.user_id == cls.local_user_id)
4745 query = query.filter(User.user_id == cls.local_user_id)
4745 return query.first()
4746 return query.first()
4746
4747
4747 @classmethod
4748 @classmethod
4748 def by_local_user_id(cls, local_user_id):
4749 def by_local_user_id(cls, local_user_id):
4749 """
4750 """
4750 Returns all tokens for user
4751 Returns all tokens for user
4751
4752
4752 :param local_user_id:
4753 :param local_user_id:
4753 :return: ExternalIdentity
4754 :return: ExternalIdentity
4754 """
4755 """
4755 query = cls.query()
4756 query = cls.query()
4756 query = query.filter(cls.local_user_id == local_user_id)
4757 query = query.filter(cls.local_user_id == local_user_id)
4757 return query
4758 return query
4758
4759
4759 @classmethod
4760 @classmethod
4760 def load_provider_plugin(cls, plugin_id):
4761 def load_provider_plugin(cls, plugin_id):
4761 from rhodecode.authentication.base import loadplugin
4762 from rhodecode.authentication.base import loadplugin
4762 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4763 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4763 auth_plugin = loadplugin(_plugin_id)
4764 auth_plugin = loadplugin(_plugin_id)
4764 return auth_plugin
4765 return auth_plugin
4765
4766
4766
4767
4767 class Integration(Base, BaseModel):
4768 class Integration(Base, BaseModel):
4768 __tablename__ = 'integrations'
4769 __tablename__ = 'integrations'
4769 __table_args__ = (
4770 __table_args__ = (
4770 base_table_args
4771 base_table_args
4771 )
4772 )
4772
4773
4773 integration_id = Column('integration_id', Integer(), primary_key=True)
4774 integration_id = Column('integration_id', Integer(), primary_key=True)
4774 integration_type = Column('integration_type', String(255))
4775 integration_type = Column('integration_type', String(255))
4775 enabled = Column('enabled', Boolean(), nullable=False)
4776 enabled = Column('enabled', Boolean(), nullable=False)
4776 name = Column('name', String(255), nullable=False)
4777 name = Column('name', String(255), nullable=False)
4777 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4778 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4778 default=False)
4779 default=False)
4779
4780
4780 settings = Column(
4781 settings = Column(
4781 'settings_json', MutationObj.as_mutable(
4782 'settings_json', MutationObj.as_mutable(
4782 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4783 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4783 repo_id = Column(
4784 repo_id = Column(
4784 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4785 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4785 nullable=True, unique=None, default=None)
4786 nullable=True, unique=None, default=None)
4786 repo = relationship('Repository', lazy='joined')
4787 repo = relationship('Repository', lazy='joined')
4787
4788
4788 repo_group_id = Column(
4789 repo_group_id = Column(
4789 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4790 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4790 nullable=True, unique=None, default=None)
4791 nullable=True, unique=None, default=None)
4791 repo_group = relationship('RepoGroup', lazy='joined')
4792 repo_group = relationship('RepoGroup', lazy='joined')
4792
4793
4793 @property
4794 @property
4794 def scope(self):
4795 def scope(self):
4795 if self.repo:
4796 if self.repo:
4796 return repr(self.repo)
4797 return repr(self.repo)
4797 if self.repo_group:
4798 if self.repo_group:
4798 if self.child_repos_only:
4799 if self.child_repos_only:
4799 return repr(self.repo_group) + ' (child repos only)'
4800 return repr(self.repo_group) + ' (child repos only)'
4800 else:
4801 else:
4801 return repr(self.repo_group) + ' (recursive)'
4802 return repr(self.repo_group) + ' (recursive)'
4802 if self.child_repos_only:
4803 if self.child_repos_only:
4803 return 'root_repos'
4804 return 'root_repos'
4804 return 'global'
4805 return 'global'
4805
4806
4806 def __repr__(self):
4807 def __repr__(self):
4807 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4808 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4808
4809
4809
4810
4810 class RepoReviewRuleUser(Base, BaseModel):
4811 class RepoReviewRuleUser(Base, BaseModel):
4811 __tablename__ = 'repo_review_rules_users'
4812 __tablename__ = 'repo_review_rules_users'
4812 __table_args__ = (
4813 __table_args__ = (
4813 base_table_args
4814 base_table_args
4814 )
4815 )
4815
4816
4816 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4817 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4817 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4818 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4818 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4819 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4819 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4820 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4820 user = relationship('User')
4821 user = relationship('User')
4821
4822
4822 def rule_data(self):
4823 def rule_data(self):
4823 return {
4824 return {
4824 'mandatory': self.mandatory
4825 'mandatory': self.mandatory
4825 }
4826 }
4826
4827
4827
4828
4828 class RepoReviewRuleUserGroup(Base, BaseModel):
4829 class RepoReviewRuleUserGroup(Base, BaseModel):
4829 __tablename__ = 'repo_review_rules_users_groups'
4830 __tablename__ = 'repo_review_rules_users_groups'
4830 __table_args__ = (
4831 __table_args__ = (
4831 base_table_args
4832 base_table_args
4832 )
4833 )
4833
4834
4834 VOTE_RULE_ALL = -1
4835 VOTE_RULE_ALL = -1
4835
4836
4836 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4837 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4837 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4838 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4838 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4839 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4839 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4840 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4840 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4841 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4841 users_group = relationship('UserGroup')
4842 users_group = relationship('UserGroup')
4842
4843
4843 def rule_data(self):
4844 def rule_data(self):
4844 return {
4845 return {
4845 'mandatory': self.mandatory,
4846 'mandatory': self.mandatory,
4846 'vote_rule': self.vote_rule
4847 'vote_rule': self.vote_rule
4847 }
4848 }
4848
4849
4849 @property
4850 @property
4850 def vote_rule_label(self):
4851 def vote_rule_label(self):
4851 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4852 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4852 return 'all must vote'
4853 return 'all must vote'
4853 else:
4854 else:
4854 return 'min. vote {}'.format(self.vote_rule)
4855 return 'min. vote {}'.format(self.vote_rule)
4855
4856
4856
4857
4857 class RepoReviewRule(Base, BaseModel):
4858 class RepoReviewRule(Base, BaseModel):
4858 __tablename__ = 'repo_review_rules'
4859 __tablename__ = 'repo_review_rules'
4859 __table_args__ = (
4860 __table_args__ = (
4860 base_table_args
4861 base_table_args
4861 )
4862 )
4862
4863
4863 repo_review_rule_id = Column(
4864 repo_review_rule_id = Column(
4864 'repo_review_rule_id', Integer(), primary_key=True)
4865 'repo_review_rule_id', Integer(), primary_key=True)
4865 repo_id = Column(
4866 repo_id = Column(
4866 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4867 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4867 repo = relationship('Repository', backref='review_rules')
4868 repo = relationship('Repository', backref='review_rules')
4868
4869
4869 review_rule_name = Column('review_rule_name', String(255))
4870 review_rule_name = Column('review_rule_name', String(255))
4870 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4871 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4871 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4872 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4872 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4873 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4873
4874
4874 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4875 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4875 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4876 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4876 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4877 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4877 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4878 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4878
4879
4879 rule_users = relationship('RepoReviewRuleUser')
4880 rule_users = relationship('RepoReviewRuleUser')
4880 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4881 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4881
4882
4882 def _validate_pattern(self, value):
4883 def _validate_pattern(self, value):
4883 re.compile('^' + glob2re(value) + '$')
4884 re.compile('^' + glob2re(value) + '$')
4884
4885
4885 @hybrid_property
4886 @hybrid_property
4886 def source_branch_pattern(self):
4887 def source_branch_pattern(self):
4887 return self._branch_pattern or '*'
4888 return self._branch_pattern or '*'
4888
4889
4889 @source_branch_pattern.setter
4890 @source_branch_pattern.setter
4890 def source_branch_pattern(self, value):
4891 def source_branch_pattern(self, value):
4891 self._validate_pattern(value)
4892 self._validate_pattern(value)
4892 self._branch_pattern = value or '*'
4893 self._branch_pattern = value or '*'
4893
4894
4894 @hybrid_property
4895 @hybrid_property
4895 def target_branch_pattern(self):
4896 def target_branch_pattern(self):
4896 return self._target_branch_pattern or '*'
4897 return self._target_branch_pattern or '*'
4897
4898
4898 @target_branch_pattern.setter
4899 @target_branch_pattern.setter
4899 def target_branch_pattern(self, value):
4900 def target_branch_pattern(self, value):
4900 self._validate_pattern(value)
4901 self._validate_pattern(value)
4901 self._target_branch_pattern = value or '*'
4902 self._target_branch_pattern = value or '*'
4902
4903
4903 @hybrid_property
4904 @hybrid_property
4904 def file_pattern(self):
4905 def file_pattern(self):
4905 return self._file_pattern or '*'
4906 return self._file_pattern or '*'
4906
4907
4907 @file_pattern.setter
4908 @file_pattern.setter
4908 def file_pattern(self, value):
4909 def file_pattern(self, value):
4909 self._validate_pattern(value)
4910 self._validate_pattern(value)
4910 self._file_pattern = value or '*'
4911 self._file_pattern = value or '*'
4911
4912
4912 def matches(self, source_branch, target_branch, files_changed):
4913 def matches(self, source_branch, target_branch, files_changed):
4913 """
4914 """
4914 Check if this review rule matches a branch/files in a pull request
4915 Check if this review rule matches a branch/files in a pull request
4915
4916
4916 :param source_branch: source branch name for the commit
4917 :param source_branch: source branch name for the commit
4917 :param target_branch: target branch name for the commit
4918 :param target_branch: target branch name for the commit
4918 :param files_changed: list of file paths changed in the pull request
4919 :param files_changed: list of file paths changed in the pull request
4919 """
4920 """
4920
4921
4921 source_branch = source_branch or ''
4922 source_branch = source_branch or ''
4922 target_branch = target_branch or ''
4923 target_branch = target_branch or ''
4923 files_changed = files_changed or []
4924 files_changed = files_changed or []
4924
4925
4925 branch_matches = True
4926 branch_matches = True
4926 if source_branch or target_branch:
4927 if source_branch or target_branch:
4927 if self.source_branch_pattern == '*':
4928 if self.source_branch_pattern == '*':
4928 source_branch_match = True
4929 source_branch_match = True
4929 else:
4930 else:
4930 if self.source_branch_pattern.startswith('re:'):
4931 if self.source_branch_pattern.startswith('re:'):
4931 source_pattern = self.source_branch_pattern[3:]
4932 source_pattern = self.source_branch_pattern[3:]
4932 else:
4933 else:
4933 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
4934 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
4934 source_branch_regex = re.compile(source_pattern)
4935 source_branch_regex = re.compile(source_pattern)
4935 source_branch_match = bool(source_branch_regex.search(source_branch))
4936 source_branch_match = bool(source_branch_regex.search(source_branch))
4936 if self.target_branch_pattern == '*':
4937 if self.target_branch_pattern == '*':
4937 target_branch_match = True
4938 target_branch_match = True
4938 else:
4939 else:
4939 if self.target_branch_pattern.startswith('re:'):
4940 if self.target_branch_pattern.startswith('re:'):
4940 target_pattern = self.target_branch_pattern[3:]
4941 target_pattern = self.target_branch_pattern[3:]
4941 else:
4942 else:
4942 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
4943 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
4943 target_branch_regex = re.compile(target_pattern)
4944 target_branch_regex = re.compile(target_pattern)
4944 target_branch_match = bool(target_branch_regex.search(target_branch))
4945 target_branch_match = bool(target_branch_regex.search(target_branch))
4945
4946
4946 branch_matches = source_branch_match and target_branch_match
4947 branch_matches = source_branch_match and target_branch_match
4947
4948
4948 files_matches = True
4949 files_matches = True
4949 if self.file_pattern != '*':
4950 if self.file_pattern != '*':
4950 files_matches = False
4951 files_matches = False
4951 if self.file_pattern.startswith('re:'):
4952 if self.file_pattern.startswith('re:'):
4952 file_pattern = self.file_pattern[3:]
4953 file_pattern = self.file_pattern[3:]
4953 else:
4954 else:
4954 file_pattern = glob2re(self.file_pattern)
4955 file_pattern = glob2re(self.file_pattern)
4955 file_regex = re.compile(file_pattern)
4956 file_regex = re.compile(file_pattern)
4956 for filename in files_changed:
4957 for filename in files_changed:
4957 if file_regex.search(filename):
4958 if file_regex.search(filename):
4958 files_matches = True
4959 files_matches = True
4959 break
4960 break
4960
4961
4961 return branch_matches and files_matches
4962 return branch_matches and files_matches
4962
4963
4963 @property
4964 @property
4964 def review_users(self):
4965 def review_users(self):
4965 """ Returns the users which this rule applies to """
4966 """ Returns the users which this rule applies to """
4966
4967
4967 users = collections.OrderedDict()
4968 users = collections.OrderedDict()
4968
4969
4969 for rule_user in self.rule_users:
4970 for rule_user in self.rule_users:
4970 if rule_user.user.active:
4971 if rule_user.user.active:
4971 if rule_user.user not in users:
4972 if rule_user.user not in users:
4972 users[rule_user.user.username] = {
4973 users[rule_user.user.username] = {
4973 'user': rule_user.user,
4974 'user': rule_user.user,
4974 'source': 'user',
4975 'source': 'user',
4975 'source_data': {},
4976 'source_data': {},
4976 'data': rule_user.rule_data()
4977 'data': rule_user.rule_data()
4977 }
4978 }
4978
4979
4979 for rule_user_group in self.rule_user_groups:
4980 for rule_user_group in self.rule_user_groups:
4980 source_data = {
4981 source_data = {
4981 'user_group_id': rule_user_group.users_group.users_group_id,
4982 'user_group_id': rule_user_group.users_group.users_group_id,
4982 'name': rule_user_group.users_group.users_group_name,
4983 'name': rule_user_group.users_group.users_group_name,
4983 'members': len(rule_user_group.users_group.members)
4984 'members': len(rule_user_group.users_group.members)
4984 }
4985 }
4985 for member in rule_user_group.users_group.members:
4986 for member in rule_user_group.users_group.members:
4986 if member.user.active:
4987 if member.user.active:
4987 key = member.user.username
4988 key = member.user.username
4988 if key in users:
4989 if key in users:
4989 # skip this member as we have him already
4990 # skip this member as we have him already
4990 # this prevents from override the "first" matched
4991 # this prevents from override the "first" matched
4991 # users with duplicates in multiple groups
4992 # users with duplicates in multiple groups
4992 continue
4993 continue
4993
4994
4994 users[key] = {
4995 users[key] = {
4995 'user': member.user,
4996 'user': member.user,
4996 'source': 'user_group',
4997 'source': 'user_group',
4997 'source_data': source_data,
4998 'source_data': source_data,
4998 'data': rule_user_group.rule_data()
4999 'data': rule_user_group.rule_data()
4999 }
5000 }
5000
5001
5001 return users
5002 return users
5002
5003
5003 def user_group_vote_rule(self, user_id):
5004 def user_group_vote_rule(self, user_id):
5004
5005
5005 rules = []
5006 rules = []
5006 if not self.rule_user_groups:
5007 if not self.rule_user_groups:
5007 return rules
5008 return rules
5008
5009
5009 for user_group in self.rule_user_groups:
5010 for user_group in self.rule_user_groups:
5010 user_group_members = [x.user_id for x in user_group.users_group.members]
5011 user_group_members = [x.user_id for x in user_group.users_group.members]
5011 if user_id in user_group_members:
5012 if user_id in user_group_members:
5012 rules.append(user_group)
5013 rules.append(user_group)
5013 return rules
5014 return rules
5014
5015
5015 def __repr__(self):
5016 def __repr__(self):
5016 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5017 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5017 self.repo_review_rule_id, self.repo)
5018 self.repo_review_rule_id, self.repo)
5018
5019
5019
5020
5020 class ScheduleEntry(Base, BaseModel):
5021 class ScheduleEntry(Base, BaseModel):
5021 __tablename__ = 'schedule_entries'
5022 __tablename__ = 'schedule_entries'
5022 __table_args__ = (
5023 __table_args__ = (
5023 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5024 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5024 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5025 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5025 base_table_args,
5026 base_table_args,
5026 )
5027 )
5027
5028
5028 schedule_types = ['crontab', 'timedelta', 'integer']
5029 schedule_types = ['crontab', 'timedelta', 'integer']
5029 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5030 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5030
5031
5031 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5032 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5032 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5033 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5033 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5034 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5034
5035
5035 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5036 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5036 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5037 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5037
5038
5038 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5039 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5039 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5040 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5040
5041
5041 # task
5042 # task
5042 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5043 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5043 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5044 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5044 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5045 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5045 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5046 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5046
5047
5047 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5048 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5048 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5049 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5049
5050
5050 @hybrid_property
5051 @hybrid_property
5051 def schedule_type(self):
5052 def schedule_type(self):
5052 return self._schedule_type
5053 return self._schedule_type
5053
5054
5054 @schedule_type.setter
5055 @schedule_type.setter
5055 def schedule_type(self, val):
5056 def schedule_type(self, val):
5056 if val not in self.schedule_types:
5057 if val not in self.schedule_types:
5057 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5058 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5058 val, self.schedule_type))
5059 val, self.schedule_type))
5059
5060
5060 self._schedule_type = val
5061 self._schedule_type = val
5061
5062
5062 @classmethod
5063 @classmethod
5063 def get_uid(cls, obj):
5064 def get_uid(cls, obj):
5064 args = obj.task_args
5065 args = obj.task_args
5065 kwargs = obj.task_kwargs
5066 kwargs = obj.task_kwargs
5066 if isinstance(args, JsonRaw):
5067 if isinstance(args, JsonRaw):
5067 try:
5068 try:
5068 args = json.loads(args)
5069 args = json.loads(args)
5069 except ValueError:
5070 except ValueError:
5070 args = tuple()
5071 args = tuple()
5071
5072
5072 if isinstance(kwargs, JsonRaw):
5073 if isinstance(kwargs, JsonRaw):
5073 try:
5074 try:
5074 kwargs = json.loads(kwargs)
5075 kwargs = json.loads(kwargs)
5075 except ValueError:
5076 except ValueError:
5076 kwargs = dict()
5077 kwargs = dict()
5077
5078
5078 dot_notation = obj.task_dot_notation
5079 dot_notation = obj.task_dot_notation
5079 val = '.'.join(map(safe_str, [
5080 val = '.'.join(map(safe_str, [
5080 sorted(dot_notation), args, sorted(kwargs.items())]))
5081 sorted(dot_notation), args, sorted(kwargs.items())]))
5081 return hashlib.sha1(val).hexdigest()
5082 return hashlib.sha1(val).hexdigest()
5082
5083
5083 @classmethod
5084 @classmethod
5084 def get_by_schedule_name(cls, schedule_name):
5085 def get_by_schedule_name(cls, schedule_name):
5085 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5086 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5086
5087
5087 @classmethod
5088 @classmethod
5088 def get_by_schedule_id(cls, schedule_id):
5089 def get_by_schedule_id(cls, schedule_id):
5089 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5090 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5090
5091
5091 @property
5092 @property
5092 def task(self):
5093 def task(self):
5093 return self.task_dot_notation
5094 return self.task_dot_notation
5094
5095
5095 @property
5096 @property
5096 def schedule(self):
5097 def schedule(self):
5097 from rhodecode.lib.celerylib.utils import raw_2_schedule
5098 from rhodecode.lib.celerylib.utils import raw_2_schedule
5098 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5099 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5099 return schedule
5100 return schedule
5100
5101
5101 @property
5102 @property
5102 def args(self):
5103 def args(self):
5103 try:
5104 try:
5104 return list(self.task_args or [])
5105 return list(self.task_args or [])
5105 except ValueError:
5106 except ValueError:
5106 return list()
5107 return list()
5107
5108
5108 @property
5109 @property
5109 def kwargs(self):
5110 def kwargs(self):
5110 try:
5111 try:
5111 return dict(self.task_kwargs or {})
5112 return dict(self.task_kwargs or {})
5112 except ValueError:
5113 except ValueError:
5113 return dict()
5114 return dict()
5114
5115
5115 def _as_raw(self, val):
5116 def _as_raw(self, val):
5116 if hasattr(val, 'de_coerce'):
5117 if hasattr(val, 'de_coerce'):
5117 val = val.de_coerce()
5118 val = val.de_coerce()
5118 if val:
5119 if val:
5119 val = json.dumps(val)
5120 val = json.dumps(val)
5120
5121
5121 return val
5122 return val
5122
5123
5123 @property
5124 @property
5124 def schedule_definition_raw(self):
5125 def schedule_definition_raw(self):
5125 return self._as_raw(self.schedule_definition)
5126 return self._as_raw(self.schedule_definition)
5126
5127
5127 @property
5128 @property
5128 def args_raw(self):
5129 def args_raw(self):
5129 return self._as_raw(self.task_args)
5130 return self._as_raw(self.task_args)
5130
5131
5131 @property
5132 @property
5132 def kwargs_raw(self):
5133 def kwargs_raw(self):
5133 return self._as_raw(self.task_kwargs)
5134 return self._as_raw(self.task_kwargs)
5134
5135
5135 def __repr__(self):
5136 def __repr__(self):
5136 return '<DB:ScheduleEntry({}:{})>'.format(
5137 return '<DB:ScheduleEntry({}:{})>'.format(
5137 self.schedule_entry_id, self.schedule_name)
5138 self.schedule_entry_id, self.schedule_name)
5138
5139
5139
5140
5140 @event.listens_for(ScheduleEntry, 'before_update')
5141 @event.listens_for(ScheduleEntry, 'before_update')
5141 def update_task_uid(mapper, connection, target):
5142 def update_task_uid(mapper, connection, target):
5142 target.task_uid = ScheduleEntry.get_uid(target)
5143 target.task_uid = ScheduleEntry.get_uid(target)
5143
5144
5144
5145
5145 @event.listens_for(ScheduleEntry, 'before_insert')
5146 @event.listens_for(ScheduleEntry, 'before_insert')
5146 def set_task_uid(mapper, connection, target):
5147 def set_task_uid(mapper, connection, target):
5147 target.task_uid = ScheduleEntry.get_uid(target)
5148 target.task_uid = ScheduleEntry.get_uid(target)
5148
5149
5149
5150
5150 class _BaseBranchPerms(BaseModel):
5151 class _BaseBranchPerms(BaseModel):
5151 @classmethod
5152 @classmethod
5152 def compute_hash(cls, value):
5153 def compute_hash(cls, value):
5153 return sha1_safe(value)
5154 return sha1_safe(value)
5154
5155
5155 @hybrid_property
5156 @hybrid_property
5156 def branch_pattern(self):
5157 def branch_pattern(self):
5157 return self._branch_pattern or '*'
5158 return self._branch_pattern or '*'
5158
5159
5159 @hybrid_property
5160 @hybrid_property
5160 def branch_hash(self):
5161 def branch_hash(self):
5161 return self._branch_hash
5162 return self._branch_hash
5162
5163
5163 def _validate_glob(self, value):
5164 def _validate_glob(self, value):
5164 re.compile('^' + glob2re(value) + '$')
5165 re.compile('^' + glob2re(value) + '$')
5165
5166
5166 @branch_pattern.setter
5167 @branch_pattern.setter
5167 def branch_pattern(self, value):
5168 def branch_pattern(self, value):
5168 self._validate_glob(value)
5169 self._validate_glob(value)
5169 self._branch_pattern = value or '*'
5170 self._branch_pattern = value or '*'
5170 # set the Hash when setting the branch pattern
5171 # set the Hash when setting the branch pattern
5171 self._branch_hash = self.compute_hash(self._branch_pattern)
5172 self._branch_hash = self.compute_hash(self._branch_pattern)
5172
5173
5173 def matches(self, branch):
5174 def matches(self, branch):
5174 """
5175 """
5175 Check if this the branch matches entry
5176 Check if this the branch matches entry
5176
5177
5177 :param branch: branch name for the commit
5178 :param branch: branch name for the commit
5178 """
5179 """
5179
5180
5180 branch = branch or ''
5181 branch = branch or ''
5181
5182
5182 branch_matches = True
5183 branch_matches = True
5183 if branch:
5184 if branch:
5184 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5185 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5185 branch_matches = bool(branch_regex.search(branch))
5186 branch_matches = bool(branch_regex.search(branch))
5186
5187
5187 return branch_matches
5188 return branch_matches
5188
5189
5189
5190
5190 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5191 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5191 __tablename__ = 'user_to_repo_branch_permissions'
5192 __tablename__ = 'user_to_repo_branch_permissions'
5192 __table_args__ = (
5193 __table_args__ = (
5193 base_table_args
5194 base_table_args
5194 )
5195 )
5195
5196
5196 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5197 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5197
5198
5198 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5199 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5199 repo = relationship('Repository', backref='user_branch_perms')
5200 repo = relationship('Repository', backref='user_branch_perms')
5200
5201
5201 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5202 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5202 permission = relationship('Permission')
5203 permission = relationship('Permission')
5203
5204
5204 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5205 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5205 user_repo_to_perm = relationship('UserRepoToPerm')
5206 user_repo_to_perm = relationship('UserRepoToPerm')
5206
5207
5207 rule_order = Column('rule_order', Integer(), nullable=False)
5208 rule_order = Column('rule_order', Integer(), nullable=False)
5208 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5209 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5209 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5210 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5210
5211
5211 def __unicode__(self):
5212 def __unicode__(self):
5212 return u'<UserBranchPermission(%s => %r)>' % (
5213 return u'<UserBranchPermission(%s => %r)>' % (
5213 self.user_repo_to_perm, self.branch_pattern)
5214 self.user_repo_to_perm, self.branch_pattern)
5214
5215
5215
5216
5216 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5217 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5217 __tablename__ = 'user_group_to_repo_branch_permissions'
5218 __tablename__ = 'user_group_to_repo_branch_permissions'
5218 __table_args__ = (
5219 __table_args__ = (
5219 base_table_args
5220 base_table_args
5220 )
5221 )
5221
5222
5222 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5223 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5223
5224
5224 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5225 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5225 repo = relationship('Repository', backref='user_group_branch_perms')
5226 repo = relationship('Repository', backref='user_group_branch_perms')
5226
5227
5227 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5228 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5228 permission = relationship('Permission')
5229 permission = relationship('Permission')
5229
5230
5230 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5231 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5231 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5232 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5232
5233
5233 rule_order = Column('rule_order', Integer(), nullable=False)
5234 rule_order = Column('rule_order', Integer(), nullable=False)
5234 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5235 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5235 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5236 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5236
5237
5237 def __unicode__(self):
5238 def __unicode__(self):
5238 return u'<UserBranchPermission(%s => %r)>' % (
5239 return u'<UserBranchPermission(%s => %r)>' % (
5239 self.user_group_repo_to_perm, self.branch_pattern)
5240 self.user_group_repo_to_perm, self.branch_pattern)
5240
5241
5241
5242
5242 class UserBookmark(Base, BaseModel):
5243 class UserBookmark(Base, BaseModel):
5243 __tablename__ = 'user_bookmarks'
5244 __tablename__ = 'user_bookmarks'
5244 __table_args__ = (
5245 __table_args__ = (
5245 UniqueConstraint('user_id', 'bookmark_repo_id'),
5246 UniqueConstraint('user_id', 'bookmark_repo_id'),
5246 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5247 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5247 UniqueConstraint('user_id', 'bookmark_position'),
5248 UniqueConstraint('user_id', 'bookmark_position'),
5248 base_table_args
5249 base_table_args
5249 )
5250 )
5250
5251
5251 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5252 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5252 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5253 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5253 position = Column("bookmark_position", Integer(), nullable=False)
5254 position = Column("bookmark_position", Integer(), nullable=False)
5254 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5255 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5255 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5256 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5256 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5257 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5257
5258
5258 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5259 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5259 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5260 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5260
5261
5261 user = relationship("User")
5262 user = relationship("User")
5262
5263
5263 repository = relationship("Repository")
5264 repository = relationship("Repository")
5264 repository_group = relationship("RepoGroup")
5265 repository_group = relationship("RepoGroup")
5265
5266
5266 @classmethod
5267 @classmethod
5267 def get_by_position_for_user(cls, position, user_id):
5268 def get_by_position_for_user(cls, position, user_id):
5268 return cls.query() \
5269 return cls.query() \
5269 .filter(UserBookmark.user_id == user_id) \
5270 .filter(UserBookmark.user_id == user_id) \
5270 .filter(UserBookmark.position == position).scalar()
5271 .filter(UserBookmark.position == position).scalar()
5271
5272
5272 @classmethod
5273 @classmethod
5273 def get_bookmarks_for_user(cls, user_id, cache=True):
5274 def get_bookmarks_for_user(cls, user_id, cache=True):
5274 bookmarks = cls.query() \
5275 bookmarks = cls.query() \
5275 .filter(UserBookmark.user_id == user_id) \
5276 .filter(UserBookmark.user_id == user_id) \
5276 .options(joinedload(UserBookmark.repository)) \
5277 .options(joinedload(UserBookmark.repository)) \
5277 .options(joinedload(UserBookmark.repository_group)) \
5278 .options(joinedload(UserBookmark.repository_group)) \
5278 .order_by(UserBookmark.position.asc())
5279 .order_by(UserBookmark.position.asc())
5279
5280
5280 if cache:
5281 if cache:
5281 bookmarks = bookmarks.options(
5282 bookmarks = bookmarks.options(
5282 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5283 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5283 )
5284 )
5284
5285
5285 return bookmarks.all()
5286 return bookmarks.all()
5286
5287
5287 def __unicode__(self):
5288 def __unicode__(self):
5288 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5289 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5289
5290
5290
5291
5291 class FileStore(Base, BaseModel):
5292 class FileStore(Base, BaseModel):
5292 __tablename__ = 'file_store'
5293 __tablename__ = 'file_store'
5293 __table_args__ = (
5294 __table_args__ = (
5294 base_table_args
5295 base_table_args
5295 )
5296 )
5296
5297
5297 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5298 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5298 file_uid = Column('file_uid', String(1024), nullable=False)
5299 file_uid = Column('file_uid', String(1024), nullable=False)
5299 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5300 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5300 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5301 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5301 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5302 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5302
5303
5303 # sha256 hash
5304 # sha256 hash
5304 file_hash = Column('file_hash', String(512), nullable=False)
5305 file_hash = Column('file_hash', String(512), nullable=False)
5305 file_size = Column('file_size', BigInteger(), nullable=False)
5306 file_size = Column('file_size', BigInteger(), nullable=False)
5306
5307
5307 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5308 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5308 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5309 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5309 accessed_count = Column('accessed_count', Integer(), default=0)
5310 accessed_count = Column('accessed_count', Integer(), default=0)
5310
5311
5311 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5312 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5312
5313
5313 # if repo/repo_group reference is set, check for permissions
5314 # if repo/repo_group reference is set, check for permissions
5314 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5315 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5315
5316
5316 # hidden defines an attachment that should be hidden from showing in artifact listing
5317 # hidden defines an attachment that should be hidden from showing in artifact listing
5317 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5318 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5318
5319
5319 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5320 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5320 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5321 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5321
5322
5322 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5323 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5323
5324
5324 # scope limited to user, which requester have access to
5325 # scope limited to user, which requester have access to
5325 scope_user_id = Column(
5326 scope_user_id = Column(
5326 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5327 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5327 nullable=True, unique=None, default=None)
5328 nullable=True, unique=None, default=None)
5328 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5329 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5329
5330
5330 # scope limited to user group, which requester have access to
5331 # scope limited to user group, which requester have access to
5331 scope_user_group_id = Column(
5332 scope_user_group_id = Column(
5332 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5333 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5333 nullable=True, unique=None, default=None)
5334 nullable=True, unique=None, default=None)
5334 user_group = relationship('UserGroup', lazy='joined')
5335 user_group = relationship('UserGroup', lazy='joined')
5335
5336
5336 # scope limited to repo, which requester have access to
5337 # scope limited to repo, which requester have access to
5337 scope_repo_id = Column(
5338 scope_repo_id = Column(
5338 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5339 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5339 nullable=True, unique=None, default=None)
5340 nullable=True, unique=None, default=None)
5340 repo = relationship('Repository', lazy='joined')
5341 repo = relationship('Repository', lazy='joined')
5341
5342
5342 # scope limited to repo group, which requester have access to
5343 # scope limited to repo group, which requester have access to
5343 scope_repo_group_id = Column(
5344 scope_repo_group_id = Column(
5344 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5345 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5345 nullable=True, unique=None, default=None)
5346 nullable=True, unique=None, default=None)
5346 repo_group = relationship('RepoGroup', lazy='joined')
5347 repo_group = relationship('RepoGroup', lazy='joined')
5347
5348
5348 @classmethod
5349 @classmethod
5349 def get_by_store_uid(cls, file_store_uid):
5350 def get_by_store_uid(cls, file_store_uid):
5350 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5351 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5351
5352
5352 @classmethod
5353 @classmethod
5353 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5354 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5354 file_description='', enabled=True, hidden=False, check_acl=True,
5355 file_description='', enabled=True, hidden=False, check_acl=True,
5355 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5356 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5356
5357
5357 store_entry = FileStore()
5358 store_entry = FileStore()
5358 store_entry.file_uid = file_uid
5359 store_entry.file_uid = file_uid
5359 store_entry.file_display_name = file_display_name
5360 store_entry.file_display_name = file_display_name
5360 store_entry.file_org_name = filename
5361 store_entry.file_org_name = filename
5361 store_entry.file_size = file_size
5362 store_entry.file_size = file_size
5362 store_entry.file_hash = file_hash
5363 store_entry.file_hash = file_hash
5363 store_entry.file_description = file_description
5364 store_entry.file_description = file_description
5364
5365
5365 store_entry.check_acl = check_acl
5366 store_entry.check_acl = check_acl
5366 store_entry.enabled = enabled
5367 store_entry.enabled = enabled
5367 store_entry.hidden = hidden
5368 store_entry.hidden = hidden
5368
5369
5369 store_entry.user_id = user_id
5370 store_entry.user_id = user_id
5370 store_entry.scope_user_id = scope_user_id
5371 store_entry.scope_user_id = scope_user_id
5371 store_entry.scope_repo_id = scope_repo_id
5372 store_entry.scope_repo_id = scope_repo_id
5372 store_entry.scope_repo_group_id = scope_repo_group_id
5373 store_entry.scope_repo_group_id = scope_repo_group_id
5373
5374
5374 return store_entry
5375 return store_entry
5375
5376
5376 @classmethod
5377 @classmethod
5377 def store_metadata(cls, file_store_id, args, commit=True):
5378 def store_metadata(cls, file_store_id, args, commit=True):
5378 file_store = FileStore.get(file_store_id)
5379 file_store = FileStore.get(file_store_id)
5379 if file_store is None:
5380 if file_store is None:
5380 return
5381 return
5381
5382
5382 for section, key, value, value_type in args:
5383 for section, key, value, value_type in args:
5383 has_key = FileStoreMetadata().query() \
5384 has_key = FileStoreMetadata().query() \
5384 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5385 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5385 .filter(FileStoreMetadata.file_store_meta_section == section) \
5386 .filter(FileStoreMetadata.file_store_meta_section == section) \
5386 .filter(FileStoreMetadata.file_store_meta_key == key) \
5387 .filter(FileStoreMetadata.file_store_meta_key == key) \
5387 .scalar()
5388 .scalar()
5388 if has_key:
5389 if has_key:
5389 msg = 'key `{}` already defined under section `{}` for this file.'\
5390 msg = 'key `{}` already defined under section `{}` for this file.'\
5390 .format(key, section)
5391 .format(key, section)
5391 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5392 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5392
5393
5393 # NOTE(marcink): raises ArtifactMetadataBadValueType
5394 # NOTE(marcink): raises ArtifactMetadataBadValueType
5394 FileStoreMetadata.valid_value_type(value_type)
5395 FileStoreMetadata.valid_value_type(value_type)
5395
5396
5396 meta_entry = FileStoreMetadata()
5397 meta_entry = FileStoreMetadata()
5397 meta_entry.file_store = file_store
5398 meta_entry.file_store = file_store
5398 meta_entry.file_store_meta_section = section
5399 meta_entry.file_store_meta_section = section
5399 meta_entry.file_store_meta_key = key
5400 meta_entry.file_store_meta_key = key
5400 meta_entry.file_store_meta_value_type = value_type
5401 meta_entry.file_store_meta_value_type = value_type
5401 meta_entry.file_store_meta_value = value
5402 meta_entry.file_store_meta_value = value
5402
5403
5403 Session().add(meta_entry)
5404 Session().add(meta_entry)
5404
5405
5405 try:
5406 try:
5406 if commit:
5407 if commit:
5407 Session().commit()
5408 Session().commit()
5408 except IntegrityError:
5409 except IntegrityError:
5409 Session().rollback()
5410 Session().rollback()
5410 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5411 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5411
5412
5412 @classmethod
5413 @classmethod
5413 def bump_access_counter(cls, file_uid, commit=True):
5414 def bump_access_counter(cls, file_uid, commit=True):
5414 FileStore().query()\
5415 FileStore().query()\
5415 .filter(FileStore.file_uid == file_uid)\
5416 .filter(FileStore.file_uid == file_uid)\
5416 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5417 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5417 FileStore.accessed_on: datetime.datetime.now()})
5418 FileStore.accessed_on: datetime.datetime.now()})
5418 if commit:
5419 if commit:
5419 Session().commit()
5420 Session().commit()
5420
5421
5421 def __json__(self):
5422 def __json__(self):
5422 data = {
5423 data = {
5423 'filename': self.file_display_name,
5424 'filename': self.file_display_name,
5424 'filename_org': self.file_org_name,
5425 'filename_org': self.file_org_name,
5425 'file_uid': self.file_uid,
5426 'file_uid': self.file_uid,
5426 'description': self.file_description,
5427 'description': self.file_description,
5427 'hidden': self.hidden,
5428 'hidden': self.hidden,
5428 'size': self.file_size,
5429 'size': self.file_size,
5429 'created_on': self.created_on,
5430 'created_on': self.created_on,
5430 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5431 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5431 'downloaded_times': self.accessed_count,
5432 'downloaded_times': self.accessed_count,
5432 'sha256': self.file_hash,
5433 'sha256': self.file_hash,
5433 'metadata': self.file_metadata,
5434 'metadata': self.file_metadata,
5434 }
5435 }
5435
5436
5436 return data
5437 return data
5437
5438
5438 def __repr__(self):
5439 def __repr__(self):
5439 return '<FileStore({})>'.format(self.file_store_id)
5440 return '<FileStore({})>'.format(self.file_store_id)
5440
5441
5441
5442
5442 class FileStoreMetadata(Base, BaseModel):
5443 class FileStoreMetadata(Base, BaseModel):
5443 __tablename__ = 'file_store_metadata'
5444 __tablename__ = 'file_store_metadata'
5444 __table_args__ = (
5445 __table_args__ = (
5445 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5446 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5446 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5447 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5447 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5448 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5448 base_table_args
5449 base_table_args
5449 )
5450 )
5450 SETTINGS_TYPES = {
5451 SETTINGS_TYPES = {
5451 'str': safe_str,
5452 'str': safe_str,
5452 'int': safe_int,
5453 'int': safe_int,
5453 'unicode': safe_unicode,
5454 'unicode': safe_unicode,
5454 'bool': str2bool,
5455 'bool': str2bool,
5455 'list': functools.partial(aslist, sep=',')
5456 'list': functools.partial(aslist, sep=',')
5456 }
5457 }
5457
5458
5458 file_store_meta_id = Column(
5459 file_store_meta_id = Column(
5459 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5460 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5460 primary_key=True)
5461 primary_key=True)
5461 _file_store_meta_section = Column(
5462 _file_store_meta_section = Column(
5462 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5463 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5463 nullable=True, unique=None, default=None)
5464 nullable=True, unique=None, default=None)
5464 _file_store_meta_section_hash = Column(
5465 _file_store_meta_section_hash = Column(
5465 "file_store_meta_section_hash", String(255),
5466 "file_store_meta_section_hash", String(255),
5466 nullable=True, unique=None, default=None)
5467 nullable=True, unique=None, default=None)
5467 _file_store_meta_key = Column(
5468 _file_store_meta_key = Column(
5468 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5469 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5469 nullable=True, unique=None, default=None)
5470 nullable=True, unique=None, default=None)
5470 _file_store_meta_key_hash = Column(
5471 _file_store_meta_key_hash = Column(
5471 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5472 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5472 _file_store_meta_value = Column(
5473 _file_store_meta_value = Column(
5473 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5474 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5474 nullable=True, unique=None, default=None)
5475 nullable=True, unique=None, default=None)
5475 _file_store_meta_value_type = Column(
5476 _file_store_meta_value_type = Column(
5476 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5477 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5477 default='unicode')
5478 default='unicode')
5478
5479
5479 file_store_id = Column(
5480 file_store_id = Column(
5480 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5481 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5481 nullable=True, unique=None, default=None)
5482 nullable=True, unique=None, default=None)
5482
5483
5483 file_store = relationship('FileStore', lazy='joined')
5484 file_store = relationship('FileStore', lazy='joined')
5484
5485
5485 @classmethod
5486 @classmethod
5486 def valid_value_type(cls, value):
5487 def valid_value_type(cls, value):
5487 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5488 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5488 raise ArtifactMetadataBadValueType(
5489 raise ArtifactMetadataBadValueType(
5489 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5490 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5490
5491
5491 @hybrid_property
5492 @hybrid_property
5492 def file_store_meta_section(self):
5493 def file_store_meta_section(self):
5493 return self._file_store_meta_section
5494 return self._file_store_meta_section
5494
5495
5495 @file_store_meta_section.setter
5496 @file_store_meta_section.setter
5496 def file_store_meta_section(self, value):
5497 def file_store_meta_section(self, value):
5497 self._file_store_meta_section = value
5498 self._file_store_meta_section = value
5498 self._file_store_meta_section_hash = _hash_key(value)
5499 self._file_store_meta_section_hash = _hash_key(value)
5499
5500
5500 @hybrid_property
5501 @hybrid_property
5501 def file_store_meta_key(self):
5502 def file_store_meta_key(self):
5502 return self._file_store_meta_key
5503 return self._file_store_meta_key
5503
5504
5504 @file_store_meta_key.setter
5505 @file_store_meta_key.setter
5505 def file_store_meta_key(self, value):
5506 def file_store_meta_key(self, value):
5506 self._file_store_meta_key = value
5507 self._file_store_meta_key = value
5507 self._file_store_meta_key_hash = _hash_key(value)
5508 self._file_store_meta_key_hash = _hash_key(value)
5508
5509
5509 @hybrid_property
5510 @hybrid_property
5510 def file_store_meta_value(self):
5511 def file_store_meta_value(self):
5511 val = self._file_store_meta_value
5512 val = self._file_store_meta_value
5512
5513
5513 if self._file_store_meta_value_type:
5514 if self._file_store_meta_value_type:
5514 # e.g unicode.encrypted == unicode
5515 # e.g unicode.encrypted == unicode
5515 _type = self._file_store_meta_value_type.split('.')[0]
5516 _type = self._file_store_meta_value_type.split('.')[0]
5516 # decode the encrypted value if it's encrypted field type
5517 # decode the encrypted value if it's encrypted field type
5517 if '.encrypted' in self._file_store_meta_value_type:
5518 if '.encrypted' in self._file_store_meta_value_type:
5518 cipher = EncryptedTextValue()
5519 cipher = EncryptedTextValue()
5519 val = safe_unicode(cipher.process_result_value(val, None))
5520 val = safe_unicode(cipher.process_result_value(val, None))
5520 # do final type conversion
5521 # do final type conversion
5521 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5522 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5522 val = converter(val)
5523 val = converter(val)
5523
5524
5524 return val
5525 return val
5525
5526
5526 @file_store_meta_value.setter
5527 @file_store_meta_value.setter
5527 def file_store_meta_value(self, val):
5528 def file_store_meta_value(self, val):
5528 val = safe_unicode(val)
5529 val = safe_unicode(val)
5529 # encode the encrypted value
5530 # encode the encrypted value
5530 if '.encrypted' in self.file_store_meta_value_type:
5531 if '.encrypted' in self.file_store_meta_value_type:
5531 cipher = EncryptedTextValue()
5532 cipher = EncryptedTextValue()
5532 val = safe_unicode(cipher.process_bind_param(val, None))
5533 val = safe_unicode(cipher.process_bind_param(val, None))
5533 self._file_store_meta_value = val
5534 self._file_store_meta_value = val
5534
5535
5535 @hybrid_property
5536 @hybrid_property
5536 def file_store_meta_value_type(self):
5537 def file_store_meta_value_type(self):
5537 return self._file_store_meta_value_type
5538 return self._file_store_meta_value_type
5538
5539
5539 @file_store_meta_value_type.setter
5540 @file_store_meta_value_type.setter
5540 def file_store_meta_value_type(self, val):
5541 def file_store_meta_value_type(self, val):
5541 # e.g unicode.encrypted
5542 # e.g unicode.encrypted
5542 self.valid_value_type(val)
5543 self.valid_value_type(val)
5543 self._file_store_meta_value_type = val
5544 self._file_store_meta_value_type = val
5544
5545
5545 def __json__(self):
5546 def __json__(self):
5546 data = {
5547 data = {
5547 'artifact': self.file_store.file_uid,
5548 'artifact': self.file_store.file_uid,
5548 'section': self.file_store_meta_section,
5549 'section': self.file_store_meta_section,
5549 'key': self.file_store_meta_key,
5550 'key': self.file_store_meta_key,
5550 'value': self.file_store_meta_value,
5551 'value': self.file_store_meta_value,
5551 }
5552 }
5552
5553
5553 return data
5554 return data
5554
5555
5555 def __repr__(self):
5556 def __repr__(self):
5556 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5557 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5557 self.file_store_meta_key, self.file_store_meta_value)
5558 self.file_store_meta_key, self.file_store_meta_value)
5558
5559
5559
5560
5560 class DbMigrateVersion(Base, BaseModel):
5561 class DbMigrateVersion(Base, BaseModel):
5561 __tablename__ = 'db_migrate_version'
5562 __tablename__ = 'db_migrate_version'
5562 __table_args__ = (
5563 __table_args__ = (
5563 base_table_args,
5564 base_table_args,
5564 )
5565 )
5565
5566
5566 repository_id = Column('repository_id', String(250), primary_key=True)
5567 repository_id = Column('repository_id', String(250), primary_key=True)
5567 repository_path = Column('repository_path', Text)
5568 repository_path = Column('repository_path', Text)
5568 version = Column('version', Integer)
5569 version = Column('version', Integer)
5569
5570
5570 @classmethod
5571 @classmethod
5571 def set_version(cls, version):
5572 def set_version(cls, version):
5572 """
5573 """
5573 Helper for forcing a different version, usually for debugging purposes via ishell.
5574 Helper for forcing a different version, usually for debugging purposes via ishell.
5574 """
5575 """
5575 ver = DbMigrateVersion.query().first()
5576 ver = DbMigrateVersion.query().first()
5576 ver.version = version
5577 ver.version = version
5577 Session().commit()
5578 Session().commit()
5578
5579
5579
5580
5580 class DbSession(Base, BaseModel):
5581 class DbSession(Base, BaseModel):
5581 __tablename__ = 'db_session'
5582 __tablename__ = 'db_session'
5582 __table_args__ = (
5583 __table_args__ = (
5583 base_table_args,
5584 base_table_args,
5584 )
5585 )
5585
5586
5586 def __repr__(self):
5587 def __repr__(self):
5587 return '<DB:DbSession({})>'.format(self.id)
5588 return '<DB:DbSession({})>'.format(self.id)
5588
5589
5589 id = Column('id', Integer())
5590 id = Column('id', Integer())
5590 namespace = Column('namespace', String(255), primary_key=True)
5591 namespace = Column('namespace', String(255), primary_key=True)
5591 accessed = Column('accessed', DateTime, nullable=False)
5592 accessed = Column('accessed', DateTime, nullable=False)
5592 created = Column('created', DateTime, nullable=False)
5593 created = Column('created', DateTime, nullable=False)
5593 data = Column('data', PickleType, nullable=False)
5594 data = Column('data', PickleType, nullable=False)
@@ -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