##// END OF EJS Templates
integrations: add recursive repo group scope to allow integrations...
dan -
r793:fc8d2069 default
parent child Browse files
Show More
@@ -1,385 +1,392 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pylons
21 import pylons
22 import deform
22 import deform
23 import logging
23 import logging
24 import colander
24 import colander
25 import peppercorn
25 import peppercorn
26 import webhelpers.paginate
26 import webhelpers.paginate
27
27
28 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
28 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30 from pyramid.response import Response
30 from pyramid.response import Response
31
31
32 from rhodecode.lib import auth
32 from rhodecode.lib import auth
33 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
33 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
34 from rhodecode.lib.utils2 import safe_int
34 from rhodecode.lib.utils2 import safe_int
35 from rhodecode.lib.helpers import Page
35 from rhodecode.lib.helpers import Page
36 from rhodecode.model.db import Repository, RepoGroup, Session, Integration
36 from rhodecode.model.db import Repository, RepoGroup, Session, Integration
37 from rhodecode.model.scm import ScmModel
37 from rhodecode.model.scm import ScmModel
38 from rhodecode.model.integration import IntegrationModel
38 from rhodecode.model.integration import IntegrationModel
39 from rhodecode.admin.navigation import navigation_list
39 from rhodecode.admin.navigation import navigation_list
40 from rhodecode.translation import _
40 from rhodecode.translation import _
41 from rhodecode.integrations import integration_type_registry
41 from rhodecode.integrations import integration_type_registry
42 from rhodecode.model.validation_schema.schemas.integration_schema import (
42 from rhodecode.model.validation_schema.schemas.integration_schema import (
43 make_integration_schema)
43 make_integration_schema, IntegrationScopeType)
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class IntegrationSettingsViewBase(object):
48 class IntegrationSettingsViewBase(object):
49 """ Base Integration settings view used by both repo / global settings """
49 """ Base Integration settings view used by both repo / global settings """
50
50
51 def __init__(self, context, request):
51 def __init__(self, context, request):
52 self.context = context
52 self.context = context
53 self.request = request
53 self.request = request
54 self._load_general_context()
54 self._load_general_context()
55
55
56 if not self.perm_check(request.user):
56 if not self.perm_check(request.user):
57 raise HTTPForbidden()
57 raise HTTPForbidden()
58
58
59 def _load_general_context(self):
59 def _load_general_context(self):
60 """
60 """
61 This avoids boilerplate for repo/global+list/edit+views/templates
61 This avoids boilerplate for repo/global+list/edit+views/templates
62 by doing all possible contexts at the same time however it should
62 by doing all possible contexts at the same time however it should
63 be split up into separate functions once more "contexts" exist
63 be split up into separate functions once more "contexts" exist
64 """
64 """
65
65
66 self.IntegrationType = None
66 self.IntegrationType = None
67 self.repo = None
67 self.repo = None
68 self.repo_group = None
68 self.repo_group = None
69 self.integration = None
69 self.integration = None
70 self.integrations = {}
70 self.integrations = {}
71
71
72 request = self.request
72 request = self.request
73
73
74 if 'repo_name' in request.matchdict: # in repo settings context
74 if 'repo_name' in request.matchdict: # in repo settings context
75 repo_name = request.matchdict['repo_name']
75 repo_name = request.matchdict['repo_name']
76 self.repo = Repository.get_by_repo_name(repo_name)
76 self.repo = Repository.get_by_repo_name(repo_name)
77
77
78 if 'repo_group_name' in request.matchdict: # in group settings context
78 if 'repo_group_name' in request.matchdict: # in group settings context
79 repo_group_name = request.matchdict['repo_group_name']
79 repo_group_name = request.matchdict['repo_group_name']
80 self.repo_group = RepoGroup.get_by_group_name(repo_group_name)
80 self.repo_group = RepoGroup.get_by_group_name(repo_group_name)
81
81
82
82
83 if 'integration' in request.matchdict: # integration type context
83 if 'integration' in request.matchdict: # integration type context
84 integration_type = request.matchdict['integration']
84 integration_type = request.matchdict['integration']
85 self.IntegrationType = integration_type_registry[integration_type]
85 self.IntegrationType = integration_type_registry[integration_type]
86
86
87 if 'integration_id' in request.matchdict: # single integration context
87 if 'integration_id' in request.matchdict: # single integration context
88 integration_id = request.matchdict['integration_id']
88 integration_id = request.matchdict['integration_id']
89 self.integration = Integration.get(integration_id)
89 self.integration = Integration.get(integration_id)
90
90
91 # extra perms check just in case
91 # extra perms check just in case
92 if not self._has_perms_for_integration(self.integration):
92 if not self._has_perms_for_integration(self.integration):
93 raise HTTPForbidden()
93 raise HTTPForbidden()
94
94
95 self.settings = self.integration and self.integration.settings or {}
95 self.settings = self.integration and self.integration.settings or {}
96 self.admin_view = not (self.repo or self.repo_group)
96 self.admin_view = not (self.repo or self.repo_group)
97
97
98 def _has_perms_for_integration(self, integration):
98 def _has_perms_for_integration(self, integration):
99 perms = self.request.user.permissions
99 perms = self.request.user.permissions
100
100
101 if 'hg.admin' in perms['global']:
101 if 'hg.admin' in perms['global']:
102 return True
102 return True
103
103
104 if integration.repo:
104 if integration.repo:
105 return perms['repositories'].get(
105 return perms['repositories'].get(
106 integration.repo.repo_name) == 'repository.admin'
106 integration.repo.repo_name) == 'repository.admin'
107
107
108 if integration.repo_group:
108 if integration.repo_group:
109 return perms['repositories_groups'].get(
109 return perms['repositories_groups'].get(
110 integration.repo_group.group_name) == 'group.admin'
110 integration.repo_group.group_name) == 'group.admin'
111
111
112 return False
112 return False
113
113
114 def _template_c_context(self):
114 def _template_c_context(self):
115 # TODO: dan: this is a stopgap in order to inherit from current pylons
115 # TODO: dan: this is a stopgap in order to inherit from current pylons
116 # based admin/repo settings templates - this should be removed entirely
116 # based admin/repo settings templates - this should be removed entirely
117 # after port to pyramid
117 # after port to pyramid
118
118
119 c = pylons.tmpl_context
119 c = pylons.tmpl_context
120 c.active = 'integrations'
120 c.active = 'integrations'
121 c.rhodecode_user = self.request.user
121 c.rhodecode_user = self.request.user
122 c.repo = self.repo
122 c.repo = self.repo
123 c.repo_group = self.repo_group
123 c.repo_group = self.repo_group
124 c.repo_name = self.repo and self.repo.repo_name or None
124 c.repo_name = self.repo and self.repo.repo_name or None
125 c.repo_group_name = self.repo_group and self.repo_group.group_name or None
125 c.repo_group_name = self.repo_group and self.repo_group.group_name or None
126
126
127 if self.repo:
127 if self.repo:
128 c.repo_info = self.repo
128 c.repo_info = self.repo
129 c.rhodecode_db_repo = self.repo
129 c.rhodecode_db_repo = self.repo
130 c.repository_pull_requests = ScmModel().get_pull_requests(self.repo)
130 c.repository_pull_requests = ScmModel().get_pull_requests(self.repo)
131 else:
131 else:
132 c.navlist = navigation_list(self.request)
132 c.navlist = navigation_list(self.request)
133
133
134 return c
134 return c
135
135
136 def _form_schema(self):
136 def _form_schema(self):
137 schema = make_integration_schema(IntegrationType=self.IntegrationType,
137 schema = make_integration_schema(IntegrationType=self.IntegrationType,
138 settings=self.settings)
138 settings=self.settings)
139
139
140 # returns a clone, important if mutating the schema later
140 # returns a clone, important if mutating the schema later
141 return schema.bind(
141 return schema.bind(
142 permissions=self.request.user.permissions,
142 permissions=self.request.user.permissions,
143 no_scope=not self.admin_view)
143 no_scope=not self.admin_view)
144
144
145
145
146 def _form_defaults(self):
146 def _form_defaults(self):
147 defaults = {}
147 defaults = {}
148
148
149 if self.integration:
149 if self.integration:
150 defaults['settings'] = self.integration.settings or {}
150 defaults['settings'] = self.integration.settings or {}
151 defaults['options'] = {
151 defaults['options'] = {
152 'name': self.integration.name,
152 'name': self.integration.name,
153 'enabled': self.integration.enabled,
153 'enabled': self.integration.enabled,
154 'scope': self.integration.scope,
154 'scope': {
155 'repo': self.integration.repo,
156 'repo_group': self.integration.repo_group,
157 'child_repos_only': self.integration.child_repos_only,
158 },
155 }
159 }
156 else:
160 else:
157 if self.repo:
161 if self.repo:
158 scope = _('{repo_name} repository').format(
162 scope = _('{repo_name} repository').format(
159 repo_name=self.repo.repo_name)
163 repo_name=self.repo.repo_name)
160 elif self.repo_group:
164 elif self.repo_group:
161 scope = _('{repo_group_name} repo group').format(
165 scope = _('{repo_group_name} repo group').format(
162 repo_group_name=self.repo_group.group_name)
166 repo_group_name=self.repo_group.group_name)
163 else:
167 else:
164 scope = _('Global')
168 scope = _('Global')
165
169
166 defaults['options'] = {
170 defaults['options'] = {
167 'enabled': True,
171 'enabled': True,
168 'name': _('{name} integration').format(
172 'name': _('{name} integration').format(
169 name=self.IntegrationType.display_name),
173 name=self.IntegrationType.display_name),
170 }
174 }
171 if self.repo:
175 defaults['options']['scope'] = {
172 defaults['options']['scope'] = self.repo
176 'repo': self.repo,
173 elif self.repo_group:
177 'repo_group': self.repo_group,
174 defaults['options']['scope'] = self.repo_group
178 }
175
179
176 return defaults
180 return defaults
177
181
178 def _delete_integration(self, integration):
182 def _delete_integration(self, integration):
179 Session().delete(self.integration)
183 Session().delete(self.integration)
180 Session().commit()
184 Session().commit()
181 self.request.session.flash(
185 self.request.session.flash(
182 _('Integration {integration_name} deleted successfully.').format(
186 _('Integration {integration_name} deleted successfully.').format(
183 integration_name=self.integration.name),
187 integration_name=self.integration.name),
184 queue='success')
188 queue='success')
185
189
186 if self.repo:
190 if self.repo:
187 redirect_to = self.request.route_url(
191 redirect_to = self.request.route_url(
188 'repo_integrations_home', repo_name=self.repo.repo_name)
192 'repo_integrations_home', repo_name=self.repo.repo_name)
189 elif self.repo_group:
193 elif self.repo_group:
190 redirect_to = self.request.route_url(
194 redirect_to = self.request.route_url(
191 'repo_group_integrations_home',
195 'repo_group_integrations_home',
192 repo_group_name=self.repo_group.group_name)
196 repo_group_name=self.repo_group.group_name)
193 else:
197 else:
194 redirect_to = self.request.route_url('global_integrations_home')
198 redirect_to = self.request.route_url('global_integrations_home')
195 raise HTTPFound(redirect_to)
199 raise HTTPFound(redirect_to)
196
200
197 def settings_get(self, defaults=None, form=None):
201 def settings_get(self, defaults=None, form=None):
198 """
202 """
199 View that displays the integration settings as a form.
203 View that displays the integration settings as a form.
200 """
204 """
201
205
202 defaults = defaults or self._form_defaults()
206 defaults = defaults or self._form_defaults()
203 schema = self._form_schema()
207 schema = self._form_schema()
204
208
205 if self.integration:
209 if self.integration:
206 buttons = ('submit', 'delete')
210 buttons = ('submit', 'delete')
207 else:
211 else:
208 buttons = ('submit',)
212 buttons = ('submit',)
209
213
210 form = form or deform.Form(schema, appstruct=defaults, buttons=buttons)
214 form = form or deform.Form(schema, appstruct=defaults, buttons=buttons)
211
215
212 template_context = {
216 template_context = {
213 'form': form,
217 'form': form,
214 'current_IntegrationType': self.IntegrationType,
218 'current_IntegrationType': self.IntegrationType,
215 'integration': self.integration,
219 'integration': self.integration,
216 'c': self._template_c_context(),
220 'c': self._template_c_context(),
217 }
221 }
218
222
219 return template_context
223 return template_context
220
224
221 @auth.CSRFRequired()
225 @auth.CSRFRequired()
222 def settings_post(self):
226 def settings_post(self):
223 """
227 """
224 View that validates and stores the integration settings.
228 View that validates and stores the integration settings.
225 """
229 """
226 controls = self.request.POST.items()
230 controls = self.request.POST.items()
227 pstruct = peppercorn.parse(controls)
231 pstruct = peppercorn.parse(controls)
228
232
229 if self.integration and pstruct.get('delete'):
233 if self.integration and pstruct.get('delete'):
230 return self._delete_integration(self.integration)
234 return self._delete_integration(self.integration)
231
235
232 schema = self._form_schema()
236 schema = self._form_schema()
233
237
234 skip_settings_validation = False
238 skip_settings_validation = False
235 if self.integration and 'enabled' not in pstruct.get('options', {}):
239 if self.integration and 'enabled' not in pstruct.get('options', {}):
236 skip_settings_validation = True
240 skip_settings_validation = True
237 schema['settings'].validator = None
241 schema['settings'].validator = None
238 for field in schema['settings'].children:
242 for field in schema['settings'].children:
239 field.validator = None
243 field.validator = None
240 field.missing = ''
244 field.missing = ''
241
245
242 if self.integration:
246 if self.integration:
243 buttons = ('submit', 'delete')
247 buttons = ('submit', 'delete')
244 else:
248 else:
245 buttons = ('submit',)
249 buttons = ('submit',)
246
250
247 form = deform.Form(schema, buttons=buttons)
251 form = deform.Form(schema, buttons=buttons)
248
252
249 if not self.admin_view:
253 if not self.admin_view:
250 # scope is read only field in these cases, and has to be added
254 # scope is read only field in these cases, and has to be added
251 options = pstruct.setdefault('options', {})
255 options = pstruct.setdefault('options', {})
252 if 'scope' not in options:
256 if 'scope' not in options:
253 if self.repo:
257 options['scope'] = IntegrationScopeType().serialize(None, {
254 options['scope'] = 'repo:{}'.format(self.repo.repo_name)
258 'repo': self.repo,
255 elif self.repo_group:
259 'repo_group': self.repo_group,
256 options['scope'] = 'repogroup:{}'.format(
260 })
257 self.repo_group.group_name)
258
261
259 try:
262 try:
260 valid_data = form.validate_pstruct(pstruct)
263 valid_data = form.validate_pstruct(pstruct)
261 except deform.ValidationFailure as e:
264 except deform.ValidationFailure as e:
262 self.request.session.flash(
265 self.request.session.flash(
263 _('Errors exist when saving integration settings. '
266 _('Errors exist when saving integration settings. '
264 'Please check the form inputs.'),
267 'Please check the form inputs.'),
265 queue='error')
268 queue='error')
266 return self.settings_get(form=e)
269 return self.settings_get(form=e)
267
270
268 if not self.integration:
271 if not self.integration:
269 self.integration = Integration()
272 self.integration = Integration()
270 self.integration.integration_type = self.IntegrationType.key
273 self.integration.integration_type = self.IntegrationType.key
271 Session().add(self.integration)
274 Session().add(self.integration)
272
275
273 scope = valid_data['options']['scope']
276 scope = valid_data['options']['scope']
274
277
275 IntegrationModel().update_integration(self.integration,
278 IntegrationModel().update_integration(self.integration,
276 name=valid_data['options']['name'],
279 name=valid_data['options']['name'],
277 enabled=valid_data['options']['enabled'],
280 enabled=valid_data['options']['enabled'],
278 settings=valid_data['settings'],
281 settings=valid_data['settings'],
279 scope=scope)
282 repo=scope['repo'],
283 repo_group=scope['repo_group'],
284 child_repos_only=scope['child_repos_only'],
285 )
286
280
287
281 self.integration.settings = valid_data['settings']
288 self.integration.settings = valid_data['settings']
282 Session().commit()
289 Session().commit()
283 # Display success message and redirect.
290 # Display success message and redirect.
284 self.request.session.flash(
291 self.request.session.flash(
285 _('Integration {integration_name} updated successfully.').format(
292 _('Integration {integration_name} updated successfully.').format(
286 integration_name=self.IntegrationType.display_name),
293 integration_name=self.IntegrationType.display_name),
287 queue='success')
294 queue='success')
288
295
289
296
290 # if integration scope changes, we must redirect to the right place
297 # if integration scope changes, we must redirect to the right place
291 # keeping in mind if the original view was for /repo/ or /_admin/
298 # keeping in mind if the original view was for /repo/ or /_admin/
292 admin_view = not (self.repo or self.repo_group)
299 admin_view = not (self.repo or self.repo_group)
293
300
294 if isinstance(self.integration.scope, Repository) and not admin_view:
301 if self.integration.repo and not admin_view:
295 redirect_to = self.request.route_path(
302 redirect_to = self.request.route_path(
296 'repo_integrations_edit',
303 'repo_integrations_edit',
297 repo_name=self.integration.scope.repo_name,
304 repo_name=self.integration.repo.repo_name,
298 integration=self.integration.integration_type,
305 integration=self.integration.integration_type,
299 integration_id=self.integration.integration_id)
306 integration_id=self.integration.integration_id)
300 elif isinstance(self.integration.scope, RepoGroup) and not admin_view:
307 elif self.integration.repo_group and not admin_view:
301 redirect_to = self.request.route_path(
308 redirect_to = self.request.route_path(
302 'repo_group_integrations_edit',
309 'repo_group_integrations_edit',
303 repo_group_name=self.integration.scope.group_name,
310 repo_group_name=self.integration.repo_group.group_name,
304 integration=self.integration.integration_type,
311 integration=self.integration.integration_type,
305 integration_id=self.integration.integration_id)
312 integration_id=self.integration.integration_id)
306 else:
313 else:
307 redirect_to = self.request.route_path(
314 redirect_to = self.request.route_path(
308 'global_integrations_edit',
315 'global_integrations_edit',
309 integration=self.integration.integration_type,
316 integration=self.integration.integration_type,
310 integration_id=self.integration.integration_id)
317 integration_id=self.integration.integration_id)
311
318
312 return HTTPFound(redirect_to)
319 return HTTPFound(redirect_to)
313
320
314 def index(self):
321 def index(self):
315 """ List integrations """
322 """ List integrations """
316 if self.repo:
323 if self.repo:
317 scope = self.repo
324 scope = self.repo
318 elif self.repo_group:
325 elif self.repo_group:
319 scope = self.repo_group
326 scope = self.repo_group
320 else:
327 else:
321 scope = 'all'
328 scope = 'all'
322
329
323 integrations = []
330 integrations = []
324
331
325 for integration in IntegrationModel().get_integrations(
332 for integration in IntegrationModel().get_integrations(
326 scope=scope, IntegrationType=self.IntegrationType):
333 scope=scope, IntegrationType=self.IntegrationType):
327
334
328 # extra permissions check *just in case*
335 # extra permissions check *just in case*
329 if not self._has_perms_for_integration(integration):
336 if not self._has_perms_for_integration(integration):
330 continue
337 continue
331 integrations.append(integration)
338 integrations.append(integration)
332
339
333 sort_arg = self.request.GET.get('sort', 'name:asc')
340 sort_arg = self.request.GET.get('sort', 'name:asc')
334 if ':' in sort_arg:
341 if ':' in sort_arg:
335 sort_field, sort_dir = sort_arg.split(':')
342 sort_field, sort_dir = sort_arg.split(':')
336 else:
343 else:
337 sort_field = sort_arg, 'asc'
344 sort_field = sort_arg, 'asc'
338
345
339 assert sort_field in ('name', 'integration_type', 'enabled', 'scope')
346 assert sort_field in ('name', 'integration_type', 'enabled', 'scope')
340
347
341 integrations.sort(
348 integrations.sort(
342 key=lambda x: getattr(x[1], sort_field), reverse=(sort_dir=='desc'))
349 key=lambda x: getattr(x[1], sort_field), reverse=(sort_dir=='desc'))
343
350
344
351
345 page_url = webhelpers.paginate.PageURL(
352 page_url = webhelpers.paginate.PageURL(
346 self.request.path, self.request.GET)
353 self.request.path, self.request.GET)
347 page = safe_int(self.request.GET.get('page', 1), 1)
354 page = safe_int(self.request.GET.get('page', 1), 1)
348
355
349 integrations = Page(integrations, page=page, items_per_page=10,
356 integrations = Page(integrations, page=page, items_per_page=10,
350 url=page_url)
357 url=page_url)
351
358
352 template_context = {
359 template_context = {
353 'sort_field': sort_field,
360 'sort_field': sort_field,
354 'rev_sort_dir': sort_dir != 'desc' and 'desc' or 'asc',
361 'rev_sort_dir': sort_dir != 'desc' and 'desc' or 'asc',
355 'current_IntegrationType': self.IntegrationType,
362 'current_IntegrationType': self.IntegrationType,
356 'integrations_list': integrations,
363 'integrations_list': integrations,
357 'available_integrations': integration_type_registry,
364 'available_integrations': integration_type_registry,
358 'c': self._template_c_context(),
365 'c': self._template_c_context(),
359 'request': self.request,
366 'request': self.request,
360 }
367 }
361 return template_context
368 return template_context
362
369
363 def new_integration(self):
370 def new_integration(self):
364 template_context = {
371 template_context = {
365 'available_integrations': integration_type_registry,
372 'available_integrations': integration_type_registry,
366 'c': self._template_c_context(),
373 'c': self._template_c_context(),
367 }
374 }
368 return template_context
375 return template_context
369
376
370 class GlobalIntegrationsView(IntegrationSettingsViewBase):
377 class GlobalIntegrationsView(IntegrationSettingsViewBase):
371 def perm_check(self, user):
378 def perm_check(self, user):
372 return auth.HasPermissionAll('hg.admin').check_permissions(user=user)
379 return auth.HasPermissionAll('hg.admin').check_permissions(user=user)
373
380
374
381
375 class RepoIntegrationsView(IntegrationSettingsViewBase):
382 class RepoIntegrationsView(IntegrationSettingsViewBase):
376 def perm_check(self, user):
383 def perm_check(self, user):
377 return auth.HasRepoPermissionAll('repository.admin'
384 return auth.HasRepoPermissionAll('repository.admin'
378 )(repo_name=self.repo.repo_name, user=user)
385 )(repo_name=self.repo.repo_name, user=user)
379
386
380
387
381 class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
388 class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
382 def perm_check(self, user):
389 def perm_check(self, user):
383 return auth.HasRepoGroupPermissionAll('group.admin'
390 return auth.HasRepoGroupPermissionAll('group.admin'
384 )(group_name=self.repo_group.group_name, user=user)
391 )(group_name=self.repo_group.group_name, user=user)
385
392
@@ -1,3534 +1,3516 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import os
25 import os
26 import sys
26 import sys
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37
37
38 from sqlalchemy import *
38 from sqlalchemy import *
39 from sqlalchemy.exc import IntegrityError
39 from sqlalchemy.exc import IntegrityError
40 from sqlalchemy.ext.declarative import declared_attr
40 from sqlalchemy.ext.declarative import declared_attr
41 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.ext.hybrid import hybrid_property
42 from sqlalchemy.orm import (
42 from sqlalchemy.orm import (
43 relationship, joinedload, class_mapper, validates, aliased)
43 relationship, joinedload, class_mapper, validates, aliased)
44 from sqlalchemy.sql.expression import true
44 from sqlalchemy.sql.expression import true
45 from beaker.cache import cache_region, region_invalidate
45 from beaker.cache import cache_region, region_invalidate
46 from webob.exc import HTTPNotFound
46 from webob.exc import HTTPNotFound
47 from zope.cachedescriptors.property import Lazy as LazyProperty
47 from zope.cachedescriptors.property import Lazy as LazyProperty
48
48
49 from pylons import url
49 from pylons import url
50 from pylons.i18n.translation import lazy_ugettext as _
50 from pylons.i18n.translation import lazy_ugettext as _
51
51
52 from rhodecode.lib.vcs import get_backend, get_vcs_instance
52 from rhodecode.lib.vcs import get_backend, get_vcs_instance
53 from rhodecode.lib.vcs.utils.helpers import get_scm
53 from rhodecode.lib.vcs.utils.helpers import get_scm
54 from rhodecode.lib.vcs.exceptions import VCSError
54 from rhodecode.lib.vcs.exceptions import VCSError
55 from rhodecode.lib.vcs.backends.base import (
55 from rhodecode.lib.vcs.backends.base import (
56 EmptyCommit, Reference, MergeFailureReason)
56 EmptyCommit, Reference, MergeFailureReason)
57 from rhodecode.lib.utils2 import (
57 from rhodecode.lib.utils2 import (
58 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
58 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
59 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict)
59 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict)
60 from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
60 from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
61 from rhodecode.lib.ext_json import json
61 from rhodecode.lib.ext_json import json
62 from rhodecode.lib.caching_query import FromCache
62 from rhodecode.lib.caching_query import FromCache
63 from rhodecode.lib.encrypt import AESCipher
63 from rhodecode.lib.encrypt import AESCipher
64
64
65 from rhodecode.model.meta import Base, Session
65 from rhodecode.model.meta import Base, Session
66
66
67 URL_SEP = '/'
67 URL_SEP = '/'
68 log = logging.getLogger(__name__)
68 log = logging.getLogger(__name__)
69
69
70 # =============================================================================
70 # =============================================================================
71 # BASE CLASSES
71 # BASE CLASSES
72 # =============================================================================
72 # =============================================================================
73
73
74 # this is propagated from .ini file rhodecode.encrypted_values.secret or
74 # this is propagated from .ini file rhodecode.encrypted_values.secret or
75 # beaker.session.secret if first is not set.
75 # beaker.session.secret if first is not set.
76 # and initialized at environment.py
76 # and initialized at environment.py
77 ENCRYPTION_KEY = None
77 ENCRYPTION_KEY = None
78
78
79 # used to sort permissions by types, '#' used here is not allowed to be in
79 # used to sort permissions by types, '#' used here is not allowed to be in
80 # usernames, and it's very early in sorted string.printable table.
80 # usernames, and it's very early in sorted string.printable table.
81 PERMISSION_TYPE_SORT = {
81 PERMISSION_TYPE_SORT = {
82 'admin': '####',
82 'admin': '####',
83 'write': '###',
83 'write': '###',
84 'read': '##',
84 'read': '##',
85 'none': '#',
85 'none': '#',
86 }
86 }
87
87
88
88
89 def display_sort(obj):
89 def display_sort(obj):
90 """
90 """
91 Sort function used to sort permissions in .permissions() function of
91 Sort function used to sort permissions in .permissions() function of
92 Repository, RepoGroup, UserGroup. Also it put the default user in front
92 Repository, RepoGroup, UserGroup. Also it put the default user in front
93 of all other resources
93 of all other resources
94 """
94 """
95
95
96 if obj.username == User.DEFAULT_USER:
96 if obj.username == User.DEFAULT_USER:
97 return '#####'
97 return '#####'
98 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
98 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
99 return prefix + obj.username
99 return prefix + obj.username
100
100
101
101
102 def _hash_key(k):
102 def _hash_key(k):
103 return md5_safe(k)
103 return md5_safe(k)
104
104
105
105
106 class EncryptedTextValue(TypeDecorator):
106 class EncryptedTextValue(TypeDecorator):
107 """
107 """
108 Special column for encrypted long text data, use like::
108 Special column for encrypted long text data, use like::
109
109
110 value = Column("encrypted_value", EncryptedValue(), nullable=False)
110 value = Column("encrypted_value", EncryptedValue(), nullable=False)
111
111
112 This column is intelligent so if value is in unencrypted form it return
112 This column is intelligent so if value is in unencrypted form it return
113 unencrypted form, but on save it always encrypts
113 unencrypted form, but on save it always encrypts
114 """
114 """
115 impl = Text
115 impl = Text
116
116
117 def process_bind_param(self, value, dialect):
117 def process_bind_param(self, value, dialect):
118 if not value:
118 if not value:
119 return value
119 return value
120 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
120 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
121 # protect against double encrypting if someone manually starts
121 # protect against double encrypting if someone manually starts
122 # doing
122 # doing
123 raise ValueError('value needs to be in unencrypted format, ie. '
123 raise ValueError('value needs to be in unencrypted format, ie. '
124 'not starting with enc$aes')
124 'not starting with enc$aes')
125 return 'enc$aes_hmac$%s' % AESCipher(
125 return 'enc$aes_hmac$%s' % AESCipher(
126 ENCRYPTION_KEY, hmac=True).encrypt(value)
126 ENCRYPTION_KEY, hmac=True).encrypt(value)
127
127
128 def process_result_value(self, value, dialect):
128 def process_result_value(self, value, dialect):
129 import rhodecode
129 import rhodecode
130
130
131 if not value:
131 if not value:
132 return value
132 return value
133
133
134 parts = value.split('$', 3)
134 parts = value.split('$', 3)
135 if not len(parts) == 3:
135 if not len(parts) == 3:
136 # probably not encrypted values
136 # probably not encrypted values
137 return value
137 return value
138 else:
138 else:
139 if parts[0] != 'enc':
139 if parts[0] != 'enc':
140 # parts ok but without our header ?
140 # parts ok but without our header ?
141 return value
141 return value
142 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
142 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
143 'rhodecode.encrypted_values.strict') or True)
143 'rhodecode.encrypted_values.strict') or True)
144 # at that stage we know it's our encryption
144 # at that stage we know it's our encryption
145 if parts[1] == 'aes':
145 if parts[1] == 'aes':
146 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
146 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
147 elif parts[1] == 'aes_hmac':
147 elif parts[1] == 'aes_hmac':
148 decrypted_data = AESCipher(
148 decrypted_data = AESCipher(
149 ENCRYPTION_KEY, hmac=True,
149 ENCRYPTION_KEY, hmac=True,
150 strict_verification=enc_strict_mode).decrypt(parts[2])
150 strict_verification=enc_strict_mode).decrypt(parts[2])
151 else:
151 else:
152 raise ValueError(
152 raise ValueError(
153 'Encryption type part is wrong, must be `aes` '
153 'Encryption type part is wrong, must be `aes` '
154 'or `aes_hmac`, got `%s` instead' % (parts[1]))
154 'or `aes_hmac`, got `%s` instead' % (parts[1]))
155 return decrypted_data
155 return decrypted_data
156
156
157
157
158 class BaseModel(object):
158 class BaseModel(object):
159 """
159 """
160 Base Model for all classes
160 Base Model for all classes
161 """
161 """
162
162
163 @classmethod
163 @classmethod
164 def _get_keys(cls):
164 def _get_keys(cls):
165 """return column names for this model """
165 """return column names for this model """
166 return class_mapper(cls).c.keys()
166 return class_mapper(cls).c.keys()
167
167
168 def get_dict(self):
168 def get_dict(self):
169 """
169 """
170 return dict with keys and values corresponding
170 return dict with keys and values corresponding
171 to this model data """
171 to this model data """
172
172
173 d = {}
173 d = {}
174 for k in self._get_keys():
174 for k in self._get_keys():
175 d[k] = getattr(self, k)
175 d[k] = getattr(self, k)
176
176
177 # also use __json__() if present to get additional fields
177 # also use __json__() if present to get additional fields
178 _json_attr = getattr(self, '__json__', None)
178 _json_attr = getattr(self, '__json__', None)
179 if _json_attr:
179 if _json_attr:
180 # update with attributes from __json__
180 # update with attributes from __json__
181 if callable(_json_attr):
181 if callable(_json_attr):
182 _json_attr = _json_attr()
182 _json_attr = _json_attr()
183 for k, val in _json_attr.iteritems():
183 for k, val in _json_attr.iteritems():
184 d[k] = val
184 d[k] = val
185 return d
185 return d
186
186
187 def get_appstruct(self):
187 def get_appstruct(self):
188 """return list with keys and values tuples corresponding
188 """return list with keys and values tuples corresponding
189 to this model data """
189 to this model data """
190
190
191 l = []
191 l = []
192 for k in self._get_keys():
192 for k in self._get_keys():
193 l.append((k, getattr(self, k),))
193 l.append((k, getattr(self, k),))
194 return l
194 return l
195
195
196 def populate_obj(self, populate_dict):
196 def populate_obj(self, populate_dict):
197 """populate model with data from given populate_dict"""
197 """populate model with data from given populate_dict"""
198
198
199 for k in self._get_keys():
199 for k in self._get_keys():
200 if k in populate_dict:
200 if k in populate_dict:
201 setattr(self, k, populate_dict[k])
201 setattr(self, k, populate_dict[k])
202
202
203 @classmethod
203 @classmethod
204 def query(cls):
204 def query(cls):
205 return Session().query(cls)
205 return Session().query(cls)
206
206
207 @classmethod
207 @classmethod
208 def get(cls, id_):
208 def get(cls, id_):
209 if id_:
209 if id_:
210 return cls.query().get(id_)
210 return cls.query().get(id_)
211
211
212 @classmethod
212 @classmethod
213 def get_or_404(cls, id_):
213 def get_or_404(cls, id_):
214 try:
214 try:
215 id_ = int(id_)
215 id_ = int(id_)
216 except (TypeError, ValueError):
216 except (TypeError, ValueError):
217 raise HTTPNotFound
217 raise HTTPNotFound
218
218
219 res = cls.query().get(id_)
219 res = cls.query().get(id_)
220 if not res:
220 if not res:
221 raise HTTPNotFound
221 raise HTTPNotFound
222 return res
222 return res
223
223
224 @classmethod
224 @classmethod
225 def getAll(cls):
225 def getAll(cls):
226 # deprecated and left for backward compatibility
226 # deprecated and left for backward compatibility
227 return cls.get_all()
227 return cls.get_all()
228
228
229 @classmethod
229 @classmethod
230 def get_all(cls):
230 def get_all(cls):
231 return cls.query().all()
231 return cls.query().all()
232
232
233 @classmethod
233 @classmethod
234 def delete(cls, id_):
234 def delete(cls, id_):
235 obj = cls.query().get(id_)
235 obj = cls.query().get(id_)
236 Session().delete(obj)
236 Session().delete(obj)
237
237
238 @classmethod
238 @classmethod
239 def identity_cache(cls, session, attr_name, value):
239 def identity_cache(cls, session, attr_name, value):
240 exist_in_session = []
240 exist_in_session = []
241 for (item_cls, pkey), instance in session.identity_map.items():
241 for (item_cls, pkey), instance in session.identity_map.items():
242 if cls == item_cls and getattr(instance, attr_name) == value:
242 if cls == item_cls and getattr(instance, attr_name) == value:
243 exist_in_session.append(instance)
243 exist_in_session.append(instance)
244 if exist_in_session:
244 if exist_in_session:
245 if len(exist_in_session) == 1:
245 if len(exist_in_session) == 1:
246 return exist_in_session[0]
246 return exist_in_session[0]
247 log.exception(
247 log.exception(
248 'multiple objects with attr %s and '
248 'multiple objects with attr %s and '
249 'value %s found with same name: %r',
249 'value %s found with same name: %r',
250 attr_name, value, exist_in_session)
250 attr_name, value, exist_in_session)
251
251
252 def __repr__(self):
252 def __repr__(self):
253 if hasattr(self, '__unicode__'):
253 if hasattr(self, '__unicode__'):
254 # python repr needs to return str
254 # python repr needs to return str
255 try:
255 try:
256 return safe_str(self.__unicode__())
256 return safe_str(self.__unicode__())
257 except UnicodeDecodeError:
257 except UnicodeDecodeError:
258 pass
258 pass
259 return '<DB:%s>' % (self.__class__.__name__)
259 return '<DB:%s>' % (self.__class__.__name__)
260
260
261
261
262 class RhodeCodeSetting(Base, BaseModel):
262 class RhodeCodeSetting(Base, BaseModel):
263 __tablename__ = 'rhodecode_settings'
263 __tablename__ = 'rhodecode_settings'
264 __table_args__ = (
264 __table_args__ = (
265 UniqueConstraint('app_settings_name'),
265 UniqueConstraint('app_settings_name'),
266 {'extend_existing': True, 'mysql_engine': 'InnoDB',
266 {'extend_existing': True, 'mysql_engine': 'InnoDB',
267 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
267 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
268 )
268 )
269
269
270 SETTINGS_TYPES = {
270 SETTINGS_TYPES = {
271 'str': safe_str,
271 'str': safe_str,
272 'int': safe_int,
272 'int': safe_int,
273 'unicode': safe_unicode,
273 'unicode': safe_unicode,
274 'bool': str2bool,
274 'bool': str2bool,
275 'list': functools.partial(aslist, sep=',')
275 'list': functools.partial(aslist, sep=',')
276 }
276 }
277 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
277 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
278 GLOBAL_CONF_KEY = 'app_settings'
278 GLOBAL_CONF_KEY = 'app_settings'
279
279
280 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
280 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
281 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
281 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
282 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
282 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
283 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
283 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
284
284
285 def __init__(self, key='', val='', type='unicode'):
285 def __init__(self, key='', val='', type='unicode'):
286 self.app_settings_name = key
286 self.app_settings_name = key
287 self.app_settings_type = type
287 self.app_settings_type = type
288 self.app_settings_value = val
288 self.app_settings_value = val
289
289
290 @validates('_app_settings_value')
290 @validates('_app_settings_value')
291 def validate_settings_value(self, key, val):
291 def validate_settings_value(self, key, val):
292 assert type(val) == unicode
292 assert type(val) == unicode
293 return val
293 return val
294
294
295 @hybrid_property
295 @hybrid_property
296 def app_settings_value(self):
296 def app_settings_value(self):
297 v = self._app_settings_value
297 v = self._app_settings_value
298 _type = self.app_settings_type
298 _type = self.app_settings_type
299 if _type:
299 if _type:
300 _type = self.app_settings_type.split('.')[0]
300 _type = self.app_settings_type.split('.')[0]
301 # decode the encrypted value
301 # decode the encrypted value
302 if 'encrypted' in self.app_settings_type:
302 if 'encrypted' in self.app_settings_type:
303 cipher = EncryptedTextValue()
303 cipher = EncryptedTextValue()
304 v = safe_unicode(cipher.process_result_value(v, None))
304 v = safe_unicode(cipher.process_result_value(v, None))
305
305
306 converter = self.SETTINGS_TYPES.get(_type) or \
306 converter = self.SETTINGS_TYPES.get(_type) or \
307 self.SETTINGS_TYPES['unicode']
307 self.SETTINGS_TYPES['unicode']
308 return converter(v)
308 return converter(v)
309
309
310 @app_settings_value.setter
310 @app_settings_value.setter
311 def app_settings_value(self, val):
311 def app_settings_value(self, val):
312 """
312 """
313 Setter that will always make sure we use unicode in app_settings_value
313 Setter that will always make sure we use unicode in app_settings_value
314
314
315 :param val:
315 :param val:
316 """
316 """
317 val = safe_unicode(val)
317 val = safe_unicode(val)
318 # encode the encrypted value
318 # encode the encrypted value
319 if 'encrypted' in self.app_settings_type:
319 if 'encrypted' in self.app_settings_type:
320 cipher = EncryptedTextValue()
320 cipher = EncryptedTextValue()
321 val = safe_unicode(cipher.process_bind_param(val, None))
321 val = safe_unicode(cipher.process_bind_param(val, None))
322 self._app_settings_value = val
322 self._app_settings_value = val
323
323
324 @hybrid_property
324 @hybrid_property
325 def app_settings_type(self):
325 def app_settings_type(self):
326 return self._app_settings_type
326 return self._app_settings_type
327
327
328 @app_settings_type.setter
328 @app_settings_type.setter
329 def app_settings_type(self, val):
329 def app_settings_type(self, val):
330 if val.split('.')[0] not in self.SETTINGS_TYPES:
330 if val.split('.')[0] not in self.SETTINGS_TYPES:
331 raise Exception('type must be one of %s got %s'
331 raise Exception('type must be one of %s got %s'
332 % (self.SETTINGS_TYPES.keys(), val))
332 % (self.SETTINGS_TYPES.keys(), val))
333 self._app_settings_type = val
333 self._app_settings_type = val
334
334
335 def __unicode__(self):
335 def __unicode__(self):
336 return u"<%s('%s:%s[%s]')>" % (
336 return u"<%s('%s:%s[%s]')>" % (
337 self.__class__.__name__,
337 self.__class__.__name__,
338 self.app_settings_name, self.app_settings_value,
338 self.app_settings_name, self.app_settings_value,
339 self.app_settings_type
339 self.app_settings_type
340 )
340 )
341
341
342
342
343 class RhodeCodeUi(Base, BaseModel):
343 class RhodeCodeUi(Base, BaseModel):
344 __tablename__ = 'rhodecode_ui'
344 __tablename__ = 'rhodecode_ui'
345 __table_args__ = (
345 __table_args__ = (
346 UniqueConstraint('ui_key'),
346 UniqueConstraint('ui_key'),
347 {'extend_existing': True, 'mysql_engine': 'InnoDB',
347 {'extend_existing': True, 'mysql_engine': 'InnoDB',
348 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
348 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
349 )
349 )
350
350
351 HOOK_REPO_SIZE = 'changegroup.repo_size'
351 HOOK_REPO_SIZE = 'changegroup.repo_size'
352 # HG
352 # HG
353 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
353 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
354 HOOK_PULL = 'outgoing.pull_logger'
354 HOOK_PULL = 'outgoing.pull_logger'
355 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
355 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
356 HOOK_PUSH = 'changegroup.push_logger'
356 HOOK_PUSH = 'changegroup.push_logger'
357
357
358 # TODO: johbo: Unify way how hooks are configured for git and hg,
358 # TODO: johbo: Unify way how hooks are configured for git and hg,
359 # git part is currently hardcoded.
359 # git part is currently hardcoded.
360
360
361 # SVN PATTERNS
361 # SVN PATTERNS
362 SVN_BRANCH_ID = 'vcs_svn_branch'
362 SVN_BRANCH_ID = 'vcs_svn_branch'
363 SVN_TAG_ID = 'vcs_svn_tag'
363 SVN_TAG_ID = 'vcs_svn_tag'
364
364
365 ui_id = Column(
365 ui_id = Column(
366 "ui_id", Integer(), nullable=False, unique=True, default=None,
366 "ui_id", Integer(), nullable=False, unique=True, default=None,
367 primary_key=True)
367 primary_key=True)
368 ui_section = Column(
368 ui_section = Column(
369 "ui_section", String(255), nullable=True, unique=None, default=None)
369 "ui_section", String(255), nullable=True, unique=None, default=None)
370 ui_key = Column(
370 ui_key = Column(
371 "ui_key", String(255), nullable=True, unique=None, default=None)
371 "ui_key", String(255), nullable=True, unique=None, default=None)
372 ui_value = Column(
372 ui_value = Column(
373 "ui_value", String(255), nullable=True, unique=None, default=None)
373 "ui_value", String(255), nullable=True, unique=None, default=None)
374 ui_active = Column(
374 ui_active = Column(
375 "ui_active", Boolean(), nullable=True, unique=None, default=True)
375 "ui_active", Boolean(), nullable=True, unique=None, default=True)
376
376
377 def __repr__(self):
377 def __repr__(self):
378 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
378 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
379 self.ui_key, self.ui_value)
379 self.ui_key, self.ui_value)
380
380
381
381
382 class RepoRhodeCodeSetting(Base, BaseModel):
382 class RepoRhodeCodeSetting(Base, BaseModel):
383 __tablename__ = 'repo_rhodecode_settings'
383 __tablename__ = 'repo_rhodecode_settings'
384 __table_args__ = (
384 __table_args__ = (
385 UniqueConstraint(
385 UniqueConstraint(
386 'app_settings_name', 'repository_id',
386 'app_settings_name', 'repository_id',
387 name='uq_repo_rhodecode_setting_name_repo_id'),
387 name='uq_repo_rhodecode_setting_name_repo_id'),
388 {'extend_existing': True, 'mysql_engine': 'InnoDB',
388 {'extend_existing': True, 'mysql_engine': 'InnoDB',
389 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
389 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
390 )
390 )
391
391
392 repository_id = Column(
392 repository_id = Column(
393 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
393 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
394 nullable=False)
394 nullable=False)
395 app_settings_id = Column(
395 app_settings_id = Column(
396 "app_settings_id", Integer(), nullable=False, unique=True,
396 "app_settings_id", Integer(), nullable=False, unique=True,
397 default=None, primary_key=True)
397 default=None, primary_key=True)
398 app_settings_name = Column(
398 app_settings_name = Column(
399 "app_settings_name", String(255), nullable=True, unique=None,
399 "app_settings_name", String(255), nullable=True, unique=None,
400 default=None)
400 default=None)
401 _app_settings_value = Column(
401 _app_settings_value = Column(
402 "app_settings_value", String(4096), nullable=True, unique=None,
402 "app_settings_value", String(4096), nullable=True, unique=None,
403 default=None)
403 default=None)
404 _app_settings_type = Column(
404 _app_settings_type = Column(
405 "app_settings_type", String(255), nullable=True, unique=None,
405 "app_settings_type", String(255), nullable=True, unique=None,
406 default=None)
406 default=None)
407
407
408 repository = relationship('Repository')
408 repository = relationship('Repository')
409
409
410 def __init__(self, repository_id, key='', val='', type='unicode'):
410 def __init__(self, repository_id, key='', val='', type='unicode'):
411 self.repository_id = repository_id
411 self.repository_id = repository_id
412 self.app_settings_name = key
412 self.app_settings_name = key
413 self.app_settings_type = type
413 self.app_settings_type = type
414 self.app_settings_value = val
414 self.app_settings_value = val
415
415
416 @validates('_app_settings_value')
416 @validates('_app_settings_value')
417 def validate_settings_value(self, key, val):
417 def validate_settings_value(self, key, val):
418 assert type(val) == unicode
418 assert type(val) == unicode
419 return val
419 return val
420
420
421 @hybrid_property
421 @hybrid_property
422 def app_settings_value(self):
422 def app_settings_value(self):
423 v = self._app_settings_value
423 v = self._app_settings_value
424 type_ = self.app_settings_type
424 type_ = self.app_settings_type
425 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
425 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
426 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
426 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
427 return converter(v)
427 return converter(v)
428
428
429 @app_settings_value.setter
429 @app_settings_value.setter
430 def app_settings_value(self, val):
430 def app_settings_value(self, val):
431 """
431 """
432 Setter that will always make sure we use unicode in app_settings_value
432 Setter that will always make sure we use unicode in app_settings_value
433
433
434 :param val:
434 :param val:
435 """
435 """
436 self._app_settings_value = safe_unicode(val)
436 self._app_settings_value = safe_unicode(val)
437
437
438 @hybrid_property
438 @hybrid_property
439 def app_settings_type(self):
439 def app_settings_type(self):
440 return self._app_settings_type
440 return self._app_settings_type
441
441
442 @app_settings_type.setter
442 @app_settings_type.setter
443 def app_settings_type(self, val):
443 def app_settings_type(self, val):
444 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
444 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
445 if val not in SETTINGS_TYPES:
445 if val not in SETTINGS_TYPES:
446 raise Exception('type must be one of %s got %s'
446 raise Exception('type must be one of %s got %s'
447 % (SETTINGS_TYPES.keys(), val))
447 % (SETTINGS_TYPES.keys(), val))
448 self._app_settings_type = val
448 self._app_settings_type = val
449
449
450 def __unicode__(self):
450 def __unicode__(self):
451 return u"<%s('%s:%s:%s[%s]')>" % (
451 return u"<%s('%s:%s:%s[%s]')>" % (
452 self.__class__.__name__, self.repository.repo_name,
452 self.__class__.__name__, self.repository.repo_name,
453 self.app_settings_name, self.app_settings_value,
453 self.app_settings_name, self.app_settings_value,
454 self.app_settings_type
454 self.app_settings_type
455 )
455 )
456
456
457
457
458 class RepoRhodeCodeUi(Base, BaseModel):
458 class RepoRhodeCodeUi(Base, BaseModel):
459 __tablename__ = 'repo_rhodecode_ui'
459 __tablename__ = 'repo_rhodecode_ui'
460 __table_args__ = (
460 __table_args__ = (
461 UniqueConstraint(
461 UniqueConstraint(
462 'repository_id', 'ui_section', 'ui_key',
462 'repository_id', 'ui_section', 'ui_key',
463 name='uq_repo_rhodecode_ui_repository_id_section_key'),
463 name='uq_repo_rhodecode_ui_repository_id_section_key'),
464 {'extend_existing': True, 'mysql_engine': 'InnoDB',
464 {'extend_existing': True, 'mysql_engine': 'InnoDB',
465 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
465 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
466 )
466 )
467
467
468 repository_id = Column(
468 repository_id = Column(
469 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
469 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
470 nullable=False)
470 nullable=False)
471 ui_id = Column(
471 ui_id = Column(
472 "ui_id", Integer(), nullable=False, unique=True, default=None,
472 "ui_id", Integer(), nullable=False, unique=True, default=None,
473 primary_key=True)
473 primary_key=True)
474 ui_section = Column(
474 ui_section = Column(
475 "ui_section", String(255), nullable=True, unique=None, default=None)
475 "ui_section", String(255), nullable=True, unique=None, default=None)
476 ui_key = Column(
476 ui_key = Column(
477 "ui_key", String(255), nullable=True, unique=None, default=None)
477 "ui_key", String(255), nullable=True, unique=None, default=None)
478 ui_value = Column(
478 ui_value = Column(
479 "ui_value", String(255), nullable=True, unique=None, default=None)
479 "ui_value", String(255), nullable=True, unique=None, default=None)
480 ui_active = Column(
480 ui_active = Column(
481 "ui_active", Boolean(), nullable=True, unique=None, default=True)
481 "ui_active", Boolean(), nullable=True, unique=None, default=True)
482
482
483 repository = relationship('Repository')
483 repository = relationship('Repository')
484
484
485 def __repr__(self):
485 def __repr__(self):
486 return '<%s[%s:%s]%s=>%s]>' % (
486 return '<%s[%s:%s]%s=>%s]>' % (
487 self.__class__.__name__, self.repository.repo_name,
487 self.__class__.__name__, self.repository.repo_name,
488 self.ui_section, self.ui_key, self.ui_value)
488 self.ui_section, self.ui_key, self.ui_value)
489
489
490
490
491 class User(Base, BaseModel):
491 class User(Base, BaseModel):
492 __tablename__ = 'users'
492 __tablename__ = 'users'
493 __table_args__ = (
493 __table_args__ = (
494 UniqueConstraint('username'), UniqueConstraint('email'),
494 UniqueConstraint('username'), UniqueConstraint('email'),
495 Index('u_username_idx', 'username'),
495 Index('u_username_idx', 'username'),
496 Index('u_email_idx', 'email'),
496 Index('u_email_idx', 'email'),
497 {'extend_existing': True, 'mysql_engine': 'InnoDB',
497 {'extend_existing': True, 'mysql_engine': 'InnoDB',
498 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
498 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
499 )
499 )
500 DEFAULT_USER = 'default'
500 DEFAULT_USER = 'default'
501 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
501 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
502 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
502 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
503
503
504 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
504 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
505 username = Column("username", String(255), nullable=True, unique=None, default=None)
505 username = Column("username", String(255), nullable=True, unique=None, default=None)
506 password = Column("password", String(255), nullable=True, unique=None, default=None)
506 password = Column("password", String(255), nullable=True, unique=None, default=None)
507 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
507 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
508 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
508 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
509 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
509 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
510 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
510 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
511 _email = Column("email", String(255), nullable=True, unique=None, default=None)
511 _email = Column("email", String(255), nullable=True, unique=None, default=None)
512 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
512 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
513 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
513 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
514 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
514 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
515 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
515 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
516 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
516 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
517 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
517 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
518 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
518 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
519
519
520 user_log = relationship('UserLog')
520 user_log = relationship('UserLog')
521 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
521 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
522
522
523 repositories = relationship('Repository')
523 repositories = relationship('Repository')
524 repository_groups = relationship('RepoGroup')
524 repository_groups = relationship('RepoGroup')
525 user_groups = relationship('UserGroup')
525 user_groups = relationship('UserGroup')
526
526
527 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
527 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
528 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
528 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
529
529
530 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
530 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
531 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
531 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
532 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
532 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
533
533
534 group_member = relationship('UserGroupMember', cascade='all')
534 group_member = relationship('UserGroupMember', cascade='all')
535
535
536 notifications = relationship('UserNotification', cascade='all')
536 notifications = relationship('UserNotification', cascade='all')
537 # notifications assigned to this user
537 # notifications assigned to this user
538 user_created_notifications = relationship('Notification', cascade='all')
538 user_created_notifications = relationship('Notification', cascade='all')
539 # comments created by this user
539 # comments created by this user
540 user_comments = relationship('ChangesetComment', cascade='all')
540 user_comments = relationship('ChangesetComment', cascade='all')
541 # user profile extra info
541 # user profile extra info
542 user_emails = relationship('UserEmailMap', cascade='all')
542 user_emails = relationship('UserEmailMap', cascade='all')
543 user_ip_map = relationship('UserIpMap', cascade='all')
543 user_ip_map = relationship('UserIpMap', cascade='all')
544 user_auth_tokens = relationship('UserApiKeys', cascade='all')
544 user_auth_tokens = relationship('UserApiKeys', cascade='all')
545 # gists
545 # gists
546 user_gists = relationship('Gist', cascade='all')
546 user_gists = relationship('Gist', cascade='all')
547 # user pull requests
547 # user pull requests
548 user_pull_requests = relationship('PullRequest', cascade='all')
548 user_pull_requests = relationship('PullRequest', cascade='all')
549 # external identities
549 # external identities
550 extenal_identities = relationship(
550 extenal_identities = relationship(
551 'ExternalIdentity',
551 'ExternalIdentity',
552 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
552 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
553 cascade='all')
553 cascade='all')
554
554
555 def __unicode__(self):
555 def __unicode__(self):
556 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
556 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
557 self.user_id, self.username)
557 self.user_id, self.username)
558
558
559 @hybrid_property
559 @hybrid_property
560 def email(self):
560 def email(self):
561 return self._email
561 return self._email
562
562
563 @email.setter
563 @email.setter
564 def email(self, val):
564 def email(self, val):
565 self._email = val.lower() if val else None
565 self._email = val.lower() if val else None
566
566
567 @property
567 @property
568 def firstname(self):
568 def firstname(self):
569 # alias for future
569 # alias for future
570 return self.name
570 return self.name
571
571
572 @property
572 @property
573 def emails(self):
573 def emails(self):
574 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
574 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
575 return [self.email] + [x.email for x in other]
575 return [self.email] + [x.email for x in other]
576
576
577 @property
577 @property
578 def auth_tokens(self):
578 def auth_tokens(self):
579 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
579 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
580
580
581 @property
581 @property
582 def extra_auth_tokens(self):
582 def extra_auth_tokens(self):
583 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
583 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
584
584
585 @property
585 @property
586 def feed_token(self):
586 def feed_token(self):
587 feed_tokens = UserApiKeys.query()\
587 feed_tokens = UserApiKeys.query()\
588 .filter(UserApiKeys.user == self)\
588 .filter(UserApiKeys.user == self)\
589 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
589 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
590 .all()
590 .all()
591 if feed_tokens:
591 if feed_tokens:
592 return feed_tokens[0].api_key
592 return feed_tokens[0].api_key
593 else:
593 else:
594 # use the main token so we don't end up with nothing...
594 # use the main token so we don't end up with nothing...
595 return self.api_key
595 return self.api_key
596
596
597 @classmethod
597 @classmethod
598 def extra_valid_auth_tokens(cls, user, role=None):
598 def extra_valid_auth_tokens(cls, user, role=None):
599 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
599 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
600 .filter(or_(UserApiKeys.expires == -1,
600 .filter(or_(UserApiKeys.expires == -1,
601 UserApiKeys.expires >= time.time()))
601 UserApiKeys.expires >= time.time()))
602 if role:
602 if role:
603 tokens = tokens.filter(or_(UserApiKeys.role == role,
603 tokens = tokens.filter(or_(UserApiKeys.role == role,
604 UserApiKeys.role == UserApiKeys.ROLE_ALL))
604 UserApiKeys.role == UserApiKeys.ROLE_ALL))
605 return tokens.all()
605 return tokens.all()
606
606
607 @property
607 @property
608 def ip_addresses(self):
608 def ip_addresses(self):
609 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
609 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
610 return [x.ip_addr for x in ret]
610 return [x.ip_addr for x in ret]
611
611
612 @property
612 @property
613 def username_and_name(self):
613 def username_and_name(self):
614 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
614 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
615
615
616 @property
616 @property
617 def username_or_name_or_email(self):
617 def username_or_name_or_email(self):
618 full_name = self.full_name if self.full_name is not ' ' else None
618 full_name = self.full_name if self.full_name is not ' ' else None
619 return self.username or full_name or self.email
619 return self.username or full_name or self.email
620
620
621 @property
621 @property
622 def full_name(self):
622 def full_name(self):
623 return '%s %s' % (self.firstname, self.lastname)
623 return '%s %s' % (self.firstname, self.lastname)
624
624
625 @property
625 @property
626 def full_name_or_username(self):
626 def full_name_or_username(self):
627 return ('%s %s' % (self.firstname, self.lastname)
627 return ('%s %s' % (self.firstname, self.lastname)
628 if (self.firstname and self.lastname) else self.username)
628 if (self.firstname and self.lastname) else self.username)
629
629
630 @property
630 @property
631 def full_contact(self):
631 def full_contact(self):
632 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
632 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
633
633
634 @property
634 @property
635 def short_contact(self):
635 def short_contact(self):
636 return '%s %s' % (self.firstname, self.lastname)
636 return '%s %s' % (self.firstname, self.lastname)
637
637
638 @property
638 @property
639 def is_admin(self):
639 def is_admin(self):
640 return self.admin
640 return self.admin
641
641
642 @property
642 @property
643 def AuthUser(self):
643 def AuthUser(self):
644 """
644 """
645 Returns instance of AuthUser for this user
645 Returns instance of AuthUser for this user
646 """
646 """
647 from rhodecode.lib.auth import AuthUser
647 from rhodecode.lib.auth import AuthUser
648 return AuthUser(user_id=self.user_id, api_key=self.api_key,
648 return AuthUser(user_id=self.user_id, api_key=self.api_key,
649 username=self.username)
649 username=self.username)
650
650
651 @hybrid_property
651 @hybrid_property
652 def user_data(self):
652 def user_data(self):
653 if not self._user_data:
653 if not self._user_data:
654 return {}
654 return {}
655
655
656 try:
656 try:
657 return json.loads(self._user_data)
657 return json.loads(self._user_data)
658 except TypeError:
658 except TypeError:
659 return {}
659 return {}
660
660
661 @user_data.setter
661 @user_data.setter
662 def user_data(self, val):
662 def user_data(self, val):
663 if not isinstance(val, dict):
663 if not isinstance(val, dict):
664 raise Exception('user_data must be dict, got %s' % type(val))
664 raise Exception('user_data must be dict, got %s' % type(val))
665 try:
665 try:
666 self._user_data = json.dumps(val)
666 self._user_data = json.dumps(val)
667 except Exception:
667 except Exception:
668 log.error(traceback.format_exc())
668 log.error(traceback.format_exc())
669
669
670 @classmethod
670 @classmethod
671 def get_by_username(cls, username, case_insensitive=False,
671 def get_by_username(cls, username, case_insensitive=False,
672 cache=False, identity_cache=False):
672 cache=False, identity_cache=False):
673 session = Session()
673 session = Session()
674
674
675 if case_insensitive:
675 if case_insensitive:
676 q = cls.query().filter(
676 q = cls.query().filter(
677 func.lower(cls.username) == func.lower(username))
677 func.lower(cls.username) == func.lower(username))
678 else:
678 else:
679 q = cls.query().filter(cls.username == username)
679 q = cls.query().filter(cls.username == username)
680
680
681 if cache:
681 if cache:
682 if identity_cache:
682 if identity_cache:
683 val = cls.identity_cache(session, 'username', username)
683 val = cls.identity_cache(session, 'username', username)
684 if val:
684 if val:
685 return val
685 return val
686 else:
686 else:
687 q = q.options(
687 q = q.options(
688 FromCache("sql_cache_short",
688 FromCache("sql_cache_short",
689 "get_user_by_name_%s" % _hash_key(username)))
689 "get_user_by_name_%s" % _hash_key(username)))
690
690
691 return q.scalar()
691 return q.scalar()
692
692
693 @classmethod
693 @classmethod
694 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
694 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
695 q = cls.query().filter(cls.api_key == auth_token)
695 q = cls.query().filter(cls.api_key == auth_token)
696
696
697 if cache:
697 if cache:
698 q = q.options(FromCache("sql_cache_short",
698 q = q.options(FromCache("sql_cache_short",
699 "get_auth_token_%s" % auth_token))
699 "get_auth_token_%s" % auth_token))
700 res = q.scalar()
700 res = q.scalar()
701
701
702 if fallback and not res:
702 if fallback and not res:
703 #fallback to additional keys
703 #fallback to additional keys
704 _res = UserApiKeys.query()\
704 _res = UserApiKeys.query()\
705 .filter(UserApiKeys.api_key == auth_token)\
705 .filter(UserApiKeys.api_key == auth_token)\
706 .filter(or_(UserApiKeys.expires == -1,
706 .filter(or_(UserApiKeys.expires == -1,
707 UserApiKeys.expires >= time.time()))\
707 UserApiKeys.expires >= time.time()))\
708 .first()
708 .first()
709 if _res:
709 if _res:
710 res = _res.user
710 res = _res.user
711 return res
711 return res
712
712
713 @classmethod
713 @classmethod
714 def get_by_email(cls, email, case_insensitive=False, cache=False):
714 def get_by_email(cls, email, case_insensitive=False, cache=False):
715
715
716 if case_insensitive:
716 if case_insensitive:
717 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
717 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
718
718
719 else:
719 else:
720 q = cls.query().filter(cls.email == email)
720 q = cls.query().filter(cls.email == email)
721
721
722 if cache:
722 if cache:
723 q = q.options(FromCache("sql_cache_short",
723 q = q.options(FromCache("sql_cache_short",
724 "get_email_key_%s" % _hash_key(email)))
724 "get_email_key_%s" % _hash_key(email)))
725
725
726 ret = q.scalar()
726 ret = q.scalar()
727 if ret is None:
727 if ret is None:
728 q = UserEmailMap.query()
728 q = UserEmailMap.query()
729 # try fetching in alternate email map
729 # try fetching in alternate email map
730 if case_insensitive:
730 if case_insensitive:
731 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
731 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
732 else:
732 else:
733 q = q.filter(UserEmailMap.email == email)
733 q = q.filter(UserEmailMap.email == email)
734 q = q.options(joinedload(UserEmailMap.user))
734 q = q.options(joinedload(UserEmailMap.user))
735 if cache:
735 if cache:
736 q = q.options(FromCache("sql_cache_short",
736 q = q.options(FromCache("sql_cache_short",
737 "get_email_map_key_%s" % email))
737 "get_email_map_key_%s" % email))
738 ret = getattr(q.scalar(), 'user', None)
738 ret = getattr(q.scalar(), 'user', None)
739
739
740 return ret
740 return ret
741
741
742 @classmethod
742 @classmethod
743 def get_from_cs_author(cls, author):
743 def get_from_cs_author(cls, author):
744 """
744 """
745 Tries to get User objects out of commit author string
745 Tries to get User objects out of commit author string
746
746
747 :param author:
747 :param author:
748 """
748 """
749 from rhodecode.lib.helpers import email, author_name
749 from rhodecode.lib.helpers import email, author_name
750 # Valid email in the attribute passed, see if they're in the system
750 # Valid email in the attribute passed, see if they're in the system
751 _email = email(author)
751 _email = email(author)
752 if _email:
752 if _email:
753 user = cls.get_by_email(_email, case_insensitive=True)
753 user = cls.get_by_email(_email, case_insensitive=True)
754 if user:
754 if user:
755 return user
755 return user
756 # Maybe we can match by username?
756 # Maybe we can match by username?
757 _author = author_name(author)
757 _author = author_name(author)
758 user = cls.get_by_username(_author, case_insensitive=True)
758 user = cls.get_by_username(_author, case_insensitive=True)
759 if user:
759 if user:
760 return user
760 return user
761
761
762 def update_userdata(self, **kwargs):
762 def update_userdata(self, **kwargs):
763 usr = self
763 usr = self
764 old = usr.user_data
764 old = usr.user_data
765 old.update(**kwargs)
765 old.update(**kwargs)
766 usr.user_data = old
766 usr.user_data = old
767 Session().add(usr)
767 Session().add(usr)
768 log.debug('updated userdata with ', kwargs)
768 log.debug('updated userdata with ', kwargs)
769
769
770 def update_lastlogin(self):
770 def update_lastlogin(self):
771 """Update user lastlogin"""
771 """Update user lastlogin"""
772 self.last_login = datetime.datetime.now()
772 self.last_login = datetime.datetime.now()
773 Session().add(self)
773 Session().add(self)
774 log.debug('updated user %s lastlogin', self.username)
774 log.debug('updated user %s lastlogin', self.username)
775
775
776 def update_lastactivity(self):
776 def update_lastactivity(self):
777 """Update user lastactivity"""
777 """Update user lastactivity"""
778 usr = self
778 usr = self
779 old = usr.user_data
779 old = usr.user_data
780 old.update({'last_activity': time.time()})
780 old.update({'last_activity': time.time()})
781 usr.user_data = old
781 usr.user_data = old
782 Session().add(usr)
782 Session().add(usr)
783 log.debug('updated user %s lastactivity', usr.username)
783 log.debug('updated user %s lastactivity', usr.username)
784
784
785 def update_password(self, new_password, change_api_key=False):
785 def update_password(self, new_password, change_api_key=False):
786 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
786 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
787
787
788 self.password = get_crypt_password(new_password)
788 self.password = get_crypt_password(new_password)
789 if change_api_key:
789 if change_api_key:
790 self.api_key = generate_auth_token(self.username)
790 self.api_key = generate_auth_token(self.username)
791 Session().add(self)
791 Session().add(self)
792
792
793 @classmethod
793 @classmethod
794 def get_first_super_admin(cls):
794 def get_first_super_admin(cls):
795 user = User.query().filter(User.admin == true()).first()
795 user = User.query().filter(User.admin == true()).first()
796 if user is None:
796 if user is None:
797 raise Exception('FATAL: Missing administrative account!')
797 raise Exception('FATAL: Missing administrative account!')
798 return user
798 return user
799
799
800 @classmethod
800 @classmethod
801 def get_all_super_admins(cls):
801 def get_all_super_admins(cls):
802 """
802 """
803 Returns all admin accounts sorted by username
803 Returns all admin accounts sorted by username
804 """
804 """
805 return User.query().filter(User.admin == true())\
805 return User.query().filter(User.admin == true())\
806 .order_by(User.username.asc()).all()
806 .order_by(User.username.asc()).all()
807
807
808 @classmethod
808 @classmethod
809 def get_default_user(cls, cache=False):
809 def get_default_user(cls, cache=False):
810 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
810 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
811 if user is None:
811 if user is None:
812 raise Exception('FATAL: Missing default account!')
812 raise Exception('FATAL: Missing default account!')
813 return user
813 return user
814
814
815 def _get_default_perms(self, user, suffix=''):
815 def _get_default_perms(self, user, suffix=''):
816 from rhodecode.model.permission import PermissionModel
816 from rhodecode.model.permission import PermissionModel
817 return PermissionModel().get_default_perms(user.user_perms, suffix)
817 return PermissionModel().get_default_perms(user.user_perms, suffix)
818
818
819 def get_default_perms(self, suffix=''):
819 def get_default_perms(self, suffix=''):
820 return self._get_default_perms(self, suffix)
820 return self._get_default_perms(self, suffix)
821
821
822 def get_api_data(self, include_secrets=False, details='full'):
822 def get_api_data(self, include_secrets=False, details='full'):
823 """
823 """
824 Common function for generating user related data for API
824 Common function for generating user related data for API
825
825
826 :param include_secrets: By default secrets in the API data will be replaced
826 :param include_secrets: By default secrets in the API data will be replaced
827 by a placeholder value to prevent exposing this data by accident. In case
827 by a placeholder value to prevent exposing this data by accident. In case
828 this data shall be exposed, set this flag to ``True``.
828 this data shall be exposed, set this flag to ``True``.
829
829
830 :param details: details can be 'basic|full' basic gives only a subset of
830 :param details: details can be 'basic|full' basic gives only a subset of
831 the available user information that includes user_id, name and emails.
831 the available user information that includes user_id, name and emails.
832 """
832 """
833 user = self
833 user = self
834 user_data = self.user_data
834 user_data = self.user_data
835 data = {
835 data = {
836 'user_id': user.user_id,
836 'user_id': user.user_id,
837 'username': user.username,
837 'username': user.username,
838 'firstname': user.name,
838 'firstname': user.name,
839 'lastname': user.lastname,
839 'lastname': user.lastname,
840 'email': user.email,
840 'email': user.email,
841 'emails': user.emails,
841 'emails': user.emails,
842 }
842 }
843 if details == 'basic':
843 if details == 'basic':
844 return data
844 return data
845
845
846 api_key_length = 40
846 api_key_length = 40
847 api_key_replacement = '*' * api_key_length
847 api_key_replacement = '*' * api_key_length
848
848
849 extras = {
849 extras = {
850 'api_key': api_key_replacement,
850 'api_key': api_key_replacement,
851 'api_keys': [api_key_replacement],
851 'api_keys': [api_key_replacement],
852 'active': user.active,
852 'active': user.active,
853 'admin': user.admin,
853 'admin': user.admin,
854 'extern_type': user.extern_type,
854 'extern_type': user.extern_type,
855 'extern_name': user.extern_name,
855 'extern_name': user.extern_name,
856 'last_login': user.last_login,
856 'last_login': user.last_login,
857 'ip_addresses': user.ip_addresses,
857 'ip_addresses': user.ip_addresses,
858 'language': user_data.get('language')
858 'language': user_data.get('language')
859 }
859 }
860 data.update(extras)
860 data.update(extras)
861
861
862 if include_secrets:
862 if include_secrets:
863 data['api_key'] = user.api_key
863 data['api_key'] = user.api_key
864 data['api_keys'] = user.auth_tokens
864 data['api_keys'] = user.auth_tokens
865 return data
865 return data
866
866
867 def __json__(self):
867 def __json__(self):
868 data = {
868 data = {
869 'full_name': self.full_name,
869 'full_name': self.full_name,
870 'full_name_or_username': self.full_name_or_username,
870 'full_name_or_username': self.full_name_or_username,
871 'short_contact': self.short_contact,
871 'short_contact': self.short_contact,
872 'full_contact': self.full_contact,
872 'full_contact': self.full_contact,
873 }
873 }
874 data.update(self.get_api_data())
874 data.update(self.get_api_data())
875 return data
875 return data
876
876
877
877
878 class UserApiKeys(Base, BaseModel):
878 class UserApiKeys(Base, BaseModel):
879 __tablename__ = 'user_api_keys'
879 __tablename__ = 'user_api_keys'
880 __table_args__ = (
880 __table_args__ = (
881 Index('uak_api_key_idx', 'api_key'),
881 Index('uak_api_key_idx', 'api_key'),
882 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
882 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
883 UniqueConstraint('api_key'),
883 UniqueConstraint('api_key'),
884 {'extend_existing': True, 'mysql_engine': 'InnoDB',
884 {'extend_existing': True, 'mysql_engine': 'InnoDB',
885 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
885 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
886 )
886 )
887 __mapper_args__ = {}
887 __mapper_args__ = {}
888
888
889 # ApiKey role
889 # ApiKey role
890 ROLE_ALL = 'token_role_all'
890 ROLE_ALL = 'token_role_all'
891 ROLE_HTTP = 'token_role_http'
891 ROLE_HTTP = 'token_role_http'
892 ROLE_VCS = 'token_role_vcs'
892 ROLE_VCS = 'token_role_vcs'
893 ROLE_API = 'token_role_api'
893 ROLE_API = 'token_role_api'
894 ROLE_FEED = 'token_role_feed'
894 ROLE_FEED = 'token_role_feed'
895 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
895 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
896
896
897 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
897 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
898 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
898 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
899 api_key = Column("api_key", String(255), nullable=False, unique=True)
899 api_key = Column("api_key", String(255), nullable=False, unique=True)
900 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
900 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
901 expires = Column('expires', Float(53), nullable=False)
901 expires = Column('expires', Float(53), nullable=False)
902 role = Column('role', String(255), nullable=True)
902 role = Column('role', String(255), nullable=True)
903 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
903 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
904
904
905 user = relationship('User', lazy='joined')
905 user = relationship('User', lazy='joined')
906
906
907 @classmethod
907 @classmethod
908 def _get_role_name(cls, role):
908 def _get_role_name(cls, role):
909 return {
909 return {
910 cls.ROLE_ALL: _('all'),
910 cls.ROLE_ALL: _('all'),
911 cls.ROLE_HTTP: _('http/web interface'),
911 cls.ROLE_HTTP: _('http/web interface'),
912 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
912 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
913 cls.ROLE_API: _('api calls'),
913 cls.ROLE_API: _('api calls'),
914 cls.ROLE_FEED: _('feed access'),
914 cls.ROLE_FEED: _('feed access'),
915 }.get(role, role)
915 }.get(role, role)
916
916
917 @property
917 @property
918 def expired(self):
918 def expired(self):
919 if self.expires == -1:
919 if self.expires == -1:
920 return False
920 return False
921 return time.time() > self.expires
921 return time.time() > self.expires
922
922
923 @property
923 @property
924 def role_humanized(self):
924 def role_humanized(self):
925 return self._get_role_name(self.role)
925 return self._get_role_name(self.role)
926
926
927
927
928 class UserEmailMap(Base, BaseModel):
928 class UserEmailMap(Base, BaseModel):
929 __tablename__ = 'user_email_map'
929 __tablename__ = 'user_email_map'
930 __table_args__ = (
930 __table_args__ = (
931 Index('uem_email_idx', 'email'),
931 Index('uem_email_idx', 'email'),
932 UniqueConstraint('email'),
932 UniqueConstraint('email'),
933 {'extend_existing': True, 'mysql_engine': 'InnoDB',
933 {'extend_existing': True, 'mysql_engine': 'InnoDB',
934 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
934 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
935 )
935 )
936 __mapper_args__ = {}
936 __mapper_args__ = {}
937
937
938 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
938 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
939 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
939 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
940 _email = Column("email", String(255), nullable=True, unique=False, default=None)
940 _email = Column("email", String(255), nullable=True, unique=False, default=None)
941 user = relationship('User', lazy='joined')
941 user = relationship('User', lazy='joined')
942
942
943 @validates('_email')
943 @validates('_email')
944 def validate_email(self, key, email):
944 def validate_email(self, key, email):
945 # check if this email is not main one
945 # check if this email is not main one
946 main_email = Session().query(User).filter(User.email == email).scalar()
946 main_email = Session().query(User).filter(User.email == email).scalar()
947 if main_email is not None:
947 if main_email is not None:
948 raise AttributeError('email %s is present is user table' % email)
948 raise AttributeError('email %s is present is user table' % email)
949 return email
949 return email
950
950
951 @hybrid_property
951 @hybrid_property
952 def email(self):
952 def email(self):
953 return self._email
953 return self._email
954
954
955 @email.setter
955 @email.setter
956 def email(self, val):
956 def email(self, val):
957 self._email = val.lower() if val else None
957 self._email = val.lower() if val else None
958
958
959
959
960 class UserIpMap(Base, BaseModel):
960 class UserIpMap(Base, BaseModel):
961 __tablename__ = 'user_ip_map'
961 __tablename__ = 'user_ip_map'
962 __table_args__ = (
962 __table_args__ = (
963 UniqueConstraint('user_id', 'ip_addr'),
963 UniqueConstraint('user_id', 'ip_addr'),
964 {'extend_existing': True, 'mysql_engine': 'InnoDB',
964 {'extend_existing': True, 'mysql_engine': 'InnoDB',
965 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
965 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
966 )
966 )
967 __mapper_args__ = {}
967 __mapper_args__ = {}
968
968
969 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
969 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
970 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
970 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
971 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
971 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
972 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
972 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
973 description = Column("description", String(10000), nullable=True, unique=None, default=None)
973 description = Column("description", String(10000), nullable=True, unique=None, default=None)
974 user = relationship('User', lazy='joined')
974 user = relationship('User', lazy='joined')
975
975
976 @classmethod
976 @classmethod
977 def _get_ip_range(cls, ip_addr):
977 def _get_ip_range(cls, ip_addr):
978 net = ipaddress.ip_network(ip_addr, strict=False)
978 net = ipaddress.ip_network(ip_addr, strict=False)
979 return [str(net.network_address), str(net.broadcast_address)]
979 return [str(net.network_address), str(net.broadcast_address)]
980
980
981 def __json__(self):
981 def __json__(self):
982 return {
982 return {
983 'ip_addr': self.ip_addr,
983 'ip_addr': self.ip_addr,
984 'ip_range': self._get_ip_range(self.ip_addr),
984 'ip_range': self._get_ip_range(self.ip_addr),
985 }
985 }
986
986
987 def __unicode__(self):
987 def __unicode__(self):
988 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
988 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
989 self.user_id, self.ip_addr)
989 self.user_id, self.ip_addr)
990
990
991 class UserLog(Base, BaseModel):
991 class UserLog(Base, BaseModel):
992 __tablename__ = 'user_logs'
992 __tablename__ = 'user_logs'
993 __table_args__ = (
993 __table_args__ = (
994 {'extend_existing': True, 'mysql_engine': 'InnoDB',
994 {'extend_existing': True, 'mysql_engine': 'InnoDB',
995 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
995 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
996 )
996 )
997 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
997 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
998 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
998 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
999 username = Column("username", String(255), nullable=True, unique=None, default=None)
999 username = Column("username", String(255), nullable=True, unique=None, default=None)
1000 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1000 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1001 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1001 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1002 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1002 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1003 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1003 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1004 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1004 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1005
1005
1006 def __unicode__(self):
1006 def __unicode__(self):
1007 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1007 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1008 self.repository_name,
1008 self.repository_name,
1009 self.action)
1009 self.action)
1010
1010
1011 @property
1011 @property
1012 def action_as_day(self):
1012 def action_as_day(self):
1013 return datetime.date(*self.action_date.timetuple()[:3])
1013 return datetime.date(*self.action_date.timetuple()[:3])
1014
1014
1015 user = relationship('User')
1015 user = relationship('User')
1016 repository = relationship('Repository', cascade='')
1016 repository = relationship('Repository', cascade='')
1017
1017
1018
1018
1019 class UserGroup(Base, BaseModel):
1019 class UserGroup(Base, BaseModel):
1020 __tablename__ = 'users_groups'
1020 __tablename__ = 'users_groups'
1021 __table_args__ = (
1021 __table_args__ = (
1022 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1022 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1023 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1023 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1024 )
1024 )
1025
1025
1026 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1026 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1027 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1027 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1028 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1028 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1029 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1029 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1030 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1030 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1031 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1031 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1032 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1032 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1033 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1033 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1034
1034
1035 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1035 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1036 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1036 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1037 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1037 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1038 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1038 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1039 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1039 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1040 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1040 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1041
1041
1042 user = relationship('User')
1042 user = relationship('User')
1043
1043
1044 @hybrid_property
1044 @hybrid_property
1045 def group_data(self):
1045 def group_data(self):
1046 if not self._group_data:
1046 if not self._group_data:
1047 return {}
1047 return {}
1048
1048
1049 try:
1049 try:
1050 return json.loads(self._group_data)
1050 return json.loads(self._group_data)
1051 except TypeError:
1051 except TypeError:
1052 return {}
1052 return {}
1053
1053
1054 @group_data.setter
1054 @group_data.setter
1055 def group_data(self, val):
1055 def group_data(self, val):
1056 try:
1056 try:
1057 self._group_data = json.dumps(val)
1057 self._group_data = json.dumps(val)
1058 except Exception:
1058 except Exception:
1059 log.error(traceback.format_exc())
1059 log.error(traceback.format_exc())
1060
1060
1061 def __unicode__(self):
1061 def __unicode__(self):
1062 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1062 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1063 self.users_group_id,
1063 self.users_group_id,
1064 self.users_group_name)
1064 self.users_group_name)
1065
1065
1066 @classmethod
1066 @classmethod
1067 def get_by_group_name(cls, group_name, cache=False,
1067 def get_by_group_name(cls, group_name, cache=False,
1068 case_insensitive=False):
1068 case_insensitive=False):
1069 if case_insensitive:
1069 if case_insensitive:
1070 q = cls.query().filter(func.lower(cls.users_group_name) ==
1070 q = cls.query().filter(func.lower(cls.users_group_name) ==
1071 func.lower(group_name))
1071 func.lower(group_name))
1072
1072
1073 else:
1073 else:
1074 q = cls.query().filter(cls.users_group_name == group_name)
1074 q = cls.query().filter(cls.users_group_name == group_name)
1075 if cache:
1075 if cache:
1076 q = q.options(FromCache(
1076 q = q.options(FromCache(
1077 "sql_cache_short",
1077 "sql_cache_short",
1078 "get_group_%s" % _hash_key(group_name)))
1078 "get_group_%s" % _hash_key(group_name)))
1079 return q.scalar()
1079 return q.scalar()
1080
1080
1081 @classmethod
1081 @classmethod
1082 def get(cls, user_group_id, cache=False):
1082 def get(cls, user_group_id, cache=False):
1083 user_group = cls.query()
1083 user_group = cls.query()
1084 if cache:
1084 if cache:
1085 user_group = user_group.options(FromCache("sql_cache_short",
1085 user_group = user_group.options(FromCache("sql_cache_short",
1086 "get_users_group_%s" % user_group_id))
1086 "get_users_group_%s" % user_group_id))
1087 return user_group.get(user_group_id)
1087 return user_group.get(user_group_id)
1088
1088
1089 def permissions(self, with_admins=True, with_owner=True):
1089 def permissions(self, with_admins=True, with_owner=True):
1090 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1090 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1091 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1091 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1092 joinedload(UserUserGroupToPerm.user),
1092 joinedload(UserUserGroupToPerm.user),
1093 joinedload(UserUserGroupToPerm.permission),)
1093 joinedload(UserUserGroupToPerm.permission),)
1094
1094
1095 # get owners and admins and permissions. We do a trick of re-writing
1095 # get owners and admins and permissions. We do a trick of re-writing
1096 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1096 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1097 # has a global reference and changing one object propagates to all
1097 # has a global reference and changing one object propagates to all
1098 # others. This means if admin is also an owner admin_row that change
1098 # others. This means if admin is also an owner admin_row that change
1099 # would propagate to both objects
1099 # would propagate to both objects
1100 perm_rows = []
1100 perm_rows = []
1101 for _usr in q.all():
1101 for _usr in q.all():
1102 usr = AttributeDict(_usr.user.get_dict())
1102 usr = AttributeDict(_usr.user.get_dict())
1103 usr.permission = _usr.permission.permission_name
1103 usr.permission = _usr.permission.permission_name
1104 perm_rows.append(usr)
1104 perm_rows.append(usr)
1105
1105
1106 # filter the perm rows by 'default' first and then sort them by
1106 # filter the perm rows by 'default' first and then sort them by
1107 # admin,write,read,none permissions sorted again alphabetically in
1107 # admin,write,read,none permissions sorted again alphabetically in
1108 # each group
1108 # each group
1109 perm_rows = sorted(perm_rows, key=display_sort)
1109 perm_rows = sorted(perm_rows, key=display_sort)
1110
1110
1111 _admin_perm = 'usergroup.admin'
1111 _admin_perm = 'usergroup.admin'
1112 owner_row = []
1112 owner_row = []
1113 if with_owner:
1113 if with_owner:
1114 usr = AttributeDict(self.user.get_dict())
1114 usr = AttributeDict(self.user.get_dict())
1115 usr.owner_row = True
1115 usr.owner_row = True
1116 usr.permission = _admin_perm
1116 usr.permission = _admin_perm
1117 owner_row.append(usr)
1117 owner_row.append(usr)
1118
1118
1119 super_admin_rows = []
1119 super_admin_rows = []
1120 if with_admins:
1120 if with_admins:
1121 for usr in User.get_all_super_admins():
1121 for usr in User.get_all_super_admins():
1122 # if this admin is also owner, don't double the record
1122 # if this admin is also owner, don't double the record
1123 if usr.user_id == owner_row[0].user_id:
1123 if usr.user_id == owner_row[0].user_id:
1124 owner_row[0].admin_row = True
1124 owner_row[0].admin_row = True
1125 else:
1125 else:
1126 usr = AttributeDict(usr.get_dict())
1126 usr = AttributeDict(usr.get_dict())
1127 usr.admin_row = True
1127 usr.admin_row = True
1128 usr.permission = _admin_perm
1128 usr.permission = _admin_perm
1129 super_admin_rows.append(usr)
1129 super_admin_rows.append(usr)
1130
1130
1131 return super_admin_rows + owner_row + perm_rows
1131 return super_admin_rows + owner_row + perm_rows
1132
1132
1133 def permission_user_groups(self):
1133 def permission_user_groups(self):
1134 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1134 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1135 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1135 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1136 joinedload(UserGroupUserGroupToPerm.target_user_group),
1136 joinedload(UserGroupUserGroupToPerm.target_user_group),
1137 joinedload(UserGroupUserGroupToPerm.permission),)
1137 joinedload(UserGroupUserGroupToPerm.permission),)
1138
1138
1139 perm_rows = []
1139 perm_rows = []
1140 for _user_group in q.all():
1140 for _user_group in q.all():
1141 usr = AttributeDict(_user_group.user_group.get_dict())
1141 usr = AttributeDict(_user_group.user_group.get_dict())
1142 usr.permission = _user_group.permission.permission_name
1142 usr.permission = _user_group.permission.permission_name
1143 perm_rows.append(usr)
1143 perm_rows.append(usr)
1144
1144
1145 return perm_rows
1145 return perm_rows
1146
1146
1147 def _get_default_perms(self, user_group, suffix=''):
1147 def _get_default_perms(self, user_group, suffix=''):
1148 from rhodecode.model.permission import PermissionModel
1148 from rhodecode.model.permission import PermissionModel
1149 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1149 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1150
1150
1151 def get_default_perms(self, suffix=''):
1151 def get_default_perms(self, suffix=''):
1152 return self._get_default_perms(self, suffix)
1152 return self._get_default_perms(self, suffix)
1153
1153
1154 def get_api_data(self, with_group_members=True, include_secrets=False):
1154 def get_api_data(self, with_group_members=True, include_secrets=False):
1155 """
1155 """
1156 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1156 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1157 basically forwarded.
1157 basically forwarded.
1158
1158
1159 """
1159 """
1160 user_group = self
1160 user_group = self
1161
1161
1162 data = {
1162 data = {
1163 'users_group_id': user_group.users_group_id,
1163 'users_group_id': user_group.users_group_id,
1164 'group_name': user_group.users_group_name,
1164 'group_name': user_group.users_group_name,
1165 'group_description': user_group.user_group_description,
1165 'group_description': user_group.user_group_description,
1166 'active': user_group.users_group_active,
1166 'active': user_group.users_group_active,
1167 'owner': user_group.user.username,
1167 'owner': user_group.user.username,
1168 }
1168 }
1169 if with_group_members:
1169 if with_group_members:
1170 users = []
1170 users = []
1171 for user in user_group.members:
1171 for user in user_group.members:
1172 user = user.user
1172 user = user.user
1173 users.append(user.get_api_data(include_secrets=include_secrets))
1173 users.append(user.get_api_data(include_secrets=include_secrets))
1174 data['users'] = users
1174 data['users'] = users
1175
1175
1176 return data
1176 return data
1177
1177
1178
1178
1179 class UserGroupMember(Base, BaseModel):
1179 class UserGroupMember(Base, BaseModel):
1180 __tablename__ = 'users_groups_members'
1180 __tablename__ = 'users_groups_members'
1181 __table_args__ = (
1181 __table_args__ = (
1182 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1182 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1183 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1183 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1184 )
1184 )
1185
1185
1186 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1186 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1187 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1187 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1188 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1188 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1189
1189
1190 user = relationship('User', lazy='joined')
1190 user = relationship('User', lazy='joined')
1191 users_group = relationship('UserGroup')
1191 users_group = relationship('UserGroup')
1192
1192
1193 def __init__(self, gr_id='', u_id=''):
1193 def __init__(self, gr_id='', u_id=''):
1194 self.users_group_id = gr_id
1194 self.users_group_id = gr_id
1195 self.user_id = u_id
1195 self.user_id = u_id
1196
1196
1197
1197
1198 class RepositoryField(Base, BaseModel):
1198 class RepositoryField(Base, BaseModel):
1199 __tablename__ = 'repositories_fields'
1199 __tablename__ = 'repositories_fields'
1200 __table_args__ = (
1200 __table_args__ = (
1201 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1201 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1202 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1202 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1203 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1203 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1204 )
1204 )
1205 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1205 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1206
1206
1207 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1207 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1208 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1208 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1209 field_key = Column("field_key", String(250))
1209 field_key = Column("field_key", String(250))
1210 field_label = Column("field_label", String(1024), nullable=False)
1210 field_label = Column("field_label", String(1024), nullable=False)
1211 field_value = Column("field_value", String(10000), nullable=False)
1211 field_value = Column("field_value", String(10000), nullable=False)
1212 field_desc = Column("field_desc", String(1024), nullable=False)
1212 field_desc = Column("field_desc", String(1024), nullable=False)
1213 field_type = Column("field_type", String(255), nullable=False, unique=None)
1213 field_type = Column("field_type", String(255), nullable=False, unique=None)
1214 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1214 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1215
1215
1216 repository = relationship('Repository')
1216 repository = relationship('Repository')
1217
1217
1218 @property
1218 @property
1219 def field_key_prefixed(self):
1219 def field_key_prefixed(self):
1220 return 'ex_%s' % self.field_key
1220 return 'ex_%s' % self.field_key
1221
1221
1222 @classmethod
1222 @classmethod
1223 def un_prefix_key(cls, key):
1223 def un_prefix_key(cls, key):
1224 if key.startswith(cls.PREFIX):
1224 if key.startswith(cls.PREFIX):
1225 return key[len(cls.PREFIX):]
1225 return key[len(cls.PREFIX):]
1226 return key
1226 return key
1227
1227
1228 @classmethod
1228 @classmethod
1229 def get_by_key_name(cls, key, repo):
1229 def get_by_key_name(cls, key, repo):
1230 row = cls.query()\
1230 row = cls.query()\
1231 .filter(cls.repository == repo)\
1231 .filter(cls.repository == repo)\
1232 .filter(cls.field_key == key).scalar()
1232 .filter(cls.field_key == key).scalar()
1233 return row
1233 return row
1234
1234
1235
1235
1236 class Repository(Base, BaseModel):
1236 class Repository(Base, BaseModel):
1237 __tablename__ = 'repositories'
1237 __tablename__ = 'repositories'
1238 __table_args__ = (
1238 __table_args__ = (
1239 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1239 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1240 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1240 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1241 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1241 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1242 )
1242 )
1243 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1243 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1244 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1244 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1245
1245
1246 STATE_CREATED = 'repo_state_created'
1246 STATE_CREATED = 'repo_state_created'
1247 STATE_PENDING = 'repo_state_pending'
1247 STATE_PENDING = 'repo_state_pending'
1248 STATE_ERROR = 'repo_state_error'
1248 STATE_ERROR = 'repo_state_error'
1249
1249
1250 LOCK_AUTOMATIC = 'lock_auto'
1250 LOCK_AUTOMATIC = 'lock_auto'
1251 LOCK_API = 'lock_api'
1251 LOCK_API = 'lock_api'
1252 LOCK_WEB = 'lock_web'
1252 LOCK_WEB = 'lock_web'
1253 LOCK_PULL = 'lock_pull'
1253 LOCK_PULL = 'lock_pull'
1254
1254
1255 NAME_SEP = URL_SEP
1255 NAME_SEP = URL_SEP
1256
1256
1257 repo_id = Column(
1257 repo_id = Column(
1258 "repo_id", Integer(), nullable=False, unique=True, default=None,
1258 "repo_id", Integer(), nullable=False, unique=True, default=None,
1259 primary_key=True)
1259 primary_key=True)
1260 _repo_name = Column(
1260 _repo_name = Column(
1261 "repo_name", Text(), nullable=False, default=None)
1261 "repo_name", Text(), nullable=False, default=None)
1262 _repo_name_hash = Column(
1262 _repo_name_hash = Column(
1263 "repo_name_hash", String(255), nullable=False, unique=True)
1263 "repo_name_hash", String(255), nullable=False, unique=True)
1264 repo_state = Column("repo_state", String(255), nullable=True)
1264 repo_state = Column("repo_state", String(255), nullable=True)
1265
1265
1266 clone_uri = Column(
1266 clone_uri = Column(
1267 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1267 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1268 default=None)
1268 default=None)
1269 repo_type = Column(
1269 repo_type = Column(
1270 "repo_type", String(255), nullable=False, unique=False, default=None)
1270 "repo_type", String(255), nullable=False, unique=False, default=None)
1271 user_id = Column(
1271 user_id = Column(
1272 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1272 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1273 unique=False, default=None)
1273 unique=False, default=None)
1274 private = Column(
1274 private = Column(
1275 "private", Boolean(), nullable=True, unique=None, default=None)
1275 "private", Boolean(), nullable=True, unique=None, default=None)
1276 enable_statistics = Column(
1276 enable_statistics = Column(
1277 "statistics", Boolean(), nullable=True, unique=None, default=True)
1277 "statistics", Boolean(), nullable=True, unique=None, default=True)
1278 enable_downloads = Column(
1278 enable_downloads = Column(
1279 "downloads", Boolean(), nullable=True, unique=None, default=True)
1279 "downloads", Boolean(), nullable=True, unique=None, default=True)
1280 description = Column(
1280 description = Column(
1281 "description", String(10000), nullable=True, unique=None, default=None)
1281 "description", String(10000), nullable=True, unique=None, default=None)
1282 created_on = Column(
1282 created_on = Column(
1283 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1283 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1284 default=datetime.datetime.now)
1284 default=datetime.datetime.now)
1285 updated_on = Column(
1285 updated_on = Column(
1286 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1286 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1287 default=datetime.datetime.now)
1287 default=datetime.datetime.now)
1288 _landing_revision = Column(
1288 _landing_revision = Column(
1289 "landing_revision", String(255), nullable=False, unique=False,
1289 "landing_revision", String(255), nullable=False, unique=False,
1290 default=None)
1290 default=None)
1291 enable_locking = Column(
1291 enable_locking = Column(
1292 "enable_locking", Boolean(), nullable=False, unique=None,
1292 "enable_locking", Boolean(), nullable=False, unique=None,
1293 default=False)
1293 default=False)
1294 _locked = Column(
1294 _locked = Column(
1295 "locked", String(255), nullable=True, unique=False, default=None)
1295 "locked", String(255), nullable=True, unique=False, default=None)
1296 _changeset_cache = Column(
1296 _changeset_cache = Column(
1297 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1297 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1298
1298
1299 fork_id = Column(
1299 fork_id = Column(
1300 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1300 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1301 nullable=True, unique=False, default=None)
1301 nullable=True, unique=False, default=None)
1302 group_id = Column(
1302 group_id = Column(
1303 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1303 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1304 unique=False, default=None)
1304 unique=False, default=None)
1305
1305
1306 user = relationship('User', lazy='joined')
1306 user = relationship('User', lazy='joined')
1307 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1307 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1308 group = relationship('RepoGroup', lazy='joined')
1308 group = relationship('RepoGroup', lazy='joined')
1309 repo_to_perm = relationship(
1309 repo_to_perm = relationship(
1310 'UserRepoToPerm', cascade='all',
1310 'UserRepoToPerm', cascade='all',
1311 order_by='UserRepoToPerm.repo_to_perm_id')
1311 order_by='UserRepoToPerm.repo_to_perm_id')
1312 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1312 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1313 stats = relationship('Statistics', cascade='all', uselist=False)
1313 stats = relationship('Statistics', cascade='all', uselist=False)
1314
1314
1315 followers = relationship(
1315 followers = relationship(
1316 'UserFollowing',
1316 'UserFollowing',
1317 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1317 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1318 cascade='all')
1318 cascade='all')
1319 extra_fields = relationship(
1319 extra_fields = relationship(
1320 'RepositoryField', cascade="all, delete, delete-orphan")
1320 'RepositoryField', cascade="all, delete, delete-orphan")
1321 logs = relationship('UserLog')
1321 logs = relationship('UserLog')
1322 comments = relationship(
1322 comments = relationship(
1323 'ChangesetComment', cascade="all, delete, delete-orphan")
1323 'ChangesetComment', cascade="all, delete, delete-orphan")
1324 pull_requests_source = relationship(
1324 pull_requests_source = relationship(
1325 'PullRequest',
1325 'PullRequest',
1326 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1326 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1327 cascade="all, delete, delete-orphan")
1327 cascade="all, delete, delete-orphan")
1328 pull_requests_target = relationship(
1328 pull_requests_target = relationship(
1329 'PullRequest',
1329 'PullRequest',
1330 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1330 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1331 cascade="all, delete, delete-orphan")
1331 cascade="all, delete, delete-orphan")
1332 ui = relationship('RepoRhodeCodeUi', cascade="all")
1332 ui = relationship('RepoRhodeCodeUi', cascade="all")
1333 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1333 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1334 integrations = relationship('Integration',
1334 integrations = relationship('Integration',
1335 cascade="all, delete, delete-orphan")
1335 cascade="all, delete, delete-orphan")
1336
1336
1337 def __unicode__(self):
1337 def __unicode__(self):
1338 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1338 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1339 safe_unicode(self.repo_name))
1339 safe_unicode(self.repo_name))
1340
1340
1341 @hybrid_property
1341 @hybrid_property
1342 def landing_rev(self):
1342 def landing_rev(self):
1343 # always should return [rev_type, rev]
1343 # always should return [rev_type, rev]
1344 if self._landing_revision:
1344 if self._landing_revision:
1345 _rev_info = self._landing_revision.split(':')
1345 _rev_info = self._landing_revision.split(':')
1346 if len(_rev_info) < 2:
1346 if len(_rev_info) < 2:
1347 _rev_info.insert(0, 'rev')
1347 _rev_info.insert(0, 'rev')
1348 return [_rev_info[0], _rev_info[1]]
1348 return [_rev_info[0], _rev_info[1]]
1349 return [None, None]
1349 return [None, None]
1350
1350
1351 @landing_rev.setter
1351 @landing_rev.setter
1352 def landing_rev(self, val):
1352 def landing_rev(self, val):
1353 if ':' not in val:
1353 if ':' not in val:
1354 raise ValueError('value must be delimited with `:` and consist '
1354 raise ValueError('value must be delimited with `:` and consist '
1355 'of <rev_type>:<rev>, got %s instead' % val)
1355 'of <rev_type>:<rev>, got %s instead' % val)
1356 self._landing_revision = val
1356 self._landing_revision = val
1357
1357
1358 @hybrid_property
1358 @hybrid_property
1359 def locked(self):
1359 def locked(self):
1360 if self._locked:
1360 if self._locked:
1361 user_id, timelocked, reason = self._locked.split(':')
1361 user_id, timelocked, reason = self._locked.split(':')
1362 lock_values = int(user_id), timelocked, reason
1362 lock_values = int(user_id), timelocked, reason
1363 else:
1363 else:
1364 lock_values = [None, None, None]
1364 lock_values = [None, None, None]
1365 return lock_values
1365 return lock_values
1366
1366
1367 @locked.setter
1367 @locked.setter
1368 def locked(self, val):
1368 def locked(self, val):
1369 if val and isinstance(val, (list, tuple)):
1369 if val and isinstance(val, (list, tuple)):
1370 self._locked = ':'.join(map(str, val))
1370 self._locked = ':'.join(map(str, val))
1371 else:
1371 else:
1372 self._locked = None
1372 self._locked = None
1373
1373
1374 @hybrid_property
1374 @hybrid_property
1375 def changeset_cache(self):
1375 def changeset_cache(self):
1376 from rhodecode.lib.vcs.backends.base import EmptyCommit
1376 from rhodecode.lib.vcs.backends.base import EmptyCommit
1377 dummy = EmptyCommit().__json__()
1377 dummy = EmptyCommit().__json__()
1378 if not self._changeset_cache:
1378 if not self._changeset_cache:
1379 return dummy
1379 return dummy
1380 try:
1380 try:
1381 return json.loads(self._changeset_cache)
1381 return json.loads(self._changeset_cache)
1382 except TypeError:
1382 except TypeError:
1383 return dummy
1383 return dummy
1384 except Exception:
1384 except Exception:
1385 log.error(traceback.format_exc())
1385 log.error(traceback.format_exc())
1386 return dummy
1386 return dummy
1387
1387
1388 @changeset_cache.setter
1388 @changeset_cache.setter
1389 def changeset_cache(self, val):
1389 def changeset_cache(self, val):
1390 try:
1390 try:
1391 self._changeset_cache = json.dumps(val)
1391 self._changeset_cache = json.dumps(val)
1392 except Exception:
1392 except Exception:
1393 log.error(traceback.format_exc())
1393 log.error(traceback.format_exc())
1394
1394
1395 @hybrid_property
1395 @hybrid_property
1396 def repo_name(self):
1396 def repo_name(self):
1397 return self._repo_name
1397 return self._repo_name
1398
1398
1399 @repo_name.setter
1399 @repo_name.setter
1400 def repo_name(self, value):
1400 def repo_name(self, value):
1401 self._repo_name = value
1401 self._repo_name = value
1402 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1402 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1403
1403
1404 @classmethod
1404 @classmethod
1405 def normalize_repo_name(cls, repo_name):
1405 def normalize_repo_name(cls, repo_name):
1406 """
1406 """
1407 Normalizes os specific repo_name to the format internally stored inside
1407 Normalizes os specific repo_name to the format internally stored inside
1408 database using URL_SEP
1408 database using URL_SEP
1409
1409
1410 :param cls:
1410 :param cls:
1411 :param repo_name:
1411 :param repo_name:
1412 """
1412 """
1413 return cls.NAME_SEP.join(repo_name.split(os.sep))
1413 return cls.NAME_SEP.join(repo_name.split(os.sep))
1414
1414
1415 @classmethod
1415 @classmethod
1416 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1416 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1417 session = Session()
1417 session = Session()
1418 q = session.query(cls).filter(cls.repo_name == repo_name)
1418 q = session.query(cls).filter(cls.repo_name == repo_name)
1419
1419
1420 if cache:
1420 if cache:
1421 if identity_cache:
1421 if identity_cache:
1422 val = cls.identity_cache(session, 'repo_name', repo_name)
1422 val = cls.identity_cache(session, 'repo_name', repo_name)
1423 if val:
1423 if val:
1424 return val
1424 return val
1425 else:
1425 else:
1426 q = q.options(
1426 q = q.options(
1427 FromCache("sql_cache_short",
1427 FromCache("sql_cache_short",
1428 "get_repo_by_name_%s" % _hash_key(repo_name)))
1428 "get_repo_by_name_%s" % _hash_key(repo_name)))
1429
1429
1430 return q.scalar()
1430 return q.scalar()
1431
1431
1432 @classmethod
1432 @classmethod
1433 def get_by_full_path(cls, repo_full_path):
1433 def get_by_full_path(cls, repo_full_path):
1434 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1434 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1435 repo_name = cls.normalize_repo_name(repo_name)
1435 repo_name = cls.normalize_repo_name(repo_name)
1436 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1436 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1437
1437
1438 @classmethod
1438 @classmethod
1439 def get_repo_forks(cls, repo_id):
1439 def get_repo_forks(cls, repo_id):
1440 return cls.query().filter(Repository.fork_id == repo_id)
1440 return cls.query().filter(Repository.fork_id == repo_id)
1441
1441
1442 @classmethod
1442 @classmethod
1443 def base_path(cls):
1443 def base_path(cls):
1444 """
1444 """
1445 Returns base path when all repos are stored
1445 Returns base path when all repos are stored
1446
1446
1447 :param cls:
1447 :param cls:
1448 """
1448 """
1449 q = Session().query(RhodeCodeUi)\
1449 q = Session().query(RhodeCodeUi)\
1450 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1450 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1451 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1451 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1452 return q.one().ui_value
1452 return q.one().ui_value
1453
1453
1454 @classmethod
1454 @classmethod
1455 def is_valid(cls, repo_name):
1455 def is_valid(cls, repo_name):
1456 """
1456 """
1457 returns True if given repo name is a valid filesystem repository
1457 returns True if given repo name is a valid filesystem repository
1458
1458
1459 :param cls:
1459 :param cls:
1460 :param repo_name:
1460 :param repo_name:
1461 """
1461 """
1462 from rhodecode.lib.utils import is_valid_repo
1462 from rhodecode.lib.utils import is_valid_repo
1463
1463
1464 return is_valid_repo(repo_name, cls.base_path())
1464 return is_valid_repo(repo_name, cls.base_path())
1465
1465
1466 @classmethod
1466 @classmethod
1467 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1467 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1468 case_insensitive=True):
1468 case_insensitive=True):
1469 q = Repository.query()
1469 q = Repository.query()
1470
1470
1471 if not isinstance(user_id, Optional):
1471 if not isinstance(user_id, Optional):
1472 q = q.filter(Repository.user_id == user_id)
1472 q = q.filter(Repository.user_id == user_id)
1473
1473
1474 if not isinstance(group_id, Optional):
1474 if not isinstance(group_id, Optional):
1475 q = q.filter(Repository.group_id == group_id)
1475 q = q.filter(Repository.group_id == group_id)
1476
1476
1477 if case_insensitive:
1477 if case_insensitive:
1478 q = q.order_by(func.lower(Repository.repo_name))
1478 q = q.order_by(func.lower(Repository.repo_name))
1479 else:
1479 else:
1480 q = q.order_by(Repository.repo_name)
1480 q = q.order_by(Repository.repo_name)
1481 return q.all()
1481 return q.all()
1482
1482
1483 @property
1483 @property
1484 def forks(self):
1484 def forks(self):
1485 """
1485 """
1486 Return forks of this repo
1486 Return forks of this repo
1487 """
1487 """
1488 return Repository.get_repo_forks(self.repo_id)
1488 return Repository.get_repo_forks(self.repo_id)
1489
1489
1490 @property
1490 @property
1491 def parent(self):
1491 def parent(self):
1492 """
1492 """
1493 Returns fork parent
1493 Returns fork parent
1494 """
1494 """
1495 return self.fork
1495 return self.fork
1496
1496
1497 @property
1497 @property
1498 def just_name(self):
1498 def just_name(self):
1499 return self.repo_name.split(self.NAME_SEP)[-1]
1499 return self.repo_name.split(self.NAME_SEP)[-1]
1500
1500
1501 @property
1501 @property
1502 def groups_with_parents(self):
1502 def groups_with_parents(self):
1503 groups = []
1503 groups = []
1504 if self.group is None:
1504 if self.group is None:
1505 return groups
1505 return groups
1506
1506
1507 cur_gr = self.group
1507 cur_gr = self.group
1508 groups.insert(0, cur_gr)
1508 groups.insert(0, cur_gr)
1509 while 1:
1509 while 1:
1510 gr = getattr(cur_gr, 'parent_group', None)
1510 gr = getattr(cur_gr, 'parent_group', None)
1511 cur_gr = cur_gr.parent_group
1511 cur_gr = cur_gr.parent_group
1512 if gr is None:
1512 if gr is None:
1513 break
1513 break
1514 groups.insert(0, gr)
1514 groups.insert(0, gr)
1515
1515
1516 return groups
1516 return groups
1517
1517
1518 @property
1518 @property
1519 def groups_and_repo(self):
1519 def groups_and_repo(self):
1520 return self.groups_with_parents, self
1520 return self.groups_with_parents, self
1521
1521
1522 @LazyProperty
1522 @LazyProperty
1523 def repo_path(self):
1523 def repo_path(self):
1524 """
1524 """
1525 Returns base full path for that repository means where it actually
1525 Returns base full path for that repository means where it actually
1526 exists on a filesystem
1526 exists on a filesystem
1527 """
1527 """
1528 q = Session().query(RhodeCodeUi).filter(
1528 q = Session().query(RhodeCodeUi).filter(
1529 RhodeCodeUi.ui_key == self.NAME_SEP)
1529 RhodeCodeUi.ui_key == self.NAME_SEP)
1530 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1530 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1531 return q.one().ui_value
1531 return q.one().ui_value
1532
1532
1533 @property
1533 @property
1534 def repo_full_path(self):
1534 def repo_full_path(self):
1535 p = [self.repo_path]
1535 p = [self.repo_path]
1536 # we need to split the name by / since this is how we store the
1536 # we need to split the name by / since this is how we store the
1537 # names in the database, but that eventually needs to be converted
1537 # names in the database, but that eventually needs to be converted
1538 # into a valid system path
1538 # into a valid system path
1539 p += self.repo_name.split(self.NAME_SEP)
1539 p += self.repo_name.split(self.NAME_SEP)
1540 return os.path.join(*map(safe_unicode, p))
1540 return os.path.join(*map(safe_unicode, p))
1541
1541
1542 @property
1542 @property
1543 def cache_keys(self):
1543 def cache_keys(self):
1544 """
1544 """
1545 Returns associated cache keys for that repo
1545 Returns associated cache keys for that repo
1546 """
1546 """
1547 return CacheKey.query()\
1547 return CacheKey.query()\
1548 .filter(CacheKey.cache_args == self.repo_name)\
1548 .filter(CacheKey.cache_args == self.repo_name)\
1549 .order_by(CacheKey.cache_key)\
1549 .order_by(CacheKey.cache_key)\
1550 .all()
1550 .all()
1551
1551
1552 def get_new_name(self, repo_name):
1552 def get_new_name(self, repo_name):
1553 """
1553 """
1554 returns new full repository name based on assigned group and new new
1554 returns new full repository name based on assigned group and new new
1555
1555
1556 :param group_name:
1556 :param group_name:
1557 """
1557 """
1558 path_prefix = self.group.full_path_splitted if self.group else []
1558 path_prefix = self.group.full_path_splitted if self.group else []
1559 return self.NAME_SEP.join(path_prefix + [repo_name])
1559 return self.NAME_SEP.join(path_prefix + [repo_name])
1560
1560
1561 @property
1561 @property
1562 def _config(self):
1562 def _config(self):
1563 """
1563 """
1564 Returns db based config object.
1564 Returns db based config object.
1565 """
1565 """
1566 from rhodecode.lib.utils import make_db_config
1566 from rhodecode.lib.utils import make_db_config
1567 return make_db_config(clear_session=False, repo=self)
1567 return make_db_config(clear_session=False, repo=self)
1568
1568
1569 def permissions(self, with_admins=True, with_owner=True):
1569 def permissions(self, with_admins=True, with_owner=True):
1570 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1570 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1571 q = q.options(joinedload(UserRepoToPerm.repository),
1571 q = q.options(joinedload(UserRepoToPerm.repository),
1572 joinedload(UserRepoToPerm.user),
1572 joinedload(UserRepoToPerm.user),
1573 joinedload(UserRepoToPerm.permission),)
1573 joinedload(UserRepoToPerm.permission),)
1574
1574
1575 # get owners and admins and permissions. We do a trick of re-writing
1575 # get owners and admins and permissions. We do a trick of re-writing
1576 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1576 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1577 # has a global reference and changing one object propagates to all
1577 # has a global reference and changing one object propagates to all
1578 # others. This means if admin is also an owner admin_row that change
1578 # others. This means if admin is also an owner admin_row that change
1579 # would propagate to both objects
1579 # would propagate to both objects
1580 perm_rows = []
1580 perm_rows = []
1581 for _usr in q.all():
1581 for _usr in q.all():
1582 usr = AttributeDict(_usr.user.get_dict())
1582 usr = AttributeDict(_usr.user.get_dict())
1583 usr.permission = _usr.permission.permission_name
1583 usr.permission = _usr.permission.permission_name
1584 perm_rows.append(usr)
1584 perm_rows.append(usr)
1585
1585
1586 # filter the perm rows by 'default' first and then sort them by
1586 # filter the perm rows by 'default' first and then sort them by
1587 # admin,write,read,none permissions sorted again alphabetically in
1587 # admin,write,read,none permissions sorted again alphabetically in
1588 # each group
1588 # each group
1589 perm_rows = sorted(perm_rows, key=display_sort)
1589 perm_rows = sorted(perm_rows, key=display_sort)
1590
1590
1591 _admin_perm = 'repository.admin'
1591 _admin_perm = 'repository.admin'
1592 owner_row = []
1592 owner_row = []
1593 if with_owner:
1593 if with_owner:
1594 usr = AttributeDict(self.user.get_dict())
1594 usr = AttributeDict(self.user.get_dict())
1595 usr.owner_row = True
1595 usr.owner_row = True
1596 usr.permission = _admin_perm
1596 usr.permission = _admin_perm
1597 owner_row.append(usr)
1597 owner_row.append(usr)
1598
1598
1599 super_admin_rows = []
1599 super_admin_rows = []
1600 if with_admins:
1600 if with_admins:
1601 for usr in User.get_all_super_admins():
1601 for usr in User.get_all_super_admins():
1602 # if this admin is also owner, don't double the record
1602 # if this admin is also owner, don't double the record
1603 if usr.user_id == owner_row[0].user_id:
1603 if usr.user_id == owner_row[0].user_id:
1604 owner_row[0].admin_row = True
1604 owner_row[0].admin_row = True
1605 else:
1605 else:
1606 usr = AttributeDict(usr.get_dict())
1606 usr = AttributeDict(usr.get_dict())
1607 usr.admin_row = True
1607 usr.admin_row = True
1608 usr.permission = _admin_perm
1608 usr.permission = _admin_perm
1609 super_admin_rows.append(usr)
1609 super_admin_rows.append(usr)
1610
1610
1611 return super_admin_rows + owner_row + perm_rows
1611 return super_admin_rows + owner_row + perm_rows
1612
1612
1613 def permission_user_groups(self):
1613 def permission_user_groups(self):
1614 q = UserGroupRepoToPerm.query().filter(
1614 q = UserGroupRepoToPerm.query().filter(
1615 UserGroupRepoToPerm.repository == self)
1615 UserGroupRepoToPerm.repository == self)
1616 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1616 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1617 joinedload(UserGroupRepoToPerm.users_group),
1617 joinedload(UserGroupRepoToPerm.users_group),
1618 joinedload(UserGroupRepoToPerm.permission),)
1618 joinedload(UserGroupRepoToPerm.permission),)
1619
1619
1620 perm_rows = []
1620 perm_rows = []
1621 for _user_group in q.all():
1621 for _user_group in q.all():
1622 usr = AttributeDict(_user_group.users_group.get_dict())
1622 usr = AttributeDict(_user_group.users_group.get_dict())
1623 usr.permission = _user_group.permission.permission_name
1623 usr.permission = _user_group.permission.permission_name
1624 perm_rows.append(usr)
1624 perm_rows.append(usr)
1625
1625
1626 return perm_rows
1626 return perm_rows
1627
1627
1628 def get_api_data(self, include_secrets=False):
1628 def get_api_data(self, include_secrets=False):
1629 """
1629 """
1630 Common function for generating repo api data
1630 Common function for generating repo api data
1631
1631
1632 :param include_secrets: See :meth:`User.get_api_data`.
1632 :param include_secrets: See :meth:`User.get_api_data`.
1633
1633
1634 """
1634 """
1635 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1635 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1636 # move this methods on models level.
1636 # move this methods on models level.
1637 from rhodecode.model.settings import SettingsModel
1637 from rhodecode.model.settings import SettingsModel
1638
1638
1639 repo = self
1639 repo = self
1640 _user_id, _time, _reason = self.locked
1640 _user_id, _time, _reason = self.locked
1641
1641
1642 data = {
1642 data = {
1643 'repo_id': repo.repo_id,
1643 'repo_id': repo.repo_id,
1644 'repo_name': repo.repo_name,
1644 'repo_name': repo.repo_name,
1645 'repo_type': repo.repo_type,
1645 'repo_type': repo.repo_type,
1646 'clone_uri': repo.clone_uri or '',
1646 'clone_uri': repo.clone_uri or '',
1647 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1647 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1648 'private': repo.private,
1648 'private': repo.private,
1649 'created_on': repo.created_on,
1649 'created_on': repo.created_on,
1650 'description': repo.description,
1650 'description': repo.description,
1651 'landing_rev': repo.landing_rev,
1651 'landing_rev': repo.landing_rev,
1652 'owner': repo.user.username,
1652 'owner': repo.user.username,
1653 'fork_of': repo.fork.repo_name if repo.fork else None,
1653 'fork_of': repo.fork.repo_name if repo.fork else None,
1654 'enable_statistics': repo.enable_statistics,
1654 'enable_statistics': repo.enable_statistics,
1655 'enable_locking': repo.enable_locking,
1655 'enable_locking': repo.enable_locking,
1656 'enable_downloads': repo.enable_downloads,
1656 'enable_downloads': repo.enable_downloads,
1657 'last_changeset': repo.changeset_cache,
1657 'last_changeset': repo.changeset_cache,
1658 'locked_by': User.get(_user_id).get_api_data(
1658 'locked_by': User.get(_user_id).get_api_data(
1659 include_secrets=include_secrets) if _user_id else None,
1659 include_secrets=include_secrets) if _user_id else None,
1660 'locked_date': time_to_datetime(_time) if _time else None,
1660 'locked_date': time_to_datetime(_time) if _time else None,
1661 'lock_reason': _reason if _reason else None,
1661 'lock_reason': _reason if _reason else None,
1662 }
1662 }
1663
1663
1664 # TODO: mikhail: should be per-repo settings here
1664 # TODO: mikhail: should be per-repo settings here
1665 rc_config = SettingsModel().get_all_settings()
1665 rc_config = SettingsModel().get_all_settings()
1666 repository_fields = str2bool(
1666 repository_fields = str2bool(
1667 rc_config.get('rhodecode_repository_fields'))
1667 rc_config.get('rhodecode_repository_fields'))
1668 if repository_fields:
1668 if repository_fields:
1669 for f in self.extra_fields:
1669 for f in self.extra_fields:
1670 data[f.field_key_prefixed] = f.field_value
1670 data[f.field_key_prefixed] = f.field_value
1671
1671
1672 return data
1672 return data
1673
1673
1674 @classmethod
1674 @classmethod
1675 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1675 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1676 if not lock_time:
1676 if not lock_time:
1677 lock_time = time.time()
1677 lock_time = time.time()
1678 if not lock_reason:
1678 if not lock_reason:
1679 lock_reason = cls.LOCK_AUTOMATIC
1679 lock_reason = cls.LOCK_AUTOMATIC
1680 repo.locked = [user_id, lock_time, lock_reason]
1680 repo.locked = [user_id, lock_time, lock_reason]
1681 Session().add(repo)
1681 Session().add(repo)
1682 Session().commit()
1682 Session().commit()
1683
1683
1684 @classmethod
1684 @classmethod
1685 def unlock(cls, repo):
1685 def unlock(cls, repo):
1686 repo.locked = None
1686 repo.locked = None
1687 Session().add(repo)
1687 Session().add(repo)
1688 Session().commit()
1688 Session().commit()
1689
1689
1690 @classmethod
1690 @classmethod
1691 def getlock(cls, repo):
1691 def getlock(cls, repo):
1692 return repo.locked
1692 return repo.locked
1693
1693
1694 def is_user_lock(self, user_id):
1694 def is_user_lock(self, user_id):
1695 if self.lock[0]:
1695 if self.lock[0]:
1696 lock_user_id = safe_int(self.lock[0])
1696 lock_user_id = safe_int(self.lock[0])
1697 user_id = safe_int(user_id)
1697 user_id = safe_int(user_id)
1698 # both are ints, and they are equal
1698 # both are ints, and they are equal
1699 return all([lock_user_id, user_id]) and lock_user_id == user_id
1699 return all([lock_user_id, user_id]) and lock_user_id == user_id
1700
1700
1701 return False
1701 return False
1702
1702
1703 def get_locking_state(self, action, user_id, only_when_enabled=True):
1703 def get_locking_state(self, action, user_id, only_when_enabled=True):
1704 """
1704 """
1705 Checks locking on this repository, if locking is enabled and lock is
1705 Checks locking on this repository, if locking is enabled and lock is
1706 present returns a tuple of make_lock, locked, locked_by.
1706 present returns a tuple of make_lock, locked, locked_by.
1707 make_lock can have 3 states None (do nothing) True, make lock
1707 make_lock can have 3 states None (do nothing) True, make lock
1708 False release lock, This value is later propagated to hooks, which
1708 False release lock, This value is later propagated to hooks, which
1709 do the locking. Think about this as signals passed to hooks what to do.
1709 do the locking. Think about this as signals passed to hooks what to do.
1710
1710
1711 """
1711 """
1712 # TODO: johbo: This is part of the business logic and should be moved
1712 # TODO: johbo: This is part of the business logic and should be moved
1713 # into the RepositoryModel.
1713 # into the RepositoryModel.
1714
1714
1715 if action not in ('push', 'pull'):
1715 if action not in ('push', 'pull'):
1716 raise ValueError("Invalid action value: %s" % repr(action))
1716 raise ValueError("Invalid action value: %s" % repr(action))
1717
1717
1718 # defines if locked error should be thrown to user
1718 # defines if locked error should be thrown to user
1719 currently_locked = False
1719 currently_locked = False
1720 # defines if new lock should be made, tri-state
1720 # defines if new lock should be made, tri-state
1721 make_lock = None
1721 make_lock = None
1722 repo = self
1722 repo = self
1723 user = User.get(user_id)
1723 user = User.get(user_id)
1724
1724
1725 lock_info = repo.locked
1725 lock_info = repo.locked
1726
1726
1727 if repo and (repo.enable_locking or not only_when_enabled):
1727 if repo and (repo.enable_locking or not only_when_enabled):
1728 if action == 'push':
1728 if action == 'push':
1729 # check if it's already locked !, if it is compare users
1729 # check if it's already locked !, if it is compare users
1730 locked_by_user_id = lock_info[0]
1730 locked_by_user_id = lock_info[0]
1731 if user.user_id == locked_by_user_id:
1731 if user.user_id == locked_by_user_id:
1732 log.debug(
1732 log.debug(
1733 'Got `push` action from user %s, now unlocking', user)
1733 'Got `push` action from user %s, now unlocking', user)
1734 # unlock if we have push from user who locked
1734 # unlock if we have push from user who locked
1735 make_lock = False
1735 make_lock = False
1736 else:
1736 else:
1737 # we're not the same user who locked, ban with
1737 # we're not the same user who locked, ban with
1738 # code defined in settings (default is 423 HTTP Locked) !
1738 # code defined in settings (default is 423 HTTP Locked) !
1739 log.debug('Repo %s is currently locked by %s', repo, user)
1739 log.debug('Repo %s is currently locked by %s', repo, user)
1740 currently_locked = True
1740 currently_locked = True
1741 elif action == 'pull':
1741 elif action == 'pull':
1742 # [0] user [1] date
1742 # [0] user [1] date
1743 if lock_info[0] and lock_info[1]:
1743 if lock_info[0] and lock_info[1]:
1744 log.debug('Repo %s is currently locked by %s', repo, user)
1744 log.debug('Repo %s is currently locked by %s', repo, user)
1745 currently_locked = True
1745 currently_locked = True
1746 else:
1746 else:
1747 log.debug('Setting lock on repo %s by %s', repo, user)
1747 log.debug('Setting lock on repo %s by %s', repo, user)
1748 make_lock = True
1748 make_lock = True
1749
1749
1750 else:
1750 else:
1751 log.debug('Repository %s do not have locking enabled', repo)
1751 log.debug('Repository %s do not have locking enabled', repo)
1752
1752
1753 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1753 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1754 make_lock, currently_locked, lock_info)
1754 make_lock, currently_locked, lock_info)
1755
1755
1756 from rhodecode.lib.auth import HasRepoPermissionAny
1756 from rhodecode.lib.auth import HasRepoPermissionAny
1757 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1757 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1758 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1758 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1759 # if we don't have at least write permission we cannot make a lock
1759 # if we don't have at least write permission we cannot make a lock
1760 log.debug('lock state reset back to FALSE due to lack '
1760 log.debug('lock state reset back to FALSE due to lack '
1761 'of at least read permission')
1761 'of at least read permission')
1762 make_lock = False
1762 make_lock = False
1763
1763
1764 return make_lock, currently_locked, lock_info
1764 return make_lock, currently_locked, lock_info
1765
1765
1766 @property
1766 @property
1767 def last_db_change(self):
1767 def last_db_change(self):
1768 return self.updated_on
1768 return self.updated_on
1769
1769
1770 @property
1770 @property
1771 def clone_uri_hidden(self):
1771 def clone_uri_hidden(self):
1772 clone_uri = self.clone_uri
1772 clone_uri = self.clone_uri
1773 if clone_uri:
1773 if clone_uri:
1774 import urlobject
1774 import urlobject
1775 url_obj = urlobject.URLObject(clone_uri)
1775 url_obj = urlobject.URLObject(clone_uri)
1776 if url_obj.password:
1776 if url_obj.password:
1777 clone_uri = url_obj.with_password('*****')
1777 clone_uri = url_obj.with_password('*****')
1778 return clone_uri
1778 return clone_uri
1779
1779
1780 def clone_url(self, **override):
1780 def clone_url(self, **override):
1781 qualified_home_url = url('home', qualified=True)
1781 qualified_home_url = url('home', qualified=True)
1782
1782
1783 uri_tmpl = None
1783 uri_tmpl = None
1784 if 'with_id' in override:
1784 if 'with_id' in override:
1785 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1785 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1786 del override['with_id']
1786 del override['with_id']
1787
1787
1788 if 'uri_tmpl' in override:
1788 if 'uri_tmpl' in override:
1789 uri_tmpl = override['uri_tmpl']
1789 uri_tmpl = override['uri_tmpl']
1790 del override['uri_tmpl']
1790 del override['uri_tmpl']
1791
1791
1792 # we didn't override our tmpl from **overrides
1792 # we didn't override our tmpl from **overrides
1793 if not uri_tmpl:
1793 if not uri_tmpl:
1794 uri_tmpl = self.DEFAULT_CLONE_URI
1794 uri_tmpl = self.DEFAULT_CLONE_URI
1795 try:
1795 try:
1796 from pylons import tmpl_context as c
1796 from pylons import tmpl_context as c
1797 uri_tmpl = c.clone_uri_tmpl
1797 uri_tmpl = c.clone_uri_tmpl
1798 except Exception:
1798 except Exception:
1799 # in any case if we call this outside of request context,
1799 # in any case if we call this outside of request context,
1800 # ie, not having tmpl_context set up
1800 # ie, not having tmpl_context set up
1801 pass
1801 pass
1802
1802
1803 return get_clone_url(uri_tmpl=uri_tmpl,
1803 return get_clone_url(uri_tmpl=uri_tmpl,
1804 qualifed_home_url=qualified_home_url,
1804 qualifed_home_url=qualified_home_url,
1805 repo_name=self.repo_name,
1805 repo_name=self.repo_name,
1806 repo_id=self.repo_id, **override)
1806 repo_id=self.repo_id, **override)
1807
1807
1808 def set_state(self, state):
1808 def set_state(self, state):
1809 self.repo_state = state
1809 self.repo_state = state
1810 Session().add(self)
1810 Session().add(self)
1811 #==========================================================================
1811 #==========================================================================
1812 # SCM PROPERTIES
1812 # SCM PROPERTIES
1813 #==========================================================================
1813 #==========================================================================
1814
1814
1815 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1815 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1816 return get_commit_safe(
1816 return get_commit_safe(
1817 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1817 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1818
1818
1819 def get_changeset(self, rev=None, pre_load=None):
1819 def get_changeset(self, rev=None, pre_load=None):
1820 warnings.warn("Use get_commit", DeprecationWarning)
1820 warnings.warn("Use get_commit", DeprecationWarning)
1821 commit_id = None
1821 commit_id = None
1822 commit_idx = None
1822 commit_idx = None
1823 if isinstance(rev, basestring):
1823 if isinstance(rev, basestring):
1824 commit_id = rev
1824 commit_id = rev
1825 else:
1825 else:
1826 commit_idx = rev
1826 commit_idx = rev
1827 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1827 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1828 pre_load=pre_load)
1828 pre_load=pre_load)
1829
1829
1830 def get_landing_commit(self):
1830 def get_landing_commit(self):
1831 """
1831 """
1832 Returns landing commit, or if that doesn't exist returns the tip
1832 Returns landing commit, or if that doesn't exist returns the tip
1833 """
1833 """
1834 _rev_type, _rev = self.landing_rev
1834 _rev_type, _rev = self.landing_rev
1835 commit = self.get_commit(_rev)
1835 commit = self.get_commit(_rev)
1836 if isinstance(commit, EmptyCommit):
1836 if isinstance(commit, EmptyCommit):
1837 return self.get_commit()
1837 return self.get_commit()
1838 return commit
1838 return commit
1839
1839
1840 def update_commit_cache(self, cs_cache=None, config=None):
1840 def update_commit_cache(self, cs_cache=None, config=None):
1841 """
1841 """
1842 Update cache of last changeset for repository, keys should be::
1842 Update cache of last changeset for repository, keys should be::
1843
1843
1844 short_id
1844 short_id
1845 raw_id
1845 raw_id
1846 revision
1846 revision
1847 parents
1847 parents
1848 message
1848 message
1849 date
1849 date
1850 author
1850 author
1851
1851
1852 :param cs_cache:
1852 :param cs_cache:
1853 """
1853 """
1854 from rhodecode.lib.vcs.backends.base import BaseChangeset
1854 from rhodecode.lib.vcs.backends.base import BaseChangeset
1855 if cs_cache is None:
1855 if cs_cache is None:
1856 # use no-cache version here
1856 # use no-cache version here
1857 scm_repo = self.scm_instance(cache=False, config=config)
1857 scm_repo = self.scm_instance(cache=False, config=config)
1858 if scm_repo:
1858 if scm_repo:
1859 cs_cache = scm_repo.get_commit(
1859 cs_cache = scm_repo.get_commit(
1860 pre_load=["author", "date", "message", "parents"])
1860 pre_load=["author", "date", "message", "parents"])
1861 else:
1861 else:
1862 cs_cache = EmptyCommit()
1862 cs_cache = EmptyCommit()
1863
1863
1864 if isinstance(cs_cache, BaseChangeset):
1864 if isinstance(cs_cache, BaseChangeset):
1865 cs_cache = cs_cache.__json__()
1865 cs_cache = cs_cache.__json__()
1866
1866
1867 def is_outdated(new_cs_cache):
1867 def is_outdated(new_cs_cache):
1868 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1868 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1869 new_cs_cache['revision'] != self.changeset_cache['revision']):
1869 new_cs_cache['revision'] != self.changeset_cache['revision']):
1870 return True
1870 return True
1871 return False
1871 return False
1872
1872
1873 # check if we have maybe already latest cached revision
1873 # check if we have maybe already latest cached revision
1874 if is_outdated(cs_cache) or not self.changeset_cache:
1874 if is_outdated(cs_cache) or not self.changeset_cache:
1875 _default = datetime.datetime.fromtimestamp(0)
1875 _default = datetime.datetime.fromtimestamp(0)
1876 last_change = cs_cache.get('date') or _default
1876 last_change = cs_cache.get('date') or _default
1877 log.debug('updated repo %s with new cs cache %s',
1877 log.debug('updated repo %s with new cs cache %s',
1878 self.repo_name, cs_cache)
1878 self.repo_name, cs_cache)
1879 self.updated_on = last_change
1879 self.updated_on = last_change
1880 self.changeset_cache = cs_cache
1880 self.changeset_cache = cs_cache
1881 Session().add(self)
1881 Session().add(self)
1882 Session().commit()
1882 Session().commit()
1883 else:
1883 else:
1884 log.debug('Skipping update_commit_cache for repo:`%s` '
1884 log.debug('Skipping update_commit_cache for repo:`%s` '
1885 'commit already with latest changes', self.repo_name)
1885 'commit already with latest changes', self.repo_name)
1886
1886
1887 @property
1887 @property
1888 def tip(self):
1888 def tip(self):
1889 return self.get_commit('tip')
1889 return self.get_commit('tip')
1890
1890
1891 @property
1891 @property
1892 def author(self):
1892 def author(self):
1893 return self.tip.author
1893 return self.tip.author
1894
1894
1895 @property
1895 @property
1896 def last_change(self):
1896 def last_change(self):
1897 return self.scm_instance().last_change
1897 return self.scm_instance().last_change
1898
1898
1899 def get_comments(self, revisions=None):
1899 def get_comments(self, revisions=None):
1900 """
1900 """
1901 Returns comments for this repository grouped by revisions
1901 Returns comments for this repository grouped by revisions
1902
1902
1903 :param revisions: filter query by revisions only
1903 :param revisions: filter query by revisions only
1904 """
1904 """
1905 cmts = ChangesetComment.query()\
1905 cmts = ChangesetComment.query()\
1906 .filter(ChangesetComment.repo == self)
1906 .filter(ChangesetComment.repo == self)
1907 if revisions:
1907 if revisions:
1908 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1908 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1909 grouped = collections.defaultdict(list)
1909 grouped = collections.defaultdict(list)
1910 for cmt in cmts.all():
1910 for cmt in cmts.all():
1911 grouped[cmt.revision].append(cmt)
1911 grouped[cmt.revision].append(cmt)
1912 return grouped
1912 return grouped
1913
1913
1914 def statuses(self, revisions=None):
1914 def statuses(self, revisions=None):
1915 """
1915 """
1916 Returns statuses for this repository
1916 Returns statuses for this repository
1917
1917
1918 :param revisions: list of revisions to get statuses for
1918 :param revisions: list of revisions to get statuses for
1919 """
1919 """
1920 statuses = ChangesetStatus.query()\
1920 statuses = ChangesetStatus.query()\
1921 .filter(ChangesetStatus.repo == self)\
1921 .filter(ChangesetStatus.repo == self)\
1922 .filter(ChangesetStatus.version == 0)
1922 .filter(ChangesetStatus.version == 0)
1923
1923
1924 if revisions:
1924 if revisions:
1925 # Try doing the filtering in chunks to avoid hitting limits
1925 # Try doing the filtering in chunks to avoid hitting limits
1926 size = 500
1926 size = 500
1927 status_results = []
1927 status_results = []
1928 for chunk in xrange(0, len(revisions), size):
1928 for chunk in xrange(0, len(revisions), size):
1929 status_results += statuses.filter(
1929 status_results += statuses.filter(
1930 ChangesetStatus.revision.in_(
1930 ChangesetStatus.revision.in_(
1931 revisions[chunk: chunk+size])
1931 revisions[chunk: chunk+size])
1932 ).all()
1932 ).all()
1933 else:
1933 else:
1934 status_results = statuses.all()
1934 status_results = statuses.all()
1935
1935
1936 grouped = {}
1936 grouped = {}
1937
1937
1938 # maybe we have open new pullrequest without a status?
1938 # maybe we have open new pullrequest without a status?
1939 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1939 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1940 status_lbl = ChangesetStatus.get_status_lbl(stat)
1940 status_lbl = ChangesetStatus.get_status_lbl(stat)
1941 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1941 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1942 for rev in pr.revisions:
1942 for rev in pr.revisions:
1943 pr_id = pr.pull_request_id
1943 pr_id = pr.pull_request_id
1944 pr_repo = pr.target_repo.repo_name
1944 pr_repo = pr.target_repo.repo_name
1945 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1945 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1946
1946
1947 for stat in status_results:
1947 for stat in status_results:
1948 pr_id = pr_repo = None
1948 pr_id = pr_repo = None
1949 if stat.pull_request:
1949 if stat.pull_request:
1950 pr_id = stat.pull_request.pull_request_id
1950 pr_id = stat.pull_request.pull_request_id
1951 pr_repo = stat.pull_request.target_repo.repo_name
1951 pr_repo = stat.pull_request.target_repo.repo_name
1952 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1952 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1953 pr_id, pr_repo]
1953 pr_id, pr_repo]
1954 return grouped
1954 return grouped
1955
1955
1956 # ==========================================================================
1956 # ==========================================================================
1957 # SCM CACHE INSTANCE
1957 # SCM CACHE INSTANCE
1958 # ==========================================================================
1958 # ==========================================================================
1959
1959
1960 def scm_instance(self, **kwargs):
1960 def scm_instance(self, **kwargs):
1961 import rhodecode
1961 import rhodecode
1962
1962
1963 # Passing a config will not hit the cache currently only used
1963 # Passing a config will not hit the cache currently only used
1964 # for repo2dbmapper
1964 # for repo2dbmapper
1965 config = kwargs.pop('config', None)
1965 config = kwargs.pop('config', None)
1966 cache = kwargs.pop('cache', None)
1966 cache = kwargs.pop('cache', None)
1967 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1967 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1968 # if cache is NOT defined use default global, else we have a full
1968 # if cache is NOT defined use default global, else we have a full
1969 # control over cache behaviour
1969 # control over cache behaviour
1970 if cache is None and full_cache and not config:
1970 if cache is None and full_cache and not config:
1971 return self._get_instance_cached()
1971 return self._get_instance_cached()
1972 return self._get_instance(cache=bool(cache), config=config)
1972 return self._get_instance(cache=bool(cache), config=config)
1973
1973
1974 def _get_instance_cached(self):
1974 def _get_instance_cached(self):
1975 @cache_region('long_term')
1975 @cache_region('long_term')
1976 def _get_repo(cache_key):
1976 def _get_repo(cache_key):
1977 return self._get_instance()
1977 return self._get_instance()
1978
1978
1979 invalidator_context = CacheKey.repo_context_cache(
1979 invalidator_context = CacheKey.repo_context_cache(
1980 _get_repo, self.repo_name, None, thread_scoped=True)
1980 _get_repo, self.repo_name, None, thread_scoped=True)
1981
1981
1982 with invalidator_context as context:
1982 with invalidator_context as context:
1983 context.invalidate()
1983 context.invalidate()
1984 repo = context.compute()
1984 repo = context.compute()
1985
1985
1986 return repo
1986 return repo
1987
1987
1988 def _get_instance(self, cache=True, config=None):
1988 def _get_instance(self, cache=True, config=None):
1989 config = config or self._config
1989 config = config or self._config
1990 custom_wire = {
1990 custom_wire = {
1991 'cache': cache # controls the vcs.remote cache
1991 'cache': cache # controls the vcs.remote cache
1992 }
1992 }
1993
1993
1994 repo = get_vcs_instance(
1994 repo = get_vcs_instance(
1995 repo_path=safe_str(self.repo_full_path),
1995 repo_path=safe_str(self.repo_full_path),
1996 config=config,
1996 config=config,
1997 with_wire=custom_wire,
1997 with_wire=custom_wire,
1998 create=False)
1998 create=False)
1999
1999
2000 return repo
2000 return repo
2001
2001
2002 def __json__(self):
2002 def __json__(self):
2003 return {'landing_rev': self.landing_rev}
2003 return {'landing_rev': self.landing_rev}
2004
2004
2005 def get_dict(self):
2005 def get_dict(self):
2006
2006
2007 # Since we transformed `repo_name` to a hybrid property, we need to
2007 # Since we transformed `repo_name` to a hybrid property, we need to
2008 # keep compatibility with the code which uses `repo_name` field.
2008 # keep compatibility with the code which uses `repo_name` field.
2009
2009
2010 result = super(Repository, self).get_dict()
2010 result = super(Repository, self).get_dict()
2011 result['repo_name'] = result.pop('_repo_name', None)
2011 result['repo_name'] = result.pop('_repo_name', None)
2012 return result
2012 return result
2013
2013
2014
2014
2015 class RepoGroup(Base, BaseModel):
2015 class RepoGroup(Base, BaseModel):
2016 __tablename__ = 'groups'
2016 __tablename__ = 'groups'
2017 __table_args__ = (
2017 __table_args__ = (
2018 UniqueConstraint('group_name', 'group_parent_id'),
2018 UniqueConstraint('group_name', 'group_parent_id'),
2019 CheckConstraint('group_id != group_parent_id'),
2019 CheckConstraint('group_id != group_parent_id'),
2020 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2020 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2021 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2021 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2022 )
2022 )
2023 __mapper_args__ = {'order_by': 'group_name'}
2023 __mapper_args__ = {'order_by': 'group_name'}
2024
2024
2025 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2025 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2026
2026
2027 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2027 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2028 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2028 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2029 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2029 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2030 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2030 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2031 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2031 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2032 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2032 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2033 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2033 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2034
2034
2035 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2035 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2036 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2036 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2037 parent_group = relationship('RepoGroup', remote_side=group_id)
2037 parent_group = relationship('RepoGroup', remote_side=group_id)
2038 user = relationship('User')
2038 user = relationship('User')
2039 integrations = relationship('Integration',
2039 integrations = relationship('Integration',
2040 cascade="all, delete, delete-orphan")
2040 cascade="all, delete, delete-orphan")
2041
2041
2042 def __init__(self, group_name='', parent_group=None):
2042 def __init__(self, group_name='', parent_group=None):
2043 self.group_name = group_name
2043 self.group_name = group_name
2044 self.parent_group = parent_group
2044 self.parent_group = parent_group
2045
2045
2046 def __unicode__(self):
2046 def __unicode__(self):
2047 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2047 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2048 self.group_name)
2048 self.group_name)
2049
2049
2050 @classmethod
2050 @classmethod
2051 def _generate_choice(cls, repo_group):
2051 def _generate_choice(cls, repo_group):
2052 from webhelpers.html import literal as _literal
2052 from webhelpers.html import literal as _literal
2053 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2053 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2054 return repo_group.group_id, _name(repo_group.full_path_splitted)
2054 return repo_group.group_id, _name(repo_group.full_path_splitted)
2055
2055
2056 @classmethod
2056 @classmethod
2057 def groups_choices(cls, groups=None, show_empty_group=True):
2057 def groups_choices(cls, groups=None, show_empty_group=True):
2058 if not groups:
2058 if not groups:
2059 groups = cls.query().all()
2059 groups = cls.query().all()
2060
2060
2061 repo_groups = []
2061 repo_groups = []
2062 if show_empty_group:
2062 if show_empty_group:
2063 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2063 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2064
2064
2065 repo_groups.extend([cls._generate_choice(x) for x in groups])
2065 repo_groups.extend([cls._generate_choice(x) for x in groups])
2066
2066
2067 repo_groups = sorted(
2067 repo_groups = sorted(
2068 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2068 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2069 return repo_groups
2069 return repo_groups
2070
2070
2071 @classmethod
2071 @classmethod
2072 def url_sep(cls):
2072 def url_sep(cls):
2073 return URL_SEP
2073 return URL_SEP
2074
2074
2075 @classmethod
2075 @classmethod
2076 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2076 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2077 if case_insensitive:
2077 if case_insensitive:
2078 gr = cls.query().filter(func.lower(cls.group_name)
2078 gr = cls.query().filter(func.lower(cls.group_name)
2079 == func.lower(group_name))
2079 == func.lower(group_name))
2080 else:
2080 else:
2081 gr = cls.query().filter(cls.group_name == group_name)
2081 gr = cls.query().filter(cls.group_name == group_name)
2082 if cache:
2082 if cache:
2083 gr = gr.options(FromCache(
2083 gr = gr.options(FromCache(
2084 "sql_cache_short",
2084 "sql_cache_short",
2085 "get_group_%s" % _hash_key(group_name)))
2085 "get_group_%s" % _hash_key(group_name)))
2086 return gr.scalar()
2086 return gr.scalar()
2087
2087
2088 @classmethod
2088 @classmethod
2089 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2089 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2090 case_insensitive=True):
2090 case_insensitive=True):
2091 q = RepoGroup.query()
2091 q = RepoGroup.query()
2092
2092
2093 if not isinstance(user_id, Optional):
2093 if not isinstance(user_id, Optional):
2094 q = q.filter(RepoGroup.user_id == user_id)
2094 q = q.filter(RepoGroup.user_id == user_id)
2095
2095
2096 if not isinstance(group_id, Optional):
2096 if not isinstance(group_id, Optional):
2097 q = q.filter(RepoGroup.group_parent_id == group_id)
2097 q = q.filter(RepoGroup.group_parent_id == group_id)
2098
2098
2099 if case_insensitive:
2099 if case_insensitive:
2100 q = q.order_by(func.lower(RepoGroup.group_name))
2100 q = q.order_by(func.lower(RepoGroup.group_name))
2101 else:
2101 else:
2102 q = q.order_by(RepoGroup.group_name)
2102 q = q.order_by(RepoGroup.group_name)
2103 return q.all()
2103 return q.all()
2104
2104
2105 @property
2105 @property
2106 def parents(self):
2106 def parents(self):
2107 parents_recursion_limit = 10
2107 parents_recursion_limit = 10
2108 groups = []
2108 groups = []
2109 if self.parent_group is None:
2109 if self.parent_group is None:
2110 return groups
2110 return groups
2111 cur_gr = self.parent_group
2111 cur_gr = self.parent_group
2112 groups.insert(0, cur_gr)
2112 groups.insert(0, cur_gr)
2113 cnt = 0
2113 cnt = 0
2114 while 1:
2114 while 1:
2115 cnt += 1
2115 cnt += 1
2116 gr = getattr(cur_gr, 'parent_group', None)
2116 gr = getattr(cur_gr, 'parent_group', None)
2117 cur_gr = cur_gr.parent_group
2117 cur_gr = cur_gr.parent_group
2118 if gr is None:
2118 if gr is None:
2119 break
2119 break
2120 if cnt == parents_recursion_limit:
2120 if cnt == parents_recursion_limit:
2121 # this will prevent accidental infinit loops
2121 # this will prevent accidental infinit loops
2122 log.error(('more than %s parents found for group %s, stopping '
2122 log.error(('more than %s parents found for group %s, stopping '
2123 'recursive parent fetching' % (parents_recursion_limit, self)))
2123 'recursive parent fetching' % (parents_recursion_limit, self)))
2124 break
2124 break
2125
2125
2126 groups.insert(0, gr)
2126 groups.insert(0, gr)
2127 return groups
2127 return groups
2128
2128
2129 @property
2129 @property
2130 def children(self):
2130 def children(self):
2131 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2131 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2132
2132
2133 @property
2133 @property
2134 def name(self):
2134 def name(self):
2135 return self.group_name.split(RepoGroup.url_sep())[-1]
2135 return self.group_name.split(RepoGroup.url_sep())[-1]
2136
2136
2137 @property
2137 @property
2138 def full_path(self):
2138 def full_path(self):
2139 return self.group_name
2139 return self.group_name
2140
2140
2141 @property
2141 @property
2142 def full_path_splitted(self):
2142 def full_path_splitted(self):
2143 return self.group_name.split(RepoGroup.url_sep())
2143 return self.group_name.split(RepoGroup.url_sep())
2144
2144
2145 @property
2145 @property
2146 def repositories(self):
2146 def repositories(self):
2147 return Repository.query()\
2147 return Repository.query()\
2148 .filter(Repository.group == self)\
2148 .filter(Repository.group == self)\
2149 .order_by(Repository.repo_name)
2149 .order_by(Repository.repo_name)
2150
2150
2151 @property
2151 @property
2152 def repositories_recursive_count(self):
2152 def repositories_recursive_count(self):
2153 cnt = self.repositories.count()
2153 cnt = self.repositories.count()
2154
2154
2155 def children_count(group):
2155 def children_count(group):
2156 cnt = 0
2156 cnt = 0
2157 for child in group.children:
2157 for child in group.children:
2158 cnt += child.repositories.count()
2158 cnt += child.repositories.count()
2159 cnt += children_count(child)
2159 cnt += children_count(child)
2160 return cnt
2160 return cnt
2161
2161
2162 return cnt + children_count(self)
2162 return cnt + children_count(self)
2163
2163
2164 def _recursive_objects(self, include_repos=True):
2164 def _recursive_objects(self, include_repos=True):
2165 all_ = []
2165 all_ = []
2166
2166
2167 def _get_members(root_gr):
2167 def _get_members(root_gr):
2168 if include_repos:
2168 if include_repos:
2169 for r in root_gr.repositories:
2169 for r in root_gr.repositories:
2170 all_.append(r)
2170 all_.append(r)
2171 childs = root_gr.children.all()
2171 childs = root_gr.children.all()
2172 if childs:
2172 if childs:
2173 for gr in childs:
2173 for gr in childs:
2174 all_.append(gr)
2174 all_.append(gr)
2175 _get_members(gr)
2175 _get_members(gr)
2176
2176
2177 _get_members(self)
2177 _get_members(self)
2178 return [self] + all_
2178 return [self] + all_
2179
2179
2180 def recursive_groups_and_repos(self):
2180 def recursive_groups_and_repos(self):
2181 """
2181 """
2182 Recursive return all groups, with repositories in those groups
2182 Recursive return all groups, with repositories in those groups
2183 """
2183 """
2184 return self._recursive_objects()
2184 return self._recursive_objects()
2185
2185
2186 def recursive_groups(self):
2186 def recursive_groups(self):
2187 """
2187 """
2188 Returns all children groups for this group including children of children
2188 Returns all children groups for this group including children of children
2189 """
2189 """
2190 return self._recursive_objects(include_repos=False)
2190 return self._recursive_objects(include_repos=False)
2191
2191
2192 def get_new_name(self, group_name):
2192 def get_new_name(self, group_name):
2193 """
2193 """
2194 returns new full group name based on parent and new name
2194 returns new full group name based on parent and new name
2195
2195
2196 :param group_name:
2196 :param group_name:
2197 """
2197 """
2198 path_prefix = (self.parent_group.full_path_splitted if
2198 path_prefix = (self.parent_group.full_path_splitted if
2199 self.parent_group else [])
2199 self.parent_group else [])
2200 return RepoGroup.url_sep().join(path_prefix + [group_name])
2200 return RepoGroup.url_sep().join(path_prefix + [group_name])
2201
2201
2202 def permissions(self, with_admins=True, with_owner=True):
2202 def permissions(self, with_admins=True, with_owner=True):
2203 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2203 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2204 q = q.options(joinedload(UserRepoGroupToPerm.group),
2204 q = q.options(joinedload(UserRepoGroupToPerm.group),
2205 joinedload(UserRepoGroupToPerm.user),
2205 joinedload(UserRepoGroupToPerm.user),
2206 joinedload(UserRepoGroupToPerm.permission),)
2206 joinedload(UserRepoGroupToPerm.permission),)
2207
2207
2208 # get owners and admins and permissions. We do a trick of re-writing
2208 # get owners and admins and permissions. We do a trick of re-writing
2209 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2209 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2210 # has a global reference and changing one object propagates to all
2210 # has a global reference and changing one object propagates to all
2211 # others. This means if admin is also an owner admin_row that change
2211 # others. This means if admin is also an owner admin_row that change
2212 # would propagate to both objects
2212 # would propagate to both objects
2213 perm_rows = []
2213 perm_rows = []
2214 for _usr in q.all():
2214 for _usr in q.all():
2215 usr = AttributeDict(_usr.user.get_dict())
2215 usr = AttributeDict(_usr.user.get_dict())
2216 usr.permission = _usr.permission.permission_name
2216 usr.permission = _usr.permission.permission_name
2217 perm_rows.append(usr)
2217 perm_rows.append(usr)
2218
2218
2219 # filter the perm rows by 'default' first and then sort them by
2219 # filter the perm rows by 'default' first and then sort them by
2220 # admin,write,read,none permissions sorted again alphabetically in
2220 # admin,write,read,none permissions sorted again alphabetically in
2221 # each group
2221 # each group
2222 perm_rows = sorted(perm_rows, key=display_sort)
2222 perm_rows = sorted(perm_rows, key=display_sort)
2223
2223
2224 _admin_perm = 'group.admin'
2224 _admin_perm = 'group.admin'
2225 owner_row = []
2225 owner_row = []
2226 if with_owner:
2226 if with_owner:
2227 usr = AttributeDict(self.user.get_dict())
2227 usr = AttributeDict(self.user.get_dict())
2228 usr.owner_row = True
2228 usr.owner_row = True
2229 usr.permission = _admin_perm
2229 usr.permission = _admin_perm
2230 owner_row.append(usr)
2230 owner_row.append(usr)
2231
2231
2232 super_admin_rows = []
2232 super_admin_rows = []
2233 if with_admins:
2233 if with_admins:
2234 for usr in User.get_all_super_admins():
2234 for usr in User.get_all_super_admins():
2235 # if this admin is also owner, don't double the record
2235 # if this admin is also owner, don't double the record
2236 if usr.user_id == owner_row[0].user_id:
2236 if usr.user_id == owner_row[0].user_id:
2237 owner_row[0].admin_row = True
2237 owner_row[0].admin_row = True
2238 else:
2238 else:
2239 usr = AttributeDict(usr.get_dict())
2239 usr = AttributeDict(usr.get_dict())
2240 usr.admin_row = True
2240 usr.admin_row = True
2241 usr.permission = _admin_perm
2241 usr.permission = _admin_perm
2242 super_admin_rows.append(usr)
2242 super_admin_rows.append(usr)
2243
2243
2244 return super_admin_rows + owner_row + perm_rows
2244 return super_admin_rows + owner_row + perm_rows
2245
2245
2246 def permission_user_groups(self):
2246 def permission_user_groups(self):
2247 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2247 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2248 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2248 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2249 joinedload(UserGroupRepoGroupToPerm.users_group),
2249 joinedload(UserGroupRepoGroupToPerm.users_group),
2250 joinedload(UserGroupRepoGroupToPerm.permission),)
2250 joinedload(UserGroupRepoGroupToPerm.permission),)
2251
2251
2252 perm_rows = []
2252 perm_rows = []
2253 for _user_group in q.all():
2253 for _user_group in q.all():
2254 usr = AttributeDict(_user_group.users_group.get_dict())
2254 usr = AttributeDict(_user_group.users_group.get_dict())
2255 usr.permission = _user_group.permission.permission_name
2255 usr.permission = _user_group.permission.permission_name
2256 perm_rows.append(usr)
2256 perm_rows.append(usr)
2257
2257
2258 return perm_rows
2258 return perm_rows
2259
2259
2260 def get_api_data(self):
2260 def get_api_data(self):
2261 """
2261 """
2262 Common function for generating api data
2262 Common function for generating api data
2263
2263
2264 """
2264 """
2265 group = self
2265 group = self
2266 data = {
2266 data = {
2267 'group_id': group.group_id,
2267 'group_id': group.group_id,
2268 'group_name': group.group_name,
2268 'group_name': group.group_name,
2269 'group_description': group.group_description,
2269 'group_description': group.group_description,
2270 'parent_group': group.parent_group.group_name if group.parent_group else None,
2270 'parent_group': group.parent_group.group_name if group.parent_group else None,
2271 'repositories': [x.repo_name for x in group.repositories],
2271 'repositories': [x.repo_name for x in group.repositories],
2272 'owner': group.user.username,
2272 'owner': group.user.username,
2273 }
2273 }
2274 return data
2274 return data
2275
2275
2276
2276
2277 class Permission(Base, BaseModel):
2277 class Permission(Base, BaseModel):
2278 __tablename__ = 'permissions'
2278 __tablename__ = 'permissions'
2279 __table_args__ = (
2279 __table_args__ = (
2280 Index('p_perm_name_idx', 'permission_name'),
2280 Index('p_perm_name_idx', 'permission_name'),
2281 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2281 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2282 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2282 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2283 )
2283 )
2284 PERMS = [
2284 PERMS = [
2285 ('hg.admin', _('RhodeCode Super Administrator')),
2285 ('hg.admin', _('RhodeCode Super Administrator')),
2286
2286
2287 ('repository.none', _('Repository no access')),
2287 ('repository.none', _('Repository no access')),
2288 ('repository.read', _('Repository read access')),
2288 ('repository.read', _('Repository read access')),
2289 ('repository.write', _('Repository write access')),
2289 ('repository.write', _('Repository write access')),
2290 ('repository.admin', _('Repository admin access')),
2290 ('repository.admin', _('Repository admin access')),
2291
2291
2292 ('group.none', _('Repository group no access')),
2292 ('group.none', _('Repository group no access')),
2293 ('group.read', _('Repository group read access')),
2293 ('group.read', _('Repository group read access')),
2294 ('group.write', _('Repository group write access')),
2294 ('group.write', _('Repository group write access')),
2295 ('group.admin', _('Repository group admin access')),
2295 ('group.admin', _('Repository group admin access')),
2296
2296
2297 ('usergroup.none', _('User group no access')),
2297 ('usergroup.none', _('User group no access')),
2298 ('usergroup.read', _('User group read access')),
2298 ('usergroup.read', _('User group read access')),
2299 ('usergroup.write', _('User group write access')),
2299 ('usergroup.write', _('User group write access')),
2300 ('usergroup.admin', _('User group admin access')),
2300 ('usergroup.admin', _('User group admin access')),
2301
2301
2302 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2302 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2303 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2303 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2304
2304
2305 ('hg.usergroup.create.false', _('User Group creation disabled')),
2305 ('hg.usergroup.create.false', _('User Group creation disabled')),
2306 ('hg.usergroup.create.true', _('User Group creation enabled')),
2306 ('hg.usergroup.create.true', _('User Group creation enabled')),
2307
2307
2308 ('hg.create.none', _('Repository creation disabled')),
2308 ('hg.create.none', _('Repository creation disabled')),
2309 ('hg.create.repository', _('Repository creation enabled')),
2309 ('hg.create.repository', _('Repository creation enabled')),
2310 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2310 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2311 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2311 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2312
2312
2313 ('hg.fork.none', _('Repository forking disabled')),
2313 ('hg.fork.none', _('Repository forking disabled')),
2314 ('hg.fork.repository', _('Repository forking enabled')),
2314 ('hg.fork.repository', _('Repository forking enabled')),
2315
2315
2316 ('hg.register.none', _('Registration disabled')),
2316 ('hg.register.none', _('Registration disabled')),
2317 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2317 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2318 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2318 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2319
2319
2320 ('hg.extern_activate.manual', _('Manual activation of external account')),
2320 ('hg.extern_activate.manual', _('Manual activation of external account')),
2321 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2321 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2322
2322
2323 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2323 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2324 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2324 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2325 ]
2325 ]
2326
2326
2327 # definition of system default permissions for DEFAULT user
2327 # definition of system default permissions for DEFAULT user
2328 DEFAULT_USER_PERMISSIONS = [
2328 DEFAULT_USER_PERMISSIONS = [
2329 'repository.read',
2329 'repository.read',
2330 'group.read',
2330 'group.read',
2331 'usergroup.read',
2331 'usergroup.read',
2332 'hg.create.repository',
2332 'hg.create.repository',
2333 'hg.repogroup.create.false',
2333 'hg.repogroup.create.false',
2334 'hg.usergroup.create.false',
2334 'hg.usergroup.create.false',
2335 'hg.create.write_on_repogroup.true',
2335 'hg.create.write_on_repogroup.true',
2336 'hg.fork.repository',
2336 'hg.fork.repository',
2337 'hg.register.manual_activate',
2337 'hg.register.manual_activate',
2338 'hg.extern_activate.auto',
2338 'hg.extern_activate.auto',
2339 'hg.inherit_default_perms.true',
2339 'hg.inherit_default_perms.true',
2340 ]
2340 ]
2341
2341
2342 # defines which permissions are more important higher the more important
2342 # defines which permissions are more important higher the more important
2343 # Weight defines which permissions are more important.
2343 # Weight defines which permissions are more important.
2344 # The higher number the more important.
2344 # The higher number the more important.
2345 PERM_WEIGHTS = {
2345 PERM_WEIGHTS = {
2346 'repository.none': 0,
2346 'repository.none': 0,
2347 'repository.read': 1,
2347 'repository.read': 1,
2348 'repository.write': 3,
2348 'repository.write': 3,
2349 'repository.admin': 4,
2349 'repository.admin': 4,
2350
2350
2351 'group.none': 0,
2351 'group.none': 0,
2352 'group.read': 1,
2352 'group.read': 1,
2353 'group.write': 3,
2353 'group.write': 3,
2354 'group.admin': 4,
2354 'group.admin': 4,
2355
2355
2356 'usergroup.none': 0,
2356 'usergroup.none': 0,
2357 'usergroup.read': 1,
2357 'usergroup.read': 1,
2358 'usergroup.write': 3,
2358 'usergroup.write': 3,
2359 'usergroup.admin': 4,
2359 'usergroup.admin': 4,
2360
2360
2361 'hg.repogroup.create.false': 0,
2361 'hg.repogroup.create.false': 0,
2362 'hg.repogroup.create.true': 1,
2362 'hg.repogroup.create.true': 1,
2363
2363
2364 'hg.usergroup.create.false': 0,
2364 'hg.usergroup.create.false': 0,
2365 'hg.usergroup.create.true': 1,
2365 'hg.usergroup.create.true': 1,
2366
2366
2367 'hg.fork.none': 0,
2367 'hg.fork.none': 0,
2368 'hg.fork.repository': 1,
2368 'hg.fork.repository': 1,
2369 'hg.create.none': 0,
2369 'hg.create.none': 0,
2370 'hg.create.repository': 1
2370 'hg.create.repository': 1
2371 }
2371 }
2372
2372
2373 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2373 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2374 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2374 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2375 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2375 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2376
2376
2377 def __unicode__(self):
2377 def __unicode__(self):
2378 return u"<%s('%s:%s')>" % (
2378 return u"<%s('%s:%s')>" % (
2379 self.__class__.__name__, self.permission_id, self.permission_name
2379 self.__class__.__name__, self.permission_id, self.permission_name
2380 )
2380 )
2381
2381
2382 @classmethod
2382 @classmethod
2383 def get_by_key(cls, key):
2383 def get_by_key(cls, key):
2384 return cls.query().filter(cls.permission_name == key).scalar()
2384 return cls.query().filter(cls.permission_name == key).scalar()
2385
2385
2386 @classmethod
2386 @classmethod
2387 def get_default_repo_perms(cls, user_id, repo_id=None):
2387 def get_default_repo_perms(cls, user_id, repo_id=None):
2388 q = Session().query(UserRepoToPerm, Repository, Permission)\
2388 q = Session().query(UserRepoToPerm, Repository, Permission)\
2389 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2389 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2390 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2390 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2391 .filter(UserRepoToPerm.user_id == user_id)
2391 .filter(UserRepoToPerm.user_id == user_id)
2392 if repo_id:
2392 if repo_id:
2393 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2393 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2394 return q.all()
2394 return q.all()
2395
2395
2396 @classmethod
2396 @classmethod
2397 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2397 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2398 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2398 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2399 .join(
2399 .join(
2400 Permission,
2400 Permission,
2401 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2401 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2402 .join(
2402 .join(
2403 Repository,
2403 Repository,
2404 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2404 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2405 .join(
2405 .join(
2406 UserGroup,
2406 UserGroup,
2407 UserGroupRepoToPerm.users_group_id ==
2407 UserGroupRepoToPerm.users_group_id ==
2408 UserGroup.users_group_id)\
2408 UserGroup.users_group_id)\
2409 .join(
2409 .join(
2410 UserGroupMember,
2410 UserGroupMember,
2411 UserGroupRepoToPerm.users_group_id ==
2411 UserGroupRepoToPerm.users_group_id ==
2412 UserGroupMember.users_group_id)\
2412 UserGroupMember.users_group_id)\
2413 .filter(
2413 .filter(
2414 UserGroupMember.user_id == user_id,
2414 UserGroupMember.user_id == user_id,
2415 UserGroup.users_group_active == true())
2415 UserGroup.users_group_active == true())
2416 if repo_id:
2416 if repo_id:
2417 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2417 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2418 return q.all()
2418 return q.all()
2419
2419
2420 @classmethod
2420 @classmethod
2421 def get_default_group_perms(cls, user_id, repo_group_id=None):
2421 def get_default_group_perms(cls, user_id, repo_group_id=None):
2422 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2422 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2423 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2423 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2424 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2424 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2425 .filter(UserRepoGroupToPerm.user_id == user_id)
2425 .filter(UserRepoGroupToPerm.user_id == user_id)
2426 if repo_group_id:
2426 if repo_group_id:
2427 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2427 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2428 return q.all()
2428 return q.all()
2429
2429
2430 @classmethod
2430 @classmethod
2431 def get_default_group_perms_from_user_group(
2431 def get_default_group_perms_from_user_group(
2432 cls, user_id, repo_group_id=None):
2432 cls, user_id, repo_group_id=None):
2433 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2433 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2434 .join(
2434 .join(
2435 Permission,
2435 Permission,
2436 UserGroupRepoGroupToPerm.permission_id ==
2436 UserGroupRepoGroupToPerm.permission_id ==
2437 Permission.permission_id)\
2437 Permission.permission_id)\
2438 .join(
2438 .join(
2439 RepoGroup,
2439 RepoGroup,
2440 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2440 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2441 .join(
2441 .join(
2442 UserGroup,
2442 UserGroup,
2443 UserGroupRepoGroupToPerm.users_group_id ==
2443 UserGroupRepoGroupToPerm.users_group_id ==
2444 UserGroup.users_group_id)\
2444 UserGroup.users_group_id)\
2445 .join(
2445 .join(
2446 UserGroupMember,
2446 UserGroupMember,
2447 UserGroupRepoGroupToPerm.users_group_id ==
2447 UserGroupRepoGroupToPerm.users_group_id ==
2448 UserGroupMember.users_group_id)\
2448 UserGroupMember.users_group_id)\
2449 .filter(
2449 .filter(
2450 UserGroupMember.user_id == user_id,
2450 UserGroupMember.user_id == user_id,
2451 UserGroup.users_group_active == true())
2451 UserGroup.users_group_active == true())
2452 if repo_group_id:
2452 if repo_group_id:
2453 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2453 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2454 return q.all()
2454 return q.all()
2455
2455
2456 @classmethod
2456 @classmethod
2457 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2457 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2458 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2458 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2459 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2459 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2460 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2460 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2461 .filter(UserUserGroupToPerm.user_id == user_id)
2461 .filter(UserUserGroupToPerm.user_id == user_id)
2462 if user_group_id:
2462 if user_group_id:
2463 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2463 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2464 return q.all()
2464 return q.all()
2465
2465
2466 @classmethod
2466 @classmethod
2467 def get_default_user_group_perms_from_user_group(
2467 def get_default_user_group_perms_from_user_group(
2468 cls, user_id, user_group_id=None):
2468 cls, user_id, user_group_id=None):
2469 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2469 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2470 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2470 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2471 .join(
2471 .join(
2472 Permission,
2472 Permission,
2473 UserGroupUserGroupToPerm.permission_id ==
2473 UserGroupUserGroupToPerm.permission_id ==
2474 Permission.permission_id)\
2474 Permission.permission_id)\
2475 .join(
2475 .join(
2476 TargetUserGroup,
2476 TargetUserGroup,
2477 UserGroupUserGroupToPerm.target_user_group_id ==
2477 UserGroupUserGroupToPerm.target_user_group_id ==
2478 TargetUserGroup.users_group_id)\
2478 TargetUserGroup.users_group_id)\
2479 .join(
2479 .join(
2480 UserGroup,
2480 UserGroup,
2481 UserGroupUserGroupToPerm.user_group_id ==
2481 UserGroupUserGroupToPerm.user_group_id ==
2482 UserGroup.users_group_id)\
2482 UserGroup.users_group_id)\
2483 .join(
2483 .join(
2484 UserGroupMember,
2484 UserGroupMember,
2485 UserGroupUserGroupToPerm.user_group_id ==
2485 UserGroupUserGroupToPerm.user_group_id ==
2486 UserGroupMember.users_group_id)\
2486 UserGroupMember.users_group_id)\
2487 .filter(
2487 .filter(
2488 UserGroupMember.user_id == user_id,
2488 UserGroupMember.user_id == user_id,
2489 UserGroup.users_group_active == true())
2489 UserGroup.users_group_active == true())
2490 if user_group_id:
2490 if user_group_id:
2491 q = q.filter(
2491 q = q.filter(
2492 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2492 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2493
2493
2494 return q.all()
2494 return q.all()
2495
2495
2496
2496
2497 class UserRepoToPerm(Base, BaseModel):
2497 class UserRepoToPerm(Base, BaseModel):
2498 __tablename__ = 'repo_to_perm'
2498 __tablename__ = 'repo_to_perm'
2499 __table_args__ = (
2499 __table_args__ = (
2500 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2500 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2501 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2501 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2502 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2502 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2503 )
2503 )
2504 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2504 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2505 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2505 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2506 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2506 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2507 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2507 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2508
2508
2509 user = relationship('User')
2509 user = relationship('User')
2510 repository = relationship('Repository')
2510 repository = relationship('Repository')
2511 permission = relationship('Permission')
2511 permission = relationship('Permission')
2512
2512
2513 @classmethod
2513 @classmethod
2514 def create(cls, user, repository, permission):
2514 def create(cls, user, repository, permission):
2515 n = cls()
2515 n = cls()
2516 n.user = user
2516 n.user = user
2517 n.repository = repository
2517 n.repository = repository
2518 n.permission = permission
2518 n.permission = permission
2519 Session().add(n)
2519 Session().add(n)
2520 return n
2520 return n
2521
2521
2522 def __unicode__(self):
2522 def __unicode__(self):
2523 return u'<%s => %s >' % (self.user, self.repository)
2523 return u'<%s => %s >' % (self.user, self.repository)
2524
2524
2525
2525
2526 class UserUserGroupToPerm(Base, BaseModel):
2526 class UserUserGroupToPerm(Base, BaseModel):
2527 __tablename__ = 'user_user_group_to_perm'
2527 __tablename__ = 'user_user_group_to_perm'
2528 __table_args__ = (
2528 __table_args__ = (
2529 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2529 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2530 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2530 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2531 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2531 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2532 )
2532 )
2533 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2533 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2534 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2534 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2535 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2535 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2536 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2536 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2537
2537
2538 user = relationship('User')
2538 user = relationship('User')
2539 user_group = relationship('UserGroup')
2539 user_group = relationship('UserGroup')
2540 permission = relationship('Permission')
2540 permission = relationship('Permission')
2541
2541
2542 @classmethod
2542 @classmethod
2543 def create(cls, user, user_group, permission):
2543 def create(cls, user, user_group, permission):
2544 n = cls()
2544 n = cls()
2545 n.user = user
2545 n.user = user
2546 n.user_group = user_group
2546 n.user_group = user_group
2547 n.permission = permission
2547 n.permission = permission
2548 Session().add(n)
2548 Session().add(n)
2549 return n
2549 return n
2550
2550
2551 def __unicode__(self):
2551 def __unicode__(self):
2552 return u'<%s => %s >' % (self.user, self.user_group)
2552 return u'<%s => %s >' % (self.user, self.user_group)
2553
2553
2554
2554
2555 class UserToPerm(Base, BaseModel):
2555 class UserToPerm(Base, BaseModel):
2556 __tablename__ = 'user_to_perm'
2556 __tablename__ = 'user_to_perm'
2557 __table_args__ = (
2557 __table_args__ = (
2558 UniqueConstraint('user_id', 'permission_id'),
2558 UniqueConstraint('user_id', 'permission_id'),
2559 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2559 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2560 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2560 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2561 )
2561 )
2562 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2562 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2563 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2563 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2564 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2564 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2565
2565
2566 user = relationship('User')
2566 user = relationship('User')
2567 permission = relationship('Permission', lazy='joined')
2567 permission = relationship('Permission', lazy='joined')
2568
2568
2569 def __unicode__(self):
2569 def __unicode__(self):
2570 return u'<%s => %s >' % (self.user, self.permission)
2570 return u'<%s => %s >' % (self.user, self.permission)
2571
2571
2572
2572
2573 class UserGroupRepoToPerm(Base, BaseModel):
2573 class UserGroupRepoToPerm(Base, BaseModel):
2574 __tablename__ = 'users_group_repo_to_perm'
2574 __tablename__ = 'users_group_repo_to_perm'
2575 __table_args__ = (
2575 __table_args__ = (
2576 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2576 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2577 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2577 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2578 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2578 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2579 )
2579 )
2580 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2580 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2581 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2581 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2582 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2582 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2583 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2583 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2584
2584
2585 users_group = relationship('UserGroup')
2585 users_group = relationship('UserGroup')
2586 permission = relationship('Permission')
2586 permission = relationship('Permission')
2587 repository = relationship('Repository')
2587 repository = relationship('Repository')
2588
2588
2589 @classmethod
2589 @classmethod
2590 def create(cls, users_group, repository, permission):
2590 def create(cls, users_group, repository, permission):
2591 n = cls()
2591 n = cls()
2592 n.users_group = users_group
2592 n.users_group = users_group
2593 n.repository = repository
2593 n.repository = repository
2594 n.permission = permission
2594 n.permission = permission
2595 Session().add(n)
2595 Session().add(n)
2596 return n
2596 return n
2597
2597
2598 def __unicode__(self):
2598 def __unicode__(self):
2599 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2599 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2600
2600
2601
2601
2602 class UserGroupUserGroupToPerm(Base, BaseModel):
2602 class UserGroupUserGroupToPerm(Base, BaseModel):
2603 __tablename__ = 'user_group_user_group_to_perm'
2603 __tablename__ = 'user_group_user_group_to_perm'
2604 __table_args__ = (
2604 __table_args__ = (
2605 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2605 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2606 CheckConstraint('target_user_group_id != user_group_id'),
2606 CheckConstraint('target_user_group_id != user_group_id'),
2607 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2607 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2608 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2608 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2609 )
2609 )
2610 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)
2610 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)
2611 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2611 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2612 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2612 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2613 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2613 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2614
2614
2615 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2615 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2616 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2616 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2617 permission = relationship('Permission')
2617 permission = relationship('Permission')
2618
2618
2619 @classmethod
2619 @classmethod
2620 def create(cls, target_user_group, user_group, permission):
2620 def create(cls, target_user_group, user_group, permission):
2621 n = cls()
2621 n = cls()
2622 n.target_user_group = target_user_group
2622 n.target_user_group = target_user_group
2623 n.user_group = user_group
2623 n.user_group = user_group
2624 n.permission = permission
2624 n.permission = permission
2625 Session().add(n)
2625 Session().add(n)
2626 return n
2626 return n
2627
2627
2628 def __unicode__(self):
2628 def __unicode__(self):
2629 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2629 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2630
2630
2631
2631
2632 class UserGroupToPerm(Base, BaseModel):
2632 class UserGroupToPerm(Base, BaseModel):
2633 __tablename__ = 'users_group_to_perm'
2633 __tablename__ = 'users_group_to_perm'
2634 __table_args__ = (
2634 __table_args__ = (
2635 UniqueConstraint('users_group_id', 'permission_id',),
2635 UniqueConstraint('users_group_id', 'permission_id',),
2636 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2636 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2637 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2637 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2638 )
2638 )
2639 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2639 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2640 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2640 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2641 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2641 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2642
2642
2643 users_group = relationship('UserGroup')
2643 users_group = relationship('UserGroup')
2644 permission = relationship('Permission')
2644 permission = relationship('Permission')
2645
2645
2646
2646
2647 class UserRepoGroupToPerm(Base, BaseModel):
2647 class UserRepoGroupToPerm(Base, BaseModel):
2648 __tablename__ = 'user_repo_group_to_perm'
2648 __tablename__ = 'user_repo_group_to_perm'
2649 __table_args__ = (
2649 __table_args__ = (
2650 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2650 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2651 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2651 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2652 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2652 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2653 )
2653 )
2654
2654
2655 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2655 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2656 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2656 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2657 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2657 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2658 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2658 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2659
2659
2660 user = relationship('User')
2660 user = relationship('User')
2661 group = relationship('RepoGroup')
2661 group = relationship('RepoGroup')
2662 permission = relationship('Permission')
2662 permission = relationship('Permission')
2663
2663
2664 @classmethod
2664 @classmethod
2665 def create(cls, user, repository_group, permission):
2665 def create(cls, user, repository_group, permission):
2666 n = cls()
2666 n = cls()
2667 n.user = user
2667 n.user = user
2668 n.group = repository_group
2668 n.group = repository_group
2669 n.permission = permission
2669 n.permission = permission
2670 Session().add(n)
2670 Session().add(n)
2671 return n
2671 return n
2672
2672
2673
2673
2674 class UserGroupRepoGroupToPerm(Base, BaseModel):
2674 class UserGroupRepoGroupToPerm(Base, BaseModel):
2675 __tablename__ = 'users_group_repo_group_to_perm'
2675 __tablename__ = 'users_group_repo_group_to_perm'
2676 __table_args__ = (
2676 __table_args__ = (
2677 UniqueConstraint('users_group_id', 'group_id'),
2677 UniqueConstraint('users_group_id', 'group_id'),
2678 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2678 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2679 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2679 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2680 )
2680 )
2681
2681
2682 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)
2682 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)
2683 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2683 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2684 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2684 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2685 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2685 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2686
2686
2687 users_group = relationship('UserGroup')
2687 users_group = relationship('UserGroup')
2688 permission = relationship('Permission')
2688 permission = relationship('Permission')
2689 group = relationship('RepoGroup')
2689 group = relationship('RepoGroup')
2690
2690
2691 @classmethod
2691 @classmethod
2692 def create(cls, user_group, repository_group, permission):
2692 def create(cls, user_group, repository_group, permission):
2693 n = cls()
2693 n = cls()
2694 n.users_group = user_group
2694 n.users_group = user_group
2695 n.group = repository_group
2695 n.group = repository_group
2696 n.permission = permission
2696 n.permission = permission
2697 Session().add(n)
2697 Session().add(n)
2698 return n
2698 return n
2699
2699
2700 def __unicode__(self):
2700 def __unicode__(self):
2701 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2701 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2702
2702
2703
2703
2704 class Statistics(Base, BaseModel):
2704 class Statistics(Base, BaseModel):
2705 __tablename__ = 'statistics'
2705 __tablename__ = 'statistics'
2706 __table_args__ = (
2706 __table_args__ = (
2707 UniqueConstraint('repository_id'),
2707 UniqueConstraint('repository_id'),
2708 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2708 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2709 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2709 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2710 )
2710 )
2711 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2711 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2712 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2712 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2713 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2713 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2714 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2714 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2715 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2715 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2716 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2716 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2717
2717
2718 repository = relationship('Repository', single_parent=True)
2718 repository = relationship('Repository', single_parent=True)
2719
2719
2720
2720
2721 class UserFollowing(Base, BaseModel):
2721 class UserFollowing(Base, BaseModel):
2722 __tablename__ = 'user_followings'
2722 __tablename__ = 'user_followings'
2723 __table_args__ = (
2723 __table_args__ = (
2724 UniqueConstraint('user_id', 'follows_repository_id'),
2724 UniqueConstraint('user_id', 'follows_repository_id'),
2725 UniqueConstraint('user_id', 'follows_user_id'),
2725 UniqueConstraint('user_id', 'follows_user_id'),
2726 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2726 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2727 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2727 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2728 )
2728 )
2729
2729
2730 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2730 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2731 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2731 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2732 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2732 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2733 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2733 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2734 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2734 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2735
2735
2736 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2736 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2737
2737
2738 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2738 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2739 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2739 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2740
2740
2741 @classmethod
2741 @classmethod
2742 def get_repo_followers(cls, repo_id):
2742 def get_repo_followers(cls, repo_id):
2743 return cls.query().filter(cls.follows_repo_id == repo_id)
2743 return cls.query().filter(cls.follows_repo_id == repo_id)
2744
2744
2745
2745
2746 class CacheKey(Base, BaseModel):
2746 class CacheKey(Base, BaseModel):
2747 __tablename__ = 'cache_invalidation'
2747 __tablename__ = 'cache_invalidation'
2748 __table_args__ = (
2748 __table_args__ = (
2749 UniqueConstraint('cache_key'),
2749 UniqueConstraint('cache_key'),
2750 Index('key_idx', 'cache_key'),
2750 Index('key_idx', 'cache_key'),
2751 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2751 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2752 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2752 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2753 )
2753 )
2754 CACHE_TYPE_ATOM = 'ATOM'
2754 CACHE_TYPE_ATOM = 'ATOM'
2755 CACHE_TYPE_RSS = 'RSS'
2755 CACHE_TYPE_RSS = 'RSS'
2756 CACHE_TYPE_README = 'README'
2756 CACHE_TYPE_README = 'README'
2757
2757
2758 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2758 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2759 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2759 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2760 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2760 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2761 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2761 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2762
2762
2763 def __init__(self, cache_key, cache_args=''):
2763 def __init__(self, cache_key, cache_args=''):
2764 self.cache_key = cache_key
2764 self.cache_key = cache_key
2765 self.cache_args = cache_args
2765 self.cache_args = cache_args
2766 self.cache_active = False
2766 self.cache_active = False
2767
2767
2768 def __unicode__(self):
2768 def __unicode__(self):
2769 return u"<%s('%s:%s[%s]')>" % (
2769 return u"<%s('%s:%s[%s]')>" % (
2770 self.__class__.__name__,
2770 self.__class__.__name__,
2771 self.cache_id, self.cache_key, self.cache_active)
2771 self.cache_id, self.cache_key, self.cache_active)
2772
2772
2773 def _cache_key_partition(self):
2773 def _cache_key_partition(self):
2774 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2774 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2775 return prefix, repo_name, suffix
2775 return prefix, repo_name, suffix
2776
2776
2777 def get_prefix(self):
2777 def get_prefix(self):
2778 """
2778 """
2779 Try to extract prefix from existing cache key. The key could consist
2779 Try to extract prefix from existing cache key. The key could consist
2780 of prefix, repo_name, suffix
2780 of prefix, repo_name, suffix
2781 """
2781 """
2782 # this returns prefix, repo_name, suffix
2782 # this returns prefix, repo_name, suffix
2783 return self._cache_key_partition()[0]
2783 return self._cache_key_partition()[0]
2784
2784
2785 def get_suffix(self):
2785 def get_suffix(self):
2786 """
2786 """
2787 get suffix that might have been used in _get_cache_key to
2787 get suffix that might have been used in _get_cache_key to
2788 generate self.cache_key. Only used for informational purposes
2788 generate self.cache_key. Only used for informational purposes
2789 in repo_edit.html.
2789 in repo_edit.html.
2790 """
2790 """
2791 # prefix, repo_name, suffix
2791 # prefix, repo_name, suffix
2792 return self._cache_key_partition()[2]
2792 return self._cache_key_partition()[2]
2793
2793
2794 @classmethod
2794 @classmethod
2795 def delete_all_cache(cls):
2795 def delete_all_cache(cls):
2796 """
2796 """
2797 Delete all cache keys from database.
2797 Delete all cache keys from database.
2798 Should only be run when all instances are down and all entries
2798 Should only be run when all instances are down and all entries
2799 thus stale.
2799 thus stale.
2800 """
2800 """
2801 cls.query().delete()
2801 cls.query().delete()
2802 Session().commit()
2802 Session().commit()
2803
2803
2804 @classmethod
2804 @classmethod
2805 def get_cache_key(cls, repo_name, cache_type):
2805 def get_cache_key(cls, repo_name, cache_type):
2806 """
2806 """
2807
2807
2808 Generate a cache key for this process of RhodeCode instance.
2808 Generate a cache key for this process of RhodeCode instance.
2809 Prefix most likely will be process id or maybe explicitly set
2809 Prefix most likely will be process id or maybe explicitly set
2810 instance_id from .ini file.
2810 instance_id from .ini file.
2811 """
2811 """
2812 import rhodecode
2812 import rhodecode
2813 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2813 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2814
2814
2815 repo_as_unicode = safe_unicode(repo_name)
2815 repo_as_unicode = safe_unicode(repo_name)
2816 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2816 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2817 if cache_type else repo_as_unicode
2817 if cache_type else repo_as_unicode
2818
2818
2819 return u'{}{}'.format(prefix, key)
2819 return u'{}{}'.format(prefix, key)
2820
2820
2821 @classmethod
2821 @classmethod
2822 def set_invalidate(cls, repo_name, delete=False):
2822 def set_invalidate(cls, repo_name, delete=False):
2823 """
2823 """
2824 Mark all caches of a repo as invalid in the database.
2824 Mark all caches of a repo as invalid in the database.
2825 """
2825 """
2826
2826
2827 try:
2827 try:
2828 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2828 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2829 if delete:
2829 if delete:
2830 log.debug('cache objects deleted for repo %s',
2830 log.debug('cache objects deleted for repo %s',
2831 safe_str(repo_name))
2831 safe_str(repo_name))
2832 qry.delete()
2832 qry.delete()
2833 else:
2833 else:
2834 log.debug('cache objects marked as invalid for repo %s',
2834 log.debug('cache objects marked as invalid for repo %s',
2835 safe_str(repo_name))
2835 safe_str(repo_name))
2836 qry.update({"cache_active": False})
2836 qry.update({"cache_active": False})
2837
2837
2838 Session().commit()
2838 Session().commit()
2839 except Exception:
2839 except Exception:
2840 log.exception(
2840 log.exception(
2841 'Cache key invalidation failed for repository %s',
2841 'Cache key invalidation failed for repository %s',
2842 safe_str(repo_name))
2842 safe_str(repo_name))
2843 Session().rollback()
2843 Session().rollback()
2844
2844
2845 @classmethod
2845 @classmethod
2846 def get_active_cache(cls, cache_key):
2846 def get_active_cache(cls, cache_key):
2847 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2847 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2848 if inv_obj:
2848 if inv_obj:
2849 return inv_obj
2849 return inv_obj
2850 return None
2850 return None
2851
2851
2852 @classmethod
2852 @classmethod
2853 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2853 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2854 thread_scoped=False):
2854 thread_scoped=False):
2855 """
2855 """
2856 @cache_region('long_term')
2856 @cache_region('long_term')
2857 def _heavy_calculation(cache_key):
2857 def _heavy_calculation(cache_key):
2858 return 'result'
2858 return 'result'
2859
2859
2860 cache_context = CacheKey.repo_context_cache(
2860 cache_context = CacheKey.repo_context_cache(
2861 _heavy_calculation, repo_name, cache_type)
2861 _heavy_calculation, repo_name, cache_type)
2862
2862
2863 with cache_context as context:
2863 with cache_context as context:
2864 context.invalidate()
2864 context.invalidate()
2865 computed = context.compute()
2865 computed = context.compute()
2866
2866
2867 assert computed == 'result'
2867 assert computed == 'result'
2868 """
2868 """
2869 from rhodecode.lib import caches
2869 from rhodecode.lib import caches
2870 return caches.InvalidationContext(
2870 return caches.InvalidationContext(
2871 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2871 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2872
2872
2873
2873
2874 class ChangesetComment(Base, BaseModel):
2874 class ChangesetComment(Base, BaseModel):
2875 __tablename__ = 'changeset_comments'
2875 __tablename__ = 'changeset_comments'
2876 __table_args__ = (
2876 __table_args__ = (
2877 Index('cc_revision_idx', 'revision'),
2877 Index('cc_revision_idx', 'revision'),
2878 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2878 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2879 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2879 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2880 )
2880 )
2881
2881
2882 COMMENT_OUTDATED = u'comment_outdated'
2882 COMMENT_OUTDATED = u'comment_outdated'
2883
2883
2884 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2884 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2885 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2885 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2886 revision = Column('revision', String(40), nullable=True)
2886 revision = Column('revision', String(40), nullable=True)
2887 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2887 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2888 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2888 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2889 line_no = Column('line_no', Unicode(10), nullable=True)
2889 line_no = Column('line_no', Unicode(10), nullable=True)
2890 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2890 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2891 f_path = Column('f_path', Unicode(1000), nullable=True)
2891 f_path = Column('f_path', Unicode(1000), nullable=True)
2892 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2892 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2893 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2893 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2894 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2894 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2895 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2895 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2896 renderer = Column('renderer', Unicode(64), nullable=True)
2896 renderer = Column('renderer', Unicode(64), nullable=True)
2897 display_state = Column('display_state', Unicode(128), nullable=True)
2897 display_state = Column('display_state', Unicode(128), nullable=True)
2898
2898
2899 author = relationship('User', lazy='joined')
2899 author = relationship('User', lazy='joined')
2900 repo = relationship('Repository')
2900 repo = relationship('Repository')
2901 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2901 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2902 pull_request = relationship('PullRequest', lazy='joined')
2902 pull_request = relationship('PullRequest', lazy='joined')
2903 pull_request_version = relationship('PullRequestVersion')
2903 pull_request_version = relationship('PullRequestVersion')
2904
2904
2905 @classmethod
2905 @classmethod
2906 def get_users(cls, revision=None, pull_request_id=None):
2906 def get_users(cls, revision=None, pull_request_id=None):
2907 """
2907 """
2908 Returns user associated with this ChangesetComment. ie those
2908 Returns user associated with this ChangesetComment. ie those
2909 who actually commented
2909 who actually commented
2910
2910
2911 :param cls:
2911 :param cls:
2912 :param revision:
2912 :param revision:
2913 """
2913 """
2914 q = Session().query(User)\
2914 q = Session().query(User)\
2915 .join(ChangesetComment.author)
2915 .join(ChangesetComment.author)
2916 if revision:
2916 if revision:
2917 q = q.filter(cls.revision == revision)
2917 q = q.filter(cls.revision == revision)
2918 elif pull_request_id:
2918 elif pull_request_id:
2919 q = q.filter(cls.pull_request_id == pull_request_id)
2919 q = q.filter(cls.pull_request_id == pull_request_id)
2920 return q.all()
2920 return q.all()
2921
2921
2922 def render(self, mentions=False):
2922 def render(self, mentions=False):
2923 from rhodecode.lib import helpers as h
2923 from rhodecode.lib import helpers as h
2924 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2924 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2925
2925
2926 def __repr__(self):
2926 def __repr__(self):
2927 if self.comment_id:
2927 if self.comment_id:
2928 return '<DB:ChangesetComment #%s>' % self.comment_id
2928 return '<DB:ChangesetComment #%s>' % self.comment_id
2929 else:
2929 else:
2930 return '<DB:ChangesetComment at %#x>' % id(self)
2930 return '<DB:ChangesetComment at %#x>' % id(self)
2931
2931
2932
2932
2933 class ChangesetStatus(Base, BaseModel):
2933 class ChangesetStatus(Base, BaseModel):
2934 __tablename__ = 'changeset_statuses'
2934 __tablename__ = 'changeset_statuses'
2935 __table_args__ = (
2935 __table_args__ = (
2936 Index('cs_revision_idx', 'revision'),
2936 Index('cs_revision_idx', 'revision'),
2937 Index('cs_version_idx', 'version'),
2937 Index('cs_version_idx', 'version'),
2938 UniqueConstraint('repo_id', 'revision', 'version'),
2938 UniqueConstraint('repo_id', 'revision', 'version'),
2939 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2939 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2940 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2940 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2941 )
2941 )
2942 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2942 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2943 STATUS_APPROVED = 'approved'
2943 STATUS_APPROVED = 'approved'
2944 STATUS_REJECTED = 'rejected'
2944 STATUS_REJECTED = 'rejected'
2945 STATUS_UNDER_REVIEW = 'under_review'
2945 STATUS_UNDER_REVIEW = 'under_review'
2946
2946
2947 STATUSES = [
2947 STATUSES = [
2948 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2948 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2949 (STATUS_APPROVED, _("Approved")),
2949 (STATUS_APPROVED, _("Approved")),
2950 (STATUS_REJECTED, _("Rejected")),
2950 (STATUS_REJECTED, _("Rejected")),
2951 (STATUS_UNDER_REVIEW, _("Under Review")),
2951 (STATUS_UNDER_REVIEW, _("Under Review")),
2952 ]
2952 ]
2953
2953
2954 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2954 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2955 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2955 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2956 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2956 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2957 revision = Column('revision', String(40), nullable=False)
2957 revision = Column('revision', String(40), nullable=False)
2958 status = Column('status', String(128), nullable=False, default=DEFAULT)
2958 status = Column('status', String(128), nullable=False, default=DEFAULT)
2959 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2959 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2960 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2960 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2961 version = Column('version', Integer(), nullable=False, default=0)
2961 version = Column('version', Integer(), nullable=False, default=0)
2962 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2962 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2963
2963
2964 author = relationship('User', lazy='joined')
2964 author = relationship('User', lazy='joined')
2965 repo = relationship('Repository')
2965 repo = relationship('Repository')
2966 comment = relationship('ChangesetComment', lazy='joined')
2966 comment = relationship('ChangesetComment', lazy='joined')
2967 pull_request = relationship('PullRequest', lazy='joined')
2967 pull_request = relationship('PullRequest', lazy='joined')
2968
2968
2969 def __unicode__(self):
2969 def __unicode__(self):
2970 return u"<%s('%s[%s]:%s')>" % (
2970 return u"<%s('%s[%s]:%s')>" % (
2971 self.__class__.__name__,
2971 self.__class__.__name__,
2972 self.status, self.version, self.author
2972 self.status, self.version, self.author
2973 )
2973 )
2974
2974
2975 @classmethod
2975 @classmethod
2976 def get_status_lbl(cls, value):
2976 def get_status_lbl(cls, value):
2977 return dict(cls.STATUSES).get(value)
2977 return dict(cls.STATUSES).get(value)
2978
2978
2979 @property
2979 @property
2980 def status_lbl(self):
2980 def status_lbl(self):
2981 return ChangesetStatus.get_status_lbl(self.status)
2981 return ChangesetStatus.get_status_lbl(self.status)
2982
2982
2983
2983
2984 class _PullRequestBase(BaseModel):
2984 class _PullRequestBase(BaseModel):
2985 """
2985 """
2986 Common attributes of pull request and version entries.
2986 Common attributes of pull request and version entries.
2987 """
2987 """
2988
2988
2989 # .status values
2989 # .status values
2990 STATUS_NEW = u'new'
2990 STATUS_NEW = u'new'
2991 STATUS_OPEN = u'open'
2991 STATUS_OPEN = u'open'
2992 STATUS_CLOSED = u'closed'
2992 STATUS_CLOSED = u'closed'
2993
2993
2994 title = Column('title', Unicode(255), nullable=True)
2994 title = Column('title', Unicode(255), nullable=True)
2995 description = Column(
2995 description = Column(
2996 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2996 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2997 nullable=True)
2997 nullable=True)
2998 # new/open/closed status of pull request (not approve/reject/etc)
2998 # new/open/closed status of pull request (not approve/reject/etc)
2999 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
2999 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3000 created_on = Column(
3000 created_on = Column(
3001 'created_on', DateTime(timezone=False), nullable=False,
3001 'created_on', DateTime(timezone=False), nullable=False,
3002 default=datetime.datetime.now)
3002 default=datetime.datetime.now)
3003 updated_on = Column(
3003 updated_on = Column(
3004 'updated_on', DateTime(timezone=False), nullable=False,
3004 'updated_on', DateTime(timezone=False), nullable=False,
3005 default=datetime.datetime.now)
3005 default=datetime.datetime.now)
3006
3006
3007 @declared_attr
3007 @declared_attr
3008 def user_id(cls):
3008 def user_id(cls):
3009 return Column(
3009 return Column(
3010 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3010 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3011 unique=None)
3011 unique=None)
3012
3012
3013 # 500 revisions max
3013 # 500 revisions max
3014 _revisions = Column(
3014 _revisions = Column(
3015 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3015 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3016
3016
3017 @declared_attr
3017 @declared_attr
3018 def source_repo_id(cls):
3018 def source_repo_id(cls):
3019 # TODO: dan: rename column to source_repo_id
3019 # TODO: dan: rename column to source_repo_id
3020 return Column(
3020 return Column(
3021 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3021 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3022 nullable=False)
3022 nullable=False)
3023
3023
3024 source_ref = Column('org_ref', Unicode(255), nullable=False)
3024 source_ref = Column('org_ref', Unicode(255), nullable=False)
3025
3025
3026 @declared_attr
3026 @declared_attr
3027 def target_repo_id(cls):
3027 def target_repo_id(cls):
3028 # TODO: dan: rename column to target_repo_id
3028 # TODO: dan: rename column to target_repo_id
3029 return Column(
3029 return Column(
3030 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3030 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3031 nullable=False)
3031 nullable=False)
3032
3032
3033 target_ref = Column('other_ref', Unicode(255), nullable=False)
3033 target_ref = Column('other_ref', Unicode(255), nullable=False)
3034
3034
3035 # TODO: dan: rename column to last_merge_source_rev
3035 # TODO: dan: rename column to last_merge_source_rev
3036 _last_merge_source_rev = Column(
3036 _last_merge_source_rev = Column(
3037 'last_merge_org_rev', String(40), nullable=True)
3037 'last_merge_org_rev', String(40), nullable=True)
3038 # TODO: dan: rename column to last_merge_target_rev
3038 # TODO: dan: rename column to last_merge_target_rev
3039 _last_merge_target_rev = Column(
3039 _last_merge_target_rev = Column(
3040 'last_merge_other_rev', String(40), nullable=True)
3040 'last_merge_other_rev', String(40), nullable=True)
3041 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3041 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3042 merge_rev = Column('merge_rev', String(40), nullable=True)
3042 merge_rev = Column('merge_rev', String(40), nullable=True)
3043
3043
3044 @hybrid_property
3044 @hybrid_property
3045 def revisions(self):
3045 def revisions(self):
3046 return self._revisions.split(':') if self._revisions else []
3046 return self._revisions.split(':') if self._revisions else []
3047
3047
3048 @revisions.setter
3048 @revisions.setter
3049 def revisions(self, val):
3049 def revisions(self, val):
3050 self._revisions = ':'.join(val)
3050 self._revisions = ':'.join(val)
3051
3051
3052 @declared_attr
3052 @declared_attr
3053 def author(cls):
3053 def author(cls):
3054 return relationship('User', lazy='joined')
3054 return relationship('User', lazy='joined')
3055
3055
3056 @declared_attr
3056 @declared_attr
3057 def source_repo(cls):
3057 def source_repo(cls):
3058 return relationship(
3058 return relationship(
3059 'Repository',
3059 'Repository',
3060 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3060 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3061
3061
3062 @property
3062 @property
3063 def source_ref_parts(self):
3063 def source_ref_parts(self):
3064 refs = self.source_ref.split(':')
3064 refs = self.source_ref.split(':')
3065 return Reference(refs[0], refs[1], refs[2])
3065 return Reference(refs[0], refs[1], refs[2])
3066
3066
3067 @declared_attr
3067 @declared_attr
3068 def target_repo(cls):
3068 def target_repo(cls):
3069 return relationship(
3069 return relationship(
3070 'Repository',
3070 'Repository',
3071 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3071 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3072
3072
3073 @property
3073 @property
3074 def target_ref_parts(self):
3074 def target_ref_parts(self):
3075 refs = self.target_ref.split(':')
3075 refs = self.target_ref.split(':')
3076 return Reference(refs[0], refs[1], refs[2])
3076 return Reference(refs[0], refs[1], refs[2])
3077
3077
3078
3078
3079 class PullRequest(Base, _PullRequestBase):
3079 class PullRequest(Base, _PullRequestBase):
3080 __tablename__ = 'pull_requests'
3080 __tablename__ = 'pull_requests'
3081 __table_args__ = (
3081 __table_args__ = (
3082 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3082 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3083 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3083 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3084 )
3084 )
3085
3085
3086 pull_request_id = Column(
3086 pull_request_id = Column(
3087 'pull_request_id', Integer(), nullable=False, primary_key=True)
3087 'pull_request_id', Integer(), nullable=False, primary_key=True)
3088
3088
3089 def __repr__(self):
3089 def __repr__(self):
3090 if self.pull_request_id:
3090 if self.pull_request_id:
3091 return '<DB:PullRequest #%s>' % self.pull_request_id
3091 return '<DB:PullRequest #%s>' % self.pull_request_id
3092 else:
3092 else:
3093 return '<DB:PullRequest at %#x>' % id(self)
3093 return '<DB:PullRequest at %#x>' % id(self)
3094
3094
3095 reviewers = relationship('PullRequestReviewers',
3095 reviewers = relationship('PullRequestReviewers',
3096 cascade="all, delete, delete-orphan")
3096 cascade="all, delete, delete-orphan")
3097 statuses = relationship('ChangesetStatus')
3097 statuses = relationship('ChangesetStatus')
3098 comments = relationship('ChangesetComment',
3098 comments = relationship('ChangesetComment',
3099 cascade="all, delete, delete-orphan")
3099 cascade="all, delete, delete-orphan")
3100 versions = relationship('PullRequestVersion',
3100 versions = relationship('PullRequestVersion',
3101 cascade="all, delete, delete-orphan")
3101 cascade="all, delete, delete-orphan")
3102
3102
3103 def is_closed(self):
3103 def is_closed(self):
3104 return self.status == self.STATUS_CLOSED
3104 return self.status == self.STATUS_CLOSED
3105
3105
3106 def get_api_data(self):
3106 def get_api_data(self):
3107 from rhodecode.model.pull_request import PullRequestModel
3107 from rhodecode.model.pull_request import PullRequestModel
3108 pull_request = self
3108 pull_request = self
3109 merge_status = PullRequestModel().merge_status(pull_request)
3109 merge_status = PullRequestModel().merge_status(pull_request)
3110 data = {
3110 data = {
3111 'pull_request_id': pull_request.pull_request_id,
3111 'pull_request_id': pull_request.pull_request_id,
3112 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name,
3112 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name,
3113 pull_request_id=self.pull_request_id,
3113 pull_request_id=self.pull_request_id,
3114 qualified=True),
3114 qualified=True),
3115 'title': pull_request.title,
3115 'title': pull_request.title,
3116 'description': pull_request.description,
3116 'description': pull_request.description,
3117 'status': pull_request.status,
3117 'status': pull_request.status,
3118 'created_on': pull_request.created_on,
3118 'created_on': pull_request.created_on,
3119 'updated_on': pull_request.updated_on,
3119 'updated_on': pull_request.updated_on,
3120 'commit_ids': pull_request.revisions,
3120 'commit_ids': pull_request.revisions,
3121 'review_status': pull_request.calculated_review_status(),
3121 'review_status': pull_request.calculated_review_status(),
3122 'mergeable': {
3122 'mergeable': {
3123 'status': merge_status[0],
3123 'status': merge_status[0],
3124 'message': unicode(merge_status[1]),
3124 'message': unicode(merge_status[1]),
3125 },
3125 },
3126 'source': {
3126 'source': {
3127 'clone_url': pull_request.source_repo.clone_url(),
3127 'clone_url': pull_request.source_repo.clone_url(),
3128 'repository': pull_request.source_repo.repo_name,
3128 'repository': pull_request.source_repo.repo_name,
3129 'reference': {
3129 'reference': {
3130 'name': pull_request.source_ref_parts.name,
3130 'name': pull_request.source_ref_parts.name,
3131 'type': pull_request.source_ref_parts.type,
3131 'type': pull_request.source_ref_parts.type,
3132 'commit_id': pull_request.source_ref_parts.commit_id,
3132 'commit_id': pull_request.source_ref_parts.commit_id,
3133 },
3133 },
3134 },
3134 },
3135 'target': {
3135 'target': {
3136 'clone_url': pull_request.target_repo.clone_url(),
3136 'clone_url': pull_request.target_repo.clone_url(),
3137 'repository': pull_request.target_repo.repo_name,
3137 'repository': pull_request.target_repo.repo_name,
3138 'reference': {
3138 'reference': {
3139 'name': pull_request.target_ref_parts.name,
3139 'name': pull_request.target_ref_parts.name,
3140 'type': pull_request.target_ref_parts.type,
3140 'type': pull_request.target_ref_parts.type,
3141 'commit_id': pull_request.target_ref_parts.commit_id,
3141 'commit_id': pull_request.target_ref_parts.commit_id,
3142 },
3142 },
3143 },
3143 },
3144 'author': pull_request.author.get_api_data(include_secrets=False,
3144 'author': pull_request.author.get_api_data(include_secrets=False,
3145 details='basic'),
3145 details='basic'),
3146 'reviewers': [
3146 'reviewers': [
3147 {
3147 {
3148 'user': reviewer.get_api_data(include_secrets=False,
3148 'user': reviewer.get_api_data(include_secrets=False,
3149 details='basic'),
3149 details='basic'),
3150 'review_status': st[0][1].status if st else 'not_reviewed',
3150 'review_status': st[0][1].status if st else 'not_reviewed',
3151 }
3151 }
3152 for reviewer, st in pull_request.reviewers_statuses()
3152 for reviewer, st in pull_request.reviewers_statuses()
3153 ]
3153 ]
3154 }
3154 }
3155
3155
3156 return data
3156 return data
3157
3157
3158 def __json__(self):
3158 def __json__(self):
3159 return {
3159 return {
3160 'revisions': self.revisions,
3160 'revisions': self.revisions,
3161 }
3161 }
3162
3162
3163 def calculated_review_status(self):
3163 def calculated_review_status(self):
3164 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3164 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3165 # because it's tricky on how to use ChangesetStatusModel from there
3165 # because it's tricky on how to use ChangesetStatusModel from there
3166 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3166 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3167 from rhodecode.model.changeset_status import ChangesetStatusModel
3167 from rhodecode.model.changeset_status import ChangesetStatusModel
3168 return ChangesetStatusModel().calculated_review_status(self)
3168 return ChangesetStatusModel().calculated_review_status(self)
3169
3169
3170 def reviewers_statuses(self):
3170 def reviewers_statuses(self):
3171 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3171 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3172 from rhodecode.model.changeset_status import ChangesetStatusModel
3172 from rhodecode.model.changeset_status import ChangesetStatusModel
3173 return ChangesetStatusModel().reviewers_statuses(self)
3173 return ChangesetStatusModel().reviewers_statuses(self)
3174
3174
3175
3175
3176 class PullRequestVersion(Base, _PullRequestBase):
3176 class PullRequestVersion(Base, _PullRequestBase):
3177 __tablename__ = 'pull_request_versions'
3177 __tablename__ = 'pull_request_versions'
3178 __table_args__ = (
3178 __table_args__ = (
3179 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3179 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3180 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3180 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3181 )
3181 )
3182
3182
3183 pull_request_version_id = Column(
3183 pull_request_version_id = Column(
3184 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3184 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3185 pull_request_id = Column(
3185 pull_request_id = Column(
3186 'pull_request_id', Integer(),
3186 'pull_request_id', Integer(),
3187 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3187 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3188 pull_request = relationship('PullRequest')
3188 pull_request = relationship('PullRequest')
3189
3189
3190 def __repr__(self):
3190 def __repr__(self):
3191 if self.pull_request_version_id:
3191 if self.pull_request_version_id:
3192 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3192 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3193 else:
3193 else:
3194 return '<DB:PullRequestVersion at %#x>' % id(self)
3194 return '<DB:PullRequestVersion at %#x>' % id(self)
3195
3195
3196
3196
3197 class PullRequestReviewers(Base, BaseModel):
3197 class PullRequestReviewers(Base, BaseModel):
3198 __tablename__ = 'pull_request_reviewers'
3198 __tablename__ = 'pull_request_reviewers'
3199 __table_args__ = (
3199 __table_args__ = (
3200 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3200 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3201 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3201 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3202 )
3202 )
3203
3203
3204 def __init__(self, user=None, pull_request=None):
3204 def __init__(self, user=None, pull_request=None):
3205 self.user = user
3205 self.user = user
3206 self.pull_request = pull_request
3206 self.pull_request = pull_request
3207
3207
3208 pull_requests_reviewers_id = Column(
3208 pull_requests_reviewers_id = Column(
3209 'pull_requests_reviewers_id', Integer(), nullable=False,
3209 'pull_requests_reviewers_id', Integer(), nullable=False,
3210 primary_key=True)
3210 primary_key=True)
3211 pull_request_id = Column(
3211 pull_request_id = Column(
3212 "pull_request_id", Integer(),
3212 "pull_request_id", Integer(),
3213 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3213 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3214 user_id = Column(
3214 user_id = Column(
3215 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3215 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3216
3216
3217 user = relationship('User')
3217 user = relationship('User')
3218 pull_request = relationship('PullRequest')
3218 pull_request = relationship('PullRequest')
3219
3219
3220
3220
3221 class Notification(Base, BaseModel):
3221 class Notification(Base, BaseModel):
3222 __tablename__ = 'notifications'
3222 __tablename__ = 'notifications'
3223 __table_args__ = (
3223 __table_args__ = (
3224 Index('notification_type_idx', 'type'),
3224 Index('notification_type_idx', 'type'),
3225 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3225 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3226 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3226 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3227 )
3227 )
3228
3228
3229 TYPE_CHANGESET_COMMENT = u'cs_comment'
3229 TYPE_CHANGESET_COMMENT = u'cs_comment'
3230 TYPE_MESSAGE = u'message'
3230 TYPE_MESSAGE = u'message'
3231 TYPE_MENTION = u'mention'
3231 TYPE_MENTION = u'mention'
3232 TYPE_REGISTRATION = u'registration'
3232 TYPE_REGISTRATION = u'registration'
3233 TYPE_PULL_REQUEST = u'pull_request'
3233 TYPE_PULL_REQUEST = u'pull_request'
3234 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3234 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3235
3235
3236 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3236 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3237 subject = Column('subject', Unicode(512), nullable=True)
3237 subject = Column('subject', Unicode(512), nullable=True)
3238 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3238 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3239 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3239 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3240 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3240 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3241 type_ = Column('type', Unicode(255))
3241 type_ = Column('type', Unicode(255))
3242
3242
3243 created_by_user = relationship('User')
3243 created_by_user = relationship('User')
3244 notifications_to_users = relationship('UserNotification', lazy='joined',
3244 notifications_to_users = relationship('UserNotification', lazy='joined',
3245 cascade="all, delete, delete-orphan")
3245 cascade="all, delete, delete-orphan")
3246
3246
3247 @property
3247 @property
3248 def recipients(self):
3248 def recipients(self):
3249 return [x.user for x in UserNotification.query()\
3249 return [x.user for x in UserNotification.query()\
3250 .filter(UserNotification.notification == self)\
3250 .filter(UserNotification.notification == self)\
3251 .order_by(UserNotification.user_id.asc()).all()]
3251 .order_by(UserNotification.user_id.asc()).all()]
3252
3252
3253 @classmethod
3253 @classmethod
3254 def create(cls, created_by, subject, body, recipients, type_=None):
3254 def create(cls, created_by, subject, body, recipients, type_=None):
3255 if type_ is None:
3255 if type_ is None:
3256 type_ = Notification.TYPE_MESSAGE
3256 type_ = Notification.TYPE_MESSAGE
3257
3257
3258 notification = cls()
3258 notification = cls()
3259 notification.created_by_user = created_by
3259 notification.created_by_user = created_by
3260 notification.subject = subject
3260 notification.subject = subject
3261 notification.body = body
3261 notification.body = body
3262 notification.type_ = type_
3262 notification.type_ = type_
3263 notification.created_on = datetime.datetime.now()
3263 notification.created_on = datetime.datetime.now()
3264
3264
3265 for u in recipients:
3265 for u in recipients:
3266 assoc = UserNotification()
3266 assoc = UserNotification()
3267 assoc.notification = notification
3267 assoc.notification = notification
3268
3268
3269 # if created_by is inside recipients mark his notification
3269 # if created_by is inside recipients mark his notification
3270 # as read
3270 # as read
3271 if u.user_id == created_by.user_id:
3271 if u.user_id == created_by.user_id:
3272 assoc.read = True
3272 assoc.read = True
3273
3273
3274 u.notifications.append(assoc)
3274 u.notifications.append(assoc)
3275 Session().add(notification)
3275 Session().add(notification)
3276
3276
3277 return notification
3277 return notification
3278
3278
3279 @property
3279 @property
3280 def description(self):
3280 def description(self):
3281 from rhodecode.model.notification import NotificationModel
3281 from rhodecode.model.notification import NotificationModel
3282 return NotificationModel().make_description(self)
3282 return NotificationModel().make_description(self)
3283
3283
3284
3284
3285 class UserNotification(Base, BaseModel):
3285 class UserNotification(Base, BaseModel):
3286 __tablename__ = 'user_to_notification'
3286 __tablename__ = 'user_to_notification'
3287 __table_args__ = (
3287 __table_args__ = (
3288 UniqueConstraint('user_id', 'notification_id'),
3288 UniqueConstraint('user_id', 'notification_id'),
3289 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3289 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3290 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3290 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3291 )
3291 )
3292 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3292 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3293 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3293 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3294 read = Column('read', Boolean, default=False)
3294 read = Column('read', Boolean, default=False)
3295 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3295 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3296
3296
3297 user = relationship('User', lazy="joined")
3297 user = relationship('User', lazy="joined")
3298 notification = relationship('Notification', lazy="joined",
3298 notification = relationship('Notification', lazy="joined",
3299 order_by=lambda: Notification.created_on.desc(),)
3299 order_by=lambda: Notification.created_on.desc(),)
3300
3300
3301 def mark_as_read(self):
3301 def mark_as_read(self):
3302 self.read = True
3302 self.read = True
3303 Session().add(self)
3303 Session().add(self)
3304
3304
3305
3305
3306 class Gist(Base, BaseModel):
3306 class Gist(Base, BaseModel):
3307 __tablename__ = 'gists'
3307 __tablename__ = 'gists'
3308 __table_args__ = (
3308 __table_args__ = (
3309 Index('g_gist_access_id_idx', 'gist_access_id'),
3309 Index('g_gist_access_id_idx', 'gist_access_id'),
3310 Index('g_created_on_idx', 'created_on'),
3310 Index('g_created_on_idx', 'created_on'),
3311 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3311 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3312 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3312 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3313 )
3313 )
3314 GIST_PUBLIC = u'public'
3314 GIST_PUBLIC = u'public'
3315 GIST_PRIVATE = u'private'
3315 GIST_PRIVATE = u'private'
3316 DEFAULT_FILENAME = u'gistfile1.txt'
3316 DEFAULT_FILENAME = u'gistfile1.txt'
3317
3317
3318 ACL_LEVEL_PUBLIC = u'acl_public'
3318 ACL_LEVEL_PUBLIC = u'acl_public'
3319 ACL_LEVEL_PRIVATE = u'acl_private'
3319 ACL_LEVEL_PRIVATE = u'acl_private'
3320
3320
3321 gist_id = Column('gist_id', Integer(), primary_key=True)
3321 gist_id = Column('gist_id', Integer(), primary_key=True)
3322 gist_access_id = Column('gist_access_id', Unicode(250))
3322 gist_access_id = Column('gist_access_id', Unicode(250))
3323 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3323 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3324 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3324 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3325 gist_expires = Column('gist_expires', Float(53), nullable=False)
3325 gist_expires = Column('gist_expires', Float(53), nullable=False)
3326 gist_type = Column('gist_type', Unicode(128), nullable=False)
3326 gist_type = Column('gist_type', Unicode(128), nullable=False)
3327 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3327 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3328 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3328 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3329 acl_level = Column('acl_level', Unicode(128), nullable=True)
3329 acl_level = Column('acl_level', Unicode(128), nullable=True)
3330
3330
3331 owner = relationship('User')
3331 owner = relationship('User')
3332
3332
3333 def __repr__(self):
3333 def __repr__(self):
3334 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3334 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3335
3335
3336 @classmethod
3336 @classmethod
3337 def get_or_404(cls, id_):
3337 def get_or_404(cls, id_):
3338 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3338 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3339 if not res:
3339 if not res:
3340 raise HTTPNotFound
3340 raise HTTPNotFound
3341 return res
3341 return res
3342
3342
3343 @classmethod
3343 @classmethod
3344 def get_by_access_id(cls, gist_access_id):
3344 def get_by_access_id(cls, gist_access_id):
3345 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3345 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3346
3346
3347 def gist_url(self):
3347 def gist_url(self):
3348 import rhodecode
3348 import rhodecode
3349 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3349 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3350 if alias_url:
3350 if alias_url:
3351 return alias_url.replace('{gistid}', self.gist_access_id)
3351 return alias_url.replace('{gistid}', self.gist_access_id)
3352
3352
3353 return url('gist', gist_id=self.gist_access_id, qualified=True)
3353 return url('gist', gist_id=self.gist_access_id, qualified=True)
3354
3354
3355 @classmethod
3355 @classmethod
3356 def base_path(cls):
3356 def base_path(cls):
3357 """
3357 """
3358 Returns base path when all gists are stored
3358 Returns base path when all gists are stored
3359
3359
3360 :param cls:
3360 :param cls:
3361 """
3361 """
3362 from rhodecode.model.gist import GIST_STORE_LOC
3362 from rhodecode.model.gist import GIST_STORE_LOC
3363 q = Session().query(RhodeCodeUi)\
3363 q = Session().query(RhodeCodeUi)\
3364 .filter(RhodeCodeUi.ui_key == URL_SEP)
3364 .filter(RhodeCodeUi.ui_key == URL_SEP)
3365 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3365 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3366 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3366 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3367
3367
3368 def get_api_data(self):
3368 def get_api_data(self):
3369 """
3369 """
3370 Common function for generating gist related data for API
3370 Common function for generating gist related data for API
3371 """
3371 """
3372 gist = self
3372 gist = self
3373 data = {
3373 data = {
3374 'gist_id': gist.gist_id,
3374 'gist_id': gist.gist_id,
3375 'type': gist.gist_type,
3375 'type': gist.gist_type,
3376 'access_id': gist.gist_access_id,
3376 'access_id': gist.gist_access_id,
3377 'description': gist.gist_description,
3377 'description': gist.gist_description,
3378 'url': gist.gist_url(),
3378 'url': gist.gist_url(),
3379 'expires': gist.gist_expires,
3379 'expires': gist.gist_expires,
3380 'created_on': gist.created_on,
3380 'created_on': gist.created_on,
3381 'modified_at': gist.modified_at,
3381 'modified_at': gist.modified_at,
3382 'content': None,
3382 'content': None,
3383 'acl_level': gist.acl_level,
3383 'acl_level': gist.acl_level,
3384 }
3384 }
3385 return data
3385 return data
3386
3386
3387 def __json__(self):
3387 def __json__(self):
3388 data = dict(
3388 data = dict(
3389 )
3389 )
3390 data.update(self.get_api_data())
3390 data.update(self.get_api_data())
3391 return data
3391 return data
3392 # SCM functions
3392 # SCM functions
3393
3393
3394 def scm_instance(self, **kwargs):
3394 def scm_instance(self, **kwargs):
3395 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3395 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3396 return get_vcs_instance(
3396 return get_vcs_instance(
3397 repo_path=safe_str(full_repo_path), create=False)
3397 repo_path=safe_str(full_repo_path), create=False)
3398
3398
3399
3399
3400 class DbMigrateVersion(Base, BaseModel):
3400 class DbMigrateVersion(Base, BaseModel):
3401 __tablename__ = 'db_migrate_version'
3401 __tablename__ = 'db_migrate_version'
3402 __table_args__ = (
3402 __table_args__ = (
3403 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3403 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3404 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3404 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3405 )
3405 )
3406 repository_id = Column('repository_id', String(250), primary_key=True)
3406 repository_id = Column('repository_id', String(250), primary_key=True)
3407 repository_path = Column('repository_path', Text)
3407 repository_path = Column('repository_path', Text)
3408 version = Column('version', Integer)
3408 version = Column('version', Integer)
3409
3409
3410
3410
3411 class ExternalIdentity(Base, BaseModel):
3411 class ExternalIdentity(Base, BaseModel):
3412 __tablename__ = 'external_identities'
3412 __tablename__ = 'external_identities'
3413 __table_args__ = (
3413 __table_args__ = (
3414 Index('local_user_id_idx', 'local_user_id'),
3414 Index('local_user_id_idx', 'local_user_id'),
3415 Index('external_id_idx', 'external_id'),
3415 Index('external_id_idx', 'external_id'),
3416 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3416 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3417 'mysql_charset': 'utf8'})
3417 'mysql_charset': 'utf8'})
3418
3418
3419 external_id = Column('external_id', Unicode(255), default=u'',
3419 external_id = Column('external_id', Unicode(255), default=u'',
3420 primary_key=True)
3420 primary_key=True)
3421 external_username = Column('external_username', Unicode(1024), default=u'')
3421 external_username = Column('external_username', Unicode(1024), default=u'')
3422 local_user_id = Column('local_user_id', Integer(),
3422 local_user_id = Column('local_user_id', Integer(),
3423 ForeignKey('users.user_id'), primary_key=True)
3423 ForeignKey('users.user_id'), primary_key=True)
3424 provider_name = Column('provider_name', Unicode(255), default=u'',
3424 provider_name = Column('provider_name', Unicode(255), default=u'',
3425 primary_key=True)
3425 primary_key=True)
3426 access_token = Column('access_token', String(1024), default=u'')
3426 access_token = Column('access_token', String(1024), default=u'')
3427 alt_token = Column('alt_token', String(1024), default=u'')
3427 alt_token = Column('alt_token', String(1024), default=u'')
3428 token_secret = Column('token_secret', String(1024), default=u'')
3428 token_secret = Column('token_secret', String(1024), default=u'')
3429
3429
3430 @classmethod
3430 @classmethod
3431 def by_external_id_and_provider(cls, external_id, provider_name,
3431 def by_external_id_and_provider(cls, external_id, provider_name,
3432 local_user_id=None):
3432 local_user_id=None):
3433 """
3433 """
3434 Returns ExternalIdentity instance based on search params
3434 Returns ExternalIdentity instance based on search params
3435
3435
3436 :param external_id:
3436 :param external_id:
3437 :param provider_name:
3437 :param provider_name:
3438 :return: ExternalIdentity
3438 :return: ExternalIdentity
3439 """
3439 """
3440 query = cls.query()
3440 query = cls.query()
3441 query = query.filter(cls.external_id == external_id)
3441 query = query.filter(cls.external_id == external_id)
3442 query = query.filter(cls.provider_name == provider_name)
3442 query = query.filter(cls.provider_name == provider_name)
3443 if local_user_id:
3443 if local_user_id:
3444 query = query.filter(cls.local_user_id == local_user_id)
3444 query = query.filter(cls.local_user_id == local_user_id)
3445 return query.first()
3445 return query.first()
3446
3446
3447 @classmethod
3447 @classmethod
3448 def user_by_external_id_and_provider(cls, external_id, provider_name):
3448 def user_by_external_id_and_provider(cls, external_id, provider_name):
3449 """
3449 """
3450 Returns User instance based on search params
3450 Returns User instance based on search params
3451
3451
3452 :param external_id:
3452 :param external_id:
3453 :param provider_name:
3453 :param provider_name:
3454 :return: User
3454 :return: User
3455 """
3455 """
3456 query = User.query()
3456 query = User.query()
3457 query = query.filter(cls.external_id == external_id)
3457 query = query.filter(cls.external_id == external_id)
3458 query = query.filter(cls.provider_name == provider_name)
3458 query = query.filter(cls.provider_name == provider_name)
3459 query = query.filter(User.user_id == cls.local_user_id)
3459 query = query.filter(User.user_id == cls.local_user_id)
3460 return query.first()
3460 return query.first()
3461
3461
3462 @classmethod
3462 @classmethod
3463 def by_local_user_id(cls, local_user_id):
3463 def by_local_user_id(cls, local_user_id):
3464 """
3464 """
3465 Returns all tokens for user
3465 Returns all tokens for user
3466
3466
3467 :param local_user_id:
3467 :param local_user_id:
3468 :return: ExternalIdentity
3468 :return: ExternalIdentity
3469 """
3469 """
3470 query = cls.query()
3470 query = cls.query()
3471 query = query.filter(cls.local_user_id == local_user_id)
3471 query = query.filter(cls.local_user_id == local_user_id)
3472 return query
3472 return query
3473
3473
3474
3474
3475 class Integration(Base, BaseModel):
3475 class Integration(Base, BaseModel):
3476 __tablename__ = 'integrations'
3476 __tablename__ = 'integrations'
3477 __table_args__ = (
3477 __table_args__ = (
3478 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3478 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3479 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3479 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3480 )
3480 )
3481
3481
3482 integration_id = Column('integration_id', Integer(), primary_key=True)
3482 integration_id = Column('integration_id', Integer(), primary_key=True)
3483 integration_type = Column('integration_type', String(255))
3483 integration_type = Column('integration_type', String(255))
3484 enabled = Column('enabled', Boolean(), nullable=False)
3484 enabled = Column('enabled', Boolean(), nullable=False)
3485 name = Column('name', String(255), nullable=False)
3485 name = Column('name', String(255), nullable=False)
3486 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3486 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3487 default=False)
3487 default=False)
3488
3488
3489 settings = Column(
3489 settings = Column(
3490 'settings_json', MutationObj.as_mutable(
3490 'settings_json', MutationObj.as_mutable(
3491 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3491 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3492 repo_id = Column(
3492 repo_id = Column(
3493 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3493 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3494 nullable=True, unique=None, default=None)
3494 nullable=True, unique=None, default=None)
3495 repo = relationship('Repository', lazy='joined')
3495 repo = relationship('Repository', lazy='joined')
3496
3496
3497 repo_group_id = Column(
3497 repo_group_id = Column(
3498 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3498 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3499 nullable=True, unique=None, default=None)
3499 nullable=True, unique=None, default=None)
3500 repo_group = relationship('RepoGroup', lazy='joined')
3500 repo_group = relationship('RepoGroup', lazy='joined')
3501
3501
3502 @hybrid_property
3502 @property
3503 def scope(self):
3503 def scope(self):
3504 if self.repo:
3504 if self.repo:
3505 return self.repo
3505 return repr(self.repo)
3506 if self.repo_group:
3506 if self.repo_group:
3507 return self.repo_group
3507 if self.child_repos_only:
3508 return repr(self.repo_group) + ' (child repos only)'
3509 else:
3510 return repr(self.repo_group) + ' (recursive)'
3508 if self.child_repos_only:
3511 if self.child_repos_only:
3509 return 'root_repos'
3512 return 'root_repos'
3510 return 'global'
3513 return 'global'
3511
3514
3512 @scope.setter
3513 def scope(self, value):
3514 self.repo = None
3515 self.repo_id = None
3516 self.repo_group_id = None
3517 self.repo_group = None
3518 self.child_repos_only = False
3519 if isinstance(value, Repository):
3520 self.repo_id = value.repo_id
3521 self.repo = value
3522 elif isinstance(value, RepoGroup):
3523 self.repo_group_id = value.group_id
3524 self.repo_group = value
3525 elif value == 'root_repos':
3526 self.child_repos_only = True
3527 elif value == 'global':
3528 pass
3529 else:
3530 raise Exception("invalid scope: %s, must be one of "
3531 "['global', 'root_repos', <RepoGroup>. <Repository>]" % value)
3532
3533 def __repr__(self):
3515 def __repr__(self):
3534 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3516 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
@@ -1,213 +1,222 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Model for integrations
23 Model for integrations
24 """
24 """
25
25
26
26
27 import logging
27 import logging
28 import traceback
28 import traceback
29
29
30 from pylons import tmpl_context as c
30 from pylons import tmpl_context as c
31 from pylons.i18n.translation import _, ungettext
31 from pylons.i18n.translation import _, ungettext
32 from sqlalchemy import or_, and_
32 from sqlalchemy import or_, and_
33 from sqlalchemy.sql.expression import false, true
33 from sqlalchemy.sql.expression import false, true
34 from mako import exceptions
34 from mako import exceptions
35
35
36 import rhodecode
36 import rhodecode
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import helpers as h
38 from rhodecode.lib import helpers as h
39 from rhodecode.lib.caching_query import FromCache
39 from rhodecode.lib.caching_query import FromCache
40 from rhodecode.lib.utils import PartialRenderer
40 from rhodecode.lib.utils import PartialRenderer
41 from rhodecode.model import BaseModel
41 from rhodecode.model import BaseModel
42 from rhodecode.model.db import Integration, User, Repository, RepoGroup
42 from rhodecode.model.db import Integration, User, Repository, RepoGroup
43 from rhodecode.model.meta import Session
43 from rhodecode.model.meta import Session
44 from rhodecode.integrations import integration_type_registry
44 from rhodecode.integrations import integration_type_registry
45 from rhodecode.integrations.types.base import IntegrationTypeBase
45 from rhodecode.integrations.types.base import IntegrationTypeBase
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 class IntegrationModel(BaseModel):
50 class IntegrationModel(BaseModel):
51
51
52 cls = Integration
52 cls = Integration
53
53
54 def __get_integration(self, integration):
54 def __get_integration(self, integration):
55 if isinstance(integration, Integration):
55 if isinstance(integration, Integration):
56 return integration
56 return integration
57 elif isinstance(integration, (int, long)):
57 elif isinstance(integration, (int, long)):
58 return self.sa.query(Integration).get(integration)
58 return self.sa.query(Integration).get(integration)
59 else:
59 else:
60 if integration:
60 if integration:
61 raise Exception('integration must be int, long or Instance'
61 raise Exception('integration must be int, long or Instance'
62 ' of Integration got %s' % type(integration))
62 ' of Integration got %s' % type(integration))
63
63
64 def create(self, IntegrationType, name, enabled, scope, settings):
64 def create(self, IntegrationType, name, enabled, repo, repo_group,
65 child_repos_only, settings):
65 """ Create an IntegrationType integration """
66 """ Create an IntegrationType integration """
66 integration = Integration()
67 integration = Integration()
67 integration.integration_type = IntegrationType.key
68 integration.integration_type = IntegrationType.key
68 self.sa.add(integration)
69 self.sa.add(integration)
69 self.update_integration(integration, name, enabled, scope, settings)
70 self.update_integration(integration, name, enabled, repo, repo_group,
71 child_repos_only, settings)
70 self.sa.commit()
72 self.sa.commit()
71 return integration
73 return integration
72
74
73 def update_integration(self, integration, name, enabled, scope, settings):
75 def update_integration(self, integration, name, enabled, repo, repo_group,
74 """
76 child_repos_only, settings):
75 :param scope: one of ['global', 'root_repos', <RepoGroup>. <Repository>]
76 """
77
78 integration = self.__get_integration(integration)
77 integration = self.__get_integration(integration)
79
78
80 integration.scope = scope
79 integration.repo = repo
80 integration.repo_group = repo_group
81 integration.child_repos_only = child_repos_only
81 integration.name = name
82 integration.name = name
82 integration.enabled = enabled
83 integration.enabled = enabled
83 integration.settings = settings
84 integration.settings = settings
84
85
85 return integration
86 return integration
86
87
87 def delete(self, integration):
88 def delete(self, integration):
88 integration = self.__get_integration(integration)
89 integration = self.__get_integration(integration)
89 if integration:
90 if integration:
90 self.sa.delete(integration)
91 self.sa.delete(integration)
91 return True
92 return True
92 return False
93 return False
93
94
94 def get_integration_handler(self, integration):
95 def get_integration_handler(self, integration):
95 TypeClass = integration_type_registry.get(integration.integration_type)
96 TypeClass = integration_type_registry.get(integration.integration_type)
96 if not TypeClass:
97 if not TypeClass:
97 log.error('No class could be found for integration type: {}'.format(
98 log.error('No class could be found for integration type: {}'.format(
98 integration.integration_type))
99 integration.integration_type))
99 return None
100 return None
100
101
101 return TypeClass(integration.settings)
102 return TypeClass(integration.settings)
102
103
103 def send_event(self, integration, event):
104 def send_event(self, integration, event):
104 """ Send an event to an integration """
105 """ Send an event to an integration """
105 handler = self.get_integration_handler(integration)
106 handler = self.get_integration_handler(integration)
106 if handler:
107 if handler:
107 handler.send_event(event)
108 handler.send_event(event)
108
109
109 def get_integrations(self, scope, IntegrationType=None):
110 def get_integrations(self, scope, IntegrationType=None):
110 """
111 """
111 Return integrations for a scope, which must be one of:
112 Return integrations for a scope, which must be one of:
112
113
113 'all' - every integration, global/repogroup/repo
114 'all' - every integration, global/repogroup/repo
114 'global' - global integrations only
115 'global' - global integrations only
115 <Repository> instance - integrations for this repo only
116 <Repository> instance - integrations for this repo only
116 <RepoGroup> instance - integrations for this repogroup only
117 <RepoGroup> instance - integrations for this repogroup only
117 """
118 """
118
119
119 if isinstance(scope, Repository):
120 if isinstance(scope, Repository):
120 query = self.sa.query(Integration).filter(
121 query = self.sa.query(Integration).filter(
121 Integration.repo==scope)
122 Integration.repo==scope)
122 elif isinstance(scope, RepoGroup):
123 elif isinstance(scope, RepoGroup):
123 query = self.sa.query(Integration).filter(
124 query = self.sa.query(Integration).filter(
124 Integration.repo_group==scope)
125 Integration.repo_group==scope)
125 elif scope == 'global':
126 elif scope == 'global':
126 # global integrations
127 # global integrations
127 query = self.sa.query(Integration).filter(
128 query = self.sa.query(Integration).filter(
128 and_(Integration.repo_id==None, Integration.repo_group_id==None)
129 and_(Integration.repo_id==None, Integration.repo_group_id==None)
129 )
130 )
130 elif scope == 'root_repos':
131 elif scope == 'root-repos':
131 query = self.sa.query(Integration).filter(
132 query = self.sa.query(Integration).filter(
132 and_(Integration.repo_id==None,
133 and_(Integration.repo_id==None,
133 Integration.repo_group_id==None,
134 Integration.repo_group_id==None,
134 Integration.child_repos_only==True)
135 Integration.child_repos_only==True)
135 )
136 )
136 elif scope == 'all':
137 elif scope == 'all':
137 query = self.sa.query(Integration)
138 query = self.sa.query(Integration)
138 else:
139 else:
139 raise Exception(
140 raise Exception(
140 "invalid `scope`, must be one of: "
141 "invalid `scope`, must be one of: "
141 "['global', 'all', <Repository>, <RepoGroup>]")
142 "['global', 'all', <Repository>, <RepoGroup>]")
142
143
143 if IntegrationType is not None:
144 if IntegrationType is not None:
144 query = query.filter(
145 query = query.filter(
145 Integration.integration_type==IntegrationType.key)
146 Integration.integration_type==IntegrationType.key)
146
147
147 result = []
148 result = []
148 for integration in query.all():
149 for integration in query.all():
149 IntType = integration_type_registry.get(integration.integration_type)
150 IntType = integration_type_registry.get(integration.integration_type)
150 result.append((IntType, integration))
151 result.append((IntType, integration))
151 return result
152 return result
152
153
153 def get_for_event(self, event, cache=False):
154 def get_for_event(self, event, cache=False):
154 """
155 """
155 Get integrations that match an event
156 Get integrations that match an event
156 """
157 """
157 query = self.sa.query(
158 query = self.sa.query(
158 Integration
159 Integration
159 ).filter(
160 ).filter(
160 Integration.enabled==True
161 Integration.enabled==True
161 )
162 )
162
163
163 global_integrations_filter = and_(
164 global_integrations_filter = and_(
164 Integration.repo_id==None,
165 Integration.repo_id==None,
165 Integration.repo_group_id==None,
166 Integration.repo_group_id==None,
166 Integration.child_repos_only==False,
167 Integration.child_repos_only==False,
167 )
168 )
168
169
169 if isinstance(event, events.RepoEvent):
170 if isinstance(event, events.RepoEvent):
170 root_repos_integrations_filter = and_(
171 root_repos_integrations_filter = and_(
171 Integration.repo_id==None,
172 Integration.repo_id==None,
172 Integration.repo_group_id==None,
173 Integration.repo_group_id==None,
173 Integration.child_repos_only==True,
174 Integration.child_repos_only==True,
174 )
175 )
175
176
176 clauses = [
177 clauses = [
177 global_integrations_filter,
178 global_integrations_filter,
178 ]
179 ]
179
180
180 # repo integrations
181 # repo integrations
181 if event.repo.repo_id: # pre create events dont have a repo_id yet
182 if event.repo.repo_id: # pre create events dont have a repo_id yet
182 clauses.append(
183 clauses.append(
183 Integration.repo_id==event.repo.repo_id
184 Integration.repo_id==event.repo.repo_id
184 )
185 )
185
186
186 if event.repo.group:
187 if event.repo.group:
187 clauses.append(
188 clauses.append(
188 Integration.repo_group_id == event.repo.group.group_id
189 and_(
190 Integration.repo_group_id==event.repo.group.group_id,
191 Integration.child_repos_only==True
192 )
189 )
193 )
190 # repo group cascade to kids (maybe implement this sometime?)
194 # repo group cascade to kids
191 # clauses.append(Integration.repo_group_id.in_(
195 clauses.append(
192 # [group.group_id for group in
196 and_(
193 # event.repo.groups_with_parents]
197 Integration.repo_group_id.in_(
194 # ))
198 [group.group_id for group in
199 event.repo.groups_with_parents]
200 ),
201 Integration.child_repos_only==False
202 )
203 )
195
204
196
205
197 if not event.repo.group: # root repo
206 if not event.repo.group: # root repo
198 clauses.append(root_repos_integrations_filter)
207 clauses.append(root_repos_integrations_filter)
199
208
200 query = query.filter(or_(*clauses))
209 query = query.filter(or_(*clauses))
201
210
202 if cache:
211 if cache:
203 query = query.options(FromCache(
212 query = query.options(FromCache(
204 "sql_cache_short",
213 "sql_cache_short",
205 "get_enabled_repo_integrations_%i" % event.repo.repo_id))
214 "get_enabled_repo_integrations_%i" % event.repo.repo_id))
206 else: # only global integrations
215 else: # only global integrations
207 query = query.filter(global_integrations_filter)
216 query = query.filter(global_integrations_filter)
208 if cache:
217 if cache:
209 query = query.options(FromCache(
218 query = query.options(FromCache(
210 "sql_cache_short", "get_enabled_global_integrations"))
219 "sql_cache_short", "get_enabled_global_integrations"))
211
220
212 result = query.all()
221 result = query.all()
213 return result No newline at end of file
222 return result
@@ -1,187 +1,226 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22
22
23 import deform
23 import deform
24 import colander
24 import colander
25
25
26 from rhodecode.translation import _
26 from rhodecode.translation import _
27 from rhodecode.model.db import Repository, RepoGroup
27 from rhodecode.model.db import Repository, RepoGroup
28 from rhodecode.model.validation_schema import validators, preparers
28 from rhodecode.model.validation_schema import validators, preparers
29
29
30
30
31 def integration_scope_choices(permissions):
31 def integration_scope_choices(permissions):
32 """
32 """
33 Return list of (value, label) choices for integration scopes depending on
33 Return list of (value, label) choices for integration scopes depending on
34 the permissions
34 the permissions
35 """
35 """
36 result = [('', _('Pick a scope:'))]
36 result = [('', _('Pick a scope:'))]
37 if 'hg.admin' in permissions['global']:
37 if 'hg.admin' in permissions['global']:
38 result.extend([
38 result.extend([
39 ('global', _('Global (all repositories)')),
39 ('global', _('Global (all repositories)')),
40 ('root_repos', _('Top level repositories only')),
40 ('root-repos', _('Top level repositories only')),
41 ])
41 ])
42
42
43 repo_choices = [
43 repo_choices = [
44 ('repo:%s' % repo_name, '/' + repo_name)
44 ('repo:%s' % repo_name, '/' + repo_name)
45 for repo_name, repo_perm
45 for repo_name, repo_perm
46 in permissions['repositories'].items()
46 in permissions['repositories'].items()
47 if repo_perm == 'repository.admin'
47 if repo_perm == 'repository.admin'
48 ]
48 ]
49 repogroup_choices = [
49 repogroup_choices = [
50 ('repogroup:%s' % repo_group_name, '/' + repo_group_name + ' (group)')
50 ('repogroup:%s' % repo_group_name, '/' + repo_group_name + '/ (child repos only)')
51 for repo_group_name, repo_group_perm
52 in permissions['repositories_groups'].items()
53 if repo_group_perm == 'group.admin'
54 ]
55 repogroup_recursive_choices = [
56 ('repogroup-recursive:%s' % repo_group_name, '/' + repo_group_name + '/ (recursive)')
51 for repo_group_name, repo_group_perm
57 for repo_group_name, repo_group_perm
52 in permissions['repositories_groups'].items()
58 in permissions['repositories_groups'].items()
53 if repo_group_perm == 'group.admin'
59 if repo_group_perm == 'group.admin'
54 ]
60 ]
55 result.extend(
61 result.extend(
56 sorted(repogroup_choices + repo_choices,
62 sorted(repogroup_recursive_choices + repogroup_choices + repo_choices,
57 key=lambda (choice, label): choice.split(':', 1)[1]
63 key=lambda (choice, label): choice.split(':', 1)[1]
58 )
64 )
59 )
65 )
60 return result
66 return result
61
67
62
68
63 @colander.deferred
69 @colander.deferred
64 def deferred_integration_scopes_validator(node, kw):
70 def deferred_integration_scopes_validator(node, kw):
65 perms = kw.get('permissions')
71 perms = kw.get('permissions')
66 def _scope_validator(_node, scope):
72 def _scope_validator(_node, scope):
67 is_super_admin = 'hg.admin' in perms['global']
73 is_super_admin = 'hg.admin' in perms['global']
68
74
69 if scope in ('global', 'root_repos'):
75 if scope.get('repo'):
76 if (is_super_admin or perms['repositories'].get(
77 scope['repo'].repo_name) == 'repository.admin'):
78 return True
79 msg = _('Only repo admins can create integrations')
80 raise colander.Invalid(_node, msg)
81 elif scope.get('repo_group'):
82 if (is_super_admin or perms['repositories_groups'].get(
83 scope['repo_group'].group_name) == 'group.admin'):
84 return True
85
86 msg = _('Only repogroup admins can create integrations')
87 raise colander.Invalid(_node, msg)
88 else:
70 if is_super_admin:
89 if is_super_admin:
71 return True
90 return True
72 msg = _('Only superadmins can create global integrations')
91 msg = _('Only superadmins can create global integrations')
73 raise colander.Invalid(_node, msg)
92 raise colander.Invalid(_node, msg)
74 elif isinstance(scope, Repository):
75 if (is_super_admin or perms['repositories'].get(
76 scope.repo_name) == 'repository.admin'):
77 return True
78 msg = _('Only repo admins can create integrations')
79 raise colander.Invalid(_node, msg)
80 elif isinstance(scope, RepoGroup):
81 if (is_super_admin or perms['repositories_groups'].get(
82 scope.group_name) == 'group.admin'):
83 return True
84
85 msg = _('Only repogroup admins can create integrations')
86 raise colander.Invalid(_node, msg)
87
88 msg = _('Invalid integration scope: %s' % scope)
89 raise colander.Invalid(node, msg)
90
93
91 return _scope_validator
94 return _scope_validator
92
95
93
96
94 @colander.deferred
97 @colander.deferred
95 def deferred_integration_scopes_widget(node, kw):
98 def deferred_integration_scopes_widget(node, kw):
96 if kw.get('no_scope'):
99 if kw.get('no_scope'):
97 return deform.widget.TextInputWidget(readonly=True)
100 return deform.widget.TextInputWidget(readonly=True)
98
101
99 choices = integration_scope_choices(kw.get('permissions'))
102 choices = integration_scope_choices(kw.get('permissions'))
100 widget = deform.widget.Select2Widget(values=choices)
103 widget = deform.widget.Select2Widget(values=choices)
101 return widget
104 return widget
102
105
103 class IntegrationScope(colander.SchemaType):
106
107 class IntegrationScopeType(colander.SchemaType):
104 def serialize(self, node, appstruct):
108 def serialize(self, node, appstruct):
105 if appstruct is colander.null:
109 if appstruct is colander.null:
106 return colander.null
110 return colander.null
107
111
108 if isinstance(appstruct, Repository):
112 if appstruct.get('repo'):
109 return 'repo:%s' % appstruct.repo_name
113 return 'repo:%s' % appstruct['repo'].repo_name
110 elif isinstance(appstruct, RepoGroup):
114 elif appstruct.get('repo_group'):
111 return 'repogroup:%s' % appstruct.group_name
115 if appstruct.get('child_repos_only'):
112 elif appstruct in ('global', 'root_repos'):
116 return 'repogroup:%s' % appstruct['repo_group'].group_name
113 return appstruct
117 else:
118 return 'repogroup-recursive:%s' % (
119 appstruct['repo_group'].group_name)
120 else:
121 if appstruct.get('child_repos_only'):
122 return 'root-repos'
123 else:
124 return 'global'
125
114 raise colander.Invalid(node, '%r is not a valid scope' % appstruct)
126 raise colander.Invalid(node, '%r is not a valid scope' % appstruct)
115
127
116 def deserialize(self, node, cstruct):
128 def deserialize(self, node, cstruct):
117 if cstruct is colander.null:
129 if cstruct is colander.null:
118 return colander.null
130 return colander.null
119
131
120 if cstruct.startswith('repo:'):
132 if cstruct.startswith('repo:'):
121 repo = Repository.get_by_repo_name(cstruct.split(':')[1])
133 repo = Repository.get_by_repo_name(cstruct.split(':')[1])
122 if repo:
134 if repo:
123 return repo
135 return {
136 'repo': repo,
137 'repo_group': None,
138 'child_repos_only': None,
139 }
140 elif cstruct.startswith('repogroup-recursive:'):
141 repo_group = RepoGroup.get_by_group_name(cstruct.split(':')[1])
142 if repo_group:
143 return {
144 'repo': None,
145 'repo_group': repo_group,
146 'child_repos_only': False
147 }
124 elif cstruct.startswith('repogroup:'):
148 elif cstruct.startswith('repogroup:'):
125 repo_group = RepoGroup.get_by_group_name(cstruct.split(':')[1])
149 repo_group = RepoGroup.get_by_group_name(cstruct.split(':')[1])
126 if repo_group:
150 if repo_group:
127 return repo_group
151 return {
128 elif cstruct in ('global', 'root_repos'):
152 'repo': None,
129 return cstruct
153 'repo_group': repo_group,
154 'child_repos_only': True
155 }
156 elif cstruct == 'global':
157 return {
158 'repo': None,
159 'repo_group': None,
160 'child_repos_only': False
161 }
162 elif cstruct == 'root-repos':
163 return {
164 'repo': None,
165 'repo_group': None,
166 'child_repos_only': True
167 }
130
168
131 raise colander.Invalid(node, '%r is not a valid scope' % cstruct)
169 raise colander.Invalid(node, '%r is not a valid scope' % cstruct)
132
170
171
133 class IntegrationOptionsSchemaBase(colander.MappingSchema):
172 class IntegrationOptionsSchemaBase(colander.MappingSchema):
134
173
135 name = colander.SchemaNode(
174 name = colander.SchemaNode(
136 colander.String(),
175 colander.String(),
137 description=_('Short name for this integration.'),
176 description=_('Short name for this integration.'),
138 missing=colander.required,
177 missing=colander.required,
139 title=_('Integration name'),
178 title=_('Integration name'),
140 )
179 )
141
180
142 scope = colander.SchemaNode(
181 scope = colander.SchemaNode(
143 IntegrationScope(),
182 IntegrationScopeType(),
144 description=_(
183 description=_(
145 'Scope of the integration. Group scope means the integration '
184 'Scope of the integration. Recursive means the integration '
146 ' runs on all child repos of that group.'),
185 ' runs on all repos of that group and children recursively.'),
147 title=_('Integration scope'),
186 title=_('Integration scope'),
148 validator=deferred_integration_scopes_validator,
187 validator=deferred_integration_scopes_validator,
149 widget=deferred_integration_scopes_widget,
188 widget=deferred_integration_scopes_widget,
150 missing=colander.required,
189 missing=colander.required,
151 )
190 )
152
191
153 enabled = colander.SchemaNode(
192 enabled = colander.SchemaNode(
154 colander.Bool(),
193 colander.Bool(),
155 default=True,
194 default=True,
156 description=_('Enable or disable this integration.'),
195 description=_('Enable or disable this integration.'),
157 missing=False,
196 missing=False,
158 title=_('Enabled'),
197 title=_('Enabled'),
159 )
198 )
160
199
161
200
162
201
163 def make_integration_schema(IntegrationType, settings=None):
202 def make_integration_schema(IntegrationType, settings=None):
164 """
203 """
165 Return a colander schema for an integration type
204 Return a colander schema for an integration type
166
205
167 :param IntegrationType: the integration type class
206 :param IntegrationType: the integration type class
168 :param settings: existing integration settings dict (optional)
207 :param settings: existing integration settings dict (optional)
169 """
208 """
170
209
171 settings = settings or {}
210 settings = settings or {}
172 settings_schema = IntegrationType(settings=settings).settings_schema()
211 settings_schema = IntegrationType(settings=settings).settings_schema()
173
212
174 class IntegrationSchema(colander.Schema):
213 class IntegrationSchema(colander.Schema):
175 options = IntegrationOptionsSchemaBase()
214 options = IntegrationOptionsSchemaBase()
176
215
177 schema = IntegrationSchema()
216 schema = IntegrationSchema()
178 schema['options'].title = _('General integration options')
217 schema['options'].title = _('General integration options')
179
218
180 settings_schema.name = 'settings'
219 settings_schema.name = 'settings'
181 settings_schema.title = _('{integration_type} settings').format(
220 settings_schema.title = _('{integration_type} settings').format(
182 integration_type=IntegrationType.display_name)
221 integration_type=IntegrationType.display_name)
183 schema.add(settings_schema)
222 schema.add(settings_schema)
184
223
185 return schema
224 return schema
186
225
187
226
@@ -1,249 +1,252 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.html"/>
2 <%inherit file="base.html"/>
3
3
4 <%def name="breadcrumbs_links()">
4 <%def name="breadcrumbs_links()">
5 %if c.repo:
5 %if c.repo:
6 ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))}
6 ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))}
7 %elif c.repo_group:
7 %elif c.repo_group:
8 ${h.link_to(_('Admin'),h.url('admin_home'))}
8 ${h.link_to(_('Admin'),h.url('admin_home'))}
9 &raquo;
9 &raquo;
10 ${h.link_to(_('Repository Groups'),h.url('repo_groups'))}
10 ${h.link_to(_('Repository Groups'),h.url('repo_groups'))}
11 &raquo;
11 &raquo;
12 ${h.link_to(c.repo_group.group_name,h.url('edit_repo_group', group_name=c.repo_group.group_name))}
12 ${h.link_to(c.repo_group.group_name,h.url('edit_repo_group', group_name=c.repo_group.group_name))}
13 %else:
13 %else:
14 ${h.link_to(_('Admin'),h.url('admin_home'))}
14 ${h.link_to(_('Admin'),h.url('admin_home'))}
15 &raquo;
15 &raquo;
16 ${h.link_to(_('Settings'),h.url('admin_settings'))}
16 ${h.link_to(_('Settings'),h.url('admin_settings'))}
17 %endif
17 %endif
18 %if current_IntegrationType:
18 %if current_IntegrationType:
19 &raquo;
19 &raquo;
20 %if c.repo:
20 %if c.repo:
21 ${h.link_to(_('Integrations'),
21 ${h.link_to(_('Integrations'),
22 request.route_url(route_name='repo_integrations_home',
22 request.route_url(route_name='repo_integrations_home',
23 repo_name=c.repo.repo_name))}
23 repo_name=c.repo.repo_name))}
24 %elif c.repo_group:
24 %elif c.repo_group:
25 ${h.link_to(_('Integrations'),
25 ${h.link_to(_('Integrations'),
26 request.route_url(route_name='repo_group_integrations_home',
26 request.route_url(route_name='repo_group_integrations_home',
27 repo_group_name=c.repo_group.group_name))}
27 repo_group_name=c.repo_group.group_name))}
28 %else:
28 %else:
29 ${h.link_to(_('Integrations'),
29 ${h.link_to(_('Integrations'),
30 request.route_url(route_name='global_integrations_home'))}
30 request.route_url(route_name='global_integrations_home'))}
31 %endif
31 %endif
32 &raquo;
32 &raquo;
33 ${current_IntegrationType.display_name}
33 ${current_IntegrationType.display_name}
34 %else:
34 %else:
35 &raquo;
35 &raquo;
36 ${_('Integrations')}
36 ${_('Integrations')}
37 %endif
37 %endif
38 </%def>
38 </%def>
39
39
40 <div class="panel panel-default">
40 <div class="panel panel-default">
41 <div class="panel-heading">
41 <div class="panel-heading">
42 <h3 class="panel-title">
42 <h3 class="panel-title">
43 %if c.repo:
43 %if c.repo:
44 ${_('Current Integrations for Repository: {repo_name}').format(repo_name=c.repo.repo_name)}
44 ${_('Current Integrations for Repository: {repo_name}').format(repo_name=c.repo.repo_name)}
45 %elif c.repo_group:
45 %elif c.repo_group:
46 ${_('Current Integrations for repository group: {repo_group_name}').format(repo_group_name=c.repo_group.group_name)}
46 ${_('Current Integrations for repository group: {repo_group_name}').format(repo_group_name=c.repo_group.group_name)}
47 %else:
47 %else:
48 ${_('Current Integrations')}
48 ${_('Current Integrations')}
49 %endif
49 %endif
50 </h3>
50 </h3>
51 </div>
51 </div>
52 <div class="panel-body">
52 <div class="panel-body">
53 <%
53 <%
54 if c.repo:
54 if c.repo:
55 home_url = request.route_path('repo_integrations_home',
55 home_url = request.route_path('repo_integrations_home',
56 repo_name=c.repo.repo_name)
56 repo_name=c.repo.repo_name)
57 elif c.repo_group:
57 elif c.repo_group:
58 home_url = request.route_path('repo_group_integrations_home',
58 home_url = request.route_path('repo_group_integrations_home',
59 repo_group_name=c.repo_group.group_name)
59 repo_group_name=c.repo_group.group_name)
60 else:
60 else:
61 home_url = request.route_path('global_integrations_home')
61 home_url = request.route_path('global_integrations_home')
62 %>
62 %>
63
63
64 <a href="${home_url}" class="btn ${not current_IntegrationType and 'btn-primary' or ''}">${_('All')}</a>
64 <a href="${home_url}" class="btn ${not current_IntegrationType and 'btn-primary' or ''}">${_('All')}</a>
65
65
66 %for integration_key, IntegrationType in available_integrations.items():
66 %for integration_key, IntegrationType in available_integrations.items():
67 <%
67 <%
68 if c.repo:
68 if c.repo:
69 list_url = request.route_path('repo_integrations_list',
69 list_url = request.route_path('repo_integrations_list',
70 repo_name=c.repo.repo_name,
70 repo_name=c.repo.repo_name,
71 integration=integration_key)
71 integration=integration_key)
72 elif c.repo_group:
72 elif c.repo_group:
73 list_url = request.route_path('repo_group_integrations_list',
73 list_url = request.route_path('repo_group_integrations_list',
74 repo_group_name=c.repo_group.group_name,
74 repo_group_name=c.repo_group.group_name,
75 integration=integration_key)
75 integration=integration_key)
76 else:
76 else:
77 list_url = request.route_path('global_integrations_list',
77 list_url = request.route_path('global_integrations_list',
78 integration=integration_key)
78 integration=integration_key)
79 %>
79 %>
80 <a href="${list_url}"
80 <a href="${list_url}"
81 class="btn ${current_IntegrationType and integration_key == current_IntegrationType.key and 'btn-primary' or ''}">
81 class="btn ${current_IntegrationType and integration_key == current_IntegrationType.key and 'btn-primary' or ''}">
82 ${IntegrationType.display_name}
82 ${IntegrationType.display_name}
83 </a>
83 </a>
84 %endfor
84 %endfor
85
85
86 <%
86 <%
87 if c.repo:
87 if c.repo:
88 create_url = h.route_path('repo_integrations_new', repo_name=c.repo.repo_name)
88 create_url = h.route_path('repo_integrations_new', repo_name=c.repo.repo_name)
89 elif c.repo_group:
89 elif c.repo_group:
90 create_url = h.route_path('repo_group_integrations_new', repo_group_name=c.repo_group.group_name)
90 create_url = h.route_path('repo_group_integrations_new', repo_group_name=c.repo_group.group_name)
91 else:
91 else:
92 create_url = h.route_path('global_integrations_new')
92 create_url = h.route_path('global_integrations_new')
93 %>
93 %>
94 <p class="pull-right">
94 <p class="pull-right">
95 <a href="${create_url}" class="btn btn-small btn-success">${_(u'Create new integration')}</a>
95 <a href="${create_url}" class="btn btn-small btn-success">${_(u'Create new integration')}</a>
96 </p>
96 </p>
97
97
98 <table class="rctable integrations">
98 <table class="rctable integrations">
99 <thead>
99 <thead>
100 <tr>
100 <tr>
101 <th><a href="?sort=enabled:${rev_sort_dir}">${_('Enabled')}</a></th>
101 <th><a href="?sort=enabled:${rev_sort_dir}">${_('Enabled')}</a></th>
102 <th><a href="?sort=name:${rev_sort_dir}">${_('Name')}</a></th>
102 <th><a href="?sort=name:${rev_sort_dir}">${_('Name')}</a></th>
103 <th colspan="2"><a href="?sort=integration_type:${rev_sort_dir}">${_('Type')}</a></th>
103 <th colspan="2"><a href="?sort=integration_type:${rev_sort_dir}">${_('Type')}</a></th>
104 <th><a href="?sort=scope:${rev_sort_dir}">${_('Scope')}</a></th>
104 <th><a href="?sort=scope:${rev_sort_dir}">${_('Scope')}</a></th>
105 <th>${_('Actions')}</th>
105 <th>${_('Actions')}</th>
106 <th></th>
106 <th></th>
107 </tr>
107 </tr>
108 </thead>
108 </thead>
109 <tbody>
109 <tbody>
110 %if not integrations_list:
110 %if not integrations_list:
111 <tr>
111 <tr>
112 <td colspan="7">
112 <td colspan="7">
113 <% integration_type = current_IntegrationType and current_IntegrationType.display_name or '' %>
113 <% integration_type = current_IntegrationType and current_IntegrationType.display_name or '' %>
114 %if c.repo:
114 %if c.repo:
115 ${_('No {type} integrations for repo {repo} exist yet.').format(type=integration_type, repo=c.repo.repo_name)}
115 ${_('No {type} integrations for repo {repo} exist yet.').format(type=integration_type, repo=c.repo.repo_name)}
116 %elif c.repo_group:
116 %elif c.repo_group:
117 ${_('No {type} integrations for repogroup {repogroup} exist yet.').format(type=integration_type, repogroup=c.repo_group.group_name)}
117 ${_('No {type} integrations for repogroup {repogroup} exist yet.').format(type=integration_type, repogroup=c.repo_group.group_name)}
118 %else:
118 %else:
119 ${_('No {type} integrations exist yet.').format(type=integration_type)}
119 ${_('No {type} integrations exist yet.').format(type=integration_type)}
120 %endif
120 %endif
121
121
122 %if current_IntegrationType:
122 %if current_IntegrationType:
123 <%
123 <%
124 if c.repo:
124 if c.repo:
125 create_url = h.route_path('repo_integrations_create', repo_name=c.repo.repo_name, integration=current_IntegrationType.key)
125 create_url = h.route_path('repo_integrations_create', repo_name=c.repo.repo_name, integration=current_IntegrationType.key)
126 elif c.repo_group:
126 elif c.repo_group:
127 create_url = h.route_path('repo_group_integrations_create', repo_group_name=c.repo_group.group_name, integration=current_IntegrationType.key)
127 create_url = h.route_path('repo_group_integrations_create', repo_group_name=c.repo_group.group_name, integration=current_IntegrationType.key)
128 else:
128 else:
129 create_url = h.route_path('global_integrations_create', integration=current_IntegrationType.key)
129 create_url = h.route_path('global_integrations_create', integration=current_IntegrationType.key)
130 %>
130 %>
131 %endif
131 %endif
132
132
133 <a href="${create_url}">${_(u'Create one')}</a>
133 <a href="${create_url}">${_(u'Create one')}</a>
134 </td>
134 </td>
135 </tr>
135 </tr>
136 %endif
136 %endif
137 %for IntegrationType, integration in integrations_list:
137 %for IntegrationType, integration in integrations_list:
138 <tr id="integration_${integration.integration_id}">
138 <tr id="integration_${integration.integration_id}">
139 <td class="td-enabled">
139 <td class="td-enabled">
140 %if integration.enabled:
140 %if integration.enabled:
141 <div class="flag_status approved pull-left"></div>
141 <div class="flag_status approved pull-left"></div>
142 %else:
142 %else:
143 <div class="flag_status rejected pull-left"></div>
143 <div class="flag_status rejected pull-left"></div>
144 %endif
144 %endif
145 </td>
145 </td>
146 <td class="td-description">
146 <td class="td-description">
147 ${integration.name}
147 ${integration.name}
148 </td>
148 </td>
149 <td class="td-icon">
149 <td class="td-icon">
150 %if integration.integration_type in available_integrations:
150 %if integration.integration_type in available_integrations:
151 <div class="integration-icon">
151 <div class="integration-icon">
152 ${available_integrations[integration.integration_type].icon|n}
152 ${available_integrations[integration.integration_type].icon|n}
153 </div>
153 </div>
154 %else:
154 %else:
155 ?
155 ?
156 %endif
156 %endif
157 </td>
157 </td>
158 <td class="td-type">
158 <td class="td-type">
159 ${integration.integration_type}
159 ${integration.integration_type}
160 </td>
160 </td>
161 <td class="td-scope">
161 <td class="td-scope">
162 %if integration.repo:
162 %if integration.repo:
163 <a href="${h.url('summary_home', repo_name=integration.repo.repo_name)}">
163 <a href="${h.url('summary_home', repo_name=integration.repo.repo_name)}">
164 ${_('repo')}:${integration.repo.repo_name}
164 ${_('repo')}:${integration.repo.repo_name}
165 </a>
165 </a>
166 %elif integration.repo_group:
166 %elif integration.repo_group:
167 <a href="${h.url('repo_group_home', group_name=integration.repo_group.group_name)}">
167 <a href="${h.url('repo_group_home', group_name=integration.repo_group.group_name)}">
168 ${_('repogroup')}:${integration.repo_group.group_name}
168 ${_('repogroup')}:${integration.repo_group.group_name}
169 %if integration.child_repos_only:
170 ${_('child repos only')}
171 %else:
172 ${_('cascade to all')}
173 %endif
169 </a>
174 </a>
170 %else:
175 %else:
171 %if integration.scope == 'root_repos':
176 %if integration.child_repos_only:
172 ${_('top level repos only')}
177 ${_('top level repos only')}
173 %elif integration.scope == 'global':
178 %else:
174 ${_('global')}
179 ${_('global')}
175 %else:
176 ${_('unknown scope')}: ${integration.scope}
177 %endif
180 %endif
178 </td>
181 </td>
179 %endif
182 %endif
180 <td class="td-action">
183 <td class="td-action">
181 %if not IntegrationType:
184 %if not IntegrationType:
182 ${_('unknown integration')}
185 ${_('unknown integration')}
183 %else:
186 %else:
184 <%
187 <%
185 if c.repo:
188 if c.repo:
186 edit_url = request.route_path('repo_integrations_edit',
189 edit_url = request.route_path('repo_integrations_edit',
187 repo_name=c.repo.repo_name,
190 repo_name=c.repo.repo_name,
188 integration=integration.integration_type,
191 integration=integration.integration_type,
189 integration_id=integration.integration_id)
192 integration_id=integration.integration_id)
190 elif c.repo_group:
193 elif c.repo_group:
191 edit_url = request.route_path('repo_group_integrations_edit',
194 edit_url = request.route_path('repo_group_integrations_edit',
192 repo_group_name=c.repo_group.group_name,
195 repo_group_name=c.repo_group.group_name,
193 integration=integration.integration_type,
196 integration=integration.integration_type,
194 integration_id=integration.integration_id)
197 integration_id=integration.integration_id)
195 else:
198 else:
196 edit_url = request.route_path('global_integrations_edit',
199 edit_url = request.route_path('global_integrations_edit',
197 integration=integration.integration_type,
200 integration=integration.integration_type,
198 integration_id=integration.integration_id)
201 integration_id=integration.integration_id)
199 %>
202 %>
200 <div class="grid_edit">
203 <div class="grid_edit">
201 <a href="${edit_url}">${_('Edit')}</a>
204 <a href="${edit_url}">${_('Edit')}</a>
202 </div>
205 </div>
203 <div class="grid_delete">
206 <div class="grid_delete">
204 <a href="${edit_url}"
207 <a href="${edit_url}"
205 class="btn btn-link btn-danger delete_integration_entry"
208 class="btn btn-link btn-danger delete_integration_entry"
206 data-desc="${integration.name}"
209 data-desc="${integration.name}"
207 data-uid="${integration.integration_id}">
210 data-uid="${integration.integration_id}">
208 ${_('Delete')}
211 ${_('Delete')}
209 </a>
212 </a>
210 </div>
213 </div>
211 %endif
214 %endif
212 </td>
215 </td>
213 </tr>
216 </tr>
214 %endfor
217 %endfor
215 <tr id="last-row"></tr>
218 <tr id="last-row"></tr>
216 </tbody>
219 </tbody>
217 </table>
220 </table>
218 <div class="integrations-paginator">
221 <div class="integrations-paginator">
219 <div class="pagination-wh pagination-left">
222 <div class="pagination-wh pagination-left">
220 ${integrations_list.pager('$link_previous ~2~ $link_next')}
223 ${integrations_list.pager('$link_previous ~2~ $link_next')}
221 </div>
224 </div>
222 </div>
225 </div>
223 </div>
226 </div>
224 </div>
227 </div>
225 <script type="text/javascript">
228 <script type="text/javascript">
226 var delete_integration = function(entry) {
229 var delete_integration = function(entry) {
227 if (confirm("Confirm to remove this integration: "+$(entry).data('desc'))) {
230 if (confirm("Confirm to remove this integration: "+$(entry).data('desc'))) {
228 var request = $.ajax({
231 var request = $.ajax({
229 type: "POST",
232 type: "POST",
230 url: $(entry).attr('href'),
233 url: $(entry).attr('href'),
231 data: {
234 data: {
232 'delete': 'delete',
235 'delete': 'delete',
233 'csrf_token': CSRF_TOKEN
236 'csrf_token': CSRF_TOKEN
234 },
237 },
235 success: function(){
238 success: function(){
236 location.reload();
239 location.reload();
237 },
240 },
238 error: function(data, textStatus, errorThrown){
241 error: function(data, textStatus, errorThrown){
239 alert("Error while deleting entry.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(entry)[0].url));
242 alert("Error while deleting entry.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(entry)[0].url));
240 }
243 }
241 });
244 });
242 };
245 };
243 }
246 }
244
247
245 $('.delete_integration_entry').on('click', function(e){
248 $('.delete_integration_entry').on('click', function(e){
246 e.preventDefault();
249 e.preventDefault();
247 delete_integration(this);
250 delete_integration(this);
248 });
251 });
249 </script> No newline at end of file
252 </script>
@@ -1,262 +1,264 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23 from webob.exc import HTTPNotFound
23 from webob.exc import HTTPNotFound
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.model.db import Integration
26 from rhodecode.model.db import Integration
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.tests import assert_session_flash, url, TEST_USER_ADMIN_LOGIN
28 from rhodecode.tests import assert_session_flash, url, TEST_USER_ADMIN_LOGIN
29 from rhodecode.tests.utils import AssertResponse
29 from rhodecode.tests.utils import AssertResponse
30 from rhodecode.integrations import integration_type_registry
30 from rhodecode.integrations import integration_type_registry
31 from rhodecode.config.routing import ADMIN_PREFIX
31 from rhodecode.config.routing import ADMIN_PREFIX
32
32
33
33
34 @pytest.mark.usefixtures('app', 'autologin_user')
34 @pytest.mark.usefixtures('app', 'autologin_user')
35 class TestIntegrationsView(object):
35 class TestIntegrationsView(object):
36 pass
36 pass
37
37
38
38
39 class TestGlobalIntegrationsView(TestIntegrationsView):
39 class TestGlobalIntegrationsView(TestIntegrationsView):
40 def test_index_no_integrations(self, app):
40 def test_index_no_integrations(self, app):
41 url = ADMIN_PREFIX + '/integrations'
41 url = ADMIN_PREFIX + '/integrations'
42 response = app.get(url)
42 response = app.get(url)
43
43
44 assert response.status_code == 200
44 assert response.status_code == 200
45 assert 'exist yet' in response.body
45 assert 'exist yet' in response.body
46
46
47 def test_index_with_integrations(self, app, global_integration_stub):
47 def test_index_with_integrations(self, app, global_integration_stub):
48 url = ADMIN_PREFIX + '/integrations'
48 url = ADMIN_PREFIX + '/integrations'
49 response = app.get(url)
49 response = app.get(url)
50
50
51 assert response.status_code == 200
51 assert response.status_code == 200
52 assert 'exist yet' not in response.body
52 assert 'exist yet' not in response.body
53 assert global_integration_stub.name in response.body
53 assert global_integration_stub.name in response.body
54
54
55 def test_new_integration_page(self, app):
55 def test_new_integration_page(self, app):
56 url = ADMIN_PREFIX + '/integrations/new'
56 url = ADMIN_PREFIX + '/integrations/new'
57
57
58 response = app.get(url)
58 response = app.get(url)
59
59
60 assert response.status_code == 200
60 assert response.status_code == 200
61
61
62 for integration_key in integration_type_registry:
62 for integration_key in integration_type_registry:
63 nurl = (ADMIN_PREFIX + '/integrations/{integration}/new').format(
63 nurl = (ADMIN_PREFIX + '/integrations/{integration}/new').format(
64 integration=integration_key)
64 integration=integration_key)
65 assert nurl in response.body
65 assert nurl in response.body
66
66
67 @pytest.mark.parametrize(
67 @pytest.mark.parametrize(
68 'IntegrationType', integration_type_registry.values())
68 'IntegrationType', integration_type_registry.values())
69 def test_get_create_integration_page(self, app, IntegrationType):
69 def test_get_create_integration_page(self, app, IntegrationType):
70 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
70 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
71 integration_key=IntegrationType.key)
71 integration_key=IntegrationType.key)
72
72
73 response = app.get(url)
73 response = app.get(url)
74
74
75 assert response.status_code == 200
75 assert response.status_code == 200
76 assert IntegrationType.display_name in response.body
76 assert IntegrationType.display_name in response.body
77
77
78 def test_post_integration_page(self, app, StubIntegrationType, csrf_token,
78 def test_post_integration_page(self, app, StubIntegrationType, csrf_token,
79 test_repo_group, backend_random):
79 test_repo_group, backend_random):
80 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
80 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
81 integration_key=StubIntegrationType.key)
81 integration_key=StubIntegrationType.key)
82
82
83 _post_integration_test_helper(app, url, csrf_token, admin_view=True,
83 _post_integration_test_helper(app, url, csrf_token, admin_view=True,
84 repo=backend_random.repo, repo_group=test_repo_group)
84 repo=backend_random.repo, repo_group=test_repo_group)
85
85
86
86
87 class TestRepoGroupIntegrationsView(TestIntegrationsView):
87 class TestRepoGroupIntegrationsView(TestIntegrationsView):
88 def test_index_no_integrations(self, app, test_repo_group):
88 def test_index_no_integrations(self, app, test_repo_group):
89 url = '/{repo_group_name}/settings/integrations'.format(
89 url = '/{repo_group_name}/settings/integrations'.format(
90 repo_group_name=test_repo_group.group_name)
90 repo_group_name=test_repo_group.group_name)
91 response = app.get(url)
91 response = app.get(url)
92
92
93 assert response.status_code == 200
93 assert response.status_code == 200
94 assert 'exist yet' in response.body
94 assert 'exist yet' in response.body
95
95
96 def test_index_with_integrations(self, app, test_repo_group,
96 def test_index_with_integrations(self, app, test_repo_group,
97 repogroup_integration_stub):
97 repogroup_integration_stub):
98 url = '/{repo_group_name}/settings/integrations'.format(
98 url = '/{repo_group_name}/settings/integrations'.format(
99 repo_group_name=test_repo_group.group_name)
99 repo_group_name=test_repo_group.group_name)
100
100
101 stub_name = repogroup_integration_stub.name
101 stub_name = repogroup_integration_stub.name
102 response = app.get(url)
102 response = app.get(url)
103
103
104 assert response.status_code == 200
104 assert response.status_code == 200
105 assert 'exist yet' not in response.body
105 assert 'exist yet' not in response.body
106 assert stub_name in response.body
106 assert stub_name in response.body
107
107
108 def test_new_integration_page(self, app, test_repo_group):
108 def test_new_integration_page(self, app, test_repo_group):
109 repo_group_name = test_repo_group.group_name
109 repo_group_name = test_repo_group.group_name
110 url = '/{repo_group_name}/settings/integrations/new'.format(
110 url = '/{repo_group_name}/settings/integrations/new'.format(
111 repo_group_name=test_repo_group.group_name)
111 repo_group_name=test_repo_group.group_name)
112
112
113 response = app.get(url)
113 response = app.get(url)
114
114
115 assert response.status_code == 200
115 assert response.status_code == 200
116
116
117 for integration_key in integration_type_registry:
117 for integration_key in integration_type_registry:
118 nurl = ('/{repo_group_name}/settings/integrations'
118 nurl = ('/{repo_group_name}/settings/integrations'
119 '/{integration}/new').format(
119 '/{integration}/new').format(
120 repo_group_name=repo_group_name,
120 repo_group_name=repo_group_name,
121 integration=integration_key)
121 integration=integration_key)
122
122
123 assert nurl in response.body
123 assert nurl in response.body
124
124
125 @pytest.mark.parametrize(
125 @pytest.mark.parametrize(
126 'IntegrationType', integration_type_registry.values())
126 'IntegrationType', integration_type_registry.values())
127 def test_get_create_integration_page(self, app, test_repo_group,
127 def test_get_create_integration_page(self, app, test_repo_group,
128 IntegrationType):
128 IntegrationType):
129 repo_group_name = test_repo_group.group_name
129 repo_group_name = test_repo_group.group_name
130 url = ('/{repo_group_name}/settings/integrations/{integration_key}/new'
130 url = ('/{repo_group_name}/settings/integrations/{integration_key}/new'
131 ).format(repo_group_name=repo_group_name,
131 ).format(repo_group_name=repo_group_name,
132 integration_key=IntegrationType.key)
132 integration_key=IntegrationType.key)
133
133
134 response = app.get(url)
134 response = app.get(url)
135
135
136 assert response.status_code == 200
136 assert response.status_code == 200
137 assert IntegrationType.display_name in response.body
137 assert IntegrationType.display_name in response.body
138
138
139 def test_post_integration_page(self, app, test_repo_group, backend_random,
139 def test_post_integration_page(self, app, test_repo_group, backend_random,
140 StubIntegrationType, csrf_token):
140 StubIntegrationType, csrf_token):
141 repo_group_name = test_repo_group.group_name
141 repo_group_name = test_repo_group.group_name
142 url = ('/{repo_group_name}/settings/integrations/{integration_key}/new'
142 url = ('/{repo_group_name}/settings/integrations/{integration_key}/new'
143 ).format(repo_group_name=repo_group_name,
143 ).format(repo_group_name=repo_group_name,
144 integration_key=StubIntegrationType.key)
144 integration_key=StubIntegrationType.key)
145
145
146 _post_integration_test_helper(app, url, csrf_token, admin_view=False,
146 _post_integration_test_helper(app, url, csrf_token, admin_view=False,
147 repo=backend_random.repo, repo_group=test_repo_group)
147 repo=backend_random.repo, repo_group=test_repo_group)
148
148
149
149
150 class TestRepoIntegrationsView(TestIntegrationsView):
150 class TestRepoIntegrationsView(TestIntegrationsView):
151 def test_index_no_integrations(self, app, backend_random):
151 def test_index_no_integrations(self, app, backend_random):
152 url = '/{repo_name}/settings/integrations'.format(
152 url = '/{repo_name}/settings/integrations'.format(
153 repo_name=backend_random.repo.repo_name)
153 repo_name=backend_random.repo.repo_name)
154 response = app.get(url)
154 response = app.get(url)
155
155
156 assert response.status_code == 200
156 assert response.status_code == 200
157 assert 'exist yet' in response.body
157 assert 'exist yet' in response.body
158
158
159 def test_index_with_integrations(self, app, repo_integration_stub):
159 def test_index_with_integrations(self, app, repo_integration_stub):
160 url = '/{repo_name}/settings/integrations'.format(
160 url = '/{repo_name}/settings/integrations'.format(
161 repo_name=repo_integration_stub.repo.repo_name)
161 repo_name=repo_integration_stub.repo.repo_name)
162 stub_name = repo_integration_stub.name
162 stub_name = repo_integration_stub.name
163
163
164 response = app.get(url)
164 response = app.get(url)
165
165
166 assert response.status_code == 200
166 assert response.status_code == 200
167 assert stub_name in response.body
167 assert stub_name in response.body
168 assert 'exist yet' not in response.body
168 assert 'exist yet' not in response.body
169
169
170 def test_new_integration_page(self, app, backend_random):
170 def test_new_integration_page(self, app, backend_random):
171 repo_name = backend_random.repo.repo_name
171 repo_name = backend_random.repo.repo_name
172 url = '/{repo_name}/settings/integrations/new'.format(
172 url = '/{repo_name}/settings/integrations/new'.format(
173 repo_name=repo_name)
173 repo_name=repo_name)
174
174
175 response = app.get(url)
175 response = app.get(url)
176
176
177 assert response.status_code == 200
177 assert response.status_code == 200
178
178
179 for integration_key in integration_type_registry:
179 for integration_key in integration_type_registry:
180 nurl = ('/{repo_name}/settings/integrations'
180 nurl = ('/{repo_name}/settings/integrations'
181 '/{integration}/new').format(
181 '/{integration}/new').format(
182 repo_name=repo_name,
182 repo_name=repo_name,
183 integration=integration_key)
183 integration=integration_key)
184
184
185 assert nurl in response.body
185 assert nurl in response.body
186
186
187 @pytest.mark.parametrize(
187 @pytest.mark.parametrize(
188 'IntegrationType', integration_type_registry.values())
188 'IntegrationType', integration_type_registry.values())
189 def test_get_create_integration_page(self, app, backend_random,
189 def test_get_create_integration_page(self, app, backend_random,
190 IntegrationType):
190 IntegrationType):
191 repo_name = backend_random.repo.repo_name
191 repo_name = backend_random.repo.repo_name
192 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
192 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
193 repo_name=repo_name, integration_key=IntegrationType.key)
193 repo_name=repo_name, integration_key=IntegrationType.key)
194
194
195 response = app.get(url)
195 response = app.get(url)
196
196
197 assert response.status_code == 200
197 assert response.status_code == 200
198 assert IntegrationType.display_name in response.body
198 assert IntegrationType.display_name in response.body
199
199
200 def test_post_integration_page(self, app, backend_random, test_repo_group,
200 def test_post_integration_page(self, app, backend_random, test_repo_group,
201 StubIntegrationType, csrf_token):
201 StubIntegrationType, csrf_token):
202 repo_name = backend_random.repo.repo_name
202 repo_name = backend_random.repo.repo_name
203 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
203 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
204 repo_name=repo_name, integration_key=StubIntegrationType.key)
204 repo_name=repo_name, integration_key=StubIntegrationType.key)
205
205
206 _post_integration_test_helper(app, url, csrf_token, admin_view=False,
206 _post_integration_test_helper(app, url, csrf_token, admin_view=False,
207 repo=backend_random.repo, repo_group=test_repo_group)
207 repo=backend_random.repo, repo_group=test_repo_group)
208
208
209
209
210 def _post_integration_test_helper(app, url, csrf_token, repo, repo_group,
210 def _post_integration_test_helper(app, url, csrf_token, repo, repo_group,
211 admin_view):
211 admin_view):
212 """
212 """
213 Posts form data to create integration at the url given then deletes it and
213 Posts form data to create integration at the url given then deletes it and
214 checks if the redirect url is correct.
214 checks if the redirect url is correct.
215 """
215 """
216
216
217 app.post(url, params={}, status=403) # missing csrf check
217 app.post(url, params={}, status=403) # missing csrf check
218 response = app.post(url, params={'csrf_token': csrf_token})
218 response = app.post(url, params={'csrf_token': csrf_token})
219 assert response.status_code == 200
219 assert response.status_code == 200
220 assert 'Errors exist' in response.body
220 assert 'Errors exist' in response.body
221
221
222 scopes_destinations = [
222 scopes_destinations = [
223 ('global',
223 ('global',
224 ADMIN_PREFIX + '/integrations'),
224 ADMIN_PREFIX + '/integrations'),
225 ('root_repos',
225 ('root-repos',
226 ADMIN_PREFIX + '/integrations'),
226 ADMIN_PREFIX + '/integrations'),
227 ('repo:%s' % repo.repo_name,
227 ('repo:%s' % repo.repo_name,
228 '/%s/settings/integrations' % repo.repo_name),
228 '/%s/settings/integrations' % repo.repo_name),
229 ('repogroup:%s' % repo_group.group_name,
229 ('repogroup:%s' % repo_group.group_name,
230 '/%s/settings/integrations' % repo_group.group_name),
230 '/%s/settings/integrations' % repo_group.group_name),
231 ('repogroup-recursive:%s' % repo_group.group_name,
232 '/%s/settings/integrations' % repo_group.group_name),
231 ]
233 ]
232
234
233 for scope, destination in scopes_destinations:
235 for scope, destination in scopes_destinations:
234 if admin_view:
236 if admin_view:
235 destination = ADMIN_PREFIX + '/integrations'
237 destination = ADMIN_PREFIX + '/integrations'
236
238
237 form_data = [
239 form_data = [
238 ('csrf_token', csrf_token),
240 ('csrf_token', csrf_token),
239 ('__start__', 'options:mapping'),
241 ('__start__', 'options:mapping'),
240 ('name', 'test integration'),
242 ('name', 'test integration'),
241 ('scope', scope),
243 ('scope', scope),
242 ('enabled', 'true'),
244 ('enabled', 'true'),
243 ('__end__', 'options:mapping'),
245 ('__end__', 'options:mapping'),
244 ('__start__', 'settings:mapping'),
246 ('__start__', 'settings:mapping'),
245 ('test_int_field', '34'),
247 ('test_int_field', '34'),
246 ('test_string_field', ''), # empty value on purpose as it's required
248 ('test_string_field', ''), # empty value on purpose as it's required
247 ('__end__', 'settings:mapping'),
249 ('__end__', 'settings:mapping'),
248 ]
250 ]
249 errors_response = app.post(url, form_data)
251 errors_response = app.post(url, form_data)
250 assert 'Errors exist' in errors_response.body
252 assert 'Errors exist' in errors_response.body
251
253
252 form_data[-2] = ('test_string_field', 'data!')
254 form_data[-2] = ('test_string_field', 'data!')
253 assert Session().query(Integration).count() == 0
255 assert Session().query(Integration).count() == 0
254 created_response = app.post(url, form_data)
256 created_response = app.post(url, form_data)
255 assert Session().query(Integration).count() == 1
257 assert Session().query(Integration).count() == 1
256
258
257 delete_response = app.post(
259 delete_response = app.post(
258 created_response.location,
260 created_response.location,
259 params={'csrf_token': csrf_token, 'delete': 'delete'})
261 params={'csrf_token': csrf_token, 'delete': 'delete'})
260
262
261 assert Session().query(Integration).count() == 0
263 assert Session().query(Integration).count() == 0
262 assert delete_response.location.endswith(destination)
264 assert delete_response.location.endswith(destination)
@@ -1,192 +1,222 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import pytest
22 import pytest
23
23
24 from rhodecode import events
24 from rhodecode import events
25 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.model.db import Session, Integration
26 from rhodecode.model.db import Session, Integration
27 from rhodecode.model.integration import IntegrationModel
27 from rhodecode.model.integration import IntegrationModel
28 from rhodecode.integrations.types.base import IntegrationTypeBase
28 from rhodecode.integrations.types.base import IntegrationTypeBase
29
29
30
30
31 class TestDeleteScopesDeletesIntegrations(object):
31 class TestDeleteScopesDeletesIntegrations(object):
32 def test_delete_repo_with_integration_deletes_integration(self,
32 def test_delete_repo_with_integration_deletes_integration(self,
33 repo_integration_stub):
33 repo_integration_stub):
34 Session().delete(repo_integration_stub.repo)
34 Session().delete(repo_integration_stub.repo)
35 Session().commit()
35 Session().commit()
36 Session().expire_all()
36 Session().expire_all()
37 integration = Integration.get(repo_integration_stub.integration_id)
37 integration = Integration.get(repo_integration_stub.integration_id)
38 assert integration is None
38 assert integration is None
39
39
40
40
41 def test_delete_repo_group_with_integration_deletes_integration(self,
41 def test_delete_repo_group_with_integration_deletes_integration(self,
42 repogroup_integration_stub):
42 repogroup_integration_stub):
43 Session().delete(repogroup_integration_stub.repo_group)
43 Session().delete(repogroup_integration_stub.repo_group)
44 Session().commit()
44 Session().commit()
45 Session().expire_all()
45 Session().expire_all()
46 integration = Integration.get(repogroup_integration_stub.integration_id)
46 integration = Integration.get(repogroup_integration_stub.integration_id)
47 assert integration is None
47 assert integration is None
48
48
49
49
50 @pytest.fixture
50 @pytest.fixture
51 def integration_repos(request, StubIntegrationType, stub_integration_settings):
51 def integration_repos(request, StubIntegrationType, stub_integration_settings):
52 """
52 """
53 Create repositories and integrations for testing, and destroy them after
53 Create repositories and integrations for testing, and destroy them after
54
55 Structure:
56 root_repo
57 parent_group/
58 parent_repo
59 child_group/
60 child_repo
61 other_group/
62 other_repo
54 """
63 """
55 fixture = Fixture()
64 fixture = Fixture()
56
65
57 repo_group_1_id = 'int_test_repo_group_1_%s' % time.time()
66
58 repo_group_1 = fixture.create_repo_group(repo_group_1_id)
67 parent_group_id = 'int_test_parent_group_%s' % time.time()
59 repo_group_2_id = 'int_test_repo_group_2_%s' % time.time()
68 parent_group = fixture.create_repo_group(parent_group_id)
60 repo_group_2 = fixture.create_repo_group(repo_group_2_id)
69
70 other_group_id = 'int_test_other_group_%s' % time.time()
71 other_group = fixture.create_repo_group(other_group_id)
61
72
62 repo_1_id = 'int_test_repo_1_%s' % time.time()
73 child_group_id = (
63 repo_1 = fixture.create_repo(repo_1_id, repo_group=repo_group_1)
74 parent_group_id + '/' + 'int_test_child_group_%s' % time.time())
64 repo_2_id = 'int_test_repo_2_%s' % time.time()
75 child_group = fixture.create_repo_group(child_group_id)
65 repo_2 = fixture.create_repo(repo_2_id, repo_group=repo_group_2)
76
77 parent_repo_id = 'int_test_parent_repo_%s' % time.time()
78 parent_repo = fixture.create_repo(parent_repo_id, repo_group=parent_group)
79
80 child_repo_id = 'int_test_child_repo_%s' % time.time()
81 child_repo = fixture.create_repo(child_repo_id, repo_group=child_group)
82
83 other_repo_id = 'int_test_other_repo_%s' % time.time()
84 other_repo = fixture.create_repo(other_repo_id, repo_group=other_group)
66
85
67 root_repo_id = 'int_test_repo_root_%s' % time.time()
86 root_repo_id = 'int_test_repo_root_%s' % time.time()
68 root_repo = fixture.create_repo(root_repo_id)
87 root_repo = fixture.create_repo(root_repo_id)
69
88
70 integration_global = IntegrationModel().create(
89 integrations = {}
71 StubIntegrationType, settings=stub_integration_settings,
90 for name, repo, repo_group, child_repos_only in [
72 enabled=True, name='test global integration', scope='global')
91 ('global', None, None, None),
73 integration_root_repos = IntegrationModel().create(
92 ('root_repos', None, None, True),
74 StubIntegrationType, settings=stub_integration_settings,
93 ('parent_repo', parent_repo, None, None),
75 enabled=True, name='test root repos integration', scope='root_repos')
94 ('child_repo', child_repo, None, None),
76 integration_repo_1 = IntegrationModel().create(
95 ('other_repo', other_repo, None, None),
77 StubIntegrationType, settings=stub_integration_settings,
96 ('root_repo', root_repo, None, None),
78 enabled=True, name='test repo 1 integration', scope=repo_1)
97 ('parent_group', None, parent_group, True),
79 integration_repo_group_1 = IntegrationModel().create(
98 ('parent_group_recursive', None, parent_group, False),
80 StubIntegrationType, settings=stub_integration_settings,
99 ('child_group', None, child_group, True),
81 enabled=True, name='test repo group 1 integration', scope=repo_group_1)
100 ('child_group_recursive', None, child_group, False),
82 integration_repo_2 = IntegrationModel().create(
101 ('other_group', None, other_group, True),
83 StubIntegrationType, settings=stub_integration_settings,
102 ('other_group_recursive', None, other_group, False),
84 enabled=True, name='test repo 2 integration', scope=repo_2)
103 ]:
85 integration_repo_group_2 = IntegrationModel().create(
104 integrations[name] = IntegrationModel().create(
86 StubIntegrationType, settings=stub_integration_settings,
105 StubIntegrationType, settings=stub_integration_settings,
87 enabled=True, name='test repo group 2 integration', scope=repo_group_2)
106 enabled=True, name='test %s integration' % name,
107 repo=repo, repo_group=repo_group, child_repos_only=child_repos_only)
88
108
89 Session().commit()
109 Session().commit()
90
110
91 def _cleanup():
111 def _cleanup():
92 Session().delete(integration_global)
112 for integration in integrations.values():
93 Session().delete(integration_root_repos)
113 Session.delete(integration)
94 Session().delete(integration_repo_1)
114
95 Session().delete(integration_repo_group_1)
96 Session().delete(integration_repo_2)
97 Session().delete(integration_repo_group_2)
98 fixture.destroy_repo(root_repo)
115 fixture.destroy_repo(root_repo)
99 fixture.destroy_repo(repo_1)
116 fixture.destroy_repo(child_repo)
100 fixture.destroy_repo(repo_2)
117 fixture.destroy_repo(parent_repo)
101 fixture.destroy_repo_group(repo_group_1)
118 fixture.destroy_repo(other_repo)
102 fixture.destroy_repo_group(repo_group_2)
119 fixture.destroy_repo_group(child_group)
120 fixture.destroy_repo_group(parent_group)
121 fixture.destroy_repo_group(other_group)
103
122
104 request.addfinalizer(_cleanup)
123 request.addfinalizer(_cleanup)
105
124
106 return {
125 return {
126 'integrations': integrations,
107 'repos': {
127 'repos': {
108 'repo_1': repo_1,
109 'repo_2': repo_2,
110 'root_repo': root_repo,
128 'root_repo': root_repo,
111 },
129 'other_repo': other_repo,
112 'repo_groups': {
130 'parent_repo': parent_repo,
113 'repo_group_1': repo_group_1,
131 'child_repo': child_repo,
114 'repo_group_2': repo_group_2,
115 },
116 'integrations': {
117 'global': integration_global,
118 'root_repos': integration_root_repos,
119 'repo_1': integration_repo_1,
120 'repo_2': integration_repo_2,
121 'repo_group_1': integration_repo_group_1,
122 'repo_group_2': integration_repo_group_2,
123 }
132 }
124 }
133 }
125
134
126
135
127 def test_enabled_integration_repo_scopes(integration_repos):
136 def test_enabled_integration_repo_scopes(integration_repos):
128 integrations = integration_repos['integrations']
137 integrations = integration_repos['integrations']
129 repos = integration_repos['repos']
138 repos = integration_repos['repos']
130
139
131 triggered_integrations = IntegrationModel().get_for_event(
140 triggered_integrations = IntegrationModel().get_for_event(
132 events.RepoEvent(repos['root_repo']))
141 events.RepoEvent(repos['root_repo']))
133
142
134 assert triggered_integrations == [
143 assert triggered_integrations == [
135 integrations['global'],
144 integrations['global'],
136 integrations['root_repos']
145 integrations['root_repos'],
146 integrations['root_repo'],
147 ]
148
149
150 triggered_integrations = IntegrationModel().get_for_event(
151 events.RepoEvent(repos['other_repo']))
152
153 assert triggered_integrations == [
154 integrations['global'],
155 integrations['other_repo'],
156 integrations['other_group'],
157 integrations['other_group_recursive'],
137 ]
158 ]
138
159
139
160
140 triggered_integrations = IntegrationModel().get_for_event(
161 triggered_integrations = IntegrationModel().get_for_event(
141 events.RepoEvent(repos['repo_1']))
162 events.RepoEvent(repos['parent_repo']))
142
163
143 assert triggered_integrations == [
164 assert triggered_integrations == [
144 integrations['global'],
165 integrations['global'],
145 integrations['repo_1'],
166 integrations['parent_repo'],
146 integrations['repo_group_1']
167 integrations['parent_group'],
168 integrations['parent_group_recursive'],
147 ]
169 ]
148
170
149
150 triggered_integrations = IntegrationModel().get_for_event(
171 triggered_integrations = IntegrationModel().get_for_event(
151 events.RepoEvent(repos['repo_2']))
172 events.RepoEvent(repos['child_repo']))
152
173
153 assert triggered_integrations == [
174 assert triggered_integrations == [
154 integrations['global'],
175 integrations['global'],
155 integrations['repo_2'],
176 integrations['child_repo'],
156 integrations['repo_group_2'],
177 integrations['parent_group_recursive'],
178 integrations['child_group'],
179 integrations['child_group_recursive'],
157 ]
180 ]
158
181
159
182
160 def test_disabled_integration_repo_scopes(integration_repos):
183 def test_disabled_integration_repo_scopes(integration_repos):
161 integrations = integration_repos['integrations']
184 integrations = integration_repos['integrations']
162 repos = integration_repos['repos']
185 repos = integration_repos['repos']
163
186
164 for integration in integrations.values():
187 for integration in integrations.values():
165 integration.enabled = False
188 integration.enabled = False
166 Session().commit()
189 Session().commit()
167
190
168 triggered_integrations = IntegrationModel().get_for_event(
191 triggered_integrations = IntegrationModel().get_for_event(
169 events.RepoEvent(repos['root_repo']))
192 events.RepoEvent(repos['root_repo']))
170
193
171 assert triggered_integrations == []
194 assert triggered_integrations == []
172
195
173
196
174 triggered_integrations = IntegrationModel().get_for_event(
197 triggered_integrations = IntegrationModel().get_for_event(
175 events.RepoEvent(repos['repo_1']))
198 events.RepoEvent(repos['parent_repo']))
176
199
177 assert triggered_integrations == []
200 assert triggered_integrations == []
178
201
179
202
180 triggered_integrations = IntegrationModel().get_for_event(
203 triggered_integrations = IntegrationModel().get_for_event(
181 events.RepoEvent(repos['repo_2']))
204 events.RepoEvent(repos['child_repo']))
182
205
183 assert triggered_integrations == []
206 assert triggered_integrations == []
184
207
185
208
209 triggered_integrations = IntegrationModel().get_for_event(
210 events.RepoEvent(repos['other_repo']))
211
212 assert triggered_integrations == []
213
214
215
186 def test_enabled_non_repo_integrations(integration_repos):
216 def test_enabled_non_repo_integrations(integration_repos):
187 integrations = integration_repos['integrations']
217 integrations = integration_repos['integrations']
188
218
189 triggered_integrations = IntegrationModel().get_for_event(
219 triggered_integrations = IntegrationModel().get_for_event(
190 events.UserPreCreate({}))
220 events.UserPreCreate({}))
191
221
192 assert triggered_integrations == [integrations['global']]
222 assert triggered_integrations == [integrations['global']]
@@ -1,120 +1,171 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import colander
21 import colander
22 import pytest
22 import pytest
23
23
24 from rhodecode.model import validation_schema
24 from rhodecode.model import validation_schema
25
25
26 from rhodecode.integrations import integration_type_registry
26 from rhodecode.integrations import integration_type_registry
27 from rhodecode.integrations.types.base import IntegrationTypeBase
27 from rhodecode.integrations.types.base import IntegrationTypeBase
28 from rhodecode.model.validation_schema.schemas.integration_schema import (
28 from rhodecode.model.validation_schema.schemas.integration_schema import (
29 make_integration_schema
29 make_integration_schema
30 )
30 )
31
31
32
32
33 @pytest.mark.usefixtures('app', 'autologin_user')
33 @pytest.mark.usefixtures('app', 'autologin_user')
34 class TestIntegrationSchema(object):
34 class TestIntegrationSchema(object):
35
35
36 def test_deserialize_integration_schema_perms(self, backend_random,
36 def test_deserialize_integration_schema_perms(self, backend_random,
37 test_repo_group,
37 test_repo_group,
38 StubIntegrationType):
38 StubIntegrationType):
39
39
40 repo = backend_random.repo
40 repo = backend_random.repo
41 repo_group = test_repo_group
41 repo_group = test_repo_group
42
42
43
43
44 empty_perms_dict = {
44 empty_perms_dict = {
45 'global': [],
45 'global': [],
46 'repositories': {},
46 'repositories': {},
47 'repositories_groups': {},
47 'repositories_groups': {},
48 }
48 }
49
49
50 perms_tests = {
50 perms_tests = [
51 ('repo:%s' % repo.repo_name, repo): [
51 (
52 ({}, False),
52 'repo:%s' % repo.repo_name,
53 ({'global': ['hg.admin']}, True),
53 {
54 ({'global': []}, False),
54 'child_repos_only': None,
55 ({'repositories': {repo.repo_name: 'repository.admin'}}, True),
55 'repo_group': None,
56 ({'repositories': {repo.repo_name: 'repository.read'}}, False),
56 'repo': repo,
57 ({'repositories': {repo.repo_name: 'repository.write'}}, False),
57 },
58 ({'repositories': {repo.repo_name: 'repository.none'}}, False),
58 [
59 ],
59 ({}, False),
60 ('repogroup:%s' % repo_group.group_name, repo_group): [
60 ({'global': ['hg.admin']}, True),
61 ({}, False),
61 ({'global': []}, False),
62 ({'global': ['hg.admin']}, True),
62 ({'repositories': {repo.repo_name: 'repository.admin'}}, True),
63 ({'global': []}, False),
63 ({'repositories': {repo.repo_name: 'repository.read'}}, False),
64 ({'repositories_groups':
64 ({'repositories': {repo.repo_name: 'repository.write'}}, False),
65 {repo_group.group_name: 'group.admin'}}, True),
65 ({'repositories': {repo.repo_name: 'repository.none'}}, False),
66 ({'repositories_groups':
66 ]
67 {repo_group.group_name: 'group.read'}}, False),
67 ),
68 ({'repositories_groups':
68 (
69 {repo_group.group_name: 'group.write'}}, False),
69 'repogroup:%s' % repo_group.group_name,
70 ({'repositories_groups':
70 {
71 {repo_group.group_name: 'group.none'}}, False),
71 'repo': None,
72 ],
72 'repo_group': repo_group,
73 ('global', 'global'): [
73 'child_repos_only': True,
74 ({}, False),
74 },
75 ({'global': ['hg.admin']}, True),
75 [
76 ({'global': []}, False),
76 ({}, False),
77 ],
77 ({'global': ['hg.admin']}, True),
78 ('root_repos', 'root_repos'): [
78 ({'global': []}, False),
79 ({}, False),
79 ({'repositories_groups':
80 ({'global': ['hg.admin']}, True),
80 {repo_group.group_name: 'group.admin'}}, True),
81 ({'global': []}, False),
81 ({'repositories_groups':
82 ],
82 {repo_group.group_name: 'group.read'}}, False),
83 }
83 ({'repositories_groups':
84 {repo_group.group_name: 'group.write'}}, False),
85 ({'repositories_groups':
86 {repo_group.group_name: 'group.none'}}, False),
87 ]
88 ),
89 (
90 'repogroup-recursive:%s' % repo_group.group_name,
91 {
92 'repo': None,
93 'repo_group': repo_group,
94 'child_repos_only': False,
95 },
96 [
97 ({}, False),
98 ({'global': ['hg.admin']}, True),
99 ({'global': []}, False),
100 ({'repositories_groups':
101 {repo_group.group_name: 'group.admin'}}, True),
102 ({'repositories_groups':
103 {repo_group.group_name: 'group.read'}}, False),
104 ({'repositories_groups':
105 {repo_group.group_name: 'group.write'}}, False),
106 ({'repositories_groups':
107 {repo_group.group_name: 'group.none'}}, False),
108 ]
109 ),
110 (
111 'global',
112 {
113 'repo': None,
114 'repo_group': None,
115 'child_repos_only': False,
116 }, [
117 ({}, False),
118 ({'global': ['hg.admin']}, True),
119 ({'global': []}, False),
120 ]
121 ),
122 (
123 'root-repos',
124 {
125 'repo': None,
126 'repo_group': None,
127 'child_repos_only': True,
128 }, [
129 ({}, False),
130 ({'global': ['hg.admin']}, True),
131 ({'global': []}, False),
132 ]
133 ),
134 ]
84
135
85 for (scope_input, scope_output), perms_allowed in perms_tests.items():
136 for scope_input, scope_output, perms_allowed in perms_tests:
86 for perms_update, allowed in perms_allowed:
137 for perms_update, allowed in perms_allowed:
87 perms = dict(empty_perms_dict, **perms_update)
138 perms = dict(empty_perms_dict, **perms_update)
88
139
89 schema = make_integration_schema(
140 schema = make_integration_schema(
90 IntegrationType=StubIntegrationType
141 IntegrationType=StubIntegrationType
91 ).bind(permissions=perms)
142 ).bind(permissions=perms)
92
143
93 input_data = {
144 input_data = {
94 'options': {
145 'options': {
95 'enabled': 'true',
146 'enabled': 'true',
96 'scope': scope_input,
147 'scope': scope_input,
97 'name': 'test integration',
148 'name': 'test integration',
98 },
149 },
99 'settings': {
150 'settings': {
100 'test_string_field': 'stringy',
151 'test_string_field': 'stringy',
101 'test_int_field': '100',
152 'test_int_field': '100',
102 }
153 }
103 }
154 }
104
155
105 if not allowed:
156 if not allowed:
106 with pytest.raises(colander.Invalid):
157 with pytest.raises(colander.Invalid):
107 schema.deserialize(input_data)
158 schema.deserialize(input_data)
108 else:
159 else:
109 assert schema.deserialize(input_data) == {
160 assert schema.deserialize(input_data) == {
110 'options': {
161 'options': {
111 'enabled': True,
162 'enabled': True,
112 'scope': scope_output,
163 'scope': scope_output,
113 'name': 'test integration',
164 'name': 'test integration',
114 },
165 },
115 'settings': {
166 'settings': {
116 'test_string_field': 'stringy',
167 'test_string_field': 'stringy',
117 'test_int_field': 100,
168 'test_int_field': 100,
118 }
169 }
119 }
170 }
120
171
@@ -1,1760 +1,1779 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import collections
21 import collections
22 import datetime
22 import datetime
23 import hashlib
23 import hashlib
24 import os
24 import os
25 import re
25 import re
26 import pprint
26 import pprint
27 import shutil
27 import shutil
28 import socket
28 import socket
29 import subprocess
29 import subprocess
30 import time
30 import time
31 import uuid
31 import uuid
32
32
33 import mock
33 import mock
34 import pyramid.testing
34 import pyramid.testing
35 import pytest
35 import pytest
36 import colander
36 import colander
37 import requests
37 import requests
38 from webtest.app import TestApp
38 from webtest.app import TestApp
39
39
40 import rhodecode
40 import rhodecode
41 from rhodecode.model.changeset_status import ChangesetStatusModel
41 from rhodecode.model.changeset_status import ChangesetStatusModel
42 from rhodecode.model.comment import ChangesetCommentsModel
42 from rhodecode.model.comment import ChangesetCommentsModel
43 from rhodecode.model.db import (
43 from rhodecode.model.db import (
44 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
44 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
45 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, Integration)
45 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, Integration)
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.repo import RepoModel
48 from rhodecode.model.repo import RepoModel
49 from rhodecode.model.repo_group import RepoGroupModel
49 from rhodecode.model.repo_group import RepoGroupModel
50 from rhodecode.model.user import UserModel
50 from rhodecode.model.user import UserModel
51 from rhodecode.model.settings import VcsSettingsModel
51 from rhodecode.model.settings import VcsSettingsModel
52 from rhodecode.model.user_group import UserGroupModel
52 from rhodecode.model.user_group import UserGroupModel
53 from rhodecode.model.integration import IntegrationModel
53 from rhodecode.model.integration import IntegrationModel
54 from rhodecode.integrations import integration_type_registry
54 from rhodecode.integrations import integration_type_registry
55 from rhodecode.integrations.types.base import IntegrationTypeBase
55 from rhodecode.integrations.types.base import IntegrationTypeBase
56 from rhodecode.lib.utils import repo2db_mapper
56 from rhodecode.lib.utils import repo2db_mapper
57 from rhodecode.lib.vcs import create_vcsserver_proxy
57 from rhodecode.lib.vcs import create_vcsserver_proxy
58 from rhodecode.lib.vcs.backends import get_backend
58 from rhodecode.lib.vcs.backends import get_backend
59 from rhodecode.lib.vcs.nodes import FileNode
59 from rhodecode.lib.vcs.nodes import FileNode
60 from rhodecode.tests import (
60 from rhodecode.tests import (
61 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
61 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
62 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
62 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
63 TEST_USER_REGULAR_PASS)
63 TEST_USER_REGULAR_PASS)
64 from rhodecode.tests.fixture import Fixture
64 from rhodecode.tests.fixture import Fixture
65
65
66
66
67 def _split_comma(value):
67 def _split_comma(value):
68 return value.split(',')
68 return value.split(',')
69
69
70
70
71 def pytest_addoption(parser):
71 def pytest_addoption(parser):
72 parser.addoption(
72 parser.addoption(
73 '--keep-tmp-path', action='store_true',
73 '--keep-tmp-path', action='store_true',
74 help="Keep the test temporary directories")
74 help="Keep the test temporary directories")
75 parser.addoption(
75 parser.addoption(
76 '--backends', action='store', type=_split_comma,
76 '--backends', action='store', type=_split_comma,
77 default=['git', 'hg', 'svn'],
77 default=['git', 'hg', 'svn'],
78 help="Select which backends to test for backend specific tests.")
78 help="Select which backends to test for backend specific tests.")
79 parser.addoption(
79 parser.addoption(
80 '--dbs', action='store', type=_split_comma,
80 '--dbs', action='store', type=_split_comma,
81 default=['sqlite'],
81 default=['sqlite'],
82 help="Select which database to test for database specific tests. "
82 help="Select which database to test for database specific tests. "
83 "Possible options are sqlite,postgres,mysql")
83 "Possible options are sqlite,postgres,mysql")
84 parser.addoption(
84 parser.addoption(
85 '--appenlight', '--ae', action='store_true',
85 '--appenlight', '--ae', action='store_true',
86 help="Track statistics in appenlight.")
86 help="Track statistics in appenlight.")
87 parser.addoption(
87 parser.addoption(
88 '--appenlight-api-key', '--ae-key',
88 '--appenlight-api-key', '--ae-key',
89 help="API key for Appenlight.")
89 help="API key for Appenlight.")
90 parser.addoption(
90 parser.addoption(
91 '--appenlight-url', '--ae-url',
91 '--appenlight-url', '--ae-url',
92 default="https://ae.rhodecode.com",
92 default="https://ae.rhodecode.com",
93 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
93 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
94 parser.addoption(
94 parser.addoption(
95 '--sqlite-connection-string', action='store',
95 '--sqlite-connection-string', action='store',
96 default='', help="Connection string for the dbs tests with SQLite")
96 default='', help="Connection string for the dbs tests with SQLite")
97 parser.addoption(
97 parser.addoption(
98 '--postgres-connection-string', action='store',
98 '--postgres-connection-string', action='store',
99 default='', help="Connection string for the dbs tests with Postgres")
99 default='', help="Connection string for the dbs tests with Postgres")
100 parser.addoption(
100 parser.addoption(
101 '--mysql-connection-string', action='store',
101 '--mysql-connection-string', action='store',
102 default='', help="Connection string for the dbs tests with MySQL")
102 default='', help="Connection string for the dbs tests with MySQL")
103 parser.addoption(
103 parser.addoption(
104 '--repeat', type=int, default=100,
104 '--repeat', type=int, default=100,
105 help="Number of repetitions in performance tests.")
105 help="Number of repetitions in performance tests.")
106
106
107
107
108 def pytest_configure(config):
108 def pytest_configure(config):
109 # Appy the kombu patch early on, needed for test discovery on Python 2.7.11
109 # Appy the kombu patch early on, needed for test discovery on Python 2.7.11
110 from rhodecode.config import patches
110 from rhodecode.config import patches
111 patches.kombu_1_5_1_python_2_7_11()
111 patches.kombu_1_5_1_python_2_7_11()
112
112
113
113
114 def pytest_collection_modifyitems(session, config, items):
114 def pytest_collection_modifyitems(session, config, items):
115 # nottest marked, compare nose, used for transition from nose to pytest
115 # nottest marked, compare nose, used for transition from nose to pytest
116 remaining = [
116 remaining = [
117 i for i in items if getattr(i.obj, '__test__', True)]
117 i for i in items if getattr(i.obj, '__test__', True)]
118 items[:] = remaining
118 items[:] = remaining
119
119
120
120
121 def pytest_generate_tests(metafunc):
121 def pytest_generate_tests(metafunc):
122 # Support test generation based on --backend parameter
122 # Support test generation based on --backend parameter
123 if 'backend_alias' in metafunc.fixturenames:
123 if 'backend_alias' in metafunc.fixturenames:
124 backends = get_backends_from_metafunc(metafunc)
124 backends = get_backends_from_metafunc(metafunc)
125 scope = None
125 scope = None
126 if not backends:
126 if not backends:
127 pytest.skip("Not enabled for any of selected backends")
127 pytest.skip("Not enabled for any of selected backends")
128 metafunc.parametrize('backend_alias', backends, scope=scope)
128 metafunc.parametrize('backend_alias', backends, scope=scope)
129 elif hasattr(metafunc.function, 'backends'):
129 elif hasattr(metafunc.function, 'backends'):
130 backends = get_backends_from_metafunc(metafunc)
130 backends = get_backends_from_metafunc(metafunc)
131 if not backends:
131 if not backends:
132 pytest.skip("Not enabled for any of selected backends")
132 pytest.skip("Not enabled for any of selected backends")
133
133
134
134
135 def get_backends_from_metafunc(metafunc):
135 def get_backends_from_metafunc(metafunc):
136 requested_backends = set(metafunc.config.getoption('--backends'))
136 requested_backends = set(metafunc.config.getoption('--backends'))
137 if hasattr(metafunc.function, 'backends'):
137 if hasattr(metafunc.function, 'backends'):
138 # Supported backends by this test function, created from
138 # Supported backends by this test function, created from
139 # pytest.mark.backends
139 # pytest.mark.backends
140 backends = metafunc.function.backends.args
140 backends = metafunc.function.backends.args
141 elif hasattr(metafunc.cls, 'backend_alias'):
141 elif hasattr(metafunc.cls, 'backend_alias'):
142 # Support class attribute "backend_alias", this is mainly
142 # Support class attribute "backend_alias", this is mainly
143 # for legacy reasons for tests not yet using pytest.mark.backends
143 # for legacy reasons for tests not yet using pytest.mark.backends
144 backends = [metafunc.cls.backend_alias]
144 backends = [metafunc.cls.backend_alias]
145 else:
145 else:
146 backends = metafunc.config.getoption('--backends')
146 backends = metafunc.config.getoption('--backends')
147 return requested_backends.intersection(backends)
147 return requested_backends.intersection(backends)
148
148
149
149
150 @pytest.fixture(scope='session', autouse=True)
150 @pytest.fixture(scope='session', autouse=True)
151 def activate_example_rcextensions(request):
151 def activate_example_rcextensions(request):
152 """
152 """
153 Patch in an example rcextensions module which verifies passed in kwargs.
153 Patch in an example rcextensions module which verifies passed in kwargs.
154 """
154 """
155 from rhodecode.tests.other import example_rcextensions
155 from rhodecode.tests.other import example_rcextensions
156
156
157 old_extensions = rhodecode.EXTENSIONS
157 old_extensions = rhodecode.EXTENSIONS
158 rhodecode.EXTENSIONS = example_rcextensions
158 rhodecode.EXTENSIONS = example_rcextensions
159
159
160 @request.addfinalizer
160 @request.addfinalizer
161 def cleanup():
161 def cleanup():
162 rhodecode.EXTENSIONS = old_extensions
162 rhodecode.EXTENSIONS = old_extensions
163
163
164
164
165 @pytest.fixture
165 @pytest.fixture
166 def capture_rcextensions():
166 def capture_rcextensions():
167 """
167 """
168 Returns the recorded calls to entry points in rcextensions.
168 Returns the recorded calls to entry points in rcextensions.
169 """
169 """
170 calls = rhodecode.EXTENSIONS.calls
170 calls = rhodecode.EXTENSIONS.calls
171 calls.clear()
171 calls.clear()
172 # Note: At this moment, it is still the empty dict, but that will
172 # Note: At this moment, it is still the empty dict, but that will
173 # be filled during the test run and since it is a reference this
173 # be filled during the test run and since it is a reference this
174 # is enough to make it work.
174 # is enough to make it work.
175 return calls
175 return calls
176
176
177
177
178 @pytest.fixture(scope='session')
178 @pytest.fixture(scope='session')
179 def http_environ_session():
179 def http_environ_session():
180 """
180 """
181 Allow to use "http_environ" in session scope.
181 Allow to use "http_environ" in session scope.
182 """
182 """
183 return http_environ(
183 return http_environ(
184 http_host_stub=http_host_stub())
184 http_host_stub=http_host_stub())
185
185
186
186
187 @pytest.fixture
187 @pytest.fixture
188 def http_host_stub():
188 def http_host_stub():
189 """
189 """
190 Value of HTTP_HOST in the test run.
190 Value of HTTP_HOST in the test run.
191 """
191 """
192 return 'test.example.com:80'
192 return 'test.example.com:80'
193
193
194
194
195 @pytest.fixture
195 @pytest.fixture
196 def http_environ(http_host_stub):
196 def http_environ(http_host_stub):
197 """
197 """
198 HTTP extra environ keys.
198 HTTP extra environ keys.
199
199
200 User by the test application and as well for setting up the pylons
200 User by the test application and as well for setting up the pylons
201 environment. In the case of the fixture "app" it should be possible
201 environment. In the case of the fixture "app" it should be possible
202 to override this for a specific test case.
202 to override this for a specific test case.
203 """
203 """
204 return {
204 return {
205 'SERVER_NAME': http_host_stub.split(':')[0],
205 'SERVER_NAME': http_host_stub.split(':')[0],
206 'SERVER_PORT': http_host_stub.split(':')[1],
206 'SERVER_PORT': http_host_stub.split(':')[1],
207 'HTTP_HOST': http_host_stub,
207 'HTTP_HOST': http_host_stub,
208 }
208 }
209
209
210
210
211 @pytest.fixture(scope='function')
211 @pytest.fixture(scope='function')
212 def app(request, pylonsapp, http_environ):
212 def app(request, pylonsapp, http_environ):
213 app = TestApp(
213 app = TestApp(
214 pylonsapp,
214 pylonsapp,
215 extra_environ=http_environ)
215 extra_environ=http_environ)
216 if request.cls:
216 if request.cls:
217 request.cls.app = app
217 request.cls.app = app
218 return app
218 return app
219
219
220
220
221 @pytest.fixture()
221 @pytest.fixture()
222 def app_settings(pylonsapp, pylons_config):
222 def app_settings(pylonsapp, pylons_config):
223 """
223 """
224 Settings dictionary used to create the app.
224 Settings dictionary used to create the app.
225
225
226 Parses the ini file and passes the result through the sanitize and apply
226 Parses the ini file and passes the result through the sanitize and apply
227 defaults mechanism in `rhodecode.config.middleware`.
227 defaults mechanism in `rhodecode.config.middleware`.
228 """
228 """
229 from paste.deploy.loadwsgi import loadcontext, APP
229 from paste.deploy.loadwsgi import loadcontext, APP
230 from rhodecode.config.middleware import (
230 from rhodecode.config.middleware import (
231 sanitize_settings_and_apply_defaults)
231 sanitize_settings_and_apply_defaults)
232 context = loadcontext(APP, 'config:' + pylons_config)
232 context = loadcontext(APP, 'config:' + pylons_config)
233 settings = sanitize_settings_and_apply_defaults(context.config())
233 settings = sanitize_settings_and_apply_defaults(context.config())
234 return settings
234 return settings
235
235
236
236
237 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
237 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
238
238
239
239
240 def _autologin_user(app, *args):
240 def _autologin_user(app, *args):
241 session = login_user_session(app, *args)
241 session = login_user_session(app, *args)
242 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
242 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
243 return LoginData(csrf_token, session['rhodecode_user'])
243 return LoginData(csrf_token, session['rhodecode_user'])
244
244
245
245
246 @pytest.fixture
246 @pytest.fixture
247 def autologin_user(app):
247 def autologin_user(app):
248 """
248 """
249 Utility fixture which makes sure that the admin user is logged in
249 Utility fixture which makes sure that the admin user is logged in
250 """
250 """
251 return _autologin_user(app)
251 return _autologin_user(app)
252
252
253
253
254 @pytest.fixture
254 @pytest.fixture
255 def autologin_regular_user(app):
255 def autologin_regular_user(app):
256 """
256 """
257 Utility fixture which makes sure that the regular user is logged in
257 Utility fixture which makes sure that the regular user is logged in
258 """
258 """
259 return _autologin_user(
259 return _autologin_user(
260 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
260 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
261
261
262
262
263 @pytest.fixture(scope='function')
263 @pytest.fixture(scope='function')
264 def csrf_token(request, autologin_user):
264 def csrf_token(request, autologin_user):
265 return autologin_user.csrf_token
265 return autologin_user.csrf_token
266
266
267
267
268 @pytest.fixture(scope='function')
268 @pytest.fixture(scope='function')
269 def xhr_header(request):
269 def xhr_header(request):
270 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
270 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
271
271
272
272
273 @pytest.fixture
273 @pytest.fixture
274 def real_crypto_backend(monkeypatch):
274 def real_crypto_backend(monkeypatch):
275 """
275 """
276 Switch the production crypto backend on for this test.
276 Switch the production crypto backend on for this test.
277
277
278 During the test run the crypto backend is replaced with a faster
278 During the test run the crypto backend is replaced with a faster
279 implementation based on the MD5 algorithm.
279 implementation based on the MD5 algorithm.
280 """
280 """
281 monkeypatch.setattr(rhodecode, 'is_test', False)
281 monkeypatch.setattr(rhodecode, 'is_test', False)
282
282
283
283
284 @pytest.fixture(scope='class')
284 @pytest.fixture(scope='class')
285 def index_location(request, pylonsapp):
285 def index_location(request, pylonsapp):
286 index_location = pylonsapp.config['app_conf']['search.location']
286 index_location = pylonsapp.config['app_conf']['search.location']
287 if request.cls:
287 if request.cls:
288 request.cls.index_location = index_location
288 request.cls.index_location = index_location
289 return index_location
289 return index_location
290
290
291
291
292 @pytest.fixture(scope='session', autouse=True)
292 @pytest.fixture(scope='session', autouse=True)
293 def tests_tmp_path(request):
293 def tests_tmp_path(request):
294 """
294 """
295 Create temporary directory to be used during the test session.
295 Create temporary directory to be used during the test session.
296 """
296 """
297 if not os.path.exists(TESTS_TMP_PATH):
297 if not os.path.exists(TESTS_TMP_PATH):
298 os.makedirs(TESTS_TMP_PATH)
298 os.makedirs(TESTS_TMP_PATH)
299
299
300 if not request.config.getoption('--keep-tmp-path'):
300 if not request.config.getoption('--keep-tmp-path'):
301 @request.addfinalizer
301 @request.addfinalizer
302 def remove_tmp_path():
302 def remove_tmp_path():
303 shutil.rmtree(TESTS_TMP_PATH)
303 shutil.rmtree(TESTS_TMP_PATH)
304
304
305 return TESTS_TMP_PATH
305 return TESTS_TMP_PATH
306
306
307
307
308 @pytest.fixture(scope='session', autouse=True)
308 @pytest.fixture(scope='session', autouse=True)
309 def patch_pyro_request_scope_proxy_factory(request):
309 def patch_pyro_request_scope_proxy_factory(request):
310 """
310 """
311 Patch the pyro proxy factory to always use the same dummy request object
311 Patch the pyro proxy factory to always use the same dummy request object
312 when under test. This will return the same pyro proxy on every call.
312 when under test. This will return the same pyro proxy on every call.
313 """
313 """
314 dummy_request = pyramid.testing.DummyRequest()
314 dummy_request = pyramid.testing.DummyRequest()
315
315
316 def mocked_call(self, request=None):
316 def mocked_call(self, request=None):
317 return self.getProxy(request=dummy_request)
317 return self.getProxy(request=dummy_request)
318
318
319 patcher = mock.patch(
319 patcher = mock.patch(
320 'rhodecode.lib.vcs.client.RequestScopeProxyFactory.__call__',
320 'rhodecode.lib.vcs.client.RequestScopeProxyFactory.__call__',
321 new=mocked_call)
321 new=mocked_call)
322 patcher.start()
322 patcher.start()
323
323
324 @request.addfinalizer
324 @request.addfinalizer
325 def undo_patching():
325 def undo_patching():
326 patcher.stop()
326 patcher.stop()
327
327
328
328
329 @pytest.fixture
329 @pytest.fixture
330 def test_repo_group(request):
330 def test_repo_group(request):
331 """
331 """
332 Create a temporary repository group, and destroy it after
332 Create a temporary repository group, and destroy it after
333 usage automatically
333 usage automatically
334 """
334 """
335 fixture = Fixture()
335 fixture = Fixture()
336 repogroupid = 'test_repo_group_%s' % int(time.time())
336 repogroupid = 'test_repo_group_%s' % int(time.time())
337 repo_group = fixture.create_repo_group(repogroupid)
337 repo_group = fixture.create_repo_group(repogroupid)
338
338
339 def _cleanup():
339 def _cleanup():
340 fixture.destroy_repo_group(repogroupid)
340 fixture.destroy_repo_group(repogroupid)
341
341
342 request.addfinalizer(_cleanup)
342 request.addfinalizer(_cleanup)
343 return repo_group
343 return repo_group
344
344
345
345
346 @pytest.fixture
346 @pytest.fixture
347 def test_user_group(request):
347 def test_user_group(request):
348 """
348 """
349 Create a temporary user group, and destroy it after
349 Create a temporary user group, and destroy it after
350 usage automatically
350 usage automatically
351 """
351 """
352 fixture = Fixture()
352 fixture = Fixture()
353 usergroupid = 'test_user_group_%s' % int(time.time())
353 usergroupid = 'test_user_group_%s' % int(time.time())
354 user_group = fixture.create_user_group(usergroupid)
354 user_group = fixture.create_user_group(usergroupid)
355
355
356 def _cleanup():
356 def _cleanup():
357 fixture.destroy_user_group(user_group)
357 fixture.destroy_user_group(user_group)
358
358
359 request.addfinalizer(_cleanup)
359 request.addfinalizer(_cleanup)
360 return user_group
360 return user_group
361
361
362
362
363 @pytest.fixture(scope='session')
363 @pytest.fixture(scope='session')
364 def test_repo(request):
364 def test_repo(request):
365 container = TestRepoContainer()
365 container = TestRepoContainer()
366 request.addfinalizer(container._cleanup)
366 request.addfinalizer(container._cleanup)
367 return container
367 return container
368
368
369
369
370 class TestRepoContainer(object):
370 class TestRepoContainer(object):
371 """
371 """
372 Container for test repositories which are used read only.
372 Container for test repositories which are used read only.
373
373
374 Repositories will be created on demand and re-used during the lifetime
374 Repositories will be created on demand and re-used during the lifetime
375 of this object.
375 of this object.
376
376
377 Usage to get the svn test repository "minimal"::
377 Usage to get the svn test repository "minimal"::
378
378
379 test_repo = TestContainer()
379 test_repo = TestContainer()
380 repo = test_repo('minimal', 'svn')
380 repo = test_repo('minimal', 'svn')
381
381
382 """
382 """
383
383
384 dump_extractors = {
384 dump_extractors = {
385 'git': utils.extract_git_repo_from_dump,
385 'git': utils.extract_git_repo_from_dump,
386 'hg': utils.extract_hg_repo_from_dump,
386 'hg': utils.extract_hg_repo_from_dump,
387 'svn': utils.extract_svn_repo_from_dump,
387 'svn': utils.extract_svn_repo_from_dump,
388 }
388 }
389
389
390 def __init__(self):
390 def __init__(self):
391 self._cleanup_repos = []
391 self._cleanup_repos = []
392 self._fixture = Fixture()
392 self._fixture = Fixture()
393 self._repos = {}
393 self._repos = {}
394
394
395 def __call__(self, dump_name, backend_alias):
395 def __call__(self, dump_name, backend_alias):
396 key = (dump_name, backend_alias)
396 key = (dump_name, backend_alias)
397 if key not in self._repos:
397 if key not in self._repos:
398 repo = self._create_repo(dump_name, backend_alias)
398 repo = self._create_repo(dump_name, backend_alias)
399 self._repos[key] = repo.repo_id
399 self._repos[key] = repo.repo_id
400 return Repository.get(self._repos[key])
400 return Repository.get(self._repos[key])
401
401
402 def _create_repo(self, dump_name, backend_alias):
402 def _create_repo(self, dump_name, backend_alias):
403 repo_name = '%s-%s' % (backend_alias, dump_name)
403 repo_name = '%s-%s' % (backend_alias, dump_name)
404 backend_class = get_backend(backend_alias)
404 backend_class = get_backend(backend_alias)
405 dump_extractor = self.dump_extractors[backend_alias]
405 dump_extractor = self.dump_extractors[backend_alias]
406 repo_path = dump_extractor(dump_name, repo_name)
406 repo_path = dump_extractor(dump_name, repo_name)
407 vcs_repo = backend_class(repo_path)
407 vcs_repo = backend_class(repo_path)
408 repo2db_mapper({repo_name: vcs_repo})
408 repo2db_mapper({repo_name: vcs_repo})
409 repo = RepoModel().get_by_repo_name(repo_name)
409 repo = RepoModel().get_by_repo_name(repo_name)
410 self._cleanup_repos.append(repo_name)
410 self._cleanup_repos.append(repo_name)
411 return repo
411 return repo
412
412
413 def _cleanup(self):
413 def _cleanup(self):
414 for repo_name in reversed(self._cleanup_repos):
414 for repo_name in reversed(self._cleanup_repos):
415 self._fixture.destroy_repo(repo_name)
415 self._fixture.destroy_repo(repo_name)
416
416
417
417
418 @pytest.fixture
418 @pytest.fixture
419 def backend(request, backend_alias, pylonsapp, test_repo):
419 def backend(request, backend_alias, pylonsapp, test_repo):
420 """
420 """
421 Parametrized fixture which represents a single backend implementation.
421 Parametrized fixture which represents a single backend implementation.
422
422
423 It respects the option `--backends` to focus the test run on specific
423 It respects the option `--backends` to focus the test run on specific
424 backend implementations.
424 backend implementations.
425
425
426 It also supports `pytest.mark.xfail_backends` to mark tests as failing
426 It also supports `pytest.mark.xfail_backends` to mark tests as failing
427 for specific backends. This is intended as a utility for incremental
427 for specific backends. This is intended as a utility for incremental
428 development of a new backend implementation.
428 development of a new backend implementation.
429 """
429 """
430 if backend_alias not in request.config.getoption('--backends'):
430 if backend_alias not in request.config.getoption('--backends'):
431 pytest.skip("Backend %s not selected." % (backend_alias, ))
431 pytest.skip("Backend %s not selected." % (backend_alias, ))
432
432
433 utils.check_xfail_backends(request.node, backend_alias)
433 utils.check_xfail_backends(request.node, backend_alias)
434 utils.check_skip_backends(request.node, backend_alias)
434 utils.check_skip_backends(request.node, backend_alias)
435
435
436 repo_name = 'vcs_test_%s' % (backend_alias, )
436 repo_name = 'vcs_test_%s' % (backend_alias, )
437 backend = Backend(
437 backend = Backend(
438 alias=backend_alias,
438 alias=backend_alias,
439 repo_name=repo_name,
439 repo_name=repo_name,
440 test_name=request.node.name,
440 test_name=request.node.name,
441 test_repo_container=test_repo)
441 test_repo_container=test_repo)
442 request.addfinalizer(backend.cleanup)
442 request.addfinalizer(backend.cleanup)
443 return backend
443 return backend
444
444
445
445
446 @pytest.fixture
446 @pytest.fixture
447 def backend_git(request, pylonsapp, test_repo):
447 def backend_git(request, pylonsapp, test_repo):
448 return backend(request, 'git', pylonsapp, test_repo)
448 return backend(request, 'git', pylonsapp, test_repo)
449
449
450
450
451 @pytest.fixture
451 @pytest.fixture
452 def backend_hg(request, pylonsapp, test_repo):
452 def backend_hg(request, pylonsapp, test_repo):
453 return backend(request, 'hg', pylonsapp, test_repo)
453 return backend(request, 'hg', pylonsapp, test_repo)
454
454
455
455
456 @pytest.fixture
456 @pytest.fixture
457 def backend_svn(request, pylonsapp, test_repo):
457 def backend_svn(request, pylonsapp, test_repo):
458 return backend(request, 'svn', pylonsapp, test_repo)
458 return backend(request, 'svn', pylonsapp, test_repo)
459
459
460
460
461 @pytest.fixture
461 @pytest.fixture
462 def backend_random(backend_git):
462 def backend_random(backend_git):
463 """
463 """
464 Use this to express that your tests need "a backend.
464 Use this to express that your tests need "a backend.
465
465
466 A few of our tests need a backend, so that we can run the code. This
466 A few of our tests need a backend, so that we can run the code. This
467 fixture is intended to be used for such cases. It will pick one of the
467 fixture is intended to be used for such cases. It will pick one of the
468 backends and run the tests.
468 backends and run the tests.
469
469
470 The fixture `backend` would run the test multiple times for each
470 The fixture `backend` would run the test multiple times for each
471 available backend which is a pure waste of time if the test is
471 available backend which is a pure waste of time if the test is
472 independent of the backend type.
472 independent of the backend type.
473 """
473 """
474 # TODO: johbo: Change this to pick a random backend
474 # TODO: johbo: Change this to pick a random backend
475 return backend_git
475 return backend_git
476
476
477
477
478 @pytest.fixture
478 @pytest.fixture
479 def backend_stub(backend_git):
479 def backend_stub(backend_git):
480 """
480 """
481 Use this to express that your tests need a backend stub
481 Use this to express that your tests need a backend stub
482
482
483 TODO: mikhail: Implement a real stub logic instead of returning
483 TODO: mikhail: Implement a real stub logic instead of returning
484 a git backend
484 a git backend
485 """
485 """
486 return backend_git
486 return backend_git
487
487
488
488
489 @pytest.fixture
489 @pytest.fixture
490 def repo_stub(backend_stub):
490 def repo_stub(backend_stub):
491 """
491 """
492 Use this to express that your tests need a repository stub
492 Use this to express that your tests need a repository stub
493 """
493 """
494 return backend_stub.create_repo()
494 return backend_stub.create_repo()
495
495
496
496
497 class Backend(object):
497 class Backend(object):
498 """
498 """
499 Represents the test configuration for one supported backend
499 Represents the test configuration for one supported backend
500
500
501 Provides easy access to different test repositories based on
501 Provides easy access to different test repositories based on
502 `__getitem__`. Such repositories will only be created once per test
502 `__getitem__`. Such repositories will only be created once per test
503 session.
503 session.
504 """
504 """
505
505
506 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
506 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
507 _master_repo = None
507 _master_repo = None
508 _commit_ids = {}
508 _commit_ids = {}
509
509
510 def __init__(self, alias, repo_name, test_name, test_repo_container):
510 def __init__(self, alias, repo_name, test_name, test_repo_container):
511 self.alias = alias
511 self.alias = alias
512 self.repo_name = repo_name
512 self.repo_name = repo_name
513 self._cleanup_repos = []
513 self._cleanup_repos = []
514 self._test_name = test_name
514 self._test_name = test_name
515 self._test_repo_container = test_repo_container
515 self._test_repo_container = test_repo_container
516 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
516 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
517 # Fixture will survive in the end.
517 # Fixture will survive in the end.
518 self._fixture = Fixture()
518 self._fixture = Fixture()
519
519
520 def __getitem__(self, key):
520 def __getitem__(self, key):
521 return self._test_repo_container(key, self.alias)
521 return self._test_repo_container(key, self.alias)
522
522
523 @property
523 @property
524 def repo(self):
524 def repo(self):
525 """
525 """
526 Returns the "current" repository. This is the vcs_test repo or the
526 Returns the "current" repository. This is the vcs_test repo or the
527 last repo which has been created with `create_repo`.
527 last repo which has been created with `create_repo`.
528 """
528 """
529 from rhodecode.model.db import Repository
529 from rhodecode.model.db import Repository
530 return Repository.get_by_repo_name(self.repo_name)
530 return Repository.get_by_repo_name(self.repo_name)
531
531
532 @property
532 @property
533 def default_branch_name(self):
533 def default_branch_name(self):
534 VcsRepository = get_backend(self.alias)
534 VcsRepository = get_backend(self.alias)
535 return VcsRepository.DEFAULT_BRANCH_NAME
535 return VcsRepository.DEFAULT_BRANCH_NAME
536
536
537 @property
537 @property
538 def default_head_id(self):
538 def default_head_id(self):
539 """
539 """
540 Returns the default head id of the underlying backend.
540 Returns the default head id of the underlying backend.
541
541
542 This will be the default branch name in case the backend does have a
542 This will be the default branch name in case the backend does have a
543 default branch. In the other cases it will point to a valid head
543 default branch. In the other cases it will point to a valid head
544 which can serve as the base to create a new commit on top of it.
544 which can serve as the base to create a new commit on top of it.
545 """
545 """
546 vcsrepo = self.repo.scm_instance()
546 vcsrepo = self.repo.scm_instance()
547 head_id = (
547 head_id = (
548 vcsrepo.DEFAULT_BRANCH_NAME or
548 vcsrepo.DEFAULT_BRANCH_NAME or
549 vcsrepo.commit_ids[-1])
549 vcsrepo.commit_ids[-1])
550 return head_id
550 return head_id
551
551
552 @property
552 @property
553 def commit_ids(self):
553 def commit_ids(self):
554 """
554 """
555 Returns the list of commits for the last created repository
555 Returns the list of commits for the last created repository
556 """
556 """
557 return self._commit_ids
557 return self._commit_ids
558
558
559 def create_master_repo(self, commits):
559 def create_master_repo(self, commits):
560 """
560 """
561 Create a repository and remember it as a template.
561 Create a repository and remember it as a template.
562
562
563 This allows to easily create derived repositories to construct
563 This allows to easily create derived repositories to construct
564 more complex scenarios for diff, compare and pull requests.
564 more complex scenarios for diff, compare and pull requests.
565
565
566 Returns a commit map which maps from commit message to raw_id.
566 Returns a commit map which maps from commit message to raw_id.
567 """
567 """
568 self._master_repo = self.create_repo(commits=commits)
568 self._master_repo = self.create_repo(commits=commits)
569 return self._commit_ids
569 return self._commit_ids
570
570
571 def create_repo(
571 def create_repo(
572 self, commits=None, number_of_commits=0, heads=None,
572 self, commits=None, number_of_commits=0, heads=None,
573 name_suffix=u'', **kwargs):
573 name_suffix=u'', **kwargs):
574 """
574 """
575 Create a repository and record it for later cleanup.
575 Create a repository and record it for later cleanup.
576
576
577 :param commits: Optional. A sequence of dict instances.
577 :param commits: Optional. A sequence of dict instances.
578 Will add a commit per entry to the new repository.
578 Will add a commit per entry to the new repository.
579 :param number_of_commits: Optional. If set to a number, this number of
579 :param number_of_commits: Optional. If set to a number, this number of
580 commits will be added to the new repository.
580 commits will be added to the new repository.
581 :param heads: Optional. Can be set to a sequence of of commit
581 :param heads: Optional. Can be set to a sequence of of commit
582 names which shall be pulled in from the master repository.
582 names which shall be pulled in from the master repository.
583
583
584 """
584 """
585 self.repo_name = self._next_repo_name() + name_suffix
585 self.repo_name = self._next_repo_name() + name_suffix
586 repo = self._fixture.create_repo(
586 repo = self._fixture.create_repo(
587 self.repo_name, repo_type=self.alias, **kwargs)
587 self.repo_name, repo_type=self.alias, **kwargs)
588 self._cleanup_repos.append(repo.repo_name)
588 self._cleanup_repos.append(repo.repo_name)
589
589
590 commits = commits or [
590 commits = commits or [
591 {'message': 'Commit %s of %s' % (x, self.repo_name)}
591 {'message': 'Commit %s of %s' % (x, self.repo_name)}
592 for x in xrange(number_of_commits)]
592 for x in xrange(number_of_commits)]
593 self._add_commits_to_repo(repo.scm_instance(), commits)
593 self._add_commits_to_repo(repo.scm_instance(), commits)
594 if heads:
594 if heads:
595 self.pull_heads(repo, heads)
595 self.pull_heads(repo, heads)
596
596
597 return repo
597 return repo
598
598
599 def pull_heads(self, repo, heads):
599 def pull_heads(self, repo, heads):
600 """
600 """
601 Make sure that repo contains all commits mentioned in `heads`
601 Make sure that repo contains all commits mentioned in `heads`
602 """
602 """
603 vcsmaster = self._master_repo.scm_instance()
603 vcsmaster = self._master_repo.scm_instance()
604 vcsrepo = repo.scm_instance()
604 vcsrepo = repo.scm_instance()
605 vcsrepo.config.clear_section('hooks')
605 vcsrepo.config.clear_section('hooks')
606 commit_ids = [self._commit_ids[h] for h in heads]
606 commit_ids = [self._commit_ids[h] for h in heads]
607 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
607 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
608
608
609 def create_fork(self):
609 def create_fork(self):
610 repo_to_fork = self.repo_name
610 repo_to_fork = self.repo_name
611 self.repo_name = self._next_repo_name()
611 self.repo_name = self._next_repo_name()
612 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
612 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
613 self._cleanup_repos.append(self.repo_name)
613 self._cleanup_repos.append(self.repo_name)
614 return repo
614 return repo
615
615
616 def new_repo_name(self, suffix=u''):
616 def new_repo_name(self, suffix=u''):
617 self.repo_name = self._next_repo_name() + suffix
617 self.repo_name = self._next_repo_name() + suffix
618 self._cleanup_repos.append(self.repo_name)
618 self._cleanup_repos.append(self.repo_name)
619 return self.repo_name
619 return self.repo_name
620
620
621 def _next_repo_name(self):
621 def _next_repo_name(self):
622 return u"%s_%s" % (
622 return u"%s_%s" % (
623 self.invalid_repo_name.sub(u'_', self._test_name),
623 self.invalid_repo_name.sub(u'_', self._test_name),
624 len(self._cleanup_repos))
624 len(self._cleanup_repos))
625
625
626 def ensure_file(self, filename, content='Test content\n'):
626 def ensure_file(self, filename, content='Test content\n'):
627 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
627 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
628 commits = [
628 commits = [
629 {'added': [
629 {'added': [
630 FileNode(filename, content=content),
630 FileNode(filename, content=content),
631 ]},
631 ]},
632 ]
632 ]
633 self._add_commits_to_repo(self.repo.scm_instance(), commits)
633 self._add_commits_to_repo(self.repo.scm_instance(), commits)
634
634
635 def enable_downloads(self):
635 def enable_downloads(self):
636 repo = self.repo
636 repo = self.repo
637 repo.enable_downloads = True
637 repo.enable_downloads = True
638 Session().add(repo)
638 Session().add(repo)
639 Session().commit()
639 Session().commit()
640
640
641 def cleanup(self):
641 def cleanup(self):
642 for repo_name in reversed(self._cleanup_repos):
642 for repo_name in reversed(self._cleanup_repos):
643 self._fixture.destroy_repo(repo_name)
643 self._fixture.destroy_repo(repo_name)
644
644
645 def _add_commits_to_repo(self, repo, commits):
645 def _add_commits_to_repo(self, repo, commits):
646 commit_ids = _add_commits_to_repo(repo, commits)
646 commit_ids = _add_commits_to_repo(repo, commits)
647 if not commit_ids:
647 if not commit_ids:
648 return
648 return
649 self._commit_ids = commit_ids
649 self._commit_ids = commit_ids
650
650
651 # Creating refs for Git to allow fetching them from remote repository
651 # Creating refs for Git to allow fetching them from remote repository
652 if self.alias == 'git':
652 if self.alias == 'git':
653 refs = {}
653 refs = {}
654 for message in self._commit_ids:
654 for message in self._commit_ids:
655 # TODO: mikhail: do more special chars replacements
655 # TODO: mikhail: do more special chars replacements
656 ref_name = 'refs/test-refs/{}'.format(
656 ref_name = 'refs/test-refs/{}'.format(
657 message.replace(' ', ''))
657 message.replace(' ', ''))
658 refs[ref_name] = self._commit_ids[message]
658 refs[ref_name] = self._commit_ids[message]
659 self._create_refs(repo, refs)
659 self._create_refs(repo, refs)
660
660
661 def _create_refs(self, repo, refs):
661 def _create_refs(self, repo, refs):
662 for ref_name in refs:
662 for ref_name in refs:
663 repo.set_refs(ref_name, refs[ref_name])
663 repo.set_refs(ref_name, refs[ref_name])
664
664
665
665
666 @pytest.fixture
666 @pytest.fixture
667 def vcsbackend(request, backend_alias, tests_tmp_path, pylonsapp, test_repo):
667 def vcsbackend(request, backend_alias, tests_tmp_path, pylonsapp, test_repo):
668 """
668 """
669 Parametrized fixture which represents a single vcs backend implementation.
669 Parametrized fixture which represents a single vcs backend implementation.
670
670
671 See the fixture `backend` for more details. This one implements the same
671 See the fixture `backend` for more details. This one implements the same
672 concept, but on vcs level. So it does not provide model instances etc.
672 concept, but on vcs level. So it does not provide model instances etc.
673
673
674 Parameters are generated dynamically, see :func:`pytest_generate_tests`
674 Parameters are generated dynamically, see :func:`pytest_generate_tests`
675 for how this works.
675 for how this works.
676 """
676 """
677 if backend_alias not in request.config.getoption('--backends'):
677 if backend_alias not in request.config.getoption('--backends'):
678 pytest.skip("Backend %s not selected." % (backend_alias, ))
678 pytest.skip("Backend %s not selected." % (backend_alias, ))
679
679
680 utils.check_xfail_backends(request.node, backend_alias)
680 utils.check_xfail_backends(request.node, backend_alias)
681 utils.check_skip_backends(request.node, backend_alias)
681 utils.check_skip_backends(request.node, backend_alias)
682
682
683 repo_name = 'vcs_test_%s' % (backend_alias, )
683 repo_name = 'vcs_test_%s' % (backend_alias, )
684 repo_path = os.path.join(tests_tmp_path, repo_name)
684 repo_path = os.path.join(tests_tmp_path, repo_name)
685 backend = VcsBackend(
685 backend = VcsBackend(
686 alias=backend_alias,
686 alias=backend_alias,
687 repo_path=repo_path,
687 repo_path=repo_path,
688 test_name=request.node.name,
688 test_name=request.node.name,
689 test_repo_container=test_repo)
689 test_repo_container=test_repo)
690 request.addfinalizer(backend.cleanup)
690 request.addfinalizer(backend.cleanup)
691 return backend
691 return backend
692
692
693
693
694 @pytest.fixture
694 @pytest.fixture
695 def vcsbackend_git(request, tests_tmp_path, pylonsapp, test_repo):
695 def vcsbackend_git(request, tests_tmp_path, pylonsapp, test_repo):
696 return vcsbackend(request, 'git', tests_tmp_path, pylonsapp, test_repo)
696 return vcsbackend(request, 'git', tests_tmp_path, pylonsapp, test_repo)
697
697
698
698
699 @pytest.fixture
699 @pytest.fixture
700 def vcsbackend_hg(request, tests_tmp_path, pylonsapp, test_repo):
700 def vcsbackend_hg(request, tests_tmp_path, pylonsapp, test_repo):
701 return vcsbackend(request, 'hg', tests_tmp_path, pylonsapp, test_repo)
701 return vcsbackend(request, 'hg', tests_tmp_path, pylonsapp, test_repo)
702
702
703
703
704 @pytest.fixture
704 @pytest.fixture
705 def vcsbackend_svn(request, tests_tmp_path, pylonsapp, test_repo):
705 def vcsbackend_svn(request, tests_tmp_path, pylonsapp, test_repo):
706 return vcsbackend(request, 'svn', tests_tmp_path, pylonsapp, test_repo)
706 return vcsbackend(request, 'svn', tests_tmp_path, pylonsapp, test_repo)
707
707
708
708
709 @pytest.fixture
709 @pytest.fixture
710 def vcsbackend_random(vcsbackend_git):
710 def vcsbackend_random(vcsbackend_git):
711 """
711 """
712 Use this to express that your tests need "a vcsbackend".
712 Use this to express that your tests need "a vcsbackend".
713
713
714 The fixture `vcsbackend` would run the test multiple times for each
714 The fixture `vcsbackend` would run the test multiple times for each
715 available vcs backend which is a pure waste of time if the test is
715 available vcs backend which is a pure waste of time if the test is
716 independent of the vcs backend type.
716 independent of the vcs backend type.
717 """
717 """
718 # TODO: johbo: Change this to pick a random backend
718 # TODO: johbo: Change this to pick a random backend
719 return vcsbackend_git
719 return vcsbackend_git
720
720
721
721
722 @pytest.fixture
722 @pytest.fixture
723 def vcsbackend_stub(vcsbackend_git):
723 def vcsbackend_stub(vcsbackend_git):
724 """
724 """
725 Use this to express that your test just needs a stub of a vcsbackend.
725 Use this to express that your test just needs a stub of a vcsbackend.
726
726
727 Plan is to eventually implement an in-memory stub to speed tests up.
727 Plan is to eventually implement an in-memory stub to speed tests up.
728 """
728 """
729 return vcsbackend_git
729 return vcsbackend_git
730
730
731
731
732 class VcsBackend(object):
732 class VcsBackend(object):
733 """
733 """
734 Represents the test configuration for one supported vcs backend.
734 Represents the test configuration for one supported vcs backend.
735 """
735 """
736
736
737 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
737 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
738
738
739 def __init__(self, alias, repo_path, test_name, test_repo_container):
739 def __init__(self, alias, repo_path, test_name, test_repo_container):
740 self.alias = alias
740 self.alias = alias
741 self._repo_path = repo_path
741 self._repo_path = repo_path
742 self._cleanup_repos = []
742 self._cleanup_repos = []
743 self._test_name = test_name
743 self._test_name = test_name
744 self._test_repo_container = test_repo_container
744 self._test_repo_container = test_repo_container
745
745
746 def __getitem__(self, key):
746 def __getitem__(self, key):
747 return self._test_repo_container(key, self.alias).scm_instance()
747 return self._test_repo_container(key, self.alias).scm_instance()
748
748
749 @property
749 @property
750 def repo(self):
750 def repo(self):
751 """
751 """
752 Returns the "current" repository. This is the vcs_test repo of the last
752 Returns the "current" repository. This is the vcs_test repo of the last
753 repo which has been created.
753 repo which has been created.
754 """
754 """
755 Repository = get_backend(self.alias)
755 Repository = get_backend(self.alias)
756 return Repository(self._repo_path)
756 return Repository(self._repo_path)
757
757
758 @property
758 @property
759 def backend(self):
759 def backend(self):
760 """
760 """
761 Returns the backend implementation class.
761 Returns the backend implementation class.
762 """
762 """
763 return get_backend(self.alias)
763 return get_backend(self.alias)
764
764
765 def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None):
765 def create_repo(self, commits=None, number_of_commits=0, _clone_repo=None):
766 repo_name = self._next_repo_name()
766 repo_name = self._next_repo_name()
767 self._repo_path = get_new_dir(repo_name)
767 self._repo_path = get_new_dir(repo_name)
768 repo_class = get_backend(self.alias)
768 repo_class = get_backend(self.alias)
769 src_url = None
769 src_url = None
770 if _clone_repo:
770 if _clone_repo:
771 src_url = _clone_repo.path
771 src_url = _clone_repo.path
772 repo = repo_class(self._repo_path, create=True, src_url=src_url)
772 repo = repo_class(self._repo_path, create=True, src_url=src_url)
773 self._cleanup_repos.append(repo)
773 self._cleanup_repos.append(repo)
774
774
775 commits = commits or [
775 commits = commits or [
776 {'message': 'Commit %s of %s' % (x, repo_name)}
776 {'message': 'Commit %s of %s' % (x, repo_name)}
777 for x in xrange(number_of_commits)]
777 for x in xrange(number_of_commits)]
778 _add_commits_to_repo(repo, commits)
778 _add_commits_to_repo(repo, commits)
779 return repo
779 return repo
780
780
781 def clone_repo(self, repo):
781 def clone_repo(self, repo):
782 return self.create_repo(_clone_repo=repo)
782 return self.create_repo(_clone_repo=repo)
783
783
784 def cleanup(self):
784 def cleanup(self):
785 for repo in self._cleanup_repos:
785 for repo in self._cleanup_repos:
786 shutil.rmtree(repo.path)
786 shutil.rmtree(repo.path)
787
787
788 def new_repo_path(self):
788 def new_repo_path(self):
789 repo_name = self._next_repo_name()
789 repo_name = self._next_repo_name()
790 self._repo_path = get_new_dir(repo_name)
790 self._repo_path = get_new_dir(repo_name)
791 return self._repo_path
791 return self._repo_path
792
792
793 def _next_repo_name(self):
793 def _next_repo_name(self):
794 return "%s_%s" % (
794 return "%s_%s" % (
795 self.invalid_repo_name.sub('_', self._test_name),
795 self.invalid_repo_name.sub('_', self._test_name),
796 len(self._cleanup_repos))
796 len(self._cleanup_repos))
797
797
798 def add_file(self, repo, filename, content='Test content\n'):
798 def add_file(self, repo, filename, content='Test content\n'):
799 imc = repo.in_memory_commit
799 imc = repo.in_memory_commit
800 imc.add(FileNode(filename, content=content))
800 imc.add(FileNode(filename, content=content))
801 imc.commit(
801 imc.commit(
802 message=u'Automatic commit from vcsbackend fixture',
802 message=u'Automatic commit from vcsbackend fixture',
803 author=u'Automatic')
803 author=u'Automatic')
804
804
805 def ensure_file(self, filename, content='Test content\n'):
805 def ensure_file(self, filename, content='Test content\n'):
806 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
806 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
807 self.add_file(self.repo, filename, content)
807 self.add_file(self.repo, filename, content)
808
808
809
809
810 def _add_commits_to_repo(vcs_repo, commits):
810 def _add_commits_to_repo(vcs_repo, commits):
811 commit_ids = {}
811 commit_ids = {}
812 if not commits:
812 if not commits:
813 return commit_ids
813 return commit_ids
814
814
815 imc = vcs_repo.in_memory_commit
815 imc = vcs_repo.in_memory_commit
816 commit = None
816 commit = None
817
817
818 for idx, commit in enumerate(commits):
818 for idx, commit in enumerate(commits):
819 message = unicode(commit.get('message', 'Commit %s' % idx))
819 message = unicode(commit.get('message', 'Commit %s' % idx))
820
820
821 for node in commit.get('added', []):
821 for node in commit.get('added', []):
822 imc.add(FileNode(node.path, content=node.content))
822 imc.add(FileNode(node.path, content=node.content))
823 for node in commit.get('changed', []):
823 for node in commit.get('changed', []):
824 imc.change(FileNode(node.path, content=node.content))
824 imc.change(FileNode(node.path, content=node.content))
825 for node in commit.get('removed', []):
825 for node in commit.get('removed', []):
826 imc.remove(FileNode(node.path))
826 imc.remove(FileNode(node.path))
827
827
828 parents = [
828 parents = [
829 vcs_repo.get_commit(commit_id=commit_ids[p])
829 vcs_repo.get_commit(commit_id=commit_ids[p])
830 for p in commit.get('parents', [])]
830 for p in commit.get('parents', [])]
831
831
832 operations = ('added', 'changed', 'removed')
832 operations = ('added', 'changed', 'removed')
833 if not any((commit.get(o) for o in operations)):
833 if not any((commit.get(o) for o in operations)):
834 imc.add(FileNode('file_%s' % idx, content=message))
834 imc.add(FileNode('file_%s' % idx, content=message))
835
835
836 commit = imc.commit(
836 commit = imc.commit(
837 message=message,
837 message=message,
838 author=unicode(commit.get('author', 'Automatic')),
838 author=unicode(commit.get('author', 'Automatic')),
839 date=commit.get('date'),
839 date=commit.get('date'),
840 branch=commit.get('branch'),
840 branch=commit.get('branch'),
841 parents=parents)
841 parents=parents)
842
842
843 commit_ids[commit.message] = commit.raw_id
843 commit_ids[commit.message] = commit.raw_id
844
844
845 return commit_ids
845 return commit_ids
846
846
847
847
848 @pytest.fixture
848 @pytest.fixture
849 def reposerver(request):
849 def reposerver(request):
850 """
850 """
851 Allows to serve a backend repository
851 Allows to serve a backend repository
852 """
852 """
853
853
854 repo_server = RepoServer()
854 repo_server = RepoServer()
855 request.addfinalizer(repo_server.cleanup)
855 request.addfinalizer(repo_server.cleanup)
856 return repo_server
856 return repo_server
857
857
858
858
859 class RepoServer(object):
859 class RepoServer(object):
860 """
860 """
861 Utility to serve a local repository for the duration of a test case.
861 Utility to serve a local repository for the duration of a test case.
862
862
863 Supports only Subversion so far.
863 Supports only Subversion so far.
864 """
864 """
865
865
866 url = None
866 url = None
867
867
868 def __init__(self):
868 def __init__(self):
869 self._cleanup_servers = []
869 self._cleanup_servers = []
870
870
871 def serve(self, vcsrepo):
871 def serve(self, vcsrepo):
872 if vcsrepo.alias != 'svn':
872 if vcsrepo.alias != 'svn':
873 raise TypeError("Backend %s not supported" % vcsrepo.alias)
873 raise TypeError("Backend %s not supported" % vcsrepo.alias)
874
874
875 proc = subprocess.Popen(
875 proc = subprocess.Popen(
876 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
876 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
877 '--root', vcsrepo.path])
877 '--root', vcsrepo.path])
878 self._cleanup_servers.append(proc)
878 self._cleanup_servers.append(proc)
879 self.url = 'svn://localhost'
879 self.url = 'svn://localhost'
880
880
881 def cleanup(self):
881 def cleanup(self):
882 for proc in self._cleanup_servers:
882 for proc in self._cleanup_servers:
883 proc.terminate()
883 proc.terminate()
884
884
885
885
886 @pytest.fixture
886 @pytest.fixture
887 def pr_util(backend, request):
887 def pr_util(backend, request):
888 """
888 """
889 Utility for tests of models and for functional tests around pull requests.
889 Utility for tests of models and for functional tests around pull requests.
890
890
891 It gives an instance of :class:`PRTestUtility` which provides various
891 It gives an instance of :class:`PRTestUtility` which provides various
892 utility methods around one pull request.
892 utility methods around one pull request.
893
893
894 This fixture uses `backend` and inherits its parameterization.
894 This fixture uses `backend` and inherits its parameterization.
895 """
895 """
896
896
897 util = PRTestUtility(backend)
897 util = PRTestUtility(backend)
898
898
899 @request.addfinalizer
899 @request.addfinalizer
900 def cleanup():
900 def cleanup():
901 util.cleanup()
901 util.cleanup()
902
902
903 return util
903 return util
904
904
905
905
906 class PRTestUtility(object):
906 class PRTestUtility(object):
907
907
908 pull_request = None
908 pull_request = None
909 pull_request_id = None
909 pull_request_id = None
910 mergeable_patcher = None
910 mergeable_patcher = None
911 mergeable_mock = None
911 mergeable_mock = None
912 notification_patcher = None
912 notification_patcher = None
913
913
914 def __init__(self, backend):
914 def __init__(self, backend):
915 self.backend = backend
915 self.backend = backend
916
916
917 def create_pull_request(
917 def create_pull_request(
918 self, commits=None, target_head=None, source_head=None,
918 self, commits=None, target_head=None, source_head=None,
919 revisions=None, approved=False, author=None, mergeable=False,
919 revisions=None, approved=False, author=None, mergeable=False,
920 enable_notifications=True, name_suffix=u'', reviewers=None,
920 enable_notifications=True, name_suffix=u'', reviewers=None,
921 title=u"Test", description=u"Description"):
921 title=u"Test", description=u"Description"):
922 self.set_mergeable(mergeable)
922 self.set_mergeable(mergeable)
923 if not enable_notifications:
923 if not enable_notifications:
924 # mock notification side effect
924 # mock notification side effect
925 self.notification_patcher = mock.patch(
925 self.notification_patcher = mock.patch(
926 'rhodecode.model.notification.NotificationModel.create')
926 'rhodecode.model.notification.NotificationModel.create')
927 self.notification_patcher.start()
927 self.notification_patcher.start()
928
928
929 if not self.pull_request:
929 if not self.pull_request:
930 if not commits:
930 if not commits:
931 commits = [
931 commits = [
932 {'message': 'c1'},
932 {'message': 'c1'},
933 {'message': 'c2'},
933 {'message': 'c2'},
934 {'message': 'c3'},
934 {'message': 'c3'},
935 ]
935 ]
936 target_head = 'c1'
936 target_head = 'c1'
937 source_head = 'c2'
937 source_head = 'c2'
938 revisions = ['c2']
938 revisions = ['c2']
939
939
940 self.commit_ids = self.backend.create_master_repo(commits)
940 self.commit_ids = self.backend.create_master_repo(commits)
941 self.target_repository = self.backend.create_repo(
941 self.target_repository = self.backend.create_repo(
942 heads=[target_head], name_suffix=name_suffix)
942 heads=[target_head], name_suffix=name_suffix)
943 self.source_repository = self.backend.create_repo(
943 self.source_repository = self.backend.create_repo(
944 heads=[source_head], name_suffix=name_suffix)
944 heads=[source_head], name_suffix=name_suffix)
945 self.author = author or UserModel().get_by_username(
945 self.author = author or UserModel().get_by_username(
946 TEST_USER_ADMIN_LOGIN)
946 TEST_USER_ADMIN_LOGIN)
947
947
948 model = PullRequestModel()
948 model = PullRequestModel()
949 self.create_parameters = {
949 self.create_parameters = {
950 'created_by': self.author,
950 'created_by': self.author,
951 'source_repo': self.source_repository.repo_name,
951 'source_repo': self.source_repository.repo_name,
952 'source_ref': self._default_branch_reference(source_head),
952 'source_ref': self._default_branch_reference(source_head),
953 'target_repo': self.target_repository.repo_name,
953 'target_repo': self.target_repository.repo_name,
954 'target_ref': self._default_branch_reference(target_head),
954 'target_ref': self._default_branch_reference(target_head),
955 'revisions': [self.commit_ids[r] for r in revisions],
955 'revisions': [self.commit_ids[r] for r in revisions],
956 'reviewers': reviewers or self._get_reviewers(),
956 'reviewers': reviewers or self._get_reviewers(),
957 'title': title,
957 'title': title,
958 'description': description,
958 'description': description,
959 }
959 }
960 self.pull_request = model.create(**self.create_parameters)
960 self.pull_request = model.create(**self.create_parameters)
961 assert model.get_versions(self.pull_request) == []
961 assert model.get_versions(self.pull_request) == []
962
962
963 self.pull_request_id = self.pull_request.pull_request_id
963 self.pull_request_id = self.pull_request.pull_request_id
964
964
965 if approved:
965 if approved:
966 self.approve()
966 self.approve()
967
967
968 Session().add(self.pull_request)
968 Session().add(self.pull_request)
969 Session().commit()
969 Session().commit()
970
970
971 return self.pull_request
971 return self.pull_request
972
972
973 def approve(self):
973 def approve(self):
974 self.create_status_votes(
974 self.create_status_votes(
975 ChangesetStatus.STATUS_APPROVED,
975 ChangesetStatus.STATUS_APPROVED,
976 *self.pull_request.reviewers)
976 *self.pull_request.reviewers)
977
977
978 def close(self):
978 def close(self):
979 PullRequestModel().close_pull_request(self.pull_request, self.author)
979 PullRequestModel().close_pull_request(self.pull_request, self.author)
980
980
981 def _default_branch_reference(self, commit_message):
981 def _default_branch_reference(self, commit_message):
982 reference = '%s:%s:%s' % (
982 reference = '%s:%s:%s' % (
983 'branch',
983 'branch',
984 self.backend.default_branch_name,
984 self.backend.default_branch_name,
985 self.commit_ids[commit_message])
985 self.commit_ids[commit_message])
986 return reference
986 return reference
987
987
988 def _get_reviewers(self):
988 def _get_reviewers(self):
989 model = UserModel()
989 model = UserModel()
990 return [
990 return [
991 model.get_by_username(TEST_USER_REGULAR_LOGIN),
991 model.get_by_username(TEST_USER_REGULAR_LOGIN),
992 model.get_by_username(TEST_USER_REGULAR2_LOGIN),
992 model.get_by_username(TEST_USER_REGULAR2_LOGIN),
993 ]
993 ]
994
994
995 def update_source_repository(self, head=None):
995 def update_source_repository(self, head=None):
996 heads = [head or 'c3']
996 heads = [head or 'c3']
997 self.backend.pull_heads(self.source_repository, heads=heads)
997 self.backend.pull_heads(self.source_repository, heads=heads)
998
998
999 def add_one_commit(self, head=None):
999 def add_one_commit(self, head=None):
1000 self.update_source_repository(head=head)
1000 self.update_source_repository(head=head)
1001 old_commit_ids = set(self.pull_request.revisions)
1001 old_commit_ids = set(self.pull_request.revisions)
1002 PullRequestModel().update_commits(self.pull_request)
1002 PullRequestModel().update_commits(self.pull_request)
1003 commit_ids = set(self.pull_request.revisions)
1003 commit_ids = set(self.pull_request.revisions)
1004 new_commit_ids = commit_ids - old_commit_ids
1004 new_commit_ids = commit_ids - old_commit_ids
1005 assert len(new_commit_ids) == 1
1005 assert len(new_commit_ids) == 1
1006 return new_commit_ids.pop()
1006 return new_commit_ids.pop()
1007
1007
1008 def remove_one_commit(self):
1008 def remove_one_commit(self):
1009 assert len(self.pull_request.revisions) == 2
1009 assert len(self.pull_request.revisions) == 2
1010 source_vcs = self.source_repository.scm_instance()
1010 source_vcs = self.source_repository.scm_instance()
1011 removed_commit_id = source_vcs.commit_ids[-1]
1011 removed_commit_id = source_vcs.commit_ids[-1]
1012
1012
1013 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
1013 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
1014 # remove the if once that's sorted out.
1014 # remove the if once that's sorted out.
1015 if self.backend.alias == "git":
1015 if self.backend.alias == "git":
1016 kwargs = {'branch_name': self.backend.default_branch_name}
1016 kwargs = {'branch_name': self.backend.default_branch_name}
1017 else:
1017 else:
1018 kwargs = {}
1018 kwargs = {}
1019 source_vcs.strip(removed_commit_id, **kwargs)
1019 source_vcs.strip(removed_commit_id, **kwargs)
1020
1020
1021 PullRequestModel().update_commits(self.pull_request)
1021 PullRequestModel().update_commits(self.pull_request)
1022 assert len(self.pull_request.revisions) == 1
1022 assert len(self.pull_request.revisions) == 1
1023 return removed_commit_id
1023 return removed_commit_id
1024
1024
1025 def create_comment(self, linked_to=None):
1025 def create_comment(self, linked_to=None):
1026 comment = ChangesetCommentsModel().create(
1026 comment = ChangesetCommentsModel().create(
1027 text=u"Test comment",
1027 text=u"Test comment",
1028 repo=self.target_repository.repo_name,
1028 repo=self.target_repository.repo_name,
1029 user=self.author,
1029 user=self.author,
1030 pull_request=self.pull_request)
1030 pull_request=self.pull_request)
1031 assert comment.pull_request_version_id is None
1031 assert comment.pull_request_version_id is None
1032
1032
1033 if linked_to:
1033 if linked_to:
1034 PullRequestModel()._link_comments_to_version(linked_to)
1034 PullRequestModel()._link_comments_to_version(linked_to)
1035
1035
1036 return comment
1036 return comment
1037
1037
1038 def create_inline_comment(
1038 def create_inline_comment(
1039 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1039 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1040 comment = ChangesetCommentsModel().create(
1040 comment = ChangesetCommentsModel().create(
1041 text=u"Test comment",
1041 text=u"Test comment",
1042 repo=self.target_repository.repo_name,
1042 repo=self.target_repository.repo_name,
1043 user=self.author,
1043 user=self.author,
1044 line_no=line_no,
1044 line_no=line_no,
1045 f_path=file_path,
1045 f_path=file_path,
1046 pull_request=self.pull_request)
1046 pull_request=self.pull_request)
1047 assert comment.pull_request_version_id is None
1047 assert comment.pull_request_version_id is None
1048
1048
1049 if linked_to:
1049 if linked_to:
1050 PullRequestModel()._link_comments_to_version(linked_to)
1050 PullRequestModel()._link_comments_to_version(linked_to)
1051
1051
1052 return comment
1052 return comment
1053
1053
1054 def create_version_of_pull_request(self):
1054 def create_version_of_pull_request(self):
1055 pull_request = self.create_pull_request()
1055 pull_request = self.create_pull_request()
1056 version = PullRequestModel()._create_version_from_snapshot(
1056 version = PullRequestModel()._create_version_from_snapshot(
1057 pull_request)
1057 pull_request)
1058 return version
1058 return version
1059
1059
1060 def create_status_votes(self, status, *reviewers):
1060 def create_status_votes(self, status, *reviewers):
1061 for reviewer in reviewers:
1061 for reviewer in reviewers:
1062 ChangesetStatusModel().set_status(
1062 ChangesetStatusModel().set_status(
1063 repo=self.pull_request.target_repo,
1063 repo=self.pull_request.target_repo,
1064 status=status,
1064 status=status,
1065 user=reviewer.user_id,
1065 user=reviewer.user_id,
1066 pull_request=self.pull_request)
1066 pull_request=self.pull_request)
1067
1067
1068 def set_mergeable(self, value):
1068 def set_mergeable(self, value):
1069 if not self.mergeable_patcher:
1069 if not self.mergeable_patcher:
1070 self.mergeable_patcher = mock.patch.object(
1070 self.mergeable_patcher = mock.patch.object(
1071 VcsSettingsModel, 'get_general_settings')
1071 VcsSettingsModel, 'get_general_settings')
1072 self.mergeable_mock = self.mergeable_patcher.start()
1072 self.mergeable_mock = self.mergeable_patcher.start()
1073 self.mergeable_mock.return_value = {
1073 self.mergeable_mock.return_value = {
1074 'rhodecode_pr_merge_enabled': value}
1074 'rhodecode_pr_merge_enabled': value}
1075
1075
1076 def cleanup(self):
1076 def cleanup(self):
1077 # In case the source repository is already cleaned up, the pull
1077 # In case the source repository is already cleaned up, the pull
1078 # request will already be deleted.
1078 # request will already be deleted.
1079 pull_request = PullRequest().get(self.pull_request_id)
1079 pull_request = PullRequest().get(self.pull_request_id)
1080 if pull_request:
1080 if pull_request:
1081 PullRequestModel().delete(pull_request)
1081 PullRequestModel().delete(pull_request)
1082 Session().commit()
1082 Session().commit()
1083
1083
1084 if self.notification_patcher:
1084 if self.notification_patcher:
1085 self.notification_patcher.stop()
1085 self.notification_patcher.stop()
1086
1086
1087 if self.mergeable_patcher:
1087 if self.mergeable_patcher:
1088 self.mergeable_patcher.stop()
1088 self.mergeable_patcher.stop()
1089
1089
1090
1090
1091 @pytest.fixture
1091 @pytest.fixture
1092 def user_admin(pylonsapp):
1092 def user_admin(pylonsapp):
1093 """
1093 """
1094 Provides the default admin test user as an instance of `db.User`.
1094 Provides the default admin test user as an instance of `db.User`.
1095 """
1095 """
1096 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1096 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1097 return user
1097 return user
1098
1098
1099
1099
1100 @pytest.fixture
1100 @pytest.fixture
1101 def user_regular(pylonsapp):
1101 def user_regular(pylonsapp):
1102 """
1102 """
1103 Provides the default regular test user as an instance of `db.User`.
1103 Provides the default regular test user as an instance of `db.User`.
1104 """
1104 """
1105 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1105 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1106 return user
1106 return user
1107
1107
1108
1108
1109 @pytest.fixture
1109 @pytest.fixture
1110 def user_util(request, pylonsapp):
1110 def user_util(request, pylonsapp):
1111 """
1111 """
1112 Provides a wired instance of `UserUtility` with integrated cleanup.
1112 Provides a wired instance of `UserUtility` with integrated cleanup.
1113 """
1113 """
1114 utility = UserUtility(test_name=request.node.name)
1114 utility = UserUtility(test_name=request.node.name)
1115 request.addfinalizer(utility.cleanup)
1115 request.addfinalizer(utility.cleanup)
1116 return utility
1116 return utility
1117
1117
1118
1118
1119 # TODO: johbo: Split this up into utilities per domain or something similar
1119 # TODO: johbo: Split this up into utilities per domain or something similar
1120 class UserUtility(object):
1120 class UserUtility(object):
1121
1121
1122 def __init__(self, test_name="test"):
1122 def __init__(self, test_name="test"):
1123 self._test_name = test_name
1123 self._test_name = test_name
1124 self.fixture = Fixture()
1124 self.fixture = Fixture()
1125 self.repo_group_ids = []
1125 self.repo_group_ids = []
1126 self.user_ids = []
1126 self.user_ids = []
1127 self.user_group_ids = []
1127 self.user_group_ids = []
1128 self.user_repo_permission_ids = []
1128 self.user_repo_permission_ids = []
1129 self.user_group_repo_permission_ids = []
1129 self.user_group_repo_permission_ids = []
1130 self.user_repo_group_permission_ids = []
1130 self.user_repo_group_permission_ids = []
1131 self.user_group_repo_group_permission_ids = []
1131 self.user_group_repo_group_permission_ids = []
1132 self.user_user_group_permission_ids = []
1132 self.user_user_group_permission_ids = []
1133 self.user_group_user_group_permission_ids = []
1133 self.user_group_user_group_permission_ids = []
1134 self.user_permissions = []
1134 self.user_permissions = []
1135
1135
1136 def create_repo_group(
1136 def create_repo_group(
1137 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1137 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1138 group_name = "{prefix}_repogroup_{count}".format(
1138 group_name = "{prefix}_repogroup_{count}".format(
1139 prefix=self._test_name,
1139 prefix=self._test_name,
1140 count=len(self.repo_group_ids))
1140 count=len(self.repo_group_ids))
1141 repo_group = self.fixture.create_repo_group(
1141 repo_group = self.fixture.create_repo_group(
1142 group_name, cur_user=owner)
1142 group_name, cur_user=owner)
1143 if auto_cleanup:
1143 if auto_cleanup:
1144 self.repo_group_ids.append(repo_group.group_id)
1144 self.repo_group_ids.append(repo_group.group_id)
1145 return repo_group
1145 return repo_group
1146
1146
1147 def create_user(self, auto_cleanup=True, **kwargs):
1147 def create_user(self, auto_cleanup=True, **kwargs):
1148 user_name = "{prefix}_user_{count}".format(
1148 user_name = "{prefix}_user_{count}".format(
1149 prefix=self._test_name,
1149 prefix=self._test_name,
1150 count=len(self.user_ids))
1150 count=len(self.user_ids))
1151 user = self.fixture.create_user(user_name, **kwargs)
1151 user = self.fixture.create_user(user_name, **kwargs)
1152 if auto_cleanup:
1152 if auto_cleanup:
1153 self.user_ids.append(user.user_id)
1153 self.user_ids.append(user.user_id)
1154 return user
1154 return user
1155
1155
1156 def create_user_with_group(self):
1156 def create_user_with_group(self):
1157 user = self.create_user()
1157 user = self.create_user()
1158 user_group = self.create_user_group(members=[user])
1158 user_group = self.create_user_group(members=[user])
1159 return user, user_group
1159 return user, user_group
1160
1160
1161 def create_user_group(self, members=None, auto_cleanup=True, **kwargs):
1161 def create_user_group(self, members=None, auto_cleanup=True, **kwargs):
1162 group_name = "{prefix}_usergroup_{count}".format(
1162 group_name = "{prefix}_usergroup_{count}".format(
1163 prefix=self._test_name,
1163 prefix=self._test_name,
1164 count=len(self.user_group_ids))
1164 count=len(self.user_group_ids))
1165 user_group = self.fixture.create_user_group(group_name, **kwargs)
1165 user_group = self.fixture.create_user_group(group_name, **kwargs)
1166 if auto_cleanup:
1166 if auto_cleanup:
1167 self.user_group_ids.append(user_group.users_group_id)
1167 self.user_group_ids.append(user_group.users_group_id)
1168 if members:
1168 if members:
1169 for user in members:
1169 for user in members:
1170 UserGroupModel().add_user_to_group(user_group, user)
1170 UserGroupModel().add_user_to_group(user_group, user)
1171 return user_group
1171 return user_group
1172
1172
1173 def grant_user_permission(self, user_name, permission_name):
1173 def grant_user_permission(self, user_name, permission_name):
1174 self._inherit_default_user_permissions(user_name, False)
1174 self._inherit_default_user_permissions(user_name, False)
1175 self.user_permissions.append((user_name, permission_name))
1175 self.user_permissions.append((user_name, permission_name))
1176
1176
1177 def grant_user_permission_to_repo_group(
1177 def grant_user_permission_to_repo_group(
1178 self, repo_group, user, permission_name):
1178 self, repo_group, user, permission_name):
1179 permission = RepoGroupModel().grant_user_permission(
1179 permission = RepoGroupModel().grant_user_permission(
1180 repo_group, user, permission_name)
1180 repo_group, user, permission_name)
1181 self.user_repo_group_permission_ids.append(
1181 self.user_repo_group_permission_ids.append(
1182 (repo_group.group_id, user.user_id))
1182 (repo_group.group_id, user.user_id))
1183 return permission
1183 return permission
1184
1184
1185 def grant_user_group_permission_to_repo_group(
1185 def grant_user_group_permission_to_repo_group(
1186 self, repo_group, user_group, permission_name):
1186 self, repo_group, user_group, permission_name):
1187 permission = RepoGroupModel().grant_user_group_permission(
1187 permission = RepoGroupModel().grant_user_group_permission(
1188 repo_group, user_group, permission_name)
1188 repo_group, user_group, permission_name)
1189 self.user_group_repo_group_permission_ids.append(
1189 self.user_group_repo_group_permission_ids.append(
1190 (repo_group.group_id, user_group.users_group_id))
1190 (repo_group.group_id, user_group.users_group_id))
1191 return permission
1191 return permission
1192
1192
1193 def grant_user_permission_to_repo(
1193 def grant_user_permission_to_repo(
1194 self, repo, user, permission_name):
1194 self, repo, user, permission_name):
1195 permission = RepoModel().grant_user_permission(
1195 permission = RepoModel().grant_user_permission(
1196 repo, user, permission_name)
1196 repo, user, permission_name)
1197 self.user_repo_permission_ids.append(
1197 self.user_repo_permission_ids.append(
1198 (repo.repo_id, user.user_id))
1198 (repo.repo_id, user.user_id))
1199 return permission
1199 return permission
1200
1200
1201 def grant_user_group_permission_to_repo(
1201 def grant_user_group_permission_to_repo(
1202 self, repo, user_group, permission_name):
1202 self, repo, user_group, permission_name):
1203 permission = RepoModel().grant_user_group_permission(
1203 permission = RepoModel().grant_user_group_permission(
1204 repo, user_group, permission_name)
1204 repo, user_group, permission_name)
1205 self.user_group_repo_permission_ids.append(
1205 self.user_group_repo_permission_ids.append(
1206 (repo.repo_id, user_group.users_group_id))
1206 (repo.repo_id, user_group.users_group_id))
1207 return permission
1207 return permission
1208
1208
1209 def grant_user_permission_to_user_group(
1209 def grant_user_permission_to_user_group(
1210 self, target_user_group, user, permission_name):
1210 self, target_user_group, user, permission_name):
1211 permission = UserGroupModel().grant_user_permission(
1211 permission = UserGroupModel().grant_user_permission(
1212 target_user_group, user, permission_name)
1212 target_user_group, user, permission_name)
1213 self.user_user_group_permission_ids.append(
1213 self.user_user_group_permission_ids.append(
1214 (target_user_group.users_group_id, user.user_id))
1214 (target_user_group.users_group_id, user.user_id))
1215 return permission
1215 return permission
1216
1216
1217 def grant_user_group_permission_to_user_group(
1217 def grant_user_group_permission_to_user_group(
1218 self, target_user_group, user_group, permission_name):
1218 self, target_user_group, user_group, permission_name):
1219 permission = UserGroupModel().grant_user_group_permission(
1219 permission = UserGroupModel().grant_user_group_permission(
1220 target_user_group, user_group, permission_name)
1220 target_user_group, user_group, permission_name)
1221 self.user_group_user_group_permission_ids.append(
1221 self.user_group_user_group_permission_ids.append(
1222 (target_user_group.users_group_id, user_group.users_group_id))
1222 (target_user_group.users_group_id, user_group.users_group_id))
1223 return permission
1223 return permission
1224
1224
1225 def revoke_user_permission(self, user_name, permission_name):
1225 def revoke_user_permission(self, user_name, permission_name):
1226 self._inherit_default_user_permissions(user_name, True)
1226 self._inherit_default_user_permissions(user_name, True)
1227 UserModel().revoke_perm(user_name, permission_name)
1227 UserModel().revoke_perm(user_name, permission_name)
1228
1228
1229 def _inherit_default_user_permissions(self, user_name, value):
1229 def _inherit_default_user_permissions(self, user_name, value):
1230 user = UserModel().get_by_username(user_name)
1230 user = UserModel().get_by_username(user_name)
1231 user.inherit_default_permissions = value
1231 user.inherit_default_permissions = value
1232 Session().add(user)
1232 Session().add(user)
1233 Session().commit()
1233 Session().commit()
1234
1234
1235 def cleanup(self):
1235 def cleanup(self):
1236 self._cleanup_permissions()
1236 self._cleanup_permissions()
1237 self._cleanup_repo_groups()
1237 self._cleanup_repo_groups()
1238 self._cleanup_user_groups()
1238 self._cleanup_user_groups()
1239 self._cleanup_users()
1239 self._cleanup_users()
1240
1240
1241 def _cleanup_permissions(self):
1241 def _cleanup_permissions(self):
1242 if self.user_permissions:
1242 if self.user_permissions:
1243 for user_name, permission_name in self.user_permissions:
1243 for user_name, permission_name in self.user_permissions:
1244 self.revoke_user_permission(user_name, permission_name)
1244 self.revoke_user_permission(user_name, permission_name)
1245
1245
1246 for permission in self.user_repo_permission_ids:
1246 for permission in self.user_repo_permission_ids:
1247 RepoModel().revoke_user_permission(*permission)
1247 RepoModel().revoke_user_permission(*permission)
1248
1248
1249 for permission in self.user_group_repo_permission_ids:
1249 for permission in self.user_group_repo_permission_ids:
1250 RepoModel().revoke_user_group_permission(*permission)
1250 RepoModel().revoke_user_group_permission(*permission)
1251
1251
1252 for permission in self.user_repo_group_permission_ids:
1252 for permission in self.user_repo_group_permission_ids:
1253 RepoGroupModel().revoke_user_permission(*permission)
1253 RepoGroupModel().revoke_user_permission(*permission)
1254
1254
1255 for permission in self.user_group_repo_group_permission_ids:
1255 for permission in self.user_group_repo_group_permission_ids:
1256 RepoGroupModel().revoke_user_group_permission(*permission)
1256 RepoGroupModel().revoke_user_group_permission(*permission)
1257
1257
1258 for permission in self.user_user_group_permission_ids:
1258 for permission in self.user_user_group_permission_ids:
1259 UserGroupModel().revoke_user_permission(*permission)
1259 UserGroupModel().revoke_user_permission(*permission)
1260
1260
1261 for permission in self.user_group_user_group_permission_ids:
1261 for permission in self.user_group_user_group_permission_ids:
1262 UserGroupModel().revoke_user_group_permission(*permission)
1262 UserGroupModel().revoke_user_group_permission(*permission)
1263
1263
1264 def _cleanup_repo_groups(self):
1264 def _cleanup_repo_groups(self):
1265 def _repo_group_compare(first_group_id, second_group_id):
1265 def _repo_group_compare(first_group_id, second_group_id):
1266 """
1266 """
1267 Gives higher priority to the groups with the most complex paths
1267 Gives higher priority to the groups with the most complex paths
1268 """
1268 """
1269 first_group = RepoGroup.get(first_group_id)
1269 first_group = RepoGroup.get(first_group_id)
1270 second_group = RepoGroup.get(second_group_id)
1270 second_group = RepoGroup.get(second_group_id)
1271 first_group_parts = (
1271 first_group_parts = (
1272 len(first_group.group_name.split('/')) if first_group else 0)
1272 len(first_group.group_name.split('/')) if first_group else 0)
1273 second_group_parts = (
1273 second_group_parts = (
1274 len(second_group.group_name.split('/')) if second_group else 0)
1274 len(second_group.group_name.split('/')) if second_group else 0)
1275 return cmp(second_group_parts, first_group_parts)
1275 return cmp(second_group_parts, first_group_parts)
1276
1276
1277 sorted_repo_group_ids = sorted(
1277 sorted_repo_group_ids = sorted(
1278 self.repo_group_ids, cmp=_repo_group_compare)
1278 self.repo_group_ids, cmp=_repo_group_compare)
1279 for repo_group_id in sorted_repo_group_ids:
1279 for repo_group_id in sorted_repo_group_ids:
1280 self.fixture.destroy_repo_group(repo_group_id)
1280 self.fixture.destroy_repo_group(repo_group_id)
1281
1281
1282 def _cleanup_user_groups(self):
1282 def _cleanup_user_groups(self):
1283 def _user_group_compare(first_group_id, second_group_id):
1283 def _user_group_compare(first_group_id, second_group_id):
1284 """
1284 """
1285 Gives higher priority to the groups with the most complex paths
1285 Gives higher priority to the groups with the most complex paths
1286 """
1286 """
1287 first_group = UserGroup.get(first_group_id)
1287 first_group = UserGroup.get(first_group_id)
1288 second_group = UserGroup.get(second_group_id)
1288 second_group = UserGroup.get(second_group_id)
1289 first_group_parts = (
1289 first_group_parts = (
1290 len(first_group.users_group_name.split('/'))
1290 len(first_group.users_group_name.split('/'))
1291 if first_group else 0)
1291 if first_group else 0)
1292 second_group_parts = (
1292 second_group_parts = (
1293 len(second_group.users_group_name.split('/'))
1293 len(second_group.users_group_name.split('/'))
1294 if second_group else 0)
1294 if second_group else 0)
1295 return cmp(second_group_parts, first_group_parts)
1295 return cmp(second_group_parts, first_group_parts)
1296
1296
1297 sorted_user_group_ids = sorted(
1297 sorted_user_group_ids = sorted(
1298 self.user_group_ids, cmp=_user_group_compare)
1298 self.user_group_ids, cmp=_user_group_compare)
1299 for user_group_id in sorted_user_group_ids:
1299 for user_group_id in sorted_user_group_ids:
1300 self.fixture.destroy_user_group(user_group_id)
1300 self.fixture.destroy_user_group(user_group_id)
1301
1301
1302 def _cleanup_users(self):
1302 def _cleanup_users(self):
1303 for user_id in self.user_ids:
1303 for user_id in self.user_ids:
1304 self.fixture.destroy_user(user_id)
1304 self.fixture.destroy_user(user_id)
1305
1305
1306
1306
1307 # TODO: Think about moving this into a pytest-pyro package and make it a
1307 # TODO: Think about moving this into a pytest-pyro package and make it a
1308 # pytest plugin
1308 # pytest plugin
1309 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1309 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1310 def pytest_runtest_makereport(item, call):
1310 def pytest_runtest_makereport(item, call):
1311 """
1311 """
1312 Adding the remote traceback if the exception has this information.
1312 Adding the remote traceback if the exception has this information.
1313
1313
1314 Pyro4 attaches this information as the attribute `_pyroTraceback`
1314 Pyro4 attaches this information as the attribute `_pyroTraceback`
1315 to the exception instance.
1315 to the exception instance.
1316 """
1316 """
1317 outcome = yield
1317 outcome = yield
1318 report = outcome.get_result()
1318 report = outcome.get_result()
1319 if call.excinfo:
1319 if call.excinfo:
1320 _add_pyro_remote_traceback(report, call.excinfo.value)
1320 _add_pyro_remote_traceback(report, call.excinfo.value)
1321
1321
1322
1322
1323 def _add_pyro_remote_traceback(report, exc):
1323 def _add_pyro_remote_traceback(report, exc):
1324 pyro_traceback = getattr(exc, '_pyroTraceback', None)
1324 pyro_traceback = getattr(exc, '_pyroTraceback', None)
1325
1325
1326 if pyro_traceback:
1326 if pyro_traceback:
1327 traceback = ''.join(pyro_traceback)
1327 traceback = ''.join(pyro_traceback)
1328 section = 'Pyro4 remote traceback ' + report.when
1328 section = 'Pyro4 remote traceback ' + report.when
1329 report.sections.append((section, traceback))
1329 report.sections.append((section, traceback))
1330
1330
1331
1331
1332 @pytest.fixture(scope='session')
1332 @pytest.fixture(scope='session')
1333 def testrun():
1333 def testrun():
1334 return {
1334 return {
1335 'uuid': uuid.uuid4(),
1335 'uuid': uuid.uuid4(),
1336 'start': datetime.datetime.utcnow().isoformat(),
1336 'start': datetime.datetime.utcnow().isoformat(),
1337 'timestamp': int(time.time()),
1337 'timestamp': int(time.time()),
1338 }
1338 }
1339
1339
1340
1340
1341 @pytest.fixture(autouse=True)
1341 @pytest.fixture(autouse=True)
1342 def collect_appenlight_stats(request, testrun):
1342 def collect_appenlight_stats(request, testrun):
1343 """
1343 """
1344 This fixture reports memory consumtion of single tests.
1344 This fixture reports memory consumtion of single tests.
1345
1345
1346 It gathers data based on `psutil` and sends them to Appenlight. The option
1346 It gathers data based on `psutil` and sends them to Appenlight. The option
1347 ``--ae`` has te be used to enable this fixture and the API key for your
1347 ``--ae`` has te be used to enable this fixture and the API key for your
1348 application has to be provided in ``--ae-key``.
1348 application has to be provided in ``--ae-key``.
1349 """
1349 """
1350 try:
1350 try:
1351 # cygwin cannot have yet psutil support.
1351 # cygwin cannot have yet psutil support.
1352 import psutil
1352 import psutil
1353 except ImportError:
1353 except ImportError:
1354 return
1354 return
1355
1355
1356 if not request.config.getoption('--appenlight'):
1356 if not request.config.getoption('--appenlight'):
1357 return
1357 return
1358 else:
1358 else:
1359 # Only request the pylonsapp fixture if appenlight tracking is
1359 # Only request the pylonsapp fixture if appenlight tracking is
1360 # enabled. This will speed up a test run of unit tests by 2 to 3
1360 # enabled. This will speed up a test run of unit tests by 2 to 3
1361 # seconds if appenlight is not enabled.
1361 # seconds if appenlight is not enabled.
1362 pylonsapp = request.getfuncargvalue("pylonsapp")
1362 pylonsapp = request.getfuncargvalue("pylonsapp")
1363 url = '{}/api/logs'.format(request.config.getoption('--appenlight-url'))
1363 url = '{}/api/logs'.format(request.config.getoption('--appenlight-url'))
1364 client = AppenlightClient(
1364 client = AppenlightClient(
1365 url=url,
1365 url=url,
1366 api_key=request.config.getoption('--appenlight-api-key'),
1366 api_key=request.config.getoption('--appenlight-api-key'),
1367 namespace=request.node.nodeid,
1367 namespace=request.node.nodeid,
1368 request=str(testrun['uuid']),
1368 request=str(testrun['uuid']),
1369 testrun=testrun)
1369 testrun=testrun)
1370
1370
1371 client.collect({
1371 client.collect({
1372 'message': "Starting",
1372 'message': "Starting",
1373 })
1373 })
1374
1374
1375 server_and_port = pylonsapp.config['vcs.server']
1375 server_and_port = pylonsapp.config['vcs.server']
1376 server = create_vcsserver_proxy(server_and_port)
1376 server = create_vcsserver_proxy(server_and_port)
1377 with server:
1377 with server:
1378 vcs_pid = server.get_pid()
1378 vcs_pid = server.get_pid()
1379 server.run_gc()
1379 server.run_gc()
1380 vcs_process = psutil.Process(vcs_pid)
1380 vcs_process = psutil.Process(vcs_pid)
1381 mem = vcs_process.memory_info()
1381 mem = vcs_process.memory_info()
1382 client.tag_before('vcsserver.rss', mem.rss)
1382 client.tag_before('vcsserver.rss', mem.rss)
1383 client.tag_before('vcsserver.vms', mem.vms)
1383 client.tag_before('vcsserver.vms', mem.vms)
1384
1384
1385 test_process = psutil.Process()
1385 test_process = psutil.Process()
1386 mem = test_process.memory_info()
1386 mem = test_process.memory_info()
1387 client.tag_before('test.rss', mem.rss)
1387 client.tag_before('test.rss', mem.rss)
1388 client.tag_before('test.vms', mem.vms)
1388 client.tag_before('test.vms', mem.vms)
1389
1389
1390 client.tag_before('time', time.time())
1390 client.tag_before('time', time.time())
1391
1391
1392 @request.addfinalizer
1392 @request.addfinalizer
1393 def send_stats():
1393 def send_stats():
1394 client.tag_after('time', time.time())
1394 client.tag_after('time', time.time())
1395 with server:
1395 with server:
1396 gc_stats = server.run_gc()
1396 gc_stats = server.run_gc()
1397 for tag, value in gc_stats.items():
1397 for tag, value in gc_stats.items():
1398 client.tag_after(tag, value)
1398 client.tag_after(tag, value)
1399 mem = vcs_process.memory_info()
1399 mem = vcs_process.memory_info()
1400 client.tag_after('vcsserver.rss', mem.rss)
1400 client.tag_after('vcsserver.rss', mem.rss)
1401 client.tag_after('vcsserver.vms', mem.vms)
1401 client.tag_after('vcsserver.vms', mem.vms)
1402
1402
1403 mem = test_process.memory_info()
1403 mem = test_process.memory_info()
1404 client.tag_after('test.rss', mem.rss)
1404 client.tag_after('test.rss', mem.rss)
1405 client.tag_after('test.vms', mem.vms)
1405 client.tag_after('test.vms', mem.vms)
1406
1406
1407 client.collect({
1407 client.collect({
1408 'message': "Finished",
1408 'message': "Finished",
1409 })
1409 })
1410 client.send_stats()
1410 client.send_stats()
1411
1411
1412 return client
1412 return client
1413
1413
1414
1414
1415 class AppenlightClient():
1415 class AppenlightClient():
1416
1416
1417 url_template = '{url}?protocol_version=0.5'
1417 url_template = '{url}?protocol_version=0.5'
1418
1418
1419 def __init__(
1419 def __init__(
1420 self, url, api_key, add_server=True, add_timestamp=True,
1420 self, url, api_key, add_server=True, add_timestamp=True,
1421 namespace=None, request=None, testrun=None):
1421 namespace=None, request=None, testrun=None):
1422 self.url = self.url_template.format(url=url)
1422 self.url = self.url_template.format(url=url)
1423 self.api_key = api_key
1423 self.api_key = api_key
1424 self.add_server = add_server
1424 self.add_server = add_server
1425 self.add_timestamp = add_timestamp
1425 self.add_timestamp = add_timestamp
1426 self.namespace = namespace
1426 self.namespace = namespace
1427 self.request = request
1427 self.request = request
1428 self.server = socket.getfqdn(socket.gethostname())
1428 self.server = socket.getfqdn(socket.gethostname())
1429 self.tags_before = {}
1429 self.tags_before = {}
1430 self.tags_after = {}
1430 self.tags_after = {}
1431 self.stats = []
1431 self.stats = []
1432 self.testrun = testrun or {}
1432 self.testrun = testrun or {}
1433
1433
1434 def tag_before(self, tag, value):
1434 def tag_before(self, tag, value):
1435 self.tags_before[tag] = value
1435 self.tags_before[tag] = value
1436
1436
1437 def tag_after(self, tag, value):
1437 def tag_after(self, tag, value):
1438 self.tags_after[tag] = value
1438 self.tags_after[tag] = value
1439
1439
1440 def collect(self, data):
1440 def collect(self, data):
1441 if self.add_server:
1441 if self.add_server:
1442 data.setdefault('server', self.server)
1442 data.setdefault('server', self.server)
1443 if self.add_timestamp:
1443 if self.add_timestamp:
1444 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1444 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1445 if self.namespace:
1445 if self.namespace:
1446 data.setdefault('namespace', self.namespace)
1446 data.setdefault('namespace', self.namespace)
1447 if self.request:
1447 if self.request:
1448 data.setdefault('request', self.request)
1448 data.setdefault('request', self.request)
1449 self.stats.append(data)
1449 self.stats.append(data)
1450
1450
1451 def send_stats(self):
1451 def send_stats(self):
1452 tags = [
1452 tags = [
1453 ('testrun', self.request),
1453 ('testrun', self.request),
1454 ('testrun.start', self.testrun['start']),
1454 ('testrun.start', self.testrun['start']),
1455 ('testrun.timestamp', self.testrun['timestamp']),
1455 ('testrun.timestamp', self.testrun['timestamp']),
1456 ('test', self.namespace),
1456 ('test', self.namespace),
1457 ]
1457 ]
1458 for key, value in self.tags_before.items():
1458 for key, value in self.tags_before.items():
1459 tags.append((key + '.before', value))
1459 tags.append((key + '.before', value))
1460 try:
1460 try:
1461 delta = self.tags_after[key] - value
1461 delta = self.tags_after[key] - value
1462 tags.append((key + '.delta', delta))
1462 tags.append((key + '.delta', delta))
1463 except Exception:
1463 except Exception:
1464 pass
1464 pass
1465 for key, value in self.tags_after.items():
1465 for key, value in self.tags_after.items():
1466 tags.append((key + '.after', value))
1466 tags.append((key + '.after', value))
1467 self.collect({
1467 self.collect({
1468 'message': "Collected tags",
1468 'message': "Collected tags",
1469 'tags': tags,
1469 'tags': tags,
1470 })
1470 })
1471
1471
1472 response = requests.post(
1472 response = requests.post(
1473 self.url,
1473 self.url,
1474 headers={
1474 headers={
1475 'X-appenlight-api-key': self.api_key},
1475 'X-appenlight-api-key': self.api_key},
1476 json=self.stats,
1476 json=self.stats,
1477 )
1477 )
1478
1478
1479 if not response.status_code == 200:
1479 if not response.status_code == 200:
1480 pprint.pprint(self.stats)
1480 pprint.pprint(self.stats)
1481 print response.headers
1481 print response.headers
1482 print response.text
1482 print response.text
1483 raise Exception('Sending to appenlight failed')
1483 raise Exception('Sending to appenlight failed')
1484
1484
1485
1485
1486 @pytest.fixture
1486 @pytest.fixture
1487 def gist_util(request, pylonsapp):
1487 def gist_util(request, pylonsapp):
1488 """
1488 """
1489 Provides a wired instance of `GistUtility` with integrated cleanup.
1489 Provides a wired instance of `GistUtility` with integrated cleanup.
1490 """
1490 """
1491 utility = GistUtility()
1491 utility = GistUtility()
1492 request.addfinalizer(utility.cleanup)
1492 request.addfinalizer(utility.cleanup)
1493 return utility
1493 return utility
1494
1494
1495
1495
1496 class GistUtility(object):
1496 class GistUtility(object):
1497 def __init__(self):
1497 def __init__(self):
1498 self.fixture = Fixture()
1498 self.fixture = Fixture()
1499 self.gist_ids = []
1499 self.gist_ids = []
1500
1500
1501 def create_gist(self, **kwargs):
1501 def create_gist(self, **kwargs):
1502 gist = self.fixture.create_gist(**kwargs)
1502 gist = self.fixture.create_gist(**kwargs)
1503 self.gist_ids.append(gist.gist_id)
1503 self.gist_ids.append(gist.gist_id)
1504 return gist
1504 return gist
1505
1505
1506 def cleanup(self):
1506 def cleanup(self):
1507 for id_ in self.gist_ids:
1507 for id_ in self.gist_ids:
1508 self.fixture.destroy_gists(str(id_))
1508 self.fixture.destroy_gists(str(id_))
1509
1509
1510
1510
1511 @pytest.fixture
1511 @pytest.fixture
1512 def enabled_backends(request):
1512 def enabled_backends(request):
1513 backends = request.config.option.backends
1513 backends = request.config.option.backends
1514 return backends[:]
1514 return backends[:]
1515
1515
1516
1516
1517 @pytest.fixture
1517 @pytest.fixture
1518 def settings_util(request):
1518 def settings_util(request):
1519 """
1519 """
1520 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1520 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1521 """
1521 """
1522 utility = SettingsUtility()
1522 utility = SettingsUtility()
1523 request.addfinalizer(utility.cleanup)
1523 request.addfinalizer(utility.cleanup)
1524 return utility
1524 return utility
1525
1525
1526
1526
1527 class SettingsUtility(object):
1527 class SettingsUtility(object):
1528 def __init__(self):
1528 def __init__(self):
1529 self.rhodecode_ui_ids = []
1529 self.rhodecode_ui_ids = []
1530 self.rhodecode_setting_ids = []
1530 self.rhodecode_setting_ids = []
1531 self.repo_rhodecode_ui_ids = []
1531 self.repo_rhodecode_ui_ids = []
1532 self.repo_rhodecode_setting_ids = []
1532 self.repo_rhodecode_setting_ids = []
1533
1533
1534 def create_repo_rhodecode_ui(
1534 def create_repo_rhodecode_ui(
1535 self, repo, section, value, key=None, active=True, cleanup=True):
1535 self, repo, section, value, key=None, active=True, cleanup=True):
1536 key = key or hashlib.sha1(
1536 key = key or hashlib.sha1(
1537 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1537 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1538
1538
1539 setting = RepoRhodeCodeUi()
1539 setting = RepoRhodeCodeUi()
1540 setting.repository_id = repo.repo_id
1540 setting.repository_id = repo.repo_id
1541 setting.ui_section = section
1541 setting.ui_section = section
1542 setting.ui_value = value
1542 setting.ui_value = value
1543 setting.ui_key = key
1543 setting.ui_key = key
1544 setting.ui_active = active
1544 setting.ui_active = active
1545 Session().add(setting)
1545 Session().add(setting)
1546 Session().commit()
1546 Session().commit()
1547
1547
1548 if cleanup:
1548 if cleanup:
1549 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1549 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1550 return setting
1550 return setting
1551
1551
1552 def create_rhodecode_ui(
1552 def create_rhodecode_ui(
1553 self, section, value, key=None, active=True, cleanup=True):
1553 self, section, value, key=None, active=True, cleanup=True):
1554 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1554 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1555
1555
1556 setting = RhodeCodeUi()
1556 setting = RhodeCodeUi()
1557 setting.ui_section = section
1557 setting.ui_section = section
1558 setting.ui_value = value
1558 setting.ui_value = value
1559 setting.ui_key = key
1559 setting.ui_key = key
1560 setting.ui_active = active
1560 setting.ui_active = active
1561 Session().add(setting)
1561 Session().add(setting)
1562 Session().commit()
1562 Session().commit()
1563
1563
1564 if cleanup:
1564 if cleanup:
1565 self.rhodecode_ui_ids.append(setting.ui_id)
1565 self.rhodecode_ui_ids.append(setting.ui_id)
1566 return setting
1566 return setting
1567
1567
1568 def create_repo_rhodecode_setting(
1568 def create_repo_rhodecode_setting(
1569 self, repo, name, value, type_, cleanup=True):
1569 self, repo, name, value, type_, cleanup=True):
1570 setting = RepoRhodeCodeSetting(
1570 setting = RepoRhodeCodeSetting(
1571 repo.repo_id, key=name, val=value, type=type_)
1571 repo.repo_id, key=name, val=value, type=type_)
1572 Session().add(setting)
1572 Session().add(setting)
1573 Session().commit()
1573 Session().commit()
1574
1574
1575 if cleanup:
1575 if cleanup:
1576 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1576 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1577 return setting
1577 return setting
1578
1578
1579 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1579 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1580 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1580 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1581 Session().add(setting)
1581 Session().add(setting)
1582 Session().commit()
1582 Session().commit()
1583
1583
1584 if cleanup:
1584 if cleanup:
1585 self.rhodecode_setting_ids.append(setting.app_settings_id)
1585 self.rhodecode_setting_ids.append(setting.app_settings_id)
1586
1586
1587 return setting
1587 return setting
1588
1588
1589 def cleanup(self):
1589 def cleanup(self):
1590 for id_ in self.rhodecode_ui_ids:
1590 for id_ in self.rhodecode_ui_ids:
1591 setting = RhodeCodeUi.get(id_)
1591 setting = RhodeCodeUi.get(id_)
1592 Session().delete(setting)
1592 Session().delete(setting)
1593
1593
1594 for id_ in self.rhodecode_setting_ids:
1594 for id_ in self.rhodecode_setting_ids:
1595 setting = RhodeCodeSetting.get(id_)
1595 setting = RhodeCodeSetting.get(id_)
1596 Session().delete(setting)
1596 Session().delete(setting)
1597
1597
1598 for id_ in self.repo_rhodecode_ui_ids:
1598 for id_ in self.repo_rhodecode_ui_ids:
1599 setting = RepoRhodeCodeUi.get(id_)
1599 setting = RepoRhodeCodeUi.get(id_)
1600 Session().delete(setting)
1600 Session().delete(setting)
1601
1601
1602 for id_ in self.repo_rhodecode_setting_ids:
1602 for id_ in self.repo_rhodecode_setting_ids:
1603 setting = RepoRhodeCodeSetting.get(id_)
1603 setting = RepoRhodeCodeSetting.get(id_)
1604 Session().delete(setting)
1604 Session().delete(setting)
1605
1605
1606 Session().commit()
1606 Session().commit()
1607
1607
1608
1608
1609 @pytest.fixture
1609 @pytest.fixture
1610 def no_notifications(request):
1610 def no_notifications(request):
1611 notification_patcher = mock.patch(
1611 notification_patcher = mock.patch(
1612 'rhodecode.model.notification.NotificationModel.create')
1612 'rhodecode.model.notification.NotificationModel.create')
1613 notification_patcher.start()
1613 notification_patcher.start()
1614 request.addfinalizer(notification_patcher.stop)
1614 request.addfinalizer(notification_patcher.stop)
1615
1615
1616
1616
1617 @pytest.fixture
1617 @pytest.fixture
1618 def silence_action_logger(request):
1618 def silence_action_logger(request):
1619 notification_patcher = mock.patch(
1619 notification_patcher = mock.patch(
1620 'rhodecode.lib.utils.action_logger')
1620 'rhodecode.lib.utils.action_logger')
1621 notification_patcher.start()
1621 notification_patcher.start()
1622 request.addfinalizer(notification_patcher.stop)
1622 request.addfinalizer(notification_patcher.stop)
1623
1623
1624
1624
1625 @pytest.fixture(scope='session')
1625 @pytest.fixture(scope='session')
1626 def repeat(request):
1626 def repeat(request):
1627 """
1627 """
1628 The number of repetitions is based on this fixture.
1628 The number of repetitions is based on this fixture.
1629
1629
1630 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1630 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1631 tests are not too slow in our default test suite.
1631 tests are not too slow in our default test suite.
1632 """
1632 """
1633 return request.config.getoption('--repeat')
1633 return request.config.getoption('--repeat')
1634
1634
1635
1635
1636 @pytest.fixture
1636 @pytest.fixture
1637 def rhodecode_fixtures():
1637 def rhodecode_fixtures():
1638 return Fixture()
1638 return Fixture()
1639
1639
1640
1640
1641 @pytest.fixture
1641 @pytest.fixture
1642 def request_stub():
1642 def request_stub():
1643 """
1643 """
1644 Stub request object.
1644 Stub request object.
1645 """
1645 """
1646 request = pyramid.testing.DummyRequest()
1646 request = pyramid.testing.DummyRequest()
1647 request.scheme = 'https'
1647 request.scheme = 'https'
1648 return request
1648 return request
1649
1649
1650
1650
1651 @pytest.fixture
1651 @pytest.fixture
1652 def config_stub(request, request_stub):
1652 def config_stub(request, request_stub):
1653 """
1653 """
1654 Set up pyramid.testing and return the Configurator.
1654 Set up pyramid.testing and return the Configurator.
1655 """
1655 """
1656 config = pyramid.testing.setUp(request=request_stub)
1656 config = pyramid.testing.setUp(request=request_stub)
1657
1657
1658 @request.addfinalizer
1658 @request.addfinalizer
1659 def cleanup():
1659 def cleanup():
1660 pyramid.testing.tearDown()
1660 pyramid.testing.tearDown()
1661
1661
1662 return config
1662 return config
1663
1663
1664
1664
1665 @pytest.fixture
1665 @pytest.fixture
1666 def StubIntegrationType():
1666 def StubIntegrationType():
1667 class _StubIntegrationType(IntegrationTypeBase):
1667 class _StubIntegrationType(IntegrationTypeBase):
1668 """ Test integration type class """
1668 """ Test integration type class """
1669
1669
1670 key = 'test'
1670 key = 'test'
1671 display_name = 'Test integration type'
1671 display_name = 'Test integration type'
1672 description = 'A test integration type for testing'
1672 description = 'A test integration type for testing'
1673 icon = 'test_icon_html_image'
1673 icon = 'test_icon_html_image'
1674
1674
1675 def __init__(self, settings):
1675 def __init__(self, settings):
1676 super(_StubIntegrationType, self).__init__(settings)
1676 super(_StubIntegrationType, self).__init__(settings)
1677 self.sent_events = [] # for testing
1677 self.sent_events = [] # for testing
1678
1678
1679 def send_event(self, event):
1679 def send_event(self, event):
1680 self.sent_events.append(event)
1680 self.sent_events.append(event)
1681
1681
1682 def settings_schema(self):
1682 def settings_schema(self):
1683 class SettingsSchema(colander.Schema):
1683 class SettingsSchema(colander.Schema):
1684 test_string_field = colander.SchemaNode(
1684 test_string_field = colander.SchemaNode(
1685 colander.String(),
1685 colander.String(),
1686 missing=colander.required,
1686 missing=colander.required,
1687 title='test string field',
1687 title='test string field',
1688 )
1688 )
1689 test_int_field = colander.SchemaNode(
1689 test_int_field = colander.SchemaNode(
1690 colander.Int(),
1690 colander.Int(),
1691 title='some integer setting',
1691 title='some integer setting',
1692 )
1692 )
1693 return SettingsSchema()
1693 return SettingsSchema()
1694
1694
1695
1695
1696 integration_type_registry.register_integration_type(_StubIntegrationType)
1696 integration_type_registry.register_integration_type(_StubIntegrationType)
1697 return _StubIntegrationType
1697 return _StubIntegrationType
1698
1698
1699 @pytest.fixture
1699 @pytest.fixture
1700 def stub_integration_settings():
1700 def stub_integration_settings():
1701 return {
1701 return {
1702 'test_string_field': 'some data',
1702 'test_string_field': 'some data',
1703 'test_int_field': 100,
1703 'test_int_field': 100,
1704 }
1704 }
1705
1705
1706
1706
1707 @pytest.fixture
1707 @pytest.fixture
1708 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1708 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1709 stub_integration_settings):
1709 stub_integration_settings):
1710 integration = IntegrationModel().create(
1710 integration = IntegrationModel().create(
1711 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1711 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1712 name='test repo integration', scope=repo_stub)
1712 name='test repo integration',
1713 repo=repo_stub, repo_group=None, child_repos_only=None)
1713
1714
1714 @request.addfinalizer
1715 @request.addfinalizer
1715 def cleanup():
1716 def cleanup():
1716 IntegrationModel().delete(integration)
1717 IntegrationModel().delete(integration)
1717
1718
1718 return integration
1719 return integration
1719
1720
1720
1721
1721 @pytest.fixture
1722 @pytest.fixture
1722 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1723 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1723 stub_integration_settings):
1724 stub_integration_settings):
1724 integration = IntegrationModel().create(
1725 integration = IntegrationModel().create(
1725 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1726 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1726 name='test repogroup integration', scope=test_repo_group)
1727 name='test repogroup integration',
1728 repo=None, repo_group=test_repo_group, child_repos_only=True)
1729
1730 @request.addfinalizer
1731 def cleanup():
1732 IntegrationModel().delete(integration)
1733
1734 return integration
1735
1736
1737 @pytest.fixture
1738 def repogroup_recursive_integration_stub(request, test_repo_group,
1739 StubIntegrationType, stub_integration_settings):
1740 integration = IntegrationModel().create(
1741 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1742 name='test recursive repogroup integration',
1743 repo=None, repo_group=test_repo_group, child_repos_only=False)
1727
1744
1728 @request.addfinalizer
1745 @request.addfinalizer
1729 def cleanup():
1746 def cleanup():
1730 IntegrationModel().delete(integration)
1747 IntegrationModel().delete(integration)
1731
1748
1732 return integration
1749 return integration
1733
1750
1734
1751
1735 @pytest.fixture
1752 @pytest.fixture
1736 def global_integration_stub(request, StubIntegrationType,
1753 def global_integration_stub(request, StubIntegrationType,
1737 stub_integration_settings):
1754 stub_integration_settings):
1738 integration = IntegrationModel().create(
1755 integration = IntegrationModel().create(
1739 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1756 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1740 name='test global integration', scope='global')
1757 name='test global integration',
1758 repo=None, repo_group=None, child_repos_only=None)
1741
1759
1742 @request.addfinalizer
1760 @request.addfinalizer
1743 def cleanup():
1761 def cleanup():
1744 IntegrationModel().delete(integration)
1762 IntegrationModel().delete(integration)
1745
1763
1746 return integration
1764 return integration
1747
1765
1748
1766
1749 @pytest.fixture
1767 @pytest.fixture
1750 def root_repos_integration_stub(request, StubIntegrationType,
1768 def root_repos_integration_stub(request, StubIntegrationType,
1751 stub_integration_settings):
1769 stub_integration_settings):
1752 integration = IntegrationModel().create(
1770 integration = IntegrationModel().create(
1753 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1771 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1754 name='test global integration', scope='root_repos')
1772 name='test global integration',
1773 repo=None, repo_group=None, child_repos_only=True)
1755
1774
1756 @request.addfinalizer
1775 @request.addfinalizer
1757 def cleanup():
1776 def cleanup():
1758 IntegrationModel().delete(integration)
1777 IntegrationModel().delete(integration)
1759
1778
1760 return integration
1779 return integration
General Comments 0
You need to be logged in to leave comments. Login now