##// END OF EJS Templates
fix(caching): fixed problems with Cache query for users....
super-admin -
r5365:ae8a165b default
parent child Browse files
Show More
@@ -1,811 +1,818 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import logging
19 import logging
20 import datetime
20 import datetime
21 import string
21 import string
22
22
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25 import peppercorn
25 import peppercorn
26 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
26 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
27
27
28 from rhodecode.apps._base import BaseAppView, DataGridAppView
28 from rhodecode.apps._base import BaseAppView, DataGridAppView
29 from rhodecode import forms
29 from rhodecode import forms
30 from rhodecode.lib import helpers as h
30 from rhodecode.lib import helpers as h
31 from rhodecode.lib import audit_logger
31 from rhodecode.lib import audit_logger
32 from rhodecode.lib import ext_json
32 from rhodecode.lib import ext_json
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, NotAnonymous, CSRFRequired,
34 LoginRequired, NotAnonymous, CSRFRequired,
35 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
35 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
36 from rhodecode.lib.channelstream import (
36 from rhodecode.lib.channelstream import (
37 channelstream_request, ChannelstreamException)
37 channelstream_request, ChannelstreamException)
38 from rhodecode.lib.hash_utils import md5_safe
38 from rhodecode.lib.hash_utils import md5_safe
39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 from rhodecode.model.auth_token import AuthTokenModel
40 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.model.comment import CommentsModel
41 from rhodecode.model.comment import CommentsModel
42 from rhodecode.model.db import (
42 from rhodecode.model.db import (
43 IntegrityError, or_, in_filter_generator,
43 IntegrityError, or_, in_filter_generator, select,
44 Repository, UserEmailMap, UserApiKeys, UserFollowing,
44 Repository, UserEmailMap, UserApiKeys, UserFollowing,
45 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
45 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47 from rhodecode.model.pull_request import PullRequestModel
47 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.user import UserModel
48 from rhodecode.model.user import UserModel
49 from rhodecode.model.user_group import UserGroupModel
49 from rhodecode.model.user_group import UserGroupModel
50 from rhodecode.model.validation_schema.schemas import user_schema
50 from rhodecode.model.validation_schema.schemas import user_schema
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 class MyAccountView(BaseAppView, DataGridAppView):
55 class MyAccountView(BaseAppView, DataGridAppView):
56 ALLOW_SCOPED_TOKENS = False
56 ALLOW_SCOPED_TOKENS = False
57 """
57 """
58 This view has alternative version inside EE, if modified please take a look
58 This view has alternative version inside EE, if modified please take a look
59 in there as well.
59 in there as well.
60 """
60 """
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context()
63 c = self._get_local_tmpl_context()
64 c.user = c.auth_user.get_instance()
64 c.user = c.auth_user.get_instance()
65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 return c
66 return c
67
67
68 @LoginRequired()
68 @LoginRequired()
69 @NotAnonymous()
69 @NotAnonymous()
70 def my_account_profile(self):
70 def my_account_profile(self):
71 c = self.load_default_context()
71 c = self.load_default_context()
72 c.active = 'profile'
72 c.active = 'profile'
73 c.extern_type = c.user.extern_type
73 c.extern_type = c.user.extern_type
74 return self._get_template_context(c)
74 return self._get_template_context(c)
75
75
76 @LoginRequired()
76 @LoginRequired()
77 @NotAnonymous()
77 @NotAnonymous()
78 def my_account_edit(self):
78 def my_account_edit(self):
79 c = self.load_default_context()
79 c = self.load_default_context()
80 c.active = 'profile_edit'
80 c.active = 'profile_edit'
81 c.extern_type = c.user.extern_type
81 c.extern_type = c.user.extern_type
82 c.extern_name = c.user.extern_name
82 c.extern_name = c.user.extern_name
83
83
84 schema = user_schema.UserProfileSchema().bind(
84 schema = user_schema.UserProfileSchema().bind(
85 username=c.user.username, user_emails=c.user.emails)
85 username=c.user.username, user_emails=c.user.emails)
86 appstruct = {
86 appstruct = {
87 'username': c.user.username,
87 'username': c.user.username,
88 'email': c.user.email,
88 'email': c.user.email,
89 'firstname': c.user.firstname,
89 'firstname': c.user.firstname,
90 'lastname': c.user.lastname,
90 'lastname': c.user.lastname,
91 'description': c.user.description,
91 'description': c.user.description,
92 }
92 }
93 c.form = forms.RcForm(
93 c.form = forms.RcForm(
94 schema, appstruct=appstruct,
94 schema, appstruct=appstruct,
95 action=h.route_path('my_account_update'),
95 action=h.route_path('my_account_update'),
96 buttons=(forms.buttons.save, forms.buttons.reset))
96 buttons=(forms.buttons.save, forms.buttons.reset))
97
97
98 return self._get_template_context(c)
98 return self._get_template_context(c)
99
99
100 @LoginRequired()
100 @LoginRequired()
101 @NotAnonymous()
101 @NotAnonymous()
102 @CSRFRequired()
102 @CSRFRequired()
103 def my_account_update(self):
103 def my_account_update(self):
104 _ = self.request.translate
104 _ = self.request.translate
105 c = self.load_default_context()
105 c = self.load_default_context()
106 c.active = 'profile_edit'
106 c.active = 'profile_edit'
107 c.perm_user = c.auth_user
107 c.perm_user = c.auth_user
108 c.extern_type = c.user.extern_type
108 c.extern_type = c.user.extern_type
109 c.extern_name = c.user.extern_name
109 c.extern_name = c.user.extern_name
110
110
111 schema = user_schema.UserProfileSchema().bind(
111 schema = user_schema.UserProfileSchema().bind(
112 username=c.user.username, user_emails=c.user.emails)
112 username=c.user.username, user_emails=c.user.emails)
113 form = forms.RcForm(
113 form = forms.RcForm(
114 schema, buttons=(forms.buttons.save, forms.buttons.reset))
114 schema, buttons=(forms.buttons.save, forms.buttons.reset))
115
115
116 controls = list(self.request.POST.items())
116 controls = list(self.request.POST.items())
117 try:
117 try:
118 valid_data = form.validate(controls)
118 valid_data = form.validate(controls)
119 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
119 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
120 'new_password', 'password_confirmation']
120 'new_password', 'password_confirmation']
121 if c.extern_type != "rhodecode":
121 if c.extern_type != "rhodecode":
122 # forbid updating username for external accounts
122 # forbid updating username for external accounts
123 skip_attrs.append('username')
123 skip_attrs.append('username')
124 old_email = c.user.email
124 old_email = c.user.email
125 UserModel().update_user(
125 UserModel().update_user(
126 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
126 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
127 **valid_data)
127 **valid_data)
128 if old_email != valid_data['email']:
128 if old_email != valid_data['email']:
129 old = UserEmailMap.query() \
129 old = UserEmailMap.query() \
130 .filter(UserEmailMap.user == c.user)\
130 .filter(UserEmailMap.user == c.user)\
131 .filter(UserEmailMap.email == valid_data['email'])\
131 .filter(UserEmailMap.email == valid_data['email'])\
132 .first()
132 .first()
133 old.email = old_email
133 old.email = old_email
134 h.flash(_('Your account was updated successfully'), category='success')
134 h.flash(_('Your account was updated successfully'), category='success')
135 Session().commit()
135 Session().commit()
136 except forms.ValidationFailure as e:
136 except forms.ValidationFailure as e:
137 c.form = e
137 c.form = e
138 return self._get_template_context(c)
138 return self._get_template_context(c)
139
139
140 except Exception:
140 except Exception:
141 log.exception("Exception updating user")
141 log.exception("Exception updating user")
142 h.flash(_('Error occurred during update of user'),
142 h.flash(_('Error occurred during update of user'),
143 category='error')
143 category='error')
144 raise HTTPFound(h.route_path('my_account_profile'))
144 raise HTTPFound(h.route_path('my_account_profile'))
145
145
146 @LoginRequired()
146 @LoginRequired()
147 @NotAnonymous()
147 @NotAnonymous()
148 def my_account_password(self):
148 def my_account_password(self):
149 c = self.load_default_context()
149 c = self.load_default_context()
150 c.active = 'password'
150 c.active = 'password'
151 c.extern_type = c.user.extern_type
151 c.extern_type = c.user.extern_type
152
152
153 schema = user_schema.ChangePasswordSchema().bind(
153 schema = user_schema.ChangePasswordSchema().bind(
154 username=c.user.username)
154 username=c.user.username)
155
155
156 form = forms.Form(
156 form = forms.Form(
157 schema,
157 schema,
158 action=h.route_path('my_account_password_update'),
158 action=h.route_path('my_account_password_update'),
159 buttons=(forms.buttons.save, forms.buttons.reset))
159 buttons=(forms.buttons.save, forms.buttons.reset))
160
160
161 c.form = form
161 c.form = form
162 return self._get_template_context(c)
162 return self._get_template_context(c)
163
163
164 @LoginRequired()
164 @LoginRequired()
165 @NotAnonymous()
165 @NotAnonymous()
166 @CSRFRequired()
166 @CSRFRequired()
167 def my_account_password_update(self):
167 def my_account_password_update(self):
168 _ = self.request.translate
168 _ = self.request.translate
169 c = self.load_default_context()
169 c = self.load_default_context()
170 c.active = 'password'
170 c.active = 'password'
171 c.extern_type = c.user.extern_type
171 c.extern_type = c.user.extern_type
172
172
173 schema = user_schema.ChangePasswordSchema().bind(
173 schema = user_schema.ChangePasswordSchema().bind(
174 username=c.user.username)
174 username=c.user.username)
175
175
176 form = forms.Form(
176 form = forms.Form(
177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
178
178
179 if c.extern_type != 'rhodecode':
179 if c.extern_type != 'rhodecode':
180 raise HTTPFound(self.request.route_path('my_account_password'))
180 raise HTTPFound(self.request.route_path('my_account_password'))
181
181
182 controls = list(self.request.POST.items())
182 controls = list(self.request.POST.items())
183 try:
183 try:
184 valid_data = form.validate(controls)
184 valid_data = form.validate(controls)
185 UserModel().update_user(c.user.user_id, **valid_data)
185 UserModel().update_user(c.user.user_id, **valid_data)
186 c.user.update_userdata(force_password_change=False)
186 c.user.update_userdata(force_password_change=False)
187 Session().commit()
187 Session().commit()
188 except forms.ValidationFailure as e:
188 except forms.ValidationFailure as e:
189 c.form = e
189 c.form = e
190 return self._get_template_context(c)
190 return self._get_template_context(c)
191
191
192 except Exception:
192 except Exception:
193 log.exception("Exception updating password")
193 log.exception("Exception updating password")
194 h.flash(_('Error occurred during update of user password'),
194 h.flash(_('Error occurred during update of user password'),
195 category='error')
195 category='error')
196 else:
196 else:
197 instance = c.auth_user.get_instance()
197 instance = c.auth_user.get_instance()
198 self.session.setdefault('rhodecode_user', {}).update(
198 self.session.setdefault('rhodecode_user', {}).update(
199 {'password': md5_safe(instance.password)})
199 {'password': md5_safe(instance.password)})
200 self.session.save()
200 self.session.save()
201 h.flash(_("Successfully updated password"), category='success')
201 h.flash(_("Successfully updated password"), category='success')
202
202
203 raise HTTPFound(self.request.route_path('my_account_password'))
203 raise HTTPFound(self.request.route_path('my_account_password'))
204
204
205 @LoginRequired()
205 @LoginRequired()
206 @NotAnonymous()
206 @NotAnonymous()
207 def my_account_2fa(self):
207 def my_account_2fa(self):
208 _ = self.request.translate
208 _ = self.request.translate
209 c = self.load_default_context()
209 c = self.load_default_context()
210 c.active = '2fa'
210 c.active = '2fa'
211 from rhodecode.model.settings import SettingsModel
211 from rhodecode.model.settings import SettingsModel
212 user_instance = self._rhodecode_db_user
212 user_instance = self._rhodecode_db_user
213 locked_by_admin = user_instance.has_forced_2fa
213 locked_by_admin = user_instance.has_forced_2fa
214 c.state_of_2fa = user_instance.has_enabled_2fa
214 c.state_of_2fa = user_instance.has_enabled_2fa
215 c.locked_2fa = str2bool(locked_by_admin)
215 c.locked_2fa = str2bool(locked_by_admin)
216 return self._get_template_context(c)
216 return self._get_template_context(c)
217
217
218 @LoginRequired()
218 @LoginRequired()
219 @NotAnonymous()
219 @NotAnonymous()
220 @CSRFRequired()
220 @CSRFRequired()
221 def my_account_2fa_configure(self):
221 def my_account_2fa_configure(self):
222 state = self.request.POST.get('state')
222 state = self.request.POST.get('state')
223 self._rhodecode_db_user.has_enabled_2fa = state
223 self._rhodecode_db_user.has_enabled_2fa = state
224 return {'state_of_2fa': state}
224 return {'state_of_2fa': state}
225
225
226 @LoginRequired()
226 @LoginRequired()
227 @NotAnonymous()
227 @NotAnonymous()
228 @CSRFRequired()
228 @CSRFRequired()
229 def my_account_2fa_regenerate_recovery_codes(self):
229 def my_account_2fa_regenerate_recovery_codes(self):
230 return {'recovery_codes': self._rhodecode_db_user.regenerate_2fa_recovery_codes()}
230 return {'recovery_codes': self._rhodecode_db_user.regenerate_2fa_recovery_codes()}
231
231
232 @LoginRequired()
232 @LoginRequired()
233 @NotAnonymous()
233 @NotAnonymous()
234 def my_account_auth_tokens(self):
234 def my_account_auth_tokens(self):
235 _ = self.request.translate
235 _ = self.request.translate
236
236
237 c = self.load_default_context()
237 c = self.load_default_context()
238 c.active = 'auth_tokens'
238 c.active = 'auth_tokens'
239 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
239 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
240 c.role_values = [
240 c.role_values = [
241 (x, AuthTokenModel.cls._get_role_name(x))
241 (x, AuthTokenModel.cls._get_role_name(x))
242 for x in AuthTokenModel.cls.ROLES]
242 for x in AuthTokenModel.cls.ROLES]
243 c.role_options = [(c.role_values, _("Role"))]
243 c.role_options = [(c.role_values, _("Role"))]
244 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
244 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
245 c.user.user_id, show_expired=True)
245 c.user.user_id, show_expired=True)
246 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
246 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
247 return self._get_template_context(c)
247 return self._get_template_context(c)
248
248
249 @LoginRequired()
249 @LoginRequired()
250 @NotAnonymous()
250 @NotAnonymous()
251 @CSRFRequired()
251 @CSRFRequired()
252 def my_account_auth_tokens_view(self):
252 def my_account_auth_tokens_view(self):
253 _ = self.request.translate
253 _ = self.request.translate
254 c = self.load_default_context()
254 c = self.load_default_context()
255
255
256 auth_token_id = self.request.POST.get('auth_token_id')
256 auth_token_id = self.request.POST.get('auth_token_id')
257
257
258 if auth_token_id:
258 if auth_token_id:
259 token = UserApiKeys.get_or_404(auth_token_id)
259 token = UserApiKeys.get_or_404(auth_token_id)
260 if token.user.user_id != c.user.user_id:
260 if token.user.user_id != c.user.user_id:
261 raise HTTPNotFound()
261 raise HTTPNotFound()
262
262
263 return {
263 return {
264 'auth_token': token.api_key
264 'auth_token': token.api_key
265 }
265 }
266
266
267 def maybe_attach_token_scope(self, token):
267 def maybe_attach_token_scope(self, token):
268 # implemented in EE edition
268 # implemented in EE edition
269 pass
269 pass
270
270
271 @LoginRequired()
271 @LoginRequired()
272 @NotAnonymous()
272 @NotAnonymous()
273 @CSRFRequired()
273 @CSRFRequired()
274 def my_account_auth_tokens_add(self):
274 def my_account_auth_tokens_add(self):
275 _ = self.request.translate
275 _ = self.request.translate
276 c = self.load_default_context()
276 c = self.load_default_context()
277
277
278 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
278 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
279 description = self.request.POST.get('description')
279 description = self.request.POST.get('description')
280 role = self.request.POST.get('role')
280 role = self.request.POST.get('role')
281
281
282 token = UserModel().add_auth_token(
282 token = UserModel().add_auth_token(
283 user=c.user.user_id,
283 user=c.user.user_id,
284 lifetime_minutes=lifetime, role=role, description=description,
284 lifetime_minutes=lifetime, role=role, description=description,
285 scope_callback=self.maybe_attach_token_scope)
285 scope_callback=self.maybe_attach_token_scope)
286 token_data = token.get_api_data()
286 token_data = token.get_api_data()
287
287
288 audit_logger.store_web(
288 audit_logger.store_web(
289 'user.edit.token.add', action_data={
289 'user.edit.token.add', action_data={
290 'data': {'token': token_data, 'user': 'self'}},
290 'data': {'token': token_data, 'user': 'self'}},
291 user=self._rhodecode_user, )
291 user=self._rhodecode_user, )
292 Session().commit()
292 Session().commit()
293
293
294 h.flash(_("Auth token successfully created"), category='success')
294 h.flash(_("Auth token successfully created"), category='success')
295 return HTTPFound(h.route_path('my_account_auth_tokens'))
295 return HTTPFound(h.route_path('my_account_auth_tokens'))
296
296
297 @LoginRequired()
297 @LoginRequired()
298 @NotAnonymous()
298 @NotAnonymous()
299 @CSRFRequired()
299 @CSRFRequired()
300 def my_account_auth_tokens_delete(self):
300 def my_account_auth_tokens_delete(self):
301 _ = self.request.translate
301 _ = self.request.translate
302 c = self.load_default_context()
302 c = self.load_default_context()
303
303
304 del_auth_token = self.request.POST.get('del_auth_token')
304 del_auth_token = self.request.POST.get('del_auth_token')
305
305
306 if del_auth_token:
306 if del_auth_token:
307 token = UserApiKeys.get_or_404(del_auth_token)
307 token = UserApiKeys.get_or_404(del_auth_token)
308 token_data = token.get_api_data()
308 token_data = token.get_api_data()
309
309
310 AuthTokenModel().delete(del_auth_token, c.user.user_id)
310 AuthTokenModel().delete(del_auth_token, c.user.user_id)
311 audit_logger.store_web(
311 audit_logger.store_web(
312 'user.edit.token.delete', action_data={
312 'user.edit.token.delete', action_data={
313 'data': {'token': token_data, 'user': 'self'}},
313 'data': {'token': token_data, 'user': 'self'}},
314 user=self._rhodecode_user,)
314 user=self._rhodecode_user,)
315 Session().commit()
315 Session().commit()
316 h.flash(_("Auth token successfully deleted"), category='success')
316 h.flash(_("Auth token successfully deleted"), category='success')
317
317
318 return HTTPFound(h.route_path('my_account_auth_tokens'))
318 return HTTPFound(h.route_path('my_account_auth_tokens'))
319
319
320 @LoginRequired()
320 @LoginRequired()
321 @NotAnonymous()
321 @NotAnonymous()
322 def my_account_emails(self):
322 def my_account_emails(self):
323 _ = self.request.translate
323 _ = self.request.translate
324
324
325 c = self.load_default_context()
325 c = self.load_default_context()
326 c.active = 'emails'
326 c.active = 'emails'
327
327
328 c.user_email_map = UserEmailMap.query()\
328 c.user_email_map = UserEmailMap.query()\
329 .filter(UserEmailMap.user == c.user).all()
329 .filter(UserEmailMap.user == c.user).all()
330
330
331 schema = user_schema.AddEmailSchema().bind(
331 schema = user_schema.AddEmailSchema().bind(
332 username=c.user.username, user_emails=c.user.emails)
332 username=c.user.username, user_emails=c.user.emails)
333
333
334 form = forms.RcForm(schema,
334 form = forms.RcForm(schema,
335 action=h.route_path('my_account_emails_add'),
335 action=h.route_path('my_account_emails_add'),
336 buttons=(forms.buttons.save, forms.buttons.reset))
336 buttons=(forms.buttons.save, forms.buttons.reset))
337
337
338 c.form = form
338 c.form = form
339 return self._get_template_context(c)
339 return self._get_template_context(c)
340
340
341 @LoginRequired()
341 @LoginRequired()
342 @NotAnonymous()
342 @NotAnonymous()
343 @CSRFRequired()
343 @CSRFRequired()
344 def my_account_emails_add(self):
344 def my_account_emails_add(self):
345 _ = self.request.translate
345 _ = self.request.translate
346 c = self.load_default_context()
346 c = self.load_default_context()
347 c.active = 'emails'
347 c.active = 'emails'
348
348
349 schema = user_schema.AddEmailSchema().bind(
349 schema = user_schema.AddEmailSchema().bind(
350 username=c.user.username, user_emails=c.user.emails)
350 username=c.user.username, user_emails=c.user.emails)
351
351
352 form = forms.RcForm(
352 form = forms.RcForm(
353 schema, action=h.route_path('my_account_emails_add'),
353 schema, action=h.route_path('my_account_emails_add'),
354 buttons=(forms.buttons.save, forms.buttons.reset))
354 buttons=(forms.buttons.save, forms.buttons.reset))
355
355
356 controls = list(self.request.POST.items())
356 controls = list(self.request.POST.items())
357 try:
357 try:
358 valid_data = form.validate(controls)
358 valid_data = form.validate(controls)
359 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
359 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
360 audit_logger.store_web(
360 audit_logger.store_web(
361 'user.edit.email.add', action_data={
361 'user.edit.email.add', action_data={
362 'data': {'email': valid_data['email'], 'user': 'self'}},
362 'data': {'email': valid_data['email'], 'user': 'self'}},
363 user=self._rhodecode_user,)
363 user=self._rhodecode_user,)
364 Session().commit()
364 Session().commit()
365 except formencode.Invalid as error:
365 except formencode.Invalid as error:
366 h.flash(h.escape(error.error_dict['email']), category='error')
366 h.flash(h.escape(error.error_dict['email']), category='error')
367 except forms.ValidationFailure as e:
367 except forms.ValidationFailure as e:
368 c.user_email_map = UserEmailMap.query() \
368 c.user_email_map = UserEmailMap.query() \
369 .filter(UserEmailMap.user == c.user).all()
369 .filter(UserEmailMap.user == c.user).all()
370 c.form = e
370 c.form = e
371 return self._get_template_context(c)
371 return self._get_template_context(c)
372 except Exception:
372 except Exception:
373 log.exception("Exception adding email")
373 log.exception("Exception adding email")
374 h.flash(_('Error occurred during adding email'),
374 h.flash(_('Error occurred during adding email'),
375 category='error')
375 category='error')
376 else:
376 else:
377 h.flash(_("Successfully added email"), category='success')
377 h.flash(_("Successfully added email"), category='success')
378
378
379 raise HTTPFound(self.request.route_path('my_account_emails'))
379 raise HTTPFound(self.request.route_path('my_account_emails'))
380
380
381 @LoginRequired()
381 @LoginRequired()
382 @NotAnonymous()
382 @NotAnonymous()
383 @CSRFRequired()
383 @CSRFRequired()
384 def my_account_emails_delete(self):
384 def my_account_emails_delete(self):
385 _ = self.request.translate
385 _ = self.request.translate
386 c = self.load_default_context()
386 c = self.load_default_context()
387
387
388 del_email_id = self.request.POST.get('del_email_id')
388 del_email_id = self.request.POST.get('del_email_id')
389 if del_email_id:
389 if del_email_id:
390 email = UserEmailMap.get_or_404(del_email_id).email
390 email = UserEmailMap.get_or_404(del_email_id).email
391 UserModel().delete_extra_email(c.user.user_id, del_email_id)
391 UserModel().delete_extra_email(c.user.user_id, del_email_id)
392 audit_logger.store_web(
392 audit_logger.store_web(
393 'user.edit.email.delete', action_data={
393 'user.edit.email.delete', action_data={
394 'data': {'email': email, 'user': 'self'}},
394 'data': {'email': email, 'user': 'self'}},
395 user=self._rhodecode_user,)
395 user=self._rhodecode_user,)
396 Session().commit()
396 Session().commit()
397 h.flash(_("Email successfully deleted"),
397 h.flash(_("Email successfully deleted"),
398 category='success')
398 category='success')
399 return HTTPFound(h.route_path('my_account_emails'))
399 return HTTPFound(h.route_path('my_account_emails'))
400
400
401 @LoginRequired()
401 @LoginRequired()
402 @NotAnonymous()
402 @NotAnonymous()
403 @CSRFRequired()
403 @CSRFRequired()
404 def my_account_notifications_test_channelstream(self):
404 def my_account_notifications_test_channelstream(self):
405 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
405 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
406 self._rhodecode_user.username, datetime.datetime.now())
406 self._rhodecode_user.username, datetime.datetime.now())
407 payload = {
407 payload = {
408 # 'channel': 'broadcast',
408 # 'channel': 'broadcast',
409 'type': 'message',
409 'type': 'message',
410 'timestamp': datetime.datetime.utcnow(),
410 'timestamp': datetime.datetime.utcnow(),
411 'user': 'system',
411 'user': 'system',
412 'pm_users': [self._rhodecode_user.username],
412 'pm_users': [self._rhodecode_user.username],
413 'message': {
413 'message': {
414 'message': message,
414 'message': message,
415 'level': 'info',
415 'level': 'info',
416 'topic': '/notifications'
416 'topic': '/notifications'
417 }
417 }
418 }
418 }
419
419
420 registry = self.request.registry
420 registry = self.request.registry
421 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
421 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
422 channelstream_config = rhodecode_plugins.get('channelstream', {})
422 channelstream_config = rhodecode_plugins.get('channelstream', {})
423
423
424 try:
424 try:
425 channelstream_request(channelstream_config, [payload], '/message')
425 channelstream_request(channelstream_config, [payload], '/message')
426 except ChannelstreamException as e:
426 except ChannelstreamException as e:
427 log.exception('Failed to send channelstream data')
427 log.exception('Failed to send channelstream data')
428 return {"response": f'ERROR: {e.__class__.__name__}'}
428 return {"response": f'ERROR: {e.__class__.__name__}'}
429 return {"response": 'Channelstream data sent. '
429 return {"response": 'Channelstream data sent. '
430 'You should see a new live message now.'}
430 'You should see a new live message now.'}
431
431
432 def _load_my_repos_data(self, watched=False):
432 def _load_my_repos_data(self, watched=False):
433
433
434 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
434 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
435
435
436 if watched:
436 if watched:
437 # repos user watch
437 # repos user watch
438 repo_list = Session().query(
438 repo_list = Session().query(
439 Repository
439 Repository
440 ) \
440 ) \
441 .join(
441 .join(
442 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
442 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
443 ) \
443 ) \
444 .filter(
444 .filter(
445 UserFollowing.user_id == self._rhodecode_user.user_id
445 UserFollowing.user_id == self._rhodecode_user.user_id
446 ) \
446 ) \
447 .filter(or_(
447 .filter(or_(
448 # generate multiple IN to fix limitation problems
448 # generate multiple IN to fix limitation problems
449 *in_filter_generator(Repository.repo_id, allowed_ids))
449 *in_filter_generator(Repository.repo_id, allowed_ids))
450 ) \
450 ) \
451 .order_by(Repository.repo_name) \
451 .order_by(Repository.repo_name) \
452 .all()
452 .all()
453
453
454 else:
454 else:
455 # repos user is owner of
455 # repos user is owner of
456 repo_list = Session().query(
456 repo_list = Session().query(
457 Repository
457 Repository
458 ) \
458 ) \
459 .filter(
459 .filter(
460 Repository.user_id == self._rhodecode_user.user_id
460 Repository.user_id == self._rhodecode_user.user_id
461 ) \
461 ) \
462 .filter(or_(
462 .filter(or_(
463 # generate multiple IN to fix limitation problems
463 # generate multiple IN to fix limitation problems
464 *in_filter_generator(Repository.repo_id, allowed_ids))
464 *in_filter_generator(Repository.repo_id, allowed_ids))
465 ) \
465 ) \
466 .order_by(Repository.repo_name) \
466 .order_by(Repository.repo_name) \
467 .all()
467 .all()
468
468
469 _render = self.request.get_partial_renderer(
469 _render = self.request.get_partial_renderer(
470 'rhodecode:templates/data_table/_dt_elements.mako')
470 'rhodecode:templates/data_table/_dt_elements.mako')
471
471
472 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
472 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
473 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
473 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
474 short_name=False, admin=False)
474 short_name=False, admin=False)
475
475
476 repos_data = []
476 repos_data = []
477 for repo in repo_list:
477 for repo in repo_list:
478 row = {
478 row = {
479 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
479 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
480 repo.private, repo.archived, repo.fork),
480 repo.private, repo.archived, repo.fork),
481 "name_raw": repo.repo_name.lower(),
481 "name_raw": repo.repo_name.lower(),
482 }
482 }
483
483
484 repos_data.append(row)
484 repos_data.append(row)
485
485
486 # json used to render the grid
486 # json used to render the grid
487 return ext_json.str_json(repos_data)
487 return ext_json.str_json(repos_data)
488
488
489 @LoginRequired()
489 @LoginRequired()
490 @NotAnonymous()
490 @NotAnonymous()
491 def my_account_repos(self):
491 def my_account_repos(self):
492 c = self.load_default_context()
492 c = self.load_default_context()
493 c.active = 'repos'
493 c.active = 'repos'
494
494
495 # json used to render the grid
495 # json used to render the grid
496 c.data = self._load_my_repos_data()
496 c.data = self._load_my_repos_data()
497 return self._get_template_context(c)
497 return self._get_template_context(c)
498
498
499 @LoginRequired()
499 @LoginRequired()
500 @NotAnonymous()
500 @NotAnonymous()
501 def my_account_watched(self):
501 def my_account_watched(self):
502 c = self.load_default_context()
502 c = self.load_default_context()
503 c.active = 'watched'
503 c.active = 'watched'
504
504
505 # json used to render the grid
505 # json used to render the grid
506 c.data = self._load_my_repos_data(watched=True)
506 c.data = self._load_my_repos_data(watched=True)
507 return self._get_template_context(c)
507 return self._get_template_context(c)
508
508
509 @LoginRequired()
509 @LoginRequired()
510 @NotAnonymous()
510 @NotAnonymous()
511 def my_account_bookmarks(self):
511 def my_account_bookmarks(self):
512 c = self.load_default_context()
512 c = self.load_default_context()
513 c.active = 'bookmarks'
513 c.active = 'bookmarks'
514 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
514
515 self._rhodecode_db_user.user_id, cache=False)
515 user_bookmarks = \
516 select(UserBookmark, Repository, RepoGroup) \
517 .where(UserBookmark.user_id == self._rhodecode_user.user_id) \
518 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
519 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
520 .order_by(UserBookmark.position.asc())
521
522 c.user_bookmark_items = Session().execute(user_bookmarks).all()
516 return self._get_template_context(c)
523 return self._get_template_context(c)
517
524
518 def _process_bookmark_entry(self, entry, user_id):
525 def _process_bookmark_entry(self, entry, user_id):
519 position = safe_int(entry.get('position'))
526 position = safe_int(entry.get('position'))
520 cur_position = safe_int(entry.get('cur_position'))
527 cur_position = safe_int(entry.get('cur_position'))
521 if position is None:
528 if position is None:
522 return
529 return
523
530
524 # check if this is an existing entry
531 # check if this is an existing entry
525 is_new = False
532 is_new = False
526 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
533 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
527
534
528 if db_entry and str2bool(entry.get('remove')):
535 if db_entry and str2bool(entry.get('remove')):
529 log.debug('Marked bookmark %s for deletion', db_entry)
536 log.debug('Marked bookmark %s for deletion', db_entry)
530 Session().delete(db_entry)
537 Session().delete(db_entry)
531 return
538 return
532
539
533 if not db_entry:
540 if not db_entry:
534 # new
541 # new
535 db_entry = UserBookmark()
542 db_entry = UserBookmark()
536 is_new = True
543 is_new = True
537
544
538 should_save = False
545 should_save = False
539 default_redirect_url = ''
546 default_redirect_url = ''
540
547
541 # save repo
548 # save repo
542 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
549 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
543 repo = Repository.get(entry['bookmark_repo'])
550 repo = Repository.get(entry['bookmark_repo'])
544 perm_check = HasRepoPermissionAny(
551 perm_check = HasRepoPermissionAny(
545 'repository.read', 'repository.write', 'repository.admin')
552 'repository.read', 'repository.write', 'repository.admin')
546 if repo and perm_check(repo_name=repo.repo_name):
553 if repo and perm_check(repo_name=repo.repo_name):
547 db_entry.repository = repo
554 db_entry.repository = repo
548 should_save = True
555 should_save = True
549 default_redirect_url = '${repo_url}'
556 default_redirect_url = '${repo_url}'
550 # save repo group
557 # save repo group
551 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
558 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
552 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
559 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
553 perm_check = HasRepoGroupPermissionAny(
560 perm_check = HasRepoGroupPermissionAny(
554 'group.read', 'group.write', 'group.admin')
561 'group.read', 'group.write', 'group.admin')
555
562
556 if repo_group and perm_check(group_name=repo_group.group_name):
563 if repo_group and perm_check(group_name=repo_group.group_name):
557 db_entry.repository_group = repo_group
564 db_entry.repository_group = repo_group
558 should_save = True
565 should_save = True
559 default_redirect_url = '${repo_group_url}'
566 default_redirect_url = '${repo_group_url}'
560 # save generic info
567 # save generic info
561 elif entry.get('title') and entry.get('redirect_url'):
568 elif entry.get('title') and entry.get('redirect_url'):
562 should_save = True
569 should_save = True
563
570
564 if should_save:
571 if should_save:
565 # mark user and position
572 # mark user and position
566 db_entry.user_id = user_id
573 db_entry.user_id = user_id
567 db_entry.position = position
574 db_entry.position = position
568 db_entry.title = entry.get('title')
575 db_entry.title = entry.get('title')
569 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
576 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
570 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
577 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
571
578
572 Session().add(db_entry)
579 Session().add(db_entry)
573
580
574 @LoginRequired()
581 @LoginRequired()
575 @NotAnonymous()
582 @NotAnonymous()
576 @CSRFRequired()
583 @CSRFRequired()
577 def my_account_bookmarks_update(self):
584 def my_account_bookmarks_update(self):
578 _ = self.request.translate
585 _ = self.request.translate
579 c = self.load_default_context()
586 c = self.load_default_context()
580 c.active = 'bookmarks'
587 c.active = 'bookmarks'
581
588
582 controls = peppercorn.parse(self.request.POST.items())
589 controls = peppercorn.parse(self.request.POST.items())
583 user_id = c.user.user_id
590 user_id = c.user.user_id
584
591
585 # validate positions
592 # validate positions
586 positions = {}
593 positions = {}
587 for entry in controls.get('bookmarks', []):
594 for entry in controls.get('bookmarks', []):
588 position = safe_int(entry['position'])
595 position = safe_int(entry['position'])
589 if position is None:
596 if position is None:
590 continue
597 continue
591
598
592 if position in positions:
599 if position in positions:
593 h.flash(_("Position {} is defined twice. "
600 h.flash(_("Position {} is defined twice. "
594 "Please correct this error.").format(position), category='error')
601 "Please correct this error.").format(position), category='error')
595 return HTTPFound(h.route_path('my_account_bookmarks'))
602 return HTTPFound(h.route_path('my_account_bookmarks'))
596
603
597 entry['position'] = position
604 entry['position'] = position
598 entry['cur_position'] = safe_int(entry.get('cur_position'))
605 entry['cur_position'] = safe_int(entry.get('cur_position'))
599 positions[position] = entry
606 positions[position] = entry
600
607
601 try:
608 try:
602 for entry in positions.values():
609 for entry in positions.values():
603 self._process_bookmark_entry(entry, user_id)
610 self._process_bookmark_entry(entry, user_id)
604
611
605 Session().commit()
612 Session().commit()
606 h.flash(_("Update Bookmarks"), category='success')
613 h.flash(_("Update Bookmarks"), category='success')
607 except IntegrityError:
614 except IntegrityError:
608 h.flash(_("Failed to update bookmarks. "
615 h.flash(_("Failed to update bookmarks. "
609 "Make sure an unique position is used."), category='error')
616 "Make sure an unique position is used."), category='error')
610
617
611 return HTTPFound(h.route_path('my_account_bookmarks'))
618 return HTTPFound(h.route_path('my_account_bookmarks'))
612
619
613 @LoginRequired()
620 @LoginRequired()
614 @NotAnonymous()
621 @NotAnonymous()
615 def my_account_goto_bookmark(self):
622 def my_account_goto_bookmark(self):
616
623
617 bookmark_id = self.request.matchdict['bookmark_id']
624 bookmark_id = self.request.matchdict['bookmark_id']
618 user_bookmark = UserBookmark().query()\
625 user_bookmark = UserBookmark().query()\
619 .filter(UserBookmark.user_id == self.request.user.user_id) \
626 .filter(UserBookmark.user_id == self.request.user.user_id) \
620 .filter(UserBookmark.position == bookmark_id).scalar()
627 .filter(UserBookmark.position == bookmark_id).scalar()
621
628
622 redirect_url = h.route_path('my_account_bookmarks')
629 redirect_url = h.route_path('my_account_bookmarks')
623 if not user_bookmark:
630 if not user_bookmark:
624 raise HTTPFound(redirect_url)
631 raise HTTPFound(redirect_url)
625
632
626 # repository set
633 # repository set
627 if user_bookmark.repository:
634 if user_bookmark.repository:
628 repo_name = user_bookmark.repository.repo_name
635 repo_name = user_bookmark.repository.repo_name
629 base_redirect_url = h.route_path(
636 base_redirect_url = h.route_path(
630 'repo_summary', repo_name=repo_name)
637 'repo_summary', repo_name=repo_name)
631 if user_bookmark.redirect_url and \
638 if user_bookmark.redirect_url and \
632 '${repo_url}' in user_bookmark.redirect_url:
639 '${repo_url}' in user_bookmark.redirect_url:
633 redirect_url = string.Template(user_bookmark.redirect_url)\
640 redirect_url = string.Template(user_bookmark.redirect_url)\
634 .safe_substitute({'repo_url': base_redirect_url})
641 .safe_substitute({'repo_url': base_redirect_url})
635 else:
642 else:
636 redirect_url = base_redirect_url
643 redirect_url = base_redirect_url
637 # repository group set
644 # repository group set
638 elif user_bookmark.repository_group:
645 elif user_bookmark.repository_group:
639 repo_group_name = user_bookmark.repository_group.group_name
646 repo_group_name = user_bookmark.repository_group.group_name
640 base_redirect_url = h.route_path(
647 base_redirect_url = h.route_path(
641 'repo_group_home', repo_group_name=repo_group_name)
648 'repo_group_home', repo_group_name=repo_group_name)
642 if user_bookmark.redirect_url and \
649 if user_bookmark.redirect_url and \
643 '${repo_group_url}' in user_bookmark.redirect_url:
650 '${repo_group_url}' in user_bookmark.redirect_url:
644 redirect_url = string.Template(user_bookmark.redirect_url)\
651 redirect_url = string.Template(user_bookmark.redirect_url)\
645 .safe_substitute({'repo_group_url': base_redirect_url})
652 .safe_substitute({'repo_group_url': base_redirect_url})
646 else:
653 else:
647 redirect_url = base_redirect_url
654 redirect_url = base_redirect_url
648 # custom URL set
655 # custom URL set
649 elif user_bookmark.redirect_url:
656 elif user_bookmark.redirect_url:
650 server_url = h.route_url('home').rstrip('/')
657 server_url = h.route_url('home').rstrip('/')
651 redirect_url = string.Template(user_bookmark.redirect_url) \
658 redirect_url = string.Template(user_bookmark.redirect_url) \
652 .safe_substitute({'server_url': server_url})
659 .safe_substitute({'server_url': server_url})
653
660
654 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
661 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
655 raise HTTPFound(redirect_url)
662 raise HTTPFound(redirect_url)
656
663
657 @LoginRequired()
664 @LoginRequired()
658 @NotAnonymous()
665 @NotAnonymous()
659 def my_account_perms(self):
666 def my_account_perms(self):
660 c = self.load_default_context()
667 c = self.load_default_context()
661 c.active = 'perms'
668 c.active = 'perms'
662
669
663 c.perm_user = c.auth_user
670 c.perm_user = c.auth_user
664 return self._get_template_context(c)
671 return self._get_template_context(c)
665
672
666 @LoginRequired()
673 @LoginRequired()
667 @NotAnonymous()
674 @NotAnonymous()
668 def my_notifications(self):
675 def my_notifications(self):
669 c = self.load_default_context()
676 c = self.load_default_context()
670 c.active = 'notifications'
677 c.active = 'notifications'
671
678
672 return self._get_template_context(c)
679 return self._get_template_context(c)
673
680
674 @LoginRequired()
681 @LoginRequired()
675 @NotAnonymous()
682 @NotAnonymous()
676 @CSRFRequired()
683 @CSRFRequired()
677 def my_notifications_toggle_visibility(self):
684 def my_notifications_toggle_visibility(self):
678 user = self._rhodecode_db_user
685 user = self._rhodecode_db_user
679 new_status = not user.user_data.get('notification_status', True)
686 new_status = not user.user_data.get('notification_status', True)
680 user.update_userdata(notification_status=new_status)
687 user.update_userdata(notification_status=new_status)
681 Session().commit()
688 Session().commit()
682 return user.user_data['notification_status']
689 return user.user_data['notification_status']
683
690
684 def _get_pull_requests_list(self, statuses, filter_type=None):
691 def _get_pull_requests_list(self, statuses, filter_type=None):
685 draw, start, limit = self._extract_chunk(self.request)
692 draw, start, limit = self._extract_chunk(self.request)
686 search_q, order_by, order_dir = self._extract_ordering(self.request)
693 search_q, order_by, order_dir = self._extract_ordering(self.request)
687
694
688 _render = self.request.get_partial_renderer(
695 _render = self.request.get_partial_renderer(
689 'rhodecode:templates/data_table/_dt_elements.mako')
696 'rhodecode:templates/data_table/_dt_elements.mako')
690
697
691 if filter_type == 'awaiting_my_review':
698 if filter_type == 'awaiting_my_review':
692 pull_requests = PullRequestModel().get_im_participating_in_for_review(
699 pull_requests = PullRequestModel().get_im_participating_in_for_review(
693 user_id=self._rhodecode_user.user_id,
700 user_id=self._rhodecode_user.user_id,
694 statuses=statuses, query=search_q,
701 statuses=statuses, query=search_q,
695 offset=start, length=limit, order_by=order_by,
702 offset=start, length=limit, order_by=order_by,
696 order_dir=order_dir)
703 order_dir=order_dir)
697
704
698 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
705 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
699 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
706 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
700 else:
707 else:
701 pull_requests = PullRequestModel().get_im_participating_in(
708 pull_requests = PullRequestModel().get_im_participating_in(
702 user_id=self._rhodecode_user.user_id,
709 user_id=self._rhodecode_user.user_id,
703 statuses=statuses, query=search_q,
710 statuses=statuses, query=search_q,
704 offset=start, length=limit, order_by=order_by,
711 offset=start, length=limit, order_by=order_by,
705 order_dir=order_dir)
712 order_dir=order_dir)
706
713
707 pull_requests_total_count = PullRequestModel().count_im_participating_in(
714 pull_requests_total_count = PullRequestModel().count_im_participating_in(
708 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
715 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
709
716
710 data = []
717 data = []
711 comments_model = CommentsModel()
718 comments_model = CommentsModel()
712 for pr in pull_requests:
719 for pr in pull_requests:
713 repo_id = pr.target_repo_id
720 repo_id = pr.target_repo_id
714 comments_count = comments_model.get_all_comments(
721 comments_count = comments_model.get_all_comments(
715 repo_id, pull_request=pr, include_drafts=False, count_only=True)
722 repo_id, pull_request=pr, include_drafts=False, count_only=True)
716 owned = pr.user_id == self._rhodecode_user.user_id
723 owned = pr.user_id == self._rhodecode_user.user_id
717
724
718 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
725 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
719 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
726 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
720 if review_statuses and review_statuses[4]:
727 if review_statuses and review_statuses[4]:
721 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
728 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
722 my_review_status = statuses[0][1].status
729 my_review_status = statuses[0][1].status
723
730
724 data.append({
731 data.append({
725 'target_repo': _render('pullrequest_target_repo',
732 'target_repo': _render('pullrequest_target_repo',
726 pr.target_repo.repo_name),
733 pr.target_repo.repo_name),
727 'name': _render('pullrequest_name',
734 'name': _render('pullrequest_name',
728 pr.pull_request_id, pr.pull_request_state,
735 pr.pull_request_id, pr.pull_request_state,
729 pr.work_in_progress, pr.target_repo.repo_name,
736 pr.work_in_progress, pr.target_repo.repo_name,
730 short=True),
737 short=True),
731 'name_raw': pr.pull_request_id,
738 'name_raw': pr.pull_request_id,
732 'status': _render('pullrequest_status',
739 'status': _render('pullrequest_status',
733 pr.calculated_review_status()),
740 pr.calculated_review_status()),
734 'my_status': _render('pullrequest_status',
741 'my_status': _render('pullrequest_status',
735 my_review_status),
742 my_review_status),
736 'title': _render('pullrequest_title', pr.title, pr.description),
743 'title': _render('pullrequest_title', pr.title, pr.description),
737 'pr_flow': _render('pullrequest_commit_flow', pr),
744 'pr_flow': _render('pullrequest_commit_flow', pr),
738 'description': h.escape(pr.description),
745 'description': h.escape(pr.description),
739 'updated_on': _render('pullrequest_updated_on',
746 'updated_on': _render('pullrequest_updated_on',
740 h.datetime_to_time(pr.updated_on),
747 h.datetime_to_time(pr.updated_on),
741 pr.versions_count),
748 pr.versions_count),
742 'updated_on_raw': h.datetime_to_time(pr.updated_on),
749 'updated_on_raw': h.datetime_to_time(pr.updated_on),
743 'created_on': _render('pullrequest_updated_on',
750 'created_on': _render('pullrequest_updated_on',
744 h.datetime_to_time(pr.created_on)),
751 h.datetime_to_time(pr.created_on)),
745 'created_on_raw': h.datetime_to_time(pr.created_on),
752 'created_on_raw': h.datetime_to_time(pr.created_on),
746 'state': pr.pull_request_state,
753 'state': pr.pull_request_state,
747 'author': _render('pullrequest_author',
754 'author': _render('pullrequest_author',
748 pr.author.full_contact, ),
755 pr.author.full_contact, ),
749 'author_raw': pr.author.full_name,
756 'author_raw': pr.author.full_name,
750 'comments': _render('pullrequest_comments', comments_count),
757 'comments': _render('pullrequest_comments', comments_count),
751 'comments_raw': comments_count,
758 'comments_raw': comments_count,
752 'closed': pr.is_closed(),
759 'closed': pr.is_closed(),
753 'owned': owned
760 'owned': owned
754 })
761 })
755
762
756 # json used to render the grid
763 # json used to render the grid
757 data = ({
764 data = ({
758 'draw': draw,
765 'draw': draw,
759 'data': data,
766 'data': data,
760 'recordsTotal': pull_requests_total_count,
767 'recordsTotal': pull_requests_total_count,
761 'recordsFiltered': pull_requests_total_count,
768 'recordsFiltered': pull_requests_total_count,
762 })
769 })
763 return data
770 return data
764
771
765 @LoginRequired()
772 @LoginRequired()
766 @NotAnonymous()
773 @NotAnonymous()
767 def my_account_pullrequests(self):
774 def my_account_pullrequests(self):
768 c = self.load_default_context()
775 c = self.load_default_context()
769 c.active = 'pullrequests'
776 c.active = 'pullrequests'
770 req_get = self.request.GET
777 req_get = self.request.GET
771
778
772 c.closed = str2bool(req_get.get('closed'))
779 c.closed = str2bool(req_get.get('closed'))
773 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
780 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
774
781
775 c.selected_filter = 'all'
782 c.selected_filter = 'all'
776 if c.closed:
783 if c.closed:
777 c.selected_filter = 'all_closed'
784 c.selected_filter = 'all_closed'
778 if c.awaiting_my_review:
785 if c.awaiting_my_review:
779 c.selected_filter = 'awaiting_my_review'
786 c.selected_filter = 'awaiting_my_review'
780
787
781 return self._get_template_context(c)
788 return self._get_template_context(c)
782
789
783 @LoginRequired()
790 @LoginRequired()
784 @NotAnonymous()
791 @NotAnonymous()
785 def my_account_pullrequests_data(self):
792 def my_account_pullrequests_data(self):
786 self.load_default_context()
793 self.load_default_context()
787 req_get = self.request.GET
794 req_get = self.request.GET
788
795
789 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
796 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
790 closed = str2bool(req_get.get('closed'))
797 closed = str2bool(req_get.get('closed'))
791
798
792 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
799 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
793 if closed:
800 if closed:
794 statuses += [PullRequest.STATUS_CLOSED]
801 statuses += [PullRequest.STATUS_CLOSED]
795
802
796 filter_type = \
803 filter_type = \
797 'awaiting_my_review' if awaiting_my_review \
804 'awaiting_my_review' if awaiting_my_review \
798 else None
805 else None
799
806
800 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
807 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
801 return data
808 return data
802
809
803 @LoginRequired()
810 @LoginRequired()
804 @NotAnonymous()
811 @NotAnonymous()
805 def my_account_user_group_membership(self):
812 def my_account_user_group_membership(self):
806 c = self.load_default_context()
813 c = self.load_default_context()
807 c.active = 'user_group_membership'
814 c.active = 'user_group_membership'
808 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
815 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
809 for group in self._rhodecode_db_user.group_member]
816 for group in self._rhodecode_db_user.group_member]
810 c.user_groups = ext_json.str_json(groups)
817 c.user_groups = ext_json.str_json(groups)
811 return self._get_template_context(c)
818 return self._get_template_context(c)
@@ -1,5981 +1,5986 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 Database Models for RhodeCode Enterprise
20 Database Models for RhodeCode Enterprise
21 """
21 """
22
22
23 import re
23 import re
24 import os
24 import os
25 import time
25 import time
26 import string
26 import string
27 import logging
27 import logging
28 import datetime
28 import datetime
29 import uuid
29 import uuid
30 import warnings
30 import warnings
31 import ipaddress
31 import ipaddress
32 import functools
32 import functools
33 import traceback
33 import traceback
34 import collections
34 import collections
35
35
36 import pyotp
36 import pyotp
37 from sqlalchemy import (
37 from sqlalchemy import (
38 or_, and_, not_, func, cast, TypeDecorator, event, select,
38 or_, and_, not_, func, cast, TypeDecorator, event, select,
39 true, false, null, union_all,
39 true, false, null, union_all,
40 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
41 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
42 Text, Float, PickleType, BigInteger)
42 Text, Float, PickleType, BigInteger)
43 from sqlalchemy.sql.expression import case
43 from sqlalchemy.sql.expression import case
44 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
44 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
45 from sqlalchemy.orm import (
45 from sqlalchemy.orm import (
46 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
46 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
47 from sqlalchemy.ext.declarative import declared_attr
47 from sqlalchemy.ext.declarative import declared_attr
48 from sqlalchemy.ext.hybrid import hybrid_property
48 from sqlalchemy.ext.hybrid import hybrid_property
49 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 from sqlalchemy.exc import IntegrityError # pragma: no cover
50 from sqlalchemy.dialects.mysql import LONGTEXT
50 from sqlalchemy.dialects.mysql import LONGTEXT
51 from zope.cachedescriptors.property import Lazy as LazyProperty
51 from zope.cachedescriptors.property import Lazy as LazyProperty
52 from pyramid.threadlocal import get_current_request
52 from pyramid.threadlocal import get_current_request
53 from webhelpers2.text import remove_formatting
53 from webhelpers2.text import remove_formatting
54
54
55 from rhodecode import ConfigGet
55 from rhodecode import ConfigGet
56 from rhodecode.lib.str_utils import safe_bytes
56 from rhodecode.lib.str_utils import safe_bytes
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 (
59 from rhodecode.lib.vcs.backends.base import (
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
61 from rhodecode.lib.utils2 import (
61 from rhodecode.lib.utils2 import (
62 str2bool, safe_str, get_commit_safe, sha1_safe,
62 str2bool, safe_str, get_commit_safe, sha1_safe,
63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
65 from rhodecode.lib.jsonalchemy import (
65 from rhodecode.lib.jsonalchemy import (
66 MutationObj, MutationList, JsonType, JsonRaw)
66 MutationObj, MutationList, JsonType, JsonRaw)
67 from rhodecode.lib.hash_utils import sha1
67 from rhodecode.lib.hash_utils import sha1
68 from rhodecode.lib import ext_json
68 from rhodecode.lib import ext_json
69 from rhodecode.lib import enc_utils
69 from rhodecode.lib import enc_utils
70 from rhodecode.lib.ext_json import json, str_json
70 from rhodecode.lib.ext_json import json, str_json
71 from rhodecode.lib.caching_query import FromCache
71 from rhodecode.lib.caching_query import FromCache
72 from rhodecode.lib.exceptions import (
72 from rhodecode.lib.exceptions import (
73 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
73 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
74 from rhodecode.model.meta import Base, Session
74 from rhodecode.model.meta import Base, Session
75
75
76 URL_SEP = '/'
76 URL_SEP = '/'
77 log = logging.getLogger(__name__)
77 log = logging.getLogger(__name__)
78
78
79 # =============================================================================
79 # =============================================================================
80 # BASE CLASSES
80 # BASE CLASSES
81 # =============================================================================
81 # =============================================================================
82
82
83 # this is propagated from .ini file rhodecode.encrypted_values.secret or
83 # this is propagated from .ini file rhodecode.encrypted_values.secret or
84 # beaker.session.secret if first is not set.
84 # beaker.session.secret if first is not set.
85 # and initialized at environment.py
85 # and initialized at environment.py
86 ENCRYPTION_KEY: bytes = b''
86 ENCRYPTION_KEY: bytes = b''
87
87
88 # used to sort permissions by types, '#' used here is not allowed to be in
88 # used to sort permissions by types, '#' used here is not allowed to be in
89 # usernames, and it's very early in sorted string.printable table.
89 # usernames, and it's very early in sorted string.printable table.
90 PERMISSION_TYPE_SORT = {
90 PERMISSION_TYPE_SORT = {
91 'admin': '####',
91 'admin': '####',
92 'write': '###',
92 'write': '###',
93 'read': '##',
93 'read': '##',
94 'none': '#',
94 'none': '#',
95 }
95 }
96
96
97
97
98 def display_user_sort(obj):
98 def display_user_sort(obj):
99 """
99 """
100 Sort function used to sort permissions in .permissions() function of
100 Sort function used to sort permissions in .permissions() function of
101 Repository, RepoGroup, UserGroup. Also it put the default user in front
101 Repository, RepoGroup, UserGroup. Also it put the default user in front
102 of all other resources
102 of all other resources
103 """
103 """
104
104
105 if obj.username == User.DEFAULT_USER:
105 if obj.username == User.DEFAULT_USER:
106 return '#####'
106 return '#####'
107 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
107 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
108 extra_sort_num = '1' # default
108 extra_sort_num = '1' # default
109
109
110 # NOTE(dan): inactive duplicates goes last
110 # NOTE(dan): inactive duplicates goes last
111 if getattr(obj, 'duplicate_perm', None):
111 if getattr(obj, 'duplicate_perm', None):
112 extra_sort_num = '9'
112 extra_sort_num = '9'
113 return prefix + extra_sort_num + obj.username
113 return prefix + extra_sort_num + obj.username
114
114
115
115
116 def display_user_group_sort(obj):
116 def display_user_group_sort(obj):
117 """
117 """
118 Sort function used to sort permissions in .permissions() function of
118 Sort function used to sort permissions in .permissions() function of
119 Repository, RepoGroup, UserGroup. Also it put the default user in front
119 Repository, RepoGroup, UserGroup. Also it put the default user in front
120 of all other resources
120 of all other resources
121 """
121 """
122
122
123 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
123 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
124 return prefix + obj.users_group_name
124 return prefix + obj.users_group_name
125
125
126
126
127 def _hash_key(k):
127 def _hash_key(k):
128 return sha1_safe(k)
128 return sha1_safe(k)
129
129
130
130
131 def in_filter_generator(qry, items, limit=500):
131 def in_filter_generator(qry, items, limit=500):
132 """
132 """
133 Splits IN() into multiple with OR
133 Splits IN() into multiple with OR
134 e.g.::
134 e.g.::
135 cnt = Repository.query().filter(
135 cnt = Repository.query().filter(
136 or_(
136 or_(
137 *in_filter_generator(Repository.repo_id, range(100000))
137 *in_filter_generator(Repository.repo_id, range(100000))
138 )).count()
138 )).count()
139 """
139 """
140 if not items:
140 if not items:
141 # empty list will cause empty query which might cause security issues
141 # empty list will cause empty query which might cause security issues
142 # this can lead to hidden unpleasant results
142 # this can lead to hidden unpleasant results
143 items = [-1]
143 items = [-1]
144
144
145 parts = []
145 parts = []
146 for chunk in range(0, len(items), limit):
146 for chunk in range(0, len(items), limit):
147 parts.append(
147 parts.append(
148 qry.in_(items[chunk: chunk + limit])
148 qry.in_(items[chunk: chunk + limit])
149 )
149 )
150
150
151 return parts
151 return parts
152
152
153
153
154 base_table_args = {
154 base_table_args = {
155 'extend_existing': True,
155 'extend_existing': True,
156 'mysql_engine': 'InnoDB',
156 'mysql_engine': 'InnoDB',
157 'mysql_charset': 'utf8',
157 'mysql_charset': 'utf8',
158 'sqlite_autoincrement': True
158 'sqlite_autoincrement': True
159 }
159 }
160
160
161
161
162 class EncryptedTextValue(TypeDecorator):
162 class EncryptedTextValue(TypeDecorator):
163 """
163 """
164 Special column for encrypted long text data, use like::
164 Special column for encrypted long text data, use like::
165
165
166 value = Column("encrypted_value", EncryptedValue(), nullable=False)
166 value = Column("encrypted_value", EncryptedValue(), nullable=False)
167
167
168 This column is intelligent so if value is in unencrypted form it return
168 This column is intelligent so if value is in unencrypted form it return
169 unencrypted form, but on save it always encrypts
169 unencrypted form, but on save it always encrypts
170 """
170 """
171 cache_ok = True
171 cache_ok = True
172 impl = Text
172 impl = Text
173
173
174 def process_bind_param(self, value, dialect):
174 def process_bind_param(self, value, dialect):
175 """
175 """
176 Setter for storing value
176 Setter for storing value
177 """
177 """
178 import rhodecode
178 import rhodecode
179 if not value:
179 if not value:
180 return value
180 return value
181
181
182 # protect against double encrypting if values is already encrypted
182 # protect against double encrypting if values is already encrypted
183 if value.startswith('enc$aes$') \
183 if value.startswith('enc$aes$') \
184 or value.startswith('enc$aes_hmac$') \
184 or value.startswith('enc$aes_hmac$') \
185 or value.startswith('enc2$'):
185 or value.startswith('enc2$'):
186 raise ValueError('value needs to be in unencrypted format, '
186 raise ValueError('value needs to be in unencrypted format, '
187 'ie. not starting with enc$ or enc2$')
187 'ie. not starting with enc$ or enc2$')
188
188
189 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
189 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
190 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
190 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
191 return safe_str(bytes_val)
191 return safe_str(bytes_val)
192
192
193 def process_result_value(self, value, dialect):
193 def process_result_value(self, value, dialect):
194 """
194 """
195 Getter for retrieving value
195 Getter for retrieving value
196 """
196 """
197
197
198 import rhodecode
198 import rhodecode
199 if not value:
199 if not value:
200 return value
200 return value
201
201
202 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
202 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
203
203
204 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
204 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
205
205
206 return safe_str(bytes_val)
206 return safe_str(bytes_val)
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.items():
234 for k, val in _json_attr.items():
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 select(cls, custom_cls=None):
259 def select(cls, custom_cls=None):
260 """
260 """
261 stmt = cls.select().where(cls.user_id==1)
261 stmt = cls.select().where(cls.user_id==1)
262 # optionally
262 # optionally
263 stmt = cls.select(User.user_id).where(cls.user_id==1)
263 stmt = cls.select(User.user_id).where(cls.user_id==1)
264 result = cls.execute(stmt) | cls.scalars(stmt)
264 result = cls.execute(stmt) | cls.scalars(stmt)
265 """
265 """
266
266
267 if custom_cls:
267 if custom_cls:
268 stmt = select(custom_cls)
268 stmt = select(custom_cls)
269 else:
269 else:
270 stmt = select(cls)
270 stmt = select(cls)
271 return stmt
271 return stmt
272
272
273 @classmethod
273 @classmethod
274 def execute(cls, stmt):
274 def execute(cls, stmt):
275 return Session().execute(stmt)
275 return Session().execute(stmt)
276
276
277 @classmethod
277 @classmethod
278 def scalars(cls, stmt):
278 def scalars(cls, stmt):
279 return Session().scalars(stmt)
279 return Session().scalars(stmt)
280
280
281 @classmethod
281 @classmethod
282 def get(cls, id_):
282 def get(cls, id_):
283 if id_:
283 if id_:
284 return cls.query().get(id_)
284 return cls.query().get(id_)
285
285
286 @classmethod
286 @classmethod
287 def get_or_404(cls, id_):
287 def get_or_404(cls, id_):
288 from pyramid.httpexceptions import HTTPNotFound
288 from pyramid.httpexceptions import HTTPNotFound
289
289
290 try:
290 try:
291 id_ = int(id_)
291 id_ = int(id_)
292 except (TypeError, ValueError):
292 except (TypeError, ValueError):
293 raise HTTPNotFound()
293 raise HTTPNotFound()
294
294
295 res = cls.query().get(id_)
295 res = cls.query().get(id_)
296 if not res:
296 if not res:
297 raise HTTPNotFound()
297 raise HTTPNotFound()
298 return res
298 return res
299
299
300 @classmethod
300 @classmethod
301 def getAll(cls):
301 def getAll(cls):
302 # deprecated and left for backward compatibility
302 # deprecated and left for backward compatibility
303 return cls.get_all()
303 return cls.get_all()
304
304
305 @classmethod
305 @classmethod
306 def get_all(cls):
306 def get_all(cls):
307 return cls.query().all()
307 return cls.query().all()
308
308
309 @classmethod
309 @classmethod
310 def delete(cls, id_):
310 def delete(cls, id_):
311 obj = cls.query().get(id_)
311 obj = cls.query().get(id_)
312 Session().delete(obj)
312 Session().delete(obj)
313
313
314 @classmethod
314 @classmethod
315 def identity_cache(cls, session, attr_name, value):
315 def identity_cache(cls, session, attr_name, value):
316 exist_in_session = []
316 exist_in_session = []
317 for (item_cls, pkey), instance in session.identity_map.items():
317 for (item_cls, pkey), instance in session.identity_map.items():
318 if cls == item_cls and getattr(instance, attr_name) == value:
318 if cls == item_cls and getattr(instance, attr_name) == value:
319 exist_in_session.append(instance)
319 exist_in_session.append(instance)
320 if exist_in_session:
320 if exist_in_session:
321 if len(exist_in_session) == 1:
321 if len(exist_in_session) == 1:
322 return exist_in_session[0]
322 return exist_in_session[0]
323 log.exception(
323 log.exception(
324 'multiple objects with attr %s and '
324 'multiple objects with attr %s and '
325 'value %s found with same name: %r',
325 'value %s found with same name: %r',
326 attr_name, value, exist_in_session)
326 attr_name, value, exist_in_session)
327
327
328 @property
328 @property
329 def cls_name(self):
329 def cls_name(self):
330 return self.__class__.__name__
330 return self.__class__.__name__
331
331
332 def __repr__(self):
332 def __repr__(self):
333 return f'<DB:{self.cls_name}>'
333 return f'<DB:{self.cls_name}>'
334
334
335
335
336 class RhodeCodeSetting(Base, BaseModel):
336 class RhodeCodeSetting(Base, BaseModel):
337 __tablename__ = 'rhodecode_settings'
337 __tablename__ = 'rhodecode_settings'
338 __table_args__ = (
338 __table_args__ = (
339 UniqueConstraint('app_settings_name'),
339 UniqueConstraint('app_settings_name'),
340 base_table_args
340 base_table_args
341 )
341 )
342
342
343 SETTINGS_TYPES = {
343 SETTINGS_TYPES = {
344 'str': safe_str,
344 'str': safe_str,
345 'int': safe_int,
345 'int': safe_int,
346 'unicode': safe_str,
346 'unicode': safe_str,
347 'bool': str2bool,
347 'bool': str2bool,
348 'list': functools.partial(aslist, sep=',')
348 'list': functools.partial(aslist, sep=',')
349 }
349 }
350 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
350 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
351 GLOBAL_CONF_KEY = 'app_settings'
351 GLOBAL_CONF_KEY = 'app_settings'
352
352
353 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
353 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
354 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
354 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
355 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
355 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
356 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
356 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
357
357
358 def __init__(self, key='', val='', type='unicode'):
358 def __init__(self, key='', val='', type='unicode'):
359 self.app_settings_name = key
359 self.app_settings_name = key
360 self.app_settings_type = type
360 self.app_settings_type = type
361 self.app_settings_value = val
361 self.app_settings_value = val
362
362
363 @validates('_app_settings_value')
363 @validates('_app_settings_value')
364 def validate_settings_value(self, key, val):
364 def validate_settings_value(self, key, val):
365 assert type(val) == str
365 assert type(val) == str
366 return val
366 return val
367
367
368 @hybrid_property
368 @hybrid_property
369 def app_settings_value(self):
369 def app_settings_value(self):
370 v = self._app_settings_value
370 v = self._app_settings_value
371 _type = self.app_settings_type
371 _type = self.app_settings_type
372 if _type:
372 if _type:
373 _type = self.app_settings_type.split('.')[0]
373 _type = self.app_settings_type.split('.')[0]
374 # decode the encrypted value
374 # decode the encrypted value
375 if 'encrypted' in self.app_settings_type:
375 if 'encrypted' in self.app_settings_type:
376 cipher = EncryptedTextValue()
376 cipher = EncryptedTextValue()
377 v = safe_str(cipher.process_result_value(v, None))
377 v = safe_str(cipher.process_result_value(v, None))
378
378
379 converter = self.SETTINGS_TYPES.get(_type) or \
379 converter = self.SETTINGS_TYPES.get(_type) or \
380 self.SETTINGS_TYPES['unicode']
380 self.SETTINGS_TYPES['unicode']
381 return converter(v)
381 return converter(v)
382
382
383 @app_settings_value.setter
383 @app_settings_value.setter
384 def app_settings_value(self, val):
384 def app_settings_value(self, val):
385 """
385 """
386 Setter that will always make sure we use unicode in app_settings_value
386 Setter that will always make sure we use unicode in app_settings_value
387
387
388 :param val:
388 :param val:
389 """
389 """
390 val = safe_str(val)
390 val = safe_str(val)
391 # encode the encrypted value
391 # encode the encrypted value
392 if 'encrypted' in self.app_settings_type:
392 if 'encrypted' in self.app_settings_type:
393 cipher = EncryptedTextValue()
393 cipher = EncryptedTextValue()
394 val = safe_str(cipher.process_bind_param(val, None))
394 val = safe_str(cipher.process_bind_param(val, None))
395 self._app_settings_value = val
395 self._app_settings_value = val
396
396
397 @hybrid_property
397 @hybrid_property
398 def app_settings_type(self):
398 def app_settings_type(self):
399 return self._app_settings_type
399 return self._app_settings_type
400
400
401 @app_settings_type.setter
401 @app_settings_type.setter
402 def app_settings_type(self, val):
402 def app_settings_type(self, val):
403 if val.split('.')[0] not in self.SETTINGS_TYPES:
403 if val.split('.')[0] not in self.SETTINGS_TYPES:
404 raise Exception('type must be one of %s got %s'
404 raise Exception('type must be one of %s got %s'
405 % (self.SETTINGS_TYPES.keys(), val))
405 % (self.SETTINGS_TYPES.keys(), val))
406 self._app_settings_type = val
406 self._app_settings_type = val
407
407
408 @classmethod
408 @classmethod
409 def get_by_prefix(cls, prefix):
409 def get_by_prefix(cls, prefix):
410 return RhodeCodeSetting.query()\
410 return RhodeCodeSetting.query()\
411 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
411 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
412 .all()
412 .all()
413
413
414 def __repr__(self):
414 def __repr__(self):
415 return "<%s('%s:%s[%s]')>" % (
415 return "<%s('%s:%s[%s]')>" % (
416 self.cls_name,
416 self.cls_name,
417 self.app_settings_name, self.app_settings_value,
417 self.app_settings_name, self.app_settings_value,
418 self.app_settings_type
418 self.app_settings_type
419 )
419 )
420
420
421
421
422 class RhodeCodeUi(Base, BaseModel):
422 class RhodeCodeUi(Base, BaseModel):
423 __tablename__ = 'rhodecode_ui'
423 __tablename__ = 'rhodecode_ui'
424 __table_args__ = (
424 __table_args__ = (
425 UniqueConstraint('ui_key'),
425 UniqueConstraint('ui_key'),
426 base_table_args
426 base_table_args
427 )
427 )
428 # Sync those values with vcsserver.config.hooks
428 # Sync those values with vcsserver.config.hooks
429
429
430 HOOK_REPO_SIZE = 'changegroup.repo_size'
430 HOOK_REPO_SIZE = 'changegroup.repo_size'
431 # HG
431 # HG
432 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
432 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
433 HOOK_PULL = 'outgoing.pull_logger'
433 HOOK_PULL = 'outgoing.pull_logger'
434 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
434 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
435 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
435 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
436 HOOK_PUSH = 'changegroup.push_logger'
436 HOOK_PUSH = 'changegroup.push_logger'
437 HOOK_PUSH_KEY = 'pushkey.key_push'
437 HOOK_PUSH_KEY = 'pushkey.key_push'
438
438
439 HOOKS_BUILTIN = [
439 HOOKS_BUILTIN = [
440 HOOK_PRE_PULL,
440 HOOK_PRE_PULL,
441 HOOK_PULL,
441 HOOK_PULL,
442 HOOK_PRE_PUSH,
442 HOOK_PRE_PUSH,
443 HOOK_PRETX_PUSH,
443 HOOK_PRETX_PUSH,
444 HOOK_PUSH,
444 HOOK_PUSH,
445 HOOK_PUSH_KEY,
445 HOOK_PUSH_KEY,
446 ]
446 ]
447
447
448 # TODO: johbo: Unify way how hooks are configured for git and hg,
448 # TODO: johbo: Unify way how hooks are configured for git and hg,
449 # git part is currently hardcoded.
449 # git part is currently hardcoded.
450
450
451 # SVN PATTERNS
451 # SVN PATTERNS
452 SVN_BRANCH_ID = 'vcs_svn_branch'
452 SVN_BRANCH_ID = 'vcs_svn_branch'
453 SVN_TAG_ID = 'vcs_svn_tag'
453 SVN_TAG_ID = 'vcs_svn_tag'
454
454
455 ui_id = Column(
455 ui_id = Column(
456 "ui_id", Integer(), nullable=False, unique=True, default=None,
456 "ui_id", Integer(), nullable=False, unique=True, default=None,
457 primary_key=True)
457 primary_key=True)
458 ui_section = Column(
458 ui_section = Column(
459 "ui_section", String(255), nullable=True, unique=None, default=None)
459 "ui_section", String(255), nullable=True, unique=None, default=None)
460 ui_key = Column(
460 ui_key = Column(
461 "ui_key", String(255), nullable=True, unique=None, default=None)
461 "ui_key", String(255), nullable=True, unique=None, default=None)
462 ui_value = Column(
462 ui_value = Column(
463 "ui_value", String(255), nullable=True, unique=None, default=None)
463 "ui_value", String(255), nullable=True, unique=None, default=None)
464 ui_active = Column(
464 ui_active = Column(
465 "ui_active", Boolean(), nullable=True, unique=None, default=True)
465 "ui_active", Boolean(), nullable=True, unique=None, default=True)
466
466
467 def __repr__(self):
467 def __repr__(self):
468 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
468 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
469 self.ui_key, self.ui_value)
469 self.ui_key, self.ui_value)
470
470
471
471
472 class RepoRhodeCodeSetting(Base, BaseModel):
472 class RepoRhodeCodeSetting(Base, BaseModel):
473 __tablename__ = 'repo_rhodecode_settings'
473 __tablename__ = 'repo_rhodecode_settings'
474 __table_args__ = (
474 __table_args__ = (
475 UniqueConstraint(
475 UniqueConstraint(
476 'app_settings_name', 'repository_id',
476 'app_settings_name', 'repository_id',
477 name='uq_repo_rhodecode_setting_name_repo_id'),
477 name='uq_repo_rhodecode_setting_name_repo_id'),
478 base_table_args
478 base_table_args
479 )
479 )
480
480
481 repository_id = Column(
481 repository_id = Column(
482 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
482 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
483 nullable=False)
483 nullable=False)
484 app_settings_id = Column(
484 app_settings_id = Column(
485 "app_settings_id", Integer(), nullable=False, unique=True,
485 "app_settings_id", Integer(), nullable=False, unique=True,
486 default=None, primary_key=True)
486 default=None, primary_key=True)
487 app_settings_name = Column(
487 app_settings_name = Column(
488 "app_settings_name", String(255), nullable=True, unique=None,
488 "app_settings_name", String(255), nullable=True, unique=None,
489 default=None)
489 default=None)
490 _app_settings_value = Column(
490 _app_settings_value = Column(
491 "app_settings_value", String(4096), nullable=True, unique=None,
491 "app_settings_value", String(4096), nullable=True, unique=None,
492 default=None)
492 default=None)
493 _app_settings_type = Column(
493 _app_settings_type = Column(
494 "app_settings_type", String(255), nullable=True, unique=None,
494 "app_settings_type", String(255), nullable=True, unique=None,
495 default=None)
495 default=None)
496
496
497 repository = relationship('Repository', viewonly=True)
497 repository = relationship('Repository', viewonly=True)
498
498
499 def __init__(self, repository_id, key='', val='', type='unicode'):
499 def __init__(self, repository_id, key='', val='', type='unicode'):
500 self.repository_id = repository_id
500 self.repository_id = repository_id
501 self.app_settings_name = key
501 self.app_settings_name = key
502 self.app_settings_type = type
502 self.app_settings_type = type
503 self.app_settings_value = val
503 self.app_settings_value = val
504
504
505 @validates('_app_settings_value')
505 @validates('_app_settings_value')
506 def validate_settings_value(self, key, val):
506 def validate_settings_value(self, key, val):
507 assert type(val) == str
507 assert type(val) == str
508 return val
508 return val
509
509
510 @hybrid_property
510 @hybrid_property
511 def app_settings_value(self):
511 def app_settings_value(self):
512 v = self._app_settings_value
512 v = self._app_settings_value
513 type_ = self.app_settings_type
513 type_ = self.app_settings_type
514 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
514 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
515 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
515 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
516 return converter(v)
516 return converter(v)
517
517
518 @app_settings_value.setter
518 @app_settings_value.setter
519 def app_settings_value(self, val):
519 def app_settings_value(self, val):
520 """
520 """
521 Setter that will always make sure we use unicode in app_settings_value
521 Setter that will always make sure we use unicode in app_settings_value
522
522
523 :param val:
523 :param val:
524 """
524 """
525 self._app_settings_value = safe_str(val)
525 self._app_settings_value = safe_str(val)
526
526
527 @hybrid_property
527 @hybrid_property
528 def app_settings_type(self):
528 def app_settings_type(self):
529 return self._app_settings_type
529 return self._app_settings_type
530
530
531 @app_settings_type.setter
531 @app_settings_type.setter
532 def app_settings_type(self, val):
532 def app_settings_type(self, val):
533 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
533 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
534 if val not in SETTINGS_TYPES:
534 if val not in SETTINGS_TYPES:
535 raise Exception('type must be one of %s got %s'
535 raise Exception('type must be one of %s got %s'
536 % (SETTINGS_TYPES.keys(), val))
536 % (SETTINGS_TYPES.keys(), val))
537 self._app_settings_type = val
537 self._app_settings_type = val
538
538
539 def __repr__(self):
539 def __repr__(self):
540 return "<%s('%s:%s:%s[%s]')>" % (
540 return "<%s('%s:%s:%s[%s]')>" % (
541 self.cls_name, self.repository.repo_name,
541 self.cls_name, self.repository.repo_name,
542 self.app_settings_name, self.app_settings_value,
542 self.app_settings_name, self.app_settings_value,
543 self.app_settings_type
543 self.app_settings_type
544 )
544 )
545
545
546
546
547 class RepoRhodeCodeUi(Base, BaseModel):
547 class RepoRhodeCodeUi(Base, BaseModel):
548 __tablename__ = 'repo_rhodecode_ui'
548 __tablename__ = 'repo_rhodecode_ui'
549 __table_args__ = (
549 __table_args__ = (
550 UniqueConstraint(
550 UniqueConstraint(
551 'repository_id', 'ui_section', 'ui_key',
551 'repository_id', 'ui_section', 'ui_key',
552 name='uq_repo_rhodecode_ui_repository_id_section_key'),
552 name='uq_repo_rhodecode_ui_repository_id_section_key'),
553 base_table_args
553 base_table_args
554 )
554 )
555
555
556 repository_id = Column(
556 repository_id = Column(
557 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
557 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
558 nullable=False)
558 nullable=False)
559 ui_id = Column(
559 ui_id = Column(
560 "ui_id", Integer(), nullable=False, unique=True, default=None,
560 "ui_id", Integer(), nullable=False, unique=True, default=None,
561 primary_key=True)
561 primary_key=True)
562 ui_section = Column(
562 ui_section = Column(
563 "ui_section", String(255), nullable=True, unique=None, default=None)
563 "ui_section", String(255), nullable=True, unique=None, default=None)
564 ui_key = Column(
564 ui_key = Column(
565 "ui_key", String(255), nullable=True, unique=None, default=None)
565 "ui_key", String(255), nullable=True, unique=None, default=None)
566 ui_value = Column(
566 ui_value = Column(
567 "ui_value", String(255), nullable=True, unique=None, default=None)
567 "ui_value", String(255), nullable=True, unique=None, default=None)
568 ui_active = Column(
568 ui_active = Column(
569 "ui_active", Boolean(), nullable=True, unique=None, default=True)
569 "ui_active", Boolean(), nullable=True, unique=None, default=True)
570
570
571 repository = relationship('Repository', viewonly=True)
571 repository = relationship('Repository', viewonly=True)
572
572
573 def __repr__(self):
573 def __repr__(self):
574 return '<%s[%s:%s]%s=>%s]>' % (
574 return '<%s[%s:%s]%s=>%s]>' % (
575 self.cls_name, self.repository.repo_name,
575 self.cls_name, self.repository.repo_name,
576 self.ui_section, self.ui_key, self.ui_value)
576 self.ui_section, self.ui_key, self.ui_value)
577
577
578
578
579 class User(Base, BaseModel):
579 class User(Base, BaseModel):
580 __tablename__ = 'users'
580 __tablename__ = 'users'
581 __table_args__ = (
581 __table_args__ = (
582 UniqueConstraint('username'), UniqueConstraint('email'),
582 UniqueConstraint('username'), UniqueConstraint('email'),
583 Index('u_username_idx', 'username'),
583 Index('u_username_idx', 'username'),
584 Index('u_email_idx', 'email'),
584 Index('u_email_idx', 'email'),
585 base_table_args
585 base_table_args
586 )
586 )
587
587
588 DEFAULT_USER = 'default'
588 DEFAULT_USER = 'default'
589 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
589 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
590 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
590 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
591 RECOVERY_CODES_COUNT = 10
591 RECOVERY_CODES_COUNT = 10
592
592
593 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
593 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
594 username = Column("username", String(255), nullable=True, unique=None, default=None)
594 username = Column("username", String(255), nullable=True, unique=None, default=None)
595 password = Column("password", String(255), nullable=True, unique=None, default=None)
595 password = Column("password", String(255), nullable=True, unique=None, default=None)
596 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
596 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
597 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
597 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
598 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
598 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
599 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
599 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
600 _email = Column("email", String(255), nullable=True, unique=None, default=None)
600 _email = Column("email", String(255), nullable=True, unique=None, default=None)
601 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
601 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
602 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
602 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
603 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
603 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
604
604
605 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
605 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
606 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
606 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
607 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
607 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
608 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
608 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
609 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
609 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
610 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
610 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
611
611
612 user_log = relationship('UserLog', back_populates='user')
612 user_log = relationship('UserLog', back_populates='user')
613 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
613 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
614
614
615 repositories = relationship('Repository', back_populates='user')
615 repositories = relationship('Repository', back_populates='user')
616 repository_groups = relationship('RepoGroup', back_populates='user')
616 repository_groups = relationship('RepoGroup', back_populates='user')
617 user_groups = relationship('UserGroup', back_populates='user')
617 user_groups = relationship('UserGroup', back_populates='user')
618
618
619 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
619 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
620 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
620 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
621
621
622 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
622 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
623 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
623 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
624 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
624 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
625
625
626 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
626 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
627
627
628 notifications = relationship('UserNotification', cascade='all', back_populates='user')
628 notifications = relationship('UserNotification', cascade='all', back_populates='user')
629 # notifications assigned to this user
629 # notifications assigned to this user
630 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
630 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
631 # comments created by this user
631 # comments created by this user
632 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
632 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
633 # user profile extra info
633 # user profile extra info
634 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
634 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
635 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
635 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
636 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
636 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
637 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
637 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
638
638
639 # gists
639 # gists
640 user_gists = relationship('Gist', cascade='all', back_populates='owner')
640 user_gists = relationship('Gist', cascade='all', back_populates='owner')
641 # user pull requests
641 # user pull requests
642 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
642 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
643
643
644 # external identities
644 # external identities
645 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
645 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
646 # review rules
646 # review rules
647 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
647 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
648
648
649 # artifacts owned
649 # artifacts owned
650 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
650 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
651
651
652 # no cascade, set NULL
652 # no cascade, set NULL
653 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
653 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
654
654
655 def __repr__(self):
655 def __repr__(self):
656 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
656 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
657
657
658 @hybrid_property
658 @hybrid_property
659 def email(self):
659 def email(self):
660 return self._email
660 return self._email
661
661
662 @email.setter
662 @email.setter
663 def email(self, val):
663 def email(self, val):
664 self._email = val.lower() if val else None
664 self._email = val.lower() if val else None
665
665
666 @hybrid_property
666 @hybrid_property
667 def first_name(self):
667 def first_name(self):
668 from rhodecode.lib import helpers as h
668 from rhodecode.lib import helpers as h
669 if self.name:
669 if self.name:
670 return h.escape(self.name)
670 return h.escape(self.name)
671 return self.name
671 return self.name
672
672
673 @hybrid_property
673 @hybrid_property
674 def last_name(self):
674 def last_name(self):
675 from rhodecode.lib import helpers as h
675 from rhodecode.lib import helpers as h
676 if self.lastname:
676 if self.lastname:
677 return h.escape(self.lastname)
677 return h.escape(self.lastname)
678 return self.lastname
678 return self.lastname
679
679
680 @hybrid_property
680 @hybrid_property
681 def api_key(self):
681 def api_key(self):
682 """
682 """
683 Fetch if exist an auth-token with role ALL connected to this user
683 Fetch if exist an auth-token with role ALL connected to this user
684 """
684 """
685 user_auth_token = UserApiKeys.query()\
685 user_auth_token = UserApiKeys.query()\
686 .filter(UserApiKeys.user_id == self.user_id)\
686 .filter(UserApiKeys.user_id == self.user_id)\
687 .filter(or_(UserApiKeys.expires == -1,
687 .filter(or_(UserApiKeys.expires == -1,
688 UserApiKeys.expires >= time.time()))\
688 UserApiKeys.expires >= time.time()))\
689 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
689 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
690 if user_auth_token:
690 if user_auth_token:
691 user_auth_token = user_auth_token.api_key
691 user_auth_token = user_auth_token.api_key
692
692
693 return user_auth_token
693 return user_auth_token
694
694
695 @api_key.setter
695 @api_key.setter
696 def api_key(self, val):
696 def api_key(self, val):
697 # don't allow to set API key this is deprecated for now
697 # don't allow to set API key this is deprecated for now
698 self._api_key = None
698 self._api_key = None
699
699
700 @property
700 @property
701 def reviewer_pull_requests(self):
701 def reviewer_pull_requests(self):
702 return PullRequestReviewers.query() \
702 return PullRequestReviewers.query() \
703 .options(joinedload(PullRequestReviewers.pull_request)) \
703 .options(joinedload(PullRequestReviewers.pull_request)) \
704 .filter(PullRequestReviewers.user_id == self.user_id) \
704 .filter(PullRequestReviewers.user_id == self.user_id) \
705 .all()
705 .all()
706
706
707 @property
707 @property
708 def firstname(self):
708 def firstname(self):
709 # alias for future
709 # alias for future
710 return self.name
710 return self.name
711
711
712 @property
712 @property
713 def emails(self):
713 def emails(self):
714 other = UserEmailMap.query()\
714 other = UserEmailMap.query()\
715 .filter(UserEmailMap.user == self) \
715 .filter(UserEmailMap.user == self) \
716 .order_by(UserEmailMap.email_id.asc()) \
716 .order_by(UserEmailMap.email_id.asc()) \
717 .all()
717 .all()
718 return [self.email] + [x.email for x in other]
718 return [self.email] + [x.email for x in other]
719
719
720 def emails_cached(self):
720 def emails_cached(self):
721 emails = []
721 emails = []
722 if self.user_id != self.get_default_user_id():
722 if self.user_id != self.get_default_user_id():
723 emails = UserEmailMap.query()\
723 emails = UserEmailMap.query()\
724 .filter(UserEmailMap.user == self) \
724 .filter(UserEmailMap.user == self) \
725 .order_by(UserEmailMap.email_id.asc())
725 .order_by(UserEmailMap.email_id.asc())
726
726
727 emails = emails.options(
727 emails = emails.options(
728 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
728 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
729 )
729 )
730
730
731 return [self.email] + [x.email for x in emails]
731 return [self.email] + [x.email for x in emails]
732
732
733 @property
733 @property
734 def auth_tokens(self):
734 def auth_tokens(self):
735 auth_tokens = self.get_auth_tokens()
735 auth_tokens = self.get_auth_tokens()
736 return [x.api_key for x in auth_tokens]
736 return [x.api_key for x in auth_tokens]
737
737
738 def get_auth_tokens(self):
738 def get_auth_tokens(self):
739 return UserApiKeys.query()\
739 return UserApiKeys.query()\
740 .filter(UserApiKeys.user == self)\
740 .filter(UserApiKeys.user == self)\
741 .order_by(UserApiKeys.user_api_key_id.asc())\
741 .order_by(UserApiKeys.user_api_key_id.asc())\
742 .all()
742 .all()
743
743
744 @LazyProperty
744 @LazyProperty
745 def feed_token(self):
745 def feed_token(self):
746 return self.get_feed_token()
746 return self.get_feed_token()
747
747
748 def get_feed_token(self, cache=True):
748 def get_feed_token(self, cache=True):
749 feed_tokens = UserApiKeys.query()\
749 feed_tokens = UserApiKeys.query()\
750 .filter(UserApiKeys.user == self)\
750 .filter(UserApiKeys.user == self)\
751 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
751 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
752 if cache:
752 if cache:
753 feed_tokens = feed_tokens.options(
753 feed_tokens = feed_tokens.options(
754 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
754 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
755
755
756 feed_tokens = feed_tokens.all()
756 feed_tokens = feed_tokens.all()
757 if feed_tokens:
757 if feed_tokens:
758 return feed_tokens[0].api_key
758 return feed_tokens[0].api_key
759 return 'NO_FEED_TOKEN_AVAILABLE'
759 return 'NO_FEED_TOKEN_AVAILABLE'
760
760
761 @LazyProperty
761 @LazyProperty
762 def artifact_token(self):
762 def artifact_token(self):
763 return self.get_artifact_token()
763 return self.get_artifact_token()
764
764
765 def get_artifact_token(self, cache=True):
765 def get_artifact_token(self, cache=True):
766 artifacts_tokens = UserApiKeys.query()\
766 artifacts_tokens = UserApiKeys.query()\
767 .filter(UserApiKeys.user == self) \
767 .filter(UserApiKeys.user == self) \
768 .filter(or_(UserApiKeys.expires == -1,
768 .filter(or_(UserApiKeys.expires == -1,
769 UserApiKeys.expires >= time.time())) \
769 UserApiKeys.expires >= time.time())) \
770 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
770 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
771
771
772 if cache:
772 if cache:
773 artifacts_tokens = artifacts_tokens.options(
773 artifacts_tokens = artifacts_tokens.options(
774 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
774 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
775
775
776 artifacts_tokens = artifacts_tokens.all()
776 artifacts_tokens = artifacts_tokens.all()
777 if artifacts_tokens:
777 if artifacts_tokens:
778 return artifacts_tokens[0].api_key
778 return artifacts_tokens[0].api_key
779 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
779 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
780
780
781 def get_or_create_artifact_token(self):
781 def get_or_create_artifact_token(self):
782 artifacts_tokens = UserApiKeys.query()\
782 artifacts_tokens = UserApiKeys.query()\
783 .filter(UserApiKeys.user == self) \
783 .filter(UserApiKeys.user == self) \
784 .filter(or_(UserApiKeys.expires == -1,
784 .filter(or_(UserApiKeys.expires == -1,
785 UserApiKeys.expires >= time.time())) \
785 UserApiKeys.expires >= time.time())) \
786 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
786 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
787
787
788 artifacts_tokens = artifacts_tokens.all()
788 artifacts_tokens = artifacts_tokens.all()
789 if artifacts_tokens:
789 if artifacts_tokens:
790 return artifacts_tokens[0].api_key
790 return artifacts_tokens[0].api_key
791 else:
791 else:
792 from rhodecode.model.auth_token import AuthTokenModel
792 from rhodecode.model.auth_token import AuthTokenModel
793 artifact_token = AuthTokenModel().create(
793 artifact_token = AuthTokenModel().create(
794 self, 'auto-generated-artifact-token',
794 self, 'auto-generated-artifact-token',
795 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
795 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
796 Session.commit()
796 Session.commit()
797 return artifact_token.api_key
797 return artifact_token.api_key
798
798
799 @hybrid_property
799 @hybrid_property
800 def secret_2fa(self):
800 def secret_2fa(self):
801 if not self.user_data.get('secret_2fa'):
801 if not self.user_data.get('secret_2fa'):
802 secret = pyotp.random_base32()
802 secret = pyotp.random_base32()
803 self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(secret, enc_key=ENCRYPTION_KEY)))
803 self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(secret, enc_key=ENCRYPTION_KEY)))
804 return secret
804 return secret
805 return safe_str(
805 return safe_str(
806 enc_utils.decrypt_value(self.user_data['secret_2fa'],
806 enc_utils.decrypt_value(self.user_data['secret_2fa'],
807 enc_key=ENCRYPTION_KEY,
807 enc_key=ENCRYPTION_KEY,
808 strict_mode=ConfigGet().get_bool('rhodecode.encrypted_values.strict',
808 strict_mode=ConfigGet().get_bool('rhodecode.encrypted_values.strict',
809 missing=True)
809 missing=True)
810 )
810 )
811 )
811 )
812
812
813 def is_totp_valid(self, received_code):
813 def is_totp_valid(self, received_code):
814 totp = pyotp.TOTP(self.secret_2fa)
814 totp = pyotp.TOTP(self.secret_2fa)
815 return totp.verify(received_code)
815 return totp.verify(received_code)
816
816
817 def is_2fa_recovery_code_valid(self, received_code):
817 def is_2fa_recovery_code_valid(self, received_code):
818 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
818 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
819 recovery_codes = list(map(
819 recovery_codes = list(map(
820 lambda x: safe_str(
820 lambda x: safe_str(
821 enc_utils.decrypt_value(
821 enc_utils.decrypt_value(
822 x,
822 x,
823 enc_key=ENCRYPTION_KEY,
823 enc_key=ENCRYPTION_KEY,
824 strict_mode=ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
824 strict_mode=ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
825 )),
825 )),
826 encrypted_recovery_codes))
826 encrypted_recovery_codes))
827 if received_code in recovery_codes:
827 if received_code in recovery_codes:
828 encrypted_recovery_codes.pop(recovery_codes.index(received_code))
828 encrypted_recovery_codes.pop(recovery_codes.index(received_code))
829 self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes)
829 self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes)
830 return True
830 return True
831 return False
831 return False
832
832
833 @hybrid_property
833 @hybrid_property
834 def has_forced_2fa(self):
834 def has_forced_2fa(self):
835 """
835 """
836 Checks if 2fa was forced for ALL users (including current one)
836 Checks if 2fa was forced for ALL users (including current one)
837 """
837 """
838 from rhodecode.model.settings import SettingsModel
838 from rhodecode.model.settings import SettingsModel
839 # So now we're supporting only auth_rhodecode_global_2f
839 # So now we're supporting only auth_rhodecode_global_2f
840 if value := SettingsModel().get_setting_by_name('auth_rhodecode_global_2fa'):
840 if value := SettingsModel().get_setting_by_name('auth_rhodecode_global_2fa'):
841 return value.app_settings_value
841 return value.app_settings_value
842 return False
842 return False
843
843
844 @hybrid_property
844 @hybrid_property
845 def has_enabled_2fa(self):
845 def has_enabled_2fa(self):
846 """
846 """
847 Checks if 2fa was enabled by user
847 Checks if 2fa was enabled by user
848 """
848 """
849 if value := self.has_forced_2fa:
849 if value := self.has_forced_2fa:
850 return value
850 return value
851 return self.user_data.get('enabled_2fa', False)
851 return self.user_data.get('enabled_2fa', False)
852
852
853 @has_enabled_2fa.setter
853 @has_enabled_2fa.setter
854 def has_enabled_2fa(self, val):
854 def has_enabled_2fa(self, val):
855 val = str2bool(val)
855 val = str2bool(val)
856 self.update_userdata(enabled_2fa=str2bool(val))
856 self.update_userdata(enabled_2fa=str2bool(val))
857 if not val:
857 if not val:
858 self.update_userdata(secret_2fa=None, recovery_codes_2fa=[])
858 self.update_userdata(secret_2fa=None, recovery_codes_2fa=[])
859 Session().commit()
859 Session().commit()
860
860
861 def get_2fa_recovery_codes(self):
861 def get_2fa_recovery_codes(self):
862 """
862 """
863 Creates 2fa recovery codes
863 Creates 2fa recovery codes
864 """
864 """
865 recovery_codes = self.user_data.get('recovery_codes_2fa', [])
865 recovery_codes = self.user_data.get('recovery_codes_2fa', [])
866 encrypted_codes = []
866 encrypted_codes = []
867 if not recovery_codes:
867 if not recovery_codes:
868 for _ in range(self.RECOVERY_CODES_COUNT):
868 for _ in range(self.RECOVERY_CODES_COUNT):
869 recovery_code = pyotp.random_base32()
869 recovery_code = pyotp.random_base32()
870 recovery_codes.append(recovery_code)
870 recovery_codes.append(recovery_code)
871 encrypted_codes.append(safe_str(enc_utils.encrypt_value(recovery_code, enc_key=ENCRYPTION_KEY)))
871 encrypted_codes.append(safe_str(enc_utils.encrypt_value(recovery_code, enc_key=ENCRYPTION_KEY)))
872 self.update_userdata(recovery_codes_2fa=encrypted_codes)
872 self.update_userdata(recovery_codes_2fa=encrypted_codes)
873 return recovery_codes
873 return recovery_codes
874 # User should not check the same recovery codes more than once
874 # User should not check the same recovery codes more than once
875 return []
875 return []
876
876
877 def regenerate_2fa_recovery_codes(self):
877 def regenerate_2fa_recovery_codes(self):
878 """
878 """
879 Regenerates 2fa recovery codes upon request
879 Regenerates 2fa recovery codes upon request
880 """
880 """
881 self.update_userdata(recovery_codes_2fa=[])
881 self.update_userdata(recovery_codes_2fa=[])
882 Session().flush()
882 Session().flush()
883 new_recovery_codes = self.get_2fa_recovery_codes()
883 new_recovery_codes = self.get_2fa_recovery_codes()
884 Session().commit()
884 Session().commit()
885 return new_recovery_codes
885 return new_recovery_codes
886
886
887 @classmethod
887 @classmethod
888 def get(cls, user_id, cache=False):
889 if not user_id:
890 return
891
892 user = cls.query()
893 if cache:
894 user = user.options(
895 FromCache("sql_cache_short", f"get_users_{user_id}"))
896 return user.get(user_id)
897
898 @classmethod
899 def extra_valid_auth_tokens(cls, user, role=None):
888 def extra_valid_auth_tokens(cls, user, role=None):
900 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
889 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
901 .filter(or_(UserApiKeys.expires == -1,
890 .filter(or_(UserApiKeys.expires == -1,
902 UserApiKeys.expires >= time.time()))
891 UserApiKeys.expires >= time.time()))
903 if role:
892 if role:
904 tokens = tokens.filter(or_(UserApiKeys.role == role,
893 tokens = tokens.filter(or_(UserApiKeys.role == role,
905 UserApiKeys.role == UserApiKeys.ROLE_ALL))
894 UserApiKeys.role == UserApiKeys.ROLE_ALL))
906 return tokens.all()
895 return tokens.all()
907
896
908 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
897 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
909 from rhodecode.lib import auth
898 from rhodecode.lib import auth
910
899
911 log.debug('Trying to authenticate user: %s via auth-token, '
900 log.debug('Trying to authenticate user: %s via auth-token, '
912 'and roles: %s', self, roles)
901 'and roles: %s', self, roles)
913
902
914 if not auth_token:
903 if not auth_token:
915 return False
904 return False
916
905
917 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
906 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
918 tokens_q = UserApiKeys.query()\
907 tokens_q = UserApiKeys.query()\
919 .filter(UserApiKeys.user_id == self.user_id)\
908 .filter(UserApiKeys.user_id == self.user_id)\
920 .filter(or_(UserApiKeys.expires == -1,
909 .filter(or_(UserApiKeys.expires == -1,
921 UserApiKeys.expires >= time.time()))
910 UserApiKeys.expires >= time.time()))
922
911
923 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
912 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
924
913
925 crypto_backend = auth.crypto_backend()
914 crypto_backend = auth.crypto_backend()
926 enc_token_map = {}
915 enc_token_map = {}
927 plain_token_map = {}
916 plain_token_map = {}
928 for token in tokens_q:
917 for token in tokens_q:
929 if token.api_key.startswith(crypto_backend.ENC_PREF):
918 if token.api_key.startswith(crypto_backend.ENC_PREF):
930 enc_token_map[token.api_key] = token
919 enc_token_map[token.api_key] = token
931 else:
920 else:
932 plain_token_map[token.api_key] = token
921 plain_token_map[token.api_key] = token
933 log.debug(
922 log.debug(
934 'Found %s plain and %s encrypted tokens to check for authentication for this user',
923 'Found %s plain and %s encrypted tokens to check for authentication for this user',
935 len(plain_token_map), len(enc_token_map))
924 len(plain_token_map), len(enc_token_map))
936
925
937 # plain token match comes first
926 # plain token match comes first
938 match = plain_token_map.get(auth_token)
927 match = plain_token_map.get(auth_token)
939
928
940 # check encrypted tokens now
929 # check encrypted tokens now
941 if not match:
930 if not match:
942 for token_hash, token in enc_token_map.items():
931 for token_hash, token in enc_token_map.items():
943 # NOTE(marcink): this is expensive to calculate, but most secure
932 # NOTE(marcink): this is expensive to calculate, but most secure
944 if crypto_backend.hash_check(auth_token, token_hash):
933 if crypto_backend.hash_check(auth_token, token_hash):
945 match = token
934 match = token
946 break
935 break
947
936
948 if match:
937 if match:
949 log.debug('Found matching token %s', match)
938 log.debug('Found matching token %s', match)
950 if match.repo_id:
939 if match.repo_id:
951 log.debug('Found scope, checking for scope match of token %s', match)
940 log.debug('Found scope, checking for scope match of token %s', match)
952 if match.repo_id == scope_repo_id:
941 if match.repo_id == scope_repo_id:
953 return True
942 return True
954 else:
943 else:
955 log.debug(
944 log.debug(
956 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
945 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
957 'and calling scope is:%s, skipping further checks',
946 'and calling scope is:%s, skipping further checks',
958 match.repo, scope_repo_id)
947 match.repo, scope_repo_id)
959 return False
948 return False
960 else:
949 else:
961 return True
950 return True
962
951
963 return False
952 return False
964
953
965 @property
954 @property
966 def ip_addresses(self):
955 def ip_addresses(self):
967 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
956 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
968 return [x.ip_addr for x in ret]
957 return [x.ip_addr for x in ret]
969
958
970 @property
959 @property
971 def username_and_name(self):
960 def username_and_name(self):
972 return f'{self.username} ({self.first_name} {self.last_name})'
961 return f'{self.username} ({self.first_name} {self.last_name})'
973
962
974 @property
963 @property
975 def username_or_name_or_email(self):
964 def username_or_name_or_email(self):
976 full_name = self.full_name if self.full_name != ' ' else None
965 full_name = self.full_name if self.full_name != ' ' else None
977 return self.username or full_name or self.email
966 return self.username or full_name or self.email
978
967
979 @property
968 @property
980 def full_name(self):
969 def full_name(self):
981 return f'{self.first_name} {self.last_name}'
970 return f'{self.first_name} {self.last_name}'
982
971
983 @property
972 @property
984 def full_name_or_username(self):
973 def full_name_or_username(self):
985 return (f'{self.first_name} {self.last_name}'
974 return (f'{self.first_name} {self.last_name}'
986 if (self.first_name and self.last_name) else self.username)
975 if (self.first_name and self.last_name) else self.username)
987
976
988 @property
977 @property
989 def full_contact(self):
978 def full_contact(self):
990 return f'{self.first_name} {self.last_name} <{self.email}>'
979 return f'{self.first_name} {self.last_name} <{self.email}>'
991
980
992 @property
981 @property
993 def short_contact(self):
982 def short_contact(self):
994 return f'{self.first_name} {self.last_name}'
983 return f'{self.first_name} {self.last_name}'
995
984
996 @property
985 @property
997 def is_admin(self):
986 def is_admin(self):
998 return self.admin
987 return self.admin
999
988
1000 @property
989 @property
1001 def language(self):
990 def language(self):
1002 return self.user_data.get('language')
991 return self.user_data.get('language')
1003
992
1004 def AuthUser(self, **kwargs):
993 def AuthUser(self, **kwargs):
1005 """
994 """
1006 Returns instance of AuthUser for this user
995 Returns instance of AuthUser for this user
1007 """
996 """
1008 from rhodecode.lib.auth import AuthUser
997 from rhodecode.lib.auth import AuthUser
1009 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
998 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
1010
999
1011 @hybrid_property
1000 @hybrid_property
1012 def user_data(self):
1001 def user_data(self):
1013 if not self._user_data:
1002 if not self._user_data:
1014 return {}
1003 return {}
1015
1004
1016 try:
1005 try:
1017 return json.loads(self._user_data) or {}
1006 return json.loads(self._user_data) or {}
1018 except TypeError:
1007 except TypeError:
1019 return {}
1008 return {}
1020
1009
1021 @user_data.setter
1010 @user_data.setter
1022 def user_data(self, val):
1011 def user_data(self, val):
1023 if not isinstance(val, dict):
1012 if not isinstance(val, dict):
1024 raise Exception('user_data must be dict, got %s' % type(val))
1013 raise Exception(f'user_data must be dict, got {type(val)}')
1025 try:
1014 try:
1026 self._user_data = safe_bytes(json.dumps(val))
1015 self._user_data = safe_bytes(json.dumps(val))
1027 except Exception:
1016 except Exception:
1028 log.error(traceback.format_exc())
1017 log.error(traceback.format_exc())
1029
1018
1030 @classmethod
1019 @classmethod
1020 def get(cls, user_id, cache=False):
1021 if not user_id:
1022 return
1023
1024 user = cls.query()
1025 if cache:
1026 user = user.options(
1027 FromCache("sql_cache_short", f"get_users_{user_id}"))
1028 return user.get(user_id)
1029
1030 @classmethod
1031 def get_by_username(cls, username, case_insensitive=False,
1031 def get_by_username(cls, username, case_insensitive=False,
1032 cache=False):
1032 cache=False):
1033
1033
1034 if case_insensitive:
1034 if case_insensitive:
1035 q = cls.select().where(
1035 q = cls.select().where(
1036 func.lower(cls.username) == func.lower(username))
1036 func.lower(cls.username) == func.lower(username))
1037 else:
1037 else:
1038 q = cls.select().where(cls.username == username)
1038 q = cls.select().where(cls.username == username)
1039
1039
1040 if cache:
1040 if cache:
1041 hash_key = _hash_key(username)
1041 hash_key = _hash_key(username)
1042 q = q.options(
1042 q = q.options(
1043 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
1043 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
1044
1044
1045 return cls.execute(q).scalar_one_or_none()
1045 return cls.execute(q).scalar_one_or_none()
1046
1046
1047 @classmethod
1047 @classmethod
1048 def get_by_username_or_primary_email(cls, user_identifier):
1048 def get_by_username_or_primary_email(cls, user_identifier):
1049 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
1049 qs = union_all(cls.select().where(func.lower(cls.username) == func.lower(user_identifier)),
1050 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
1050 cls.select().where(func.lower(cls.email) == func.lower(user_identifier)))
1051 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
1051 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
1052
1052
1053 @classmethod
1053 @classmethod
1054 def get_by_auth_token(cls, auth_token, cache=False):
1054 def get_by_auth_token(cls, auth_token, cache=False):
1055
1055
1056 q = cls.select(User)\
1056 q = cls.select(User)\
1057 .join(UserApiKeys)\
1057 .join(UserApiKeys)\
1058 .where(UserApiKeys.api_key == auth_token)\
1058 .where(UserApiKeys.api_key == auth_token)\
1059 .where(or_(UserApiKeys.expires == -1,
1059 .where(or_(UserApiKeys.expires == -1,
1060 UserApiKeys.expires >= time.time()))
1060 UserApiKeys.expires >= time.time()))
1061
1061
1062 if cache:
1062 if cache:
1063 q = q.options(
1063 q = q.options(
1064 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
1064 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
1065
1065
1066 matched_user = cls.execute(q).scalar_one_or_none()
1066 matched_user = cls.execute(q).scalar_one_or_none()
1067
1067
1068 return matched_user
1068 return matched_user
1069
1069
1070 @classmethod
1070 @classmethod
1071 def get_by_email(cls, email, case_insensitive=False, cache=False):
1071 def get_by_email(cls, email, case_insensitive=False, cache=False):
1072
1072
1073 if case_insensitive:
1073 if case_insensitive:
1074 q = cls.select().where(func.lower(cls.email) == func.lower(email))
1074 q = cls.select().where(func.lower(cls.email) == func.lower(email))
1075 else:
1075 else:
1076 q = cls.select().where(cls.email == email)
1076 q = cls.select().where(cls.email == email)
1077
1077
1078 if cache:
1078 if cache:
1079 email_key = _hash_key(email)
1079 email_key = _hash_key(email)
1080 q = q.options(
1080 q = q.options(
1081 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
1081 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
1082
1082
1083 ret = cls.execute(q).scalar_one_or_none()
1083 ret = cls.execute(q).scalar_one_or_none()
1084
1084
1085 if ret is None:
1085 if ret is None:
1086 q = cls.select(UserEmailMap)
1086 q = cls.select(UserEmailMap)
1087 # try fetching in alternate email map
1087 # try fetching in alternate email map
1088 if case_insensitive:
1088 if case_insensitive:
1089 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
1089 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
1090 else:
1090 else:
1091 q = q.where(UserEmailMap.email == email)
1091 q = q.where(UserEmailMap.email == email)
1092 q = q.options(joinedload(UserEmailMap.user))
1092 q = q.options(joinedload(UserEmailMap.user))
1093 if cache:
1093 if cache:
1094 q = q.options(
1094 q = q.options(
1095 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1095 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1096
1096
1097 result = cls.execute(q).scalar_one_or_none()
1097 result = cls.execute(q).scalar_one_or_none()
1098 ret = getattr(result, 'user', None)
1098 ret = getattr(result, 'user', None)
1099
1099
1100 return ret
1100 return ret
1101
1101
1102 @classmethod
1102 @classmethod
1103 def get_from_cs_author(cls, author):
1103 def get_from_cs_author(cls, author):
1104 """
1104 """
1105 Tries to get User objects out of commit author string
1105 Tries to get User objects out of commit author string
1106
1106
1107 :param author:
1107 :param author:
1108 """
1108 """
1109 from rhodecode.lib.helpers import email, author_name
1109 from rhodecode.lib.helpers import email, author_name
1110 # Valid email in the attribute passed, see if they're in the system
1110 # Valid email in the attribute passed, see if they're in the system
1111 _email = email(author)
1111 _email = email(author)
1112 if _email:
1112 if _email:
1113 user = cls.get_by_email(_email, case_insensitive=True)
1113 user = cls.get_by_email(_email, case_insensitive=True)
1114 if user:
1114 if user:
1115 return user
1115 return user
1116 # Maybe we can match by username?
1116 # Maybe we can match by username?
1117 _author = author_name(author)
1117 _author = author_name(author)
1118 user = cls.get_by_username(_author, case_insensitive=True)
1118 user = cls.get_by_username(_author, case_insensitive=True)
1119 if user:
1119 if user:
1120 return user
1120 return user
1121
1121
1122 def update_userdata(self, **kwargs):
1122 def update_userdata(self, **kwargs):
1123 usr = self
1123 usr = self
1124 old = usr.user_data
1124 old = usr.user_data
1125 old.update(**kwargs)
1125 old.update(**kwargs)
1126 usr.user_data = old
1126 usr.user_data = old
1127 Session().add(usr)
1127 Session().add(usr)
1128 log.debug('updated userdata with %s', kwargs)
1128 log.debug('updated userdata with %s', kwargs)
1129
1129
1130 def update_lastlogin(self):
1130 def update_lastlogin(self):
1131 """Update user lastlogin"""
1131 """Update user lastlogin"""
1132 self.last_login = datetime.datetime.now()
1132 self.last_login = datetime.datetime.now()
1133 Session().add(self)
1133 Session().add(self)
1134 log.debug('updated user %s lastlogin', self.username)
1134 log.debug('updated user %s lastlogin', self.username)
1135
1135
1136 def update_password(self, new_password):
1136 def update_password(self, new_password):
1137 from rhodecode.lib.auth import get_crypt_password
1137 from rhodecode.lib.auth import get_crypt_password
1138
1138
1139 self.password = get_crypt_password(new_password)
1139 self.password = get_crypt_password(new_password)
1140 Session().add(self)
1140 Session().add(self)
1141
1141
1142 @classmethod
1142 @classmethod
1143 def get_first_super_admin(cls):
1143 def get_first_super_admin(cls):
1144 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1144 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1145 user = cls.scalars(stmt).first()
1145 user = cls.scalars(stmt).first()
1146
1146
1147 if user is None:
1147 if user is None:
1148 raise Exception('FATAL: Missing administrative account!')
1148 raise Exception('FATAL: Missing administrative account!')
1149 return user
1149 return user
1150
1150
1151 @classmethod
1151 @classmethod
1152 def get_all_super_admins(cls, only_active=False):
1152 def get_all_super_admins(cls, only_active=False):
1153 """
1153 """
1154 Returns all admin accounts sorted by username
1154 Returns all admin accounts sorted by username
1155 """
1155 """
1156 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1156 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1157 if only_active:
1157 if only_active:
1158 qry = qry.filter(User.active == true())
1158 qry = qry.filter(User.active == true())
1159 return qry.all()
1159 return qry.all()
1160
1160
1161 @classmethod
1161 @classmethod
1162 def get_all_user_ids(cls, only_active=True):
1162 def get_all_user_ids(cls, only_active=True):
1163 """
1163 """
1164 Returns all users IDs
1164 Returns all users IDs
1165 """
1165 """
1166 qry = Session().query(User.user_id)
1166 qry = Session().query(User.user_id)
1167
1167
1168 if only_active:
1168 if only_active:
1169 qry = qry.filter(User.active == true())
1169 qry = qry.filter(User.active == true())
1170 return [x.user_id for x in qry]
1170 return [x.user_id for x in qry]
1171
1171
1172 @classmethod
1172 @classmethod
1173 def get_default_user(cls, cache=False, refresh=False):
1173 def get_default_user(cls, cache=False, refresh=False):
1174 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1174 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1175 if user is None:
1175 if user is None:
1176 raise Exception('FATAL: Missing default account!')
1176 raise Exception('FATAL: Missing default account!')
1177 if refresh:
1177 if refresh:
1178 # The default user might be based on outdated state which
1178 # The default user might be based on outdated state which
1179 # has been loaded from the cache.
1179 # has been loaded from the cache.
1180 # A call to refresh() ensures that the
1180 # A call to refresh() ensures that the
1181 # latest state from the database is used.
1181 # latest state from the database is used.
1182 Session().refresh(user)
1182 Session().refresh(user)
1183
1183
1184 return user
1184 return user
1185
1185
1186 @classmethod
1186 @classmethod
1187 def get_default_user_id(cls):
1187 def get_default_user_id(cls):
1188 import rhodecode
1188 import rhodecode
1189 return rhodecode.CONFIG['default_user_id']
1189 return rhodecode.CONFIG['default_user_id']
1190
1190
1191 def _get_default_perms(self, user, suffix=''):
1191 def _get_default_perms(self, user, suffix=''):
1192 from rhodecode.model.permission import PermissionModel
1192 from rhodecode.model.permission import PermissionModel
1193 return PermissionModel().get_default_perms(user.user_perms, suffix)
1193 return PermissionModel().get_default_perms(user.user_perms, suffix)
1194
1194
1195 def get_default_perms(self, suffix=''):
1195 def get_default_perms(self, suffix=''):
1196 return self._get_default_perms(self, suffix)
1196 return self._get_default_perms(self, suffix)
1197
1197
1198 def get_api_data(self, include_secrets=False, details='full'):
1198 def get_api_data(self, include_secrets=False, details='full'):
1199 """
1199 """
1200 Common function for generating user related data for API
1200 Common function for generating user related data for API
1201
1201
1202 :param include_secrets: By default secrets in the API data will be replaced
1202 :param include_secrets: By default secrets in the API data will be replaced
1203 by a placeholder value to prevent exposing this data by accident. In case
1203 by a placeholder value to prevent exposing this data by accident. In case
1204 this data shall be exposed, set this flag to ``True``.
1204 this data shall be exposed, set this flag to ``True``.
1205
1205
1206 :param details: details can be 'basic|full' basic gives only a subset of
1206 :param details: details can be 'basic|full' basic gives only a subset of
1207 the available user information that includes user_id, name and emails.
1207 the available user information that includes user_id, name and emails.
1208 """
1208 """
1209 user = self
1209 user = self
1210 user_data = self.user_data
1210 user_data = self.user_data
1211 data = {
1211 data = {
1212 'user_id': user.user_id,
1212 'user_id': user.user_id,
1213 'username': user.username,
1213 'username': user.username,
1214 'firstname': user.name,
1214 'firstname': user.name,
1215 'lastname': user.lastname,
1215 'lastname': user.lastname,
1216 'description': user.description,
1216 'description': user.description,
1217 'email': user.email,
1217 'email': user.email,
1218 'emails': user.emails,
1218 'emails': user.emails,
1219 }
1219 }
1220 if details == 'basic':
1220 if details == 'basic':
1221 return data
1221 return data
1222
1222
1223 auth_token_length = 40
1223 auth_token_length = 40
1224 auth_token_replacement = '*' * auth_token_length
1224 auth_token_replacement = '*' * auth_token_length
1225
1225
1226 extras = {
1226 extras = {
1227 'auth_tokens': [auth_token_replacement],
1227 'auth_tokens': [auth_token_replacement],
1228 'active': user.active,
1228 'active': user.active,
1229 'admin': user.admin,
1229 'admin': user.admin,
1230 'extern_type': user.extern_type,
1230 'extern_type': user.extern_type,
1231 'extern_name': user.extern_name,
1231 'extern_name': user.extern_name,
1232 'last_login': user.last_login,
1232 'last_login': user.last_login,
1233 'last_activity': user.last_activity,
1233 'last_activity': user.last_activity,
1234 'ip_addresses': user.ip_addresses,
1234 'ip_addresses': user.ip_addresses,
1235 'language': user_data.get('language')
1235 'language': user_data.get('language')
1236 }
1236 }
1237 data.update(extras)
1237 data.update(extras)
1238
1238
1239 if include_secrets:
1239 if include_secrets:
1240 data['auth_tokens'] = user.auth_tokens
1240 data['auth_tokens'] = user.auth_tokens
1241 return data
1241 return data
1242
1242
1243 def __json__(self):
1243 def __json__(self):
1244 data = {
1244 data = {
1245 'full_name': self.full_name,
1245 'full_name': self.full_name,
1246 'full_name_or_username': self.full_name_or_username,
1246 'full_name_or_username': self.full_name_or_username,
1247 'short_contact': self.short_contact,
1247 'short_contact': self.short_contact,
1248 'full_contact': self.full_contact,
1248 'full_contact': self.full_contact,
1249 }
1249 }
1250 data.update(self.get_api_data())
1250 data.update(self.get_api_data())
1251 return data
1251 return data
1252
1252
1253
1253
1254 class UserApiKeys(Base, BaseModel):
1254 class UserApiKeys(Base, BaseModel):
1255 __tablename__ = 'user_api_keys'
1255 __tablename__ = 'user_api_keys'
1256 __table_args__ = (
1256 __table_args__ = (
1257 Index('uak_api_key_idx', 'api_key'),
1257 Index('uak_api_key_idx', 'api_key'),
1258 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1258 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1259 base_table_args
1259 base_table_args
1260 )
1260 )
1261
1261
1262 # ApiKey role
1262 # ApiKey role
1263 ROLE_ALL = 'token_role_all'
1263 ROLE_ALL = 'token_role_all'
1264 ROLE_VCS = 'token_role_vcs'
1264 ROLE_VCS = 'token_role_vcs'
1265 ROLE_API = 'token_role_api'
1265 ROLE_API = 'token_role_api'
1266 ROLE_HTTP = 'token_role_http'
1266 ROLE_HTTP = 'token_role_http'
1267 ROLE_FEED = 'token_role_feed'
1267 ROLE_FEED = 'token_role_feed'
1268 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1268 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1269 # The last one is ignored in the list as we only
1269 # The last one is ignored in the list as we only
1270 # use it for one action, and cannot be created by users
1270 # use it for one action, and cannot be created by users
1271 ROLE_PASSWORD_RESET = 'token_password_reset'
1271 ROLE_PASSWORD_RESET = 'token_password_reset'
1272
1272
1273 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1273 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1274
1274
1275 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1275 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1276 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1276 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1277 api_key = Column("api_key", String(255), nullable=False, unique=True)
1277 api_key = Column("api_key", String(255), nullable=False, unique=True)
1278 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1278 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1279 expires = Column('expires', Float(53), nullable=False)
1279 expires = Column('expires', Float(53), nullable=False)
1280 role = Column('role', String(255), nullable=True)
1280 role = Column('role', String(255), nullable=True)
1281 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1281 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1282
1282
1283 # scope columns
1283 # scope columns
1284 repo_id = Column(
1284 repo_id = Column(
1285 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1285 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1286 nullable=True, unique=None, default=None)
1286 nullable=True, unique=None, default=None)
1287 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1287 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1288
1288
1289 repo_group_id = Column(
1289 repo_group_id = Column(
1290 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1290 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1291 nullable=True, unique=None, default=None)
1291 nullable=True, unique=None, default=None)
1292 repo_group = relationship('RepoGroup', lazy='joined')
1292 repo_group = relationship('RepoGroup', lazy='joined')
1293
1293
1294 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1294 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1295
1295
1296 def __repr__(self):
1296 def __repr__(self):
1297 return f"<{self.cls_name}('{self.role}')>"
1297 return f"<{self.cls_name}('{self.role}')>"
1298
1298
1299 def __json__(self):
1299 def __json__(self):
1300 data = {
1300 data = {
1301 'auth_token': self.api_key,
1301 'auth_token': self.api_key,
1302 'role': self.role,
1302 'role': self.role,
1303 'scope': self.scope_humanized,
1303 'scope': self.scope_humanized,
1304 'expired': self.expired
1304 'expired': self.expired
1305 }
1305 }
1306 return data
1306 return data
1307
1307
1308 def get_api_data(self, include_secrets=False):
1308 def get_api_data(self, include_secrets=False):
1309 data = self.__json__()
1309 data = self.__json__()
1310 if include_secrets:
1310 if include_secrets:
1311 return data
1311 return data
1312 else:
1312 else:
1313 data['auth_token'] = self.token_obfuscated
1313 data['auth_token'] = self.token_obfuscated
1314 return data
1314 return data
1315
1315
1316 @hybrid_property
1316 @hybrid_property
1317 def description_safe(self):
1317 def description_safe(self):
1318 from rhodecode.lib import helpers as h
1318 from rhodecode.lib import helpers as h
1319 return h.escape(self.description)
1319 return h.escape(self.description)
1320
1320
1321 @property
1321 @property
1322 def expired(self):
1322 def expired(self):
1323 if self.expires == -1:
1323 if self.expires == -1:
1324 return False
1324 return False
1325 return time.time() > self.expires
1325 return time.time() > self.expires
1326
1326
1327 @classmethod
1327 @classmethod
1328 def _get_role_name(cls, role):
1328 def _get_role_name(cls, role):
1329 return {
1329 return {
1330 cls.ROLE_ALL: _('all'),
1330 cls.ROLE_ALL: _('all'),
1331 cls.ROLE_HTTP: _('http/web interface'),
1331 cls.ROLE_HTTP: _('http/web interface'),
1332 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1332 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1333 cls.ROLE_API: _('api calls'),
1333 cls.ROLE_API: _('api calls'),
1334 cls.ROLE_FEED: _('feed access'),
1334 cls.ROLE_FEED: _('feed access'),
1335 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1335 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1336 }.get(role, role)
1336 }.get(role, role)
1337
1337
1338 @classmethod
1338 @classmethod
1339 def _get_role_description(cls, role):
1339 def _get_role_description(cls, role):
1340 return {
1340 return {
1341 cls.ROLE_ALL: _('Token for all actions.'),
1341 cls.ROLE_ALL: _('Token for all actions.'),
1342 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1342 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1343 'login using `api_access_controllers_whitelist` functionality.'),
1343 'login using `api_access_controllers_whitelist` functionality.'),
1344 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1344 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1345 'Requires auth_token authentication plugin to be active. <br/>'
1345 'Requires auth_token authentication plugin to be active. <br/>'
1346 'Such Token should be used then instead of a password to '
1346 'Such Token should be used then instead of a password to '
1347 'interact with a repository, and additionally can be '
1347 'interact with a repository, and additionally can be '
1348 'limited to single repository using repo scope.'),
1348 'limited to single repository using repo scope.'),
1349 cls.ROLE_API: _('Token limited to api calls.'),
1349 cls.ROLE_API: _('Token limited to api calls.'),
1350 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1350 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1351 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1351 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1352 }.get(role, role)
1352 }.get(role, role)
1353
1353
1354 @property
1354 @property
1355 def role_humanized(self):
1355 def role_humanized(self):
1356 return self._get_role_name(self.role)
1356 return self._get_role_name(self.role)
1357
1357
1358 def _get_scope(self):
1358 def _get_scope(self):
1359 if self.repo:
1359 if self.repo:
1360 return 'Repository: {}'.format(self.repo.repo_name)
1360 return 'Repository: {}'.format(self.repo.repo_name)
1361 if self.repo_group:
1361 if self.repo_group:
1362 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1362 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1363 return 'Global'
1363 return 'Global'
1364
1364
1365 @property
1365 @property
1366 def scope_humanized(self):
1366 def scope_humanized(self):
1367 return self._get_scope()
1367 return self._get_scope()
1368
1368
1369 @property
1369 @property
1370 def token_obfuscated(self):
1370 def token_obfuscated(self):
1371 if self.api_key:
1371 if self.api_key:
1372 return self.api_key[:4] + "****"
1372 return self.api_key[:4] + "****"
1373
1373
1374
1374
1375 class UserEmailMap(Base, BaseModel):
1375 class UserEmailMap(Base, BaseModel):
1376 __tablename__ = 'user_email_map'
1376 __tablename__ = 'user_email_map'
1377 __table_args__ = (
1377 __table_args__ = (
1378 Index('uem_email_idx', 'email'),
1378 Index('uem_email_idx', 'email'),
1379 Index('uem_user_id_idx', 'user_id'),
1379 Index('uem_user_id_idx', 'user_id'),
1380 UniqueConstraint('email'),
1380 UniqueConstraint('email'),
1381 base_table_args
1381 base_table_args
1382 )
1382 )
1383
1383
1384 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1384 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1385 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1385 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1386 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1386 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1387 user = relationship('User', lazy='joined', back_populates='user_emails')
1387 user = relationship('User', lazy='joined', back_populates='user_emails')
1388
1388
1389 @validates('_email')
1389 @validates('_email')
1390 def validate_email(self, key, email):
1390 def validate_email(self, key, email):
1391 # check if this email is not main one
1391 # check if this email is not main one
1392 main_email = Session().query(User).filter(User.email == email).scalar()
1392 main_email = Session().query(User).filter(User.email == email).scalar()
1393 if main_email is not None:
1393 if main_email is not None:
1394 raise AttributeError('email %s is present is user table' % email)
1394 raise AttributeError('email %s is present is user table' % email)
1395 return email
1395 return email
1396
1396
1397 @hybrid_property
1397 @hybrid_property
1398 def email(self):
1398 def email(self):
1399 return self._email
1399 return self._email
1400
1400
1401 @email.setter
1401 @email.setter
1402 def email(self, val):
1402 def email(self, val):
1403 self._email = val.lower() if val else None
1403 self._email = val.lower() if val else None
1404
1404
1405
1405
1406 class UserIpMap(Base, BaseModel):
1406 class UserIpMap(Base, BaseModel):
1407 __tablename__ = 'user_ip_map'
1407 __tablename__ = 'user_ip_map'
1408 __table_args__ = (
1408 __table_args__ = (
1409 UniqueConstraint('user_id', 'ip_addr'),
1409 UniqueConstraint('user_id', 'ip_addr'),
1410 base_table_args
1410 base_table_args
1411 )
1411 )
1412
1412
1413 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1413 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1414 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1414 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1415 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1415 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1416 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1416 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1417 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1417 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1418 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1418 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1419
1419
1420 @hybrid_property
1420 @hybrid_property
1421 def description_safe(self):
1421 def description_safe(self):
1422 from rhodecode.lib import helpers as h
1422 from rhodecode.lib import helpers as h
1423 return h.escape(self.description)
1423 return h.escape(self.description)
1424
1424
1425 @classmethod
1425 @classmethod
1426 def _get_ip_range(cls, ip_addr):
1426 def _get_ip_range(cls, ip_addr):
1427 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1427 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1428 return [str(net.network_address), str(net.broadcast_address)]
1428 return [str(net.network_address), str(net.broadcast_address)]
1429
1429
1430 def __json__(self):
1430 def __json__(self):
1431 return {
1431 return {
1432 'ip_addr': self.ip_addr,
1432 'ip_addr': self.ip_addr,
1433 'ip_range': self._get_ip_range(self.ip_addr),
1433 'ip_range': self._get_ip_range(self.ip_addr),
1434 }
1434 }
1435
1435
1436 def __repr__(self):
1436 def __repr__(self):
1437 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1437 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1438
1438
1439
1439
1440 class UserSshKeys(Base, BaseModel):
1440 class UserSshKeys(Base, BaseModel):
1441 __tablename__ = 'user_ssh_keys'
1441 __tablename__ = 'user_ssh_keys'
1442 __table_args__ = (
1442 __table_args__ = (
1443 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1443 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1444
1444
1445 UniqueConstraint('ssh_key_fingerprint'),
1445 UniqueConstraint('ssh_key_fingerprint'),
1446
1446
1447 base_table_args
1447 base_table_args
1448 )
1448 )
1449
1449
1450 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1450 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1451 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1451 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1452 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1452 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1453
1453
1454 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1454 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1455
1455
1456 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1456 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1457 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1457 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1458 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1458 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1459
1459
1460 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1460 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1461
1461
1462 def __json__(self):
1462 def __json__(self):
1463 data = {
1463 data = {
1464 'ssh_fingerprint': self.ssh_key_fingerprint,
1464 'ssh_fingerprint': self.ssh_key_fingerprint,
1465 'description': self.description,
1465 'description': self.description,
1466 'created_on': self.created_on
1466 'created_on': self.created_on
1467 }
1467 }
1468 return data
1468 return data
1469
1469
1470 def get_api_data(self):
1470 def get_api_data(self):
1471 data = self.__json__()
1471 data = self.__json__()
1472 return data
1472 return data
1473
1473
1474
1474
1475 class UserLog(Base, BaseModel):
1475 class UserLog(Base, BaseModel):
1476 __tablename__ = 'user_logs'
1476 __tablename__ = 'user_logs'
1477 __table_args__ = (
1477 __table_args__ = (
1478 base_table_args,
1478 base_table_args,
1479 )
1479 )
1480
1480
1481 VERSION_1 = 'v1'
1481 VERSION_1 = 'v1'
1482 VERSION_2 = 'v2'
1482 VERSION_2 = 'v2'
1483 VERSIONS = [VERSION_1, VERSION_2]
1483 VERSIONS = [VERSION_1, VERSION_2]
1484
1484
1485 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1485 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1486 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1486 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1487 username = Column("username", String(255), nullable=True, unique=None, default=None)
1487 username = Column("username", String(255), nullable=True, unique=None, default=None)
1488 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1488 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1489 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1489 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1490 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1490 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1491 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1491 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1492 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1492 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1493
1493
1494 version = Column("version", String(255), nullable=True, default=VERSION_1)
1494 version = Column("version", String(255), nullable=True, default=VERSION_1)
1495 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1495 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1496 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1496 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1497 user = relationship('User', cascade='', back_populates='user_log')
1497 user = relationship('User', cascade='', back_populates='user_log')
1498 repository = relationship('Repository', cascade='', back_populates='logs')
1498 repository = relationship('Repository', cascade='', back_populates='logs')
1499
1499
1500 def __repr__(self):
1500 def __repr__(self):
1501 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1501 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1502
1502
1503 def __json__(self):
1503 def __json__(self):
1504 return {
1504 return {
1505 'user_id': self.user_id,
1505 'user_id': self.user_id,
1506 'username': self.username,
1506 'username': self.username,
1507 'repository_id': self.repository_id,
1507 'repository_id': self.repository_id,
1508 'repository_name': self.repository_name,
1508 'repository_name': self.repository_name,
1509 'user_ip': self.user_ip,
1509 'user_ip': self.user_ip,
1510 'action_date': self.action_date,
1510 'action_date': self.action_date,
1511 'action': self.action,
1511 'action': self.action,
1512 }
1512 }
1513
1513
1514 @hybrid_property
1514 @hybrid_property
1515 def entry_id(self):
1515 def entry_id(self):
1516 return self.user_log_id
1516 return self.user_log_id
1517
1517
1518 @property
1518 @property
1519 def action_as_day(self):
1519 def action_as_day(self):
1520 return datetime.date(*self.action_date.timetuple()[:3])
1520 return datetime.date(*self.action_date.timetuple()[:3])
1521
1521
1522
1522
1523 class UserGroup(Base, BaseModel):
1523 class UserGroup(Base, BaseModel):
1524 __tablename__ = 'users_groups'
1524 __tablename__ = 'users_groups'
1525 __table_args__ = (
1525 __table_args__ = (
1526 base_table_args,
1526 base_table_args,
1527 )
1527 )
1528
1528
1529 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1529 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1530 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1530 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1531 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1531 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1532 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1532 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1533 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1533 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1534 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1534 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1535 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1535 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1536 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1536 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1537
1537
1538 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1538 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1539 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1539 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1540 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1540 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1541 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1541 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1542 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1542 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1543
1543
1544 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all', back_populates='target_user_group')
1544 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all', back_populates='target_user_group')
1545
1545
1546 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1546 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1547 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1547 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1548
1548
1549 @classmethod
1549 @classmethod
1550 def _load_group_data(cls, column):
1550 def _load_group_data(cls, column):
1551 if not column:
1551 if not column:
1552 return {}
1552 return {}
1553
1553
1554 try:
1554 try:
1555 return json.loads(column) or {}
1555 return json.loads(column) or {}
1556 except TypeError:
1556 except TypeError:
1557 return {}
1557 return {}
1558
1558
1559 @hybrid_property
1559 @hybrid_property
1560 def description_safe(self):
1560 def description_safe(self):
1561 from rhodecode.lib import helpers as h
1561 from rhodecode.lib import helpers as h
1562 return h.escape(self.user_group_description)
1562 return h.escape(self.user_group_description)
1563
1563
1564 @hybrid_property
1564 @hybrid_property
1565 def group_data(self):
1565 def group_data(self):
1566 return self._load_group_data(self._group_data)
1566 return self._load_group_data(self._group_data)
1567
1567
1568 @group_data.expression
1568 @group_data.expression
1569 def group_data(self, **kwargs):
1569 def group_data(self, **kwargs):
1570 return self._group_data
1570 return self._group_data
1571
1571
1572 @group_data.setter
1572 @group_data.setter
1573 def group_data(self, val):
1573 def group_data(self, val):
1574 try:
1574 try:
1575 self._group_data = json.dumps(val)
1575 self._group_data = json.dumps(val)
1576 except Exception:
1576 except Exception:
1577 log.error(traceback.format_exc())
1577 log.error(traceback.format_exc())
1578
1578
1579 @classmethod
1579 @classmethod
1580 def _load_sync(cls, group_data):
1580 def _load_sync(cls, group_data):
1581 if group_data:
1581 if group_data:
1582 return group_data.get('extern_type')
1582 return group_data.get('extern_type')
1583
1583
1584 @property
1584 @property
1585 def sync(self):
1585 def sync(self):
1586 return self._load_sync(self.group_data)
1586 return self._load_sync(self.group_data)
1587
1587
1588 def __repr__(self):
1588 def __repr__(self):
1589 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1589 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1590
1590
1591 @classmethod
1591 @classmethod
1592 def get_by_group_name(cls, group_name, cache=False,
1592 def get_by_group_name(cls, group_name, cache=False,
1593 case_insensitive=False):
1593 case_insensitive=False):
1594 if case_insensitive:
1594 if case_insensitive:
1595 q = cls.query().filter(func.lower(cls.users_group_name) ==
1595 q = cls.query().filter(func.lower(cls.users_group_name) ==
1596 func.lower(group_name))
1596 func.lower(group_name))
1597
1597
1598 else:
1598 else:
1599 q = cls.query().filter(cls.users_group_name == group_name)
1599 q = cls.query().filter(cls.users_group_name == group_name)
1600 if cache:
1600 if cache:
1601 name_key = _hash_key(group_name)
1601 name_key = _hash_key(group_name)
1602 q = q.options(
1602 q = q.options(
1603 FromCache("sql_cache_short", f"get_group_{name_key}"))
1603 FromCache("sql_cache_short", f"get_group_{name_key}"))
1604 return q.scalar()
1604 return q.scalar()
1605
1605
1606 @classmethod
1606 @classmethod
1607 def get(cls, user_group_id, cache=False):
1607 def get(cls, user_group_id, cache=False):
1608 if not user_group_id:
1608 if not user_group_id:
1609 return
1609 return
1610
1610
1611 user_group = cls.query()
1611 user_group = cls.query()
1612 if cache:
1612 if cache:
1613 user_group = user_group.options(
1613 user_group = user_group.options(
1614 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1614 FromCache("sql_cache_short", f"get_users_group_{user_group_id}"))
1615 return user_group.get(user_group_id)
1615 return user_group.get(user_group_id)
1616
1616
1617 def permissions(self, with_admins=True, with_owner=True,
1617 def permissions(self, with_admins=True, with_owner=True,
1618 expand_from_user_groups=False):
1618 expand_from_user_groups=False):
1619 """
1619 """
1620 Permissions for user groups
1620 Permissions for user groups
1621 """
1621 """
1622 _admin_perm = 'usergroup.admin'
1622 _admin_perm = 'usergroup.admin'
1623
1623
1624 owner_row = []
1624 owner_row = []
1625 if with_owner:
1625 if with_owner:
1626 usr = AttributeDict(self.user.get_dict())
1626 usr = AttributeDict(self.user.get_dict())
1627 usr.owner_row = True
1627 usr.owner_row = True
1628 usr.permission = _admin_perm
1628 usr.permission = _admin_perm
1629 owner_row.append(usr)
1629 owner_row.append(usr)
1630
1630
1631 super_admin_ids = []
1631 super_admin_ids = []
1632 super_admin_rows = []
1632 super_admin_rows = []
1633 if with_admins:
1633 if with_admins:
1634 for usr in User.get_all_super_admins():
1634 for usr in User.get_all_super_admins():
1635 super_admin_ids.append(usr.user_id)
1635 super_admin_ids.append(usr.user_id)
1636 # if this admin is also owner, don't double the record
1636 # if this admin is also owner, don't double the record
1637 if usr.user_id == owner_row[0].user_id:
1637 if usr.user_id == owner_row[0].user_id:
1638 owner_row[0].admin_row = True
1638 owner_row[0].admin_row = True
1639 else:
1639 else:
1640 usr = AttributeDict(usr.get_dict())
1640 usr = AttributeDict(usr.get_dict())
1641 usr.admin_row = True
1641 usr.admin_row = True
1642 usr.permission = _admin_perm
1642 usr.permission = _admin_perm
1643 super_admin_rows.append(usr)
1643 super_admin_rows.append(usr)
1644
1644
1645 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1645 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1646 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1646 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1647 joinedload(UserUserGroupToPerm.user),
1647 joinedload(UserUserGroupToPerm.user),
1648 joinedload(UserUserGroupToPerm.permission),)
1648 joinedload(UserUserGroupToPerm.permission),)
1649
1649
1650 # get owners and admins and permissions. We do a trick of re-writing
1650 # get owners and admins and permissions. We do a trick of re-writing
1651 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1651 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1652 # has a global reference and changing one object propagates to all
1652 # has a global reference and changing one object propagates to all
1653 # others. This means if admin is also an owner admin_row that change
1653 # others. This means if admin is also an owner admin_row that change
1654 # would propagate to both objects
1654 # would propagate to both objects
1655 perm_rows = []
1655 perm_rows = []
1656 for _usr in q.all():
1656 for _usr in q.all():
1657 usr = AttributeDict(_usr.user.get_dict())
1657 usr = AttributeDict(_usr.user.get_dict())
1658 # if this user is also owner/admin, mark as duplicate record
1658 # if this user is also owner/admin, mark as duplicate record
1659 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1659 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1660 usr.duplicate_perm = True
1660 usr.duplicate_perm = True
1661 usr.permission = _usr.permission.permission_name
1661 usr.permission = _usr.permission.permission_name
1662 perm_rows.append(usr)
1662 perm_rows.append(usr)
1663
1663
1664 # filter the perm rows by 'default' first and then sort them by
1664 # filter the perm rows by 'default' first and then sort them by
1665 # admin,write,read,none permissions sorted again alphabetically in
1665 # admin,write,read,none permissions sorted again alphabetically in
1666 # each group
1666 # each group
1667 perm_rows = sorted(perm_rows, key=display_user_sort)
1667 perm_rows = sorted(perm_rows, key=display_user_sort)
1668
1668
1669 user_groups_rows = []
1669 user_groups_rows = []
1670 if expand_from_user_groups:
1670 if expand_from_user_groups:
1671 for ug in self.permission_user_groups(with_members=True):
1671 for ug in self.permission_user_groups(with_members=True):
1672 for user_data in ug.members:
1672 for user_data in ug.members:
1673 user_groups_rows.append(user_data)
1673 user_groups_rows.append(user_data)
1674
1674
1675 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1675 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1676
1676
1677 def permission_user_groups(self, with_members=False):
1677 def permission_user_groups(self, with_members=False):
1678 q = UserGroupUserGroupToPerm.query()\
1678 q = UserGroupUserGroupToPerm.query()\
1679 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1679 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1680 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1680 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1681 joinedload(UserGroupUserGroupToPerm.target_user_group),
1681 joinedload(UserGroupUserGroupToPerm.target_user_group),
1682 joinedload(UserGroupUserGroupToPerm.permission),)
1682 joinedload(UserGroupUserGroupToPerm.permission),)
1683
1683
1684 perm_rows = []
1684 perm_rows = []
1685 for _user_group in q.all():
1685 for _user_group in q.all():
1686 entry = AttributeDict(_user_group.user_group.get_dict())
1686 entry = AttributeDict(_user_group.user_group.get_dict())
1687 entry.permission = _user_group.permission.permission_name
1687 entry.permission = _user_group.permission.permission_name
1688 if with_members:
1688 if with_members:
1689 entry.members = [x.user.get_dict()
1689 entry.members = [x.user.get_dict()
1690 for x in _user_group.user_group.members]
1690 for x in _user_group.user_group.members]
1691 perm_rows.append(entry)
1691 perm_rows.append(entry)
1692
1692
1693 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1693 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1694 return perm_rows
1694 return perm_rows
1695
1695
1696 def _get_default_perms(self, user_group, suffix=''):
1696 def _get_default_perms(self, user_group, suffix=''):
1697 from rhodecode.model.permission import PermissionModel
1697 from rhodecode.model.permission import PermissionModel
1698 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1698 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1699
1699
1700 def get_default_perms(self, suffix=''):
1700 def get_default_perms(self, suffix=''):
1701 return self._get_default_perms(self, suffix)
1701 return self._get_default_perms(self, suffix)
1702
1702
1703 def get_api_data(self, with_group_members=True, include_secrets=False):
1703 def get_api_data(self, with_group_members=True, include_secrets=False):
1704 """
1704 """
1705 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1705 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1706 basically forwarded.
1706 basically forwarded.
1707
1707
1708 """
1708 """
1709 user_group = self
1709 user_group = self
1710 data = {
1710 data = {
1711 'users_group_id': user_group.users_group_id,
1711 'users_group_id': user_group.users_group_id,
1712 'group_name': user_group.users_group_name,
1712 'group_name': user_group.users_group_name,
1713 'group_description': user_group.user_group_description,
1713 'group_description': user_group.user_group_description,
1714 'active': user_group.users_group_active,
1714 'active': user_group.users_group_active,
1715 'owner': user_group.user.username,
1715 'owner': user_group.user.username,
1716 'sync': user_group.sync,
1716 'sync': user_group.sync,
1717 'owner_email': user_group.user.email,
1717 'owner_email': user_group.user.email,
1718 }
1718 }
1719
1719
1720 if with_group_members:
1720 if with_group_members:
1721 users = []
1721 users = []
1722 for user in user_group.members:
1722 for user in user_group.members:
1723 user = user.user
1723 user = user.user
1724 users.append(user.get_api_data(include_secrets=include_secrets))
1724 users.append(user.get_api_data(include_secrets=include_secrets))
1725 data['users'] = users
1725 data['users'] = users
1726
1726
1727 return data
1727 return data
1728
1728
1729
1729
1730 class UserGroupMember(Base, BaseModel):
1730 class UserGroupMember(Base, BaseModel):
1731 __tablename__ = 'users_groups_members'
1731 __tablename__ = 'users_groups_members'
1732 __table_args__ = (
1732 __table_args__ = (
1733 base_table_args,
1733 base_table_args,
1734 )
1734 )
1735
1735
1736 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1736 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1737 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1737 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1738 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1738 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1739
1739
1740 user = relationship('User', lazy='joined', back_populates='group_member')
1740 user = relationship('User', lazy='joined', back_populates='group_member')
1741 users_group = relationship('UserGroup', back_populates='members')
1741 users_group = relationship('UserGroup', back_populates='members')
1742
1742
1743 def __init__(self, gr_id='', u_id=''):
1743 def __init__(self, gr_id='', u_id=''):
1744 self.users_group_id = gr_id
1744 self.users_group_id = gr_id
1745 self.user_id = u_id
1745 self.user_id = u_id
1746
1746
1747
1747
1748 class RepositoryField(Base, BaseModel):
1748 class RepositoryField(Base, BaseModel):
1749 __tablename__ = 'repositories_fields'
1749 __tablename__ = 'repositories_fields'
1750 __table_args__ = (
1750 __table_args__ = (
1751 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1751 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1752 base_table_args,
1752 base_table_args,
1753 )
1753 )
1754
1754
1755 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1755 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1756
1756
1757 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1757 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1758 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1758 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1759 field_key = Column("field_key", String(250))
1759 field_key = Column("field_key", String(250))
1760 field_label = Column("field_label", String(1024), nullable=False)
1760 field_label = Column("field_label", String(1024), nullable=False)
1761 field_value = Column("field_value", String(10000), nullable=False)
1761 field_value = Column("field_value", String(10000), nullable=False)
1762 field_desc = Column("field_desc", String(1024), nullable=False)
1762 field_desc = Column("field_desc", String(1024), nullable=False)
1763 field_type = Column("field_type", String(255), nullable=False, unique=None)
1763 field_type = Column("field_type", String(255), nullable=False, unique=None)
1764 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1764 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1765
1765
1766 repository = relationship('Repository', back_populates='extra_fields')
1766 repository = relationship('Repository', back_populates='extra_fields')
1767
1767
1768 @property
1768 @property
1769 def field_key_prefixed(self):
1769 def field_key_prefixed(self):
1770 return 'ex_%s' % self.field_key
1770 return 'ex_%s' % self.field_key
1771
1771
1772 @classmethod
1772 @classmethod
1773 def un_prefix_key(cls, key):
1773 def un_prefix_key(cls, key):
1774 if key.startswith(cls.PREFIX):
1774 if key.startswith(cls.PREFIX):
1775 return key[len(cls.PREFIX):]
1775 return key[len(cls.PREFIX):]
1776 return key
1776 return key
1777
1777
1778 @classmethod
1778 @classmethod
1779 def get_by_key_name(cls, key, repo):
1779 def get_by_key_name(cls, key, repo):
1780 row = cls.query()\
1780 row = cls.query()\
1781 .filter(cls.repository == repo)\
1781 .filter(cls.repository == repo)\
1782 .filter(cls.field_key == key).scalar()
1782 .filter(cls.field_key == key).scalar()
1783 return row
1783 return row
1784
1784
1785
1785
1786 class Repository(Base, BaseModel):
1786 class Repository(Base, BaseModel):
1787 __tablename__ = 'repositories'
1787 __tablename__ = 'repositories'
1788 __table_args__ = (
1788 __table_args__ = (
1789 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1789 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1790 base_table_args,
1790 base_table_args,
1791 )
1791 )
1792 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1792 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1793 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1793 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1794 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1794 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1795
1795
1796 STATE_CREATED = 'repo_state_created'
1796 STATE_CREATED = 'repo_state_created'
1797 STATE_PENDING = 'repo_state_pending'
1797 STATE_PENDING = 'repo_state_pending'
1798 STATE_ERROR = 'repo_state_error'
1798 STATE_ERROR = 'repo_state_error'
1799
1799
1800 LOCK_AUTOMATIC = 'lock_auto'
1800 LOCK_AUTOMATIC = 'lock_auto'
1801 LOCK_API = 'lock_api'
1801 LOCK_API = 'lock_api'
1802 LOCK_WEB = 'lock_web'
1802 LOCK_WEB = 'lock_web'
1803 LOCK_PULL = 'lock_pull'
1803 LOCK_PULL = 'lock_pull'
1804
1804
1805 NAME_SEP = URL_SEP
1805 NAME_SEP = URL_SEP
1806
1806
1807 repo_id = Column(
1807 repo_id = Column(
1808 "repo_id", Integer(), nullable=False, unique=True, default=None,
1808 "repo_id", Integer(), nullable=False, unique=True, default=None,
1809 primary_key=True)
1809 primary_key=True)
1810 _repo_name = Column(
1810 _repo_name = Column(
1811 "repo_name", Text(), nullable=False, default=None)
1811 "repo_name", Text(), nullable=False, default=None)
1812 repo_name_hash = Column(
1812 repo_name_hash = Column(
1813 "repo_name_hash", String(255), nullable=False, unique=True)
1813 "repo_name_hash", String(255), nullable=False, unique=True)
1814 repo_state = Column("repo_state", String(255), nullable=True)
1814 repo_state = Column("repo_state", String(255), nullable=True)
1815
1815
1816 clone_uri = Column(
1816 clone_uri = Column(
1817 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1817 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1818 default=None)
1818 default=None)
1819 push_uri = Column(
1819 push_uri = Column(
1820 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1820 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1821 default=None)
1821 default=None)
1822 repo_type = Column(
1822 repo_type = Column(
1823 "repo_type", String(255), nullable=False, unique=False, default=None)
1823 "repo_type", String(255), nullable=False, unique=False, default=None)
1824 user_id = Column(
1824 user_id = Column(
1825 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1825 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1826 unique=False, default=None)
1826 unique=False, default=None)
1827 private = Column(
1827 private = Column(
1828 "private", Boolean(), nullable=True, unique=None, default=None)
1828 "private", Boolean(), nullable=True, unique=None, default=None)
1829 archived = Column(
1829 archived = Column(
1830 "archived", Boolean(), nullable=True, unique=None, default=None)
1830 "archived", Boolean(), nullable=True, unique=None, default=None)
1831 enable_statistics = Column(
1831 enable_statistics = Column(
1832 "statistics", Boolean(), nullable=True, unique=None, default=True)
1832 "statistics", Boolean(), nullable=True, unique=None, default=True)
1833 enable_downloads = Column(
1833 enable_downloads = Column(
1834 "downloads", Boolean(), nullable=True, unique=None, default=True)
1834 "downloads", Boolean(), nullable=True, unique=None, default=True)
1835 description = Column(
1835 description = Column(
1836 "description", String(10000), nullable=True, unique=None, default=None)
1836 "description", String(10000), nullable=True, unique=None, default=None)
1837 created_on = Column(
1837 created_on = Column(
1838 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1838 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1839 default=datetime.datetime.now)
1839 default=datetime.datetime.now)
1840 updated_on = Column(
1840 updated_on = Column(
1841 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1841 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1842 default=datetime.datetime.now)
1842 default=datetime.datetime.now)
1843 _landing_revision = Column(
1843 _landing_revision = Column(
1844 "landing_revision", String(255), nullable=False, unique=False,
1844 "landing_revision", String(255), nullable=False, unique=False,
1845 default=None)
1845 default=None)
1846 enable_locking = Column(
1846 enable_locking = Column(
1847 "enable_locking", Boolean(), nullable=False, unique=None,
1847 "enable_locking", Boolean(), nullable=False, unique=None,
1848 default=False)
1848 default=False)
1849 _locked = Column(
1849 _locked = Column(
1850 "locked", String(255), nullable=True, unique=False, default=None)
1850 "locked", String(255), nullable=True, unique=False, default=None)
1851 _changeset_cache = Column(
1851 _changeset_cache = Column(
1852 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1852 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1853
1853
1854 fork_id = Column(
1854 fork_id = Column(
1855 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1855 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1856 nullable=True, unique=False, default=None)
1856 nullable=True, unique=False, default=None)
1857 group_id = Column(
1857 group_id = Column(
1858 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1858 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1859 unique=False, default=None)
1859 unique=False, default=None)
1860
1860
1861 user = relationship('User', lazy='joined', back_populates='repositories')
1861 user = relationship('User', lazy='joined', back_populates='repositories')
1862 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1862 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1863 group = relationship('RepoGroup', lazy='joined')
1863 group = relationship('RepoGroup', lazy='joined')
1864 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1864 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1865 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1865 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1866 stats = relationship('Statistics', cascade='all', uselist=False)
1866 stats = relationship('Statistics', cascade='all', uselist=False)
1867
1867
1868 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1868 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1869 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1869 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1870
1870
1871 logs = relationship('UserLog', back_populates='repository')
1871 logs = relationship('UserLog', back_populates='repository')
1872
1872
1873 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1873 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1874
1874
1875 pull_requests_source = relationship(
1875 pull_requests_source = relationship(
1876 'PullRequest',
1876 'PullRequest',
1877 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1877 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1878 cascade="all, delete-orphan",
1878 cascade="all, delete-orphan",
1879 overlaps="source_repo"
1879 overlaps="source_repo"
1880 )
1880 )
1881 pull_requests_target = relationship(
1881 pull_requests_target = relationship(
1882 'PullRequest',
1882 'PullRequest',
1883 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1883 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1884 cascade="all, delete-orphan",
1884 cascade="all, delete-orphan",
1885 overlaps="target_repo"
1885 overlaps="target_repo"
1886 )
1886 )
1887
1887
1888 ui = relationship('RepoRhodeCodeUi', cascade="all")
1888 ui = relationship('RepoRhodeCodeUi', cascade="all")
1889 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1889 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1890 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1890 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1891
1891
1892 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1892 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1893
1893
1894 # no cascade, set NULL
1894 # no cascade, set NULL
1895 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1895 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1896
1896
1897 review_rules = relationship('RepoReviewRule')
1897 review_rules = relationship('RepoReviewRule')
1898 user_branch_perms = relationship('UserToRepoBranchPermission')
1898 user_branch_perms = relationship('UserToRepoBranchPermission')
1899 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1899 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1900
1900
1901 def __repr__(self):
1901 def __repr__(self):
1902 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1902 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1903
1903
1904 @hybrid_property
1904 @hybrid_property
1905 def description_safe(self):
1905 def description_safe(self):
1906 from rhodecode.lib import helpers as h
1906 from rhodecode.lib import helpers as h
1907 return h.escape(self.description)
1907 return h.escape(self.description)
1908
1908
1909 @hybrid_property
1909 @hybrid_property
1910 def landing_rev(self):
1910 def landing_rev(self):
1911 # always should return [rev_type, rev], e.g ['branch', 'master']
1911 # always should return [rev_type, rev], e.g ['branch', 'master']
1912 if self._landing_revision:
1912 if self._landing_revision:
1913 _rev_info = self._landing_revision.split(':')
1913 _rev_info = self._landing_revision.split(':')
1914 if len(_rev_info) < 2:
1914 if len(_rev_info) < 2:
1915 _rev_info.insert(0, 'rev')
1915 _rev_info.insert(0, 'rev')
1916 return [_rev_info[0], _rev_info[1]]
1916 return [_rev_info[0], _rev_info[1]]
1917 return [None, None]
1917 return [None, None]
1918
1918
1919 @property
1919 @property
1920 def landing_ref_type(self):
1920 def landing_ref_type(self):
1921 return self.landing_rev[0]
1921 return self.landing_rev[0]
1922
1922
1923 @property
1923 @property
1924 def landing_ref_name(self):
1924 def landing_ref_name(self):
1925 return self.landing_rev[1]
1925 return self.landing_rev[1]
1926
1926
1927 @landing_rev.setter
1927 @landing_rev.setter
1928 def landing_rev(self, val):
1928 def landing_rev(self, val):
1929 if ':' not in val:
1929 if ':' not in val:
1930 raise ValueError('value must be delimited with `:` and consist '
1930 raise ValueError('value must be delimited with `:` and consist '
1931 'of <rev_type>:<rev>, got %s instead' % val)
1931 'of <rev_type>:<rev>, got %s instead' % val)
1932 self._landing_revision = val
1932 self._landing_revision = val
1933
1933
1934 @hybrid_property
1934 @hybrid_property
1935 def locked(self):
1935 def locked(self):
1936 if self._locked:
1936 if self._locked:
1937 user_id, timelocked, reason = self._locked.split(':')
1937 user_id, timelocked, reason = self._locked.split(':')
1938 lock_values = int(user_id), timelocked, reason
1938 lock_values = int(user_id), timelocked, reason
1939 else:
1939 else:
1940 lock_values = [None, None, None]
1940 lock_values = [None, None, None]
1941 return lock_values
1941 return lock_values
1942
1942
1943 @locked.setter
1943 @locked.setter
1944 def locked(self, val):
1944 def locked(self, val):
1945 if val and isinstance(val, (list, tuple)):
1945 if val and isinstance(val, (list, tuple)):
1946 self._locked = ':'.join(map(str, val))
1946 self._locked = ':'.join(map(str, val))
1947 else:
1947 else:
1948 self._locked = None
1948 self._locked = None
1949
1949
1950 @classmethod
1950 @classmethod
1951 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1951 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1952 from rhodecode.lib.vcs.backends.base import EmptyCommit
1952 from rhodecode.lib.vcs.backends.base import EmptyCommit
1953 dummy = EmptyCommit().__json__()
1953 dummy = EmptyCommit().__json__()
1954 if not changeset_cache_raw:
1954 if not changeset_cache_raw:
1955 dummy['source_repo_id'] = repo_id
1955 dummy['source_repo_id'] = repo_id
1956 return json.loads(json.dumps(dummy))
1956 return json.loads(json.dumps(dummy))
1957
1957
1958 try:
1958 try:
1959 return json.loads(changeset_cache_raw)
1959 return json.loads(changeset_cache_raw)
1960 except TypeError:
1960 except TypeError:
1961 return dummy
1961 return dummy
1962 except Exception:
1962 except Exception:
1963 log.error(traceback.format_exc())
1963 log.error(traceback.format_exc())
1964 return dummy
1964 return dummy
1965
1965
1966 @hybrid_property
1966 @hybrid_property
1967 def changeset_cache(self):
1967 def changeset_cache(self):
1968 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1968 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1969
1969
1970 @changeset_cache.setter
1970 @changeset_cache.setter
1971 def changeset_cache(self, val):
1971 def changeset_cache(self, val):
1972 try:
1972 try:
1973 self._changeset_cache = json.dumps(val)
1973 self._changeset_cache = json.dumps(val)
1974 except Exception:
1974 except Exception:
1975 log.error(traceback.format_exc())
1975 log.error(traceback.format_exc())
1976
1976
1977 @hybrid_property
1977 @hybrid_property
1978 def repo_name(self):
1978 def repo_name(self):
1979 return self._repo_name
1979 return self._repo_name
1980
1980
1981 @repo_name.setter
1981 @repo_name.setter
1982 def repo_name(self, value):
1982 def repo_name(self, value):
1983 self._repo_name = value
1983 self._repo_name = value
1984 self.repo_name_hash = sha1(safe_bytes(value))
1984 self.repo_name_hash = sha1(safe_bytes(value))
1985
1985
1986 @classmethod
1986 @classmethod
1987 def normalize_repo_name(cls, repo_name):
1987 def normalize_repo_name(cls, repo_name):
1988 """
1988 """
1989 Normalizes os specific repo_name to the format internally stored inside
1989 Normalizes os specific repo_name to the format internally stored inside
1990 database using URL_SEP
1990 database using URL_SEP
1991
1991
1992 :param cls:
1992 :param cls:
1993 :param repo_name:
1993 :param repo_name:
1994 """
1994 """
1995 return cls.NAME_SEP.join(repo_name.split(os.sep))
1995 return cls.NAME_SEP.join(repo_name.split(os.sep))
1996
1996
1997 @classmethod
1997 @classmethod
1998 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1998 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1999 session = Session()
1999 session = Session()
2000 q = session.query(cls).filter(cls.repo_name == repo_name)
2000 q = session.query(cls).filter(cls.repo_name == repo_name)
2001
2001
2002 if cache:
2002 if cache:
2003 if identity_cache:
2003 if identity_cache:
2004 val = cls.identity_cache(session, 'repo_name', repo_name)
2004 val = cls.identity_cache(session, 'repo_name', repo_name)
2005 if val:
2005 if val:
2006 return val
2006 return val
2007 else:
2007 else:
2008 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
2008 cache_key = f"get_repo_by_name_{_hash_key(repo_name)}"
2009 q = q.options(
2009 q = q.options(
2010 FromCache("sql_cache_short", cache_key))
2010 FromCache("sql_cache_short", cache_key))
2011
2011
2012 return q.scalar()
2012 return q.scalar()
2013
2013
2014 @classmethod
2014 @classmethod
2015 def get_by_id_or_repo_name(cls, repoid):
2015 def get_by_id_or_repo_name(cls, repoid):
2016 if isinstance(repoid, int):
2016 if isinstance(repoid, int):
2017 try:
2017 try:
2018 repo = cls.get(repoid)
2018 repo = cls.get(repoid)
2019 except ValueError:
2019 except ValueError:
2020 repo = None
2020 repo = None
2021 else:
2021 else:
2022 repo = cls.get_by_repo_name(repoid)
2022 repo = cls.get_by_repo_name(repoid)
2023 return repo
2023 return repo
2024
2024
2025 @classmethod
2025 @classmethod
2026 def get_by_full_path(cls, repo_full_path):
2026 def get_by_full_path(cls, repo_full_path):
2027 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
2027 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
2028 repo_name = cls.normalize_repo_name(repo_name)
2028 repo_name = cls.normalize_repo_name(repo_name)
2029 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
2029 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
2030
2030
2031 @classmethod
2031 @classmethod
2032 def get_repo_forks(cls, repo_id):
2032 def get_repo_forks(cls, repo_id):
2033 return cls.query().filter(Repository.fork_id == repo_id)
2033 return cls.query().filter(Repository.fork_id == repo_id)
2034
2034
2035 @classmethod
2035 @classmethod
2036 def base_path(cls):
2036 def base_path(cls):
2037 """
2037 """
2038 Returns base path when all repos are stored
2038 Returns base path when all repos are stored
2039
2039
2040 :param cls:
2040 :param cls:
2041 """
2041 """
2042 from rhodecode.lib.utils import get_rhodecode_repo_store_path
2042 from rhodecode.lib.utils import get_rhodecode_repo_store_path
2043 return get_rhodecode_repo_store_path()
2043 return get_rhodecode_repo_store_path()
2044
2044
2045 @classmethod
2045 @classmethod
2046 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
2046 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
2047 case_insensitive=True, archived=False):
2047 case_insensitive=True, archived=False):
2048 q = Repository.query()
2048 q = Repository.query()
2049
2049
2050 if not archived:
2050 if not archived:
2051 q = q.filter(Repository.archived.isnot(true()))
2051 q = q.filter(Repository.archived.isnot(true()))
2052
2052
2053 if not isinstance(user_id, Optional):
2053 if not isinstance(user_id, Optional):
2054 q = q.filter(Repository.user_id == user_id)
2054 q = q.filter(Repository.user_id == user_id)
2055
2055
2056 if not isinstance(group_id, Optional):
2056 if not isinstance(group_id, Optional):
2057 q = q.filter(Repository.group_id == group_id)
2057 q = q.filter(Repository.group_id == group_id)
2058
2058
2059 if case_insensitive:
2059 if case_insensitive:
2060 q = q.order_by(func.lower(Repository.repo_name))
2060 q = q.order_by(func.lower(Repository.repo_name))
2061 else:
2061 else:
2062 q = q.order_by(Repository.repo_name)
2062 q = q.order_by(Repository.repo_name)
2063
2063
2064 return q.all()
2064 return q.all()
2065
2065
2066 @property
2066 @property
2067 def repo_uid(self):
2067 def repo_uid(self):
2068 return '_{}'.format(self.repo_id)
2068 return '_{}'.format(self.repo_id)
2069
2069
2070 @property
2070 @property
2071 def forks(self):
2071 def forks(self):
2072 """
2072 """
2073 Return forks of this repo
2073 Return forks of this repo
2074 """
2074 """
2075 return Repository.get_repo_forks(self.repo_id)
2075 return Repository.get_repo_forks(self.repo_id)
2076
2076
2077 @property
2077 @property
2078 def parent(self):
2078 def parent(self):
2079 """
2079 """
2080 Returns fork parent
2080 Returns fork parent
2081 """
2081 """
2082 return self.fork
2082 return self.fork
2083
2083
2084 @property
2084 @property
2085 def just_name(self):
2085 def just_name(self):
2086 return self.repo_name.split(self.NAME_SEP)[-1]
2086 return self.repo_name.split(self.NAME_SEP)[-1]
2087
2087
2088 @property
2088 @property
2089 def groups_with_parents(self):
2089 def groups_with_parents(self):
2090 groups = []
2090 groups = []
2091 if self.group is None:
2091 if self.group is None:
2092 return groups
2092 return groups
2093
2093
2094 cur_gr = self.group
2094 cur_gr = self.group
2095 groups.insert(0, cur_gr)
2095 groups.insert(0, cur_gr)
2096 while 1:
2096 while 1:
2097 gr = getattr(cur_gr, 'parent_group', None)
2097 gr = getattr(cur_gr, 'parent_group', None)
2098 cur_gr = cur_gr.parent_group
2098 cur_gr = cur_gr.parent_group
2099 if gr is None:
2099 if gr is None:
2100 break
2100 break
2101 groups.insert(0, gr)
2101 groups.insert(0, gr)
2102
2102
2103 return groups
2103 return groups
2104
2104
2105 @property
2105 @property
2106 def groups_and_repo(self):
2106 def groups_and_repo(self):
2107 return self.groups_with_parents, self
2107 return self.groups_with_parents, self
2108
2108
2109 @property
2109 @property
2110 def repo_path(self):
2110 def repo_path(self):
2111 """
2111 """
2112 Returns base full path for that repository means where it actually
2112 Returns base full path for that repository means where it actually
2113 exists on a filesystem
2113 exists on a filesystem
2114 """
2114 """
2115 return self.base_path()
2115 return self.base_path()
2116
2116
2117 @property
2117 @property
2118 def repo_full_path(self):
2118 def repo_full_path(self):
2119 p = [self.repo_path]
2119 p = [self.repo_path]
2120 # we need to split the name by / since this is how we store the
2120 # we need to split the name by / since this is how we store the
2121 # names in the database, but that eventually needs to be converted
2121 # names in the database, but that eventually needs to be converted
2122 # into a valid system path
2122 # into a valid system path
2123 p += self.repo_name.split(self.NAME_SEP)
2123 p += self.repo_name.split(self.NAME_SEP)
2124 return os.path.join(*map(safe_str, p))
2124 return os.path.join(*map(safe_str, p))
2125
2125
2126 @property
2126 @property
2127 def cache_keys(self):
2127 def cache_keys(self):
2128 """
2128 """
2129 Returns associated cache keys for that repo
2129 Returns associated cache keys for that repo
2130 """
2130 """
2131 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2131 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2132 return CacheKey.query()\
2132 return CacheKey.query()\
2133 .filter(CacheKey.cache_key == repo_namespace_key)\
2133 .filter(CacheKey.cache_key == repo_namespace_key)\
2134 .order_by(CacheKey.cache_key)\
2134 .order_by(CacheKey.cache_key)\
2135 .all()
2135 .all()
2136
2136
2137 @property
2137 @property
2138 def cached_diffs_relative_dir(self):
2138 def cached_diffs_relative_dir(self):
2139 """
2139 """
2140 Return a relative to the repository store path of cached diffs
2140 Return a relative to the repository store path of cached diffs
2141 used for safe display for users, who shouldn't know the absolute store
2141 used for safe display for users, who shouldn't know the absolute store
2142 path
2142 path
2143 """
2143 """
2144 return os.path.join(
2144 return os.path.join(
2145 os.path.dirname(self.repo_name),
2145 os.path.dirname(self.repo_name),
2146 self.cached_diffs_dir.split(os.path.sep)[-1])
2146 self.cached_diffs_dir.split(os.path.sep)[-1])
2147
2147
2148 @property
2148 @property
2149 def cached_diffs_dir(self):
2149 def cached_diffs_dir(self):
2150 path = self.repo_full_path
2150 path = self.repo_full_path
2151 return os.path.join(
2151 return os.path.join(
2152 os.path.dirname(path),
2152 os.path.dirname(path),
2153 f'.__shadow_diff_cache_repo_{self.repo_id}')
2153 f'.__shadow_diff_cache_repo_{self.repo_id}')
2154
2154
2155 def cached_diffs(self):
2155 def cached_diffs(self):
2156 diff_cache_dir = self.cached_diffs_dir
2156 diff_cache_dir = self.cached_diffs_dir
2157 if os.path.isdir(diff_cache_dir):
2157 if os.path.isdir(diff_cache_dir):
2158 return os.listdir(diff_cache_dir)
2158 return os.listdir(diff_cache_dir)
2159 return []
2159 return []
2160
2160
2161 def shadow_repos(self):
2161 def shadow_repos(self):
2162 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2162 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2163 return [
2163 return [
2164 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2164 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2165 if x.startswith(shadow_repos_pattern)
2165 if x.startswith(shadow_repos_pattern)
2166 ]
2166 ]
2167
2167
2168 def get_new_name(self, repo_name):
2168 def get_new_name(self, repo_name):
2169 """
2169 """
2170 returns new full repository name based on assigned group and new new
2170 returns new full repository name based on assigned group and new new
2171
2171
2172 :param repo_name:
2172 :param repo_name:
2173 """
2173 """
2174 path_prefix = self.group.full_path_splitted if self.group else []
2174 path_prefix = self.group.full_path_splitted if self.group else []
2175 return self.NAME_SEP.join(path_prefix + [repo_name])
2175 return self.NAME_SEP.join(path_prefix + [repo_name])
2176
2176
2177 @property
2177 @property
2178 def _config(self):
2178 def _config(self):
2179 """
2179 """
2180 Returns db based config object.
2180 Returns db based config object.
2181 """
2181 """
2182 from rhodecode.lib.utils import make_db_config
2182 from rhodecode.lib.utils import make_db_config
2183 return make_db_config(clear_session=False, repo=self)
2183 return make_db_config(clear_session=False, repo=self)
2184
2184
2185 def permissions(self, with_admins=True, with_owner=True,
2185 def permissions(self, with_admins=True, with_owner=True,
2186 expand_from_user_groups=False):
2186 expand_from_user_groups=False):
2187 """
2187 """
2188 Permissions for repositories
2188 Permissions for repositories
2189 """
2189 """
2190 _admin_perm = 'repository.admin'
2190 _admin_perm = 'repository.admin'
2191
2191
2192 owner_row = []
2192 owner_row = []
2193 if with_owner:
2193 if with_owner:
2194 usr = AttributeDict(self.user.get_dict())
2194 usr = AttributeDict(self.user.get_dict())
2195 usr.owner_row = True
2195 usr.owner_row = True
2196 usr.permission = _admin_perm
2196 usr.permission = _admin_perm
2197 usr.permission_id = None
2197 usr.permission_id = None
2198 owner_row.append(usr)
2198 owner_row.append(usr)
2199
2199
2200 super_admin_ids = []
2200 super_admin_ids = []
2201 super_admin_rows = []
2201 super_admin_rows = []
2202 if with_admins:
2202 if with_admins:
2203 for usr in User.get_all_super_admins():
2203 for usr in User.get_all_super_admins():
2204 super_admin_ids.append(usr.user_id)
2204 super_admin_ids.append(usr.user_id)
2205 # if this admin is also owner, don't double the record
2205 # if this admin is also owner, don't double the record
2206 if usr.user_id == owner_row[0].user_id:
2206 if usr.user_id == owner_row[0].user_id:
2207 owner_row[0].admin_row = True
2207 owner_row[0].admin_row = True
2208 else:
2208 else:
2209 usr = AttributeDict(usr.get_dict())
2209 usr = AttributeDict(usr.get_dict())
2210 usr.admin_row = True
2210 usr.admin_row = True
2211 usr.permission = _admin_perm
2211 usr.permission = _admin_perm
2212 usr.permission_id = None
2212 usr.permission_id = None
2213 super_admin_rows.append(usr)
2213 super_admin_rows.append(usr)
2214
2214
2215 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2215 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2216 q = q.options(joinedload(UserRepoToPerm.repository),
2216 q = q.options(joinedload(UserRepoToPerm.repository),
2217 joinedload(UserRepoToPerm.user),
2217 joinedload(UserRepoToPerm.user),
2218 joinedload(UserRepoToPerm.permission),)
2218 joinedload(UserRepoToPerm.permission),)
2219
2219
2220 # get owners and admins and permissions. We do a trick of re-writing
2220 # get owners and admins and permissions. We do a trick of re-writing
2221 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2221 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2222 # has a global reference and changing one object propagates to all
2222 # has a global reference and changing one object propagates to all
2223 # others. This means if admin is also an owner admin_row that change
2223 # others. This means if admin is also an owner admin_row that change
2224 # would propagate to both objects
2224 # would propagate to both objects
2225 perm_rows = []
2225 perm_rows = []
2226 for _usr in q.all():
2226 for _usr in q.all():
2227 usr = AttributeDict(_usr.user.get_dict())
2227 usr = AttributeDict(_usr.user.get_dict())
2228 # if this user is also owner/admin, mark as duplicate record
2228 # if this user is also owner/admin, mark as duplicate record
2229 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2229 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2230 usr.duplicate_perm = True
2230 usr.duplicate_perm = True
2231 # also check if this permission is maybe used by branch_permissions
2231 # also check if this permission is maybe used by branch_permissions
2232 if _usr.branch_perm_entry:
2232 if _usr.branch_perm_entry:
2233 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2233 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2234
2234
2235 usr.permission = _usr.permission.permission_name
2235 usr.permission = _usr.permission.permission_name
2236 usr.permission_id = _usr.repo_to_perm_id
2236 usr.permission_id = _usr.repo_to_perm_id
2237 perm_rows.append(usr)
2237 perm_rows.append(usr)
2238
2238
2239 # filter the perm rows by 'default' first and then sort them by
2239 # filter the perm rows by 'default' first and then sort them by
2240 # admin,write,read,none permissions sorted again alphabetically in
2240 # admin,write,read,none permissions sorted again alphabetically in
2241 # each group
2241 # each group
2242 perm_rows = sorted(perm_rows, key=display_user_sort)
2242 perm_rows = sorted(perm_rows, key=display_user_sort)
2243
2243
2244 user_groups_rows = []
2244 user_groups_rows = []
2245 if expand_from_user_groups:
2245 if expand_from_user_groups:
2246 for ug in self.permission_user_groups(with_members=True):
2246 for ug in self.permission_user_groups(with_members=True):
2247 for user_data in ug.members:
2247 for user_data in ug.members:
2248 user_groups_rows.append(user_data)
2248 user_groups_rows.append(user_data)
2249
2249
2250 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2250 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2251
2251
2252 def permission_user_groups(self, with_members=True):
2252 def permission_user_groups(self, with_members=True):
2253 q = UserGroupRepoToPerm.query()\
2253 q = UserGroupRepoToPerm.query()\
2254 .filter(UserGroupRepoToPerm.repository == self)
2254 .filter(UserGroupRepoToPerm.repository == self)
2255 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2255 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2256 joinedload(UserGroupRepoToPerm.users_group),
2256 joinedload(UserGroupRepoToPerm.users_group),
2257 joinedload(UserGroupRepoToPerm.permission),)
2257 joinedload(UserGroupRepoToPerm.permission),)
2258
2258
2259 perm_rows = []
2259 perm_rows = []
2260 for _user_group in q.all():
2260 for _user_group in q.all():
2261 entry = AttributeDict(_user_group.users_group.get_dict())
2261 entry = AttributeDict(_user_group.users_group.get_dict())
2262 entry.permission = _user_group.permission.permission_name
2262 entry.permission = _user_group.permission.permission_name
2263 if with_members:
2263 if with_members:
2264 entry.members = [x.user.get_dict()
2264 entry.members = [x.user.get_dict()
2265 for x in _user_group.users_group.members]
2265 for x in _user_group.users_group.members]
2266 perm_rows.append(entry)
2266 perm_rows.append(entry)
2267
2267
2268 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2268 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2269 return perm_rows
2269 return perm_rows
2270
2270
2271 def get_api_data(self, include_secrets=False):
2271 def get_api_data(self, include_secrets=False):
2272 """
2272 """
2273 Common function for generating repo api data
2273 Common function for generating repo api data
2274
2274
2275 :param include_secrets: See :meth:`User.get_api_data`.
2275 :param include_secrets: See :meth:`User.get_api_data`.
2276
2276
2277 """
2277 """
2278 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2278 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2279 # move this methods on models level.
2279 # move this methods on models level.
2280 from rhodecode.model.settings import SettingsModel
2280 from rhodecode.model.settings import SettingsModel
2281 from rhodecode.model.repo import RepoModel
2281 from rhodecode.model.repo import RepoModel
2282
2282
2283 repo = self
2283 repo = self
2284 _user_id, _time, _reason = self.locked
2284 _user_id, _time, _reason = self.locked
2285
2285
2286 data = {
2286 data = {
2287 'repo_id': repo.repo_id,
2287 'repo_id': repo.repo_id,
2288 'repo_name': repo.repo_name,
2288 'repo_name': repo.repo_name,
2289 'repo_type': repo.repo_type,
2289 'repo_type': repo.repo_type,
2290 'clone_uri': repo.clone_uri or '',
2290 'clone_uri': repo.clone_uri or '',
2291 'push_uri': repo.push_uri or '',
2291 'push_uri': repo.push_uri or '',
2292 'url': RepoModel().get_url(self),
2292 'url': RepoModel().get_url(self),
2293 'private': repo.private,
2293 'private': repo.private,
2294 'created_on': repo.created_on,
2294 'created_on': repo.created_on,
2295 'description': repo.description_safe,
2295 'description': repo.description_safe,
2296 'landing_rev': repo.landing_rev,
2296 'landing_rev': repo.landing_rev,
2297 'owner': repo.user.username,
2297 'owner': repo.user.username,
2298 'fork_of': repo.fork.repo_name if repo.fork else None,
2298 'fork_of': repo.fork.repo_name if repo.fork else None,
2299 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2299 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2300 'enable_statistics': repo.enable_statistics,
2300 'enable_statistics': repo.enable_statistics,
2301 'enable_locking': repo.enable_locking,
2301 'enable_locking': repo.enable_locking,
2302 'enable_downloads': repo.enable_downloads,
2302 'enable_downloads': repo.enable_downloads,
2303 'last_changeset': repo.changeset_cache,
2303 'last_changeset': repo.changeset_cache,
2304 'locked_by': User.get(_user_id).get_api_data(
2304 'locked_by': User.get(_user_id).get_api_data(
2305 include_secrets=include_secrets) if _user_id else None,
2305 include_secrets=include_secrets) if _user_id else None,
2306 'locked_date': time_to_datetime(_time) if _time else None,
2306 'locked_date': time_to_datetime(_time) if _time else None,
2307 'lock_reason': _reason if _reason else None,
2307 'lock_reason': _reason if _reason else None,
2308 }
2308 }
2309
2309
2310 # TODO: mikhail: should be per-repo settings here
2310 # TODO: mikhail: should be per-repo settings here
2311 rc_config = SettingsModel().get_all_settings()
2311 rc_config = SettingsModel().get_all_settings()
2312 repository_fields = str2bool(
2312 repository_fields = str2bool(
2313 rc_config.get('rhodecode_repository_fields'))
2313 rc_config.get('rhodecode_repository_fields'))
2314 if repository_fields:
2314 if repository_fields:
2315 for f in self.extra_fields:
2315 for f in self.extra_fields:
2316 data[f.field_key_prefixed] = f.field_value
2316 data[f.field_key_prefixed] = f.field_value
2317
2317
2318 return data
2318 return data
2319
2319
2320 @classmethod
2320 @classmethod
2321 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2321 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2322 if not lock_time:
2322 if not lock_time:
2323 lock_time = time.time()
2323 lock_time = time.time()
2324 if not lock_reason:
2324 if not lock_reason:
2325 lock_reason = cls.LOCK_AUTOMATIC
2325 lock_reason = cls.LOCK_AUTOMATIC
2326 repo.locked = [user_id, lock_time, lock_reason]
2326 repo.locked = [user_id, lock_time, lock_reason]
2327 Session().add(repo)
2327 Session().add(repo)
2328 Session().commit()
2328 Session().commit()
2329
2329
2330 @classmethod
2330 @classmethod
2331 def unlock(cls, repo):
2331 def unlock(cls, repo):
2332 repo.locked = None
2332 repo.locked = None
2333 Session().add(repo)
2333 Session().add(repo)
2334 Session().commit()
2334 Session().commit()
2335
2335
2336 @classmethod
2336 @classmethod
2337 def getlock(cls, repo):
2337 def getlock(cls, repo):
2338 return repo.locked
2338 return repo.locked
2339
2339
2340 def get_locking_state(self, action, user_id, only_when_enabled=True):
2340 def get_locking_state(self, action, user_id, only_when_enabled=True):
2341 """
2341 """
2342 Checks locking on this repository, if locking is enabled and lock is
2342 Checks locking on this repository, if locking is enabled and lock is
2343 present returns a tuple of make_lock, locked, locked_by.
2343 present returns a tuple of make_lock, locked, locked_by.
2344 make_lock can have 3 states None (do nothing) True, make lock
2344 make_lock can have 3 states None (do nothing) True, make lock
2345 False release lock, This value is later propagated to hooks, which
2345 False release lock, This value is later propagated to hooks, which
2346 do the locking. Think about this as signals passed to hooks what to do.
2346 do the locking. Think about this as signals passed to hooks what to do.
2347
2347
2348 """
2348 """
2349 # TODO: johbo: This is part of the business logic and should be moved
2349 # TODO: johbo: This is part of the business logic and should be moved
2350 # into the RepositoryModel.
2350 # into the RepositoryModel.
2351
2351
2352 if action not in ('push', 'pull'):
2352 if action not in ('push', 'pull'):
2353 raise ValueError("Invalid action value: %s" % repr(action))
2353 raise ValueError("Invalid action value: %s" % repr(action))
2354
2354
2355 # defines if locked error should be thrown to user
2355 # defines if locked error should be thrown to user
2356 currently_locked = False
2356 currently_locked = False
2357 # defines if new lock should be made, tri-state
2357 # defines if new lock should be made, tri-state
2358 make_lock = None
2358 make_lock = None
2359 repo = self
2359 repo = self
2360 user = User.get(user_id)
2360 user = User.get(user_id)
2361
2361
2362 lock_info = repo.locked
2362 lock_info = repo.locked
2363
2363
2364 if repo and (repo.enable_locking or not only_when_enabled):
2364 if repo and (repo.enable_locking or not only_when_enabled):
2365 if action == 'push':
2365 if action == 'push':
2366 # check if it's already locked !, if it is compare users
2366 # check if it's already locked !, if it is compare users
2367 locked_by_user_id = lock_info[0]
2367 locked_by_user_id = lock_info[0]
2368 if user.user_id == locked_by_user_id:
2368 if user.user_id == locked_by_user_id:
2369 log.debug(
2369 log.debug(
2370 'Got `push` action from user %s, now unlocking', user)
2370 'Got `push` action from user %s, now unlocking', user)
2371 # unlock if we have push from user who locked
2371 # unlock if we have push from user who locked
2372 make_lock = False
2372 make_lock = False
2373 else:
2373 else:
2374 # we're not the same user who locked, ban with
2374 # we're not the same user who locked, ban with
2375 # code defined in settings (default is 423 HTTP Locked) !
2375 # code defined in settings (default is 423 HTTP Locked) !
2376 log.debug('Repo %s is currently locked by %s', repo, user)
2376 log.debug('Repo %s is currently locked by %s', repo, user)
2377 currently_locked = True
2377 currently_locked = True
2378 elif action == 'pull':
2378 elif action == 'pull':
2379 # [0] user [1] date
2379 # [0] user [1] date
2380 if lock_info[0] and lock_info[1]:
2380 if lock_info[0] and lock_info[1]:
2381 log.debug('Repo %s is currently locked by %s', repo, user)
2381 log.debug('Repo %s is currently locked by %s', repo, user)
2382 currently_locked = True
2382 currently_locked = True
2383 else:
2383 else:
2384 log.debug('Setting lock on repo %s by %s', repo, user)
2384 log.debug('Setting lock on repo %s by %s', repo, user)
2385 make_lock = True
2385 make_lock = True
2386
2386
2387 else:
2387 else:
2388 log.debug('Repository %s do not have locking enabled', repo)
2388 log.debug('Repository %s do not have locking enabled', repo)
2389
2389
2390 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2390 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2391 make_lock, currently_locked, lock_info)
2391 make_lock, currently_locked, lock_info)
2392
2392
2393 from rhodecode.lib.auth import HasRepoPermissionAny
2393 from rhodecode.lib.auth import HasRepoPermissionAny
2394 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2394 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2395 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2395 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2396 # if we don't have at least write permission we cannot make a lock
2396 # if we don't have at least write permission we cannot make a lock
2397 log.debug('lock state reset back to FALSE due to lack '
2397 log.debug('lock state reset back to FALSE due to lack '
2398 'of at least read permission')
2398 'of at least read permission')
2399 make_lock = False
2399 make_lock = False
2400
2400
2401 return make_lock, currently_locked, lock_info
2401 return make_lock, currently_locked, lock_info
2402
2402
2403 @property
2403 @property
2404 def last_commit_cache_update_diff(self):
2404 def last_commit_cache_update_diff(self):
2405 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2405 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2406
2406
2407 @classmethod
2407 @classmethod
2408 def _load_commit_change(cls, last_commit_cache):
2408 def _load_commit_change(cls, last_commit_cache):
2409 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2409 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2410 empty_date = datetime.datetime.fromtimestamp(0)
2410 empty_date = datetime.datetime.fromtimestamp(0)
2411 date_latest = last_commit_cache.get('date', empty_date)
2411 date_latest = last_commit_cache.get('date', empty_date)
2412 try:
2412 try:
2413 return parse_datetime(date_latest)
2413 return parse_datetime(date_latest)
2414 except Exception:
2414 except Exception:
2415 return empty_date
2415 return empty_date
2416
2416
2417 @property
2417 @property
2418 def last_commit_change(self):
2418 def last_commit_change(self):
2419 return self._load_commit_change(self.changeset_cache)
2419 return self._load_commit_change(self.changeset_cache)
2420
2420
2421 @property
2421 @property
2422 def last_db_change(self):
2422 def last_db_change(self):
2423 return self.updated_on
2423 return self.updated_on
2424
2424
2425 @property
2425 @property
2426 def clone_uri_hidden(self):
2426 def clone_uri_hidden(self):
2427 clone_uri = self.clone_uri
2427 clone_uri = self.clone_uri
2428 if clone_uri:
2428 if clone_uri:
2429 import urlobject
2429 import urlobject
2430 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2430 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2431 if url_obj.password:
2431 if url_obj.password:
2432 clone_uri = url_obj.with_password('*****')
2432 clone_uri = url_obj.with_password('*****')
2433 return clone_uri
2433 return clone_uri
2434
2434
2435 @property
2435 @property
2436 def push_uri_hidden(self):
2436 def push_uri_hidden(self):
2437 push_uri = self.push_uri
2437 push_uri = self.push_uri
2438 if push_uri:
2438 if push_uri:
2439 import urlobject
2439 import urlobject
2440 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2440 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2441 if url_obj.password:
2441 if url_obj.password:
2442 push_uri = url_obj.with_password('*****')
2442 push_uri = url_obj.with_password('*****')
2443 return push_uri
2443 return push_uri
2444
2444
2445 def clone_url(self, **override):
2445 def clone_url(self, **override):
2446 from rhodecode.model.settings import SettingsModel
2446 from rhodecode.model.settings import SettingsModel
2447
2447
2448 uri_tmpl = None
2448 uri_tmpl = None
2449 if 'with_id' in override:
2449 if 'with_id' in override:
2450 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2450 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2451 del override['with_id']
2451 del override['with_id']
2452
2452
2453 if 'uri_tmpl' in override:
2453 if 'uri_tmpl' in override:
2454 uri_tmpl = override['uri_tmpl']
2454 uri_tmpl = override['uri_tmpl']
2455 del override['uri_tmpl']
2455 del override['uri_tmpl']
2456
2456
2457 ssh = False
2457 ssh = False
2458 if 'ssh' in override:
2458 if 'ssh' in override:
2459 ssh = True
2459 ssh = True
2460 del override['ssh']
2460 del override['ssh']
2461
2461
2462 # we didn't override our tmpl from **overrides
2462 # we didn't override our tmpl from **overrides
2463 request = get_current_request()
2463 request = get_current_request()
2464 if not uri_tmpl:
2464 if not uri_tmpl:
2465 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2465 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2466 rc_config = request.call_context.rc_config
2466 rc_config = request.call_context.rc_config
2467 else:
2467 else:
2468 rc_config = SettingsModel().get_all_settings(cache=True)
2468 rc_config = SettingsModel().get_all_settings(cache=True)
2469
2469
2470 if ssh:
2470 if ssh:
2471 uri_tmpl = rc_config.get(
2471 uri_tmpl = rc_config.get(
2472 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2472 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2473
2473
2474 else:
2474 else:
2475 uri_tmpl = rc_config.get(
2475 uri_tmpl = rc_config.get(
2476 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2476 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2477
2477
2478 return get_clone_url(request=request,
2478 return get_clone_url(request=request,
2479 uri_tmpl=uri_tmpl,
2479 uri_tmpl=uri_tmpl,
2480 repo_name=self.repo_name,
2480 repo_name=self.repo_name,
2481 repo_id=self.repo_id,
2481 repo_id=self.repo_id,
2482 repo_type=self.repo_type,
2482 repo_type=self.repo_type,
2483 **override)
2483 **override)
2484
2484
2485 def set_state(self, state):
2485 def set_state(self, state):
2486 self.repo_state = state
2486 self.repo_state = state
2487 Session().add(self)
2487 Session().add(self)
2488 #==========================================================================
2488 #==========================================================================
2489 # SCM PROPERTIES
2489 # SCM PROPERTIES
2490 #==========================================================================
2490 #==========================================================================
2491
2491
2492 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2492 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2493 return get_commit_safe(
2493 return get_commit_safe(
2494 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2494 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2495 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2495 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2496
2496
2497 def get_changeset(self, rev=None, pre_load=None):
2497 def get_changeset(self, rev=None, pre_load=None):
2498 warnings.warn("Use get_commit", DeprecationWarning)
2498 warnings.warn("Use get_commit", DeprecationWarning)
2499 commit_id = None
2499 commit_id = None
2500 commit_idx = None
2500 commit_idx = None
2501 if isinstance(rev, str):
2501 if isinstance(rev, str):
2502 commit_id = rev
2502 commit_id = rev
2503 else:
2503 else:
2504 commit_idx = rev
2504 commit_idx = rev
2505 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2505 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2506 pre_load=pre_load)
2506 pre_load=pre_load)
2507
2507
2508 def get_landing_commit(self):
2508 def get_landing_commit(self):
2509 """
2509 """
2510 Returns landing commit, or if that doesn't exist returns the tip
2510 Returns landing commit, or if that doesn't exist returns the tip
2511 """
2511 """
2512 _rev_type, _rev = self.landing_rev
2512 _rev_type, _rev = self.landing_rev
2513 commit = self.get_commit(_rev)
2513 commit = self.get_commit(_rev)
2514 if isinstance(commit, EmptyCommit):
2514 if isinstance(commit, EmptyCommit):
2515 return self.get_commit()
2515 return self.get_commit()
2516 return commit
2516 return commit
2517
2517
2518 def flush_commit_cache(self):
2518 def flush_commit_cache(self):
2519 self.update_commit_cache(cs_cache={'raw_id':'0'})
2519 self.update_commit_cache(cs_cache={'raw_id':'0'})
2520 self.update_commit_cache()
2520 self.update_commit_cache()
2521
2521
2522 def update_commit_cache(self, cs_cache=None, config=None):
2522 def update_commit_cache(self, cs_cache=None, config=None):
2523 """
2523 """
2524 Update cache of last commit for repository
2524 Update cache of last commit for repository
2525 cache_keys should be::
2525 cache_keys should be::
2526
2526
2527 source_repo_id
2527 source_repo_id
2528 short_id
2528 short_id
2529 raw_id
2529 raw_id
2530 revision
2530 revision
2531 parents
2531 parents
2532 message
2532 message
2533 date
2533 date
2534 author
2534 author
2535 updated_on
2535 updated_on
2536
2536
2537 """
2537 """
2538 from rhodecode.lib.vcs.backends.base import BaseCommit
2538 from rhodecode.lib.vcs.backends.base import BaseCommit
2539 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2539 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2540 empty_date = datetime.datetime.fromtimestamp(0)
2540 empty_date = datetime.datetime.fromtimestamp(0)
2541 repo_commit_count = 0
2541 repo_commit_count = 0
2542
2542
2543 if cs_cache is None:
2543 if cs_cache is None:
2544 # use no-cache version here
2544 # use no-cache version here
2545 try:
2545 try:
2546 scm_repo = self.scm_instance(cache=False, config=config)
2546 scm_repo = self.scm_instance(cache=False, config=config)
2547 except VCSError:
2547 except VCSError:
2548 scm_repo = None
2548 scm_repo = None
2549 empty = scm_repo is None or scm_repo.is_empty()
2549 empty = scm_repo is None or scm_repo.is_empty()
2550
2550
2551 if not empty:
2551 if not empty:
2552 cs_cache = scm_repo.get_commit(
2552 cs_cache = scm_repo.get_commit(
2553 pre_load=["author", "date", "message", "parents", "branch"])
2553 pre_load=["author", "date", "message", "parents", "branch"])
2554 repo_commit_count = scm_repo.count()
2554 repo_commit_count = scm_repo.count()
2555 else:
2555 else:
2556 cs_cache = EmptyCommit()
2556 cs_cache = EmptyCommit()
2557
2557
2558 if isinstance(cs_cache, BaseCommit):
2558 if isinstance(cs_cache, BaseCommit):
2559 cs_cache = cs_cache.__json__()
2559 cs_cache = cs_cache.__json__()
2560
2560
2561 def is_outdated(new_cs_cache):
2561 def is_outdated(new_cs_cache):
2562 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2562 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2563 new_cs_cache['revision'] != self.changeset_cache['revision']):
2563 new_cs_cache['revision'] != self.changeset_cache['revision']):
2564 return True
2564 return True
2565 return False
2565 return False
2566
2566
2567 # check if we have maybe already latest cached revision
2567 # check if we have maybe already latest cached revision
2568 if is_outdated(cs_cache) or not self.changeset_cache:
2568 if is_outdated(cs_cache) or not self.changeset_cache:
2569 _current_datetime = datetime.datetime.utcnow()
2569 _current_datetime = datetime.datetime.utcnow()
2570 last_change = cs_cache.get('date') or _current_datetime
2570 last_change = cs_cache.get('date') or _current_datetime
2571 # we check if last update is newer than the new value
2571 # we check if last update is newer than the new value
2572 # if yes, we use the current timestamp instead. Imagine you get
2572 # if yes, we use the current timestamp instead. Imagine you get
2573 # old commit pushed 1y ago, we'd set last update 1y to ago.
2573 # old commit pushed 1y ago, we'd set last update 1y to ago.
2574 last_change_timestamp = datetime_to_time(last_change)
2574 last_change_timestamp = datetime_to_time(last_change)
2575 current_timestamp = datetime_to_time(last_change)
2575 current_timestamp = datetime_to_time(last_change)
2576 if last_change_timestamp > current_timestamp and not empty:
2576 if last_change_timestamp > current_timestamp and not empty:
2577 cs_cache['date'] = _current_datetime
2577 cs_cache['date'] = _current_datetime
2578
2578
2579 # also store size of repo
2579 # also store size of repo
2580 cs_cache['repo_commit_count'] = repo_commit_count
2580 cs_cache['repo_commit_count'] = repo_commit_count
2581
2581
2582 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2582 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2583 cs_cache['updated_on'] = time.time()
2583 cs_cache['updated_on'] = time.time()
2584 self.changeset_cache = cs_cache
2584 self.changeset_cache = cs_cache
2585 self.updated_on = last_change
2585 self.updated_on = last_change
2586 Session().add(self)
2586 Session().add(self)
2587 Session().commit()
2587 Session().commit()
2588
2588
2589 else:
2589 else:
2590 if empty:
2590 if empty:
2591 cs_cache = EmptyCommit().__json__()
2591 cs_cache = EmptyCommit().__json__()
2592 else:
2592 else:
2593 cs_cache = self.changeset_cache
2593 cs_cache = self.changeset_cache
2594
2594
2595 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2595 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2596
2596
2597 cs_cache['updated_on'] = time.time()
2597 cs_cache['updated_on'] = time.time()
2598 self.changeset_cache = cs_cache
2598 self.changeset_cache = cs_cache
2599 self.updated_on = _date_latest
2599 self.updated_on = _date_latest
2600 Session().add(self)
2600 Session().add(self)
2601 Session().commit()
2601 Session().commit()
2602
2602
2603 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2603 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2604 self.repo_name, cs_cache, _date_latest)
2604 self.repo_name, cs_cache, _date_latest)
2605
2605
2606 @property
2606 @property
2607 def tip(self):
2607 def tip(self):
2608 return self.get_commit('tip')
2608 return self.get_commit('tip')
2609
2609
2610 @property
2610 @property
2611 def author(self):
2611 def author(self):
2612 return self.tip.author
2612 return self.tip.author
2613
2613
2614 @property
2614 @property
2615 def last_change(self):
2615 def last_change(self):
2616 return self.scm_instance().last_change
2616 return self.scm_instance().last_change
2617
2617
2618 def get_comments(self, revisions=None):
2618 def get_comments(self, revisions=None):
2619 """
2619 """
2620 Returns comments for this repository grouped by revisions
2620 Returns comments for this repository grouped by revisions
2621
2621
2622 :param revisions: filter query by revisions only
2622 :param revisions: filter query by revisions only
2623 """
2623 """
2624 cmts = ChangesetComment.query()\
2624 cmts = ChangesetComment.query()\
2625 .filter(ChangesetComment.repo == self)
2625 .filter(ChangesetComment.repo == self)
2626 if revisions:
2626 if revisions:
2627 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2627 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2628 grouped = collections.defaultdict(list)
2628 grouped = collections.defaultdict(list)
2629 for cmt in cmts.all():
2629 for cmt in cmts.all():
2630 grouped[cmt.revision].append(cmt)
2630 grouped[cmt.revision].append(cmt)
2631 return grouped
2631 return grouped
2632
2632
2633 def statuses(self, revisions=None):
2633 def statuses(self, revisions=None):
2634 """
2634 """
2635 Returns statuses for this repository
2635 Returns statuses for this repository
2636
2636
2637 :param revisions: list of revisions to get statuses for
2637 :param revisions: list of revisions to get statuses for
2638 """
2638 """
2639 statuses = ChangesetStatus.query()\
2639 statuses = ChangesetStatus.query()\
2640 .filter(ChangesetStatus.repo == self)\
2640 .filter(ChangesetStatus.repo == self)\
2641 .filter(ChangesetStatus.version == 0)
2641 .filter(ChangesetStatus.version == 0)
2642
2642
2643 if revisions:
2643 if revisions:
2644 # Try doing the filtering in chunks to avoid hitting limits
2644 # Try doing the filtering in chunks to avoid hitting limits
2645 size = 500
2645 size = 500
2646 status_results = []
2646 status_results = []
2647 for chunk in range(0, len(revisions), size):
2647 for chunk in range(0, len(revisions), size):
2648 status_results += statuses.filter(
2648 status_results += statuses.filter(
2649 ChangesetStatus.revision.in_(
2649 ChangesetStatus.revision.in_(
2650 revisions[chunk: chunk+size])
2650 revisions[chunk: chunk+size])
2651 ).all()
2651 ).all()
2652 else:
2652 else:
2653 status_results = statuses.all()
2653 status_results = statuses.all()
2654
2654
2655 grouped = {}
2655 grouped = {}
2656
2656
2657 # maybe we have open new pullrequest without a status?
2657 # maybe we have open new pullrequest without a status?
2658 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2658 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2659 status_lbl = ChangesetStatus.get_status_lbl(stat)
2659 status_lbl = ChangesetStatus.get_status_lbl(stat)
2660 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2660 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2661 for rev in pr.revisions:
2661 for rev in pr.revisions:
2662 pr_id = pr.pull_request_id
2662 pr_id = pr.pull_request_id
2663 pr_repo = pr.target_repo.repo_name
2663 pr_repo = pr.target_repo.repo_name
2664 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2664 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2665
2665
2666 for stat in status_results:
2666 for stat in status_results:
2667 pr_id = pr_repo = None
2667 pr_id = pr_repo = None
2668 if stat.pull_request:
2668 if stat.pull_request:
2669 pr_id = stat.pull_request.pull_request_id
2669 pr_id = stat.pull_request.pull_request_id
2670 pr_repo = stat.pull_request.target_repo.repo_name
2670 pr_repo = stat.pull_request.target_repo.repo_name
2671 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2671 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2672 pr_id, pr_repo]
2672 pr_id, pr_repo]
2673 return grouped
2673 return grouped
2674
2674
2675 # ==========================================================================
2675 # ==========================================================================
2676 # SCM CACHE INSTANCE
2676 # SCM CACHE INSTANCE
2677 # ==========================================================================
2677 # ==========================================================================
2678
2678
2679 def scm_instance(self, **kwargs):
2679 def scm_instance(self, **kwargs):
2680 import rhodecode
2680 import rhodecode
2681
2681
2682 # Passing a config will not hit the cache currently only used
2682 # Passing a config will not hit the cache currently only used
2683 # for repo2dbmapper
2683 # for repo2dbmapper
2684 config = kwargs.pop('config', None)
2684 config = kwargs.pop('config', None)
2685 cache = kwargs.pop('cache', None)
2685 cache = kwargs.pop('cache', None)
2686 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2686 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2687 if vcs_full_cache is not None:
2687 if vcs_full_cache is not None:
2688 # allows override global config
2688 # allows override global config
2689 full_cache = vcs_full_cache
2689 full_cache = vcs_full_cache
2690 else:
2690 else:
2691 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2691 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2692 # if cache is NOT defined use default global, else we have a full
2692 # if cache is NOT defined use default global, else we have a full
2693 # control over cache behaviour
2693 # control over cache behaviour
2694 if cache is None and full_cache and not config:
2694 if cache is None and full_cache and not config:
2695 log.debug('Initializing pure cached instance for %s', self.repo_path)
2695 log.debug('Initializing pure cached instance for %s', self.repo_path)
2696 return self._get_instance_cached()
2696 return self._get_instance_cached()
2697
2697
2698 # cache here is sent to the "vcs server"
2698 # cache here is sent to the "vcs server"
2699 return self._get_instance(cache=bool(cache), config=config)
2699 return self._get_instance(cache=bool(cache), config=config)
2700
2700
2701 def _get_instance_cached(self):
2701 def _get_instance_cached(self):
2702 from rhodecode.lib import rc_cache
2702 from rhodecode.lib import rc_cache
2703
2703
2704 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2704 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2705 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2705 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2706
2706
2707 # we must use thread scoped cache here,
2707 # we must use thread scoped cache here,
2708 # because each thread of gevent needs it's own not shared connection and cache
2708 # because each thread of gevent needs it's own not shared connection and cache
2709 # we also alter `args` so the cache key is individual for every green thread.
2709 # we also alter `args` so the cache key is individual for every green thread.
2710 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2710 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2711 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2711 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2712
2712
2713 # our wrapped caching function that takes state_uid to save the previous state in
2713 # our wrapped caching function that takes state_uid to save the previous state in
2714 def cache_generator(_state_uid):
2714 def cache_generator(_state_uid):
2715
2715
2716 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2716 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2717 def get_instance_cached(_repo_id, _process_context_id):
2717 def get_instance_cached(_repo_id, _process_context_id):
2718 # we save in cached func the generation state so we can detect a change and invalidate caches
2718 # we save in cached func the generation state so we can detect a change and invalidate caches
2719 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2719 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2720
2720
2721 return get_instance_cached
2721 return get_instance_cached
2722
2722
2723 with inv_context_manager as invalidation_context:
2723 with inv_context_manager as invalidation_context:
2724 cache_state_uid = invalidation_context.state_uid
2724 cache_state_uid = invalidation_context.state_uid
2725 cache_func = cache_generator(cache_state_uid)
2725 cache_func = cache_generator(cache_state_uid)
2726
2726
2727 args = self.repo_id, inv_context_manager.proc_key
2727 args = self.repo_id, inv_context_manager.proc_key
2728
2728
2729 previous_state_uid, instance = cache_func(*args)
2729 previous_state_uid, instance = cache_func(*args)
2730
2730
2731 # now compare keys, the "cache" state vs expected state.
2731 # now compare keys, the "cache" state vs expected state.
2732 if previous_state_uid != cache_state_uid:
2732 if previous_state_uid != cache_state_uid:
2733 log.warning('Cached state uid %s is different than current state uid %s',
2733 log.warning('Cached state uid %s is different than current state uid %s',
2734 previous_state_uid, cache_state_uid)
2734 previous_state_uid, cache_state_uid)
2735 _, instance = cache_func.refresh(*args)
2735 _, instance = cache_func.refresh(*args)
2736
2736
2737 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2737 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2738 return instance
2738 return instance
2739
2739
2740 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2740 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2741 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2741 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2742 self.repo_type, self.repo_path, cache)
2742 self.repo_type, self.repo_path, cache)
2743 config = config or self._config
2743 config = config or self._config
2744 custom_wire = {
2744 custom_wire = {
2745 'cache': cache, # controls the vcs.remote cache
2745 'cache': cache, # controls the vcs.remote cache
2746 'repo_state_uid': repo_state_uid
2746 'repo_state_uid': repo_state_uid
2747 }
2747 }
2748
2748
2749 repo = get_vcs_instance(
2749 repo = get_vcs_instance(
2750 repo_path=safe_str(self.repo_full_path),
2750 repo_path=safe_str(self.repo_full_path),
2751 config=config,
2751 config=config,
2752 with_wire=custom_wire,
2752 with_wire=custom_wire,
2753 create=False,
2753 create=False,
2754 _vcs_alias=self.repo_type)
2754 _vcs_alias=self.repo_type)
2755 if repo is not None:
2755 if repo is not None:
2756 repo.count() # cache rebuild
2756 repo.count() # cache rebuild
2757
2757
2758 return repo
2758 return repo
2759
2759
2760 def get_shadow_repository_path(self, workspace_id):
2760 def get_shadow_repository_path(self, workspace_id):
2761 from rhodecode.lib.vcs.backends.base import BaseRepository
2761 from rhodecode.lib.vcs.backends.base import BaseRepository
2762 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2762 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2763 self.repo_full_path, self.repo_id, workspace_id)
2763 self.repo_full_path, self.repo_id, workspace_id)
2764 return shadow_repo_path
2764 return shadow_repo_path
2765
2765
2766 def __json__(self):
2766 def __json__(self):
2767 return {'landing_rev': self.landing_rev}
2767 return {'landing_rev': self.landing_rev}
2768
2768
2769 def get_dict(self):
2769 def get_dict(self):
2770
2770
2771 # Since we transformed `repo_name` to a hybrid property, we need to
2771 # Since we transformed `repo_name` to a hybrid property, we need to
2772 # keep compatibility with the code which uses `repo_name` field.
2772 # keep compatibility with the code which uses `repo_name` field.
2773
2773
2774 result = super(Repository, self).get_dict()
2774 result = super(Repository, self).get_dict()
2775 result['repo_name'] = result.pop('_repo_name', None)
2775 result['repo_name'] = result.pop('_repo_name', None)
2776 result.pop('_changeset_cache', '')
2776 result.pop('_changeset_cache', '')
2777 return result
2777 return result
2778
2778
2779
2779
2780 class RepoGroup(Base, BaseModel):
2780 class RepoGroup(Base, BaseModel):
2781 __tablename__ = 'groups'
2781 __tablename__ = 'groups'
2782 __table_args__ = (
2782 __table_args__ = (
2783 UniqueConstraint('group_name', 'group_parent_id'),
2783 UniqueConstraint('group_name', 'group_parent_id'),
2784 base_table_args,
2784 base_table_args,
2785 )
2785 )
2786
2786
2787 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2787 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2788
2788
2789 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2789 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2790 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2790 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2791 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2791 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2792 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2792 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2793 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2793 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2794 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2794 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2795 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2795 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2796 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2796 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2797 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2797 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2798 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2798 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2799 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2799 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2800
2800
2801 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2801 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2802 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2802 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2803 parent_group = relationship('RepoGroup', remote_side=group_id)
2803 parent_group = relationship('RepoGroup', remote_side=group_id)
2804 user = relationship('User', back_populates='repository_groups')
2804 user = relationship('User', back_populates='repository_groups')
2805 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2805 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2806
2806
2807 # no cascade, set NULL
2807 # no cascade, set NULL
2808 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2808 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2809
2809
2810 def __init__(self, group_name='', parent_group=None):
2810 def __init__(self, group_name='', parent_group=None):
2811 self.group_name = group_name
2811 self.group_name = group_name
2812 self.parent_group = parent_group
2812 self.parent_group = parent_group
2813
2813
2814 def __repr__(self):
2814 def __repr__(self):
2815 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2815 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2816
2816
2817 @hybrid_property
2817 @hybrid_property
2818 def group_name(self):
2818 def group_name(self):
2819 return self._group_name
2819 return self._group_name
2820
2820
2821 @group_name.setter
2821 @group_name.setter
2822 def group_name(self, value):
2822 def group_name(self, value):
2823 self._group_name = value
2823 self._group_name = value
2824 self.group_name_hash = self.hash_repo_group_name(value)
2824 self.group_name_hash = self.hash_repo_group_name(value)
2825
2825
2826 @classmethod
2826 @classmethod
2827 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2827 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2828 from rhodecode.lib.vcs.backends.base import EmptyCommit
2828 from rhodecode.lib.vcs.backends.base import EmptyCommit
2829 dummy = EmptyCommit().__json__()
2829 dummy = EmptyCommit().__json__()
2830 if not changeset_cache_raw:
2830 if not changeset_cache_raw:
2831 dummy['source_repo_id'] = repo_id
2831 dummy['source_repo_id'] = repo_id
2832 return json.loads(json.dumps(dummy))
2832 return json.loads(json.dumps(dummy))
2833
2833
2834 try:
2834 try:
2835 return json.loads(changeset_cache_raw)
2835 return json.loads(changeset_cache_raw)
2836 except TypeError:
2836 except TypeError:
2837 return dummy
2837 return dummy
2838 except Exception:
2838 except Exception:
2839 log.error(traceback.format_exc())
2839 log.error(traceback.format_exc())
2840 return dummy
2840 return dummy
2841
2841
2842 @hybrid_property
2842 @hybrid_property
2843 def changeset_cache(self):
2843 def changeset_cache(self):
2844 return self._load_changeset_cache('', self._changeset_cache)
2844 return self._load_changeset_cache('', self._changeset_cache)
2845
2845
2846 @changeset_cache.setter
2846 @changeset_cache.setter
2847 def changeset_cache(self, val):
2847 def changeset_cache(self, val):
2848 try:
2848 try:
2849 self._changeset_cache = json.dumps(val)
2849 self._changeset_cache = json.dumps(val)
2850 except Exception:
2850 except Exception:
2851 log.error(traceback.format_exc())
2851 log.error(traceback.format_exc())
2852
2852
2853 @validates('group_parent_id')
2853 @validates('group_parent_id')
2854 def validate_group_parent_id(self, key, val):
2854 def validate_group_parent_id(self, key, val):
2855 """
2855 """
2856 Check cycle references for a parent group to self
2856 Check cycle references for a parent group to self
2857 """
2857 """
2858 if self.group_id and val:
2858 if self.group_id and val:
2859 assert val != self.group_id
2859 assert val != self.group_id
2860
2860
2861 return val
2861 return val
2862
2862
2863 @hybrid_property
2863 @hybrid_property
2864 def description_safe(self):
2864 def description_safe(self):
2865 from rhodecode.lib import helpers as h
2865 from rhodecode.lib import helpers as h
2866 return h.escape(self.group_description)
2866 return h.escape(self.group_description)
2867
2867
2868 @classmethod
2868 @classmethod
2869 def hash_repo_group_name(cls, repo_group_name):
2869 def hash_repo_group_name(cls, repo_group_name):
2870 val = remove_formatting(repo_group_name)
2870 val = remove_formatting(repo_group_name)
2871 val = safe_str(val).lower()
2871 val = safe_str(val).lower()
2872 chars = []
2872 chars = []
2873 for c in val:
2873 for c in val:
2874 if c not in string.ascii_letters:
2874 if c not in string.ascii_letters:
2875 c = str(ord(c))
2875 c = str(ord(c))
2876 chars.append(c)
2876 chars.append(c)
2877
2877
2878 return ''.join(chars)
2878 return ''.join(chars)
2879
2879
2880 @classmethod
2880 @classmethod
2881 def _generate_choice(cls, repo_group):
2881 def _generate_choice(cls, repo_group):
2882 from webhelpers2.html import literal as _literal
2882 from webhelpers2.html import literal as _literal
2883
2883
2884 def _name(k):
2884 def _name(k):
2885 return _literal(cls.CHOICES_SEPARATOR.join(k))
2885 return _literal(cls.CHOICES_SEPARATOR.join(k))
2886
2886
2887 return repo_group.group_id, _name(repo_group.full_path_splitted)
2887 return repo_group.group_id, _name(repo_group.full_path_splitted)
2888
2888
2889 @classmethod
2889 @classmethod
2890 def groups_choices(cls, groups=None, show_empty_group=True):
2890 def groups_choices(cls, groups=None, show_empty_group=True):
2891 if not groups:
2891 if not groups:
2892 groups = cls.query().all()
2892 groups = cls.query().all()
2893
2893
2894 repo_groups = []
2894 repo_groups = []
2895 if show_empty_group:
2895 if show_empty_group:
2896 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2896 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2897
2897
2898 repo_groups.extend([cls._generate_choice(x) for x in groups])
2898 repo_groups.extend([cls._generate_choice(x) for x in groups])
2899
2899
2900 repo_groups = sorted(
2900 repo_groups = sorted(
2901 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2901 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2902 return repo_groups
2902 return repo_groups
2903
2903
2904 @classmethod
2904 @classmethod
2905 def url_sep(cls):
2905 def url_sep(cls):
2906 return URL_SEP
2906 return URL_SEP
2907
2907
2908 @classmethod
2908 @classmethod
2909 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2909 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2910 if case_insensitive:
2910 if case_insensitive:
2911 gr = cls.query().filter(func.lower(cls.group_name)
2911 gr = cls.query().filter(func.lower(cls.group_name)
2912 == func.lower(group_name))
2912 == func.lower(group_name))
2913 else:
2913 else:
2914 gr = cls.query().filter(cls.group_name == group_name)
2914 gr = cls.query().filter(cls.group_name == group_name)
2915 if cache:
2915 if cache:
2916 name_key = _hash_key(group_name)
2916 name_key = _hash_key(group_name)
2917 gr = gr.options(
2917 gr = gr.options(
2918 FromCache("sql_cache_short", f"get_group_{name_key}"))
2918 FromCache("sql_cache_short", f"get_group_{name_key}"))
2919 return gr.scalar()
2919 return gr.scalar()
2920
2920
2921 @classmethod
2921 @classmethod
2922 def get_user_personal_repo_group(cls, user_id):
2922 def get_user_personal_repo_group(cls, user_id):
2923 user = User.get(user_id)
2923 user = User.get(user_id)
2924 if user.username == User.DEFAULT_USER:
2924 if user.username == User.DEFAULT_USER:
2925 return None
2925 return None
2926
2926
2927 return cls.query()\
2927 return cls.query()\
2928 .filter(cls.personal == true()) \
2928 .filter(cls.personal == true()) \
2929 .filter(cls.user == user) \
2929 .filter(cls.user == user) \
2930 .order_by(cls.group_id.asc()) \
2930 .order_by(cls.group_id.asc()) \
2931 .first()
2931 .first()
2932
2932
2933 @classmethod
2933 @classmethod
2934 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2934 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2935 case_insensitive=True):
2935 case_insensitive=True):
2936 q = RepoGroup.query()
2936 q = RepoGroup.query()
2937
2937
2938 if not isinstance(user_id, Optional):
2938 if not isinstance(user_id, Optional):
2939 q = q.filter(RepoGroup.user_id == user_id)
2939 q = q.filter(RepoGroup.user_id == user_id)
2940
2940
2941 if not isinstance(group_id, Optional):
2941 if not isinstance(group_id, Optional):
2942 q = q.filter(RepoGroup.group_parent_id == group_id)
2942 q = q.filter(RepoGroup.group_parent_id == group_id)
2943
2943
2944 if case_insensitive:
2944 if case_insensitive:
2945 q = q.order_by(func.lower(RepoGroup.group_name))
2945 q = q.order_by(func.lower(RepoGroup.group_name))
2946 else:
2946 else:
2947 q = q.order_by(RepoGroup.group_name)
2947 q = q.order_by(RepoGroup.group_name)
2948 return q.all()
2948 return q.all()
2949
2949
2950 @property
2950 @property
2951 def parents(self, parents_recursion_limit=10):
2951 def parents(self, parents_recursion_limit=10):
2952 groups = []
2952 groups = []
2953 if self.parent_group is None:
2953 if self.parent_group is None:
2954 return groups
2954 return groups
2955 cur_gr = self.parent_group
2955 cur_gr = self.parent_group
2956 groups.insert(0, cur_gr)
2956 groups.insert(0, cur_gr)
2957 cnt = 0
2957 cnt = 0
2958 while 1:
2958 while 1:
2959 cnt += 1
2959 cnt += 1
2960 gr = getattr(cur_gr, 'parent_group', None)
2960 gr = getattr(cur_gr, 'parent_group', None)
2961 cur_gr = cur_gr.parent_group
2961 cur_gr = cur_gr.parent_group
2962 if gr is None:
2962 if gr is None:
2963 break
2963 break
2964 if cnt == parents_recursion_limit:
2964 if cnt == parents_recursion_limit:
2965 # this will prevent accidental infinit loops
2965 # this will prevent accidental infinit loops
2966 log.error('more than %s parents found for group %s, stopping '
2966 log.error('more than %s parents found for group %s, stopping '
2967 'recursive parent fetching', parents_recursion_limit, self)
2967 'recursive parent fetching', parents_recursion_limit, self)
2968 break
2968 break
2969
2969
2970 groups.insert(0, gr)
2970 groups.insert(0, gr)
2971 return groups
2971 return groups
2972
2972
2973 @property
2973 @property
2974 def last_commit_cache_update_diff(self):
2974 def last_commit_cache_update_diff(self):
2975 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2975 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2976
2976
2977 @classmethod
2977 @classmethod
2978 def _load_commit_change(cls, last_commit_cache):
2978 def _load_commit_change(cls, last_commit_cache):
2979 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2979 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2980 empty_date = datetime.datetime.fromtimestamp(0)
2980 empty_date = datetime.datetime.fromtimestamp(0)
2981 date_latest = last_commit_cache.get('date', empty_date)
2981 date_latest = last_commit_cache.get('date', empty_date)
2982 try:
2982 try:
2983 return parse_datetime(date_latest)
2983 return parse_datetime(date_latest)
2984 except Exception:
2984 except Exception:
2985 return empty_date
2985 return empty_date
2986
2986
2987 @property
2987 @property
2988 def last_commit_change(self):
2988 def last_commit_change(self):
2989 return self._load_commit_change(self.changeset_cache)
2989 return self._load_commit_change(self.changeset_cache)
2990
2990
2991 @property
2991 @property
2992 def last_db_change(self):
2992 def last_db_change(self):
2993 return self.updated_on
2993 return self.updated_on
2994
2994
2995 @property
2995 @property
2996 def children(self):
2996 def children(self):
2997 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2997 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2998
2998
2999 @property
2999 @property
3000 def name(self):
3000 def name(self):
3001 return self.group_name.split(RepoGroup.url_sep())[-1]
3001 return self.group_name.split(RepoGroup.url_sep())[-1]
3002
3002
3003 @property
3003 @property
3004 def full_path(self):
3004 def full_path(self):
3005 return self.group_name
3005 return self.group_name
3006
3006
3007 @property
3007 @property
3008 def full_path_splitted(self):
3008 def full_path_splitted(self):
3009 return self.group_name.split(RepoGroup.url_sep())
3009 return self.group_name.split(RepoGroup.url_sep())
3010
3010
3011 @property
3011 @property
3012 def repositories(self):
3012 def repositories(self):
3013 return Repository.query()\
3013 return Repository.query()\
3014 .filter(Repository.group == self)\
3014 .filter(Repository.group == self)\
3015 .order_by(Repository.repo_name)
3015 .order_by(Repository.repo_name)
3016
3016
3017 @property
3017 @property
3018 def repositories_recursive_count(self):
3018 def repositories_recursive_count(self):
3019 cnt = self.repositories.count()
3019 cnt = self.repositories.count()
3020
3020
3021 def children_count(group):
3021 def children_count(group):
3022 cnt = 0
3022 cnt = 0
3023 for child in group.children:
3023 for child in group.children:
3024 cnt += child.repositories.count()
3024 cnt += child.repositories.count()
3025 cnt += children_count(child)
3025 cnt += children_count(child)
3026 return cnt
3026 return cnt
3027
3027
3028 return cnt + children_count(self)
3028 return cnt + children_count(self)
3029
3029
3030 def _recursive_objects(self, include_repos=True, include_groups=True):
3030 def _recursive_objects(self, include_repos=True, include_groups=True):
3031 all_ = []
3031 all_ = []
3032
3032
3033 def _get_members(root_gr):
3033 def _get_members(root_gr):
3034 if include_repos:
3034 if include_repos:
3035 for r in root_gr.repositories:
3035 for r in root_gr.repositories:
3036 all_.append(r)
3036 all_.append(r)
3037 childs = root_gr.children.all()
3037 childs = root_gr.children.all()
3038 if childs:
3038 if childs:
3039 for gr in childs:
3039 for gr in childs:
3040 if include_groups:
3040 if include_groups:
3041 all_.append(gr)
3041 all_.append(gr)
3042 _get_members(gr)
3042 _get_members(gr)
3043
3043
3044 root_group = []
3044 root_group = []
3045 if include_groups:
3045 if include_groups:
3046 root_group = [self]
3046 root_group = [self]
3047
3047
3048 _get_members(self)
3048 _get_members(self)
3049 return root_group + all_
3049 return root_group + all_
3050
3050
3051 def recursive_groups_and_repos(self):
3051 def recursive_groups_and_repos(self):
3052 """
3052 """
3053 Recursive return all groups, with repositories in those groups
3053 Recursive return all groups, with repositories in those groups
3054 """
3054 """
3055 return self._recursive_objects()
3055 return self._recursive_objects()
3056
3056
3057 def recursive_groups(self):
3057 def recursive_groups(self):
3058 """
3058 """
3059 Returns all children groups for this group including children of children
3059 Returns all children groups for this group including children of children
3060 """
3060 """
3061 return self._recursive_objects(include_repos=False)
3061 return self._recursive_objects(include_repos=False)
3062
3062
3063 def recursive_repos(self):
3063 def recursive_repos(self):
3064 """
3064 """
3065 Returns all children repositories for this group
3065 Returns all children repositories for this group
3066 """
3066 """
3067 return self._recursive_objects(include_groups=False)
3067 return self._recursive_objects(include_groups=False)
3068
3068
3069 def get_new_name(self, group_name):
3069 def get_new_name(self, group_name):
3070 """
3070 """
3071 returns new full group name based on parent and new name
3071 returns new full group name based on parent and new name
3072
3072
3073 :param group_name:
3073 :param group_name:
3074 """
3074 """
3075 path_prefix = (self.parent_group.full_path_splitted if
3075 path_prefix = (self.parent_group.full_path_splitted if
3076 self.parent_group else [])
3076 self.parent_group else [])
3077 return RepoGroup.url_sep().join(path_prefix + [group_name])
3077 return RepoGroup.url_sep().join(path_prefix + [group_name])
3078
3078
3079 def update_commit_cache(self, config=None):
3079 def update_commit_cache(self, config=None):
3080 """
3080 """
3081 Update cache of last commit for newest repository inside this repository group.
3081 Update cache of last commit for newest repository inside this repository group.
3082 cache_keys should be::
3082 cache_keys should be::
3083
3083
3084 source_repo_id
3084 source_repo_id
3085 short_id
3085 short_id
3086 raw_id
3086 raw_id
3087 revision
3087 revision
3088 parents
3088 parents
3089 message
3089 message
3090 date
3090 date
3091 author
3091 author
3092
3092
3093 """
3093 """
3094 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3094 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3095 empty_date = datetime.datetime.fromtimestamp(0)
3095 empty_date = datetime.datetime.fromtimestamp(0)
3096
3096
3097 def repo_groups_and_repos(root_gr):
3097 def repo_groups_and_repos(root_gr):
3098 for _repo in root_gr.repositories:
3098 for _repo in root_gr.repositories:
3099 yield _repo
3099 yield _repo
3100 for child_group in root_gr.children.all():
3100 for child_group in root_gr.children.all():
3101 yield child_group
3101 yield child_group
3102
3102
3103 latest_repo_cs_cache = {}
3103 latest_repo_cs_cache = {}
3104 for obj in repo_groups_and_repos(self):
3104 for obj in repo_groups_and_repos(self):
3105 repo_cs_cache = obj.changeset_cache
3105 repo_cs_cache = obj.changeset_cache
3106 date_latest = latest_repo_cs_cache.get('date', empty_date)
3106 date_latest = latest_repo_cs_cache.get('date', empty_date)
3107 date_current = repo_cs_cache.get('date', empty_date)
3107 date_current = repo_cs_cache.get('date', empty_date)
3108 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3108 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3109 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3109 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3110 latest_repo_cs_cache = repo_cs_cache
3110 latest_repo_cs_cache = repo_cs_cache
3111 if hasattr(obj, 'repo_id'):
3111 if hasattr(obj, 'repo_id'):
3112 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3112 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3113 else:
3113 else:
3114 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3114 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3115
3115
3116 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3116 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3117
3117
3118 latest_repo_cs_cache['updated_on'] = time.time()
3118 latest_repo_cs_cache['updated_on'] = time.time()
3119 self.changeset_cache = latest_repo_cs_cache
3119 self.changeset_cache = latest_repo_cs_cache
3120 self.updated_on = _date_latest
3120 self.updated_on = _date_latest
3121 Session().add(self)
3121 Session().add(self)
3122 Session().commit()
3122 Session().commit()
3123
3123
3124 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3124 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3125 self.group_name, latest_repo_cs_cache, _date_latest)
3125 self.group_name, latest_repo_cs_cache, _date_latest)
3126
3126
3127 def permissions(self, with_admins=True, with_owner=True,
3127 def permissions(self, with_admins=True, with_owner=True,
3128 expand_from_user_groups=False):
3128 expand_from_user_groups=False):
3129 """
3129 """
3130 Permissions for repository groups
3130 Permissions for repository groups
3131 """
3131 """
3132 _admin_perm = 'group.admin'
3132 _admin_perm = 'group.admin'
3133
3133
3134 owner_row = []
3134 owner_row = []
3135 if with_owner:
3135 if with_owner:
3136 usr = AttributeDict(self.user.get_dict())
3136 usr = AttributeDict(self.user.get_dict())
3137 usr.owner_row = True
3137 usr.owner_row = True
3138 usr.permission = _admin_perm
3138 usr.permission = _admin_perm
3139 owner_row.append(usr)
3139 owner_row.append(usr)
3140
3140
3141 super_admin_ids = []
3141 super_admin_ids = []
3142 super_admin_rows = []
3142 super_admin_rows = []
3143 if with_admins:
3143 if with_admins:
3144 for usr in User.get_all_super_admins():
3144 for usr in User.get_all_super_admins():
3145 super_admin_ids.append(usr.user_id)
3145 super_admin_ids.append(usr.user_id)
3146 # if this admin is also owner, don't double the record
3146 # if this admin is also owner, don't double the record
3147 if usr.user_id == owner_row[0].user_id:
3147 if usr.user_id == owner_row[0].user_id:
3148 owner_row[0].admin_row = True
3148 owner_row[0].admin_row = True
3149 else:
3149 else:
3150 usr = AttributeDict(usr.get_dict())
3150 usr = AttributeDict(usr.get_dict())
3151 usr.admin_row = True
3151 usr.admin_row = True
3152 usr.permission = _admin_perm
3152 usr.permission = _admin_perm
3153 super_admin_rows.append(usr)
3153 super_admin_rows.append(usr)
3154
3154
3155 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3155 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3156 q = q.options(joinedload(UserRepoGroupToPerm.group),
3156 q = q.options(joinedload(UserRepoGroupToPerm.group),
3157 joinedload(UserRepoGroupToPerm.user),
3157 joinedload(UserRepoGroupToPerm.user),
3158 joinedload(UserRepoGroupToPerm.permission),)
3158 joinedload(UserRepoGroupToPerm.permission),)
3159
3159
3160 # get owners and admins and permissions. We do a trick of re-writing
3160 # get owners and admins and permissions. We do a trick of re-writing
3161 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3161 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3162 # has a global reference and changing one object propagates to all
3162 # has a global reference and changing one object propagates to all
3163 # others. This means if admin is also an owner admin_row that change
3163 # others. This means if admin is also an owner admin_row that change
3164 # would propagate to both objects
3164 # would propagate to both objects
3165 perm_rows = []
3165 perm_rows = []
3166 for _usr in q.all():
3166 for _usr in q.all():
3167 usr = AttributeDict(_usr.user.get_dict())
3167 usr = AttributeDict(_usr.user.get_dict())
3168 # if this user is also owner/admin, mark as duplicate record
3168 # if this user is also owner/admin, mark as duplicate record
3169 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3169 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3170 usr.duplicate_perm = True
3170 usr.duplicate_perm = True
3171 usr.permission = _usr.permission.permission_name
3171 usr.permission = _usr.permission.permission_name
3172 perm_rows.append(usr)
3172 perm_rows.append(usr)
3173
3173
3174 # filter the perm rows by 'default' first and then sort them by
3174 # filter the perm rows by 'default' first and then sort them by
3175 # admin,write,read,none permissions sorted again alphabetically in
3175 # admin,write,read,none permissions sorted again alphabetically in
3176 # each group
3176 # each group
3177 perm_rows = sorted(perm_rows, key=display_user_sort)
3177 perm_rows = sorted(perm_rows, key=display_user_sort)
3178
3178
3179 user_groups_rows = []
3179 user_groups_rows = []
3180 if expand_from_user_groups:
3180 if expand_from_user_groups:
3181 for ug in self.permission_user_groups(with_members=True):
3181 for ug in self.permission_user_groups(with_members=True):
3182 for user_data in ug.members:
3182 for user_data in ug.members:
3183 user_groups_rows.append(user_data)
3183 user_groups_rows.append(user_data)
3184
3184
3185 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3185 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3186
3186
3187 def permission_user_groups(self, with_members=False):
3187 def permission_user_groups(self, with_members=False):
3188 q = UserGroupRepoGroupToPerm.query()\
3188 q = UserGroupRepoGroupToPerm.query()\
3189 .filter(UserGroupRepoGroupToPerm.group == self)
3189 .filter(UserGroupRepoGroupToPerm.group == self)
3190 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3190 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3191 joinedload(UserGroupRepoGroupToPerm.users_group),
3191 joinedload(UserGroupRepoGroupToPerm.users_group),
3192 joinedload(UserGroupRepoGroupToPerm.permission),)
3192 joinedload(UserGroupRepoGroupToPerm.permission),)
3193
3193
3194 perm_rows = []
3194 perm_rows = []
3195 for _user_group in q.all():
3195 for _user_group in q.all():
3196 entry = AttributeDict(_user_group.users_group.get_dict())
3196 entry = AttributeDict(_user_group.users_group.get_dict())
3197 entry.permission = _user_group.permission.permission_name
3197 entry.permission = _user_group.permission.permission_name
3198 if with_members:
3198 if with_members:
3199 entry.members = [x.user.get_dict()
3199 entry.members = [x.user.get_dict()
3200 for x in _user_group.users_group.members]
3200 for x in _user_group.users_group.members]
3201 perm_rows.append(entry)
3201 perm_rows.append(entry)
3202
3202
3203 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3203 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3204 return perm_rows
3204 return perm_rows
3205
3205
3206 def get_api_data(self):
3206 def get_api_data(self):
3207 """
3207 """
3208 Common function for generating api data
3208 Common function for generating api data
3209
3209
3210 """
3210 """
3211 group = self
3211 group = self
3212 data = {
3212 data = {
3213 'group_id': group.group_id,
3213 'group_id': group.group_id,
3214 'group_name': group.group_name,
3214 'group_name': group.group_name,
3215 'group_description': group.description_safe,
3215 'group_description': group.description_safe,
3216 'parent_group': group.parent_group.group_name if group.parent_group else None,
3216 'parent_group': group.parent_group.group_name if group.parent_group else None,
3217 'repositories': [x.repo_name for x in group.repositories],
3217 'repositories': [x.repo_name for x in group.repositories],
3218 'owner': group.user.username,
3218 'owner': group.user.username,
3219 }
3219 }
3220 return data
3220 return data
3221
3221
3222 def get_dict(self):
3222 def get_dict(self):
3223 # Since we transformed `group_name` to a hybrid property, we need to
3223 # Since we transformed `group_name` to a hybrid property, we need to
3224 # keep compatibility with the code which uses `group_name` field.
3224 # keep compatibility with the code which uses `group_name` field.
3225 result = super(RepoGroup, self).get_dict()
3225 result = super(RepoGroup, self).get_dict()
3226 result['group_name'] = result.pop('_group_name', None)
3226 result['group_name'] = result.pop('_group_name', None)
3227 result.pop('_changeset_cache', '')
3227 result.pop('_changeset_cache', '')
3228 return result
3228 return result
3229
3229
3230
3230
3231 class Permission(Base, BaseModel):
3231 class Permission(Base, BaseModel):
3232 __tablename__ = 'permissions'
3232 __tablename__ = 'permissions'
3233 __table_args__ = (
3233 __table_args__ = (
3234 Index('p_perm_name_idx', 'permission_name'),
3234 Index('p_perm_name_idx', 'permission_name'),
3235 base_table_args,
3235 base_table_args,
3236 )
3236 )
3237
3237
3238 PERMS = [
3238 PERMS = [
3239 ('hg.admin', _('RhodeCode Super Administrator')),
3239 ('hg.admin', _('RhodeCode Super Administrator')),
3240
3240
3241 ('repository.none', _('Repository no access')),
3241 ('repository.none', _('Repository no access')),
3242 ('repository.read', _('Repository read access')),
3242 ('repository.read', _('Repository read access')),
3243 ('repository.write', _('Repository write access')),
3243 ('repository.write', _('Repository write access')),
3244 ('repository.admin', _('Repository admin access')),
3244 ('repository.admin', _('Repository admin access')),
3245
3245
3246 ('group.none', _('Repository group no access')),
3246 ('group.none', _('Repository group no access')),
3247 ('group.read', _('Repository group read access')),
3247 ('group.read', _('Repository group read access')),
3248 ('group.write', _('Repository group write access')),
3248 ('group.write', _('Repository group write access')),
3249 ('group.admin', _('Repository group admin access')),
3249 ('group.admin', _('Repository group admin access')),
3250
3250
3251 ('usergroup.none', _('User group no access')),
3251 ('usergroup.none', _('User group no access')),
3252 ('usergroup.read', _('User group read access')),
3252 ('usergroup.read', _('User group read access')),
3253 ('usergroup.write', _('User group write access')),
3253 ('usergroup.write', _('User group write access')),
3254 ('usergroup.admin', _('User group admin access')),
3254 ('usergroup.admin', _('User group admin access')),
3255
3255
3256 ('branch.none', _('Branch no permissions')),
3256 ('branch.none', _('Branch no permissions')),
3257 ('branch.merge', _('Branch access by web merge')),
3257 ('branch.merge', _('Branch access by web merge')),
3258 ('branch.push', _('Branch access by push')),
3258 ('branch.push', _('Branch access by push')),
3259 ('branch.push_force', _('Branch access by push with force')),
3259 ('branch.push_force', _('Branch access by push with force')),
3260
3260
3261 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3261 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3262 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3262 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3263
3263
3264 ('hg.usergroup.create.false', _('User Group creation disabled')),
3264 ('hg.usergroup.create.false', _('User Group creation disabled')),
3265 ('hg.usergroup.create.true', _('User Group creation enabled')),
3265 ('hg.usergroup.create.true', _('User Group creation enabled')),
3266
3266
3267 ('hg.create.none', _('Repository creation disabled')),
3267 ('hg.create.none', _('Repository creation disabled')),
3268 ('hg.create.repository', _('Repository creation enabled')),
3268 ('hg.create.repository', _('Repository creation enabled')),
3269 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3269 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3270 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3270 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3271
3271
3272 ('hg.fork.none', _('Repository forking disabled')),
3272 ('hg.fork.none', _('Repository forking disabled')),
3273 ('hg.fork.repository', _('Repository forking enabled')),
3273 ('hg.fork.repository', _('Repository forking enabled')),
3274
3274
3275 ('hg.register.none', _('Registration disabled')),
3275 ('hg.register.none', _('Registration disabled')),
3276 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3276 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3277 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3277 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3278
3278
3279 ('hg.password_reset.enabled', _('Password reset enabled')),
3279 ('hg.password_reset.enabled', _('Password reset enabled')),
3280 ('hg.password_reset.hidden', _('Password reset hidden')),
3280 ('hg.password_reset.hidden', _('Password reset hidden')),
3281 ('hg.password_reset.disabled', _('Password reset disabled')),
3281 ('hg.password_reset.disabled', _('Password reset disabled')),
3282
3282
3283 ('hg.extern_activate.manual', _('Manual activation of external account')),
3283 ('hg.extern_activate.manual', _('Manual activation of external account')),
3284 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3284 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3285
3285
3286 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3286 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3287 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3287 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3288 ]
3288 ]
3289
3289
3290 # definition of system default permissions for DEFAULT user, created on
3290 # definition of system default permissions for DEFAULT user, created on
3291 # system setup
3291 # system setup
3292 DEFAULT_USER_PERMISSIONS = [
3292 DEFAULT_USER_PERMISSIONS = [
3293 # object perms
3293 # object perms
3294 'repository.read',
3294 'repository.read',
3295 'group.read',
3295 'group.read',
3296 'usergroup.read',
3296 'usergroup.read',
3297 # branch, for backward compat we need same value as before so forced pushed
3297 # branch, for backward compat we need same value as before so forced pushed
3298 'branch.push_force',
3298 'branch.push_force',
3299 # global
3299 # global
3300 'hg.create.repository',
3300 'hg.create.repository',
3301 'hg.repogroup.create.false',
3301 'hg.repogroup.create.false',
3302 'hg.usergroup.create.false',
3302 'hg.usergroup.create.false',
3303 'hg.create.write_on_repogroup.true',
3303 'hg.create.write_on_repogroup.true',
3304 'hg.fork.repository',
3304 'hg.fork.repository',
3305 'hg.register.manual_activate',
3305 'hg.register.manual_activate',
3306 'hg.password_reset.enabled',
3306 'hg.password_reset.enabled',
3307 'hg.extern_activate.auto',
3307 'hg.extern_activate.auto',
3308 'hg.inherit_default_perms.true',
3308 'hg.inherit_default_perms.true',
3309 ]
3309 ]
3310
3310
3311 # defines which permissions are more important higher the more important
3311 # defines which permissions are more important higher the more important
3312 # Weight defines which permissions are more important.
3312 # Weight defines which permissions are more important.
3313 # The higher number the more important.
3313 # The higher number the more important.
3314 PERM_WEIGHTS = {
3314 PERM_WEIGHTS = {
3315 'repository.none': 0,
3315 'repository.none': 0,
3316 'repository.read': 1,
3316 'repository.read': 1,
3317 'repository.write': 3,
3317 'repository.write': 3,
3318 'repository.admin': 4,
3318 'repository.admin': 4,
3319
3319
3320 'group.none': 0,
3320 'group.none': 0,
3321 'group.read': 1,
3321 'group.read': 1,
3322 'group.write': 3,
3322 'group.write': 3,
3323 'group.admin': 4,
3323 'group.admin': 4,
3324
3324
3325 'usergroup.none': 0,
3325 'usergroup.none': 0,
3326 'usergroup.read': 1,
3326 'usergroup.read': 1,
3327 'usergroup.write': 3,
3327 'usergroup.write': 3,
3328 'usergroup.admin': 4,
3328 'usergroup.admin': 4,
3329
3329
3330 'branch.none': 0,
3330 'branch.none': 0,
3331 'branch.merge': 1,
3331 'branch.merge': 1,
3332 'branch.push': 3,
3332 'branch.push': 3,
3333 'branch.push_force': 4,
3333 'branch.push_force': 4,
3334
3334
3335 'hg.repogroup.create.false': 0,
3335 'hg.repogroup.create.false': 0,
3336 'hg.repogroup.create.true': 1,
3336 'hg.repogroup.create.true': 1,
3337
3337
3338 'hg.usergroup.create.false': 0,
3338 'hg.usergroup.create.false': 0,
3339 'hg.usergroup.create.true': 1,
3339 'hg.usergroup.create.true': 1,
3340
3340
3341 'hg.fork.none': 0,
3341 'hg.fork.none': 0,
3342 'hg.fork.repository': 1,
3342 'hg.fork.repository': 1,
3343 'hg.create.none': 0,
3343 'hg.create.none': 0,
3344 'hg.create.repository': 1
3344 'hg.create.repository': 1
3345 }
3345 }
3346
3346
3347 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3347 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3348 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3348 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3349 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3349 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3350
3350
3351 def __repr__(self):
3351 def __repr__(self):
3352 return "<%s('%s:%s')>" % (
3352 return "<%s('%s:%s')>" % (
3353 self.cls_name, self.permission_id, self.permission_name
3353 self.cls_name, self.permission_id, self.permission_name
3354 )
3354 )
3355
3355
3356 @classmethod
3356 @classmethod
3357 def get_by_key(cls, key):
3357 def get_by_key(cls, key):
3358 return cls.query().filter(cls.permission_name == key).scalar()
3358 return cls.query().filter(cls.permission_name == key).scalar()
3359
3359
3360 @classmethod
3360 @classmethod
3361 def get_default_repo_perms(cls, user_id, repo_id=None):
3361 def get_default_repo_perms(cls, user_id, repo_id=None):
3362 q = Session().query(UserRepoToPerm, Repository, Permission)\
3362 q = Session().query(UserRepoToPerm, Repository, Permission)\
3363 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3363 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3364 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3364 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3365 .filter(UserRepoToPerm.user_id == user_id)
3365 .filter(UserRepoToPerm.user_id == user_id)
3366 if repo_id:
3366 if repo_id:
3367 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3367 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3368 return q.all()
3368 return q.all()
3369
3369
3370 @classmethod
3370 @classmethod
3371 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3371 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3372 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3372 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3373 .join(
3373 .join(
3374 Permission,
3374 Permission,
3375 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3375 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3376 .join(
3376 .join(
3377 UserRepoToPerm,
3377 UserRepoToPerm,
3378 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3378 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3379 .filter(UserRepoToPerm.user_id == user_id)
3379 .filter(UserRepoToPerm.user_id == user_id)
3380
3380
3381 if repo_id:
3381 if repo_id:
3382 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3382 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3383 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3383 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3384
3384
3385 @classmethod
3385 @classmethod
3386 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3386 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3387 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3387 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3388 .join(
3388 .join(
3389 Permission,
3389 Permission,
3390 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3390 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3391 .join(
3391 .join(
3392 Repository,
3392 Repository,
3393 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3393 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3394 .join(
3394 .join(
3395 UserGroup,
3395 UserGroup,
3396 UserGroupRepoToPerm.users_group_id ==
3396 UserGroupRepoToPerm.users_group_id ==
3397 UserGroup.users_group_id)\
3397 UserGroup.users_group_id)\
3398 .join(
3398 .join(
3399 UserGroupMember,
3399 UserGroupMember,
3400 UserGroupRepoToPerm.users_group_id ==
3400 UserGroupRepoToPerm.users_group_id ==
3401 UserGroupMember.users_group_id)\
3401 UserGroupMember.users_group_id)\
3402 .filter(
3402 .filter(
3403 UserGroupMember.user_id == user_id,
3403 UserGroupMember.user_id == user_id,
3404 UserGroup.users_group_active == true())
3404 UserGroup.users_group_active == true())
3405 if repo_id:
3405 if repo_id:
3406 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3406 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3407 return q.all()
3407 return q.all()
3408
3408
3409 @classmethod
3409 @classmethod
3410 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3410 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3411 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3411 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3412 .join(
3412 .join(
3413 Permission,
3413 Permission,
3414 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3414 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3415 .join(
3415 .join(
3416 UserGroupRepoToPerm,
3416 UserGroupRepoToPerm,
3417 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3417 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3418 .join(
3418 .join(
3419 UserGroup,
3419 UserGroup,
3420 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3420 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3421 .join(
3421 .join(
3422 UserGroupMember,
3422 UserGroupMember,
3423 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3423 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3424 .filter(
3424 .filter(
3425 UserGroupMember.user_id == user_id,
3425 UserGroupMember.user_id == user_id,
3426 UserGroup.users_group_active == true())
3426 UserGroup.users_group_active == true())
3427
3427
3428 if repo_id:
3428 if repo_id:
3429 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3429 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3430 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3430 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3431
3431
3432 @classmethod
3432 @classmethod
3433 def get_default_group_perms(cls, user_id, repo_group_id=None):
3433 def get_default_group_perms(cls, user_id, repo_group_id=None):
3434 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3434 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3435 .join(
3435 .join(
3436 Permission,
3436 Permission,
3437 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3437 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3438 .join(
3438 .join(
3439 RepoGroup,
3439 RepoGroup,
3440 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3440 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3441 .filter(UserRepoGroupToPerm.user_id == user_id)
3441 .filter(UserRepoGroupToPerm.user_id == user_id)
3442 if repo_group_id:
3442 if repo_group_id:
3443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3444 return q.all()
3444 return q.all()
3445
3445
3446 @classmethod
3446 @classmethod
3447 def get_default_group_perms_from_user_group(
3447 def get_default_group_perms_from_user_group(
3448 cls, user_id, repo_group_id=None):
3448 cls, user_id, repo_group_id=None):
3449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3450 .join(
3450 .join(
3451 Permission,
3451 Permission,
3452 UserGroupRepoGroupToPerm.permission_id ==
3452 UserGroupRepoGroupToPerm.permission_id ==
3453 Permission.permission_id)\
3453 Permission.permission_id)\
3454 .join(
3454 .join(
3455 RepoGroup,
3455 RepoGroup,
3456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3457 .join(
3457 .join(
3458 UserGroup,
3458 UserGroup,
3459 UserGroupRepoGroupToPerm.users_group_id ==
3459 UserGroupRepoGroupToPerm.users_group_id ==
3460 UserGroup.users_group_id)\
3460 UserGroup.users_group_id)\
3461 .join(
3461 .join(
3462 UserGroupMember,
3462 UserGroupMember,
3463 UserGroupRepoGroupToPerm.users_group_id ==
3463 UserGroupRepoGroupToPerm.users_group_id ==
3464 UserGroupMember.users_group_id)\
3464 UserGroupMember.users_group_id)\
3465 .filter(
3465 .filter(
3466 UserGroupMember.user_id == user_id,
3466 UserGroupMember.user_id == user_id,
3467 UserGroup.users_group_active == true())
3467 UserGroup.users_group_active == true())
3468 if repo_group_id:
3468 if repo_group_id:
3469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3470 return q.all()
3470 return q.all()
3471
3471
3472 @classmethod
3472 @classmethod
3473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3477 .filter(UserUserGroupToPerm.user_id == user_id)
3477 .filter(UserUserGroupToPerm.user_id == user_id)
3478 if user_group_id:
3478 if user_group_id:
3479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3480 return q.all()
3480 return q.all()
3481
3481
3482 @classmethod
3482 @classmethod
3483 def get_default_user_group_perms_from_user_group(
3483 def get_default_user_group_perms_from_user_group(
3484 cls, user_id, user_group_id=None):
3484 cls, user_id, user_group_id=None):
3485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3487 .join(
3487 .join(
3488 Permission,
3488 Permission,
3489 UserGroupUserGroupToPerm.permission_id ==
3489 UserGroupUserGroupToPerm.permission_id ==
3490 Permission.permission_id)\
3490 Permission.permission_id)\
3491 .join(
3491 .join(
3492 TargetUserGroup,
3492 TargetUserGroup,
3493 UserGroupUserGroupToPerm.target_user_group_id ==
3493 UserGroupUserGroupToPerm.target_user_group_id ==
3494 TargetUserGroup.users_group_id)\
3494 TargetUserGroup.users_group_id)\
3495 .join(
3495 .join(
3496 UserGroup,
3496 UserGroup,
3497 UserGroupUserGroupToPerm.user_group_id ==
3497 UserGroupUserGroupToPerm.user_group_id ==
3498 UserGroup.users_group_id)\
3498 UserGroup.users_group_id)\
3499 .join(
3499 .join(
3500 UserGroupMember,
3500 UserGroupMember,
3501 UserGroupUserGroupToPerm.user_group_id ==
3501 UserGroupUserGroupToPerm.user_group_id ==
3502 UserGroupMember.users_group_id)\
3502 UserGroupMember.users_group_id)\
3503 .filter(
3503 .filter(
3504 UserGroupMember.user_id == user_id,
3504 UserGroupMember.user_id == user_id,
3505 UserGroup.users_group_active == true())
3505 UserGroup.users_group_active == true())
3506 if user_group_id:
3506 if user_group_id:
3507 q = q.filter(
3507 q = q.filter(
3508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3509
3509
3510 return q.all()
3510 return q.all()
3511
3511
3512
3512
3513 class UserRepoToPerm(Base, BaseModel):
3513 class UserRepoToPerm(Base, BaseModel):
3514 __tablename__ = 'repo_to_perm'
3514 __tablename__ = 'repo_to_perm'
3515 __table_args__ = (
3515 __table_args__ = (
3516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3517 base_table_args
3517 base_table_args
3518 )
3518 )
3519
3519
3520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3524
3524
3525 user = relationship('User', back_populates="repo_to_perm")
3525 user = relationship('User', back_populates="repo_to_perm")
3526 repository = relationship('Repository', back_populates="repo_to_perm")
3526 repository = relationship('Repository', back_populates="repo_to_perm")
3527 permission = relationship('Permission')
3527 permission = relationship('Permission')
3528
3528
3529 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3529 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3530
3530
3531 @classmethod
3531 @classmethod
3532 def create(cls, user, repository, permission):
3532 def create(cls, user, repository, permission):
3533 n = cls()
3533 n = cls()
3534 n.user = user
3534 n.user = user
3535 n.repository = repository
3535 n.repository = repository
3536 n.permission = permission
3536 n.permission = permission
3537 Session().add(n)
3537 Session().add(n)
3538 return n
3538 return n
3539
3539
3540 def __repr__(self):
3540 def __repr__(self):
3541 return f'<{self.user} => {self.repository} >'
3541 return f'<{self.user} => {self.repository} >'
3542
3542
3543
3543
3544 class UserUserGroupToPerm(Base, BaseModel):
3544 class UserUserGroupToPerm(Base, BaseModel):
3545 __tablename__ = 'user_user_group_to_perm'
3545 __tablename__ = 'user_user_group_to_perm'
3546 __table_args__ = (
3546 __table_args__ = (
3547 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3547 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3548 base_table_args
3548 base_table_args
3549 )
3549 )
3550
3550
3551 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3551 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3552 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3552 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3553 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3553 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3554 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3554 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3555
3555
3556 user = relationship('User', back_populates='user_group_to_perm')
3556 user = relationship('User', back_populates='user_group_to_perm')
3557 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3557 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3558 permission = relationship('Permission')
3558 permission = relationship('Permission')
3559
3559
3560 @classmethod
3560 @classmethod
3561 def create(cls, user, user_group, permission):
3561 def create(cls, user, user_group, permission):
3562 n = cls()
3562 n = cls()
3563 n.user = user
3563 n.user = user
3564 n.user_group = user_group
3564 n.user_group = user_group
3565 n.permission = permission
3565 n.permission = permission
3566 Session().add(n)
3566 Session().add(n)
3567 return n
3567 return n
3568
3568
3569 def __repr__(self):
3569 def __repr__(self):
3570 return f'<{self.user} => {self.user_group} >'
3570 return f'<{self.user} => {self.user_group} >'
3571
3571
3572
3572
3573 class UserToPerm(Base, BaseModel):
3573 class UserToPerm(Base, BaseModel):
3574 __tablename__ = 'user_to_perm'
3574 __tablename__ = 'user_to_perm'
3575 __table_args__ = (
3575 __table_args__ = (
3576 UniqueConstraint('user_id', 'permission_id'),
3576 UniqueConstraint('user_id', 'permission_id'),
3577 base_table_args
3577 base_table_args
3578 )
3578 )
3579
3579
3580 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3580 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3581 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3581 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3582 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3582 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3583
3583
3584 user = relationship('User', back_populates='user_perms')
3584 user = relationship('User', back_populates='user_perms')
3585 permission = relationship('Permission', lazy='joined')
3585 permission = relationship('Permission', lazy='joined')
3586
3586
3587 def __repr__(self):
3587 def __repr__(self):
3588 return f'<{self.user} => {self.permission} >'
3588 return f'<{self.user} => {self.permission} >'
3589
3589
3590
3590
3591 class UserGroupRepoToPerm(Base, BaseModel):
3591 class UserGroupRepoToPerm(Base, BaseModel):
3592 __tablename__ = 'users_group_repo_to_perm'
3592 __tablename__ = 'users_group_repo_to_perm'
3593 __table_args__ = (
3593 __table_args__ = (
3594 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3594 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3595 base_table_args
3595 base_table_args
3596 )
3596 )
3597
3597
3598 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3598 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3599 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3599 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3600 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3600 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3601 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3601 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3602
3602
3603 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3603 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3604 permission = relationship('Permission')
3604 permission = relationship('Permission')
3605 repository = relationship('Repository', back_populates='users_group_to_perm')
3605 repository = relationship('Repository', back_populates='users_group_to_perm')
3606 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3606 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3607
3607
3608 @classmethod
3608 @classmethod
3609 def create(cls, users_group, repository, permission):
3609 def create(cls, users_group, repository, permission):
3610 n = cls()
3610 n = cls()
3611 n.users_group = users_group
3611 n.users_group = users_group
3612 n.repository = repository
3612 n.repository = repository
3613 n.permission = permission
3613 n.permission = permission
3614 Session().add(n)
3614 Session().add(n)
3615 return n
3615 return n
3616
3616
3617 def __repr__(self):
3617 def __repr__(self):
3618 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3618 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3619
3619
3620
3620
3621 class UserGroupUserGroupToPerm(Base, BaseModel):
3621 class UserGroupUserGroupToPerm(Base, BaseModel):
3622 __tablename__ = 'user_group_user_group_to_perm'
3622 __tablename__ = 'user_group_user_group_to_perm'
3623 __table_args__ = (
3623 __table_args__ = (
3624 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3624 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3625 CheckConstraint('target_user_group_id != user_group_id'),
3625 CheckConstraint('target_user_group_id != user_group_id'),
3626 base_table_args
3626 base_table_args
3627 )
3627 )
3628
3628
3629 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)
3629 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)
3630 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3630 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3631 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3631 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3632 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3632 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3633
3633
3634 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
3634 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
3635 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3635 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3636 permission = relationship('Permission')
3636 permission = relationship('Permission')
3637
3637
3638 @classmethod
3638 @classmethod
3639 def create(cls, target_user_group, user_group, permission):
3639 def create(cls, target_user_group, user_group, permission):
3640 n = cls()
3640 n = cls()
3641 n.target_user_group = target_user_group
3641 n.target_user_group = target_user_group
3642 n.user_group = user_group
3642 n.user_group = user_group
3643 n.permission = permission
3643 n.permission = permission
3644 Session().add(n)
3644 Session().add(n)
3645 return n
3645 return n
3646
3646
3647 def __repr__(self):
3647 def __repr__(self):
3648 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3648 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3649
3649
3650
3650
3651 class UserGroupToPerm(Base, BaseModel):
3651 class UserGroupToPerm(Base, BaseModel):
3652 __tablename__ = 'users_group_to_perm'
3652 __tablename__ = 'users_group_to_perm'
3653 __table_args__ = (
3653 __table_args__ = (
3654 UniqueConstraint('users_group_id', 'permission_id',),
3654 UniqueConstraint('users_group_id', 'permission_id',),
3655 base_table_args
3655 base_table_args
3656 )
3656 )
3657
3657
3658 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3658 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3659 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3659 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3661
3661
3662 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3662 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3663 permission = relationship('Permission')
3663 permission = relationship('Permission')
3664
3664
3665
3665
3666 class UserRepoGroupToPerm(Base, BaseModel):
3666 class UserRepoGroupToPerm(Base, BaseModel):
3667 __tablename__ = 'user_repo_group_to_perm'
3667 __tablename__ = 'user_repo_group_to_perm'
3668 __table_args__ = (
3668 __table_args__ = (
3669 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3669 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3670 base_table_args
3670 base_table_args
3671 )
3671 )
3672
3672
3673 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3673 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3674 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3674 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3675 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3675 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3676 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3676 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3677
3677
3678 user = relationship('User', back_populates='repo_group_to_perm')
3678 user = relationship('User', back_populates='repo_group_to_perm')
3679 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3679 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3680 permission = relationship('Permission')
3680 permission = relationship('Permission')
3681
3681
3682 @classmethod
3682 @classmethod
3683 def create(cls, user, repository_group, permission):
3683 def create(cls, user, repository_group, permission):
3684 n = cls()
3684 n = cls()
3685 n.user = user
3685 n.user = user
3686 n.group = repository_group
3686 n.group = repository_group
3687 n.permission = permission
3687 n.permission = permission
3688 Session().add(n)
3688 Session().add(n)
3689 return n
3689 return n
3690
3690
3691
3691
3692 class UserGroupRepoGroupToPerm(Base, BaseModel):
3692 class UserGroupRepoGroupToPerm(Base, BaseModel):
3693 __tablename__ = 'users_group_repo_group_to_perm'
3693 __tablename__ = 'users_group_repo_group_to_perm'
3694 __table_args__ = (
3694 __table_args__ = (
3695 UniqueConstraint('users_group_id', 'group_id'),
3695 UniqueConstraint('users_group_id', 'group_id'),
3696 base_table_args
3696 base_table_args
3697 )
3697 )
3698
3698
3699 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)
3699 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)
3700 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3700 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3701 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3701 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3702 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3702 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3703
3703
3704 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3704 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3705 permission = relationship('Permission')
3705 permission = relationship('Permission')
3706 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3706 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3707
3707
3708 @classmethod
3708 @classmethod
3709 def create(cls, user_group, repository_group, permission):
3709 def create(cls, user_group, repository_group, permission):
3710 n = cls()
3710 n = cls()
3711 n.users_group = user_group
3711 n.users_group = user_group
3712 n.group = repository_group
3712 n.group = repository_group
3713 n.permission = permission
3713 n.permission = permission
3714 Session().add(n)
3714 Session().add(n)
3715 return n
3715 return n
3716
3716
3717 def __repr__(self):
3717 def __repr__(self):
3718 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3718 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3719
3719
3720
3720
3721 class Statistics(Base, BaseModel):
3721 class Statistics(Base, BaseModel):
3722 __tablename__ = 'statistics'
3722 __tablename__ = 'statistics'
3723 __table_args__ = (
3723 __table_args__ = (
3724 base_table_args
3724 base_table_args
3725 )
3725 )
3726
3726
3727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3732 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3732 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3733
3733
3734 repository = relationship('Repository', single_parent=True, viewonly=True)
3734 repository = relationship('Repository', single_parent=True, viewonly=True)
3735
3735
3736
3736
3737 class UserFollowing(Base, BaseModel):
3737 class UserFollowing(Base, BaseModel):
3738 __tablename__ = 'user_followings'
3738 __tablename__ = 'user_followings'
3739 __table_args__ = (
3739 __table_args__ = (
3740 UniqueConstraint('user_id', 'follows_repository_id'),
3740 UniqueConstraint('user_id', 'follows_repository_id'),
3741 UniqueConstraint('user_id', 'follows_user_id'),
3741 UniqueConstraint('user_id', 'follows_user_id'),
3742 base_table_args
3742 base_table_args
3743 )
3743 )
3744
3744
3745 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3745 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3746 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3746 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3747 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3747 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3748 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3748 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3749 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3749 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3750
3750
3751 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3751 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3752
3752
3753 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3753 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3754 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3754 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3755
3755
3756 @classmethod
3756 @classmethod
3757 def get_repo_followers(cls, repo_id):
3757 def get_repo_followers(cls, repo_id):
3758 return cls.query().filter(cls.follows_repo_id == repo_id)
3758 return cls.query().filter(cls.follows_repo_id == repo_id)
3759
3759
3760
3760
3761 class CacheKey(Base, BaseModel):
3761 class CacheKey(Base, BaseModel):
3762 __tablename__ = 'cache_invalidation'
3762 __tablename__ = 'cache_invalidation'
3763 __table_args__ = (
3763 __table_args__ = (
3764 UniqueConstraint('cache_key'),
3764 UniqueConstraint('cache_key'),
3765 Index('key_idx', 'cache_key'),
3765 Index('key_idx', 'cache_key'),
3766 Index('cache_args_idx', 'cache_args'),
3766 Index('cache_args_idx', 'cache_args'),
3767 base_table_args,
3767 base_table_args,
3768 )
3768 )
3769
3769
3770 CACHE_TYPE_FEED = 'FEED'
3770 CACHE_TYPE_FEED = 'FEED'
3771
3771
3772 # namespaces used to register process/thread aware caches
3772 # namespaces used to register process/thread aware caches
3773 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3773 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3774
3774
3775 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3775 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3776 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3776 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3777 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3777 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3778 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3778 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3779 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3779 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3780
3780
3781 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3781 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3782 self.cache_key = cache_key
3782 self.cache_key = cache_key
3783 self.cache_args = cache_args
3783 self.cache_args = cache_args
3784 self.cache_active = cache_active
3784 self.cache_active = cache_active
3785 # first key should be same for all entries, since all workers should share it
3785 # first key should be same for all entries, since all workers should share it
3786 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3786 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3787
3787
3788 def __repr__(self):
3788 def __repr__(self):
3789 return "<%s('%s:%s[%s]')>" % (
3789 return "<%s('%s:%s[%s]')>" % (
3790 self.cls_name,
3790 self.cls_name,
3791 self.cache_id, self.cache_key, self.cache_active)
3791 self.cache_id, self.cache_key, self.cache_active)
3792
3792
3793 def _cache_key_partition(self):
3793 def _cache_key_partition(self):
3794 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3794 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3795 return prefix, repo_name, suffix
3795 return prefix, repo_name, suffix
3796
3796
3797 def get_prefix(self):
3797 def get_prefix(self):
3798 """
3798 """
3799 Try to extract prefix from existing cache key. The key could consist
3799 Try to extract prefix from existing cache key. The key could consist
3800 of prefix, repo_name, suffix
3800 of prefix, repo_name, suffix
3801 """
3801 """
3802 # this returns prefix, repo_name, suffix
3802 # this returns prefix, repo_name, suffix
3803 return self._cache_key_partition()[0]
3803 return self._cache_key_partition()[0]
3804
3804
3805 def get_suffix(self):
3805 def get_suffix(self):
3806 """
3806 """
3807 get suffix that might have been used in _get_cache_key to
3807 get suffix that might have been used in _get_cache_key to
3808 generate self.cache_key. Only used for informational purposes
3808 generate self.cache_key. Only used for informational purposes
3809 in repo_edit.mako.
3809 in repo_edit.mako.
3810 """
3810 """
3811 # prefix, repo_name, suffix
3811 # prefix, repo_name, suffix
3812 return self._cache_key_partition()[2]
3812 return self._cache_key_partition()[2]
3813
3813
3814 @classmethod
3814 @classmethod
3815 def generate_new_state_uid(cls, based_on=None):
3815 def generate_new_state_uid(cls, based_on=None):
3816 if based_on:
3816 if based_on:
3817 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3817 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3818 else:
3818 else:
3819 return str(uuid.uuid4())
3819 return str(uuid.uuid4())
3820
3820
3821 @classmethod
3821 @classmethod
3822 def delete_all_cache(cls):
3822 def delete_all_cache(cls):
3823 """
3823 """
3824 Delete all cache keys from database.
3824 Delete all cache keys from database.
3825 Should only be run when all instances are down and all entries
3825 Should only be run when all instances are down and all entries
3826 thus stale.
3826 thus stale.
3827 """
3827 """
3828 cls.query().delete()
3828 cls.query().delete()
3829 Session().commit()
3829 Session().commit()
3830
3830
3831 @classmethod
3831 @classmethod
3832 def set_invalidate(cls, cache_uid, delete=False):
3832 def set_invalidate(cls, cache_uid, delete=False):
3833 """
3833 """
3834 Mark all caches of a repo as invalid in the database.
3834 Mark all caches of a repo as invalid in the database.
3835 """
3835 """
3836 try:
3836 try:
3837 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3837 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3838 if delete:
3838 if delete:
3839 qry.delete()
3839 qry.delete()
3840 log.debug('cache objects deleted for cache args %s',
3840 log.debug('cache objects deleted for cache args %s',
3841 safe_str(cache_uid))
3841 safe_str(cache_uid))
3842 else:
3842 else:
3843 new_uid = cls.generate_new_state_uid()
3843 new_uid = cls.generate_new_state_uid()
3844 qry.update({"cache_state_uid": new_uid,
3844 qry.update({"cache_state_uid": new_uid,
3845 "cache_args": f"repo_state:{time.time()}"})
3845 "cache_args": f"repo_state:{time.time()}"})
3846 log.debug('cache object %s set new UID %s',
3846 log.debug('cache object %s set new UID %s',
3847 safe_str(cache_uid), new_uid)
3847 safe_str(cache_uid), new_uid)
3848
3848
3849 Session().commit()
3849 Session().commit()
3850 except Exception:
3850 except Exception:
3851 log.exception(
3851 log.exception(
3852 'Cache key invalidation failed for cache args %s',
3852 'Cache key invalidation failed for cache args %s',
3853 safe_str(cache_uid))
3853 safe_str(cache_uid))
3854 Session().rollback()
3854 Session().rollback()
3855
3855
3856 @classmethod
3856 @classmethod
3857 def get_active_cache(cls, cache_key):
3857 def get_active_cache(cls, cache_key):
3858 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3858 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3859 if inv_obj:
3859 if inv_obj:
3860 return inv_obj
3860 return inv_obj
3861 return None
3861 return None
3862
3862
3863 @classmethod
3863 @classmethod
3864 def get_namespace_map(cls, namespace):
3864 def get_namespace_map(cls, namespace):
3865 return {
3865 return {
3866 x.cache_key: x
3866 x.cache_key: x
3867 for x in cls.query().filter(cls.cache_args == namespace)}
3867 for x in cls.query().filter(cls.cache_args == namespace)}
3868
3868
3869
3869
3870 class ChangesetComment(Base, BaseModel):
3870 class ChangesetComment(Base, BaseModel):
3871 __tablename__ = 'changeset_comments'
3871 __tablename__ = 'changeset_comments'
3872 __table_args__ = (
3872 __table_args__ = (
3873 Index('cc_revision_idx', 'revision'),
3873 Index('cc_revision_idx', 'revision'),
3874 base_table_args,
3874 base_table_args,
3875 )
3875 )
3876
3876
3877 COMMENT_OUTDATED = 'comment_outdated'
3877 COMMENT_OUTDATED = 'comment_outdated'
3878 COMMENT_TYPE_NOTE = 'note'
3878 COMMENT_TYPE_NOTE = 'note'
3879 COMMENT_TYPE_TODO = 'todo'
3879 COMMENT_TYPE_TODO = 'todo'
3880 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3880 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3881
3881
3882 OP_IMMUTABLE = 'immutable'
3882 OP_IMMUTABLE = 'immutable'
3883 OP_CHANGEABLE = 'changeable'
3883 OP_CHANGEABLE = 'changeable'
3884
3884
3885 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3885 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3886 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3886 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3887 revision = Column('revision', String(40), nullable=True)
3887 revision = Column('revision', String(40), nullable=True)
3888 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3888 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3889 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3889 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3890 line_no = Column('line_no', Unicode(10), nullable=True)
3890 line_no = Column('line_no', Unicode(10), nullable=True)
3891 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3891 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3892 f_path = Column('f_path', Unicode(1000), nullable=True)
3892 f_path = Column('f_path', Unicode(1000), nullable=True)
3893 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3893 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3894 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3894 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3895 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3895 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3896 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3896 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3897 renderer = Column('renderer', Unicode(64), nullable=True)
3897 renderer = Column('renderer', Unicode(64), nullable=True)
3898 display_state = Column('display_state', Unicode(128), nullable=True)
3898 display_state = Column('display_state', Unicode(128), nullable=True)
3899 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3899 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3900 draft = Column('draft', Boolean(), nullable=True, default=False)
3900 draft = Column('draft', Boolean(), nullable=True, default=False)
3901
3901
3902 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3902 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3903 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3903 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3904
3904
3905 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3905 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3906 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3906 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3907
3907
3908 author = relationship('User', lazy='select', back_populates='user_comments')
3908 author = relationship('User', lazy='select', back_populates='user_comments')
3909 repo = relationship('Repository', back_populates='comments')
3909 repo = relationship('Repository', back_populates='comments')
3910 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3910 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3911 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3911 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3912 pull_request_version = relationship('PullRequestVersion', lazy='select')
3912 pull_request_version = relationship('PullRequestVersion', lazy='select')
3913 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3913 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3914
3914
3915 @classmethod
3915 @classmethod
3916 def get_users(cls, revision=None, pull_request_id=None):
3916 def get_users(cls, revision=None, pull_request_id=None):
3917 """
3917 """
3918 Returns user associated with this ChangesetComment. ie those
3918 Returns user associated with this ChangesetComment. ie those
3919 who actually commented
3919 who actually commented
3920
3920
3921 :param cls:
3921 :param cls:
3922 :param revision:
3922 :param revision:
3923 """
3923 """
3924 q = Session().query(User).join(ChangesetComment.author)
3924 q = Session().query(User).join(ChangesetComment.author)
3925 if revision:
3925 if revision:
3926 q = q.filter(cls.revision == revision)
3926 q = q.filter(cls.revision == revision)
3927 elif pull_request_id:
3927 elif pull_request_id:
3928 q = q.filter(cls.pull_request_id == pull_request_id)
3928 q = q.filter(cls.pull_request_id == pull_request_id)
3929 return q.all()
3929 return q.all()
3930
3930
3931 @classmethod
3931 @classmethod
3932 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3932 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3933 if pr_version is None:
3933 if pr_version is None:
3934 return 0
3934 return 0
3935
3935
3936 if versions is not None:
3936 if versions is not None:
3937 num_versions = [x.pull_request_version_id for x in versions]
3937 num_versions = [x.pull_request_version_id for x in versions]
3938
3938
3939 num_versions = num_versions or []
3939 num_versions = num_versions or []
3940 try:
3940 try:
3941 return num_versions.index(pr_version) + 1
3941 return num_versions.index(pr_version) + 1
3942 except (IndexError, ValueError):
3942 except (IndexError, ValueError):
3943 return 0
3943 return 0
3944
3944
3945 @property
3945 @property
3946 def outdated(self):
3946 def outdated(self):
3947 return self.display_state == self.COMMENT_OUTDATED
3947 return self.display_state == self.COMMENT_OUTDATED
3948
3948
3949 @property
3949 @property
3950 def outdated_js(self):
3950 def outdated_js(self):
3951 return str_json(self.display_state == self.COMMENT_OUTDATED)
3951 return str_json(self.display_state == self.COMMENT_OUTDATED)
3952
3952
3953 @property
3953 @property
3954 def immutable(self):
3954 def immutable(self):
3955 return self.immutable_state == self.OP_IMMUTABLE
3955 return self.immutable_state == self.OP_IMMUTABLE
3956
3956
3957 def outdated_at_version(self, version: int) -> bool:
3957 def outdated_at_version(self, version: int) -> bool:
3958 """
3958 """
3959 Checks if comment is outdated for given pull request version
3959 Checks if comment is outdated for given pull request version
3960 """
3960 """
3961
3961
3962 def version_check():
3962 def version_check():
3963 return self.pull_request_version_id and self.pull_request_version_id != version
3963 return self.pull_request_version_id and self.pull_request_version_id != version
3964
3964
3965 if self.is_inline:
3965 if self.is_inline:
3966 return self.outdated and version_check()
3966 return self.outdated and version_check()
3967 else:
3967 else:
3968 # general comments don't have .outdated set, also latest don't have a version
3968 # general comments don't have .outdated set, also latest don't have a version
3969 return version_check()
3969 return version_check()
3970
3970
3971 def outdated_at_version_js(self, version):
3971 def outdated_at_version_js(self, version):
3972 """
3972 """
3973 Checks if comment is outdated for given pull request version
3973 Checks if comment is outdated for given pull request version
3974 """
3974 """
3975 return str_json(self.outdated_at_version(version))
3975 return str_json(self.outdated_at_version(version))
3976
3976
3977 def older_than_version(self, version: int) -> bool:
3977 def older_than_version(self, version: int) -> bool:
3978 """
3978 """
3979 Checks if comment is made from a previous version than given.
3979 Checks if comment is made from a previous version than given.
3980 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
3980 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
3981 """
3981 """
3982
3982
3983 # If version is None, return False as the current version cannot be less than None
3983 # If version is None, return False as the current version cannot be less than None
3984 if version is None:
3984 if version is None:
3985 return False
3985 return False
3986
3986
3987 # Ensure that the version is an integer to prevent TypeError on comparison
3987 # Ensure that the version is an integer to prevent TypeError on comparison
3988 if not isinstance(version, int):
3988 if not isinstance(version, int):
3989 raise ValueError("The provided version must be an integer.")
3989 raise ValueError("The provided version must be an integer.")
3990
3990
3991 # Initialize current version to 0 or pull_request_version_id if it's available
3991 # Initialize current version to 0 or pull_request_version_id if it's available
3992 cur_ver = 0
3992 cur_ver = 0
3993 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
3993 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
3994 cur_ver = self.pull_request_version.pull_request_version_id
3994 cur_ver = self.pull_request_version.pull_request_version_id
3995
3995
3996 # Return True if the current version is less than the given version
3996 # Return True if the current version is less than the given version
3997 return cur_ver < version
3997 return cur_ver < version
3998
3998
3999 def older_than_version_js(self, version):
3999 def older_than_version_js(self, version):
4000 """
4000 """
4001 Checks if comment is made from previous version than given
4001 Checks if comment is made from previous version than given
4002 """
4002 """
4003 return str_json(self.older_than_version(version))
4003 return str_json(self.older_than_version(version))
4004
4004
4005 @property
4005 @property
4006 def commit_id(self):
4006 def commit_id(self):
4007 """New style naming to stop using .revision"""
4007 """New style naming to stop using .revision"""
4008 return self.revision
4008 return self.revision
4009
4009
4010 @property
4010 @property
4011 def resolved(self):
4011 def resolved(self):
4012 return self.resolved_by[0] if self.resolved_by else None
4012 return self.resolved_by[0] if self.resolved_by else None
4013
4013
4014 @property
4014 @property
4015 def is_todo(self):
4015 def is_todo(self):
4016 return self.comment_type == self.COMMENT_TYPE_TODO
4016 return self.comment_type == self.COMMENT_TYPE_TODO
4017
4017
4018 @property
4018 @property
4019 def is_inline(self):
4019 def is_inline(self):
4020 if self.line_no and self.f_path:
4020 if self.line_no and self.f_path:
4021 return True
4021 return True
4022 return False
4022 return False
4023
4023
4024 @property
4024 @property
4025 def last_version(self):
4025 def last_version(self):
4026 version = 0
4026 version = 0
4027 if self.history:
4027 if self.history:
4028 version = self.history[-1].version
4028 version = self.history[-1].version
4029 return version
4029 return version
4030
4030
4031 def get_index_version(self, versions):
4031 def get_index_version(self, versions):
4032 return self.get_index_from_version(
4032 return self.get_index_from_version(
4033 self.pull_request_version_id, versions)
4033 self.pull_request_version_id, versions)
4034
4034
4035 @property
4035 @property
4036 def review_status(self):
4036 def review_status(self):
4037 if self.status_change:
4037 if self.status_change:
4038 return self.status_change[0].status
4038 return self.status_change[0].status
4039
4039
4040 @property
4040 @property
4041 def review_status_lbl(self):
4041 def review_status_lbl(self):
4042 if self.status_change:
4042 if self.status_change:
4043 return self.status_change[0].status_lbl
4043 return self.status_change[0].status_lbl
4044
4044
4045 def __repr__(self):
4045 def __repr__(self):
4046 if self.comment_id:
4046 if self.comment_id:
4047 return f'<DB:Comment #{self.comment_id}>'
4047 return f'<DB:Comment #{self.comment_id}>'
4048 else:
4048 else:
4049 return f'<DB:Comment at {id(self)!r}>'
4049 return f'<DB:Comment at {id(self)!r}>'
4050
4050
4051 def get_api_data(self):
4051 def get_api_data(self):
4052 comment = self
4052 comment = self
4053
4053
4054 data = {
4054 data = {
4055 'comment_id': comment.comment_id,
4055 'comment_id': comment.comment_id,
4056 'comment_type': comment.comment_type,
4056 'comment_type': comment.comment_type,
4057 'comment_text': comment.text,
4057 'comment_text': comment.text,
4058 'comment_status': comment.status_change,
4058 'comment_status': comment.status_change,
4059 'comment_f_path': comment.f_path,
4059 'comment_f_path': comment.f_path,
4060 'comment_lineno': comment.line_no,
4060 'comment_lineno': comment.line_no,
4061 'comment_author': comment.author,
4061 'comment_author': comment.author,
4062 'comment_created_on': comment.created_on,
4062 'comment_created_on': comment.created_on,
4063 'comment_resolved_by': self.resolved,
4063 'comment_resolved_by': self.resolved,
4064 'comment_commit_id': comment.revision,
4064 'comment_commit_id': comment.revision,
4065 'comment_pull_request_id': comment.pull_request_id,
4065 'comment_pull_request_id': comment.pull_request_id,
4066 'comment_last_version': self.last_version
4066 'comment_last_version': self.last_version
4067 }
4067 }
4068 return data
4068 return data
4069
4069
4070 def __json__(self):
4070 def __json__(self):
4071 data = dict()
4071 data = dict()
4072 data.update(self.get_api_data())
4072 data.update(self.get_api_data())
4073 return data
4073 return data
4074
4074
4075
4075
4076 class ChangesetCommentHistory(Base, BaseModel):
4076 class ChangesetCommentHistory(Base, BaseModel):
4077 __tablename__ = 'changeset_comments_history'
4077 __tablename__ = 'changeset_comments_history'
4078 __table_args__ = (
4078 __table_args__ = (
4079 Index('cch_comment_id_idx', 'comment_id'),
4079 Index('cch_comment_id_idx', 'comment_id'),
4080 base_table_args,
4080 base_table_args,
4081 )
4081 )
4082
4082
4083 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
4083 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
4084 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
4084 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
4085 version = Column("version", Integer(), nullable=False, default=0)
4085 version = Column("version", Integer(), nullable=False, default=0)
4086 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
4086 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
4087 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
4087 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
4088 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4088 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4089 deleted = Column('deleted', Boolean(), default=False)
4089 deleted = Column('deleted', Boolean(), default=False)
4090
4090
4091 author = relationship('User', lazy='joined')
4091 author = relationship('User', lazy='joined')
4092 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4092 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4093
4093
4094 @classmethod
4094 @classmethod
4095 def get_version(cls, comment_id):
4095 def get_version(cls, comment_id):
4096 q = Session().query(ChangesetCommentHistory).filter(
4096 q = Session().query(ChangesetCommentHistory).filter(
4097 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4097 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4098 if q.count() == 0:
4098 if q.count() == 0:
4099 return 1
4099 return 1
4100 elif q.count() >= q[0].version:
4100 elif q.count() >= q[0].version:
4101 return q.count() + 1
4101 return q.count() + 1
4102 else:
4102 else:
4103 return q[0].version + 1
4103 return q[0].version + 1
4104
4104
4105
4105
4106 class ChangesetStatus(Base, BaseModel):
4106 class ChangesetStatus(Base, BaseModel):
4107 __tablename__ = 'changeset_statuses'
4107 __tablename__ = 'changeset_statuses'
4108 __table_args__ = (
4108 __table_args__ = (
4109 Index('cs_revision_idx', 'revision'),
4109 Index('cs_revision_idx', 'revision'),
4110 Index('cs_version_idx', 'version'),
4110 Index('cs_version_idx', 'version'),
4111 UniqueConstraint('repo_id', 'revision', 'version'),
4111 UniqueConstraint('repo_id', 'revision', 'version'),
4112 base_table_args
4112 base_table_args
4113 )
4113 )
4114
4114
4115 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4115 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4116 STATUS_APPROVED = 'approved'
4116 STATUS_APPROVED = 'approved'
4117 STATUS_REJECTED = 'rejected'
4117 STATUS_REJECTED = 'rejected'
4118 STATUS_UNDER_REVIEW = 'under_review'
4118 STATUS_UNDER_REVIEW = 'under_review'
4119
4119
4120 STATUSES = [
4120 STATUSES = [
4121 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4121 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4122 (STATUS_APPROVED, _("Approved")),
4122 (STATUS_APPROVED, _("Approved")),
4123 (STATUS_REJECTED, _("Rejected")),
4123 (STATUS_REJECTED, _("Rejected")),
4124 (STATUS_UNDER_REVIEW, _("Under Review")),
4124 (STATUS_UNDER_REVIEW, _("Under Review")),
4125 ]
4125 ]
4126
4126
4127 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4127 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4128 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4128 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4129 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4129 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4130 revision = Column('revision', String(40), nullable=False)
4130 revision = Column('revision', String(40), nullable=False)
4131 status = Column('status', String(128), nullable=False, default=DEFAULT)
4131 status = Column('status', String(128), nullable=False, default=DEFAULT)
4132 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4132 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4133 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4133 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4134 version = Column('version', Integer(), nullable=False, default=0)
4134 version = Column('version', Integer(), nullable=False, default=0)
4135 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4135 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4136
4136
4137 author = relationship('User', lazy='select')
4137 author = relationship('User', lazy='select')
4138 repo = relationship('Repository', lazy='select')
4138 repo = relationship('Repository', lazy='select')
4139 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4139 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4140 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4140 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4141
4141
4142 def __repr__(self):
4142 def __repr__(self):
4143 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4143 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4144
4144
4145 @classmethod
4145 @classmethod
4146 def get_status_lbl(cls, value):
4146 def get_status_lbl(cls, value):
4147 return dict(cls.STATUSES).get(value)
4147 return dict(cls.STATUSES).get(value)
4148
4148
4149 @property
4149 @property
4150 def status_lbl(self):
4150 def status_lbl(self):
4151 return ChangesetStatus.get_status_lbl(self.status)
4151 return ChangesetStatus.get_status_lbl(self.status)
4152
4152
4153 def get_api_data(self):
4153 def get_api_data(self):
4154 status = self
4154 status = self
4155 data = {
4155 data = {
4156 'status_id': status.changeset_status_id,
4156 'status_id': status.changeset_status_id,
4157 'status': status.status,
4157 'status': status.status,
4158 }
4158 }
4159 return data
4159 return data
4160
4160
4161 def __json__(self):
4161 def __json__(self):
4162 data = dict()
4162 data = dict()
4163 data.update(self.get_api_data())
4163 data.update(self.get_api_data())
4164 return data
4164 return data
4165
4165
4166
4166
4167 class _SetState(object):
4167 class _SetState(object):
4168 """
4168 """
4169 Context processor allowing changing state for sensitive operation such as
4169 Context processor allowing changing state for sensitive operation such as
4170 pull request update or merge
4170 pull request update or merge
4171 """
4171 """
4172
4172
4173 def __init__(self, pull_request, pr_state, back_state=None):
4173 def __init__(self, pull_request, pr_state, back_state=None):
4174 self._pr = pull_request
4174 self._pr = pull_request
4175 self._org_state = back_state or pull_request.pull_request_state
4175 self._org_state = back_state or pull_request.pull_request_state
4176 self._pr_state = pr_state
4176 self._pr_state = pr_state
4177 self._current_state = None
4177 self._current_state = None
4178
4178
4179 def __enter__(self):
4179 def __enter__(self):
4180 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4180 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4181 self._pr, self._pr_state)
4181 self._pr, self._pr_state)
4182 self.set_pr_state(self._pr_state)
4182 self.set_pr_state(self._pr_state)
4183 return self
4183 return self
4184
4184
4185 def __exit__(self, exc_type, exc_val, exc_tb):
4185 def __exit__(self, exc_type, exc_val, exc_tb):
4186 if exc_val is not None or exc_type is not None:
4186 if exc_val is not None or exc_type is not None:
4187 log.error(traceback.format_tb(exc_tb))
4187 log.error(traceback.format_tb(exc_tb))
4188 return None
4188 return None
4189
4189
4190 self.set_pr_state(self._org_state)
4190 self.set_pr_state(self._org_state)
4191 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4191 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4192 self._pr, self._org_state)
4192 self._pr, self._org_state)
4193
4193
4194 @property
4194 @property
4195 def state(self):
4195 def state(self):
4196 return self._current_state
4196 return self._current_state
4197
4197
4198 def set_pr_state(self, pr_state):
4198 def set_pr_state(self, pr_state):
4199 try:
4199 try:
4200 self._pr.pull_request_state = pr_state
4200 self._pr.pull_request_state = pr_state
4201 Session().add(self._pr)
4201 Session().add(self._pr)
4202 Session().commit()
4202 Session().commit()
4203 self._current_state = pr_state
4203 self._current_state = pr_state
4204 except Exception:
4204 except Exception:
4205 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4205 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4206 raise
4206 raise
4207
4207
4208
4208
4209 class _PullRequestBase(BaseModel):
4209 class _PullRequestBase(BaseModel):
4210 """
4210 """
4211 Common attributes of pull request and version entries.
4211 Common attributes of pull request and version entries.
4212 """
4212 """
4213
4213
4214 # .status values
4214 # .status values
4215 STATUS_NEW = 'new'
4215 STATUS_NEW = 'new'
4216 STATUS_OPEN = 'open'
4216 STATUS_OPEN = 'open'
4217 STATUS_CLOSED = 'closed'
4217 STATUS_CLOSED = 'closed'
4218
4218
4219 # available states
4219 # available states
4220 STATE_CREATING = 'creating'
4220 STATE_CREATING = 'creating'
4221 STATE_UPDATING = 'updating'
4221 STATE_UPDATING = 'updating'
4222 STATE_MERGING = 'merging'
4222 STATE_MERGING = 'merging'
4223 STATE_CREATED = 'created'
4223 STATE_CREATED = 'created'
4224
4224
4225 title = Column('title', Unicode(255), nullable=True)
4225 title = Column('title', Unicode(255), nullable=True)
4226 description = Column(
4226 description = Column(
4227 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4227 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4228 nullable=True)
4228 nullable=True)
4229 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4229 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4230
4230
4231 # new/open/closed status of pull request (not approve/reject/etc)
4231 # new/open/closed status of pull request (not approve/reject/etc)
4232 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4232 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4233 created_on = Column(
4233 created_on = Column(
4234 'created_on', DateTime(timezone=False), nullable=False,
4234 'created_on', DateTime(timezone=False), nullable=False,
4235 default=datetime.datetime.now)
4235 default=datetime.datetime.now)
4236 updated_on = Column(
4236 updated_on = Column(
4237 'updated_on', DateTime(timezone=False), nullable=False,
4237 'updated_on', DateTime(timezone=False), nullable=False,
4238 default=datetime.datetime.now)
4238 default=datetime.datetime.now)
4239
4239
4240 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4240 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4241
4241
4242 @declared_attr
4242 @declared_attr
4243 def user_id(cls):
4243 def user_id(cls):
4244 return Column(
4244 return Column(
4245 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4245 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4246 unique=None)
4246 unique=None)
4247
4247
4248 # 500 revisions max
4248 # 500 revisions max
4249 _revisions = Column(
4249 _revisions = Column(
4250 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4250 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4251
4251
4252 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4252 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4253
4253
4254 @declared_attr
4254 @declared_attr
4255 def source_repo_id(cls):
4255 def source_repo_id(cls):
4256 # TODO: dan: rename column to source_repo_id
4256 # TODO: dan: rename column to source_repo_id
4257 return Column(
4257 return Column(
4258 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4258 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4259 nullable=False)
4259 nullable=False)
4260
4260
4261 @declared_attr
4261 @declared_attr
4262 def pr_source(cls):
4262 def pr_source(cls):
4263 return relationship(
4263 return relationship(
4264 'Repository',
4264 'Repository',
4265 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4265 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4266 overlaps="pull_requests_source"
4266 overlaps="pull_requests_source"
4267 )
4267 )
4268
4268
4269 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4269 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4270
4270
4271 @hybrid_property
4271 @hybrid_property
4272 def source_ref(self):
4272 def source_ref(self):
4273 return self._source_ref
4273 return self._source_ref
4274
4274
4275 @source_ref.setter
4275 @source_ref.setter
4276 def source_ref(self, val):
4276 def source_ref(self, val):
4277 parts = (val or '').split(':')
4277 parts = (val or '').split(':')
4278 if len(parts) != 3:
4278 if len(parts) != 3:
4279 raise ValueError(
4279 raise ValueError(
4280 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4280 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4281 self._source_ref = safe_str(val)
4281 self._source_ref = safe_str(val)
4282
4282
4283 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4283 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4284
4284
4285 @hybrid_property
4285 @hybrid_property
4286 def target_ref(self):
4286 def target_ref(self):
4287 return self._target_ref
4287 return self._target_ref
4288
4288
4289 @target_ref.setter
4289 @target_ref.setter
4290 def target_ref(self, val):
4290 def target_ref(self, val):
4291 parts = (val or '').split(':')
4291 parts = (val or '').split(':')
4292 if len(parts) != 3:
4292 if len(parts) != 3:
4293 raise ValueError(
4293 raise ValueError(
4294 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4294 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4295 self._target_ref = safe_str(val)
4295 self._target_ref = safe_str(val)
4296
4296
4297 @declared_attr
4297 @declared_attr
4298 def target_repo_id(cls):
4298 def target_repo_id(cls):
4299 # TODO: dan: rename column to target_repo_id
4299 # TODO: dan: rename column to target_repo_id
4300 return Column(
4300 return Column(
4301 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4301 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4302 nullable=False)
4302 nullable=False)
4303
4303
4304 @declared_attr
4304 @declared_attr
4305 def pr_target(cls):
4305 def pr_target(cls):
4306 return relationship(
4306 return relationship(
4307 'Repository',
4307 'Repository',
4308 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4308 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4309 overlaps="pull_requests_target"
4309 overlaps="pull_requests_target"
4310 )
4310 )
4311
4311
4312 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4312 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4313
4313
4314 # TODO: dan: rename column to last_merge_source_rev
4314 # TODO: dan: rename column to last_merge_source_rev
4315 _last_merge_source_rev = Column(
4315 _last_merge_source_rev = Column(
4316 'last_merge_org_rev', String(40), nullable=True)
4316 'last_merge_org_rev', String(40), nullable=True)
4317 # TODO: dan: rename column to last_merge_target_rev
4317 # TODO: dan: rename column to last_merge_target_rev
4318 _last_merge_target_rev = Column(
4318 _last_merge_target_rev = Column(
4319 'last_merge_other_rev', String(40), nullable=True)
4319 'last_merge_other_rev', String(40), nullable=True)
4320 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4320 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4321 last_merge_metadata = Column(
4321 last_merge_metadata = Column(
4322 'last_merge_metadata', MutationObj.as_mutable(
4322 'last_merge_metadata', MutationObj.as_mutable(
4323 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4323 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4324
4324
4325 merge_rev = Column('merge_rev', String(40), nullable=True)
4325 merge_rev = Column('merge_rev', String(40), nullable=True)
4326
4326
4327 reviewer_data = Column(
4327 reviewer_data = Column(
4328 'reviewer_data_json', MutationObj.as_mutable(
4328 'reviewer_data_json', MutationObj.as_mutable(
4329 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4329 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4330
4330
4331 @property
4331 @property
4332 def reviewer_data_json(self):
4332 def reviewer_data_json(self):
4333 return str_json(self.reviewer_data)
4333 return str_json(self.reviewer_data)
4334
4334
4335 @property
4335 @property
4336 def last_merge_metadata_parsed(self):
4336 def last_merge_metadata_parsed(self):
4337 metadata = {}
4337 metadata = {}
4338 if not self.last_merge_metadata:
4338 if not self.last_merge_metadata:
4339 return metadata
4339 return metadata
4340
4340
4341 if hasattr(self.last_merge_metadata, 'de_coerce'):
4341 if hasattr(self.last_merge_metadata, 'de_coerce'):
4342 for k, v in self.last_merge_metadata.de_coerce().items():
4342 for k, v in self.last_merge_metadata.de_coerce().items():
4343 if k in ['target_ref', 'source_ref']:
4343 if k in ['target_ref', 'source_ref']:
4344 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4344 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4345 else:
4345 else:
4346 if hasattr(v, 'de_coerce'):
4346 if hasattr(v, 'de_coerce'):
4347 metadata[k] = v.de_coerce()
4347 metadata[k] = v.de_coerce()
4348 else:
4348 else:
4349 metadata[k] = v
4349 metadata[k] = v
4350 return metadata
4350 return metadata
4351
4351
4352 @property
4352 @property
4353 def work_in_progress(self):
4353 def work_in_progress(self):
4354 """checks if pull request is work in progress by checking the title"""
4354 """checks if pull request is work in progress by checking the title"""
4355 title = self.title.upper()
4355 title = self.title.upper()
4356 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4356 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4357 return True
4357 return True
4358 return False
4358 return False
4359
4359
4360 @property
4360 @property
4361 def title_safe(self):
4361 def title_safe(self):
4362 return self.title\
4362 return self.title\
4363 .replace('{', '{{')\
4363 .replace('{', '{{')\
4364 .replace('}', '}}')
4364 .replace('}', '}}')
4365
4365
4366 @hybrid_property
4366 @hybrid_property
4367 def description_safe(self):
4367 def description_safe(self):
4368 from rhodecode.lib import helpers as h
4368 from rhodecode.lib import helpers as h
4369 return h.escape(self.description)
4369 return h.escape(self.description)
4370
4370
4371 @hybrid_property
4371 @hybrid_property
4372 def revisions(self):
4372 def revisions(self):
4373 return self._revisions.split(':') if self._revisions else []
4373 return self._revisions.split(':') if self._revisions else []
4374
4374
4375 @revisions.setter
4375 @revisions.setter
4376 def revisions(self, val):
4376 def revisions(self, val):
4377 self._revisions = ':'.join(val)
4377 self._revisions = ':'.join(val)
4378
4378
4379 @hybrid_property
4379 @hybrid_property
4380 def last_merge_status(self):
4380 def last_merge_status(self):
4381 return safe_int(self._last_merge_status)
4381 return safe_int(self._last_merge_status)
4382
4382
4383 @last_merge_status.setter
4383 @last_merge_status.setter
4384 def last_merge_status(self, val):
4384 def last_merge_status(self, val):
4385 self._last_merge_status = val
4385 self._last_merge_status = val
4386
4386
4387 @declared_attr
4387 @declared_attr
4388 def author(cls):
4388 def author(cls):
4389 return relationship(
4389 return relationship(
4390 'User', lazy='joined',
4390 'User', lazy='joined',
4391 #TODO, problem that is somehow :?
4391 #TODO, problem that is somehow :?
4392 #back_populates='user_pull_requests'
4392 #back_populates='user_pull_requests'
4393 )
4393 )
4394
4394
4395 @declared_attr
4395 @declared_attr
4396 def source_repo(cls):
4396 def source_repo(cls):
4397 return relationship(
4397 return relationship(
4398 'Repository',
4398 'Repository',
4399 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4399 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4400 overlaps="pr_source"
4400 overlaps="pr_source"
4401 )
4401 )
4402
4402
4403 @property
4403 @property
4404 def source_ref_parts(self):
4404 def source_ref_parts(self):
4405 return self.unicode_to_reference(self.source_ref)
4405 return self.unicode_to_reference(self.source_ref)
4406
4406
4407 @declared_attr
4407 @declared_attr
4408 def target_repo(cls):
4408 def target_repo(cls):
4409 return relationship(
4409 return relationship(
4410 'Repository',
4410 'Repository',
4411 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4411 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4412 overlaps="pr_target"
4412 overlaps="pr_target"
4413 )
4413 )
4414
4414
4415 @property
4415 @property
4416 def target_ref_parts(self):
4416 def target_ref_parts(self):
4417 return self.unicode_to_reference(self.target_ref)
4417 return self.unicode_to_reference(self.target_ref)
4418
4418
4419 @property
4419 @property
4420 def shadow_merge_ref(self):
4420 def shadow_merge_ref(self):
4421 return self.unicode_to_reference(self._shadow_merge_ref)
4421 return self.unicode_to_reference(self._shadow_merge_ref)
4422
4422
4423 @shadow_merge_ref.setter
4423 @shadow_merge_ref.setter
4424 def shadow_merge_ref(self, ref):
4424 def shadow_merge_ref(self, ref):
4425 self._shadow_merge_ref = self.reference_to_unicode(ref)
4425 self._shadow_merge_ref = self.reference_to_unicode(ref)
4426
4426
4427 @staticmethod
4427 @staticmethod
4428 def unicode_to_reference(raw):
4428 def unicode_to_reference(raw):
4429 return unicode_to_reference(raw)
4429 return unicode_to_reference(raw)
4430
4430
4431 @staticmethod
4431 @staticmethod
4432 def reference_to_unicode(ref):
4432 def reference_to_unicode(ref):
4433 return reference_to_unicode(ref)
4433 return reference_to_unicode(ref)
4434
4434
4435 def get_api_data(self, with_merge_state=True):
4435 def get_api_data(self, with_merge_state=True):
4436 from rhodecode.model.pull_request import PullRequestModel
4436 from rhodecode.model.pull_request import PullRequestModel
4437
4437
4438 pull_request = self
4438 pull_request = self
4439 if with_merge_state:
4439 if with_merge_state:
4440 merge_response, merge_status, msg = \
4440 merge_response, merge_status, msg = \
4441 PullRequestModel().merge_status(pull_request)
4441 PullRequestModel().merge_status(pull_request)
4442 merge_state = {
4442 merge_state = {
4443 'status': merge_status,
4443 'status': merge_status,
4444 'message': safe_str(msg),
4444 'message': safe_str(msg),
4445 }
4445 }
4446 else:
4446 else:
4447 merge_state = {'status': 'not_available',
4447 merge_state = {'status': 'not_available',
4448 'message': 'not_available'}
4448 'message': 'not_available'}
4449
4449
4450 merge_data = {
4450 merge_data = {
4451 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4451 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4452 'reference': (
4452 'reference': (
4453 pull_request.shadow_merge_ref.asdict()
4453 pull_request.shadow_merge_ref.asdict()
4454 if pull_request.shadow_merge_ref else None),
4454 if pull_request.shadow_merge_ref else None),
4455 }
4455 }
4456
4456
4457 data = {
4457 data = {
4458 'pull_request_id': pull_request.pull_request_id,
4458 'pull_request_id': pull_request.pull_request_id,
4459 'url': PullRequestModel().get_url(pull_request),
4459 'url': PullRequestModel().get_url(pull_request),
4460 'title': pull_request.title,
4460 'title': pull_request.title,
4461 'description': pull_request.description,
4461 'description': pull_request.description,
4462 'status': pull_request.status,
4462 'status': pull_request.status,
4463 'state': pull_request.pull_request_state,
4463 'state': pull_request.pull_request_state,
4464 'created_on': pull_request.created_on,
4464 'created_on': pull_request.created_on,
4465 'updated_on': pull_request.updated_on,
4465 'updated_on': pull_request.updated_on,
4466 'commit_ids': pull_request.revisions,
4466 'commit_ids': pull_request.revisions,
4467 'review_status': pull_request.calculated_review_status(),
4467 'review_status': pull_request.calculated_review_status(),
4468 'mergeable': merge_state,
4468 'mergeable': merge_state,
4469 'source': {
4469 'source': {
4470 'clone_url': pull_request.source_repo.clone_url(),
4470 'clone_url': pull_request.source_repo.clone_url(),
4471 'repository': pull_request.source_repo.repo_name,
4471 'repository': pull_request.source_repo.repo_name,
4472 'reference': {
4472 'reference': {
4473 'name': pull_request.source_ref_parts.name,
4473 'name': pull_request.source_ref_parts.name,
4474 'type': pull_request.source_ref_parts.type,
4474 'type': pull_request.source_ref_parts.type,
4475 'commit_id': pull_request.source_ref_parts.commit_id,
4475 'commit_id': pull_request.source_ref_parts.commit_id,
4476 },
4476 },
4477 },
4477 },
4478 'target': {
4478 'target': {
4479 'clone_url': pull_request.target_repo.clone_url(),
4479 'clone_url': pull_request.target_repo.clone_url(),
4480 'repository': pull_request.target_repo.repo_name,
4480 'repository': pull_request.target_repo.repo_name,
4481 'reference': {
4481 'reference': {
4482 'name': pull_request.target_ref_parts.name,
4482 'name': pull_request.target_ref_parts.name,
4483 'type': pull_request.target_ref_parts.type,
4483 'type': pull_request.target_ref_parts.type,
4484 'commit_id': pull_request.target_ref_parts.commit_id,
4484 'commit_id': pull_request.target_ref_parts.commit_id,
4485 },
4485 },
4486 },
4486 },
4487 'merge': merge_data,
4487 'merge': merge_data,
4488 'author': pull_request.author.get_api_data(include_secrets=False,
4488 'author': pull_request.author.get_api_data(include_secrets=False,
4489 details='basic'),
4489 details='basic'),
4490 'reviewers': [
4490 'reviewers': [
4491 {
4491 {
4492 'user': reviewer.get_api_data(include_secrets=False,
4492 'user': reviewer.get_api_data(include_secrets=False,
4493 details='basic'),
4493 details='basic'),
4494 'reasons': reasons,
4494 'reasons': reasons,
4495 'review_status': st[0][1].status if st else 'not_reviewed',
4495 'review_status': st[0][1].status if st else 'not_reviewed',
4496 }
4496 }
4497 for obj, reviewer, reasons, mandatory, st in
4497 for obj, reviewer, reasons, mandatory, st in
4498 pull_request.reviewers_statuses()
4498 pull_request.reviewers_statuses()
4499 ]
4499 ]
4500 }
4500 }
4501
4501
4502 return data
4502 return data
4503
4503
4504 def set_state(self, pull_request_state, final_state=None):
4504 def set_state(self, pull_request_state, final_state=None):
4505 """
4505 """
4506 # goes from initial state to updating to initial state.
4506 # goes from initial state to updating to initial state.
4507 # initial state can be changed by specifying back_state=
4507 # initial state can be changed by specifying back_state=
4508 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4508 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4509 pull_request.merge()
4509 pull_request.merge()
4510
4510
4511 :param pull_request_state:
4511 :param pull_request_state:
4512 :param final_state:
4512 :param final_state:
4513
4513
4514 """
4514 """
4515
4515
4516 return _SetState(self, pull_request_state, back_state=final_state)
4516 return _SetState(self, pull_request_state, back_state=final_state)
4517
4517
4518
4518
4519 class PullRequest(Base, _PullRequestBase):
4519 class PullRequest(Base, _PullRequestBase):
4520 __tablename__ = 'pull_requests'
4520 __tablename__ = 'pull_requests'
4521 __table_args__ = (
4521 __table_args__ = (
4522 base_table_args,
4522 base_table_args,
4523 )
4523 )
4524 LATEST_VER = 'latest'
4524 LATEST_VER = 'latest'
4525
4525
4526 pull_request_id = Column(
4526 pull_request_id = Column(
4527 'pull_request_id', Integer(), nullable=False, primary_key=True)
4527 'pull_request_id', Integer(), nullable=False, primary_key=True)
4528
4528
4529 def __repr__(self):
4529 def __repr__(self):
4530 if self.pull_request_id:
4530 if self.pull_request_id:
4531 return f'<DB:PullRequest #{self.pull_request_id}>'
4531 return f'<DB:PullRequest #{self.pull_request_id}>'
4532 else:
4532 else:
4533 return f'<DB:PullRequest at {id(self)!r}>'
4533 return f'<DB:PullRequest at {id(self)!r}>'
4534
4534
4535 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4535 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4536 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4536 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4537 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4537 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4538 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4538 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4539
4539
4540 @classmethod
4540 @classmethod
4541 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4541 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4542 internal_methods=None):
4542 internal_methods=None):
4543
4543
4544 class PullRequestDisplay(object):
4544 class PullRequestDisplay(object):
4545 """
4545 """
4546 Special object wrapper for showing PullRequest data via Versions
4546 Special object wrapper for showing PullRequest data via Versions
4547 It mimics PR object as close as possible. This is read only object
4547 It mimics PR object as close as possible. This is read only object
4548 just for display
4548 just for display
4549 """
4549 """
4550
4550
4551 def __init__(self, attrs, internal=None):
4551 def __init__(self, attrs, internal=None):
4552 self.attrs = attrs
4552 self.attrs = attrs
4553 # internal have priority over the given ones via attrs
4553 # internal have priority over the given ones via attrs
4554 self.internal = internal or ['versions']
4554 self.internal = internal or ['versions']
4555
4555
4556 def __getattr__(self, item):
4556 def __getattr__(self, item):
4557 if item in self.internal:
4557 if item in self.internal:
4558 return getattr(self, item)
4558 return getattr(self, item)
4559 try:
4559 try:
4560 return self.attrs[item]
4560 return self.attrs[item]
4561 except KeyError:
4561 except KeyError:
4562 raise AttributeError(
4562 raise AttributeError(
4563 '%s object has no attribute %s' % (self, item))
4563 '%s object has no attribute %s' % (self, item))
4564
4564
4565 def __repr__(self):
4565 def __repr__(self):
4566 pr_id = self.attrs.get('pull_request_id')
4566 pr_id = self.attrs.get('pull_request_id')
4567 return f'<DB:PullRequestDisplay #{pr_id}>'
4567 return f'<DB:PullRequestDisplay #{pr_id}>'
4568
4568
4569 def versions(self):
4569 def versions(self):
4570 return pull_request_obj.versions.order_by(
4570 return pull_request_obj.versions.order_by(
4571 PullRequestVersion.pull_request_version_id).all()
4571 PullRequestVersion.pull_request_version_id).all()
4572
4572
4573 def is_closed(self):
4573 def is_closed(self):
4574 return pull_request_obj.is_closed()
4574 return pull_request_obj.is_closed()
4575
4575
4576 def is_state_changing(self):
4576 def is_state_changing(self):
4577 return pull_request_obj.is_state_changing()
4577 return pull_request_obj.is_state_changing()
4578
4578
4579 @property
4579 @property
4580 def pull_request_version_id(self):
4580 def pull_request_version_id(self):
4581 return getattr(pull_request_obj, 'pull_request_version_id', None)
4581 return getattr(pull_request_obj, 'pull_request_version_id', None)
4582
4582
4583 @property
4583 @property
4584 def pull_request_last_version(self):
4584 def pull_request_last_version(self):
4585 return pull_request_obj.pull_request_last_version
4585 return pull_request_obj.pull_request_last_version
4586
4586
4587 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4587 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4588
4588
4589 attrs.author = StrictAttributeDict(
4589 attrs.author = StrictAttributeDict(
4590 pull_request_obj.author.get_api_data())
4590 pull_request_obj.author.get_api_data())
4591 if pull_request_obj.target_repo:
4591 if pull_request_obj.target_repo:
4592 attrs.target_repo = StrictAttributeDict(
4592 attrs.target_repo = StrictAttributeDict(
4593 pull_request_obj.target_repo.get_api_data())
4593 pull_request_obj.target_repo.get_api_data())
4594 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4594 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4595
4595
4596 if pull_request_obj.source_repo:
4596 if pull_request_obj.source_repo:
4597 attrs.source_repo = StrictAttributeDict(
4597 attrs.source_repo = StrictAttributeDict(
4598 pull_request_obj.source_repo.get_api_data())
4598 pull_request_obj.source_repo.get_api_data())
4599 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4599 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4600
4600
4601 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4601 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4602 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4602 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4603 attrs.revisions = pull_request_obj.revisions
4603 attrs.revisions = pull_request_obj.revisions
4604 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4604 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4605 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4605 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4606 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4606 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4607 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4607 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4608
4608
4609 return PullRequestDisplay(attrs, internal=internal_methods)
4609 return PullRequestDisplay(attrs, internal=internal_methods)
4610
4610
4611 def is_closed(self):
4611 def is_closed(self):
4612 return self.status == self.STATUS_CLOSED
4612 return self.status == self.STATUS_CLOSED
4613
4613
4614 def is_state_changing(self):
4614 def is_state_changing(self):
4615 return self.pull_request_state != PullRequest.STATE_CREATED
4615 return self.pull_request_state != PullRequest.STATE_CREATED
4616
4616
4617 def __json__(self):
4617 def __json__(self):
4618 return {
4618 return {
4619 'revisions': self.revisions,
4619 'revisions': self.revisions,
4620 'versions': self.versions_count
4620 'versions': self.versions_count
4621 }
4621 }
4622
4622
4623 def calculated_review_status(self):
4623 def calculated_review_status(self):
4624 from rhodecode.model.changeset_status import ChangesetStatusModel
4624 from rhodecode.model.changeset_status import ChangesetStatusModel
4625 return ChangesetStatusModel().calculated_review_status(self)
4625 return ChangesetStatusModel().calculated_review_status(self)
4626
4626
4627 def reviewers_statuses(self, user=None):
4627 def reviewers_statuses(self, user=None):
4628 from rhodecode.model.changeset_status import ChangesetStatusModel
4628 from rhodecode.model.changeset_status import ChangesetStatusModel
4629 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4629 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4630
4630
4631 def get_pull_request_reviewers(self, role=None):
4631 def get_pull_request_reviewers(self, role=None):
4632 qry = PullRequestReviewers.query()\
4632 qry = PullRequestReviewers.query()\
4633 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4633 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4634 if role:
4634 if role:
4635 qry = qry.filter(PullRequestReviewers.role == role)
4635 qry = qry.filter(PullRequestReviewers.role == role)
4636
4636
4637 return qry.all()
4637 return qry.all()
4638
4638
4639 @property
4639 @property
4640 def reviewers_count(self):
4640 def reviewers_count(self):
4641 qry = PullRequestReviewers.query()\
4641 qry = PullRequestReviewers.query()\
4642 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4642 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4643 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4643 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4644 return qry.count()
4644 return qry.count()
4645
4645
4646 @property
4646 @property
4647 def observers_count(self):
4647 def observers_count(self):
4648 qry = PullRequestReviewers.query()\
4648 qry = PullRequestReviewers.query()\
4649 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4649 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4650 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4650 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4651 return qry.count()
4651 return qry.count()
4652
4652
4653 def observers(self):
4653 def observers(self):
4654 qry = PullRequestReviewers.query()\
4654 qry = PullRequestReviewers.query()\
4655 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4655 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4656 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4656 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4657 .all()
4657 .all()
4658
4658
4659 for entry in qry:
4659 for entry in qry:
4660 yield entry, entry.user
4660 yield entry, entry.user
4661
4661
4662 @property
4662 @property
4663 def workspace_id(self):
4663 def workspace_id(self):
4664 from rhodecode.model.pull_request import PullRequestModel
4664 from rhodecode.model.pull_request import PullRequestModel
4665 return PullRequestModel()._workspace_id(self)
4665 return PullRequestModel()._workspace_id(self)
4666
4666
4667 def get_shadow_repo(self):
4667 def get_shadow_repo(self):
4668 workspace_id = self.workspace_id
4668 workspace_id = self.workspace_id
4669 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4669 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4670 if os.path.isdir(shadow_repository_path):
4670 if os.path.isdir(shadow_repository_path):
4671 vcs_obj = self.target_repo.scm_instance()
4671 vcs_obj = self.target_repo.scm_instance()
4672 return vcs_obj.get_shadow_instance(shadow_repository_path)
4672 return vcs_obj.get_shadow_instance(shadow_repository_path)
4673
4673
4674 @property
4674 @property
4675 def versions_count(self):
4675 def versions_count(self):
4676 """
4676 """
4677 return number of versions this PR have, e.g a PR that once been
4677 return number of versions this PR have, e.g a PR that once been
4678 updated will have 2 versions
4678 updated will have 2 versions
4679 """
4679 """
4680 return self.versions.count() + 1
4680 return self.versions.count() + 1
4681
4681
4682 @property
4682 @property
4683 def pull_request_last_version(self):
4683 def pull_request_last_version(self):
4684 return self.versions_count
4684 return self.versions_count
4685
4685
4686
4686
4687 class PullRequestVersion(Base, _PullRequestBase):
4687 class PullRequestVersion(Base, _PullRequestBase):
4688 __tablename__ = 'pull_request_versions'
4688 __tablename__ = 'pull_request_versions'
4689 __table_args__ = (
4689 __table_args__ = (
4690 base_table_args,
4690 base_table_args,
4691 )
4691 )
4692
4692
4693 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4693 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4694 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4694 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4695 pull_request = relationship('PullRequest', back_populates='versions')
4695 pull_request = relationship('PullRequest', back_populates='versions')
4696
4696
4697 def __repr__(self):
4697 def __repr__(self):
4698 if self.pull_request_version_id:
4698 if self.pull_request_version_id:
4699 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4699 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4700 else:
4700 else:
4701 return f'<DB:PullRequestVersion at {id(self)!r}>'
4701 return f'<DB:PullRequestVersion at {id(self)!r}>'
4702
4702
4703 @property
4703 @property
4704 def reviewers(self):
4704 def reviewers(self):
4705 return self.pull_request.reviewers
4705 return self.pull_request.reviewers
4706
4706
4707 @property
4707 @property
4708 def versions(self):
4708 def versions(self):
4709 return self.pull_request.versions
4709 return self.pull_request.versions
4710
4710
4711 def is_closed(self):
4711 def is_closed(self):
4712 # calculate from original
4712 # calculate from original
4713 return self.pull_request.status == self.STATUS_CLOSED
4713 return self.pull_request.status == self.STATUS_CLOSED
4714
4714
4715 def is_state_changing(self):
4715 def is_state_changing(self):
4716 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4716 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4717
4717
4718 def calculated_review_status(self):
4718 def calculated_review_status(self):
4719 return self.pull_request.calculated_review_status()
4719 return self.pull_request.calculated_review_status()
4720
4720
4721 def reviewers_statuses(self):
4721 def reviewers_statuses(self):
4722 return self.pull_request.reviewers_statuses()
4722 return self.pull_request.reviewers_statuses()
4723
4723
4724 def observers(self):
4724 def observers(self):
4725 return self.pull_request.observers()
4725 return self.pull_request.observers()
4726
4726
4727
4727
4728 class PullRequestReviewers(Base, BaseModel):
4728 class PullRequestReviewers(Base, BaseModel):
4729 __tablename__ = 'pull_request_reviewers'
4729 __tablename__ = 'pull_request_reviewers'
4730 __table_args__ = (
4730 __table_args__ = (
4731 base_table_args,
4731 base_table_args,
4732 )
4732 )
4733 ROLE_REVIEWER = 'reviewer'
4733 ROLE_REVIEWER = 'reviewer'
4734 ROLE_OBSERVER = 'observer'
4734 ROLE_OBSERVER = 'observer'
4735 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4735 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4736
4736
4737 @hybrid_property
4737 @hybrid_property
4738 def reasons(self):
4738 def reasons(self):
4739 if not self._reasons:
4739 if not self._reasons:
4740 return []
4740 return []
4741 return self._reasons
4741 return self._reasons
4742
4742
4743 @reasons.setter
4743 @reasons.setter
4744 def reasons(self, val):
4744 def reasons(self, val):
4745 val = val or []
4745 val = val or []
4746 if any(not isinstance(x, str) for x in val):
4746 if any(not isinstance(x, str) for x in val):
4747 raise Exception('invalid reasons type, must be list of strings')
4747 raise Exception('invalid reasons type, must be list of strings')
4748 self._reasons = val
4748 self._reasons = val
4749
4749
4750 pull_requests_reviewers_id = Column(
4750 pull_requests_reviewers_id = Column(
4751 'pull_requests_reviewers_id', Integer(), nullable=False,
4751 'pull_requests_reviewers_id', Integer(), nullable=False,
4752 primary_key=True)
4752 primary_key=True)
4753 pull_request_id = Column(
4753 pull_request_id = Column(
4754 "pull_request_id", Integer(),
4754 "pull_request_id", Integer(),
4755 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4755 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4756 user_id = Column(
4756 user_id = Column(
4757 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4757 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4758 _reasons = Column(
4758 _reasons = Column(
4759 'reason', MutationList.as_mutable(
4759 'reason', MutationList.as_mutable(
4760 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4760 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4761
4761
4762 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4762 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4763 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4763 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4764
4764
4765 user = relationship('User')
4765 user = relationship('User')
4766 pull_request = relationship('PullRequest', back_populates='reviewers')
4766 pull_request = relationship('PullRequest', back_populates='reviewers')
4767
4767
4768 rule_data = Column(
4768 rule_data = Column(
4769 'rule_data_json',
4769 'rule_data_json',
4770 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4770 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4771
4771
4772 def rule_user_group_data(self):
4772 def rule_user_group_data(self):
4773 """
4773 """
4774 Returns the voting user group rule data for this reviewer
4774 Returns the voting user group rule data for this reviewer
4775 """
4775 """
4776
4776
4777 if self.rule_data and 'vote_rule' in self.rule_data:
4777 if self.rule_data and 'vote_rule' in self.rule_data:
4778 user_group_data = {}
4778 user_group_data = {}
4779 if 'rule_user_group_entry_id' in self.rule_data:
4779 if 'rule_user_group_entry_id' in self.rule_data:
4780 # means a group with voting rules !
4780 # means a group with voting rules !
4781 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4781 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4782 user_group_data['name'] = self.rule_data['rule_name']
4782 user_group_data['name'] = self.rule_data['rule_name']
4783 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4783 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4784
4784
4785 return user_group_data
4785 return user_group_data
4786
4786
4787 @classmethod
4787 @classmethod
4788 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4788 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4789 qry = PullRequestReviewers.query()\
4789 qry = PullRequestReviewers.query()\
4790 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4790 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4791 if role:
4791 if role:
4792 qry = qry.filter(PullRequestReviewers.role == role)
4792 qry = qry.filter(PullRequestReviewers.role == role)
4793
4793
4794 return qry.all()
4794 return qry.all()
4795
4795
4796 def __repr__(self):
4796 def __repr__(self):
4797 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4797 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4798
4798
4799
4799
4800 class Notification(Base, BaseModel):
4800 class Notification(Base, BaseModel):
4801 __tablename__ = 'notifications'
4801 __tablename__ = 'notifications'
4802 __table_args__ = (
4802 __table_args__ = (
4803 Index('notification_type_idx', 'type'),
4803 Index('notification_type_idx', 'type'),
4804 base_table_args,
4804 base_table_args,
4805 )
4805 )
4806
4806
4807 TYPE_CHANGESET_COMMENT = 'cs_comment'
4807 TYPE_CHANGESET_COMMENT = 'cs_comment'
4808 TYPE_MESSAGE = 'message'
4808 TYPE_MESSAGE = 'message'
4809 TYPE_MENTION = 'mention'
4809 TYPE_MENTION = 'mention'
4810 TYPE_REGISTRATION = 'registration'
4810 TYPE_REGISTRATION = 'registration'
4811 TYPE_PULL_REQUEST = 'pull_request'
4811 TYPE_PULL_REQUEST = 'pull_request'
4812 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4812 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4813 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4813 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4814
4814
4815 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4815 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4816 subject = Column('subject', Unicode(512), nullable=True)
4816 subject = Column('subject', Unicode(512), nullable=True)
4817 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4817 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4818 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4818 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4819 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4819 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4820 type_ = Column('type', Unicode(255))
4820 type_ = Column('type', Unicode(255))
4821
4821
4822 created_by_user = relationship('User', back_populates='user_created_notifications')
4822 created_by_user = relationship('User', back_populates='user_created_notifications')
4823 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4823 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4824
4824
4825 @property
4825 @property
4826 def recipients(self):
4826 def recipients(self):
4827 return [x.user for x in UserNotification.query()\
4827 return [x.user for x in UserNotification.query()\
4828 .filter(UserNotification.notification == self)\
4828 .filter(UserNotification.notification == self)\
4829 .order_by(UserNotification.user_id.asc()).all()]
4829 .order_by(UserNotification.user_id.asc()).all()]
4830
4830
4831 @classmethod
4831 @classmethod
4832 def create(cls, created_by, subject, body, recipients, type_=None):
4832 def create(cls, created_by, subject, body, recipients, type_=None):
4833 if type_ is None:
4833 if type_ is None:
4834 type_ = Notification.TYPE_MESSAGE
4834 type_ = Notification.TYPE_MESSAGE
4835
4835
4836 notification = cls()
4836 notification = cls()
4837 notification.created_by_user = created_by
4837 notification.created_by_user = created_by
4838 notification.subject = subject
4838 notification.subject = subject
4839 notification.body = body
4839 notification.body = body
4840 notification.type_ = type_
4840 notification.type_ = type_
4841 notification.created_on = datetime.datetime.now()
4841 notification.created_on = datetime.datetime.now()
4842
4842
4843 # For each recipient link the created notification to his account
4843 # For each recipient link the created notification to his account
4844 for u in recipients:
4844 for u in recipients:
4845 assoc = UserNotification()
4845 assoc = UserNotification()
4846 assoc.user_id = u.user_id
4846 assoc.user_id = u.user_id
4847 assoc.notification = notification
4847 assoc.notification = notification
4848
4848
4849 # if created_by is inside recipients mark his notification
4849 # if created_by is inside recipients mark his notification
4850 # as read
4850 # as read
4851 if u.user_id == created_by.user_id:
4851 if u.user_id == created_by.user_id:
4852 assoc.read = True
4852 assoc.read = True
4853 Session().add(assoc)
4853 Session().add(assoc)
4854
4854
4855 Session().add(notification)
4855 Session().add(notification)
4856
4856
4857 return notification
4857 return notification
4858
4858
4859
4859
4860 class UserNotification(Base, BaseModel):
4860 class UserNotification(Base, BaseModel):
4861 __tablename__ = 'user_to_notification'
4861 __tablename__ = 'user_to_notification'
4862 __table_args__ = (
4862 __table_args__ = (
4863 UniqueConstraint('user_id', 'notification_id'),
4863 UniqueConstraint('user_id', 'notification_id'),
4864 base_table_args
4864 base_table_args
4865 )
4865 )
4866
4866
4867 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4867 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4868 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4868 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4869 read = Column('read', Boolean, default=False)
4869 read = Column('read', Boolean, default=False)
4870 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4870 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4871
4871
4872 user = relationship('User', lazy="joined", back_populates='notifications')
4872 user = relationship('User', lazy="joined", back_populates='notifications')
4873 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4873 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4874
4874
4875 def mark_as_read(self):
4875 def mark_as_read(self):
4876 self.read = True
4876 self.read = True
4877 Session().add(self)
4877 Session().add(self)
4878
4878
4879
4879
4880 class UserNotice(Base, BaseModel):
4880 class UserNotice(Base, BaseModel):
4881 __tablename__ = 'user_notices'
4881 __tablename__ = 'user_notices'
4882 __table_args__ = (
4882 __table_args__ = (
4883 base_table_args
4883 base_table_args
4884 )
4884 )
4885
4885
4886 NOTIFICATION_TYPE_MESSAGE = 'message'
4886 NOTIFICATION_TYPE_MESSAGE = 'message'
4887 NOTIFICATION_TYPE_NOTICE = 'notice'
4887 NOTIFICATION_TYPE_NOTICE = 'notice'
4888
4888
4889 NOTIFICATION_LEVEL_INFO = 'info'
4889 NOTIFICATION_LEVEL_INFO = 'info'
4890 NOTIFICATION_LEVEL_WARNING = 'warning'
4890 NOTIFICATION_LEVEL_WARNING = 'warning'
4891 NOTIFICATION_LEVEL_ERROR = 'error'
4891 NOTIFICATION_LEVEL_ERROR = 'error'
4892
4892
4893 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4893 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4894
4894
4895 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4895 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4896 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4896 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4897
4897
4898 notice_read = Column('notice_read', Boolean, default=False)
4898 notice_read = Column('notice_read', Boolean, default=False)
4899
4899
4900 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4900 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4901 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4901 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4902
4902
4903 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4903 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4904 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4904 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4905
4905
4906 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4906 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4907 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4907 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4908
4908
4909 @classmethod
4909 @classmethod
4910 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4910 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4911
4911
4912 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4912 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4913 cls.NOTIFICATION_LEVEL_WARNING,
4913 cls.NOTIFICATION_LEVEL_WARNING,
4914 cls.NOTIFICATION_LEVEL_INFO]:
4914 cls.NOTIFICATION_LEVEL_INFO]:
4915 return
4915 return
4916
4916
4917 from rhodecode.model.user import UserModel
4917 from rhodecode.model.user import UserModel
4918 user = UserModel().get_user(user)
4918 user = UserModel().get_user(user)
4919
4919
4920 new_notice = UserNotice()
4920 new_notice = UserNotice()
4921 if not allow_duplicate:
4921 if not allow_duplicate:
4922 existing_msg = UserNotice().query() \
4922 existing_msg = UserNotice().query() \
4923 .filter(UserNotice.user == user) \
4923 .filter(UserNotice.user == user) \
4924 .filter(UserNotice.notice_body == body) \
4924 .filter(UserNotice.notice_body == body) \
4925 .filter(UserNotice.notice_read == false()) \
4925 .filter(UserNotice.notice_read == false()) \
4926 .scalar()
4926 .scalar()
4927 if existing_msg:
4927 if existing_msg:
4928 log.warning('Ignoring duplicate notice for user %s', user)
4928 log.warning('Ignoring duplicate notice for user %s', user)
4929 return
4929 return
4930
4930
4931 new_notice.user = user
4931 new_notice.user = user
4932 new_notice.notice_subject = subject
4932 new_notice.notice_subject = subject
4933 new_notice.notice_body = body
4933 new_notice.notice_body = body
4934 new_notice.notification_level = notice_level
4934 new_notice.notification_level = notice_level
4935 Session().add(new_notice)
4935 Session().add(new_notice)
4936 Session().commit()
4936 Session().commit()
4937
4937
4938
4938
4939 class Gist(Base, BaseModel):
4939 class Gist(Base, BaseModel):
4940 __tablename__ = 'gists'
4940 __tablename__ = 'gists'
4941 __table_args__ = (
4941 __table_args__ = (
4942 Index('g_gist_access_id_idx', 'gist_access_id'),
4942 Index('g_gist_access_id_idx', 'gist_access_id'),
4943 Index('g_created_on_idx', 'created_on'),
4943 Index('g_created_on_idx', 'created_on'),
4944 base_table_args
4944 base_table_args
4945 )
4945 )
4946
4946
4947 GIST_PUBLIC = 'public'
4947 GIST_PUBLIC = 'public'
4948 GIST_PRIVATE = 'private'
4948 GIST_PRIVATE = 'private'
4949 DEFAULT_FILENAME = 'gistfile1.txt'
4949 DEFAULT_FILENAME = 'gistfile1.txt'
4950
4950
4951 ACL_LEVEL_PUBLIC = 'acl_public'
4951 ACL_LEVEL_PUBLIC = 'acl_public'
4952 ACL_LEVEL_PRIVATE = 'acl_private'
4952 ACL_LEVEL_PRIVATE = 'acl_private'
4953
4953
4954 gist_id = Column('gist_id', Integer(), primary_key=True)
4954 gist_id = Column('gist_id', Integer(), primary_key=True)
4955 gist_access_id = Column('gist_access_id', Unicode(250))
4955 gist_access_id = Column('gist_access_id', Unicode(250))
4956 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4956 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4957 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4957 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4958 gist_expires = Column('gist_expires', Float(53), nullable=False)
4958 gist_expires = Column('gist_expires', Float(53), nullable=False)
4959 gist_type = Column('gist_type', Unicode(128), nullable=False)
4959 gist_type = Column('gist_type', Unicode(128), nullable=False)
4960 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4960 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4961 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4961 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4962 acl_level = Column('acl_level', Unicode(128), nullable=True)
4962 acl_level = Column('acl_level', Unicode(128), nullable=True)
4963
4963
4964 owner = relationship('User', back_populates='user_gists')
4964 owner = relationship('User', back_populates='user_gists')
4965
4965
4966 def __repr__(self):
4966 def __repr__(self):
4967 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
4967 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
4968
4968
4969 @hybrid_property
4969 @hybrid_property
4970 def description_safe(self):
4970 def description_safe(self):
4971 from rhodecode.lib import helpers as h
4971 from rhodecode.lib import helpers as h
4972 return h.escape(self.gist_description)
4972 return h.escape(self.gist_description)
4973
4973
4974 @classmethod
4974 @classmethod
4975 def get_or_404(cls, id_):
4975 def get_or_404(cls, id_):
4976 from pyramid.httpexceptions import HTTPNotFound
4976 from pyramid.httpexceptions import HTTPNotFound
4977
4977
4978 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4978 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4979 if not res:
4979 if not res:
4980 log.debug('WARN: No DB entry with id %s', id_)
4980 log.debug('WARN: No DB entry with id %s', id_)
4981 raise HTTPNotFound()
4981 raise HTTPNotFound()
4982 return res
4982 return res
4983
4983
4984 @classmethod
4984 @classmethod
4985 def get_by_access_id(cls, gist_access_id):
4985 def get_by_access_id(cls, gist_access_id):
4986 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4986 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4987
4987
4988 def gist_url(self):
4988 def gist_url(self):
4989 from rhodecode.model.gist import GistModel
4989 from rhodecode.model.gist import GistModel
4990 return GistModel().get_url(self)
4990 return GistModel().get_url(self)
4991
4991
4992 @classmethod
4992 @classmethod
4993 def base_path(cls):
4993 def base_path(cls):
4994 """
4994 """
4995 Returns base path when all gists are stored
4995 Returns base path when all gists are stored
4996
4996
4997 :param cls:
4997 :param cls:
4998 """
4998 """
4999 from rhodecode.model.gist import GIST_STORE_LOC
4999 from rhodecode.model.gist import GIST_STORE_LOC
5000 q = Session().query(RhodeCodeUi)\
5000 q = Session().query(RhodeCodeUi)\
5001 .filter(RhodeCodeUi.ui_key == URL_SEP)
5001 .filter(RhodeCodeUi.ui_key == URL_SEP)
5002 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
5002 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
5003 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
5003 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
5004
5004
5005 def get_api_data(self):
5005 def get_api_data(self):
5006 """
5006 """
5007 Common function for generating gist related data for API
5007 Common function for generating gist related data for API
5008 """
5008 """
5009 gist = self
5009 gist = self
5010 data = {
5010 data = {
5011 'gist_id': gist.gist_id,
5011 'gist_id': gist.gist_id,
5012 'type': gist.gist_type,
5012 'type': gist.gist_type,
5013 'access_id': gist.gist_access_id,
5013 'access_id': gist.gist_access_id,
5014 'description': gist.gist_description,
5014 'description': gist.gist_description,
5015 'url': gist.gist_url(),
5015 'url': gist.gist_url(),
5016 'expires': gist.gist_expires,
5016 'expires': gist.gist_expires,
5017 'created_on': gist.created_on,
5017 'created_on': gist.created_on,
5018 'modified_at': gist.modified_at,
5018 'modified_at': gist.modified_at,
5019 'content': None,
5019 'content': None,
5020 'acl_level': gist.acl_level,
5020 'acl_level': gist.acl_level,
5021 }
5021 }
5022 return data
5022 return data
5023
5023
5024 def __json__(self):
5024 def __json__(self):
5025 data = dict(
5025 data = dict(
5026 )
5026 )
5027 data.update(self.get_api_data())
5027 data.update(self.get_api_data())
5028 return data
5028 return data
5029 # SCM functions
5029 # SCM functions
5030
5030
5031 def scm_instance(self, **kwargs):
5031 def scm_instance(self, **kwargs):
5032 """
5032 """
5033 Get an instance of VCS Repository
5033 Get an instance of VCS Repository
5034
5034
5035 :param kwargs:
5035 :param kwargs:
5036 """
5036 """
5037 from rhodecode.model.gist import GistModel
5037 from rhodecode.model.gist import GistModel
5038 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
5038 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
5039 return get_vcs_instance(
5039 return get_vcs_instance(
5040 repo_path=safe_str(full_repo_path), create=False,
5040 repo_path=safe_str(full_repo_path), create=False,
5041 _vcs_alias=GistModel.vcs_backend)
5041 _vcs_alias=GistModel.vcs_backend)
5042
5042
5043
5043
5044 class ExternalIdentity(Base, BaseModel):
5044 class ExternalIdentity(Base, BaseModel):
5045 __tablename__ = 'external_identities'
5045 __tablename__ = 'external_identities'
5046 __table_args__ = (
5046 __table_args__ = (
5047 Index('local_user_id_idx', 'local_user_id'),
5047 Index('local_user_id_idx', 'local_user_id'),
5048 Index('external_id_idx', 'external_id'),
5048 Index('external_id_idx', 'external_id'),
5049 base_table_args
5049 base_table_args
5050 )
5050 )
5051
5051
5052 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
5052 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
5053 external_username = Column('external_username', Unicode(1024), default='')
5053 external_username = Column('external_username', Unicode(1024), default='')
5054 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
5054 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
5055 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
5055 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
5056 access_token = Column('access_token', String(1024), default='')
5056 access_token = Column('access_token', String(1024), default='')
5057 alt_token = Column('alt_token', String(1024), default='')
5057 alt_token = Column('alt_token', String(1024), default='')
5058 token_secret = Column('token_secret', String(1024), default='')
5058 token_secret = Column('token_secret', String(1024), default='')
5059
5059
5060 @classmethod
5060 @classmethod
5061 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
5061 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
5062 """
5062 """
5063 Returns ExternalIdentity instance based on search params
5063 Returns ExternalIdentity instance based on search params
5064
5064
5065 :param external_id:
5065 :param external_id:
5066 :param provider_name:
5066 :param provider_name:
5067 :return: ExternalIdentity
5067 :return: ExternalIdentity
5068 """
5068 """
5069 query = cls.query()
5069 query = cls.query()
5070 query = query.filter(cls.external_id == external_id)
5070 query = query.filter(cls.external_id == external_id)
5071 query = query.filter(cls.provider_name == provider_name)
5071 query = query.filter(cls.provider_name == provider_name)
5072 if local_user_id:
5072 if local_user_id:
5073 query = query.filter(cls.local_user_id == local_user_id)
5073 query = query.filter(cls.local_user_id == local_user_id)
5074 return query.first()
5074 return query.first()
5075
5075
5076 @classmethod
5076 @classmethod
5077 def user_by_external_id_and_provider(cls, external_id, provider_name):
5077 def user_by_external_id_and_provider(cls, external_id, provider_name):
5078 """
5078 """
5079 Returns User instance based on search params
5079 Returns User instance based on search params
5080
5080
5081 :param external_id:
5081 :param external_id:
5082 :param provider_name:
5082 :param provider_name:
5083 :return: User
5083 :return: User
5084 """
5084 """
5085 query = User.query()
5085 query = User.query()
5086 query = query.filter(cls.external_id == external_id)
5086 query = query.filter(cls.external_id == external_id)
5087 query = query.filter(cls.provider_name == provider_name)
5087 query = query.filter(cls.provider_name == provider_name)
5088 query = query.filter(User.user_id == cls.local_user_id)
5088 query = query.filter(User.user_id == cls.local_user_id)
5089 return query.first()
5089 return query.first()
5090
5090
5091 @classmethod
5091 @classmethod
5092 def by_local_user_id(cls, local_user_id):
5092 def by_local_user_id(cls, local_user_id):
5093 """
5093 """
5094 Returns all tokens for user
5094 Returns all tokens for user
5095
5095
5096 :param local_user_id:
5096 :param local_user_id:
5097 :return: ExternalIdentity
5097 :return: ExternalIdentity
5098 """
5098 """
5099 query = cls.query()
5099 query = cls.query()
5100 query = query.filter(cls.local_user_id == local_user_id)
5100 query = query.filter(cls.local_user_id == local_user_id)
5101 return query
5101 return query
5102
5102
5103 @classmethod
5103 @classmethod
5104 def load_provider_plugin(cls, plugin_id):
5104 def load_provider_plugin(cls, plugin_id):
5105 from rhodecode.authentication.base import loadplugin
5105 from rhodecode.authentication.base import loadplugin
5106 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5106 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5107 auth_plugin = loadplugin(_plugin_id)
5107 auth_plugin = loadplugin(_plugin_id)
5108 return auth_plugin
5108 return auth_plugin
5109
5109
5110
5110
5111 class Integration(Base, BaseModel):
5111 class Integration(Base, BaseModel):
5112 __tablename__ = 'integrations'
5112 __tablename__ = 'integrations'
5113 __table_args__ = (
5113 __table_args__ = (
5114 base_table_args
5114 base_table_args
5115 )
5115 )
5116
5116
5117 integration_id = Column('integration_id', Integer(), primary_key=True)
5117 integration_id = Column('integration_id', Integer(), primary_key=True)
5118 integration_type = Column('integration_type', String(255))
5118 integration_type = Column('integration_type', String(255))
5119 enabled = Column('enabled', Boolean(), nullable=False)
5119 enabled = Column('enabled', Boolean(), nullable=False)
5120 name = Column('name', String(255), nullable=False)
5120 name = Column('name', String(255), nullable=False)
5121 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5121 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5122
5122
5123 settings = Column(
5123 settings = Column(
5124 'settings_json', MutationObj.as_mutable(
5124 'settings_json', MutationObj.as_mutable(
5125 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5125 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5126 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5126 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5127 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5127 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5128
5128
5129 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5129 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5130 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5130 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5131
5131
5132 @property
5132 @property
5133 def scope(self):
5133 def scope(self):
5134 if self.repo:
5134 if self.repo:
5135 return repr(self.repo)
5135 return repr(self.repo)
5136 if self.repo_group:
5136 if self.repo_group:
5137 if self.child_repos_only:
5137 if self.child_repos_only:
5138 return repr(self.repo_group) + ' (child repos only)'
5138 return repr(self.repo_group) + ' (child repos only)'
5139 else:
5139 else:
5140 return repr(self.repo_group) + ' (recursive)'
5140 return repr(self.repo_group) + ' (recursive)'
5141 if self.child_repos_only:
5141 if self.child_repos_only:
5142 return 'root_repos'
5142 return 'root_repos'
5143 return 'global'
5143 return 'global'
5144
5144
5145 def __repr__(self):
5145 def __repr__(self):
5146 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5146 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5147
5147
5148
5148
5149 class RepoReviewRuleUser(Base, BaseModel):
5149 class RepoReviewRuleUser(Base, BaseModel):
5150 __tablename__ = 'repo_review_rules_users'
5150 __tablename__ = 'repo_review_rules_users'
5151 __table_args__ = (
5151 __table_args__ = (
5152 base_table_args
5152 base_table_args
5153 )
5153 )
5154 ROLE_REVIEWER = 'reviewer'
5154 ROLE_REVIEWER = 'reviewer'
5155 ROLE_OBSERVER = 'observer'
5155 ROLE_OBSERVER = 'observer'
5156 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5156 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5157
5157
5158 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5158 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5159 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5159 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5160 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5160 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5161 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5161 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5162 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5162 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5163 user = relationship('User', back_populates='user_review_rules')
5163 user = relationship('User', back_populates='user_review_rules')
5164
5164
5165 def rule_data(self):
5165 def rule_data(self):
5166 return {
5166 return {
5167 'mandatory': self.mandatory,
5167 'mandatory': self.mandatory,
5168 'role': self.role,
5168 'role': self.role,
5169 }
5169 }
5170
5170
5171
5171
5172 class RepoReviewRuleUserGroup(Base, BaseModel):
5172 class RepoReviewRuleUserGroup(Base, BaseModel):
5173 __tablename__ = 'repo_review_rules_users_groups'
5173 __tablename__ = 'repo_review_rules_users_groups'
5174 __table_args__ = (
5174 __table_args__ = (
5175 base_table_args
5175 base_table_args
5176 )
5176 )
5177
5177
5178 VOTE_RULE_ALL = -1
5178 VOTE_RULE_ALL = -1
5179 ROLE_REVIEWER = 'reviewer'
5179 ROLE_REVIEWER = 'reviewer'
5180 ROLE_OBSERVER = 'observer'
5180 ROLE_OBSERVER = 'observer'
5181 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5181 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5182
5182
5183 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5183 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5184 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5184 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5185 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5185 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5186 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5186 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5187 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5187 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5188 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5188 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5189 users_group = relationship('UserGroup')
5189 users_group = relationship('UserGroup')
5190
5190
5191 def rule_data(self):
5191 def rule_data(self):
5192 return {
5192 return {
5193 'mandatory': self.mandatory,
5193 'mandatory': self.mandatory,
5194 'role': self.role,
5194 'role': self.role,
5195 'vote_rule': self.vote_rule
5195 'vote_rule': self.vote_rule
5196 }
5196 }
5197
5197
5198 @property
5198 @property
5199 def vote_rule_label(self):
5199 def vote_rule_label(self):
5200 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5200 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5201 return 'all must vote'
5201 return 'all must vote'
5202 else:
5202 else:
5203 return 'min. vote {}'.format(self.vote_rule)
5203 return 'min. vote {}'.format(self.vote_rule)
5204
5204
5205
5205
5206 class RepoReviewRule(Base, BaseModel):
5206 class RepoReviewRule(Base, BaseModel):
5207 __tablename__ = 'repo_review_rules'
5207 __tablename__ = 'repo_review_rules'
5208 __table_args__ = (
5208 __table_args__ = (
5209 base_table_args
5209 base_table_args
5210 )
5210 )
5211
5211
5212 repo_review_rule_id = Column(
5212 repo_review_rule_id = Column(
5213 'repo_review_rule_id', Integer(), primary_key=True)
5213 'repo_review_rule_id', Integer(), primary_key=True)
5214 repo_id = Column(
5214 repo_id = Column(
5215 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5215 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5216 repo = relationship('Repository', back_populates='review_rules')
5216 repo = relationship('Repository', back_populates='review_rules')
5217
5217
5218 review_rule_name = Column('review_rule_name', String(255))
5218 review_rule_name = Column('review_rule_name', String(255))
5219 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5219 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5220 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5220 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5221 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5221 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5222
5222
5223 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5223 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5224
5224
5225 # Legacy fields, just for backward compat
5225 # Legacy fields, just for backward compat
5226 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5226 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5227 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5227 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5228
5228
5229 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5229 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5230 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5230 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5231
5231
5232 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5232 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5233
5233
5234 rule_users = relationship('RepoReviewRuleUser')
5234 rule_users = relationship('RepoReviewRuleUser')
5235 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5235 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5236
5236
5237 def _validate_pattern(self, value):
5237 def _validate_pattern(self, value):
5238 re.compile('^' + glob2re(value) + '$')
5238 re.compile('^' + glob2re(value) + '$')
5239
5239
5240 @hybrid_property
5240 @hybrid_property
5241 def source_branch_pattern(self):
5241 def source_branch_pattern(self):
5242 return self._branch_pattern or '*'
5242 return self._branch_pattern or '*'
5243
5243
5244 @source_branch_pattern.setter
5244 @source_branch_pattern.setter
5245 def source_branch_pattern(self, value):
5245 def source_branch_pattern(self, value):
5246 self._validate_pattern(value)
5246 self._validate_pattern(value)
5247 self._branch_pattern = value or '*'
5247 self._branch_pattern = value or '*'
5248
5248
5249 @hybrid_property
5249 @hybrid_property
5250 def target_branch_pattern(self):
5250 def target_branch_pattern(self):
5251 return self._target_branch_pattern or '*'
5251 return self._target_branch_pattern or '*'
5252
5252
5253 @target_branch_pattern.setter
5253 @target_branch_pattern.setter
5254 def target_branch_pattern(self, value):
5254 def target_branch_pattern(self, value):
5255 self._validate_pattern(value)
5255 self._validate_pattern(value)
5256 self._target_branch_pattern = value or '*'
5256 self._target_branch_pattern = value or '*'
5257
5257
5258 @hybrid_property
5258 @hybrid_property
5259 def file_pattern(self):
5259 def file_pattern(self):
5260 return self._file_pattern or '*'
5260 return self._file_pattern or '*'
5261
5261
5262 @file_pattern.setter
5262 @file_pattern.setter
5263 def file_pattern(self, value):
5263 def file_pattern(self, value):
5264 self._validate_pattern(value)
5264 self._validate_pattern(value)
5265 self._file_pattern = value or '*'
5265 self._file_pattern = value or '*'
5266
5266
5267 @hybrid_property
5267 @hybrid_property
5268 def forbid_pr_author_to_review(self):
5268 def forbid_pr_author_to_review(self):
5269 return self.pr_author == 'forbid_pr_author'
5269 return self.pr_author == 'forbid_pr_author'
5270
5270
5271 @hybrid_property
5271 @hybrid_property
5272 def include_pr_author_to_review(self):
5272 def include_pr_author_to_review(self):
5273 return self.pr_author == 'include_pr_author'
5273 return self.pr_author == 'include_pr_author'
5274
5274
5275 @hybrid_property
5275 @hybrid_property
5276 def forbid_commit_author_to_review(self):
5276 def forbid_commit_author_to_review(self):
5277 return self.commit_author == 'forbid_commit_author'
5277 return self.commit_author == 'forbid_commit_author'
5278
5278
5279 @hybrid_property
5279 @hybrid_property
5280 def include_commit_author_to_review(self):
5280 def include_commit_author_to_review(self):
5281 return self.commit_author == 'include_commit_author'
5281 return self.commit_author == 'include_commit_author'
5282
5282
5283 def matches(self, source_branch, target_branch, files_changed):
5283 def matches(self, source_branch, target_branch, files_changed):
5284 """
5284 """
5285 Check if this review rule matches a branch/files in a pull request
5285 Check if this review rule matches a branch/files in a pull request
5286
5286
5287 :param source_branch: source branch name for the commit
5287 :param source_branch: source branch name for the commit
5288 :param target_branch: target branch name for the commit
5288 :param target_branch: target branch name for the commit
5289 :param files_changed: list of file paths changed in the pull request
5289 :param files_changed: list of file paths changed in the pull request
5290 """
5290 """
5291
5291
5292 source_branch = source_branch or ''
5292 source_branch = source_branch or ''
5293 target_branch = target_branch or ''
5293 target_branch = target_branch or ''
5294 files_changed = files_changed or []
5294 files_changed = files_changed or []
5295
5295
5296 branch_matches = True
5296 branch_matches = True
5297 if source_branch or target_branch:
5297 if source_branch or target_branch:
5298 if self.source_branch_pattern == '*':
5298 if self.source_branch_pattern == '*':
5299 source_branch_match = True
5299 source_branch_match = True
5300 else:
5300 else:
5301 if self.source_branch_pattern.startswith('re:'):
5301 if self.source_branch_pattern.startswith('re:'):
5302 source_pattern = self.source_branch_pattern[3:]
5302 source_pattern = self.source_branch_pattern[3:]
5303 else:
5303 else:
5304 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5304 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5305 source_branch_regex = re.compile(source_pattern)
5305 source_branch_regex = re.compile(source_pattern)
5306 source_branch_match = bool(source_branch_regex.search(source_branch))
5306 source_branch_match = bool(source_branch_regex.search(source_branch))
5307 if self.target_branch_pattern == '*':
5307 if self.target_branch_pattern == '*':
5308 target_branch_match = True
5308 target_branch_match = True
5309 else:
5309 else:
5310 if self.target_branch_pattern.startswith('re:'):
5310 if self.target_branch_pattern.startswith('re:'):
5311 target_pattern = self.target_branch_pattern[3:]
5311 target_pattern = self.target_branch_pattern[3:]
5312 else:
5312 else:
5313 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5313 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5314 target_branch_regex = re.compile(target_pattern)
5314 target_branch_regex = re.compile(target_pattern)
5315 target_branch_match = bool(target_branch_regex.search(target_branch))
5315 target_branch_match = bool(target_branch_regex.search(target_branch))
5316
5316
5317 branch_matches = source_branch_match and target_branch_match
5317 branch_matches = source_branch_match and target_branch_match
5318
5318
5319 files_matches = True
5319 files_matches = True
5320 if self.file_pattern != '*':
5320 if self.file_pattern != '*':
5321 files_matches = False
5321 files_matches = False
5322 if self.file_pattern.startswith('re:'):
5322 if self.file_pattern.startswith('re:'):
5323 file_pattern = self.file_pattern[3:]
5323 file_pattern = self.file_pattern[3:]
5324 else:
5324 else:
5325 file_pattern = glob2re(self.file_pattern)
5325 file_pattern = glob2re(self.file_pattern)
5326 file_regex = re.compile(file_pattern)
5326 file_regex = re.compile(file_pattern)
5327 for file_data in files_changed:
5327 for file_data in files_changed:
5328 filename = file_data.get('filename')
5328 filename = file_data.get('filename')
5329
5329
5330 if file_regex.search(filename):
5330 if file_regex.search(filename):
5331 files_matches = True
5331 files_matches = True
5332 break
5332 break
5333
5333
5334 return branch_matches and files_matches
5334 return branch_matches and files_matches
5335
5335
5336 @property
5336 @property
5337 def review_users(self):
5337 def review_users(self):
5338 """ Returns the users which this rule applies to """
5338 """ Returns the users which this rule applies to """
5339
5339
5340 users = collections.OrderedDict()
5340 users = collections.OrderedDict()
5341
5341
5342 for rule_user in self.rule_users:
5342 for rule_user in self.rule_users:
5343 if rule_user.user.active:
5343 if rule_user.user.active:
5344 if rule_user.user not in users:
5344 if rule_user.user not in users:
5345 users[rule_user.user.username] = {
5345 users[rule_user.user.username] = {
5346 'user': rule_user.user,
5346 'user': rule_user.user,
5347 'source': 'user',
5347 'source': 'user',
5348 'source_data': {},
5348 'source_data': {},
5349 'data': rule_user.rule_data()
5349 'data': rule_user.rule_data()
5350 }
5350 }
5351
5351
5352 for rule_user_group in self.rule_user_groups:
5352 for rule_user_group in self.rule_user_groups:
5353 source_data = {
5353 source_data = {
5354 'user_group_id': rule_user_group.users_group.users_group_id,
5354 'user_group_id': rule_user_group.users_group.users_group_id,
5355 'name': rule_user_group.users_group.users_group_name,
5355 'name': rule_user_group.users_group.users_group_name,
5356 'members': len(rule_user_group.users_group.members)
5356 'members': len(rule_user_group.users_group.members)
5357 }
5357 }
5358 for member in rule_user_group.users_group.members:
5358 for member in rule_user_group.users_group.members:
5359 if member.user.active:
5359 if member.user.active:
5360 key = member.user.username
5360 key = member.user.username
5361 if key in users:
5361 if key in users:
5362 # skip this member as we have him already
5362 # skip this member as we have him already
5363 # this prevents from override the "first" matched
5363 # this prevents from override the "first" matched
5364 # users with duplicates in multiple groups
5364 # users with duplicates in multiple groups
5365 continue
5365 continue
5366
5366
5367 users[key] = {
5367 users[key] = {
5368 'user': member.user,
5368 'user': member.user,
5369 'source': 'user_group',
5369 'source': 'user_group',
5370 'source_data': source_data,
5370 'source_data': source_data,
5371 'data': rule_user_group.rule_data()
5371 'data': rule_user_group.rule_data()
5372 }
5372 }
5373
5373
5374 return users
5374 return users
5375
5375
5376 def user_group_vote_rule(self, user_id):
5376 def user_group_vote_rule(self, user_id):
5377
5377
5378 rules = []
5378 rules = []
5379 if not self.rule_user_groups:
5379 if not self.rule_user_groups:
5380 return rules
5380 return rules
5381
5381
5382 for user_group in self.rule_user_groups:
5382 for user_group in self.rule_user_groups:
5383 user_group_members = [x.user_id for x in user_group.users_group.members]
5383 user_group_members = [x.user_id for x in user_group.users_group.members]
5384 if user_id in user_group_members:
5384 if user_id in user_group_members:
5385 rules.append(user_group)
5385 rules.append(user_group)
5386 return rules
5386 return rules
5387
5387
5388 def __repr__(self):
5388 def __repr__(self):
5389 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5389 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5390
5390
5391
5391
5392 class ScheduleEntry(Base, BaseModel):
5392 class ScheduleEntry(Base, BaseModel):
5393 __tablename__ = 'schedule_entries'
5393 __tablename__ = 'schedule_entries'
5394 __table_args__ = (
5394 __table_args__ = (
5395 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5395 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5396 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5396 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5397 base_table_args,
5397 base_table_args,
5398 )
5398 )
5399 SCHEDULE_TYPE_INTEGER = "integer"
5399 SCHEDULE_TYPE_INTEGER = "integer"
5400 SCHEDULE_TYPE_CRONTAB = "crontab"
5400 SCHEDULE_TYPE_CRONTAB = "crontab"
5401
5401
5402 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5402 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5403 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5403 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5404
5404
5405 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5405 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5406 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5406 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5407 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5407 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5408
5408
5409 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5409 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5410 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5410 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5411
5411
5412 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5412 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5413 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5413 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5414
5414
5415 # task
5415 # task
5416 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5416 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5417 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5417 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5418 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5418 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5419 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5419 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5420
5420
5421 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5421 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5422 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5422 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5423
5423
5424 @hybrid_property
5424 @hybrid_property
5425 def schedule_type(self):
5425 def schedule_type(self):
5426 return self._schedule_type
5426 return self._schedule_type
5427
5427
5428 @schedule_type.setter
5428 @schedule_type.setter
5429 def schedule_type(self, val):
5429 def schedule_type(self, val):
5430 if val not in self.schedule_types:
5430 if val not in self.schedule_types:
5431 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5431 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5432 val, self.schedule_type))
5432 val, self.schedule_type))
5433
5433
5434 self._schedule_type = val
5434 self._schedule_type = val
5435
5435
5436 @classmethod
5436 @classmethod
5437 def get_uid(cls, obj):
5437 def get_uid(cls, obj):
5438 args = obj.task_args
5438 args = obj.task_args
5439 kwargs = obj.task_kwargs
5439 kwargs = obj.task_kwargs
5440 if isinstance(args, JsonRaw):
5440 if isinstance(args, JsonRaw):
5441 try:
5441 try:
5442 args = json.loads(args)
5442 args = json.loads(args)
5443 except ValueError:
5443 except ValueError:
5444 args = tuple()
5444 args = tuple()
5445
5445
5446 if isinstance(kwargs, JsonRaw):
5446 if isinstance(kwargs, JsonRaw):
5447 try:
5447 try:
5448 kwargs = json.loads(kwargs)
5448 kwargs = json.loads(kwargs)
5449 except ValueError:
5449 except ValueError:
5450 kwargs = dict()
5450 kwargs = dict()
5451
5451
5452 dot_notation = obj.task_dot_notation
5452 dot_notation = obj.task_dot_notation
5453 val = '.'.join(map(safe_str, [
5453 val = '.'.join(map(safe_str, [
5454 sorted(dot_notation), args, sorted(kwargs.items())]))
5454 sorted(dot_notation), args, sorted(kwargs.items())]))
5455 return sha1(safe_bytes(val))
5455 return sha1(safe_bytes(val))
5456
5456
5457 @classmethod
5457 @classmethod
5458 def get_by_schedule_name(cls, schedule_name):
5458 def get_by_schedule_name(cls, schedule_name):
5459 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5459 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5460
5460
5461 @classmethod
5461 @classmethod
5462 def get_by_schedule_id(cls, schedule_id):
5462 def get_by_schedule_id(cls, schedule_id):
5463 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5463 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5464
5464
5465 @property
5465 @property
5466 def task(self):
5466 def task(self):
5467 return self.task_dot_notation
5467 return self.task_dot_notation
5468
5468
5469 @property
5469 @property
5470 def schedule(self):
5470 def schedule(self):
5471 from rhodecode.lib.celerylib.utils import raw_2_schedule
5471 from rhodecode.lib.celerylib.utils import raw_2_schedule
5472 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5472 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5473 return schedule
5473 return schedule
5474
5474
5475 @property
5475 @property
5476 def args(self):
5476 def args(self):
5477 try:
5477 try:
5478 return list(self.task_args or [])
5478 return list(self.task_args or [])
5479 except ValueError:
5479 except ValueError:
5480 return list()
5480 return list()
5481
5481
5482 @property
5482 @property
5483 def kwargs(self):
5483 def kwargs(self):
5484 try:
5484 try:
5485 return dict(self.task_kwargs or {})
5485 return dict(self.task_kwargs or {})
5486 except ValueError:
5486 except ValueError:
5487 return dict()
5487 return dict()
5488
5488
5489 def _as_raw(self, val, indent=False):
5489 def _as_raw(self, val, indent=False):
5490 if hasattr(val, 'de_coerce'):
5490 if hasattr(val, 'de_coerce'):
5491 val = val.de_coerce()
5491 val = val.de_coerce()
5492 if val:
5492 if val:
5493 if indent:
5493 if indent:
5494 val = ext_json.formatted_str_json(val)
5494 val = ext_json.formatted_str_json(val)
5495 else:
5495 else:
5496 val = ext_json.str_json(val)
5496 val = ext_json.str_json(val)
5497
5497
5498 return val
5498 return val
5499
5499
5500 @property
5500 @property
5501 def schedule_definition_raw(self):
5501 def schedule_definition_raw(self):
5502 return self._as_raw(self.schedule_definition)
5502 return self._as_raw(self.schedule_definition)
5503
5503
5504 def args_raw(self, indent=False):
5504 def args_raw(self, indent=False):
5505 return self._as_raw(self.task_args, indent)
5505 return self._as_raw(self.task_args, indent)
5506
5506
5507 def kwargs_raw(self, indent=False):
5507 def kwargs_raw(self, indent=False):
5508 return self._as_raw(self.task_kwargs, indent)
5508 return self._as_raw(self.task_kwargs, indent)
5509
5509
5510 def __repr__(self):
5510 def __repr__(self):
5511 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5511 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5512
5512
5513
5513
5514 @event.listens_for(ScheduleEntry, 'before_update')
5514 @event.listens_for(ScheduleEntry, 'before_update')
5515 def update_task_uid(mapper, connection, target):
5515 def update_task_uid(mapper, connection, target):
5516 target.task_uid = ScheduleEntry.get_uid(target)
5516 target.task_uid = ScheduleEntry.get_uid(target)
5517
5517
5518
5518
5519 @event.listens_for(ScheduleEntry, 'before_insert')
5519 @event.listens_for(ScheduleEntry, 'before_insert')
5520 def set_task_uid(mapper, connection, target):
5520 def set_task_uid(mapper, connection, target):
5521 target.task_uid = ScheduleEntry.get_uid(target)
5521 target.task_uid = ScheduleEntry.get_uid(target)
5522
5522
5523
5523
5524 class _BaseBranchPerms(BaseModel):
5524 class _BaseBranchPerms(BaseModel):
5525 @classmethod
5525 @classmethod
5526 def compute_hash(cls, value):
5526 def compute_hash(cls, value):
5527 return sha1_safe(value)
5527 return sha1_safe(value)
5528
5528
5529 @hybrid_property
5529 @hybrid_property
5530 def branch_pattern(self):
5530 def branch_pattern(self):
5531 return self._branch_pattern or '*'
5531 return self._branch_pattern or '*'
5532
5532
5533 @hybrid_property
5533 @hybrid_property
5534 def branch_hash(self):
5534 def branch_hash(self):
5535 return self._branch_hash
5535 return self._branch_hash
5536
5536
5537 def _validate_glob(self, value):
5537 def _validate_glob(self, value):
5538 re.compile('^' + glob2re(value) + '$')
5538 re.compile('^' + glob2re(value) + '$')
5539
5539
5540 @branch_pattern.setter
5540 @branch_pattern.setter
5541 def branch_pattern(self, value):
5541 def branch_pattern(self, value):
5542 self._validate_glob(value)
5542 self._validate_glob(value)
5543 self._branch_pattern = value or '*'
5543 self._branch_pattern = value or '*'
5544 # set the Hash when setting the branch pattern
5544 # set the Hash when setting the branch pattern
5545 self._branch_hash = self.compute_hash(self._branch_pattern)
5545 self._branch_hash = self.compute_hash(self._branch_pattern)
5546
5546
5547 def matches(self, branch):
5547 def matches(self, branch):
5548 """
5548 """
5549 Check if this the branch matches entry
5549 Check if this the branch matches entry
5550
5550
5551 :param branch: branch name for the commit
5551 :param branch: branch name for the commit
5552 """
5552 """
5553
5553
5554 branch = branch or ''
5554 branch = branch or ''
5555
5555
5556 branch_matches = True
5556 branch_matches = True
5557 if branch:
5557 if branch:
5558 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5558 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5559 branch_matches = bool(branch_regex.search(branch))
5559 branch_matches = bool(branch_regex.search(branch))
5560
5560
5561 return branch_matches
5561 return branch_matches
5562
5562
5563
5563
5564 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5564 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5565 __tablename__ = 'user_to_repo_branch_permissions'
5565 __tablename__ = 'user_to_repo_branch_permissions'
5566 __table_args__ = (
5566 __table_args__ = (
5567 base_table_args
5567 base_table_args
5568 )
5568 )
5569
5569
5570 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5570 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5571
5571
5572 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5572 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5573 repo = relationship('Repository', back_populates='user_branch_perms')
5573 repo = relationship('Repository', back_populates='user_branch_perms')
5574
5574
5575 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5575 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5576 permission = relationship('Permission')
5576 permission = relationship('Permission')
5577
5577
5578 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5578 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5579 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5579 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5580
5580
5581 rule_order = Column('rule_order', Integer(), nullable=False)
5581 rule_order = Column('rule_order', Integer(), nullable=False)
5582 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5582 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5583 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5583 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5584
5584
5585 def __repr__(self):
5585 def __repr__(self):
5586 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5586 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5587
5587
5588
5588
5589 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5589 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5590 __tablename__ = 'user_group_to_repo_branch_permissions'
5590 __tablename__ = 'user_group_to_repo_branch_permissions'
5591 __table_args__ = (
5591 __table_args__ = (
5592 base_table_args
5592 base_table_args
5593 )
5593 )
5594
5594
5595 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5595 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5596
5596
5597 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5597 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5598 repo = relationship('Repository', back_populates='user_group_branch_perms')
5598 repo = relationship('Repository', back_populates='user_group_branch_perms')
5599
5599
5600 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5600 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5601 permission = relationship('Permission')
5601 permission = relationship('Permission')
5602
5602
5603 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)
5603 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)
5604 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5604 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5605
5605
5606 rule_order = Column('rule_order', Integer(), nullable=False)
5606 rule_order = Column('rule_order', Integer(), nullable=False)
5607 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5607 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5608 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5608 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5609
5609
5610 def __repr__(self):
5610 def __repr__(self):
5611 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5611 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5612
5612
5613
5613
5614 class UserBookmark(Base, BaseModel):
5614 class UserBookmark(Base, BaseModel):
5615 __tablename__ = 'user_bookmarks'
5615 __tablename__ = 'user_bookmarks'
5616 __table_args__ = (
5616 __table_args__ = (
5617 UniqueConstraint('user_id', 'bookmark_repo_id'),
5617 UniqueConstraint('user_id', 'bookmark_repo_id'),
5618 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5618 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5619 UniqueConstraint('user_id', 'bookmark_position'),
5619 UniqueConstraint('user_id', 'bookmark_position'),
5620 base_table_args
5620 base_table_args
5621 )
5621 )
5622
5622
5623 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5623 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5624 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5624 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5625 position = Column("bookmark_position", Integer(), nullable=False)
5625 position = Column("bookmark_position", Integer(), nullable=False)
5626 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5626 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5627 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5627 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5628 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5628 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5629
5629
5630 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5630 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5631 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5631 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5632
5632
5633 user = relationship("User")
5633 user = relationship("User")
5634
5634
5635 repository = relationship("Repository")
5635 repository = relationship("Repository")
5636 repository_group = relationship("RepoGroup")
5636 repository_group = relationship("RepoGroup")
5637
5637
5638 @classmethod
5638 @classmethod
5639 def get_by_position_for_user(cls, position, user_id):
5639 def get_by_position_for_user(cls, position, user_id):
5640 return cls.query() \
5640 return cls.query() \
5641 .filter(UserBookmark.user_id == user_id) \
5641 .filter(UserBookmark.user_id == user_id) \
5642 .filter(UserBookmark.position == position).scalar()
5642 .filter(UserBookmark.position == position).scalar()
5643
5643
5644 @classmethod
5644 @classmethod
5645 def get_bookmarks_for_user(cls, user_id, cache=True):
5645 def get_bookmarks_for_user(cls, user_id, cache=True):
5646 bookmarks = cls.query() \
5646 bookmarks = select(
5647 .filter(UserBookmark.user_id == user_id) \
5647 UserBookmark.title,
5648 .options(joinedload(UserBookmark.repository)) \
5648 UserBookmark.position,
5649 .options(joinedload(UserBookmark.repository_group)) \
5649 ) \
5650 .add_columns(Repository.repo_id, Repository.repo_type, Repository.repo_name) \
5651 .add_columns(RepoGroup.group_id, RepoGroup.group_name) \
5652 .where(UserBookmark.user_id == user_id) \
5653 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
5654 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
5650 .order_by(UserBookmark.position.asc())
5655 .order_by(UserBookmark.position.asc())
5651
5656
5652 if cache:
5657 if cache:
5653 bookmarks = bookmarks.options(
5658 bookmarks = bookmarks.options(
5654 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5659 FromCache("sql_cache_short", f"get_user_{user_id}_bookmarks")
5655 )
5660 )
5656
5661
5657 return bookmarks.all()
5662 return Session().execute(bookmarks).all()
5658
5663
5659 def __repr__(self):
5664 def __repr__(self):
5660 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5665 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5661
5666
5662
5667
5663 class FileStore(Base, BaseModel):
5668 class FileStore(Base, BaseModel):
5664 __tablename__ = 'file_store'
5669 __tablename__ = 'file_store'
5665 __table_args__ = (
5670 __table_args__ = (
5666 base_table_args
5671 base_table_args
5667 )
5672 )
5668
5673
5669 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5674 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5670 file_uid = Column('file_uid', String(1024), nullable=False)
5675 file_uid = Column('file_uid', String(1024), nullable=False)
5671 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5676 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5672 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5677 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5673 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5678 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5674
5679
5675 # sha256 hash
5680 # sha256 hash
5676 file_hash = Column('file_hash', String(512), nullable=False)
5681 file_hash = Column('file_hash', String(512), nullable=False)
5677 file_size = Column('file_size', BigInteger(), nullable=False)
5682 file_size = Column('file_size', BigInteger(), nullable=False)
5678
5683
5679 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5684 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5680 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5685 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5681 accessed_count = Column('accessed_count', Integer(), default=0)
5686 accessed_count = Column('accessed_count', Integer(), default=0)
5682
5687
5683 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5688 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5684
5689
5685 # if repo/repo_group reference is set, check for permissions
5690 # if repo/repo_group reference is set, check for permissions
5686 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5691 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5687
5692
5688 # hidden defines an attachment that should be hidden from showing in artifact listing
5693 # hidden defines an attachment that should be hidden from showing in artifact listing
5689 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5694 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5690
5695
5691 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5696 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5692 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5697 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5693
5698
5694 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5699 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5695
5700
5696 # scope limited to user, which requester have access to
5701 # scope limited to user, which requester have access to
5697 scope_user_id = Column(
5702 scope_user_id = Column(
5698 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5703 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5699 nullable=True, unique=None, default=None)
5704 nullable=True, unique=None, default=None)
5700 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5705 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5701
5706
5702 # scope limited to user group, which requester have access to
5707 # scope limited to user group, which requester have access to
5703 scope_user_group_id = Column(
5708 scope_user_group_id = Column(
5704 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5709 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5705 nullable=True, unique=None, default=None)
5710 nullable=True, unique=None, default=None)
5706 user_group = relationship('UserGroup', lazy='joined')
5711 user_group = relationship('UserGroup', lazy='joined')
5707
5712
5708 # scope limited to repo, which requester have access to
5713 # scope limited to repo, which requester have access to
5709 scope_repo_id = Column(
5714 scope_repo_id = Column(
5710 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5715 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5711 nullable=True, unique=None, default=None)
5716 nullable=True, unique=None, default=None)
5712 repo = relationship('Repository', lazy='joined')
5717 repo = relationship('Repository', lazy='joined')
5713
5718
5714 # scope limited to repo group, which requester have access to
5719 # scope limited to repo group, which requester have access to
5715 scope_repo_group_id = Column(
5720 scope_repo_group_id = Column(
5716 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5721 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5717 nullable=True, unique=None, default=None)
5722 nullable=True, unique=None, default=None)
5718 repo_group = relationship('RepoGroup', lazy='joined')
5723 repo_group = relationship('RepoGroup', lazy='joined')
5719
5724
5720 @classmethod
5725 @classmethod
5721 def get_scope(cls, scope_type, scope_id):
5726 def get_scope(cls, scope_type, scope_id):
5722 if scope_type == 'repo':
5727 if scope_type == 'repo':
5723 return f'repo:{scope_id}'
5728 return f'repo:{scope_id}'
5724 elif scope_type == 'repo-group':
5729 elif scope_type == 'repo-group':
5725 return f'repo-group:{scope_id}'
5730 return f'repo-group:{scope_id}'
5726 elif scope_type == 'user':
5731 elif scope_type == 'user':
5727 return f'user:{scope_id}'
5732 return f'user:{scope_id}'
5728 elif scope_type == 'user-group':
5733 elif scope_type == 'user-group':
5729 return f'user-group:{scope_id}'
5734 return f'user-group:{scope_id}'
5730 else:
5735 else:
5731 return scope_type
5736 return scope_type
5732
5737
5733 @classmethod
5738 @classmethod
5734 def get_by_store_uid(cls, file_store_uid, safe=False):
5739 def get_by_store_uid(cls, file_store_uid, safe=False):
5735 if safe:
5740 if safe:
5736 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5741 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5737 else:
5742 else:
5738 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5743 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5739
5744
5740 @classmethod
5745 @classmethod
5741 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5746 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5742 file_description='', enabled=True, hidden=False, check_acl=True,
5747 file_description='', enabled=True, hidden=False, check_acl=True,
5743 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5748 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5744
5749
5745 store_entry = FileStore()
5750 store_entry = FileStore()
5746 store_entry.file_uid = file_uid
5751 store_entry.file_uid = file_uid
5747 store_entry.file_display_name = file_display_name
5752 store_entry.file_display_name = file_display_name
5748 store_entry.file_org_name = filename
5753 store_entry.file_org_name = filename
5749 store_entry.file_size = file_size
5754 store_entry.file_size = file_size
5750 store_entry.file_hash = file_hash
5755 store_entry.file_hash = file_hash
5751 store_entry.file_description = file_description
5756 store_entry.file_description = file_description
5752
5757
5753 store_entry.check_acl = check_acl
5758 store_entry.check_acl = check_acl
5754 store_entry.enabled = enabled
5759 store_entry.enabled = enabled
5755 store_entry.hidden = hidden
5760 store_entry.hidden = hidden
5756
5761
5757 store_entry.user_id = user_id
5762 store_entry.user_id = user_id
5758 store_entry.scope_user_id = scope_user_id
5763 store_entry.scope_user_id = scope_user_id
5759 store_entry.scope_repo_id = scope_repo_id
5764 store_entry.scope_repo_id = scope_repo_id
5760 store_entry.scope_repo_group_id = scope_repo_group_id
5765 store_entry.scope_repo_group_id = scope_repo_group_id
5761
5766
5762 return store_entry
5767 return store_entry
5763
5768
5764 @classmethod
5769 @classmethod
5765 def store_metadata(cls, file_store_id, args, commit=True):
5770 def store_metadata(cls, file_store_id, args, commit=True):
5766 file_store = FileStore.get(file_store_id)
5771 file_store = FileStore.get(file_store_id)
5767 if file_store is None:
5772 if file_store is None:
5768 return
5773 return
5769
5774
5770 for section, key, value, value_type in args:
5775 for section, key, value, value_type in args:
5771 has_key = FileStoreMetadata().query() \
5776 has_key = FileStoreMetadata().query() \
5772 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5777 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5773 .filter(FileStoreMetadata.file_store_meta_section == section) \
5778 .filter(FileStoreMetadata.file_store_meta_section == section) \
5774 .filter(FileStoreMetadata.file_store_meta_key == key) \
5779 .filter(FileStoreMetadata.file_store_meta_key == key) \
5775 .scalar()
5780 .scalar()
5776 if has_key:
5781 if has_key:
5777 msg = 'key `{}` already defined under section `{}` for this file.'\
5782 msg = 'key `{}` already defined under section `{}` for this file.'\
5778 .format(key, section)
5783 .format(key, section)
5779 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5784 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5780
5785
5781 # NOTE(marcink): raises ArtifactMetadataBadValueType
5786 # NOTE(marcink): raises ArtifactMetadataBadValueType
5782 FileStoreMetadata.valid_value_type(value_type)
5787 FileStoreMetadata.valid_value_type(value_type)
5783
5788
5784 meta_entry = FileStoreMetadata()
5789 meta_entry = FileStoreMetadata()
5785 meta_entry.file_store = file_store
5790 meta_entry.file_store = file_store
5786 meta_entry.file_store_meta_section = section
5791 meta_entry.file_store_meta_section = section
5787 meta_entry.file_store_meta_key = key
5792 meta_entry.file_store_meta_key = key
5788 meta_entry.file_store_meta_value_type = value_type
5793 meta_entry.file_store_meta_value_type = value_type
5789 meta_entry.file_store_meta_value = value
5794 meta_entry.file_store_meta_value = value
5790
5795
5791 Session().add(meta_entry)
5796 Session().add(meta_entry)
5792
5797
5793 try:
5798 try:
5794 if commit:
5799 if commit:
5795 Session().commit()
5800 Session().commit()
5796 except IntegrityError:
5801 except IntegrityError:
5797 Session().rollback()
5802 Session().rollback()
5798 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5803 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5799
5804
5800 @classmethod
5805 @classmethod
5801 def bump_access_counter(cls, file_uid, commit=True):
5806 def bump_access_counter(cls, file_uid, commit=True):
5802 FileStore().query()\
5807 FileStore().query()\
5803 .filter(FileStore.file_uid == file_uid)\
5808 .filter(FileStore.file_uid == file_uid)\
5804 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5809 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5805 FileStore.accessed_on: datetime.datetime.now()})
5810 FileStore.accessed_on: datetime.datetime.now()})
5806 if commit:
5811 if commit:
5807 Session().commit()
5812 Session().commit()
5808
5813
5809 def __json__(self):
5814 def __json__(self):
5810 data = {
5815 data = {
5811 'filename': self.file_display_name,
5816 'filename': self.file_display_name,
5812 'filename_org': self.file_org_name,
5817 'filename_org': self.file_org_name,
5813 'file_uid': self.file_uid,
5818 'file_uid': self.file_uid,
5814 'description': self.file_description,
5819 'description': self.file_description,
5815 'hidden': self.hidden,
5820 'hidden': self.hidden,
5816 'size': self.file_size,
5821 'size': self.file_size,
5817 'created_on': self.created_on,
5822 'created_on': self.created_on,
5818 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5823 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5819 'downloaded_times': self.accessed_count,
5824 'downloaded_times': self.accessed_count,
5820 'sha256': self.file_hash,
5825 'sha256': self.file_hash,
5821 'metadata': self.file_metadata,
5826 'metadata': self.file_metadata,
5822 }
5827 }
5823
5828
5824 return data
5829 return data
5825
5830
5826 def __repr__(self):
5831 def __repr__(self):
5827 return f'<FileStore({self.file_store_id})>'
5832 return f'<FileStore({self.file_store_id})>'
5828
5833
5829
5834
5830 class FileStoreMetadata(Base, BaseModel):
5835 class FileStoreMetadata(Base, BaseModel):
5831 __tablename__ = 'file_store_metadata'
5836 __tablename__ = 'file_store_metadata'
5832 __table_args__ = (
5837 __table_args__ = (
5833 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5838 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5834 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5839 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5835 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5840 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5836 base_table_args
5841 base_table_args
5837 )
5842 )
5838 SETTINGS_TYPES = {
5843 SETTINGS_TYPES = {
5839 'str': safe_str,
5844 'str': safe_str,
5840 'int': safe_int,
5845 'int': safe_int,
5841 'unicode': safe_str,
5846 'unicode': safe_str,
5842 'bool': str2bool,
5847 'bool': str2bool,
5843 'list': functools.partial(aslist, sep=',')
5848 'list': functools.partial(aslist, sep=',')
5844 }
5849 }
5845
5850
5846 file_store_meta_id = Column(
5851 file_store_meta_id = Column(
5847 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5852 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5848 primary_key=True)
5853 primary_key=True)
5849 _file_store_meta_section = Column(
5854 _file_store_meta_section = Column(
5850 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5855 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5851 nullable=True, unique=None, default=None)
5856 nullable=True, unique=None, default=None)
5852 _file_store_meta_section_hash = Column(
5857 _file_store_meta_section_hash = Column(
5853 "file_store_meta_section_hash", String(255),
5858 "file_store_meta_section_hash", String(255),
5854 nullable=True, unique=None, default=None)
5859 nullable=True, unique=None, default=None)
5855 _file_store_meta_key = Column(
5860 _file_store_meta_key = Column(
5856 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5861 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5857 nullable=True, unique=None, default=None)
5862 nullable=True, unique=None, default=None)
5858 _file_store_meta_key_hash = Column(
5863 _file_store_meta_key_hash = Column(
5859 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5864 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5860 _file_store_meta_value = Column(
5865 _file_store_meta_value = Column(
5861 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5866 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5862 nullable=True, unique=None, default=None)
5867 nullable=True, unique=None, default=None)
5863 _file_store_meta_value_type = Column(
5868 _file_store_meta_value_type = Column(
5864 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5869 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5865 default='unicode')
5870 default='unicode')
5866
5871
5867 file_store_id = Column(
5872 file_store_id = Column(
5868 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5873 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5869 nullable=True, unique=None, default=None)
5874 nullable=True, unique=None, default=None)
5870
5875
5871 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5876 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5872
5877
5873 @classmethod
5878 @classmethod
5874 def valid_value_type(cls, value):
5879 def valid_value_type(cls, value):
5875 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5880 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5876 raise ArtifactMetadataBadValueType(
5881 raise ArtifactMetadataBadValueType(
5877 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5882 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5878
5883
5879 @hybrid_property
5884 @hybrid_property
5880 def file_store_meta_section(self):
5885 def file_store_meta_section(self):
5881 return self._file_store_meta_section
5886 return self._file_store_meta_section
5882
5887
5883 @file_store_meta_section.setter
5888 @file_store_meta_section.setter
5884 def file_store_meta_section(self, value):
5889 def file_store_meta_section(self, value):
5885 self._file_store_meta_section = value
5890 self._file_store_meta_section = value
5886 self._file_store_meta_section_hash = _hash_key(value)
5891 self._file_store_meta_section_hash = _hash_key(value)
5887
5892
5888 @hybrid_property
5893 @hybrid_property
5889 def file_store_meta_key(self):
5894 def file_store_meta_key(self):
5890 return self._file_store_meta_key
5895 return self._file_store_meta_key
5891
5896
5892 @file_store_meta_key.setter
5897 @file_store_meta_key.setter
5893 def file_store_meta_key(self, value):
5898 def file_store_meta_key(self, value):
5894 self._file_store_meta_key = value
5899 self._file_store_meta_key = value
5895 self._file_store_meta_key_hash = _hash_key(value)
5900 self._file_store_meta_key_hash = _hash_key(value)
5896
5901
5897 @hybrid_property
5902 @hybrid_property
5898 def file_store_meta_value(self):
5903 def file_store_meta_value(self):
5899 val = self._file_store_meta_value
5904 val = self._file_store_meta_value
5900
5905
5901 if self._file_store_meta_value_type:
5906 if self._file_store_meta_value_type:
5902 # e.g unicode.encrypted == unicode
5907 # e.g unicode.encrypted == unicode
5903 _type = self._file_store_meta_value_type.split('.')[0]
5908 _type = self._file_store_meta_value_type.split('.')[0]
5904 # decode the encrypted value if it's encrypted field type
5909 # decode the encrypted value if it's encrypted field type
5905 if '.encrypted' in self._file_store_meta_value_type:
5910 if '.encrypted' in self._file_store_meta_value_type:
5906 cipher = EncryptedTextValue()
5911 cipher = EncryptedTextValue()
5907 val = safe_str(cipher.process_result_value(val, None))
5912 val = safe_str(cipher.process_result_value(val, None))
5908 # do final type conversion
5913 # do final type conversion
5909 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5914 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5910 val = converter(val)
5915 val = converter(val)
5911
5916
5912 return val
5917 return val
5913
5918
5914 @file_store_meta_value.setter
5919 @file_store_meta_value.setter
5915 def file_store_meta_value(self, val):
5920 def file_store_meta_value(self, val):
5916 val = safe_str(val)
5921 val = safe_str(val)
5917 # encode the encrypted value
5922 # encode the encrypted value
5918 if '.encrypted' in self.file_store_meta_value_type:
5923 if '.encrypted' in self.file_store_meta_value_type:
5919 cipher = EncryptedTextValue()
5924 cipher = EncryptedTextValue()
5920 val = safe_str(cipher.process_bind_param(val, None))
5925 val = safe_str(cipher.process_bind_param(val, None))
5921 self._file_store_meta_value = val
5926 self._file_store_meta_value = val
5922
5927
5923 @hybrid_property
5928 @hybrid_property
5924 def file_store_meta_value_type(self):
5929 def file_store_meta_value_type(self):
5925 return self._file_store_meta_value_type
5930 return self._file_store_meta_value_type
5926
5931
5927 @file_store_meta_value_type.setter
5932 @file_store_meta_value_type.setter
5928 def file_store_meta_value_type(self, val):
5933 def file_store_meta_value_type(self, val):
5929 # e.g unicode.encrypted
5934 # e.g unicode.encrypted
5930 self.valid_value_type(val)
5935 self.valid_value_type(val)
5931 self._file_store_meta_value_type = val
5936 self._file_store_meta_value_type = val
5932
5937
5933 def __json__(self):
5938 def __json__(self):
5934 data = {
5939 data = {
5935 'artifact': self.file_store.file_uid,
5940 'artifact': self.file_store.file_uid,
5936 'section': self.file_store_meta_section,
5941 'section': self.file_store_meta_section,
5937 'key': self.file_store_meta_key,
5942 'key': self.file_store_meta_key,
5938 'value': self.file_store_meta_value,
5943 'value': self.file_store_meta_value,
5939 }
5944 }
5940
5945
5941 return data
5946 return data
5942
5947
5943 def __repr__(self):
5948 def __repr__(self):
5944 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
5949 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
5945 self.file_store_meta_key, self.file_store_meta_value)
5950 self.file_store_meta_key, self.file_store_meta_value)
5946
5951
5947
5952
5948 class DbMigrateVersion(Base, BaseModel):
5953 class DbMigrateVersion(Base, BaseModel):
5949 __tablename__ = 'db_migrate_version'
5954 __tablename__ = 'db_migrate_version'
5950 __table_args__ = (
5955 __table_args__ = (
5951 base_table_args,
5956 base_table_args,
5952 )
5957 )
5953
5958
5954 repository_id = Column('repository_id', String(250), primary_key=True)
5959 repository_id = Column('repository_id', String(250), primary_key=True)
5955 repository_path = Column('repository_path', Text)
5960 repository_path = Column('repository_path', Text)
5956 version = Column('version', Integer)
5961 version = Column('version', Integer)
5957
5962
5958 @classmethod
5963 @classmethod
5959 def set_version(cls, version):
5964 def set_version(cls, version):
5960 """
5965 """
5961 Helper for forcing a different version, usually for debugging purposes via ishell.
5966 Helper for forcing a different version, usually for debugging purposes via ishell.
5962 """
5967 """
5963 ver = DbMigrateVersion.query().first()
5968 ver = DbMigrateVersion.query().first()
5964 ver.version = version
5969 ver.version = version
5965 Session().commit()
5970 Session().commit()
5966
5971
5967
5972
5968 class DbSession(Base, BaseModel):
5973 class DbSession(Base, BaseModel):
5969 __tablename__ = 'db_session'
5974 __tablename__ = 'db_session'
5970 __table_args__ = (
5975 __table_args__ = (
5971 base_table_args,
5976 base_table_args,
5972 )
5977 )
5973
5978
5974 def __repr__(self):
5979 def __repr__(self):
5975 return f'<DB:DbSession({self.id})>'
5980 return f'<DB:DbSession({self.id})>'
5976
5981
5977 id = Column('id', Integer())
5982 id = Column('id', Integer())
5978 namespace = Column('namespace', String(255), primary_key=True)
5983 namespace = Column('namespace', String(255), primary_key=True)
5979 accessed = Column('accessed', DateTime, nullable=False)
5984 accessed = Column('accessed', DateTime, nullable=False)
5980 created = Column('created', DateTime, nullable=False)
5985 created = Column('created', DateTime, nullable=False)
5981 data = Column('data', PickleType, nullable=False)
5986 data = Column('data', PickleType, nullable=False)
@@ -1,1050 +1,1055 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 users model for RhodeCode
20 users model for RhodeCode
21 """
21 """
22
22
23 import logging
23 import logging
24 import traceback
24 import traceback
25 import datetime
25 import datetime
26 import ipaddress
26 import ipaddress
27
27
28 from pyramid.threadlocal import get_current_request
28 from pyramid.threadlocal import get_current_request
29 from sqlalchemy.exc import DatabaseError
29 from sqlalchemy.exc import DatabaseError
30
30
31 from rhodecode import events
31 from rhodecode import events
32 from rhodecode.lib.user_log_filter import user_log_filter
32 from rhodecode.lib.user_log_filter import user_log_filter
33 from rhodecode.lib.utils2 import (
33 from rhodecode.lib.utils2 import (
34 get_current_rhodecode_user, action_logger_generic,
34 get_current_rhodecode_user, action_logger_generic,
35 AttributeDict, str2bool)
35 AttributeDict, str2bool)
36 from rhodecode.lib.str_utils import safe_str
36 from rhodecode.lib.str_utils import safe_str
37 from rhodecode.lib.exceptions import (
37 from rhodecode.lib.exceptions import (
38 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
38 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
39 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
39 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
40 UserOwnsPullRequestsException, UserOwnsArtifactsException, DuplicateUpdateUserError)
40 UserOwnsPullRequestsException, UserOwnsArtifactsException, DuplicateUpdateUserError)
41 from rhodecode.lib.caching_query import FromCache
41 from rhodecode.lib.caching_query import FromCache
42 from rhodecode.model import BaseModel
42 from rhodecode.model import BaseModel
43 from rhodecode.model.db import (
43 from rhodecode.model.db import (
44 _hash_key, func, true, false, or_, joinedload, User, UserToPerm,
44 _hash_key, func, true, false, or_, joinedload, User, UserToPerm,
45 UserEmailMap, UserIpMap, UserLog)
45 UserEmailMap, UserIpMap, UserLog)
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47 from rhodecode.model.auth_token import AuthTokenModel
47 from rhodecode.model.auth_token import AuthTokenModel
48 from rhodecode.model.repo_group import RepoGroupModel
48 from rhodecode.model.repo_group import RepoGroupModel
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class UserModel(BaseModel):
53 class UserModel(BaseModel):
54 cls = User
54 cls = User
55
55
56 def get(self, user_id, cache=False):
56 def get(self, user_id, cache=False):
57 user = self.sa.query(User)
57 user = self.sa.query(User)
58 if cache:
58 if cache:
59 user = user.options(
59 user = user.options(
60 FromCache("sql_cache_short", f"get_user_{user_id}"))
60 FromCache("sql_cache_short", f"get_user_{user_id}"))
61 return user.get(user_id)
61 return user.get(user_id)
62
62
63 def get_user(self, user):
63 def get_user(self, user):
64 return self._get_user(user)
64 return self._get_user(user)
65
65
66 def _serialize_user(self, user):
66 def _serialize_user(self, user):
67 import rhodecode.lib.helpers as h
67 import rhodecode.lib.helpers as h
68
68
69 return {
69 return {
70 'id': user.user_id,
70 'id': user.user_id,
71 'first_name': user.first_name,
71 'first_name': user.first_name,
72 'last_name': user.last_name,
72 'last_name': user.last_name,
73 'username': user.username,
73 'username': user.username,
74 'email': user.email,
74 'email': user.email,
75 'icon_link': h.gravatar_url(user.email, 30),
75 'icon_link': h.gravatar_url(user.email, 30),
76 'profile_link': h.link_to_user(user),
76 'profile_link': h.link_to_user(user),
77 'value_display': h.escape(h.person(user)),
77 'value_display': h.escape(h.person(user)),
78 'value': user.username,
78 'value': user.username,
79 'value_type': 'user',
79 'value_type': 'user',
80 'active': user.active,
80 'active': user.active,
81 }
81 }
82
82
83 def get_users(self, name_contains=None, limit=20, only_active=True):
83 def get_users(self, name_contains=None, limit=20, only_active=True):
84
84
85 query = self.sa.query(User)
85 query = self.sa.query(User)
86 if only_active:
86 if only_active:
87 query = query.filter(User.active == true())
87 query = query.filter(User.active == true())
88
88
89 if name_contains:
89 if name_contains:
90 ilike_expression = f'%{safe_str(name_contains)}%'
90 ilike_expression = f'%{safe_str(name_contains)}%'
91 query = query.filter(
91 query = query.filter(
92 or_(
92 or_(
93 User.name.ilike(ilike_expression),
93 User.name.ilike(ilike_expression),
94 User.lastname.ilike(ilike_expression),
94 User.lastname.ilike(ilike_expression),
95 User.username.ilike(ilike_expression)
95 User.username.ilike(ilike_expression)
96 )
96 )
97 )
97 )
98 # sort by len to have top most matches first
98 # sort by len to have top most matches first
99 query = query.order_by(func.length(User.username))\
99 query = query.order_by(func.length(User.username))\
100 .order_by(User.username)
100 .order_by(User.username)
101 query = query.limit(limit)
101 query = query.limit(limit)
102
102
103 users = query.all()
103 users = query.all()
104
104
105 _users = [
105 _users = [
106 self._serialize_user(user) for user in users
106 self._serialize_user(user) for user in users
107 ]
107 ]
108 return _users
108 return _users
109
109
110 def get_by_username(self, username, cache=False, case_insensitive=False):
110 def get_by_username(self, username, cache=False, case_insensitive=False):
111
111
112 if case_insensitive:
112 if case_insensitive:
113 user = self.sa.query(User).filter(User.username.ilike(username))
113 user = self.sa.query(User).filter(User.username.ilike(username))
114 else:
114 else:
115 user = self.sa.query(User)\
115 user = self.sa.query(User)\
116 .filter(User.username == username)
116 .filter(User.username == username)
117
117 if cache:
118 if cache:
118 name_key = _hash_key(username)
119 name_key = _hash_key(username)
119 user = user.options(
120 user = user.options(
120 FromCache("sql_cache_short", f"get_user_{name_key}"))
121 FromCache("sql_cache_short", f"get_user_{name_key}"))
121 return user.scalar()
122 return user.scalar()
122
123
123 def get_by_email(self, email, cache=False, case_insensitive=False):
124 def get_by_email(self, email, cache=False, case_insensitive=False):
124 return User.get_by_email(email, case_insensitive, cache)
125 return User.get_by_email(email, case_insensitive, cache)
125
126
126 def get_by_auth_token(self, auth_token, cache=False):
127 def get_by_auth_token(self, auth_token, cache=False):
127 return User.get_by_auth_token(auth_token, cache)
128 return User.get_by_auth_token(auth_token, cache)
128
129
129 def get_active_user_count(self, cache=False):
130 def get_active_user_count(self, cache=False):
130 qry = User.query().filter(
131 qry = User.query().filter(
131 User.active == true()).filter(
132 User.active == true()).filter(
132 User.username != User.DEFAULT_USER)
133 User.username != User.DEFAULT_USER)
133 if cache:
134 if cache:
134 qry = qry.options(
135 qry = qry.options(
135 FromCache("sql_cache_short", "get_active_users"))
136 FromCache("sql_cache_short", "get_active_users"))
136 return qry.count()
137 return qry.count()
137
138
138 def create(self, form_data, cur_user=None):
139 def create(self, form_data, cur_user=None):
139 if not cur_user:
140 if not cur_user:
140 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
141 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
141
142
142 user_data = {
143 user_data = {
143 'username': form_data['username'],
144 'username': form_data['username'],
144 'password': form_data['password'],
145 'password': form_data['password'],
145 'email': form_data['email'],
146 'email': form_data['email'],
146 'firstname': form_data['firstname'],
147 'firstname': form_data['firstname'],
147 'lastname': form_data['lastname'],
148 'lastname': form_data['lastname'],
148 'active': form_data['active'],
149 'active': form_data['active'],
149 'extern_type': form_data['extern_type'],
150 'extern_type': form_data['extern_type'],
150 'extern_name': form_data['extern_name'],
151 'extern_name': form_data['extern_name'],
151 'admin': False,
152 'admin': False,
152 'cur_user': cur_user
153 'cur_user': cur_user
153 }
154 }
154
155
155 if 'create_repo_group' in form_data:
156 if 'create_repo_group' in form_data:
156 user_data['create_repo_group'] = str2bool(
157 user_data['create_repo_group'] = str2bool(
157 form_data.get('create_repo_group'))
158 form_data.get('create_repo_group'))
158
159
159 try:
160 try:
160 if form_data.get('password_change'):
161 if form_data.get('password_change'):
161 user_data['force_password_change'] = True
162 user_data['force_password_change'] = True
162 return UserModel().create_or_update(**user_data)
163 return UserModel().create_or_update(**user_data)
163 except Exception:
164 except Exception:
164 log.error(traceback.format_exc())
165 log.error(traceback.format_exc())
165 raise
166 raise
166
167
167 def update_user(self, user, skip_attrs=None, **kwargs):
168 def update_user(self, user, skip_attrs=None, **kwargs):
168 from rhodecode.lib.auth import get_crypt_password
169 from rhodecode.lib.auth import get_crypt_password
169
170
170 user = self._get_user(user)
171 user = self._get_user(user)
171 if user.username == User.DEFAULT_USER:
172 if user.username == User.DEFAULT_USER:
172 raise DefaultUserException(
173 raise DefaultUserException(
173 "You can't edit this user (`%(username)s`) since it's "
174 "You can't edit this user (`%(username)s`) since it's "
174 "crucial for entire application" % {
175 "crucial for entire application" % {
175 'username': user.username})
176 'username': user.username})
176
177
177 # first store only defaults
178 # first store only defaults
178 user_attrs = {
179 user_attrs = {
179 'updating_user_id': user.user_id,
180 'updating_user_id': user.user_id,
180 'username': user.username,
181 'username': user.username,
181 'password': user.password,
182 'password': user.password,
182 'email': user.email,
183 'email': user.email,
183 'firstname': user.name,
184 'firstname': user.name,
184 'lastname': user.lastname,
185 'lastname': user.lastname,
185 'description': user.description,
186 'description': user.description,
186 'active': user.active,
187 'active': user.active,
187 'admin': user.admin,
188 'admin': user.admin,
188 'extern_name': user.extern_name,
189 'extern_name': user.extern_name,
189 'extern_type': user.extern_type,
190 'extern_type': user.extern_type,
190 'language': user.user_data.get('language')
191 'language': user.user_data.get('language')
191 }
192 }
192
193
193 # in case there's new_password, that comes from form, use it to
194 # in case there's new_password, that comes from form, use it to
194 # store password
195 # store password
195 if kwargs.get('new_password'):
196 if kwargs.get('new_password'):
196 kwargs['password'] = kwargs['new_password']
197 kwargs['password'] = kwargs['new_password']
197
198
198 # cleanups, my_account password change form
199 # cleanups, my_account password change form
199 kwargs.pop('current_password', None)
200 kwargs.pop('current_password', None)
200 kwargs.pop('new_password', None)
201 kwargs.pop('new_password', None)
201
202
202 # cleanups, user edit password change form
203 # cleanups, user edit password change form
203 kwargs.pop('password_confirmation', None)
204 kwargs.pop('password_confirmation', None)
204 kwargs.pop('password_change', None)
205 kwargs.pop('password_change', None)
205
206
206 # create repo group on user creation
207 # create repo group on user creation
207 kwargs.pop('create_repo_group', None)
208 kwargs.pop('create_repo_group', None)
208
209
209 # legacy forms send name, which is the firstname
210 # legacy forms send name, which is the firstname
210 firstname = kwargs.pop('name', None)
211 firstname = kwargs.pop('name', None)
211 if firstname:
212 if firstname:
212 kwargs['firstname'] = firstname
213 kwargs['firstname'] = firstname
213
214
214 for k, v in kwargs.items():
215 for k, v in kwargs.items():
215 # skip if we don't want to update this
216 # skip if we don't want to update this
216 if skip_attrs and k in skip_attrs:
217 if skip_attrs and k in skip_attrs:
217 continue
218 continue
218
219
219 user_attrs[k] = v
220 user_attrs[k] = v
220
221
221 try:
222 try:
222 return self.create_or_update(**user_attrs)
223 return self.create_or_update(**user_attrs)
223 except Exception:
224 except Exception:
224 log.error(traceback.format_exc())
225 log.error(traceback.format_exc())
225 raise
226 raise
226
227
227 def create_or_update(
228 def create_or_update(
228 self, username, password, email, firstname='', lastname='',
229 self, username, password, email, firstname='', lastname='',
229 active=True, admin=False, extern_type=None, extern_name=None,
230 active=True, admin=False, extern_type=None, extern_name=None,
230 cur_user=None, plugin=None, force_password_change=False,
231 cur_user=None, plugin=None, force_password_change=False,
231 allow_to_create_user=True, create_repo_group=None,
232 allow_to_create_user=True, create_repo_group=None,
232 updating_user_id=None, language=None, description='',
233 updating_user_id=None, language=None, description='',
233 strict_creation_check=True):
234 strict_creation_check=True):
234 """
235 """
235 Creates a new instance if not found, or updates current one
236 Creates a new instance if not found, or updates current one
236
237
237 :param username:
238 :param username:
238 :param password:
239 :param password:
239 :param email:
240 :param email:
240 :param firstname:
241 :param firstname:
241 :param lastname:
242 :param lastname:
242 :param active:
243 :param active:
243 :param admin:
244 :param admin:
244 :param extern_type:
245 :param extern_type:
245 :param extern_name:
246 :param extern_name:
246 :param cur_user:
247 :param cur_user:
247 :param plugin: optional plugin this method was called from
248 :param plugin: optional plugin this method was called from
248 :param force_password_change: toggles new or existing user flag
249 :param force_password_change: toggles new or existing user flag
249 for password change
250 for password change
250 :param allow_to_create_user: Defines if the method can actually create
251 :param allow_to_create_user: Defines if the method can actually create
251 new users
252 new users
252 :param create_repo_group: Defines if the method should also
253 :param create_repo_group: Defines if the method should also
253 create an repo group with user name, and owner
254 create an repo group with user name, and owner
254 :param updating_user_id: if we set it up this is the user we want to
255 :param updating_user_id: if we set it up this is the user we want to
255 update this allows to editing username.
256 update this allows to editing username.
256 :param language: language of user from interface.
257 :param language: language of user from interface.
257 :param description: user description
258 :param description: user description
258 :param strict_creation_check: checks for allowed creation license wise etc.
259 :param strict_creation_check: checks for allowed creation license wise etc.
259
260
260 :returns: new User object with injected `is_new_user` attribute.
261 :returns: new User object with injected `is_new_user` attribute.
261 """
262 """
262
263
263 if not cur_user:
264 if not cur_user:
264 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
265 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
265
266
266 from rhodecode.lib.auth import (
267 from rhodecode.lib.auth import (
267 get_crypt_password, check_password)
268 get_crypt_password, check_password)
268 from rhodecode.lib import hooks_base
269 from rhodecode.lib import hooks_base
269
270
270 def _password_change(new_user, password):
271 def _password_change(new_user, password):
271 old_password = new_user.password or ''
272 old_password = new_user.password or ''
272 # empty password
273 # empty password
273 if not old_password:
274 if not old_password:
274 return False
275 return False
275
276
276 # password check is only needed for RhodeCode internal auth calls
277 # password check is only needed for RhodeCode internal auth calls
277 # in case it's a plugin we don't care
278 # in case it's a plugin we don't care
278 if not plugin:
279 if not plugin:
279
280
280 # first check if we gave crypted password back, and if it
281 # first check if we gave crypted password back, and if it
281 # matches it's not password change
282 # matches it's not password change
282 if new_user.password == password:
283 if new_user.password == password:
283 return False
284 return False
284
285
285 password_match = check_password(password, old_password)
286 password_match = check_password(password, old_password)
286 if not password_match:
287 if not password_match:
287 return True
288 return True
288
289
289 return False
290 return False
290
291
291 # read settings on default personal repo group creation
292 # read settings on default personal repo group creation
292 if create_repo_group is None:
293 if create_repo_group is None:
293 default_create_repo_group = RepoGroupModel()\
294 default_create_repo_group = RepoGroupModel()\
294 .get_default_create_personal_repo_group()
295 .get_default_create_personal_repo_group()
295 create_repo_group = default_create_repo_group
296 create_repo_group = default_create_repo_group
296
297
297 user_data = {
298 user_data = {
298 'username': username,
299 'username': username,
299 'password': password,
300 'password': password,
300 'email': email,
301 'email': email,
301 'firstname': firstname,
302 'firstname': firstname,
302 'lastname': lastname,
303 'lastname': lastname,
303 'active': active,
304 'active': active,
304 'admin': admin
305 'admin': admin
305 }
306 }
306
307
307 if updating_user_id:
308 if updating_user_id:
308 log.debug('Checking for existing account in RhodeCode '
309 log.debug('Checking for existing account in RhodeCode '
309 'database with user_id `%s` ', updating_user_id)
310 'database with user_id `%s` ', updating_user_id)
310 user = User.get(updating_user_id)
311 user = User.get(updating_user_id)
311 # now also validate if USERNAME belongs to potentially other user
312 # now also validate if USERNAME belongs to potentially other user
312 maybe_other_user = User.get_by_username(username, case_insensitive=True)
313 maybe_other_user = User.get_by_username(username, case_insensitive=True)
313 if maybe_other_user and maybe_other_user.user_id != updating_user_id:
314 if maybe_other_user and maybe_other_user.user_id != updating_user_id:
314 raise DuplicateUpdateUserError(f'different user exists with the {username} username')
315 raise DuplicateUpdateUserError(f'different user exists with the {username} username')
315 else:
316 else:
316 log.debug('Checking for existing account in RhodeCode '
317 log.debug('Checking for existing account in RhodeCode '
317 'database with username `%s` ', username)
318 'database with username `%s` ', username)
318 user = User.get_by_username(username, case_insensitive=True)
319 user = User.get_by_username(username, case_insensitive=True)
319
320
320 if user is None:
321 if user is None:
321 # we check internal flag if this method is actually allowed to
322 # we check internal flag if this method is actually allowed to
322 # create new user
323 # create new user
323 if not allow_to_create_user:
324 if not allow_to_create_user:
324 msg = ('Method wants to create new user, but it is not '
325 msg = ('Method wants to create new user, but it is not '
325 'allowed to do so')
326 'allowed to do so')
326 log.warning(msg)
327 log.warning(msg)
327 raise NotAllowedToCreateUserError(msg)
328 raise NotAllowedToCreateUserError(msg)
328
329
329 log.debug('Creating new user %s', username)
330 log.debug('Creating new user %s', username)
330
331
331 # only if we create user that is active
332 # only if we create user that is active
332 new_active_user = active
333 new_active_user = active
333 if new_active_user and strict_creation_check:
334 if new_active_user and strict_creation_check:
334 # raises UserCreationError if it's not allowed for any reason to
335 # raises UserCreationError if it's not allowed for any reason to
335 # create new active user, this also executes pre-create hooks
336 # create new active user, this also executes pre-create hooks
336 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
337 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
337 events.trigger(events.UserPreCreate(user_data))
338 events.trigger(events.UserPreCreate(user_data))
338 new_user = User()
339 new_user = User()
339 edit = False
340 edit = False
340 else:
341 else:
341 log.debug('updating user `%s`', username)
342 log.debug('updating user `%s`', username)
342 events.trigger(events.UserPreUpdate(user, user_data))
343 events.trigger(events.UserPreUpdate(user, user_data))
343 new_user = user
344 new_user = user
344 edit = True
345 edit = True
345
346
346 # we're not allowed to edit default user
347 # we're not allowed to edit default user
347 if user.username == User.DEFAULT_USER:
348 if user.username == User.DEFAULT_USER:
348 raise DefaultUserException(
349 raise DefaultUserException(
349 "You can't edit this user (`%(username)s`) since it's "
350 "You can't edit this user (`%(username)s`) since it's "
350 "crucial for entire application"
351 "crucial for entire application"
351 % {'username': user.username})
352 % {'username': user.username})
352
353
353 # inject special attribute that will tell us if User is new or old
354 # inject special attribute that will tell us if User is new or old
354 new_user.is_new_user = not edit
355 new_user.is_new_user = not edit
355 # for users that didn's specify auth type, we use RhodeCode built in
356 # for users that didn's specify auth type, we use RhodeCode built in
356 from rhodecode.authentication.plugins import auth_rhodecode
357 from rhodecode.authentication.plugins import auth_rhodecode
357 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
358 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
358 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
359 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
359
360
360 try:
361 try:
361 new_user.username = username
362 new_user.username = username
362 new_user.admin = admin
363 new_user.admin = admin
363 new_user.email = email
364 new_user.email = email
364 new_user.active = active
365 new_user.active = active
365 new_user.extern_name = safe_str(extern_name)
366 new_user.extern_name = safe_str(extern_name)
366 new_user.extern_type = safe_str(extern_type)
367 new_user.extern_type = safe_str(extern_type)
367 new_user.name = firstname
368 new_user.name = firstname
368 new_user.lastname = lastname
369 new_user.lastname = lastname
369 new_user.description = description
370 new_user.description = description
370
371
371 # set password only if creating an user or password is changed
372 # set password only if creating an user or password is changed
372 if not edit or _password_change(new_user, password):
373 if not edit or _password_change(new_user, password):
373 reason = 'new password' if edit else 'new user'
374 reason = 'new password' if edit else 'new user'
374 log.debug('Updating password reason=>%s', reason)
375 log.debug('Updating password reason=>%s', reason)
375 new_user.password = get_crypt_password(password) if password else None
376 new_user.password = get_crypt_password(password) if password else None
376
377
377 if force_password_change:
378 if force_password_change:
378 new_user.update_userdata(force_password_change=True)
379 new_user.update_userdata(force_password_change=True)
379 if language:
380 if language:
380 new_user.update_userdata(language=language)
381 new_user.update_userdata(language=language)
381 new_user.update_userdata(notification_status=True)
382 new_user.update_userdata(notification_status=True)
382
383
383 self.sa.add(new_user)
384 self.sa.add(new_user)
384
385
385 if not edit and create_repo_group:
386 if not edit and create_repo_group:
386 RepoGroupModel().create_personal_repo_group(
387 RepoGroupModel().create_personal_repo_group(
387 new_user, commit_early=False)
388 new_user, commit_early=False)
388
389
389 if not edit:
390 if not edit:
390 # add the RSS token
391 # add the RSS token
391 self.add_auth_token(
392 self.add_auth_token(
392 user=username, lifetime_minutes=-1,
393 user=username, lifetime_minutes=-1,
393 role=self.auth_token_role.ROLE_FEED,
394 role=self.auth_token_role.ROLE_FEED,
394 description='Generated feed token')
395 description='Generated feed token')
395
396
396 kwargs = new_user.get_dict()
397 kwargs = new_user.get_dict()
397 # backward compat, require api_keys present
398 # backward compat, require api_keys present
398 kwargs['api_keys'] = kwargs['auth_tokens']
399 kwargs['api_keys'] = kwargs['auth_tokens']
399 hooks_base.create_user(created_by=cur_user, **kwargs)
400 hooks_base.create_user(created_by=cur_user, **kwargs)
400 events.trigger(events.UserPostCreate(user_data))
401 events.trigger(events.UserPostCreate(user_data))
401 return new_user
402 return new_user
402 except (DatabaseError,):
403 except (DatabaseError,):
403 log.error(traceback.format_exc())
404 log.error(traceback.format_exc())
404 raise
405 raise
405
406
406 def create_registration(self, form_data,
407 def create_registration(self, form_data,
407 extern_name='rhodecode', extern_type='rhodecode'):
408 extern_name='rhodecode', extern_type='rhodecode'):
408 from rhodecode.model.notification import NotificationModel
409 from rhodecode.model.notification import NotificationModel
409 from rhodecode.model.notification import EmailNotificationModel
410 from rhodecode.model.notification import EmailNotificationModel
410
411
411 try:
412 try:
412 form_data['admin'] = False
413 form_data['admin'] = False
413 form_data['extern_name'] = extern_name
414 form_data['extern_name'] = extern_name
414 form_data['extern_type'] = extern_type
415 form_data['extern_type'] = extern_type
415 new_user = self.create(form_data)
416 new_user = self.create(form_data)
416
417
417 self.sa.add(new_user)
418 self.sa.add(new_user)
418 self.sa.flush()
419 self.sa.flush()
419
420
420 user_data = new_user.get_dict()
421 user_data = new_user.get_dict()
421 user_data.update({
422 user_data.update({
422 'first_name': user_data.get('firstname'),
423 'first_name': user_data.get('firstname'),
423 'last_name': user_data.get('lastname'),
424 'last_name': user_data.get('lastname'),
424 })
425 })
425 kwargs = {
426 kwargs = {
426 # use SQLALCHEMY safe dump of user data
427 # use SQLALCHEMY safe dump of user data
427 'user': AttributeDict(user_data),
428 'user': AttributeDict(user_data),
428 'date': datetime.datetime.now()
429 'date': datetime.datetime.now()
429 }
430 }
430 notification_type = EmailNotificationModel.TYPE_REGISTRATION
431 notification_type = EmailNotificationModel.TYPE_REGISTRATION
431
432
432 # create notification objects, and emails
433 # create notification objects, and emails
433 NotificationModel().create(
434 NotificationModel().create(
434 created_by=new_user,
435 created_by=new_user,
435 notification_subject='', # Filled in based on the notification_type
436 notification_subject='', # Filled in based on the notification_type
436 notification_body='', # Filled in based on the notification_type
437 notification_body='', # Filled in based on the notification_type
437 notification_type=notification_type,
438 notification_type=notification_type,
438 recipients=None, # all admins
439 recipients=None, # all admins
439 email_kwargs=kwargs,
440 email_kwargs=kwargs,
440 )
441 )
441
442
442 return new_user
443 return new_user
443 except Exception:
444 except Exception:
444 log.error(traceback.format_exc())
445 log.error(traceback.format_exc())
445 raise
446 raise
446
447
447 def _handle_user_repos(self, username, repositories, handle_user,
448 def _handle_user_repos(self, username, repositories, handle_user,
448 handle_mode=None):
449 handle_mode=None):
449
450
450 left_overs = True
451 left_overs = True
451
452
452 from rhodecode.model.repo import RepoModel
453 from rhodecode.model.repo import RepoModel
453
454
454 if handle_mode == 'detach':
455 if handle_mode == 'detach':
455 for obj in repositories:
456 for obj in repositories:
456 obj.user = handle_user
457 obj.user = handle_user
457 # set description we know why we super admin now owns
458 # set description we know why we super admin now owns
458 # additional repositories that were orphaned !
459 # additional repositories that were orphaned !
459 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
460 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
460 self.sa.add(obj)
461 self.sa.add(obj)
461 left_overs = False
462 left_overs = False
462 elif handle_mode == 'delete':
463 elif handle_mode == 'delete':
463 for obj in repositories:
464 for obj in repositories:
464 RepoModel().delete(obj, forks='detach')
465 RepoModel().delete(obj, forks='detach')
465 left_overs = False
466 left_overs = False
466
467
467 # if nothing is done we have left overs left
468 # if nothing is done we have left overs left
468 return left_overs
469 return left_overs
469
470
470 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
471 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
471 handle_mode=None):
472 handle_mode=None):
472
473
473 left_overs = True
474 left_overs = True
474
475
475 from rhodecode.model.repo_group import RepoGroupModel
476 from rhodecode.model.repo_group import RepoGroupModel
476
477
477 if handle_mode == 'detach':
478 if handle_mode == 'detach':
478 for r in repository_groups:
479 for r in repository_groups:
479 r.user = handle_user
480 r.user = handle_user
480 # set description we know why we super admin now owns
481 # set description we know why we super admin now owns
481 # additional repositories that were orphaned !
482 # additional repositories that were orphaned !
482 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
483 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
483 r.personal = False
484 r.personal = False
484 self.sa.add(r)
485 self.sa.add(r)
485 left_overs = False
486 left_overs = False
486 elif handle_mode == 'delete':
487 elif handle_mode == 'delete':
487 for r in repository_groups:
488 for r in repository_groups:
488 RepoGroupModel().delete(r)
489 RepoGroupModel().delete(r)
489 left_overs = False
490 left_overs = False
490
491
491 # if nothing is done we have left overs left
492 # if nothing is done we have left overs left
492 return left_overs
493 return left_overs
493
494
494 def _handle_user_user_groups(self, username, user_groups, handle_user,
495 def _handle_user_user_groups(self, username, user_groups, handle_user,
495 handle_mode=None):
496 handle_mode=None):
496
497
497 left_overs = True
498 left_overs = True
498
499
499 from rhodecode.model.user_group import UserGroupModel
500 from rhodecode.model.user_group import UserGroupModel
500
501
501 if handle_mode == 'detach':
502 if handle_mode == 'detach':
502 for r in user_groups:
503 for r in user_groups:
503 for user_user_group_to_perm in r.user_user_group_to_perm:
504 for user_user_group_to_perm in r.user_user_group_to_perm:
504 if user_user_group_to_perm.user.username == username:
505 if user_user_group_to_perm.user.username == username:
505 user_user_group_to_perm.user = handle_user
506 user_user_group_to_perm.user = handle_user
506 r.user = handle_user
507 r.user = handle_user
507 # set description we know why we super admin now owns
508 # set description we know why we super admin now owns
508 # additional repositories that were orphaned !
509 # additional repositories that were orphaned !
509 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
510 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
510 self.sa.add(r)
511 self.sa.add(r)
511 left_overs = False
512 left_overs = False
512 elif handle_mode == 'delete':
513 elif handle_mode == 'delete':
513 for r in user_groups:
514 for r in user_groups:
514 UserGroupModel().delete(r)
515 UserGroupModel().delete(r)
515 left_overs = False
516 left_overs = False
516
517
517 # if nothing is done we have left overs left
518 # if nothing is done we have left overs left
518 return left_overs
519 return left_overs
519
520
520 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
521 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
521 handle_mode=None):
522 handle_mode=None):
522 left_overs = True
523 left_overs = True
523
524
524 from rhodecode.model.pull_request import PullRequestModel
525 from rhodecode.model.pull_request import PullRequestModel
525
526
526 if handle_mode == 'detach':
527 if handle_mode == 'detach':
527 for pr in pull_requests:
528 for pr in pull_requests:
528 pr.user_id = handle_user.user_id
529 pr.user_id = handle_user.user_id
529 # set description we know why we super admin now owns
530 # set description we know why we super admin now owns
530 # additional repositories that were orphaned !
531 # additional repositories that were orphaned !
531 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
532 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
532 self.sa.add(pr)
533 self.sa.add(pr)
533 left_overs = False
534 left_overs = False
534 elif handle_mode == 'delete':
535 elif handle_mode == 'delete':
535 for pr in pull_requests:
536 for pr in pull_requests:
536 PullRequestModel().delete(pr)
537 PullRequestModel().delete(pr)
537
538
538 left_overs = False
539 left_overs = False
539
540
540 # if nothing is done we have leftovers left
541 # if nothing is done we have leftovers left
541 return left_overs
542 return left_overs
542
543
543 def _handle_user_artifacts(self, username, artifacts, handle_user,
544 def _handle_user_artifacts(self, username, artifacts, handle_user,
544 handle_mode=None):
545 handle_mode=None):
545
546
546 left_overs = True
547 left_overs = True
547
548
548 if handle_mode == 'detach':
549 if handle_mode == 'detach':
549 for a in artifacts:
550 for a in artifacts:
550 a.upload_user = handle_user
551 a.upload_user = handle_user
551 # set description we know why we super admin now owns
552 # set description we know why we super admin now owns
552 # additional artifacts that were orphaned !
553 # additional artifacts that were orphaned !
553 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
554 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
554 self.sa.add(a)
555 self.sa.add(a)
555 left_overs = False
556 left_overs = False
556 elif handle_mode == 'delete':
557 elif handle_mode == 'delete':
557 from rhodecode.apps.file_store import utils as store_utils
558 from rhodecode.apps.file_store import utils as store_utils
558 request = get_current_request()
559 request = get_current_request()
559 storage = store_utils.get_file_storage(request.registry.settings)
560 storage = store_utils.get_file_storage(request.registry.settings)
560 for a in artifacts:
561 for a in artifacts:
561 file_uid = a.file_uid
562 file_uid = a.file_uid
562 storage.delete(file_uid)
563 storage.delete(file_uid)
563 self.sa.delete(a)
564 self.sa.delete(a)
564
565
565 left_overs = False
566 left_overs = False
566
567
567 # if nothing is done we have left overs left
568 # if nothing is done we have left overs left
568 return left_overs
569 return left_overs
569
570
570 def delete(self, user, cur_user=None, handle_repos=None,
571 def delete(self, user, cur_user=None, handle_repos=None,
571 handle_repo_groups=None, handle_user_groups=None,
572 handle_repo_groups=None, handle_user_groups=None,
572 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
573 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
573 from rhodecode.lib import hooks_base
574 from rhodecode.lib import hooks_base
574
575
575 if not cur_user:
576 if not cur_user:
576 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
577 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
577
578
578 user = self._get_user(user)
579 user = self._get_user(user)
579
580
580 try:
581 try:
581 if user.username == User.DEFAULT_USER:
582 if user.username == User.DEFAULT_USER:
582 raise DefaultUserException(
583 raise DefaultUserException(
583 "You can't remove this user since it's"
584 "You can't remove this user since it's"
584 " crucial for entire application")
585 " crucial for entire application")
585 handle_user = handle_new_owner or self.cls.get_first_super_admin()
586 handle_user = handle_new_owner or self.cls.get_first_super_admin()
586 log.debug('New detached objects owner %s', handle_user)
587 log.debug('New detached objects owner %s', handle_user)
587
588
588 left_overs = self._handle_user_repos(
589 left_overs = self._handle_user_repos(
589 user.username, user.repositories, handle_user, handle_repos)
590 user.username, user.repositories, handle_user, handle_repos)
590 if left_overs and user.repositories:
591 if left_overs and user.repositories:
591 repos = [x.repo_name for x in user.repositories]
592 repos = [x.repo_name for x in user.repositories]
592 raise UserOwnsReposException(
593 raise UserOwnsReposException(
593 'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
594 'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
594 'removed. Switch owners or remove those repositories:%(list_repos)s'
595 'removed. Switch owners or remove those repositories:%(list_repos)s'
595 % {'username': user.username, 'len_repos': len(repos),
596 % {'username': user.username, 'len_repos': len(repos),
596 'list_repos': ', '.join(repos)})
597 'list_repos': ', '.join(repos)})
597
598
598 left_overs = self._handle_user_repo_groups(
599 left_overs = self._handle_user_repo_groups(
599 user.username, user.repository_groups, handle_user, handle_repo_groups)
600 user.username, user.repository_groups, handle_user, handle_repo_groups)
600 if left_overs and user.repository_groups:
601 if left_overs and user.repository_groups:
601 repo_groups = [x.group_name for x in user.repository_groups]
602 repo_groups = [x.group_name for x in user.repository_groups]
602 raise UserOwnsRepoGroupsException(
603 raise UserOwnsRepoGroupsException(
603 'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
604 'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
604 'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
605 'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
605 % {'username': user.username, 'len_repo_groups': len(repo_groups),
606 % {'username': user.username, 'len_repo_groups': len(repo_groups),
606 'list_repo_groups': ', '.join(repo_groups)})
607 'list_repo_groups': ', '.join(repo_groups)})
607
608
608 left_overs = self._handle_user_user_groups(
609 left_overs = self._handle_user_user_groups(
609 user.username, user.user_groups, handle_user, handle_user_groups)
610 user.username, user.user_groups, handle_user, handle_user_groups)
610 if left_overs and user.user_groups:
611 if left_overs and user.user_groups:
611 user_groups = [x.users_group_name for x in user.user_groups]
612 user_groups = [x.users_group_name for x in user.user_groups]
612 raise UserOwnsUserGroupsException(
613 raise UserOwnsUserGroupsException(
613 'user "%s" still owns %s user groups and cannot be '
614 'user "%s" still owns %s user groups and cannot be '
614 'removed. Switch owners or remove those user groups:%s'
615 'removed. Switch owners or remove those user groups:%s'
615 % (user.username, len(user_groups), ', '.join(user_groups)))
616 % (user.username, len(user_groups), ', '.join(user_groups)))
616
617
617 left_overs = self._handle_user_pull_requests(
618 left_overs = self._handle_user_pull_requests(
618 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
619 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
619 if left_overs and user.user_pull_requests:
620 if left_overs and user.user_pull_requests:
620 pull_requests = [f'!{x.pull_request_id}' for x in user.user_pull_requests]
621 pull_requests = [f'!{x.pull_request_id}' for x in user.user_pull_requests]
621 raise UserOwnsPullRequestsException(
622 raise UserOwnsPullRequestsException(
622 'user "%s" still owns %s pull requests and cannot be '
623 'user "%s" still owns %s pull requests and cannot be '
623 'removed. Switch owners or remove those pull requests:%s'
624 'removed. Switch owners or remove those pull requests:%s'
624 % (user.username, len(pull_requests), ', '.join(pull_requests)))
625 % (user.username, len(pull_requests), ', '.join(pull_requests)))
625
626
626 left_overs = self._handle_user_artifacts(
627 left_overs = self._handle_user_artifacts(
627 user.username, user.artifacts, handle_user, handle_artifacts)
628 user.username, user.artifacts, handle_user, handle_artifacts)
628 if left_overs and user.artifacts:
629 if left_overs and user.artifacts:
629 artifacts = [x.file_uid for x in user.artifacts]
630 artifacts = [x.file_uid for x in user.artifacts]
630 raise UserOwnsArtifactsException(
631 raise UserOwnsArtifactsException(
631 'user "%s" still owns %s artifacts and cannot be '
632 'user "%s" still owns %s artifacts and cannot be '
632 'removed. Switch owners or remove those artifacts:%s'
633 'removed. Switch owners or remove those artifacts:%s'
633 % (user.username, len(artifacts), ', '.join(artifacts)))
634 % (user.username, len(artifacts), ', '.join(artifacts)))
634
635
635 user_data = user.get_dict() # fetch user data before expire
636 user_data = user.get_dict() # fetch user data before expire
636
637
637 # we might change the user data with detach/delete, make sure
638 # we might change the user data with detach/delete, make sure
638 # the object is marked as expired before actually deleting !
639 # the object is marked as expired before actually deleting !
639 self.sa.expire(user)
640 self.sa.expire(user)
640 self.sa.delete(user)
641 self.sa.delete(user)
641
642
642 hooks_base.delete_user(deleted_by=cur_user, **user_data)
643 hooks_base.delete_user(deleted_by=cur_user, **user_data)
643 except Exception:
644 except Exception:
644 log.error(traceback.format_exc())
645 log.error(traceback.format_exc())
645 raise
646 raise
646
647
647 def reset_password_link(self, data, pwd_reset_url):
648 def reset_password_link(self, data, pwd_reset_url):
648 from rhodecode.lib.celerylib import tasks, run_task
649 from rhodecode.lib.celerylib import tasks, run_task
649 from rhodecode.model.notification import EmailNotificationModel
650 from rhodecode.model.notification import EmailNotificationModel
650 user_email = data['email']
651 user_email = data['email']
651 try:
652 try:
652 user = User.get_by_email(user_email)
653 user = User.get_by_email(user_email)
653 if user:
654 if user:
654 log.debug('password reset user found %s', user)
655 log.debug('password reset user found %s', user)
655
656
656 email_kwargs = {
657 email_kwargs = {
657 'password_reset_url': pwd_reset_url,
658 'password_reset_url': pwd_reset_url,
658 'user': user,
659 'user': user,
659 'email': user_email,
660 'email': user_email,
660 'date': datetime.datetime.now(),
661 'date': datetime.datetime.now(),
661 'first_admin_email': User.get_first_super_admin().email
662 'first_admin_email': User.get_first_super_admin().email
662 }
663 }
663
664
664 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
665 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
665 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
666 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
666
667
667 recipients = [user_email]
668 recipients = [user_email]
668
669
669 action_logger_generic(
670 action_logger_generic(
670 'sending password reset email to user: {}'.format(
671 'sending password reset email to user: {}'.format(
671 user), namespace='security.password_reset')
672 user), namespace='security.password_reset')
672
673
673 run_task(tasks.send_email, recipients, subject,
674 run_task(tasks.send_email, recipients, subject,
674 email_body_plaintext, email_body)
675 email_body_plaintext, email_body)
675
676
676 else:
677 else:
677 log.debug("password reset email %s not found", user_email)
678 log.debug("password reset email %s not found", user_email)
678 except Exception:
679 except Exception:
679 log.error(traceback.format_exc())
680 log.error(traceback.format_exc())
680 return False
681 return False
681
682
682 return True
683 return True
683
684
684 def reset_password(self, data):
685 def reset_password(self, data):
685 from rhodecode.lib.celerylib import tasks, run_task
686 from rhodecode.lib.celerylib import tasks, run_task
686 from rhodecode.model.notification import EmailNotificationModel
687 from rhodecode.model.notification import EmailNotificationModel
687 from rhodecode.lib import auth
688 from rhodecode.lib import auth
688 user_email = data['email']
689 user_email = data['email']
689 pre_db = True
690 pre_db = True
690 try:
691 try:
691 user = User.get_by_email(user_email)
692 user = User.get_by_email(user_email)
692 new_passwd = auth.PasswordGenerator().gen_password(
693 new_passwd = auth.PasswordGenerator().gen_password(
693 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
694 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
694 if user:
695 if user:
695 user.password = auth.get_crypt_password(new_passwd)
696 user.password = auth.get_crypt_password(new_passwd)
696 # also force this user to reset his password !
697 # also force this user to reset his password !
697 user.update_userdata(force_password_change=True)
698 user.update_userdata(force_password_change=True)
698
699
699 Session().add(user)
700 Session().add(user)
700
701
701 # now delete the token in question
702 # now delete the token in question
702 UserApiKeys = AuthTokenModel.cls
703 UserApiKeys = AuthTokenModel.cls
703 UserApiKeys().query().filter(
704 UserApiKeys().query().filter(
704 UserApiKeys.api_key == data['token']).delete()
705 UserApiKeys.api_key == data['token']).delete()
705
706
706 Session().commit()
707 Session().commit()
707 log.info('successfully reset password for `%s`', user_email)
708 log.info('successfully reset password for `%s`', user_email)
708
709
709 if new_passwd is None:
710 if new_passwd is None:
710 raise Exception('unable to generate new password')
711 raise Exception('unable to generate new password')
711
712
712 pre_db = False
713 pre_db = False
713
714
714 email_kwargs = {
715 email_kwargs = {
715 'new_password': new_passwd,
716 'new_password': new_passwd,
716 'user': user,
717 'user': user,
717 'email': user_email,
718 'email': user_email,
718 'date': datetime.datetime.now(),
719 'date': datetime.datetime.now(),
719 'first_admin_email': User.get_first_super_admin().email
720 'first_admin_email': User.get_first_super_admin().email
720 }
721 }
721
722
722 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
723 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
723 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
724 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
724 **email_kwargs)
725 **email_kwargs)
725
726
726 recipients = [user_email]
727 recipients = [user_email]
727
728
728 action_logger_generic(
729 action_logger_generic(
729 'sent new password to user: {} with email: {}'.format(
730 'sent new password to user: {} with email: {}'.format(
730 user, user_email), namespace='security.password_reset')
731 user, user_email), namespace='security.password_reset')
731
732
732 run_task(tasks.send_email, recipients, subject,
733 run_task(tasks.send_email, recipients, subject,
733 email_body_plaintext, email_body)
734 email_body_plaintext, email_body)
734
735
735 except Exception:
736 except Exception:
736 log.error('Failed to update user password')
737 log.error('Failed to update user password')
737 log.error(traceback.format_exc())
738 log.error(traceback.format_exc())
738 if pre_db:
739 if pre_db:
739 # 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
740 # 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
741 Session().rollback()
742 Session().rollback()
742
743
743 return True
744 return True
744
745
745 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):
746 """
747 """
747 Fetches auth_user by user_id,or api_key if present.
748 Fetches auth_user by user_id,or api_key if present.
748 Fills auth_user attributes with those taken from database.
749 Fills auth_user attributes with those taken from database.
749 Additionally set's is_authenitated if lookup fails
750 Additionally set's is_authenitated if lookup fails
750 present in database
751 present in database
751
752
752 :param auth_user: instance of user to set attributes
753 :param auth_user: instance of user to set attributes
753 :param user_id: user id to fetch by
754 :param user_id: user id to fetch by
754 :param api_key: api key to fetch by
755 :param api_key: api key to fetch by
755 :param username: username to fetch by
756 :param username: username to fetch by
756 """
757 """
757 def token_obfuscate(token):
758 def token_obfuscate(token):
758 if token:
759 if token:
759 return token[:4] + "****"
760 return token[:4] + "****"
760
761
761 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:
762 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')
763
764
764 log.debug(
765 log.debug(
765 'AuthUser: fill data execution based on: '
766 'AuthUser: fill data execution based on: '
766 '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)
767 try:
768 try:
769 found_with = ''
768 dbuser = None
770 dbuser = None
769 if user_id:
771 if user_id:
770 dbuser = self.get(user_id)
772 dbuser = self.get(user_id)
773 found_with = 'user_id'
771 elif api_key:
774 elif api_key:
772 dbuser = self.get_by_auth_token(api_key)
775 dbuser = self.get_by_auth_token(api_key)
776 found_with = 'auth_token'
773 elif username:
777 elif username:
774 dbuser = self.get_by_username(username)
778 dbuser = self.get_by_username(username)
779 found_with = 'username'
775
780
776 if not dbuser:
781 if not dbuser:
777 log.warning(
782 log.warning(
778 'Unable to lookup user by id:%s api_key:%s username:%s',
783 'Unable to lookup user by id:%s api_key:%s username:%s, found with: %s',
779 user_id, token_obfuscate(api_key), username)
784 user_id, token_obfuscate(api_key), username, found_with)
780 return False
785 return False
781 if not dbuser.active:
786 if not dbuser.active:
782 log.debug('User `%s:%s` is inactive, skipping fill data',
787 log.debug('User `%s:%s` is inactive, skipping fill data',
783 username, user_id)
788 username, user_id)
784 return False
789 return False
785
790
786 log.debug('AuthUser: filling found user:%s data', dbuser)
791 log.debug('AuthUser: filling found user:%s data, found with: %s', dbuser, found_with)
787
792
788 attrs = {
793 attrs = {
789 'user_id': dbuser.user_id,
794 'user_id': dbuser.user_id,
790 'username': dbuser.username,
795 'username': dbuser.username,
791 'name': dbuser.name,
796 'name': dbuser.name,
792 'first_name': dbuser.first_name,
797 'first_name': dbuser.first_name,
793 'firstname': dbuser.firstname,
798 'firstname': dbuser.firstname,
794 'last_name': dbuser.last_name,
799 'last_name': dbuser.last_name,
795 'lastname': dbuser.lastname,
800 'lastname': dbuser.lastname,
796 'admin': dbuser.admin,
801 'admin': dbuser.admin,
797 'active': dbuser.active,
802 'active': dbuser.active,
798
803
799 'email': dbuser.email,
804 'email': dbuser.email,
800 'emails': dbuser.emails_cached(),
805 'emails': dbuser.emails_cached(),
801 'short_contact': dbuser.short_contact,
806 'short_contact': dbuser.short_contact,
802 'full_contact': dbuser.full_contact,
807 'full_contact': dbuser.full_contact,
803 'full_name': dbuser.full_name,
808 'full_name': dbuser.full_name,
804 'full_name_or_username': dbuser.full_name_or_username,
809 'full_name_or_username': dbuser.full_name_or_username,
805
810
806 '_api_key': dbuser._api_key,
811 '_api_key': dbuser._api_key,
807 '_user_data': dbuser._user_data,
812 '_user_data': dbuser._user_data,
808
813
809 'created_on': dbuser.created_on,
814 'created_on': dbuser.created_on,
810 'extern_name': dbuser.extern_name,
815 'extern_name': dbuser.extern_name,
811 'extern_type': dbuser.extern_type,
816 'extern_type': dbuser.extern_type,
812
817
813 'inherit_default_permissions': dbuser.inherit_default_permissions,
818 'inherit_default_permissions': dbuser.inherit_default_permissions,
814
819
815 'language': dbuser.language,
820 'language': dbuser.language,
816 'last_activity': dbuser.last_activity,
821 'last_activity': dbuser.last_activity,
817 'last_login': dbuser.last_login,
822 'last_login': dbuser.last_login,
818 'password': dbuser.password,
823 'password': dbuser.password,
819 }
824 }
820 auth_user.__dict__.update(attrs)
825 auth_user.__dict__.update(attrs)
821 except Exception:
826 except Exception:
822 log.error(traceback.format_exc())
827 log.error(traceback.format_exc())
823 auth_user.is_authenticated = False
828 auth_user.is_authenticated = False
824 return False
829 return False
825
830
826 return True
831 return True
827
832
828 def has_perm(self, user, perm):
833 def has_perm(self, user, perm):
829 perm = self._get_perm(perm)
834 perm = self._get_perm(perm)
830 user = self._get_user(user)
835 user = self._get_user(user)
831
836
832 return UserToPerm.query().filter(UserToPerm.user == user)\
837 return UserToPerm.query().filter(UserToPerm.user == user)\
833 .filter(UserToPerm.permission == perm).scalar() is not None
838 .filter(UserToPerm.permission == perm).scalar() is not None
834
839
835 def grant_perm(self, user, perm):
840 def grant_perm(self, user, perm):
836 """
841 """
837 Grant user global permissions
842 Grant user global permissions
838
843
839 :param user:
844 :param user:
840 :param perm:
845 :param perm:
841 """
846 """
842 user = self._get_user(user)
847 user = self._get_user(user)
843 perm = self._get_perm(perm)
848 perm = self._get_perm(perm)
844 # if this permission is already granted skip it
849 # if this permission is already granted skip it
845 _perm = UserToPerm.query()\
850 _perm = UserToPerm.query()\
846 .filter(UserToPerm.user == user)\
851 .filter(UserToPerm.user == user)\
847 .filter(UserToPerm.permission == perm)\
852 .filter(UserToPerm.permission == perm)\
848 .scalar()
853 .scalar()
849 if _perm:
854 if _perm:
850 return
855 return
851 new = UserToPerm()
856 new = UserToPerm()
852 new.user = user
857 new.user = user
853 new.permission = perm
858 new.permission = perm
854 self.sa.add(new)
859 self.sa.add(new)
855 return new
860 return new
856
861
857 def revoke_perm(self, user, perm):
862 def revoke_perm(self, user, perm):
858 """
863 """
859 Revoke users global permissions
864 Revoke users global permissions
860
865
861 :param user:
866 :param user:
862 :param perm:
867 :param perm:
863 """
868 """
864 user = self._get_user(user)
869 user = self._get_user(user)
865 perm = self._get_perm(perm)
870 perm = self._get_perm(perm)
866
871
867 obj = UserToPerm.query()\
872 obj = UserToPerm.query()\
868 .filter(UserToPerm.user == user)\
873 .filter(UserToPerm.user == user)\
869 .filter(UserToPerm.permission == perm)\
874 .filter(UserToPerm.permission == perm)\
870 .scalar()
875 .scalar()
871 if obj:
876 if obj:
872 self.sa.delete(obj)
877 self.sa.delete(obj)
873
878
874 def add_extra_email(self, user, email):
879 def add_extra_email(self, user, email):
875 """
880 """
876 Adds email address to UserEmailMap
881 Adds email address to UserEmailMap
877
882
878 :param user:
883 :param user:
879 :param email:
884 :param email:
880 """
885 """
881
886
882 user = self._get_user(user)
887 user = self._get_user(user)
883
888
884 obj = UserEmailMap()
889 obj = UserEmailMap()
885 obj.user = user
890 obj.user = user
886 obj.email = email
891 obj.email = email
887 self.sa.add(obj)
892 self.sa.add(obj)
888 return obj
893 return obj
889
894
890 def delete_extra_email(self, user, email_id):
895 def delete_extra_email(self, user, email_id):
891 """
896 """
892 Removes email address from UserEmailMap
897 Removes email address from UserEmailMap
893
898
894 :param user:
899 :param user:
895 :param email_id:
900 :param email_id:
896 """
901 """
897 user = self._get_user(user)
902 user = self._get_user(user)
898 obj = UserEmailMap.query().get(email_id)
903 obj = UserEmailMap.query().get(email_id)
899 if obj and obj.user_id == user.user_id:
904 if obj and obj.user_id == user.user_id:
900 self.sa.delete(obj)
905 self.sa.delete(obj)
901
906
902 def parse_ip_range(self, ip_range):
907 def parse_ip_range(self, ip_range):
903 ip_list = []
908 ip_list = []
904
909
905 def make_unique(value):
910 def make_unique(value):
906 seen = []
911 seen = []
907 return [c for c in value if not (c in seen or seen.append(c))]
912 return [c for c in value if not (c in seen or seen.append(c))]
908
913
909 # firsts split by commas
914 # firsts split by commas
910 for ip_range in ip_range.split(','):
915 for ip_range in ip_range.split(','):
911 if not ip_range:
916 if not ip_range:
912 continue
917 continue
913 ip_range = ip_range.strip()
918 ip_range = ip_range.strip()
914 if '-' in ip_range:
919 if '-' in ip_range:
915 start_ip, end_ip = ip_range.split('-', 1)
920 start_ip, end_ip = ip_range.split('-', 1)
916 start_ip = ipaddress.ip_address(safe_str(start_ip.strip()))
921 start_ip = ipaddress.ip_address(safe_str(start_ip.strip()))
917 end_ip = ipaddress.ip_address(safe_str(end_ip.strip()))
922 end_ip = ipaddress.ip_address(safe_str(end_ip.strip()))
918 parsed_ip_range = []
923 parsed_ip_range = []
919
924
920 for index in range(int(start_ip), int(end_ip) + 1):
925 for index in range(int(start_ip), int(end_ip) + 1):
921 new_ip = ipaddress.ip_address(index)
926 new_ip = ipaddress.ip_address(index)
922 parsed_ip_range.append(str(new_ip))
927 parsed_ip_range.append(str(new_ip))
923 ip_list.extend(parsed_ip_range)
928 ip_list.extend(parsed_ip_range)
924 else:
929 else:
925 ip_list.append(ip_range)
930 ip_list.append(ip_range)
926
931
927 return make_unique(ip_list)
932 return make_unique(ip_list)
928
933
929 def add_extra_ip(self, user, ip, description=None):
934 def add_extra_ip(self, user, ip, description=None):
930 """
935 """
931 Adds ip address to UserIpMap
936 Adds ip address to UserIpMap
932
937
933 :param user:
938 :param user:
934 :param ip:
939 :param ip:
935 """
940 """
936
941
937 user = self._get_user(user)
942 user = self._get_user(user)
938 obj = UserIpMap()
943 obj = UserIpMap()
939 obj.user = user
944 obj.user = user
940 obj.ip_addr = ip
945 obj.ip_addr = ip
941 obj.description = description
946 obj.description = description
942 self.sa.add(obj)
947 self.sa.add(obj)
943 return obj
948 return obj
944
949
945 auth_token_role = AuthTokenModel.cls
950 auth_token_role = AuthTokenModel.cls
946
951
947 def add_auth_token(self, user, lifetime_minutes, role, description='',
952 def add_auth_token(self, user, lifetime_minutes, role, description='',
948 scope_callback=None):
953 scope_callback=None):
949 """
954 """
950 Add AuthToken for user.
955 Add AuthToken for user.
951
956
952 :param user: username/user_id
957 :param user: username/user_id
953 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
958 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
954 :param role: one of AuthTokenModel.cls.ROLE_*
959 :param role: one of AuthTokenModel.cls.ROLE_*
955 :param description: optional string description
960 :param description: optional string description
956 """
961 """
957
962
958 token = AuthTokenModel().create(
963 token = AuthTokenModel().create(
959 user, description, lifetime_minutes, role)
964 user, description, lifetime_minutes, role)
960 if scope_callback and callable(scope_callback):
965 if scope_callback and callable(scope_callback):
961 # call the callback if we provide, used to attach scope for EE edition
966 # call the callback if we provide, used to attach scope for EE edition
962 scope_callback(token)
967 scope_callback(token)
963 return token
968 return token
964
969
965 def delete_extra_ip(self, user, ip_id):
970 def delete_extra_ip(self, user, ip_id):
966 """
971 """
967 Removes ip address from UserIpMap
972 Removes ip address from UserIpMap
968
973
969 :param user:
974 :param user:
970 :param ip_id:
975 :param ip_id:
971 """
976 """
972 user = self._get_user(user)
977 user = self._get_user(user)
973 obj = UserIpMap.query().get(ip_id)
978 obj = UserIpMap.query().get(ip_id)
974 if obj and obj.user_id == user.user_id:
979 if obj and obj.user_id == user.user_id:
975 self.sa.delete(obj)
980 self.sa.delete(obj)
976
981
977 def get_accounts_in_creation_order(self, current_user=None):
982 def get_accounts_in_creation_order(self, current_user=None):
978 """
983 """
979 Get accounts in order of creation for deactivation for license limits
984 Get accounts in order of creation for deactivation for license limits
980
985
981 pick currently logged in user, and append to the list in position 0
986 pick currently logged in user, and append to the list in position 0
982 pick all super-admins in order of creation date and add it to the list
987 pick all super-admins in order of creation date and add it to the list
983 pick all other accounts in order of creation and add it to the list.
988 pick all other accounts in order of creation and add it to the list.
984
989
985 Based on that list, the last accounts can be disabled as they are
990 Based on that list, the last accounts can be disabled as they are
986 created at the end and don't include any of the super admins as well
991 created at the end and don't include any of the super admins as well
987 as the current user.
992 as the current user.
988
993
989 :param current_user: optionally current user running this operation
994 :param current_user: optionally current user running this operation
990 """
995 """
991
996
992 if not current_user:
997 if not current_user:
993 current_user = get_current_rhodecode_user()
998 current_user = get_current_rhodecode_user()
994 active_super_admins = [
999 active_super_admins = [
995 x.user_id for x in User.query()
1000 x.user_id for x in User.query()
996 .filter(User.user_id != current_user.user_id)
1001 .filter(User.user_id != current_user.user_id)
997 .filter(User.active == true())
1002 .filter(User.active == true())
998 .filter(User.admin == true())
1003 .filter(User.admin == true())
999 .order_by(User.created_on.asc())]
1004 .order_by(User.created_on.asc())]
1000
1005
1001 active_regular_users = [
1006 active_regular_users = [
1002 x.user_id for x in User.query()
1007 x.user_id for x in User.query()
1003 .filter(User.user_id != current_user.user_id)
1008 .filter(User.user_id != current_user.user_id)
1004 .filter(User.active == true())
1009 .filter(User.active == true())
1005 .filter(User.admin == false())
1010 .filter(User.admin == false())
1006 .order_by(User.created_on.asc())]
1011 .order_by(User.created_on.asc())]
1007
1012
1008 list_of_accounts = [current_user.user_id]
1013 list_of_accounts = [current_user.user_id]
1009 list_of_accounts += active_super_admins
1014 list_of_accounts += active_super_admins
1010 list_of_accounts += active_regular_users
1015 list_of_accounts += active_regular_users
1011
1016
1012 return list_of_accounts
1017 return list_of_accounts
1013
1018
1014 def deactivate_last_users(self, expected_users, current_user=None):
1019 def deactivate_last_users(self, expected_users, current_user=None):
1015 """
1020 """
1016 Deactivate accounts that are over the license limits.
1021 Deactivate accounts that are over the license limits.
1017 Algorithm of which accounts to disabled is based on the formula:
1022 Algorithm of which accounts to disabled is based on the formula:
1018
1023
1019 Get current user, then super admins in creation order, then regular
1024 Get current user, then super admins in creation order, then regular
1020 active users in creation order.
1025 active users in creation order.
1021
1026
1022 Using that list we mark all accounts from the end of it as inactive.
1027 Using that list we mark all accounts from the end of it as inactive.
1023 This way we block only latest created accounts.
1028 This way we block only latest created accounts.
1024
1029
1025 :param expected_users: list of users in special order, we deactivate
1030 :param expected_users: list of users in special order, we deactivate
1026 the end N amount of users from that list
1031 the end N amount of users from that list
1027 """
1032 """
1028
1033
1029 list_of_accounts = self.get_accounts_in_creation_order(
1034 list_of_accounts = self.get_accounts_in_creation_order(
1030 current_user=current_user)
1035 current_user=current_user)
1031
1036
1032 for acc_id in list_of_accounts[expected_users + 1:]:
1037 for acc_id in list_of_accounts[expected_users + 1:]:
1033 user = User.get(acc_id)
1038 user = User.get(acc_id)
1034 log.info('Deactivating account %s for license unlock', user)
1039 log.info('Deactivating account %s for license unlock', user)
1035 user.active = False
1040 user.active = False
1036 Session().add(user)
1041 Session().add(user)
1037 Session().commit()
1042 Session().commit()
1038
1043
1039 return
1044 return
1040
1045
1041 def get_user_log(self, user, filter_term):
1046 def get_user_log(self, user, filter_term):
1042 user_log = UserLog.query()\
1047 user_log = UserLog.query()\
1043 .filter(or_(UserLog.user_id == user.user_id,
1048 .filter(or_(UserLog.user_id == user.user_id,
1044 UserLog.username == user.username))\
1049 UserLog.username == user.username))\
1045 .options(joinedload(UserLog.user))\
1050 .options(joinedload(UserLog.user))\
1046 .options(joinedload(UserLog.repository))\
1051 .options(joinedload(UserLog.repository))\
1047 .order_by(UserLog.action_date.desc())
1052 .order_by(UserLog.action_date.desc())
1048
1053
1049 user_log = user_log_filter(user_log, filter_term)
1054 user_log = user_log_filter(user_log, filter_term)
1050 return user_log
1055 return user_log
@@ -1,217 +1,217 b''
1 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
1 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
2
2
3 <%def name="form_item(position=None, title=None, redirect_url=None, repo=None, repo_group=None)">
3 <%def name="form_item(position=None, title=None, redirect_url=None, repo=None, repo_group=None)">
4 <tr>
4 <tr>
5 <td class="td-align-top" >
5 <td class="td-align-top" >
6 <div class="label">
6 <div class="label">
7 <label for="position">${_('Position')}:</label>
7 <label for="position">${_('Position')}:</label>
8 </div>
8 </div>
9 <div class="input">
9 <div class="input">
10 <input type="text" name="position" value="${position}" style="width: 40px"/>
10 <input type="text" name="position" value="${position}" style="width: 40px"/>
11 ${h.hidden('cur_position', position)}
11 ${h.hidden('cur_position', position)}
12 </div>
12 </div>
13 </td>
13 </td>
14
14
15 <td>
15 <td>
16 <div class="label">
16 <div class="label">
17 <label for="title">${_('Bookmark title (max 30 characters, optional)')}:</label>
17 <label for="title">${_('Bookmark title (max 30 characters, optional)')}:</label>
18 </div>
18 </div>
19 <div class="input">
19 <div class="input">
20 <input type="text" name="title" value="${title}" style="width: 300px" maxlength="30"/>
20 <input type="text" name="title" value="${title}" style="width: 300px" maxlength="30"/>
21
21
22 <div class="field pull-right">
22 <div class="field pull-right">
23 <div>
23 <div>
24 <label class="btn-link btn-danger">${_('Clear')}:</label>
24 <label class="btn-link btn-danger">${_('Clear')}:</label>
25 ${h.checkbox('remove', value=True)}
25 ${h.checkbox('remove', value=True)}
26 </div>
26 </div>
27 </div>
27 </div>
28 </div>
28 </div>
29
29
30 <div class="label" style="margin-top:10px">
30 <div class="label" style="margin-top:10px">
31 <label for="redirect_url">${_('Redirect URL')}:</label>
31 <label for="redirect_url">${_('Redirect URL')}:</label>
32 </div>
32 </div>
33 <div class="input">
33 <div class="input">
34 <input type="text" name="redirect_url" value="${redirect_url}" style="width: 600px"/>
34 <input type="text" name="redirect_url" value="${redirect_url}" style="width: 600px"/>
35 </div>
35 </div>
36 <p class="help-block help-block-inline">
36 <p class="help-block help-block-inline">
37 ${_('Server URL is available as ${server_url} variable. E.g. Redirect url: ${server_url}/_admin/exception_tracker')}
37 ${_('Server URL is available as ${server_url} variable. E.g. Redirect url: ${server_url}/_admin/exception_tracker')}
38 </p>
38 </p>
39
39
40 <div class="select" style="margin-top:5px">
40 <div class="select" style="margin-top:5px">
41 <div class="label">
41 <div class="label">
42 <label for="redirect_url">${_('Templates')}:</label>
42 <label for="redirect_url">${_('Templates')}:</label>
43 </div>
43 </div>
44
44
45 % if repo:
45 % if repo:
46 ${dt.repo_name(name=repo.repo_name, rtype=repo.repo_type,rstate=None,private=None,archived=False, fork_repo_name=None)}
46 ${dt.repo_name(name=repo.repo_name, rtype=repo.repo_type,rstate=None,private=None,archived=False, fork_repo_name=None)}
47 ${h.hidden('bookmark_repo', repo.repo_id)}
47 ${h.hidden('bookmark_repo', repo.repo_id)}
48 % elif repo_group:
48 % elif repo_group:
49 ${dt.repo_group_name(repo_group.group_name)}
49 ${dt.repo_group_name(repo_group.group_name)}
50 ${h.hidden('bookmark_repo_group', repo_group.group_id)}
50 ${h.hidden('bookmark_repo_group', repo_group.group_id)}
51 % else:
51 % else:
52 <div>
52 <div>
53 ${h.hidden('bookmark_repo', class_='bookmark_repo')}
53 ${h.hidden('bookmark_repo', class_='bookmark_repo')}
54 <p class="help-block help-block-inline">${_('Available as ${repo_url} e.g. Redirect url: ${repo_url}/changelog')}</p>
54 <p class="help-block help-block-inline">${_('Available as ${repo_url} e.g. Redirect url: ${repo_url}/changelog')}</p>
55 </div>
55 </div>
56 <div style="margin-top:5px">
56 <div style="margin-top:5px">
57 ${h.hidden('bookmark_repo_group', class_='bookmark_repo_group')}
57 ${h.hidden('bookmark_repo_group', class_='bookmark_repo_group')}
58 <p class="help-block help-block-inline">${_('Available as ${repo_group_url} e.g. Redirect url: ${repo_group_url}')}</p>
58 <p class="help-block help-block-inline">${_('Available as ${repo_group_url} e.g. Redirect url: ${repo_group_url}')}</p>
59 </div>
59 </div>
60
60
61 % endif
61 % endif
62 </div>
62 </div>
63
63
64 </td>
64 </td>
65
65
66 </tr>
66 </tr>
67 </%def>
67 </%def>
68
68
69 <div class="panel panel-default">
69 <div class="panel panel-default">
70 <div class="panel-heading">
70 <div class="panel-heading">
71 <h3 class="panel-title">${_('Your Bookmarks')}</h3>
71 <h3 class="panel-title">${_('Your Bookmarks')}</h3>
72 </div>
72 </div>
73
73
74 <div class="panel-body">
74 <div class="panel-body">
75 <p>
75 <p>
76 ${_('Store upto 10 bookmark links to favorite repositories, external issue tracker or CI server. ')}
76 ${_('Store upto 10 bookmark links to favorite repositories, external issue tracker or CI server. ')}
77 <br/>
77 <br/>
78 ${_('Bookmarks are accessible from your username dropdown or by keyboard shortcut `g 0-9`')}
78 ${_('Bookmarks are accessible from your username dropdown or by keyboard shortcut `g 0-9`')}
79 </p>
79 </p>
80
80
81 ${h.secure_form(h.route_path('my_account_bookmarks_update'), request=request)}
81 ${h.secure_form(h.route_path('my_account_bookmarks_update'), request=request)}
82 <div class="form-vertical">
82 <div class="form-vertical">
83 <table class="rctable">
83 <table class="rctable">
84 ## generate always 10 entries
84 ## generate always 10 entries
85 <input type="hidden" name="__start__" value="bookmarks:sequence"/>
85 <input type="hidden" name="__start__" value="bookmarks:sequence"/>
86 % for item in (c.bookmark_items + [None for i in range(10)])[:10]:
86 % for item in (c.user_bookmark_items + [None for i in range(10)])[:10]:
87 <input type="hidden" name="__start__" value="bookmark:mapping"/>
87 <input type="hidden" name="__start__" value="bookmark:mapping"/>
88 % if item is None:
88 % if item is None:
89 ## empty placehodlder
89 ## empty placehodlder
90 ${form_item()}
90 ${form_item()}
91 % else:
91 % else:
92 ## actual entry
92 ## actual entry
93 ${form_item(position=item.position, title=item.title, redirect_url=item.redirect_url, repo=item.repository, repo_group=item.repository_group)}
93 ${form_item(position=item[0].position, title=item[0].title, redirect_url=item[0].redirect_url, repo=item[1], repo_group=item[2])}
94 % endif
94 % endif
95 <input type="hidden" name="__end__" value="bookmark:mapping"/>
95 <input type="hidden" name="__end__" value="bookmark:mapping"/>
96 % endfor
96 % endfor
97 <input type="hidden" name="__end__" value="bookmarks:sequence"/>
97 <input type="hidden" name="__end__" value="bookmarks:sequence"/>
98 </table>
98 </table>
99 <div class="buttons">
99 <div class="buttons">
100 ${h.submit('save',_('Save'),class_="btn")}
100 ${h.submit('save',_('Save'),class_="btn")}
101 </div>
101 </div>
102 </div>
102 </div>
103 ${h.end_form()}
103 ${h.end_form()}
104 </div>
104 </div>
105 </div>
105 </div>
106
106
107 <script>
107 <script>
108 $(document).ready(function(){
108 $(document).ready(function(){
109
109
110
110
111 var repoFilter = function (data) {
111 var repoFilter = function (data) {
112 var results = [];
112 var results = [];
113
113
114 if (!data.results[0]) {
114 if (!data.results[0]) {
115 return data
115 return data
116 }
116 }
117
117
118 $.each(data.results[0].children, function () {
118 $.each(data.results[0].children, function () {
119 // replace name to ID for submision
119 // replace name to ID for submision
120 this.id = this.repo_id;
120 this.id = this.repo_id;
121 results.push(this);
121 results.push(this);
122 });
122 });
123
123
124 data.results[0].children = results;
124 data.results[0].children = results;
125 return data;
125 return data;
126 };
126 };
127
127
128
128
129 $(".bookmark_repo").select2({
129 $(".bookmark_repo").select2({
130 cachedDataSource: {},
130 cachedDataSource: {},
131 minimumInputLength: 2,
131 minimumInputLength: 2,
132 placeholder: "${_('repository')}",
132 placeholder: "${_('repository')}",
133 dropdownAutoWidth: true,
133 dropdownAutoWidth: true,
134 containerCssClass: "drop-menu",
134 containerCssClass: "drop-menu",
135 dropdownCssClass: "drop-menu-dropdown",
135 dropdownCssClass: "drop-menu-dropdown",
136 formatResult: formatRepoResult,
136 formatResult: formatRepoResult,
137 query: $.debounce(250, function (query) {
137 query: $.debounce(250, function (query) {
138 self = this;
138 self = this;
139 var cacheKey = query.term;
139 var cacheKey = query.term;
140 var cachedData = self.cachedDataSource[cacheKey];
140 var cachedData = self.cachedDataSource[cacheKey];
141
141
142 if (cachedData) {
142 if (cachedData) {
143 query.callback({results: cachedData.results});
143 query.callback({results: cachedData.results});
144 } else {
144 } else {
145 $.ajax({
145 $.ajax({
146 url: pyroutes.url('repo_list_data'),
146 url: pyroutes.url('repo_list_data'),
147 data: {'query': query.term},
147 data: {'query': query.term},
148 dataType: 'json',
148 dataType: 'json',
149 type: 'GET',
149 type: 'GET',
150 success: function (data) {
150 success: function (data) {
151 data = repoFilter(data);
151 data = repoFilter(data);
152 self.cachedDataSource[cacheKey] = data;
152 self.cachedDataSource[cacheKey] = data;
153 query.callback({results: data.results});
153 query.callback({results: data.results});
154 },
154 },
155 error: function (data, textStatus, errorThrown) {
155 error: function (data, textStatus, errorThrown) {
156 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
156 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
157 }
157 }
158 })
158 })
159 }
159 }
160 }),
160 }),
161 });
161 });
162
162
163 var repoGroupFilter = function (data) {
163 var repoGroupFilter = function (data) {
164 var results = [];
164 var results = [];
165
165
166 if (!data.results[0]) {
166 if (!data.results[0]) {
167 return data
167 return data
168 }
168 }
169
169
170 $.each(data.results[0].children, function () {
170 $.each(data.results[0].children, function () {
171 // replace name to ID for submision
171 // replace name to ID for submision
172 this.id = this.repo_group_id;
172 this.id = this.repo_group_id;
173 results.push(this);
173 results.push(this);
174 });
174 });
175
175
176 data.results[0].children = results;
176 data.results[0].children = results;
177 return data;
177 return data;
178 };
178 };
179
179
180 $(".bookmark_repo_group").select2({
180 $(".bookmark_repo_group").select2({
181 cachedDataSource: {},
181 cachedDataSource: {},
182 minimumInputLength: 2,
182 minimumInputLength: 2,
183 placeholder: "${_('repository group')}",
183 placeholder: "${_('repository group')}",
184 dropdownAutoWidth: true,
184 dropdownAutoWidth: true,
185 containerCssClass: "drop-menu",
185 containerCssClass: "drop-menu",
186 dropdownCssClass: "drop-menu-dropdown",
186 dropdownCssClass: "drop-menu-dropdown",
187 formatResult: formatRepoGroupResult,
187 formatResult: formatRepoGroupResult,
188 query: $.debounce(250, function (query) {
188 query: $.debounce(250, function (query) {
189 self = this;
189 self = this;
190 var cacheKey = query.term;
190 var cacheKey = query.term;
191 var cachedData = self.cachedDataSource[cacheKey];
191 var cachedData = self.cachedDataSource[cacheKey];
192
192
193 if (cachedData) {
193 if (cachedData) {
194 query.callback({results: cachedData.results});
194 query.callback({results: cachedData.results});
195 } else {
195 } else {
196 $.ajax({
196 $.ajax({
197 url: pyroutes.url('repo_group_list_data'),
197 url: pyroutes.url('repo_group_list_data'),
198 data: {'query': query.term},
198 data: {'query': query.term},
199 dataType: 'json',
199 dataType: 'json',
200 type: 'GET',
200 type: 'GET',
201 success: function (data) {
201 success: function (data) {
202 data = repoGroupFilter(data);
202 data = repoGroupFilter(data);
203 self.cachedDataSource[cacheKey] = data;
203 self.cachedDataSource[cacheKey] = data;
204 query.callback({results: data.results});
204 query.callback({results: data.results});
205 },
205 },
206 error: function (data, textStatus, errorThrown) {
206 error: function (data, textStatus, errorThrown) {
207 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
207 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
208 }
208 }
209 })
209 })
210 }
210 }
211 })
211 })
212 });
212 });
213
213
214
214
215 });
215 });
216
216
217 </script>
217 </script>
@@ -1,1263 +1,1263 b''
1
1
2 <%!
2 <%!
3 from rhodecode.lib import html_filters
3 from rhodecode.lib import html_filters
4 %>
4 %>
5
5
6 <%inherit file="root.mako"/>
6 <%inherit file="root.mako"/>
7
7
8 <%include file="/ejs_templates/templates.html"/>
8 <%include file="/ejs_templates/templates.html"/>
9
9
10 <div class="outerwrapper">
10 <div class="outerwrapper">
11 <!-- HEADER -->
11 <!-- HEADER -->
12 <div class="header">
12 <div class="header">
13 <div id="header-inner" class="wrapper">
13 <div id="header-inner" class="wrapper">
14 <div id="logo">
14 <div id="logo">
15 <div class="logo-wrapper">
15 <div class="logo-wrapper">
16 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
16 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
17 </div>
17 </div>
18 % if c.rhodecode_name:
18 % if c.rhodecode_name:
19 <div class="branding">
19 <div class="branding">
20 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
20 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
21 </div>
21 </div>
22 % endif
22 % endif
23 </div>
23 </div>
24 <!-- MENU BAR NAV -->
24 <!-- MENU BAR NAV -->
25 ${self.menu_bar_nav()}
25 ${self.menu_bar_nav()}
26 <!-- END MENU BAR NAV -->
26 <!-- END MENU BAR NAV -->
27 </div>
27 </div>
28 </div>
28 </div>
29 ${self.menu_bar_subnav()}
29 ${self.menu_bar_subnav()}
30 <!-- END HEADER -->
30 <!-- END HEADER -->
31
31
32 <!-- CONTENT -->
32 <!-- CONTENT -->
33 <div id="content" class="wrapper">
33 <div id="content" class="wrapper">
34
34
35 <rhodecode-toast id="notifications"></rhodecode-toast>
35 <rhodecode-toast id="notifications"></rhodecode-toast>
36
36
37 <div class="main">
37 <div class="main">
38 ${next.main()}
38 ${next.main()}
39 </div>
39 </div>
40
40
41 </div>
41 </div>
42 <!-- END CONTENT -->
42 <!-- END CONTENT -->
43
43
44 </div>
44 </div>
45
45
46 <!-- FOOTER -->
46 <!-- FOOTER -->
47 <div id="footer">
47 <div id="footer">
48 <div id="footer-inner" class="title wrapper">
48 <div id="footer-inner" class="title wrapper">
49 <div>
49 <div>
50 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
50 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
51
51
52 <p class="footer-link-right">
52 <p class="footer-link-right">
53 <a class="grey-link-action" href="${h.route_path('home', _query={'showrcid': 1})}">
53 <a class="grey-link-action" href="${h.route_path('home', _query={'showrcid': 1})}">
54 RhodeCode
54 RhodeCode
55 % if c.visual.show_version:
55 % if c.visual.show_version:
56 ${c.rhodecode_version}
56 ${c.rhodecode_version}
57 % endif
57 % endif
58 ${c.rhodecode_edition}
58 ${c.rhodecode_edition}
59 </a> |
59 </a> |
60
60
61 % if c.visual.rhodecode_support_url:
61 % if c.visual.rhodecode_support_url:
62 <a class="grey-link-action" href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a> |
62 <a class="grey-link-action" href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a> |
63 <a class="grey-link-action" href="https://docs.rhodecode.com" target="_blank">${_('Documentation')}</a>
63 <a class="grey-link-action" href="https://docs.rhodecode.com" target="_blank">${_('Documentation')}</a>
64 % endif
64 % endif
65
65
66 </p>
66 </p>
67
67
68 <p class="server-instance" style="display:${sid}">
68 <p class="server-instance" style="display:${sid}">
69 ## display hidden instance ID if specially defined
69 ## display hidden instance ID if specially defined
70 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
70 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
71 % if c.rhodecode_instanceid:
71 % if c.rhodecode_instanceid:
72 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
72 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
73 % endif
73 % endif
74 </p>
74 </p>
75 </div>
75 </div>
76 </div>
76 </div>
77 </div>
77 </div>
78
78
79 <!-- END FOOTER -->
79 <!-- END FOOTER -->
80
80
81 ### MAKO DEFS ###
81 ### MAKO DEFS ###
82
82
83 <%def name="menu_bar_subnav()">
83 <%def name="menu_bar_subnav()">
84 </%def>
84 </%def>
85
85
86 <%def name="breadcrumbs(class_='breadcrumbs')">
86 <%def name="breadcrumbs(class_='breadcrumbs')">
87 <div class="${class_}">
87 <div class="${class_}">
88 ${self.breadcrumbs_links()}
88 ${self.breadcrumbs_links()}
89 </div>
89 </div>
90 </%def>
90 </%def>
91
91
92 <%def name="admin_menu(active=None)">
92 <%def name="admin_menu(active=None)">
93
93
94 <div id="context-bar">
94 <div id="context-bar">
95 <div class="wrapper">
95 <div class="wrapper">
96 <div class="title">
96 <div class="title">
97 <div class="title-content">
97 <div class="title-content">
98 <div class="title-main">
98 <div class="title-main">
99 % if c.is_super_admin:
99 % if c.is_super_admin:
100 ${_('Super-admin Panel')}
100 ${_('Super-admin Panel')}
101 % else:
101 % else:
102 ${_('Delegated Admin Panel')}
102 ${_('Delegated Admin Panel')}
103 % endif
103 % endif
104 </div>
104 </div>
105 </div>
105 </div>
106 </div>
106 </div>
107
107
108 <ul id="context-pages" class="navigation horizontal-list">
108 <ul id="context-pages" class="navigation horizontal-list">
109
109
110 ## super-admin case (Top Menu)
110 ## super-admin case (Top Menu)
111 % if c.is_super_admin:
111 % if c.is_super_admin:
112 <li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
112 <li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
113 <li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
113 <li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
114 <li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
114 <li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
115 <li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
115 <li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
116 <li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
116 <li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
117 <li class="${h.is_active('artifacts', active)}"><a href="${h.route_path('admin_artifacts')}">${_('Artifacts')}</a></li>
117 <li class="${h.is_active('artifacts', active)}"><a href="${h.route_path('admin_artifacts')}">${_('Artifacts')}</a></li>
118 <li class="${h.is_active('automation', active)}"><a href="${h.route_path('admin_automation')}">${_('Automation')}</a></li>
118 <li class="${h.is_active('automation', active)}"><a href="${h.route_path('admin_automation')}">${_('Automation')}</a></li>
119 <li class="${h.is_active('scheduler', active)}"><a href="${h.route_path('admin_scheduler')}">${_('Scheduler')}</a></li>
119 <li class="${h.is_active('scheduler', active)}"><a href="${h.route_path('admin_scheduler')}">${_('Scheduler')}</a></li>
120 <li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
120 <li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
121 <li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
121 <li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
122 <li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
122 <li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
123 <li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
123 <li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
124 <li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
124 <li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
125
125
126 ## delegated admin
126 ## delegated admin
127 % elif c.is_delegated_admin:
127 % elif c.is_delegated_admin:
128 <%
128 <%
129 repositories=c.auth_user.repositories_admin or c.can_create_repo
129 repositories=c.auth_user.repositories_admin or c.can_create_repo
130 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
130 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
131 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
131 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
132 %>
132 %>
133
133
134 %if repositories:
134 %if repositories:
135 <li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
135 <li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
136 %endif
136 %endif
137 %if repository_groups:
137 %if repository_groups:
138 <li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
138 <li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
139 %endif
139 %endif
140 %if user_groups:
140 %if user_groups:
141 <li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
141 <li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
142 %endif
142 %endif
143 % endif
143 % endif
144 </ul>
144 </ul>
145
145
146 </div>
146 </div>
147 <div class="clear"></div>
147 <div class="clear"></div>
148 </div>
148 </div>
149 </%def>
149 </%def>
150
150
151 <%def name="dt_info_panel(elements)">
151 <%def name="dt_info_panel(elements)">
152 <dl class="dl-horizontal">
152 <dl class="dl-horizontal">
153 %for dt, dd, title, show_items in elements:
153 %for dt, dd, title, show_items in elements:
154 <dt>${dt}:</dt>
154 <dt>${dt}:</dt>
155 <dd title="${h.tooltip(title)}">
155 <dd title="${h.tooltip(title)}">
156 %if callable(dd):
156 %if callable(dd):
157 ## allow lazy evaluation of elements
157 ## allow lazy evaluation of elements
158 ${dd()}
158 ${dd()}
159 %else:
159 %else:
160 ${dd}
160 ${dd}
161 %endif
161 %endif
162 %if show_items:
162 %if show_items:
163 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
163 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
164 %endif
164 %endif
165 </dd>
165 </dd>
166
166
167 %if show_items:
167 %if show_items:
168 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
168 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
169 %for item in show_items:
169 %for item in show_items:
170 <dt></dt>
170 <dt></dt>
171 <dd>${item}</dd>
171 <dd>${item}</dd>
172 %endfor
172 %endfor
173 </div>
173 </div>
174 %endif
174 %endif
175
175
176 %endfor
176 %endfor
177 </dl>
177 </dl>
178 </%def>
178 </%def>
179
179
180 <%def name="tr_info_entry(element)">
180 <%def name="tr_info_entry(element)">
181 <% key, val, title, show_items = element %>
181 <% key, val, title, show_items = element %>
182
182
183 <tr>
183 <tr>
184 <td style="vertical-align: top">${key}</td>
184 <td style="vertical-align: top">${key}</td>
185 <td title="${h.tooltip(title)}">
185 <td title="${h.tooltip(title)}">
186 %if callable(val):
186 %if callable(val):
187 ## allow lazy evaluation of elements
187 ## allow lazy evaluation of elements
188 ${val()}
188 ${val()}
189 %else:
189 %else:
190 ${val}
190 ${val}
191 %endif
191 %endif
192 %if show_items:
192 %if show_items:
193 <div class="collapsable-content" data-toggle="item-${h.md5_safe(h.safe_str(val))[:6]}-details" style="display: none">
193 <div class="collapsable-content" data-toggle="item-${h.md5_safe(h.safe_str(val))[:6]}-details" style="display: none">
194 % for item in show_items:
194 % for item in show_items:
195 <dt></dt>
195 <dt></dt>
196 <dd>${item}</dd>
196 <dd>${item}</dd>
197 % endfor
197 % endfor
198 </div>
198 </div>
199 %endif
199 %endif
200 </td>
200 </td>
201 <td style="vertical-align: top">
201 <td style="vertical-align: top">
202 %if show_items:
202 %if show_items:
203 <span class="btn-collapse" data-toggle="item-${h.md5_safe(h.safe_str(val))[:6]}-details">${_('Show More')} </span>
203 <span class="btn-collapse" data-toggle="item-${h.md5_safe(h.safe_str(val))[:6]}-details">${_('Show More')} </span>
204 %endif
204 %endif
205 </td>
205 </td>
206 </tr>
206 </tr>
207
207
208 </%def>
208 </%def>
209
209
210 <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None, extra_class=None)">
210 <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None, extra_class=None)">
211 <%
211 <%
212 if size > 16:
212 if size > 16:
213 gravatar_class = ['gravatar','gravatar-large']
213 gravatar_class = ['gravatar','gravatar-large']
214 else:
214 else:
215 gravatar_class = ['gravatar']
215 gravatar_class = ['gravatar']
216
216
217 data_hovercard_url = ''
217 data_hovercard_url = ''
218 data_hovercard_alt = tooltip_alt.replace('<', '&lt;').replace('>', '&gt;') if tooltip_alt else ''
218 data_hovercard_alt = tooltip_alt.replace('<', '&lt;').replace('>', '&gt;') if tooltip_alt else ''
219
219
220 if tooltip:
220 if tooltip:
221 gravatar_class += ['tooltip-hovercard']
221 gravatar_class += ['tooltip-hovercard']
222 if extra_class:
222 if extra_class:
223 gravatar_class += extra_class
223 gravatar_class += extra_class
224 if tooltip and user:
224 if tooltip and user:
225 if user.username == h.DEFAULT_USER:
225 if user.username == h.DEFAULT_USER:
226 gravatar_class.pop(-1)
226 gravatar_class.pop(-1)
227 else:
227 else:
228 data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', ''))
228 data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', ''))
229 gravatar_class = ' '.join(gravatar_class)
229 gravatar_class = ' '.join(gravatar_class)
230
230
231 %>
231 %>
232 <%doc>
232 <%doc>
233 TODO: johbo: For now we serve double size images to make it smooth
233 TODO: johbo: For now we serve double size images to make it smooth
234 for retina. This is how it worked until now. Should be replaced
234 for retina. This is how it worked until now. Should be replaced
235 with a better solution at some point.
235 with a better solution at some point.
236 </%doc>
236 </%doc>
237
237
238 <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2, request=request)}" />
238 <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2, request=request)}" />
239 </%def>
239 </%def>
240
240
241
241
242 <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False, _class='rc-user')">
242 <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False, _class='rc-user')">
243 <%
243 <%
244 email = h.email_or_none(contact)
244 email = h.email_or_none(contact)
245 rc_user = h.discover_user(contact)
245 rc_user = h.discover_user(contact)
246 %>
246 %>
247
247
248 <div class="${_class}">
248 <div class="${_class}">
249 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
249 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
250 <span class="${('user user-disabled' if show_disabled else 'user')}">
250 <span class="${('user user-disabled' if show_disabled else 'user')}">
251 ${h.link_to_user(rc_user or contact)}
251 ${h.link_to_user(rc_user or contact)}
252 </span>
252 </span>
253 </div>
253 </div>
254 </%def>
254 </%def>
255
255
256
256
257 <%def name="user_group_icon(user_group=None, size=16, tooltip=False)">
257 <%def name="user_group_icon(user_group=None, size=16, tooltip=False)">
258 <%
258 <%
259 if (size > 16):
259 if (size > 16):
260 gravatar_class = 'icon-user-group-alt'
260 gravatar_class = 'icon-user-group-alt'
261 else:
261 else:
262 gravatar_class = 'icon-user-group-alt'
262 gravatar_class = 'icon-user-group-alt'
263
263
264 if tooltip:
264 if tooltip:
265 gravatar_class += ' tooltip-hovercard'
265 gravatar_class += ' tooltip-hovercard'
266
266
267 data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id)
267 data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id)
268 %>
268 %>
269 <%doc>
269 <%doc>
270 TODO: johbo: For now we serve double size images to make it smooth
270 TODO: johbo: For now we serve double size images to make it smooth
271 for retina. This is how it worked until now. Should be replaced
271 for retina. This is how it worked until now. Should be replaced
272 with a better solution at some point.
272 with a better solution at some point.
273 </%doc>
273 </%doc>
274
274
275 <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i>
275 <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i>
276 </%def>
276 </%def>
277
277
278 <%def name="repo_page_title(repo_instance)">
278 <%def name="repo_page_title(repo_instance)">
279 <div class="title-content repo-title">
279 <div class="title-content repo-title">
280
280
281 <div class="title-main">
281 <div class="title-main">
282 ## SVN/HG/GIT icons
282 ## SVN/HG/GIT icons
283 %if h.is_hg(repo_instance):
283 %if h.is_hg(repo_instance):
284 <i class="icon-hg"></i>
284 <i class="icon-hg"></i>
285 %endif
285 %endif
286 %if h.is_git(repo_instance):
286 %if h.is_git(repo_instance):
287 <i class="icon-git"></i>
287 <i class="icon-git"></i>
288 %endif
288 %endif
289 %if h.is_svn(repo_instance):
289 %if h.is_svn(repo_instance):
290 <i class="icon-svn"></i>
290 <i class="icon-svn"></i>
291 %endif
291 %endif
292
292
293 ## public/private
293 ## public/private
294 %if repo_instance.private:
294 %if repo_instance.private:
295 <i class="icon-repo-private"></i>
295 <i class="icon-repo-private"></i>
296 %else:
296 %else:
297 <i class="icon-repo-public"></i>
297 <i class="icon-repo-public"></i>
298 %endif
298 %endif
299
299
300 ## repo name with group name
300 ## repo name with group name
301 ${h.breadcrumb_repo_link(repo_instance)}
301 ${h.breadcrumb_repo_link(repo_instance)}
302
302
303 ## Context Actions
303 ## Context Actions
304 <div class="pull-right">
304 <div class="pull-right">
305 %if c.rhodecode_user.username != h.DEFAULT_USER:
305 %if c.rhodecode_user.username != h.DEFAULT_USER:
306 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
306 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
307
307
308 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
308 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
309 % if c.repository_is_user_following:
309 % if c.repository_is_user_following:
310 <i class="icon-eye-off"></i>${_('Unwatch')}
310 <i class="icon-eye-off"></i>${_('Unwatch')}
311 % else:
311 % else:
312 <i class="icon-eye"></i>${_('Watch')}
312 <i class="icon-eye"></i>${_('Watch')}
313 % endif
313 % endif
314
314
315 </a>
315 </a>
316 %else:
316 %else:
317 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
317 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
318 %endif
318 %endif
319 </div>
319 </div>
320
320
321 </div>
321 </div>
322
322
323 ## FORKED
323 ## FORKED
324 %if repo_instance.fork:
324 %if repo_instance.fork:
325 <p class="discreet">
325 <p class="discreet">
326 <i class="icon-code-fork"></i> ${_('Fork of')}
326 <i class="icon-code-fork"></i> ${_('Fork of')}
327 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
327 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
328 </p>
328 </p>
329 %endif
329 %endif
330
330
331 ## IMPORTED FROM REMOTE
331 ## IMPORTED FROM REMOTE
332 %if repo_instance.clone_uri:
332 %if repo_instance.clone_uri:
333 <p class="discreet">
333 <p class="discreet">
334 <i class="icon-code-fork"></i> ${_('Clone from')}
334 <i class="icon-code-fork"></i> ${_('Clone from')}
335 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
335 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
336 </p>
336 </p>
337 %endif
337 %endif
338
338
339 ## LOCKING STATUS
339 ## LOCKING STATUS
340 %if repo_instance.locked[0]:
340 %if repo_instance.locked[0]:
341 <p class="locking_locked discreet">
341 <p class="locking_locked discreet">
342 <i class="icon-repo-lock"></i>
342 <i class="icon-repo-lock"></i>
343 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
343 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
344 </p>
344 </p>
345 %elif repo_instance.enable_locking:
345 %elif repo_instance.enable_locking:
346 <p class="locking_unlocked discreet">
346 <p class="locking_unlocked discreet">
347 ${_('Repository not locked. Pull repository to lock it.')}
347 ${_('Repository not locked. Pull repository to lock it.')}
348 </p>
348 </p>
349 %endif
349 %endif
350
350
351 </div>
351 </div>
352 </%def>
352 </%def>
353
353
354 <%def name="repo_menu(active=None)">
354 <%def name="repo_menu(active=None)">
355 <%
355 <%
356 ## determine if we have "any" option available
356 ## determine if we have "any" option available
357 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
357 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
358 has_actions = can_lock
358 has_actions = can_lock
359
359
360 %>
360 %>
361 % if c.rhodecode_db_repo.archived:
361 % if c.rhodecode_db_repo.archived:
362 <div class="alert alert-warning text-center">
362 <div class="alert alert-warning text-center">
363 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
363 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
364 </div>
364 </div>
365 % endif
365 % endif
366
366
367 <!--- REPO CONTEXT BAR -->
367 <!--- REPO CONTEXT BAR -->
368 <div id="context-bar">
368 <div id="context-bar">
369 <div class="wrapper">
369 <div class="wrapper">
370
370
371 <div class="title">
371 <div class="title">
372 ${self.repo_page_title(c.rhodecode_db_repo)}
372 ${self.repo_page_title(c.rhodecode_db_repo)}
373 </div>
373 </div>
374
374
375 <ul id="context-pages" class="navigation horizontal-list">
375 <ul id="context-pages" class="navigation horizontal-list">
376 <li class="${h.is_active('summary', active)}"><a class="menulink" href="${h.route_path('repo_summary_explicit', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
376 <li class="${h.is_active('summary', active)}"><a class="menulink" href="${h.route_path('repo_summary_explicit', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
377 <li class="${h.is_active('commits', active)}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
377 <li class="${h.is_active('commits', active)}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
378 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.repo_files_by_ref_url(c.repo_name, c.rhodecode_db_repo.repo_type, f_path='', ref_name=c.rhodecode_db_repo.landing_ref_name, commit_id='tip', query={'at':c.rhodecode_db_repo.landing_ref_name})}"><div class="menulabel">${_('Files')}</div></a></li>
378 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.repo_files_by_ref_url(c.repo_name, c.rhodecode_db_repo.repo_type, f_path='', ref_name=c.rhodecode_db_repo.landing_ref_name, commit_id='tip', query={'at':c.rhodecode_db_repo.landing_ref_name})}"><div class="menulabel">${_('Files')}</div></a></li>
379 <li class="${h.is_active('compare', active)}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
379 <li class="${h.is_active('compare', active)}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
380
380
381 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
381 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
382 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
382 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
383 <li class="${h.is_active('showpullrequest', active)}">
383 <li class="${h.is_active('showpullrequest', active)}">
384 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
384 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
385 <div class="menulabel">
385 <div class="menulabel">
386 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
386 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
387 </div>
387 </div>
388 </a>
388 </a>
389 </li>
389 </li>
390 %endif
390 %endif
391
391
392 <li class="${h.is_active('artifacts', active)}">
392 <li class="${h.is_active('artifacts', active)}">
393 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
393 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
394 <div class="menulabel">
394 <div class="menulabel">
395 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
395 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
396 </div>
396 </div>
397 </a>
397 </a>
398 </li>
398 </li>
399
399
400 %if not c.rhodecode_db_repo.archived and h.HasRepoPermissionAll('repository.admin')(c.repo_name):
400 %if not c.rhodecode_db_repo.archived and h.HasRepoPermissionAll('repository.admin')(c.repo_name):
401 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
401 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
402 %endif
402 %endif
403
403
404 <li class="${h.is_active('options', active)}">
404 <li class="${h.is_active('options', active)}">
405 % if has_actions:
405 % if has_actions:
406 <a class="menulink dropdown">
406 <a class="menulink dropdown">
407 <div class="menulabel">${_('Quick Actions')}<div class="show_more"></div></div>
407 <div class="menulabel">${_('Quick Actions')}<div class="show_more"></div></div>
408 </a>
408 </a>
409 <ul class="submenu">
409 <ul class="submenu">
410 %if can_lock:
410 %if can_lock:
411 %if c.rhodecode_db_repo.locked[0]:
411 %if c.rhodecode_db_repo.locked[0]:
412 <li><a class="locking_del" href="${h.route_path('repo_settings_quick_actions',repo_name=c.repo_name, _query={'action': 'toggle-lock', 'set_unlock': 1})}">${_('Unlock Repository')}</a></li>
412 <li><a class="locking_del" href="${h.route_path('repo_settings_quick_actions',repo_name=c.repo_name, _query={'action': 'toggle-lock', 'set_unlock': 1})}">${_('Unlock Repository')}</a></li>
413 %else:
413 %else:
414 <li><a class="locking_add" href="${h.route_path('repo_settings_quick_actions',repo_name=c.repo_name, _query={'action': 'toggle-lock', 'set_lock': 1})}">${_('Lock Repository')}</a></li>
414 <li><a class="locking_add" href="${h.route_path('repo_settings_quick_actions',repo_name=c.repo_name, _query={'action': 'toggle-lock', 'set_lock': 1})}">${_('Lock Repository')}</a></li>
415 %endif
415 %endif
416 %endif
416 %endif
417 </ul>
417 </ul>
418 % endif
418 % endif
419 </li>
419 </li>
420
420
421 </ul>
421 </ul>
422 </div>
422 </div>
423 <div class="clear"></div>
423 <div class="clear"></div>
424 </div>
424 </div>
425
425
426 <!--- REPO END CONTEXT BAR -->
426 <!--- REPO END CONTEXT BAR -->
427
427
428 </%def>
428 </%def>
429
429
430 <%def name="repo_group_page_title(repo_group_instance)">
430 <%def name="repo_group_page_title(repo_group_instance)">
431 <div class="title-content">
431 <div class="title-content">
432 <div class="title-main">
432 <div class="title-main">
433 ## Repository Group icon
433 ## Repository Group icon
434 <i class="icon-repo-group"></i>
434 <i class="icon-repo-group"></i>
435
435
436 ## repo name with group name
436 ## repo name with group name
437 ${h.breadcrumb_repo_group_link(repo_group_instance)}
437 ${h.breadcrumb_repo_group_link(repo_group_instance)}
438 </div>
438 </div>
439
439
440 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
440 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
441 <div class="repo-group-desc discreet">
441 <div class="repo-group-desc discreet">
442 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
442 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
443 </div>
443 </div>
444
444
445 </div>
445 </div>
446 </%def>
446 </%def>
447
447
448
448
449 <%def name="repo_group_menu(active=None)">
449 <%def name="repo_group_menu(active=None)">
450 <%
450 <%
451 gr_name = c.repo_group.group_name if c.repo_group else None
451 gr_name = c.repo_group.group_name if c.repo_group else None
452 # create repositories with write permission on group is set to true
452 # create repositories with write permission on group is set to true
453 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
453 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
454
454
455 %>
455 %>
456
456
457
457
458 <!--- REPO GROUP CONTEXT BAR -->
458 <!--- REPO GROUP CONTEXT BAR -->
459 <div id="context-bar">
459 <div id="context-bar">
460 <div class="wrapper">
460 <div class="wrapper">
461 <div class="title">
461 <div class="title">
462 ${self.repo_group_page_title(c.repo_group)}
462 ${self.repo_group_page_title(c.repo_group)}
463 </div>
463 </div>
464
464
465 <ul id="context-pages" class="navigation horizontal-list">
465 <ul id="context-pages" class="navigation horizontal-list">
466 <li class="${h.is_active('home', active)}">
466 <li class="${h.is_active('home', active)}">
467 <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a>
467 <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a>
468 </li>
468 </li>
469 % if c.is_super_admin or group_admin:
469 % if c.is_super_admin or group_admin:
470 <li class="${h.is_active('settings', active)}">
470 <li class="${h.is_active('settings', active)}">
471 <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
471 <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
472 </li>
472 </li>
473 % endif
473 % endif
474
474
475 </ul>
475 </ul>
476 </div>
476 </div>
477 <div class="clear"></div>
477 <div class="clear"></div>
478 </div>
478 </div>
479
479
480 <!--- REPO GROUP CONTEXT BAR -->
480 <!--- REPO GROUP CONTEXT BAR -->
481
481
482 </%def>
482 </%def>
483
483
484
484
485 <%def name="usermenu(active=False)">
485 <%def name="usermenu(active=False)">
486 <%
486 <%
487 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
487 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
488
488
489 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
489 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
490 # create repositories with write permission on group is set to true
490 # create repositories with write permission on group is set to true
491
491
492 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
492 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
493 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
493 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
494 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
494 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
495 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
495 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
496
496
497 can_create_repos = c.is_super_admin or c.can_create_repo
497 can_create_repos = c.is_super_admin or c.can_create_repo
498 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
498 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
499
499
500 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
500 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
501 can_create_repo_groups_in_group = c.is_super_admin or group_admin
501 can_create_repo_groups_in_group = c.is_super_admin or group_admin
502 %>
502 %>
503
503
504 % if not_anonymous:
504 % if not_anonymous:
505 <%
505 <%
506 default_target_group = dict()
506 default_target_group = dict()
507 if c.rhodecode_user.personal_repo_group:
507 if c.rhodecode_user.personal_repo_group:
508 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
508 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
509 %>
509 %>
510
510
511 ## create action
511 ## create action
512 <li>
512 <li>
513 <a href="#create-actions" onclick="return false;" class="menulink childs">
513 <a href="#create-actions" onclick="return false;" class="menulink childs">
514 <i class="icon-plus-circled"></i>
514 <i class="icon-plus-circled"></i>
515 </a>
515 </a>
516
516
517 <div class="action-menu submenu">
517 <div class="action-menu submenu">
518
518
519 <ol>
519 <ol>
520 ## scope of within a repository
520 ## scope of within a repository
521 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
521 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
522 <li class="submenu-title">${_('This Repository')}</li>
522 <li class="submenu-title">${_('This Repository')}</li>
523 <li>
523 <li>
524 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
524 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
525 </li>
525 </li>
526 % if can_fork:
526 % if can_fork:
527 <li>
527 <li>
528 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
528 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
529 </li>
529 </li>
530 % endif
530 % endif
531 % endif
531 % endif
532
532
533 ## scope of within repository groups
533 ## scope of within repository groups
534 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
534 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
535 <li class="submenu-title">${_('This Repository Group')}</li>
535 <li class="submenu-title">${_('This Repository Group')}</li>
536
536
537 % if can_create_repos_in_group:
537 % if can_create_repos_in_group:
538 <li>
538 <li>
539 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository')}</a>
539 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository')}</a>
540 </li>
540 </li>
541 % endif
541 % endif
542
542
543 % if can_create_repo_groups_in_group:
543 % if can_create_repo_groups_in_group:
544 <li>
544 <li>
545 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository Group')}</a>
545 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository Group')}</a>
546 </li>
546 </li>
547 % endif
547 % endif
548 % endif
548 % endif
549
549
550 ## personal group
550 ## personal group
551 % if c.rhodecode_user.personal_repo_group:
551 % if c.rhodecode_user.personal_repo_group:
552 <li class="submenu-title">Personal Group</li>
552 <li class="submenu-title">Personal Group</li>
553
553
554 <li>
554 <li>
555 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
555 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
556 </li>
556 </li>
557
557
558 <li>
558 <li>
559 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
559 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
560 </li>
560 </li>
561 % endif
561 % endif
562
562
563 ## Global actions
563 ## Global actions
564 <li class="submenu-title">RhodeCode</li>
564 <li class="submenu-title">RhodeCode</li>
565 % if can_create_repos:
565 % if can_create_repos:
566 <li>
566 <li>
567 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
567 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
568 </li>
568 </li>
569 % endif
569 % endif
570
570
571 % if can_create_repo_groups:
571 % if can_create_repo_groups:
572 <li>
572 <li>
573 <a href="${h.route_path('repo_group_new')}" >${_('New Repository Group')}</a>
573 <a href="${h.route_path('repo_group_new')}" >${_('New Repository Group')}</a>
574 </li>
574 </li>
575 % endif
575 % endif
576
576
577 <li>
577 <li>
578 <a href="${h.route_path('gists_new')}">${_('New Gist')}</a>
578 <a href="${h.route_path('gists_new')}">${_('New Gist')}</a>
579 </li>
579 </li>
580
580
581 </ol>
581 </ol>
582
582
583 </div>
583 </div>
584 </li>
584 </li>
585
585
586 ## notifications
586 ## notifications
587 <li>
587 <li>
588 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
588 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
589 ${c.unread_notifications}
589 ${c.unread_notifications}
590 </a>
590 </a>
591 </li>
591 </li>
592 % endif
592 % endif
593
593
594 ## USER MENU
594 ## USER MENU
595 <li id="quick_login_li" class="${'active' if active else ''}">
595 <li id="quick_login_li" class="${'active' if active else ''}">
596 % if c.rhodecode_user.username == h.DEFAULT_USER:
596 % if c.rhodecode_user.username == h.DEFAULT_USER:
597 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
597 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
598 ${gravatar(c.rhodecode_user.email, 20)}
598 ${gravatar(c.rhodecode_user.email, 20)}
599 <span class="user">
599 <span class="user">
600 <span>${_('Sign in')}</span>
600 <span>${_('Sign in')}</span>
601 </span>
601 </span>
602 </a>
602 </a>
603 % else:
603 % else:
604 ## logged in user
604 ## logged in user
605 <a id="quick_login_link" class="menulink childs">
605 <a id="quick_login_link" class="menulink childs">
606 ${gravatar(c.rhodecode_user.email, 20)}
606 ${gravatar(c.rhodecode_user.email, 20)}
607 <span class="user">
607 <span class="user">
608 <span class="menu_link_user">${c.rhodecode_user.username}</span>
608 <span class="menu_link_user">${c.rhodecode_user.username}</span>
609 <div class="show_more"></div>
609 <div class="show_more"></div>
610 </span>
610 </span>
611 </a>
611 </a>
612 ## subnav with menu for logged in user
612 ## subnav with menu for logged in user
613 <div class="user-menu submenu">
613 <div class="user-menu submenu">
614 <div id="quick_login">
614 <div id="quick_login">
615 %if c.rhodecode_user.username != h.DEFAULT_USER:
615 %if c.rhodecode_user.username != h.DEFAULT_USER:
616 <div class="">
616 <div class="">
617 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
617 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
618 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
618 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
619 <div class="email">${c.rhodecode_user.email}</div>
619 <div class="email">${c.rhodecode_user.email}</div>
620 </div>
620 </div>
621 <div class="">
621 <div class="">
622 <ol class="links">
622 <ol class="links">
623 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
623 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
624 % if c.rhodecode_user.personal_repo_group:
624 % if c.rhodecode_user.personal_repo_group:
625 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
625 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
626 % endif
626 % endif
627 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
627 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
628
628
629 % if c.debug_style:
629 % if c.debug_style:
630 <li>
630 <li>
631 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
631 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
632 <div class="menulabel">${_('[Style]')}</div>
632 <div class="menulabel">${_('[Style]')}</div>
633 </a>
633 </a>
634 </li>
634 </li>
635 % endif
635 % endif
636
636
637 ## bookmark-items
637 ## bookmark-items
638 <li class="bookmark-items">
638 <li class="bookmark-items">
639 ${_('Bookmarks')}
639 ${_('Bookmarks')}
640 <div class="pull-right">
640 <div class="pull-right">
641 <a href="${h.route_path('my_account_bookmarks')}">
641 <a href="${h.route_path('my_account_bookmarks')}">
642
642
643 <i class="icon-cog"></i>
643 <i class="icon-cog"></i>
644 </a>
644 </a>
645 </div>
645 </div>
646 </li>
646 </li>
647 % if not c.bookmark_items:
647 % if not c.bookmark_items:
648 <li>
648 <li>
649 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
649 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
650 </li>
650 </li>
651 % endif
651 % endif
652 % for item in c.bookmark_items:
652 % for item in c.bookmark_items:
653 <li>
653 <li>
654 % if item.repository:
654 % if item.repo_id:
655 <div>
655 <div>
656 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
656 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
657 <code>${item.position}</code>
657 <code>${item.position}</code>
658 % if item.repository.repo_type == 'hg':
658 % if item.repo_type == 'hg':
659 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
659 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
660 % elif item.repository.repo_type == 'git':
660 % elif item.repo_type == 'git':
661 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
661 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
662 % elif item.repository.repo_type == 'svn':
662 % elif item.repo_type == 'svn':
663 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
663 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
664 % endif
664 % endif
665 ${(item.title or h.shorter(item.repository.repo_name, 30))}
665 ${(item.title or h.shorter(item.repo_name, 30))}
666 </a>
666 </a>
667 </div>
667 </div>
668 % elif item.repository_group:
668 % elif item.group_id:
669 <div>
669 <div>
670 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
670 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
671 <code>${item.position}</code>
671 <code>${item.position}</code>
672 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
672 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
673 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
673 ${(item.title or h.shorter(item.group_name, 30))}
674 </a>
674 </a>
675 </div>
675 </div>
676 % else:
676 % else:
677 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
677 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
678 <code>${item.position}</code>
678 <code>${item.position}</code>
679 ${item.title}
679 ${item.title}
680 </a>
680 </a>
681 % endif
681 % endif
682 </li>
682 </li>
683 % endfor
683 % endfor
684
684
685 <li class="logout">
685 <li class="logout">
686 ${h.secure_form(h.route_path('logout'), request=request)}
686 ${h.secure_form(h.route_path('logout'), request=request)}
687 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
687 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
688 ${h.end_form()}
688 ${h.end_form()}
689 </li>
689 </li>
690 </ol>
690 </ol>
691 </div>
691 </div>
692 %endif
692 %endif
693 </div>
693 </div>
694 </div>
694 </div>
695
695
696 % endif
696 % endif
697 </li>
697 </li>
698 </%def>
698 </%def>
699
699
700 <%def name="menu_items(active=None)">
700 <%def name="menu_items(active=None)">
701 <%
701 <%
702 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
702 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
703 notice_display = 'none' if len(notice_messages) == 0 else ''
703 notice_display = 'none' if len(notice_messages) == 0 else ''
704 %>
704 %>
705
705
706 <ul id="quick" class="main_nav navigation horizontal-list">
706 <ul id="quick" class="main_nav navigation horizontal-list">
707 ## notice box for important system messages
707 ## notice box for important system messages
708 <li style="display: ${notice_display}">
708 <li style="display: ${notice_display}">
709 <a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false">
709 <a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false">
710 <div class="menulabel-notice ${notice_level}" >
710 <div class="menulabel-notice ${notice_level}" >
711 ${len(notice_messages)}
711 ${len(notice_messages)}
712 </div>
712 </div>
713 </a>
713 </a>
714 </li>
714 </li>
715 <div class="notice-messages-container" style="display: none">
715 <div class="notice-messages-container" style="display: none">
716 <div class="notice-messages">
716 <div class="notice-messages">
717 <table class="rctable">
717 <table class="rctable">
718 % for notice in notice_messages:
718 % for notice in notice_messages:
719 <tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}">
719 <tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}">
720 <td style="vertical-align: text-top; width: 20px">
720 <td style="vertical-align: text-top; width: 20px">
721 <i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i>
721 <i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i>
722 </td>
722 </td>
723 <td>
723 <td>
724 <span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span>
724 <span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span>
725 ${notice['subject']}
725 ${notice['subject']}
726
726
727 <div id="notice-${notice['msg_id']}" style="display: none">
727 <div id="notice-${notice['msg_id']}" style="display: none">
728 ${h.render(notice['body'], renderer='markdown')}
728 ${h.render(notice['body'], renderer='markdown')}
729 </div>
729 </div>
730 </td>
730 </td>
731 <td style="vertical-align: text-top; width: 35px;">
731 <td style="vertical-align: text-top; width: 35px;">
732 <a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false">
732 <a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false">
733 <i class="icon-remove icon-filled-red"></i>
733 <i class="icon-remove icon-filled-red"></i>
734 </a>
734 </a>
735 </td>
735 </td>
736 </tr>
736 </tr>
737
737
738 % endfor
738 % endfor
739 </table>
739 </table>
740 </div>
740 </div>
741 </div>
741 </div>
742 ## Main filter
742 ## Main filter
743 <li>
743 <li>
744 <div class="menulabel main_filter_box">
744 <div class="menulabel main_filter_box">
745 <div class="main_filter_input_box">
745 <div class="main_filter_input_box">
746 <ul class="searchItems">
746 <ul class="searchItems">
747
747
748 <li class="searchTag searchTagIcon">
748 <li class="searchTag searchTagIcon">
749 <i class="icon-search"></i>
749 <i class="icon-search"></i>
750 </li>
750 </li>
751
751
752 % if c.template_context['search_context']['repo_id']:
752 % if c.template_context['search_context']['repo_id']:
753 <li class="searchTag searchTagFilter searchTagHidable" >
753 <li class="searchTag searchTagFilter searchTagHidable" >
754 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
754 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
755 <span class="tag">
755 <span class="tag">
756 This repo
756 This repo
757 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
757 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
758 </span>
758 </span>
759 ##</a>
759 ##</a>
760 </li>
760 </li>
761 % elif c.template_context['search_context']['repo_group_id']:
761 % elif c.template_context['search_context']['repo_group_id']:
762 <li class="searchTag searchTagFilter searchTagHidable">
762 <li class="searchTag searchTagFilter searchTagHidable">
763 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
763 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
764 <span class="tag">
764 <span class="tag">
765 This group
765 This group
766 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
766 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
767 </span>
767 </span>
768 ##</a>
768 ##</a>
769 </li>
769 </li>
770 % endif
770 % endif
771
771
772 <li class="searchTagInput">
772 <li class="searchTagInput">
773 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
773 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
774 </li>
774 </li>
775 <li class="searchTag searchTagHelp">
775 <li class="searchTag searchTagHelp">
776 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
776 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
777 </li>
777 </li>
778 </ul>
778 </ul>
779 </div>
779 </div>
780 </div>
780 </div>
781
781
782 <div id="main_filter_help" style="display: none">
782 <div id="main_filter_help" style="display: none">
783 - Use '/' key to quickly access this field.
783 - Use '/' key to quickly access this field.
784
784
785 - Enter a name of repository, or repository group for quick search.
785 - Enter a name of repository, or repository group for quick search.
786
786
787 - Prefix query to allow special search:
787 - Prefix query to allow special search:
788
788
789 <strong>user:</strong>admin, to search for usernames, always global
789 <strong>user:</strong>admin, to search for usernames, always global
790
790
791 <strong>user_group:</strong>devops, to search for user groups, always global
791 <strong>user_group:</strong>devops, to search for user groups, always global
792
792
793 <strong>pr:</strong>303, to search for pull request number, title, or description, always global
793 <strong>pr:</strong>303, to search for pull request number, title, or description, always global
794
794
795 <strong>commit:</strong>efced4, to search for commits, scoped to repositories or groups
795 <strong>commit:</strong>efced4, to search for commits, scoped to repositories or groups
796
796
797 <strong>file:</strong>models.py, to search for file paths, scoped to repositories or groups
797 <strong>file:</strong>models.py, to search for file paths, scoped to repositories or groups
798
798
799 % if c.template_context['search_context']['repo_id']:
799 % if c.template_context['search_context']['repo_id']:
800 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
800 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
801 % elif c.template_context['search_context']['repo_group_id']:
801 % elif c.template_context['search_context']['repo_group_id']:
802 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
802 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
803 % else:
803 % else:
804 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
804 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
805 % endif
805 % endif
806 </div>
806 </div>
807 </li>
807 </li>
808
808
809 ## ROOT MENU
809 ## ROOT MENU
810 <li class="${h.is_active('home', active)}">
810 <li class="${h.is_active('home', active)}">
811 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
811 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
812 <div class="menulabel">${_('Home')}</div>
812 <div class="menulabel">${_('Home')}</div>
813 </a>
813 </a>
814 </li>
814 </li>
815
815
816 %if c.rhodecode_user.username != h.DEFAULT_USER:
816 %if c.rhodecode_user.username != h.DEFAULT_USER:
817 <li class="${h.is_active('journal', active)}">
817 <li class="${h.is_active('journal', active)}">
818 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
818 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
819 <div class="menulabel">${_('Journal')}</div>
819 <div class="menulabel">${_('Journal')}</div>
820 </a>
820 </a>
821 </li>
821 </li>
822 %else:
822 %else:
823 <li class="${h.is_active('journal', active)}">
823 <li class="${h.is_active('journal', active)}">
824 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
824 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
825 <div class="menulabel">${_('Public journal')}</div>
825 <div class="menulabel">${_('Public journal')}</div>
826 </a>
826 </a>
827 </li>
827 </li>
828 %endif
828 %endif
829
829
830 <li class="${h.is_active('gists', active)}">
830 <li class="${h.is_active('gists', active)}">
831 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
831 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
832 <div class="menulabel">${_('Gists')}</div>
832 <div class="menulabel">${_('Gists')}</div>
833 </a>
833 </a>
834 </li>
834 </li>
835
835
836 % if c.is_super_admin or c.is_delegated_admin:
836 % if c.is_super_admin or c.is_delegated_admin:
837 <li class="${h.is_active('admin', active)}">
837 <li class="${h.is_active('admin', active)}">
838 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
838 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
839 <div class="menulabel">${_('Admin')} </div>
839 <div class="menulabel">${_('Admin')} </div>
840 </a>
840 </a>
841 </li>
841 </li>
842 % endif
842 % endif
843
843
844 ## render extra user menu
844 ## render extra user menu
845 ${usermenu(active=(active=='my_account'))}
845 ${usermenu(active=(active=='my_account'))}
846
846
847 </ul>
847 </ul>
848
848
849 <script type="text/javascript">
849 <script type="text/javascript">
850 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
850 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
851
851
852 var formatRepoResult = function(result, container, query, escapeMarkup) {
852 var formatRepoResult = function(result, container, query, escapeMarkup) {
853 return function(data, escapeMarkup) {
853 return function(data, escapeMarkup) {
854 if (!data.repo_id){
854 if (!data.repo_id){
855 return data.text; // optgroup text Repositories
855 return data.text; // optgroup text Repositories
856 }
856 }
857
857
858 var tmpl = '';
858 var tmpl = '';
859 var repoType = data['repo_type'];
859 var repoType = data['repo_type'];
860 var repoName = data['text'];
860 var repoName = data['text'];
861
861
862 if(data && data.type == 'repo'){
862 if(data && data.type == 'repo'){
863 if(repoType === 'hg'){
863 if(repoType === 'hg'){
864 tmpl += '<i class="icon-hg"></i> ';
864 tmpl += '<i class="icon-hg"></i> ';
865 }
865 }
866 else if(repoType === 'git'){
866 else if(repoType === 'git'){
867 tmpl += '<i class="icon-git"></i> ';
867 tmpl += '<i class="icon-git"></i> ';
868 }
868 }
869 else if(repoType === 'svn'){
869 else if(repoType === 'svn'){
870 tmpl += '<i class="icon-svn"></i> ';
870 tmpl += '<i class="icon-svn"></i> ';
871 }
871 }
872 if(data['private']){
872 if(data['private']){
873 tmpl += '<i class="icon-lock" ></i> ';
873 tmpl += '<i class="icon-lock" ></i> ';
874 }
874 }
875 else if(visualShowPublicIcon){
875 else if(visualShowPublicIcon){
876 tmpl += '<i class="icon-unlock-alt"></i> ';
876 tmpl += '<i class="icon-unlock-alt"></i> ';
877 }
877 }
878 }
878 }
879 tmpl += escapeMarkup(repoName);
879 tmpl += escapeMarkup(repoName);
880 return tmpl;
880 return tmpl;
881
881
882 }(result, escapeMarkup);
882 }(result, escapeMarkup);
883 };
883 };
884
884
885 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
885 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
886 return function(data, escapeMarkup) {
886 return function(data, escapeMarkup) {
887 if (!data.repo_group_id){
887 if (!data.repo_group_id){
888 return data.text; // optgroup text Repositories
888 return data.text; // optgroup text Repositories
889 }
889 }
890
890
891 var tmpl = '';
891 var tmpl = '';
892 var repoGroupName = data['text'];
892 var repoGroupName = data['text'];
893
893
894 if(data){
894 if(data){
895
895
896 tmpl += '<i class="icon-repo-group"></i> ';
896 tmpl += '<i class="icon-repo-group"></i> ';
897
897
898 }
898 }
899 tmpl += escapeMarkup(repoGroupName);
899 tmpl += escapeMarkup(repoGroupName);
900 return tmpl;
900 return tmpl;
901
901
902 }(result, escapeMarkup);
902 }(result, escapeMarkup);
903 };
903 };
904
904
905 var escapeRegExChars = function (value) {
905 var escapeRegExChars = function (value) {
906 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
906 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
907 };
907 };
908
908
909 var getRepoIcon = function(repo_type) {
909 var getRepoIcon = function(repo_type) {
910 if (repo_type === 'hg') {
910 if (repo_type === 'hg') {
911 return '<i class="icon-hg"></i> ';
911 return '<i class="icon-hg"></i> ';
912 }
912 }
913 else if (repo_type === 'git') {
913 else if (repo_type === 'git') {
914 return '<i class="icon-git"></i> ';
914 return '<i class="icon-git"></i> ';
915 }
915 }
916 else if (repo_type === 'svn') {
916 else if (repo_type === 'svn') {
917 return '<i class="icon-svn"></i> ';
917 return '<i class="icon-svn"></i> ';
918 }
918 }
919 return ''
919 return ''
920 };
920 };
921
921
922 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
922 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
923
923
924 if (value.split(':').length === 2) {
924 if (value.split(':').length === 2) {
925 value = value.split(':')[1]
925 value = value.split(':')[1]
926 }
926 }
927
927
928 var searchType = data['type'];
928 var searchType = data['type'];
929 var searchSubType = data['subtype'];
929 var searchSubType = data['subtype'];
930 var valueDisplay = data['value_display'];
930 var valueDisplay = data['value_display'];
931 var valueIcon = data['value_icon'];
931 var valueIcon = data['value_icon'];
932
932
933 var pattern = '(' + escapeRegExChars(value) + ')';
933 var pattern = '(' + escapeRegExChars(value) + ')';
934
934
935 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
935 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
936
936
937 // highlight match
937 // highlight match
938 if (searchType != 'text') {
938 if (searchType != 'text') {
939 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
939 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
940 }
940 }
941
941
942 var icon = '';
942 var icon = '';
943
943
944 if (searchType === 'hint') {
944 if (searchType === 'hint') {
945 icon += '<i class="icon-repo-group"></i> ';
945 icon += '<i class="icon-repo-group"></i> ';
946 }
946 }
947 // full text search/hints
947 // full text search/hints
948 else if (searchType === 'search') {
948 else if (searchType === 'search') {
949 if (valueIcon === undefined) {
949 if (valueIcon === undefined) {
950 icon += '<i class="icon-more"></i> ';
950 icon += '<i class="icon-more"></i> ';
951 } else {
951 } else {
952 icon += valueIcon + ' ';
952 icon += valueIcon + ' ';
953 }
953 }
954
954
955 if (searchSubType !== undefined && searchSubType == 'repo') {
955 if (searchSubType !== undefined && searchSubType == 'repo') {
956 valueDisplay += '<div class="pull-right tag">repository</div>';
956 valueDisplay += '<div class="pull-right tag">repository</div>';
957 }
957 }
958 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
958 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
959 valueDisplay += '<div class="pull-right tag">repo group</div>';
959 valueDisplay += '<div class="pull-right tag">repo group</div>';
960 }
960 }
961 }
961 }
962 // repository
962 // repository
963 else if (searchType === 'repo') {
963 else if (searchType === 'repo') {
964
964
965 var repoIcon = getRepoIcon(data['repo_type']);
965 var repoIcon = getRepoIcon(data['repo_type']);
966 icon += repoIcon;
966 icon += repoIcon;
967
967
968 if (data['private']) {
968 if (data['private']) {
969 icon += '<i class="icon-lock" ></i> ';
969 icon += '<i class="icon-lock" ></i> ';
970 }
970 }
971 else if (visualShowPublicIcon) {
971 else if (visualShowPublicIcon) {
972 icon += '<i class="icon-unlock-alt"></i> ';
972 icon += '<i class="icon-unlock-alt"></i> ';
973 }
973 }
974 }
974 }
975 // repository groups
975 // repository groups
976 else if (searchType === 'repo_group') {
976 else if (searchType === 'repo_group') {
977 icon += '<i class="icon-repo-group"></i> ';
977 icon += '<i class="icon-repo-group"></i> ';
978 }
978 }
979 // user group
979 // user group
980 else if (searchType === 'user_group') {
980 else if (searchType === 'user_group') {
981 icon += '<i class="icon-group"></i> ';
981 icon += '<i class="icon-group"></i> ';
982 }
982 }
983 // user
983 // user
984 else if (searchType === 'user') {
984 else if (searchType === 'user') {
985 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
985 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
986 }
986 }
987 // pull request
987 // pull request
988 else if (searchType === 'pull_request') {
988 else if (searchType === 'pull_request') {
989 icon += '<i class="icon-merge"></i> ';
989 icon += '<i class="icon-merge"></i> ';
990 }
990 }
991 // commit
991 // commit
992 else if (searchType === 'commit') {
992 else if (searchType === 'commit') {
993 var repo_data = data['repo_data'];
993 var repo_data = data['repo_data'];
994 var repoIcon = getRepoIcon(repo_data['repository_type']);
994 var repoIcon = getRepoIcon(repo_data['repository_type']);
995 if (repoIcon) {
995 if (repoIcon) {
996 icon += repoIcon;
996 icon += repoIcon;
997 } else {
997 } else {
998 icon += '<i class="icon-tag"></i>';
998 icon += '<i class="icon-tag"></i>';
999 }
999 }
1000 }
1000 }
1001 // file
1001 // file
1002 else if (searchType === 'file') {
1002 else if (searchType === 'file') {
1003 var repo_data = data['repo_data'];
1003 var repo_data = data['repo_data'];
1004 var repoIcon = getRepoIcon(repo_data['repository_type']);
1004 var repoIcon = getRepoIcon(repo_data['repository_type']);
1005 if (repoIcon) {
1005 if (repoIcon) {
1006 icon += repoIcon;
1006 icon += repoIcon;
1007 } else {
1007 } else {
1008 icon += '<i class="icon-tag"></i>';
1008 icon += '<i class="icon-tag"></i>';
1009 }
1009 }
1010 }
1010 }
1011 // generic text
1011 // generic text
1012 else if (searchType === 'text') {
1012 else if (searchType === 'text') {
1013 icon = '';
1013 icon = '';
1014 }
1014 }
1015
1015
1016 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
1016 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
1017 return tmpl.format(icon, valueDisplay);
1017 return tmpl.format(icon, valueDisplay);
1018 };
1018 };
1019
1019
1020 var handleSelect = function(element, suggestion) {
1020 var handleSelect = function(element, suggestion) {
1021 if (suggestion.type === "hint") {
1021 if (suggestion.type === "hint") {
1022 // we skip action
1022 // we skip action
1023 $('#main_filter').focus();
1023 $('#main_filter').focus();
1024 }
1024 }
1025 else if (suggestion.type === "text") {
1025 else if (suggestion.type === "text") {
1026 // we skip action
1026 // we skip action
1027 $('#main_filter').focus();
1027 $('#main_filter').focus();
1028
1028
1029 } else {
1029 } else {
1030 window.location = suggestion['url'];
1030 window.location = suggestion['url'];
1031 }
1031 }
1032 };
1032 };
1033
1033
1034 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
1034 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
1035 if (queryLowerCase.split(':').length === 2) {
1035 if (queryLowerCase.split(':').length === 2) {
1036 queryLowerCase = queryLowerCase.split(':')[1]
1036 queryLowerCase = queryLowerCase.split(':')[1]
1037 }
1037 }
1038 if (suggestion.type === "text") {
1038 if (suggestion.type === "text") {
1039 // special case we don't want to "skip" display for
1039 // special case we don't want to "skip" display for
1040 return true
1040 return true
1041 }
1041 }
1042 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
1042 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
1043 };
1043 };
1044
1044
1045 var cleanContext = {
1045 var cleanContext = {
1046 repo_view_type: null,
1046 repo_view_type: null,
1047
1047
1048 repo_id: null,
1048 repo_id: null,
1049 repo_name: "",
1049 repo_name: "",
1050
1050
1051 repo_group_id: null,
1051 repo_group_id: null,
1052 repo_group_name: null
1052 repo_group_name: null
1053 };
1053 };
1054 var removeGoToFilter = function () {
1054 var removeGoToFilter = function () {
1055 $('.searchTagHidable').hide();
1055 $('.searchTagHidable').hide();
1056 $('#main_filter').autocomplete(
1056 $('#main_filter').autocomplete(
1057 'setOptions', {params:{search_context: cleanContext}});
1057 'setOptions', {params:{search_context: cleanContext}});
1058 };
1058 };
1059
1059
1060 $('#main_filter').autocomplete({
1060 $('#main_filter').autocomplete({
1061 serviceUrl: pyroutes.url('goto_switcher_data'),
1061 serviceUrl: pyroutes.url('goto_switcher_data'),
1062 params: {
1062 params: {
1063 "search_context": templateContext.search_context
1063 "search_context": templateContext.search_context
1064 },
1064 },
1065 minChars:2,
1065 minChars:2,
1066 maxHeight:400,
1066 maxHeight:400,
1067 deferRequestBy: 300, //miliseconds
1067 deferRequestBy: 300, //miliseconds
1068 tabDisabled: true,
1068 tabDisabled: true,
1069 autoSelectFirst: false,
1069 autoSelectFirst: false,
1070 containerClass: 'autocomplete-qfilter-suggestions',
1070 containerClass: 'autocomplete-qfilter-suggestions',
1071 formatResult: autocompleteMainFilterFormatResult,
1071 formatResult: autocompleteMainFilterFormatResult,
1072 lookupFilter: autocompleteMainFilterResult,
1072 lookupFilter: autocompleteMainFilterResult,
1073 onSelect: function (element, suggestion) {
1073 onSelect: function (element, suggestion) {
1074 handleSelect(element, suggestion);
1074 handleSelect(element, suggestion);
1075 return false;
1075 return false;
1076 },
1076 },
1077 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
1077 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
1078 if (jqXHR !== 'abort') {
1078 if (jqXHR !== 'abort') {
1079 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
1079 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
1080 SwalNoAnimation.fire({
1080 SwalNoAnimation.fire({
1081 icon: 'error',
1081 icon: 'error',
1082 title: _gettext('Error during search operation'),
1082 title: _gettext('Error during search operation'),
1083 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
1083 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
1084 }).then(function(result) {
1084 }).then(function(result) {
1085 window.location.reload();
1085 window.location.reload();
1086 })
1086 })
1087 }
1087 }
1088 },
1088 },
1089 onSearchStart: function (params) {
1089 onSearchStart: function (params) {
1090 $('.searchTag.searchTagIcon').html('<i class="icon-spin animate-spin"></i>')
1090 $('.searchTag.searchTagIcon').html('<i class="icon-spin animate-spin"></i>')
1091 },
1091 },
1092 onSearchComplete: function (query, suggestions) {
1092 onSearchComplete: function (query, suggestions) {
1093 $('.searchTag.searchTagIcon').html('<i class="icon-search"></i>')
1093 $('.searchTag.searchTagIcon').html('<i class="icon-search"></i>')
1094 },
1094 },
1095 });
1095 });
1096
1096
1097 showMainFilterBox = function () {
1097 showMainFilterBox = function () {
1098 $('#main_filter_help').toggle();
1098 $('#main_filter_help').toggle();
1099 };
1099 };
1100
1100
1101 $('#main_filter').on('keydown.autocomplete', function (e) {
1101 $('#main_filter').on('keydown.autocomplete', function (e) {
1102
1102
1103 var BACKSPACE = 8;
1103 var BACKSPACE = 8;
1104 var el = $(e.currentTarget);
1104 var el = $(e.currentTarget);
1105 if(e.which === BACKSPACE){
1105 if(e.which === BACKSPACE){
1106 var inputVal = el.val();
1106 var inputVal = el.val();
1107 if (inputVal === ""){
1107 if (inputVal === ""){
1108 removeGoToFilter()
1108 removeGoToFilter()
1109 }
1109 }
1110 }
1110 }
1111 });
1111 });
1112
1112
1113 var dismissNotice = function(noticeId) {
1113 var dismissNotice = function(noticeId) {
1114
1114
1115 var url = pyroutes.url('user_notice_dismiss',
1115 var url = pyroutes.url('user_notice_dismiss',
1116 {"user_id": templateContext.rhodecode_user.user_id});
1116 {"user_id": templateContext.rhodecode_user.user_id});
1117
1117
1118 var postData = {
1118 var postData = {
1119 'csrf_token': CSRF_TOKEN,
1119 'csrf_token': CSRF_TOKEN,
1120 'notice_id': noticeId,
1120 'notice_id': noticeId,
1121 };
1121 };
1122
1122
1123 var success = function(response) {
1123 var success = function(response) {
1124 $('#notice-message-' + noticeId).remove();
1124 $('#notice-message-' + noticeId).remove();
1125 return false;
1125 return false;
1126 };
1126 };
1127 var failure = function(data, textStatus, xhr) {
1127 var failure = function(data, textStatus, xhr) {
1128 alert("error processing request: " + textStatus);
1128 alert("error processing request: " + textStatus);
1129 return false;
1129 return false;
1130 };
1130 };
1131 ajaxPOST(url, postData, success, failure);
1131 ajaxPOST(url, postData, success, failure);
1132 }
1132 }
1133
1133
1134 var hideLicenseWarning = function () {
1134 var hideLicenseWarning = function () {
1135 var fingerprint = templateContext.session_attrs.license_fingerprint;
1135 var fingerprint = templateContext.session_attrs.license_fingerprint;
1136 storeUserSessionAttr('rc_user_session_attr.hide_license_warning', fingerprint);
1136 storeUserSessionAttr('rc_user_session_attr.hide_license_warning', fingerprint);
1137 $('#notifications').hide();
1137 $('#notifications').hide();
1138 }
1138 }
1139
1139
1140 var hideLicenseError = function () {
1140 var hideLicenseError = function () {
1141 var fingerprint = templateContext.session_attrs.license_fingerprint;
1141 var fingerprint = templateContext.session_attrs.license_fingerprint;
1142 storeUserSessionAttr('rc_user_session_attr.hide_license_error', fingerprint);
1142 storeUserSessionAttr('rc_user_session_attr.hide_license_error', fingerprint);
1143 $('#notifications').hide();
1143 $('#notifications').hide();
1144 }
1144 }
1145
1145
1146 </script>
1146 </script>
1147 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1147 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1148 </%def>
1148 </%def>
1149
1149
1150 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
1150 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
1151 <div class="modal-dialog">
1151 <div class="modal-dialog">
1152 <div class="modal-content">
1152 <div class="modal-content">
1153 <div class="modal-header">
1153 <div class="modal-header">
1154 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
1154 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
1155 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
1155 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
1156 </div>
1156 </div>
1157 <div class="modal-body">
1157 <div class="modal-body">
1158 <div class="block-left">
1158 <div class="block-left">
1159 <table class="keyboard-mappings">
1159 <table class="keyboard-mappings">
1160 <tbody>
1160 <tbody>
1161 <tr>
1161 <tr>
1162 <th></th>
1162 <th></th>
1163 <th>${_('Site-wide shortcuts')}</th>
1163 <th>${_('Site-wide shortcuts')}</th>
1164 </tr>
1164 </tr>
1165 <%
1165 <%
1166 elems = [
1166 elems = [
1167 ('/', 'Use quick search box'),
1167 ('/', 'Use quick search box'),
1168 ('g h', 'Goto home page'),
1168 ('g h', 'Goto home page'),
1169 ('g g', 'Goto my private gists page'),
1169 ('g g', 'Goto my private gists page'),
1170 ('g G', 'Goto my public gists page'),
1170 ('g G', 'Goto my public gists page'),
1171 ('g 0-9', 'Goto bookmarked items from 0-9'),
1171 ('g 0-9', 'Goto bookmarked items from 0-9'),
1172 ('n r', 'New repository page'),
1172 ('n r', 'New repository page'),
1173 ('n g', 'New gist page'),
1173 ('n g', 'New gist page'),
1174 ]
1174 ]
1175 %>
1175 %>
1176 %for key, desc in elems:
1176 %for key, desc in elems:
1177 <tr>
1177 <tr>
1178 <td class="keys">
1178 <td class="keys">
1179 <span class="key tag">${key}</span>
1179 <span class="key tag">${key}</span>
1180 </td>
1180 </td>
1181 <td>${desc}</td>
1181 <td>${desc}</td>
1182 </tr>
1182 </tr>
1183 %endfor
1183 %endfor
1184 </tbody>
1184 </tbody>
1185 </table>
1185 </table>
1186 </div>
1186 </div>
1187 <div class="block-left">
1187 <div class="block-left">
1188 <table class="keyboard-mappings">
1188 <table class="keyboard-mappings">
1189 <tbody>
1189 <tbody>
1190 <tr>
1190 <tr>
1191 <th></th>
1191 <th></th>
1192 <th>${_('Repositories')}</th>
1192 <th>${_('Repositories')}</th>
1193 </tr>
1193 </tr>
1194 <%
1194 <%
1195 elems = [
1195 elems = [
1196 ('g s', 'Goto summary page'),
1196 ('g s', 'Goto summary page'),
1197 ('g c', 'Goto changelog page'),
1197 ('g c', 'Goto changelog page'),
1198 ('g f', 'Goto files page'),
1198 ('g f', 'Goto files page'),
1199 ('g F', 'Goto files page with file search activated'),
1199 ('g F', 'Goto files page with file search activated'),
1200 ('g p', 'Goto pull requests page'),
1200 ('g p', 'Goto pull requests page'),
1201 ('g o', 'Goto repository settings'),
1201 ('g o', 'Goto repository settings'),
1202 ('g O', 'Goto repository access permissions settings'),
1202 ('g O', 'Goto repository access permissions settings'),
1203 ('t s', 'Toggle sidebar on some pages'),
1203 ('t s', 'Toggle sidebar on some pages'),
1204 ]
1204 ]
1205 %>
1205 %>
1206 %for key, desc in elems:
1206 %for key, desc in elems:
1207 <tr>
1207 <tr>
1208 <td class="keys">
1208 <td class="keys">
1209 <span class="key tag">${key}</span>
1209 <span class="key tag">${key}</span>
1210 </td>
1210 </td>
1211 <td>${desc}</td>
1211 <td>${desc}</td>
1212 </tr>
1212 </tr>
1213 %endfor
1213 %endfor
1214 </tbody>
1214 </tbody>
1215 </table>
1215 </table>
1216 </div>
1216 </div>
1217 </div>
1217 </div>
1218 <div class="modal-footer">
1218 <div class="modal-footer">
1219 </div>
1219 </div>
1220 </div><!-- /.modal-content -->
1220 </div><!-- /.modal-content -->
1221 </div><!-- /.modal-dialog -->
1221 </div><!-- /.modal-dialog -->
1222 </div><!-- /.modal -->
1222 </div><!-- /.modal -->
1223
1223
1224
1224
1225 <script type="text/javascript">
1225 <script type="text/javascript">
1226 (function () {
1226 (function () {
1227 "use sctrict";
1227 "use sctrict";
1228
1228
1229 // details block auto-hide menu
1229 // details block auto-hide menu
1230 $(document).mouseup(function(e) {
1230 $(document).mouseup(function(e) {
1231 var container = $('.details-inline-block');
1231 var container = $('.details-inline-block');
1232 if (!container.is(e.target) && container.has(e.target).length === 0) {
1232 if (!container.is(e.target) && container.has(e.target).length === 0) {
1233 $('.details-inline-block[open]').removeAttr('open')
1233 $('.details-inline-block[open]').removeAttr('open')
1234 }
1234 }
1235 });
1235 });
1236
1236
1237 var $sideBar = $('.right-sidebar');
1237 var $sideBar = $('.right-sidebar');
1238 var expanded = $sideBar.hasClass('right-sidebar-expanded');
1238 var expanded = $sideBar.hasClass('right-sidebar-expanded');
1239 var sidebarState = templateContext.session_attrs.sidebarState;
1239 var sidebarState = templateContext.session_attrs.sidebarState;
1240 var sidebarEnabled = $('aside.right-sidebar').get(0);
1240 var sidebarEnabled = $('aside.right-sidebar').get(0);
1241
1241
1242 if (sidebarState === 'expanded') {
1242 if (sidebarState === 'expanded') {
1243 expanded = true
1243 expanded = true
1244 } else if (sidebarState === 'collapsed') {
1244 } else if (sidebarState === 'collapsed') {
1245 expanded = false
1245 expanded = false
1246 }
1246 }
1247 if (sidebarEnabled) {
1247 if (sidebarEnabled) {
1248 // show sidebar since it's hidden on load
1248 // show sidebar since it's hidden on load
1249 $('.right-sidebar').show();
1249 $('.right-sidebar').show();
1250
1250
1251 // init based on set initial class, or if defined user session attrs
1251 // init based on set initial class, or if defined user session attrs
1252 if (expanded) {
1252 if (expanded) {
1253 window.expandSidebar();
1253 window.expandSidebar();
1254 window.updateStickyHeader();
1254 window.updateStickyHeader();
1255
1255
1256 } else {
1256 } else {
1257 window.collapseSidebar();
1257 window.collapseSidebar();
1258 window.updateStickyHeader();
1258 window.updateStickyHeader();
1259 }
1259 }
1260 }
1260 }
1261 })()
1261 })()
1262
1262
1263 </script>
1263 </script>
General Comments 0
You need to be logged in to leave comments. Login now