##// END OF EJS Templates
sessions: added interface to show, and cleanup user auth sessions.
marcink -
r1295:5854ddda default
parent child Browse files
Show More
@@ -0,0 +1,125 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 #
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
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import datetime
22 import dateutil
23 from rhodecode.model.db import DbSession, Session
24
25
26 class CleanupCommand(Exception):
27 pass
28
29
30 class BaseAuthSessions(object):
31 SESSION_TYPE = None
32
33 def __init__(self, config):
34 session_conf = {}
35 for k, v in config.items():
36 if k.startswith('beaker.session'):
37 session_conf[k] = v
38 self.config = session_conf
39
40 def get_count(self):
41 raise NotImplementedError
42
43 def get_expired_count(self):
44 raise NotImplementedError
45
46 def clean_sessions(self, older_than_seconds=None):
47 raise NotImplementedError
48
49 def _seconds_to_date(self, seconds):
50 return datetime.datetime.utcnow() - dateutil.relativedelta.relativedelta(
51 seconds=seconds)
52
53
54 class DbAuthSessions(BaseAuthSessions):
55 SESSION_TYPE = 'ext:database'
56
57 def get_count(self):
58 return DbSession.query().count()
59
60 def get_expired_count(self, older_than_seconds=None):
61 expiry_date = self._seconds_to_date(older_than_seconds)
62 return DbSession.query().filter(DbSession.accessed < expiry_date).count()
63
64 def clean_sessions(self, older_than_seconds=None):
65 expiry_date = self._seconds_to_date(older_than_seconds)
66 DbSession.query().filter(DbSession.accessed < expiry_date).delete()
67 Session().commit()
68
69
70 class FileAuthSessions(BaseAuthSessions):
71 SESSION_TYPE = 'file sessions'
72
73 def get_count(self):
74 return 'NOT AVAILABLE'
75
76 def get_expired_count(self):
77 return self.get_count()
78
79 def clean_sessions(self, older_than_seconds=None):
80 data_dir = self.config.get('beaker.session.data_dir')
81 raise CleanupCommand(
82 'Please execute this command: '
83 '`find . -mtime +60 -exec rm {{}} \;` inside {} directory'.format(
84 data_dir))
85
86
87 class MemcachedAuthSessions(BaseAuthSessions):
88 SESSION_TYPE = 'ext:memcached'
89
90 def get_count(self):
91 return 'NOT AVAILABLE'
92
93 def get_expired_count(self):
94 return self.get_count()
95
96 def clean_sessions(self, older_than_seconds=None):
97 raise CleanupCommand('Cleanup for this session type not yet available')
98
99
100 class MemoryAuthSessions(BaseAuthSessions):
101 SESSION_TYPE = 'memory'
102
103 def get_count(self):
104 return 'NOT AVAILABLE'
105
106 def get_expired_count(self):
107 return self.get_count()
108
109 def clean_sessions(self, older_than_seconds=None):
110 raise CleanupCommand('Cleanup for this session type not yet available')
111
112
113 def get_session_handler(session_type):
114 types = {
115 'file': FileAuthSessions,
116 'ext:memcached': MemcachedAuthSessions,
117 'ext:database': DbAuthSessions,
118 'memory': MemoryAuthSessions
119 }
120
121 try:
122 return types[session_type]
123 except KeyError:
124 raise ValueError(
125 'This type {} is not supported'.format(session_type))
@@ -0,0 +1,60 b''
1 <div class="panel panel-default">
2 <div class="panel-heading">
3 <h3 class="panel-title">${_('User Sessions Configuration')}</h3>
4 </div>
5 <div class="panel-body">
6 <%
7 elems = [
8 (_('Session type'), c.session_model.SESSION_TYPE, ''),
9 (_('Session expiration period'), '{} seconds'.format(c.session_conf.get('beaker.session.timeout', 0)), ''),
10
11 (_('Total sessions'), c.session_count, ''),
12 (_('Expired sessions ({} days)').format(c.cleanup_older_days ), c.session_expired_count, ''),
13
14 ]
15 %>
16 <dl class="dl-horizontal settings">
17 %for dt, dd, tt in elems:
18 <dt>${dt}:</dt>
19 <dd title="${tt}">${dd}</dd>
20 %endfor
21 </dl>
22 </div>
23 </div>
24
25
26 <div class="panel panel-warning">
27 <div class="panel-heading">
28 <h3 class="panel-title">${_('Cleanup Old Sessions')}</h3>
29 </div>
30 <div class="panel-body">
31 ${h.secure_form(h.url('admin_settings_sessions_cleanup'), method='post')}
32
33 <div style="margin: 0 0 20px 0" class="fake-space">
34 ${_('Cleanup all sessions that were not active during choosen time frame')} <br/>
35 ${_('Picking All will log-out all users in the system, and each user will be required to log in again.')}
36 </div>
37 <select id="expire_days" name="expire_days">
38 % for n in [60, 90, 30, 7, 0]:
39 <option value="${n}">${'{} days'.format(n) if n != 0 else 'All'}</option>
40 % endfor
41 </select>
42 <button class="btn btn-small" type="submit"
43 onclick="return confirm('${_('Confirm to cleanup user sessions')}');">
44 ${_('Cleanup sessions')}
45 </button>
46 ${h.end_form()}
47 </div>
48 </div>
49
50
51 <script type="text/javascript">
52 $(document).ready(function() {
53 $('#expire_days').select2({
54 containerCssClass: 'drop-menu',
55 dropdownCssClass: 'drop-menu-dropdown',
56 dropdownAutoWidth: true,
57 minimumResultsForSearch: -1
58 });
59 });
60 </script> No newline at end of file
@@ -1,126 +1,128 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23 import collections
24 24
25 25 from pylons import url
26 26 from zope.interface import implementer
27 27
28 28 from rhodecode.admin.interfaces import IAdminNavigationRegistry
29 29 from rhodecode.lib.utils import get_registry
30 30 from rhodecode.translation import _
31 31
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35 NavListEntry = collections.namedtuple('NavListEntry', ['key', 'name', 'url'])
36 36
37 37
38 38 class NavEntry(object):
39 39 """
40 40 Represents an entry in the admin navigation.
41 41
42 42 :param key: Unique identifier used to store reference in an OrderedDict.
43 43 :param name: Display name, usually a translation string.
44 44 :param view_name: Name of the view, used generate the URL.
45 45 :param pyramid: Indicator to use pyramid for URL generation. This should
46 46 be removed as soon as we are fully migrated to pyramid.
47 47 """
48 48
49 49 def __init__(self, key, name, view_name, pyramid=False):
50 50 self.key = key
51 51 self.name = name
52 52 self.view_name = view_name
53 53 self.pyramid = pyramid
54 54
55 55 def generate_url(self, request):
56 56 if self.pyramid:
57 57 if hasattr(request, 'route_path'):
58 58 return request.route_path(self.view_name)
59 59 else:
60 60 # TODO: johbo: Remove this after migrating to pyramid.
61 61 # We need the pyramid request here to generate URLs to pyramid
62 62 # views from within pylons views.
63 63 from pyramid.threadlocal import get_current_request
64 64 pyramid_request = get_current_request()
65 65 return pyramid_request.route_path(self.view_name)
66 66 else:
67 67 return url(self.view_name)
68 68
69 69
70 70 @implementer(IAdminNavigationRegistry)
71 71 class NavigationRegistry(object):
72 72
73 73 _base_entries = [
74 74 NavEntry('global', _('Global'), 'admin_settings_global'),
75 75 NavEntry('vcs', _('VCS'), 'admin_settings_vcs'),
76 76 NavEntry('visual', _('Visual'), 'admin_settings_visual'),
77 77 NavEntry('mapping', _('Remap and Rescan'), 'admin_settings_mapping'),
78 78 NavEntry('issuetracker', _('Issue Tracker'),
79 79 'admin_settings_issuetracker'),
80 80 NavEntry('email', _('Email'), 'admin_settings_email'),
81 81 NavEntry('hooks', _('Hooks'), 'admin_settings_hooks'),
82 82 NavEntry('search', _('Full Text Search'), 'admin_settings_search'),
83 83 NavEntry('integrations', _('Integrations'),
84 84 'global_integrations_home', pyramid=True),
85 85 NavEntry('system', _('System Info'), 'admin_settings_system'),
86 NavEntry('session', _('User Sessions'), 'admin_settings_sessions'),
86 87 NavEntry('open_source', _('Open Source Licenses'),
87 88 'admin_settings_open_source', pyramid=True),
89
88 90 # TODO: marcink: we disable supervisor now until the supervisor stats
89 91 # page is fixed in the nix configuration
90 92 # NavEntry('supervisor', _('Supervisor'), 'admin_settings_supervisor'),
91 93 ]
92 94
93 95 _labs_entry = NavEntry('labs', _('Labs'),
94 96 'admin_settings_labs')
95 97
96 98 def __init__(self, labs_active=False):
97 99 self._registered_entries = collections.OrderedDict([
98 100 (item.key, item) for item in self.__class__._base_entries
99 101 ])
100 102
101 103 if labs_active:
102 104 self.add_entry(self._labs_entry)
103 105
104 106 def add_entry(self, entry):
105 107 self._registered_entries[entry.key] = entry
106 108
107 109 def get_navlist(self, request):
108 110 navlist = [NavListEntry(i.key, i.name, i.generate_url(request))
109 111 for i in self._registered_entries.values()]
110 112 return navlist
111 113
112 114
113 115 def navigation_registry(request):
114 116 """
115 117 Helper that returns the admin navigation registry.
116 118 """
117 119 pyramid_registry = get_registry(request)
118 120 nav_registry = pyramid_registry.queryUtility(IAdminNavigationRegistry)
119 121 return nav_registry
120 122
121 123
122 124 def navigation_list(request):
123 125 """
124 126 Helper that returns the admin navigation as list of NavListEntry objects.
125 127 """
126 128 return navigation_registry(request).get_navlist(request)
@@ -1,1173 +1,1179 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 from rhodecode.config import routing_links
36 36
37 37 # prefix for non repository related links needs to be prefixed with `/`
38 38 ADMIN_PREFIX = '/_admin'
39 39 STATIC_FILE_PREFIX = '/_static'
40 40
41 41 # Default requirements for URL parts
42 42 URL_NAME_REQUIREMENTS = {
43 43 # group name can have a slash in them, but they must not end with a slash
44 44 'group_name': r'.*?[^/]',
45 45 'repo_group_name': r'.*?[^/]',
46 46 # repo names can have a slash in them, but they must not end with a slash
47 47 'repo_name': r'.*?[^/]',
48 48 # file path eats up everything at the end
49 49 'f_path': r'.*',
50 50 # reference types
51 51 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
52 52 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
53 53 }
54 54
55 55
56 56 def add_route_requirements(route_path, requirements):
57 57 """
58 58 Adds regex requirements to pyramid routes using a mapping dict
59 59
60 60 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
61 61 '/{action}/{id:\d+}'
62 62
63 63 """
64 64 for key, regex in requirements.items():
65 65 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
66 66 return route_path
67 67
68 68
69 69 class JSRoutesMapper(Mapper):
70 70 """
71 71 Wrapper for routes.Mapper to make pyroutes compatible url definitions
72 72 """
73 73 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
74 74 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
75 75 def __init__(self, *args, **kw):
76 76 super(JSRoutesMapper, self).__init__(*args, **kw)
77 77 self._jsroutes = []
78 78
79 79 def connect(self, *args, **kw):
80 80 """
81 81 Wrapper for connect to take an extra argument jsroute=True
82 82
83 83 :param jsroute: boolean, if True will add the route to the pyroutes list
84 84 """
85 85 if kw.pop('jsroute', False):
86 86 if not self._named_route_regex.match(args[0]):
87 87 raise Exception('only named routes can be added to pyroutes')
88 88 self._jsroutes.append(args[0])
89 89
90 90 super(JSRoutesMapper, self).connect(*args, **kw)
91 91
92 92 def _extract_route_information(self, route):
93 93 """
94 94 Convert a route into tuple(name, path, args), eg:
95 95 ('user_profile', '/profile/%(username)s', ['username'])
96 96 """
97 97 routepath = route.routepath
98 98 def replace(matchobj):
99 99 if matchobj.group(1):
100 100 return "%%(%s)s" % matchobj.group(1).split(':')[0]
101 101 else:
102 102 return "%%(%s)s" % matchobj.group(2)
103 103
104 104 routepath = self._argument_prog.sub(replace, routepath)
105 105 return (
106 106 route.name,
107 107 routepath,
108 108 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
109 109 for arg in self._argument_prog.findall(route.routepath)]
110 110 )
111 111
112 112 def jsroutes(self):
113 113 """
114 114 Return a list of pyroutes.js compatible routes
115 115 """
116 116 for route_name in self._jsroutes:
117 117 yield self._extract_route_information(self._routenames[route_name])
118 118
119 119
120 120 def make_map(config):
121 121 """Create, configure and return the routes Mapper"""
122 122 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
123 123 always_scan=config['debug'])
124 124 rmap.minimization = False
125 125 rmap.explicit = False
126 126
127 127 from rhodecode.lib.utils2 import str2bool
128 128 from rhodecode.model import repo, repo_group
129 129
130 130 def check_repo(environ, match_dict):
131 131 """
132 132 check for valid repository for proper 404 handling
133 133
134 134 :param environ:
135 135 :param match_dict:
136 136 """
137 137 repo_name = match_dict.get('repo_name')
138 138
139 139 if match_dict.get('f_path'):
140 140 # fix for multiple initial slashes that causes errors
141 141 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
142 142 repo_model = repo.RepoModel()
143 143 by_name_match = repo_model.get_by_repo_name(repo_name)
144 144 # if we match quickly from database, short circuit the operation,
145 145 # and validate repo based on the type.
146 146 if by_name_match:
147 147 return True
148 148
149 149 by_id_match = repo_model.get_repo_by_id(repo_name)
150 150 if by_id_match:
151 151 repo_name = by_id_match.repo_name
152 152 match_dict['repo_name'] = repo_name
153 153 return True
154 154
155 155 return False
156 156
157 157 def check_group(environ, match_dict):
158 158 """
159 159 check for valid repository group path for proper 404 handling
160 160
161 161 :param environ:
162 162 :param match_dict:
163 163 """
164 164 repo_group_name = match_dict.get('group_name')
165 165 repo_group_model = repo_group.RepoGroupModel()
166 166 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
167 167 if by_name_match:
168 168 return True
169 169
170 170 return False
171 171
172 172 def check_user_group(environ, match_dict):
173 173 """
174 174 check for valid user group for proper 404 handling
175 175
176 176 :param environ:
177 177 :param match_dict:
178 178 """
179 179 return True
180 180
181 181 def check_int(environ, match_dict):
182 182 return match_dict.get('id').isdigit()
183 183
184 184
185 185 #==========================================================================
186 186 # CUSTOM ROUTES HERE
187 187 #==========================================================================
188 188
189 189 # MAIN PAGE
190 190 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
191 191 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
192 192 action='goto_switcher_data')
193 193 rmap.connect('repo_list_data', '/_repos', controller='home',
194 194 action='repo_list_data')
195 195
196 196 rmap.connect('user_autocomplete_data', '/_users', controller='home',
197 197 action='user_autocomplete_data', jsroute=True)
198 198 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
199 199 action='user_group_autocomplete_data', jsroute=True)
200 200
201 201 rmap.connect(
202 202 'user_profile', '/_profiles/{username}', controller='users',
203 203 action='user_profile')
204 204
205 205 # TODO: johbo: Static links, to be replaced by our redirection mechanism
206 206 rmap.connect('rst_help',
207 207 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
208 208 _static=True)
209 209 rmap.connect('markdown_help',
210 210 'http://daringfireball.net/projects/markdown/syntax',
211 211 _static=True)
212 212 rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True)
213 213 rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True)
214 214 rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True)
215 215 # TODO: anderson - making this a static link since redirect won't play
216 216 # nice with POST requests
217 217 rmap.connect('enterprise_license_convert_from_old',
218 218 'https://rhodecode.com/u/license-upgrade',
219 219 _static=True)
220 220
221 221 routing_links.connect_redirection_links(rmap)
222 222
223 223 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
224 224 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
225 225
226 226 # ADMIN REPOSITORY ROUTES
227 227 with rmap.submapper(path_prefix=ADMIN_PREFIX,
228 228 controller='admin/repos') as m:
229 229 m.connect('repos', '/repos',
230 230 action='create', conditions={'method': ['POST']})
231 231 m.connect('repos', '/repos',
232 232 action='index', conditions={'method': ['GET']})
233 233 m.connect('new_repo', '/create_repository', jsroute=True,
234 234 action='create_repository', conditions={'method': ['GET']})
235 235 m.connect('/repos/{repo_name}',
236 236 action='update', conditions={'method': ['PUT'],
237 237 'function': check_repo},
238 238 requirements=URL_NAME_REQUIREMENTS)
239 239 m.connect('delete_repo', '/repos/{repo_name}',
240 240 action='delete', conditions={'method': ['DELETE']},
241 241 requirements=URL_NAME_REQUIREMENTS)
242 242 m.connect('repo', '/repos/{repo_name}',
243 243 action='show', conditions={'method': ['GET'],
244 244 'function': check_repo},
245 245 requirements=URL_NAME_REQUIREMENTS)
246 246
247 247 # ADMIN REPOSITORY GROUPS ROUTES
248 248 with rmap.submapper(path_prefix=ADMIN_PREFIX,
249 249 controller='admin/repo_groups') as m:
250 250 m.connect('repo_groups', '/repo_groups',
251 251 action='create', conditions={'method': ['POST']})
252 252 m.connect('repo_groups', '/repo_groups',
253 253 action='index', conditions={'method': ['GET']})
254 254 m.connect('new_repo_group', '/repo_groups/new',
255 255 action='new', conditions={'method': ['GET']})
256 256 m.connect('update_repo_group', '/repo_groups/{group_name}',
257 257 action='update', conditions={'method': ['PUT'],
258 258 'function': check_group},
259 259 requirements=URL_NAME_REQUIREMENTS)
260 260
261 261 # EXTRAS REPO GROUP ROUTES
262 262 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
263 263 action='edit',
264 264 conditions={'method': ['GET'], 'function': check_group},
265 265 requirements=URL_NAME_REQUIREMENTS)
266 266 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
267 267 action='edit',
268 268 conditions={'method': ['PUT'], 'function': check_group},
269 269 requirements=URL_NAME_REQUIREMENTS)
270 270
271 271 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
272 272 action='edit_repo_group_advanced',
273 273 conditions={'method': ['GET'], 'function': check_group},
274 274 requirements=URL_NAME_REQUIREMENTS)
275 275 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
276 276 action='edit_repo_group_advanced',
277 277 conditions={'method': ['PUT'], 'function': check_group},
278 278 requirements=URL_NAME_REQUIREMENTS)
279 279
280 280 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
281 281 action='edit_repo_group_perms',
282 282 conditions={'method': ['GET'], 'function': check_group},
283 283 requirements=URL_NAME_REQUIREMENTS)
284 284 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
285 285 action='update_perms',
286 286 conditions={'method': ['PUT'], 'function': check_group},
287 287 requirements=URL_NAME_REQUIREMENTS)
288 288
289 289 m.connect('delete_repo_group', '/repo_groups/{group_name}',
290 290 action='delete', conditions={'method': ['DELETE'],
291 291 'function': check_group},
292 292 requirements=URL_NAME_REQUIREMENTS)
293 293
294 294 # ADMIN USER ROUTES
295 295 with rmap.submapper(path_prefix=ADMIN_PREFIX,
296 296 controller='admin/users') as m:
297 297 m.connect('users', '/users',
298 298 action='create', conditions={'method': ['POST']})
299 299 m.connect('users', '/users',
300 300 action='index', conditions={'method': ['GET']})
301 301 m.connect('new_user', '/users/new',
302 302 action='new', conditions={'method': ['GET']})
303 303 m.connect('update_user', '/users/{user_id}',
304 304 action='update', conditions={'method': ['PUT']})
305 305 m.connect('delete_user', '/users/{user_id}',
306 306 action='delete', conditions={'method': ['DELETE']})
307 307 m.connect('edit_user', '/users/{user_id}/edit',
308 308 action='edit', conditions={'method': ['GET']}, jsroute=True)
309 309 m.connect('user', '/users/{user_id}',
310 310 action='show', conditions={'method': ['GET']})
311 311 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
312 312 action='reset_password', conditions={'method': ['POST']})
313 313 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
314 314 action='create_personal_repo_group', conditions={'method': ['POST']})
315 315
316 316 # EXTRAS USER ROUTES
317 317 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
318 318 action='edit_advanced', conditions={'method': ['GET']})
319 319 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
320 320 action='update_advanced', conditions={'method': ['PUT']})
321 321
322 322 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
323 323 action='edit_auth_tokens', conditions={'method': ['GET']})
324 324 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
325 325 action='add_auth_token', conditions={'method': ['PUT']})
326 326 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
327 327 action='delete_auth_token', conditions={'method': ['DELETE']})
328 328
329 329 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
330 330 action='edit_global_perms', conditions={'method': ['GET']})
331 331 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
332 332 action='update_global_perms', conditions={'method': ['PUT']})
333 333
334 334 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
335 335 action='edit_perms_summary', conditions={'method': ['GET']})
336 336
337 337 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
338 338 action='edit_emails', conditions={'method': ['GET']})
339 339 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
340 340 action='add_email', conditions={'method': ['PUT']})
341 341 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
342 342 action='delete_email', conditions={'method': ['DELETE']})
343 343
344 344 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
345 345 action='edit_ips', conditions={'method': ['GET']})
346 346 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
347 347 action='add_ip', conditions={'method': ['PUT']})
348 348 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
349 349 action='delete_ip', conditions={'method': ['DELETE']})
350 350
351 351 # ADMIN USER GROUPS REST ROUTES
352 352 with rmap.submapper(path_prefix=ADMIN_PREFIX,
353 353 controller='admin/user_groups') as m:
354 354 m.connect('users_groups', '/user_groups',
355 355 action='create', conditions={'method': ['POST']})
356 356 m.connect('users_groups', '/user_groups',
357 357 action='index', conditions={'method': ['GET']})
358 358 m.connect('new_users_group', '/user_groups/new',
359 359 action='new', conditions={'method': ['GET']})
360 360 m.connect('update_users_group', '/user_groups/{user_group_id}',
361 361 action='update', conditions={'method': ['PUT']})
362 362 m.connect('delete_users_group', '/user_groups/{user_group_id}',
363 363 action='delete', conditions={'method': ['DELETE']})
364 364 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
365 365 action='edit', conditions={'method': ['GET']},
366 366 function=check_user_group)
367 367
368 368 # EXTRAS USER GROUP ROUTES
369 369 m.connect('edit_user_group_global_perms',
370 370 '/user_groups/{user_group_id}/edit/global_permissions',
371 371 action='edit_global_perms', conditions={'method': ['GET']})
372 372 m.connect('edit_user_group_global_perms',
373 373 '/user_groups/{user_group_id}/edit/global_permissions',
374 374 action='update_global_perms', conditions={'method': ['PUT']})
375 375 m.connect('edit_user_group_perms_summary',
376 376 '/user_groups/{user_group_id}/edit/permissions_summary',
377 377 action='edit_perms_summary', conditions={'method': ['GET']})
378 378
379 379 m.connect('edit_user_group_perms',
380 380 '/user_groups/{user_group_id}/edit/permissions',
381 381 action='edit_perms', conditions={'method': ['GET']})
382 382 m.connect('edit_user_group_perms',
383 383 '/user_groups/{user_group_id}/edit/permissions',
384 384 action='update_perms', conditions={'method': ['PUT']})
385 385
386 386 m.connect('edit_user_group_advanced',
387 387 '/user_groups/{user_group_id}/edit/advanced',
388 388 action='edit_advanced', conditions={'method': ['GET']})
389 389
390 390 m.connect('edit_user_group_members',
391 391 '/user_groups/{user_group_id}/edit/members', jsroute=True,
392 392 action='user_group_members', conditions={'method': ['GET']})
393 393
394 394 # ADMIN PERMISSIONS ROUTES
395 395 with rmap.submapper(path_prefix=ADMIN_PREFIX,
396 396 controller='admin/permissions') as m:
397 397 m.connect('admin_permissions_application', '/permissions/application',
398 398 action='permission_application_update', conditions={'method': ['POST']})
399 399 m.connect('admin_permissions_application', '/permissions/application',
400 400 action='permission_application', conditions={'method': ['GET']})
401 401
402 402 m.connect('admin_permissions_global', '/permissions/global',
403 403 action='permission_global_update', conditions={'method': ['POST']})
404 404 m.connect('admin_permissions_global', '/permissions/global',
405 405 action='permission_global', conditions={'method': ['GET']})
406 406
407 407 m.connect('admin_permissions_object', '/permissions/object',
408 408 action='permission_objects_update', conditions={'method': ['POST']})
409 409 m.connect('admin_permissions_object', '/permissions/object',
410 410 action='permission_objects', conditions={'method': ['GET']})
411 411
412 412 m.connect('admin_permissions_ips', '/permissions/ips',
413 413 action='permission_ips', conditions={'method': ['POST']})
414 414 m.connect('admin_permissions_ips', '/permissions/ips',
415 415 action='permission_ips', conditions={'method': ['GET']})
416 416
417 417 m.connect('admin_permissions_overview', '/permissions/overview',
418 418 action='permission_perms', conditions={'method': ['GET']})
419 419
420 420 # ADMIN DEFAULTS REST ROUTES
421 421 with rmap.submapper(path_prefix=ADMIN_PREFIX,
422 422 controller='admin/defaults') as m:
423 423 m.connect('admin_defaults_repositories', '/defaults/repositories',
424 424 action='update_repository_defaults', conditions={'method': ['POST']})
425 425 m.connect('admin_defaults_repositories', '/defaults/repositories',
426 426 action='index', conditions={'method': ['GET']})
427 427
428 428 # ADMIN DEBUG STYLE ROUTES
429 429 if str2bool(config.get('debug_style')):
430 430 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
431 431 controller='debug_style') as m:
432 432 m.connect('debug_style_home', '',
433 433 action='index', conditions={'method': ['GET']})
434 434 m.connect('debug_style_template', '/t/{t_path}',
435 435 action='template', conditions={'method': ['GET']})
436 436
437 437 # ADMIN SETTINGS ROUTES
438 438 with rmap.submapper(path_prefix=ADMIN_PREFIX,
439 439 controller='admin/settings') as m:
440 440
441 441 # default
442 442 m.connect('admin_settings', '/settings',
443 443 action='settings_global_update',
444 444 conditions={'method': ['POST']})
445 445 m.connect('admin_settings', '/settings',
446 446 action='settings_global', conditions={'method': ['GET']})
447 447
448 448 m.connect('admin_settings_vcs', '/settings/vcs',
449 449 action='settings_vcs_update',
450 450 conditions={'method': ['POST']})
451 451 m.connect('admin_settings_vcs', '/settings/vcs',
452 452 action='settings_vcs',
453 453 conditions={'method': ['GET']})
454 454 m.connect('admin_settings_vcs', '/settings/vcs',
455 455 action='delete_svn_pattern',
456 456 conditions={'method': ['DELETE']})
457 457
458 458 m.connect('admin_settings_mapping', '/settings/mapping',
459 459 action='settings_mapping_update',
460 460 conditions={'method': ['POST']})
461 461 m.connect('admin_settings_mapping', '/settings/mapping',
462 462 action='settings_mapping', conditions={'method': ['GET']})
463 463
464 464 m.connect('admin_settings_global', '/settings/global',
465 465 action='settings_global_update',
466 466 conditions={'method': ['POST']})
467 467 m.connect('admin_settings_global', '/settings/global',
468 468 action='settings_global', conditions={'method': ['GET']})
469 469
470 470 m.connect('admin_settings_visual', '/settings/visual',
471 471 action='settings_visual_update',
472 472 conditions={'method': ['POST']})
473 473 m.connect('admin_settings_visual', '/settings/visual',
474 474 action='settings_visual', conditions={'method': ['GET']})
475 475
476 476 m.connect('admin_settings_issuetracker',
477 477 '/settings/issue-tracker', action='settings_issuetracker',
478 478 conditions={'method': ['GET']})
479 479 m.connect('admin_settings_issuetracker_save',
480 480 '/settings/issue-tracker/save',
481 481 action='settings_issuetracker_save',
482 482 conditions={'method': ['POST']})
483 483 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
484 484 action='settings_issuetracker_test',
485 485 conditions={'method': ['POST']})
486 486 m.connect('admin_issuetracker_delete',
487 487 '/settings/issue-tracker/delete',
488 488 action='settings_issuetracker_delete',
489 489 conditions={'method': ['DELETE']})
490 490
491 491 m.connect('admin_settings_email', '/settings/email',
492 492 action='settings_email_update',
493 493 conditions={'method': ['POST']})
494 494 m.connect('admin_settings_email', '/settings/email',
495 495 action='settings_email', conditions={'method': ['GET']})
496 496
497 497 m.connect('admin_settings_hooks', '/settings/hooks',
498 498 action='settings_hooks_update',
499 499 conditions={'method': ['POST', 'DELETE']})
500 500 m.connect('admin_settings_hooks', '/settings/hooks',
501 501 action='settings_hooks', conditions={'method': ['GET']})
502 502
503 503 m.connect('admin_settings_search', '/settings/search',
504 504 action='settings_search', conditions={'method': ['GET']})
505 505
506 506 m.connect('admin_settings_system', '/settings/system',
507 507 action='settings_system', conditions={'method': ['GET']})
508 508
509 509 m.connect('admin_settings_system_update', '/settings/system/updates',
510 510 action='settings_system_update', conditions={'method': ['GET']})
511 511
512 m.connect('admin_settings_sessions', '/settings/sessions',
513 action='settings_sessions', conditions={'method': ['GET']})
514
515 m.connect('admin_settings_sessions_cleanup', '/settings/sessions/cleanup',
516 action='settings_sessions_cleanup', conditions={'method': ['POST']})
517
512 518 m.connect('admin_settings_supervisor', '/settings/supervisor',
513 519 action='settings_supervisor', conditions={'method': ['GET']})
514 520 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
515 521 action='settings_supervisor_log', conditions={'method': ['GET']})
516 522
517 523 m.connect('admin_settings_labs', '/settings/labs',
518 524 action='settings_labs_update',
519 525 conditions={'method': ['POST']})
520 526 m.connect('admin_settings_labs', '/settings/labs',
521 527 action='settings_labs', conditions={'method': ['GET']})
522 528
523 529 # ADMIN MY ACCOUNT
524 530 with rmap.submapper(path_prefix=ADMIN_PREFIX,
525 531 controller='admin/my_account') as m:
526 532
527 533 m.connect('my_account', '/my_account',
528 534 action='my_account', conditions={'method': ['GET']})
529 535 m.connect('my_account_edit', '/my_account/edit',
530 536 action='my_account_edit', conditions={'method': ['GET']})
531 537 m.connect('my_account', '/my_account',
532 538 action='my_account_update', conditions={'method': ['POST']})
533 539
534 540 m.connect('my_account_password', '/my_account/password',
535 541 action='my_account_password', conditions={'method': ['GET', 'POST']})
536 542
537 543 m.connect('my_account_repos', '/my_account/repos',
538 544 action='my_account_repos', conditions={'method': ['GET']})
539 545
540 546 m.connect('my_account_watched', '/my_account/watched',
541 547 action='my_account_watched', conditions={'method': ['GET']})
542 548
543 549 m.connect('my_account_pullrequests', '/my_account/pull_requests',
544 550 action='my_account_pullrequests', conditions={'method': ['GET']})
545 551
546 552 m.connect('my_account_perms', '/my_account/perms',
547 553 action='my_account_perms', conditions={'method': ['GET']})
548 554
549 555 m.connect('my_account_emails', '/my_account/emails',
550 556 action='my_account_emails', conditions={'method': ['GET']})
551 557 m.connect('my_account_emails', '/my_account/emails',
552 558 action='my_account_emails_add', conditions={'method': ['POST']})
553 559 m.connect('my_account_emails', '/my_account/emails',
554 560 action='my_account_emails_delete', conditions={'method': ['DELETE']})
555 561
556 562 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
557 563 action='my_account_auth_tokens', conditions={'method': ['GET']})
558 564 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
559 565 action='my_account_auth_tokens_add', conditions={'method': ['POST']})
560 566 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
561 567 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
562 568 m.connect('my_account_notifications', '/my_account/notifications',
563 569 action='my_notifications',
564 570 conditions={'method': ['GET']})
565 571 m.connect('my_account_notifications_toggle_visibility',
566 572 '/my_account/toggle_visibility',
567 573 action='my_notifications_toggle_visibility',
568 574 conditions={'method': ['POST']})
569 575 m.connect('my_account_notifications_test_channelstream',
570 576 '/my_account/test_channelstream',
571 577 action='my_account_notifications_test_channelstream',
572 578 conditions={'method': ['POST']})
573 579
574 580 # NOTIFICATION REST ROUTES
575 581 with rmap.submapper(path_prefix=ADMIN_PREFIX,
576 582 controller='admin/notifications') as m:
577 583 m.connect('notifications', '/notifications',
578 584 action='index', conditions={'method': ['GET']})
579 585 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
580 586 action='mark_all_read', conditions={'method': ['POST']})
581 587 m.connect('/notifications/{notification_id}',
582 588 action='update', conditions={'method': ['PUT']})
583 589 m.connect('/notifications/{notification_id}',
584 590 action='delete', conditions={'method': ['DELETE']})
585 591 m.connect('notification', '/notifications/{notification_id}',
586 592 action='show', conditions={'method': ['GET']})
587 593
588 594 # ADMIN GIST
589 595 with rmap.submapper(path_prefix=ADMIN_PREFIX,
590 596 controller='admin/gists') as m:
591 597 m.connect('gists', '/gists',
592 598 action='create', conditions={'method': ['POST']})
593 599 m.connect('gists', '/gists', jsroute=True,
594 600 action='index', conditions={'method': ['GET']})
595 601 m.connect('new_gist', '/gists/new', jsroute=True,
596 602 action='new', conditions={'method': ['GET']})
597 603
598 604 m.connect('/gists/{gist_id}',
599 605 action='delete', conditions={'method': ['DELETE']})
600 606 m.connect('edit_gist', '/gists/{gist_id}/edit',
601 607 action='edit_form', conditions={'method': ['GET']})
602 608 m.connect('edit_gist', '/gists/{gist_id}/edit',
603 609 action='edit', conditions={'method': ['POST']})
604 610 m.connect(
605 611 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
606 612 action='check_revision', conditions={'method': ['GET']})
607 613
608 614 m.connect('gist', '/gists/{gist_id}',
609 615 action='show', conditions={'method': ['GET']})
610 616 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
611 617 revision='tip',
612 618 action='show', conditions={'method': ['GET']})
613 619 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
614 620 revision='tip',
615 621 action='show', conditions={'method': ['GET']})
616 622 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
617 623 revision='tip',
618 624 action='show', conditions={'method': ['GET']},
619 625 requirements=URL_NAME_REQUIREMENTS)
620 626
621 627 # ADMIN MAIN PAGES
622 628 with rmap.submapper(path_prefix=ADMIN_PREFIX,
623 629 controller='admin/admin') as m:
624 630 m.connect('admin_home', '', action='index')
625 631 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
626 632 action='add_repo')
627 633 m.connect(
628 634 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
629 635 action='pull_requests')
630 636 m.connect(
631 637 'pull_requests_global_1', '/pull-requests/{pull_request_id:[0-9]+}',
632 638 action='pull_requests')
633 639 m.connect(
634 640 'pull_requests_global', '/pull-request/{pull_request_id:[0-9]+}',
635 641 action='pull_requests')
636 642
637 643 # USER JOURNAL
638 644 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
639 645 controller='journal', action='index')
640 646 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
641 647 controller='journal', action='journal_rss')
642 648 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
643 649 controller='journal', action='journal_atom')
644 650
645 651 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
646 652 controller='journal', action='public_journal')
647 653
648 654 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
649 655 controller='journal', action='public_journal_rss')
650 656
651 657 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
652 658 controller='journal', action='public_journal_rss')
653 659
654 660 rmap.connect('public_journal_atom',
655 661 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
656 662 action='public_journal_atom')
657 663
658 664 rmap.connect('public_journal_atom_old',
659 665 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
660 666 action='public_journal_atom')
661 667
662 668 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
663 669 controller='journal', action='toggle_following', jsroute=True,
664 670 conditions={'method': ['POST']})
665 671
666 672 # FULL TEXT SEARCH
667 673 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
668 674 controller='search')
669 675 rmap.connect('search_repo_home', '/{repo_name}/search',
670 676 controller='search',
671 677 action='index',
672 678 conditions={'function': check_repo},
673 679 requirements=URL_NAME_REQUIREMENTS)
674 680
675 681 # FEEDS
676 682 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
677 683 controller='feed', action='rss',
678 684 conditions={'function': check_repo},
679 685 requirements=URL_NAME_REQUIREMENTS)
680 686
681 687 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
682 688 controller='feed', action='atom',
683 689 conditions={'function': check_repo},
684 690 requirements=URL_NAME_REQUIREMENTS)
685 691
686 692 #==========================================================================
687 693 # REPOSITORY ROUTES
688 694 #==========================================================================
689 695
690 696 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
691 697 controller='admin/repos', action='repo_creating',
692 698 requirements=URL_NAME_REQUIREMENTS)
693 699 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
694 700 controller='admin/repos', action='repo_check',
695 701 requirements=URL_NAME_REQUIREMENTS)
696 702
697 703 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
698 704 controller='summary', action='repo_stats',
699 705 conditions={'function': check_repo},
700 706 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
701 707
702 708 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
703 709 controller='summary', action='repo_refs_data', jsroute=True,
704 710 requirements=URL_NAME_REQUIREMENTS)
705 711 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
706 712 controller='summary', action='repo_refs_changelog_data',
707 713 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
708 714 rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers',
709 715 controller='summary', action='repo_default_reviewers_data',
710 716 jsroute=True, requirements=URL_NAME_REQUIREMENTS)
711 717
712 718 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
713 719 controller='changeset', revision='tip', jsroute=True,
714 720 conditions={'function': check_repo},
715 721 requirements=URL_NAME_REQUIREMENTS)
716 722 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
717 723 controller='changeset', revision='tip', action='changeset_children',
718 724 conditions={'function': check_repo},
719 725 requirements=URL_NAME_REQUIREMENTS)
720 726 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
721 727 controller='changeset', revision='tip', action='changeset_parents',
722 728 conditions={'function': check_repo},
723 729 requirements=URL_NAME_REQUIREMENTS)
724 730
725 731 # repo edit options
726 732 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
727 733 controller='admin/repos', action='edit',
728 734 conditions={'method': ['GET'], 'function': check_repo},
729 735 requirements=URL_NAME_REQUIREMENTS)
730 736
731 737 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
732 738 jsroute=True,
733 739 controller='admin/repos', action='edit_permissions',
734 740 conditions={'method': ['GET'], 'function': check_repo},
735 741 requirements=URL_NAME_REQUIREMENTS)
736 742 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
737 743 controller='admin/repos', action='edit_permissions_update',
738 744 conditions={'method': ['PUT'], 'function': check_repo},
739 745 requirements=URL_NAME_REQUIREMENTS)
740 746
741 747 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
742 748 controller='admin/repos', action='edit_fields',
743 749 conditions={'method': ['GET'], 'function': check_repo},
744 750 requirements=URL_NAME_REQUIREMENTS)
745 751 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
746 752 controller='admin/repos', action='create_repo_field',
747 753 conditions={'method': ['PUT'], 'function': check_repo},
748 754 requirements=URL_NAME_REQUIREMENTS)
749 755 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
750 756 controller='admin/repos', action='delete_repo_field',
751 757 conditions={'method': ['DELETE'], 'function': check_repo},
752 758 requirements=URL_NAME_REQUIREMENTS)
753 759
754 760 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
755 761 controller='admin/repos', action='edit_advanced',
756 762 conditions={'method': ['GET'], 'function': check_repo},
757 763 requirements=URL_NAME_REQUIREMENTS)
758 764
759 765 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
760 766 controller='admin/repos', action='edit_advanced_locking',
761 767 conditions={'method': ['PUT'], 'function': check_repo},
762 768 requirements=URL_NAME_REQUIREMENTS)
763 769 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
764 770 controller='admin/repos', action='toggle_locking',
765 771 conditions={'method': ['GET'], 'function': check_repo},
766 772 requirements=URL_NAME_REQUIREMENTS)
767 773
768 774 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
769 775 controller='admin/repos', action='edit_advanced_journal',
770 776 conditions={'method': ['PUT'], 'function': check_repo},
771 777 requirements=URL_NAME_REQUIREMENTS)
772 778
773 779 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
774 780 controller='admin/repos', action='edit_advanced_fork',
775 781 conditions={'method': ['PUT'], 'function': check_repo},
776 782 requirements=URL_NAME_REQUIREMENTS)
777 783
778 784 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
779 785 controller='admin/repos', action='edit_caches_form',
780 786 conditions={'method': ['GET'], 'function': check_repo},
781 787 requirements=URL_NAME_REQUIREMENTS)
782 788 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
783 789 controller='admin/repos', action='edit_caches',
784 790 conditions={'method': ['PUT'], 'function': check_repo},
785 791 requirements=URL_NAME_REQUIREMENTS)
786 792
787 793 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
788 794 controller='admin/repos', action='edit_remote_form',
789 795 conditions={'method': ['GET'], 'function': check_repo},
790 796 requirements=URL_NAME_REQUIREMENTS)
791 797 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
792 798 controller='admin/repos', action='edit_remote',
793 799 conditions={'method': ['PUT'], 'function': check_repo},
794 800 requirements=URL_NAME_REQUIREMENTS)
795 801
796 802 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
797 803 controller='admin/repos', action='edit_statistics_form',
798 804 conditions={'method': ['GET'], 'function': check_repo},
799 805 requirements=URL_NAME_REQUIREMENTS)
800 806 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
801 807 controller='admin/repos', action='edit_statistics',
802 808 conditions={'method': ['PUT'], 'function': check_repo},
803 809 requirements=URL_NAME_REQUIREMENTS)
804 810 rmap.connect('repo_settings_issuetracker',
805 811 '/{repo_name}/settings/issue-tracker',
806 812 controller='admin/repos', action='repo_issuetracker',
807 813 conditions={'method': ['GET'], 'function': check_repo},
808 814 requirements=URL_NAME_REQUIREMENTS)
809 815 rmap.connect('repo_issuetracker_test',
810 816 '/{repo_name}/settings/issue-tracker/test',
811 817 controller='admin/repos', action='repo_issuetracker_test',
812 818 conditions={'method': ['POST'], 'function': check_repo},
813 819 requirements=URL_NAME_REQUIREMENTS)
814 820 rmap.connect('repo_issuetracker_delete',
815 821 '/{repo_name}/settings/issue-tracker/delete',
816 822 controller='admin/repos', action='repo_issuetracker_delete',
817 823 conditions={'method': ['DELETE'], 'function': check_repo},
818 824 requirements=URL_NAME_REQUIREMENTS)
819 825 rmap.connect('repo_issuetracker_save',
820 826 '/{repo_name}/settings/issue-tracker/save',
821 827 controller='admin/repos', action='repo_issuetracker_save',
822 828 conditions={'method': ['POST'], 'function': check_repo},
823 829 requirements=URL_NAME_REQUIREMENTS)
824 830 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
825 831 controller='admin/repos', action='repo_settings_vcs_update',
826 832 conditions={'method': ['POST'], 'function': check_repo},
827 833 requirements=URL_NAME_REQUIREMENTS)
828 834 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
829 835 controller='admin/repos', action='repo_settings_vcs',
830 836 conditions={'method': ['GET'], 'function': check_repo},
831 837 requirements=URL_NAME_REQUIREMENTS)
832 838 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
833 839 controller='admin/repos', action='repo_delete_svn_pattern',
834 840 conditions={'method': ['DELETE'], 'function': check_repo},
835 841 requirements=URL_NAME_REQUIREMENTS)
836 842 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
837 843 controller='admin/repos', action='repo_settings_pullrequest',
838 844 conditions={'method': ['GET', 'POST'], 'function': check_repo},
839 845 requirements=URL_NAME_REQUIREMENTS)
840 846
841 847 # still working url for backward compat.
842 848 rmap.connect('raw_changeset_home_depraced',
843 849 '/{repo_name}/raw-changeset/{revision}',
844 850 controller='changeset', action='changeset_raw',
845 851 revision='tip', conditions={'function': check_repo},
846 852 requirements=URL_NAME_REQUIREMENTS)
847 853
848 854 # new URLs
849 855 rmap.connect('changeset_raw_home',
850 856 '/{repo_name}/changeset-diff/{revision}',
851 857 controller='changeset', action='changeset_raw',
852 858 revision='tip', conditions={'function': check_repo},
853 859 requirements=URL_NAME_REQUIREMENTS)
854 860
855 861 rmap.connect('changeset_patch_home',
856 862 '/{repo_name}/changeset-patch/{revision}',
857 863 controller='changeset', action='changeset_patch',
858 864 revision='tip', conditions={'function': check_repo},
859 865 requirements=URL_NAME_REQUIREMENTS)
860 866
861 867 rmap.connect('changeset_download_home',
862 868 '/{repo_name}/changeset-download/{revision}',
863 869 controller='changeset', action='changeset_download',
864 870 revision='tip', conditions={'function': check_repo},
865 871 requirements=URL_NAME_REQUIREMENTS)
866 872
867 873 rmap.connect('changeset_comment',
868 874 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
869 875 controller='changeset', revision='tip', action='comment',
870 876 conditions={'function': check_repo},
871 877 requirements=URL_NAME_REQUIREMENTS)
872 878
873 879 rmap.connect('changeset_comment_preview',
874 880 '/{repo_name}/changeset/comment/preview', jsroute=True,
875 881 controller='changeset', action='preview_comment',
876 882 conditions={'function': check_repo, 'method': ['POST']},
877 883 requirements=URL_NAME_REQUIREMENTS)
878 884
879 885 rmap.connect('changeset_comment_delete',
880 886 '/{repo_name}/changeset/comment/{comment_id}/delete',
881 887 controller='changeset', action='delete_comment',
882 888 conditions={'function': check_repo, 'method': ['DELETE']},
883 889 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
884 890
885 891 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
886 892 controller='changeset', action='changeset_info',
887 893 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
888 894
889 895 rmap.connect('compare_home',
890 896 '/{repo_name}/compare',
891 897 controller='compare', action='index',
892 898 conditions={'function': check_repo},
893 899 requirements=URL_NAME_REQUIREMENTS)
894 900
895 901 rmap.connect('compare_url',
896 902 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
897 903 controller='compare', action='compare',
898 904 conditions={'function': check_repo},
899 905 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
900 906
901 907 rmap.connect('pullrequest_home',
902 908 '/{repo_name}/pull-request/new', controller='pullrequests',
903 909 action='index', conditions={'function': check_repo,
904 910 'method': ['GET']},
905 911 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
906 912
907 913 rmap.connect('pullrequest',
908 914 '/{repo_name}/pull-request/new', controller='pullrequests',
909 915 action='create', conditions={'function': check_repo,
910 916 'method': ['POST']},
911 917 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
912 918
913 919 rmap.connect('pullrequest_repo_refs',
914 920 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
915 921 controller='pullrequests',
916 922 action='get_repo_refs',
917 923 conditions={'function': check_repo, 'method': ['GET']},
918 924 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
919 925
920 926 rmap.connect('pullrequest_repo_destinations',
921 927 '/{repo_name}/pull-request/repo-destinations',
922 928 controller='pullrequests',
923 929 action='get_repo_destinations',
924 930 conditions={'function': check_repo, 'method': ['GET']},
925 931 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
926 932
927 933 rmap.connect('pullrequest_show',
928 934 '/{repo_name}/pull-request/{pull_request_id}',
929 935 controller='pullrequests',
930 936 action='show', conditions={'function': check_repo,
931 937 'method': ['GET']},
932 938 requirements=URL_NAME_REQUIREMENTS)
933 939
934 940 rmap.connect('pullrequest_update',
935 941 '/{repo_name}/pull-request/{pull_request_id}',
936 942 controller='pullrequests',
937 943 action='update', conditions={'function': check_repo,
938 944 'method': ['PUT']},
939 945 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
940 946
941 947 rmap.connect('pullrequest_merge',
942 948 '/{repo_name}/pull-request/{pull_request_id}',
943 949 controller='pullrequests',
944 950 action='merge', conditions={'function': check_repo,
945 951 'method': ['POST']},
946 952 requirements=URL_NAME_REQUIREMENTS)
947 953
948 954 rmap.connect('pullrequest_delete',
949 955 '/{repo_name}/pull-request/{pull_request_id}',
950 956 controller='pullrequests',
951 957 action='delete', conditions={'function': check_repo,
952 958 'method': ['DELETE']},
953 959 requirements=URL_NAME_REQUIREMENTS)
954 960
955 961 rmap.connect('pullrequest_show_all',
956 962 '/{repo_name}/pull-request',
957 963 controller='pullrequests',
958 964 action='show_all', conditions={'function': check_repo,
959 965 'method': ['GET']},
960 966 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
961 967
962 968 rmap.connect('pullrequest_comment',
963 969 '/{repo_name}/pull-request-comment/{pull_request_id}',
964 970 controller='pullrequests',
965 971 action='comment', conditions={'function': check_repo,
966 972 'method': ['POST']},
967 973 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
968 974
969 975 rmap.connect('pullrequest_comment_delete',
970 976 '/{repo_name}/pull-request-comment/{comment_id}/delete',
971 977 controller='pullrequests', action='delete_comment',
972 978 conditions={'function': check_repo, 'method': ['DELETE']},
973 979 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
974 980
975 981 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
976 982 controller='summary', conditions={'function': check_repo},
977 983 requirements=URL_NAME_REQUIREMENTS)
978 984
979 985 rmap.connect('branches_home', '/{repo_name}/branches',
980 986 controller='branches', conditions={'function': check_repo},
981 987 requirements=URL_NAME_REQUIREMENTS)
982 988
983 989 rmap.connect('tags_home', '/{repo_name}/tags',
984 990 controller='tags', conditions={'function': check_repo},
985 991 requirements=URL_NAME_REQUIREMENTS)
986 992
987 993 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
988 994 controller='bookmarks', conditions={'function': check_repo},
989 995 requirements=URL_NAME_REQUIREMENTS)
990 996
991 997 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
992 998 controller='changelog', conditions={'function': check_repo},
993 999 requirements=URL_NAME_REQUIREMENTS)
994 1000
995 1001 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
996 1002 controller='changelog', action='changelog_summary',
997 1003 conditions={'function': check_repo},
998 1004 requirements=URL_NAME_REQUIREMENTS)
999 1005
1000 1006 rmap.connect('changelog_file_home',
1001 1007 '/{repo_name}/changelog/{revision}/{f_path}',
1002 1008 controller='changelog', f_path=None,
1003 1009 conditions={'function': check_repo},
1004 1010 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1005 1011
1006 1012 rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}',
1007 1013 controller='changelog', action='changelog_details',
1008 1014 conditions={'function': check_repo},
1009 1015 requirements=URL_NAME_REQUIREMENTS)
1010 1016
1011 1017 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
1012 1018 controller='files', revision='tip', f_path='',
1013 1019 conditions={'function': check_repo},
1014 1020 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1015 1021
1016 1022 rmap.connect('files_home_simple_catchrev',
1017 1023 '/{repo_name}/files/{revision}',
1018 1024 controller='files', revision='tip', f_path='',
1019 1025 conditions={'function': check_repo},
1020 1026 requirements=URL_NAME_REQUIREMENTS)
1021 1027
1022 1028 rmap.connect('files_home_simple_catchall',
1023 1029 '/{repo_name}/files',
1024 1030 controller='files', revision='tip', f_path='',
1025 1031 conditions={'function': check_repo},
1026 1032 requirements=URL_NAME_REQUIREMENTS)
1027 1033
1028 1034 rmap.connect('files_history_home',
1029 1035 '/{repo_name}/history/{revision}/{f_path}',
1030 1036 controller='files', action='history', revision='tip', f_path='',
1031 1037 conditions={'function': check_repo},
1032 1038 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1033 1039
1034 1040 rmap.connect('files_authors_home',
1035 1041 '/{repo_name}/authors/{revision}/{f_path}',
1036 1042 controller='files', action='authors', revision='tip', f_path='',
1037 1043 conditions={'function': check_repo},
1038 1044 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1039 1045
1040 1046 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1041 1047 controller='files', action='diff', f_path='',
1042 1048 conditions={'function': check_repo},
1043 1049 requirements=URL_NAME_REQUIREMENTS)
1044 1050
1045 1051 rmap.connect('files_diff_2way_home',
1046 1052 '/{repo_name}/diff-2way/{f_path}',
1047 1053 controller='files', action='diff_2way', f_path='',
1048 1054 conditions={'function': check_repo},
1049 1055 requirements=URL_NAME_REQUIREMENTS)
1050 1056
1051 1057 rmap.connect('files_rawfile_home',
1052 1058 '/{repo_name}/rawfile/{revision}/{f_path}',
1053 1059 controller='files', action='rawfile', revision='tip',
1054 1060 f_path='', conditions={'function': check_repo},
1055 1061 requirements=URL_NAME_REQUIREMENTS)
1056 1062
1057 1063 rmap.connect('files_raw_home',
1058 1064 '/{repo_name}/raw/{revision}/{f_path}',
1059 1065 controller='files', action='raw', revision='tip', f_path='',
1060 1066 conditions={'function': check_repo},
1061 1067 requirements=URL_NAME_REQUIREMENTS)
1062 1068
1063 1069 rmap.connect('files_render_home',
1064 1070 '/{repo_name}/render/{revision}/{f_path}',
1065 1071 controller='files', action='index', revision='tip', f_path='',
1066 1072 rendered=True, conditions={'function': check_repo},
1067 1073 requirements=URL_NAME_REQUIREMENTS)
1068 1074
1069 1075 rmap.connect('files_annotate_home',
1070 1076 '/{repo_name}/annotate/{revision}/{f_path}',
1071 1077 controller='files', action='index', revision='tip',
1072 1078 f_path='', annotate=True, conditions={'function': check_repo},
1073 1079 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1074 1080
1075 1081 rmap.connect('files_edit',
1076 1082 '/{repo_name}/edit/{revision}/{f_path}',
1077 1083 controller='files', action='edit', revision='tip',
1078 1084 f_path='',
1079 1085 conditions={'function': check_repo, 'method': ['POST']},
1080 1086 requirements=URL_NAME_REQUIREMENTS)
1081 1087
1082 1088 rmap.connect('files_edit_home',
1083 1089 '/{repo_name}/edit/{revision}/{f_path}',
1084 1090 controller='files', action='edit_home', revision='tip',
1085 1091 f_path='', conditions={'function': check_repo},
1086 1092 requirements=URL_NAME_REQUIREMENTS)
1087 1093
1088 1094 rmap.connect('files_add',
1089 1095 '/{repo_name}/add/{revision}/{f_path}',
1090 1096 controller='files', action='add', revision='tip',
1091 1097 f_path='',
1092 1098 conditions={'function': check_repo, 'method': ['POST']},
1093 1099 requirements=URL_NAME_REQUIREMENTS)
1094 1100
1095 1101 rmap.connect('files_add_home',
1096 1102 '/{repo_name}/add/{revision}/{f_path}',
1097 1103 controller='files', action='add_home', revision='tip',
1098 1104 f_path='', conditions={'function': check_repo},
1099 1105 requirements=URL_NAME_REQUIREMENTS)
1100 1106
1101 1107 rmap.connect('files_delete',
1102 1108 '/{repo_name}/delete/{revision}/{f_path}',
1103 1109 controller='files', action='delete', revision='tip',
1104 1110 f_path='',
1105 1111 conditions={'function': check_repo, 'method': ['POST']},
1106 1112 requirements=URL_NAME_REQUIREMENTS)
1107 1113
1108 1114 rmap.connect('files_delete_home',
1109 1115 '/{repo_name}/delete/{revision}/{f_path}',
1110 1116 controller='files', action='delete_home', revision='tip',
1111 1117 f_path='', conditions={'function': check_repo},
1112 1118 requirements=URL_NAME_REQUIREMENTS)
1113 1119
1114 1120 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1115 1121 controller='files', action='archivefile',
1116 1122 conditions={'function': check_repo},
1117 1123 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1118 1124
1119 1125 rmap.connect('files_nodelist_home',
1120 1126 '/{repo_name}/nodelist/{revision}/{f_path}',
1121 1127 controller='files', action='nodelist',
1122 1128 conditions={'function': check_repo},
1123 1129 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1124 1130
1125 1131 rmap.connect('files_nodetree_full',
1126 1132 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
1127 1133 controller='files', action='nodetree_full',
1128 1134 conditions={'function': check_repo},
1129 1135 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1130 1136
1131 1137 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1132 1138 controller='forks', action='fork_create',
1133 1139 conditions={'function': check_repo, 'method': ['POST']},
1134 1140 requirements=URL_NAME_REQUIREMENTS)
1135 1141
1136 1142 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1137 1143 controller='forks', action='fork',
1138 1144 conditions={'function': check_repo},
1139 1145 requirements=URL_NAME_REQUIREMENTS)
1140 1146
1141 1147 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1142 1148 controller='forks', action='forks',
1143 1149 conditions={'function': check_repo},
1144 1150 requirements=URL_NAME_REQUIREMENTS)
1145 1151
1146 1152 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1147 1153 controller='followers', action='followers',
1148 1154 conditions={'function': check_repo},
1149 1155 requirements=URL_NAME_REQUIREMENTS)
1150 1156
1151 1157 # must be here for proper group/repo catching pattern
1152 1158 _connect_with_slash(
1153 1159 rmap, 'repo_group_home', '/{group_name}',
1154 1160 controller='home', action='index_repo_group',
1155 1161 conditions={'function': check_group},
1156 1162 requirements=URL_NAME_REQUIREMENTS)
1157 1163
1158 1164 # catch all, at the end
1159 1165 _connect_with_slash(
1160 1166 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1161 1167 controller='summary', action='index',
1162 1168 conditions={'function': check_repo},
1163 1169 requirements=URL_NAME_REQUIREMENTS)
1164 1170
1165 1171 return rmap
1166 1172
1167 1173
1168 1174 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1169 1175 """
1170 1176 Connect a route with an optional trailing slash in `path`.
1171 1177 """
1172 1178 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1173 1179 mapper.connect(name, path, *args, **kwargs)
@@ -1,842 +1,890 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 settings controller for rhodecode admin
24 24 """
25 25
26 26 import collections
27 27 import logging
28 28 import urllib2
29 29
30 30 import datetime
31 31 import formencode
32 32 from formencode import htmlfill
33 33 import packaging.version
34 34 from pylons import request, tmpl_context as c, url, config
35 35 from pylons.controllers.util import redirect
36 36 from pylons.i18n.translation import _, lazy_ugettext
37 37 from pyramid.threadlocal import get_current_registry
38 38 from webob.exc import HTTPBadRequest
39 39
40 40 import rhodecode
41 41 from rhodecode.admin.navigation import navigation_list
42 42 from rhodecode.lib import auth
43 43 from rhodecode.lib import helpers as h
44 44 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
45 45 from rhodecode.lib.base import BaseController, render
46 46 from rhodecode.lib.celerylib import tasks, run_task
47 47 from rhodecode.lib.utils import repo2db_mapper
48 48 from rhodecode.lib.utils2 import (
49 49 str2bool, safe_unicode, AttributeDict, safe_int)
50 50 from rhodecode.lib.compat import OrderedDict
51 51 from rhodecode.lib.ext_json import json
52 52 from rhodecode.lib.utils import jsonify
53 from rhodecode.lib import system_info
54 from rhodecode.lib import user_sessions
53 55
54 56 from rhodecode.model.db import RhodeCodeUi, Repository
55 57 from rhodecode.model.forms import ApplicationSettingsForm, \
56 58 ApplicationUiSettingsForm, ApplicationVisualisationForm, \
57 59 LabsSettingsForm, IssueTrackerPatternsForm
58 60 from rhodecode.model.repo_group import RepoGroupModel
59 61
60 62 from rhodecode.model.scm import ScmModel
61 63 from rhodecode.model.notification import EmailNotificationModel
62 64 from rhodecode.model.meta import Session
63 65 from rhodecode.model.settings import (
64 66 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
65 67 SettingsModel)
66 68
67 69 from rhodecode.model.supervisor import SupervisorModel, SUPERVISOR_MASTER
68 70 from rhodecode.svn_support.config_keys import generate_config
69 71
70 72
71 73 log = logging.getLogger(__name__)
72 74
73 75
74 76 class SettingsController(BaseController):
75 77 """REST Controller styled on the Atom Publishing Protocol"""
76 78 # To properly map this controller, ensure your config/routing.py
77 79 # file has a resource setup:
78 80 # map.resource('setting', 'settings', controller='admin/settings',
79 81 # path_prefix='/admin', name_prefix='admin_')
80 82
81 83 @LoginRequired()
82 84 def __before__(self):
83 85 super(SettingsController, self).__before__()
84 86 c.labs_active = str2bool(
85 87 rhodecode.CONFIG.get('labs_settings_active', 'true'))
86 88 c.navlist = navigation_list(request)
87 89
88 90 def _get_hg_ui_settings(self):
89 91 ret = RhodeCodeUi.query().all()
90 92
91 93 if not ret:
92 94 raise Exception('Could not get application ui settings !')
93 95 settings = {}
94 96 for each in ret:
95 97 k = each.ui_key
96 98 v = each.ui_value
97 99 if k == '/':
98 100 k = 'root_path'
99 101
100 102 if k in ['push_ssl', 'publish']:
101 103 v = str2bool(v)
102 104
103 105 if k.find('.') != -1:
104 106 k = k.replace('.', '_')
105 107
106 108 if each.ui_section in ['hooks', 'extensions']:
107 109 v = each.ui_active
108 110
109 111 settings[each.ui_section + '_' + k] = v
110 112 return settings
111 113
112 114 @HasPermissionAllDecorator('hg.admin')
113 115 @auth.CSRFRequired()
114 116 @jsonify
115 117 def delete_svn_pattern(self):
116 118 if not request.is_xhr:
117 119 raise HTTPBadRequest()
118 120
119 121 delete_pattern_id = request.POST.get('delete_svn_pattern')
120 122 model = VcsSettingsModel()
121 123 try:
122 124 model.delete_global_svn_pattern(delete_pattern_id)
123 125 except SettingNotFound:
124 126 raise HTTPBadRequest()
125 127
126 128 Session().commit()
127 129 return True
128 130
129 131 @HasPermissionAllDecorator('hg.admin')
130 132 @auth.CSRFRequired()
131 133 def settings_vcs_update(self):
132 134 """POST /admin/settings: All items in the collection"""
133 135 # url('admin_settings_vcs')
134 136 c.active = 'vcs'
135 137
136 138 model = VcsSettingsModel()
137 139 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
138 140 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
139 141
140 142 # TODO: Replace with request.registry after migrating to pyramid.
141 143 pyramid_settings = get_current_registry().settings
142 144 c.svn_proxy_generate_config = pyramid_settings[generate_config]
143 145
144 146 application_form = ApplicationUiSettingsForm()()
145 147
146 148 try:
147 149 form_result = application_form.to_python(dict(request.POST))
148 150 except formencode.Invalid as errors:
149 151 h.flash(
150 152 _("Some form inputs contain invalid data."),
151 153 category='error')
152 154 return htmlfill.render(
153 155 render('admin/settings/settings.mako'),
154 156 defaults=errors.value,
155 157 errors=errors.error_dict or {},
156 158 prefix_error=False,
157 159 encoding="UTF-8",
158 160 force_defaults=False
159 161 )
160 162
161 163 try:
162 164 if c.visual.allow_repo_location_change:
163 165 model.update_global_path_setting(
164 166 form_result['paths_root_path'])
165 167
166 168 model.update_global_ssl_setting(form_result['web_push_ssl'])
167 169 model.update_global_hook_settings(form_result)
168 170
169 171 model.create_or_update_global_svn_settings(form_result)
170 172 model.create_or_update_global_hg_settings(form_result)
171 173 model.create_or_update_global_pr_settings(form_result)
172 174 except Exception:
173 175 log.exception("Exception while updating settings")
174 176 h.flash(_('Error occurred during updating '
175 177 'application settings'), category='error')
176 178 else:
177 179 Session().commit()
178 180 h.flash(_('Updated VCS settings'), category='success')
179 181 return redirect(url('admin_settings_vcs'))
180 182
181 183 return htmlfill.render(
182 184 render('admin/settings/settings.mako'),
183 185 defaults=self._form_defaults(),
184 186 encoding="UTF-8",
185 187 force_defaults=False)
186 188
187 189 @HasPermissionAllDecorator('hg.admin')
188 190 def settings_vcs(self):
189 191 """GET /admin/settings: All items in the collection"""
190 192 # url('admin_settings_vcs')
191 193 c.active = 'vcs'
192 194 model = VcsSettingsModel()
193 195 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
194 196 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
195 197
196 198 # TODO: Replace with request.registry after migrating to pyramid.
197 199 pyramid_settings = get_current_registry().settings
198 200 c.svn_proxy_generate_config = pyramid_settings[generate_config]
199 201
200 202 return htmlfill.render(
201 203 render('admin/settings/settings.mako'),
202 204 defaults=self._form_defaults(),
203 205 encoding="UTF-8",
204 206 force_defaults=False)
205 207
206 208 @HasPermissionAllDecorator('hg.admin')
207 209 @auth.CSRFRequired()
208 210 def settings_mapping_update(self):
209 211 """POST /admin/settings/mapping: All items in the collection"""
210 212 # url('admin_settings_mapping')
211 213 c.active = 'mapping'
212 214 rm_obsolete = request.POST.get('destroy', False)
213 215 invalidate_cache = request.POST.get('invalidate', False)
214 216 log.debug(
215 217 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
216 218
217 219 if invalidate_cache:
218 220 log.debug('invalidating all repositories cache')
219 221 for repo in Repository.get_all():
220 222 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
221 223
222 224 filesystem_repos = ScmModel().repo_scan()
223 225 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
224 226 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
225 227 h.flash(_('Repositories successfully '
226 228 'rescanned added: %s ; removed: %s') %
227 229 (_repr(added), _repr(removed)),
228 230 category='success')
229 231 return redirect(url('admin_settings_mapping'))
230 232
231 233 @HasPermissionAllDecorator('hg.admin')
232 234 def settings_mapping(self):
233 235 """GET /admin/settings/mapping: All items in the collection"""
234 236 # url('admin_settings_mapping')
235 237 c.active = 'mapping'
236 238
237 239 return htmlfill.render(
238 240 render('admin/settings/settings.mako'),
239 241 defaults=self._form_defaults(),
240 242 encoding="UTF-8",
241 243 force_defaults=False)
242 244
243 245 @HasPermissionAllDecorator('hg.admin')
244 246 @auth.CSRFRequired()
245 247 def settings_global_update(self):
246 248 """POST /admin/settings/global: All items in the collection"""
247 249 # url('admin_settings_global')
248 250 c.active = 'global'
249 251 c.personal_repo_group_default_pattern = RepoGroupModel()\
250 252 .get_personal_group_name_pattern()
251 253 application_form = ApplicationSettingsForm()()
252 254 try:
253 255 form_result = application_form.to_python(dict(request.POST))
254 256 except formencode.Invalid as errors:
255 257 return htmlfill.render(
256 258 render('admin/settings/settings.mako'),
257 259 defaults=errors.value,
258 260 errors=errors.error_dict or {},
259 261 prefix_error=False,
260 262 encoding="UTF-8",
261 263 force_defaults=False)
262 264
263 265 try:
264 266 settings = [
265 267 ('title', 'rhodecode_title', 'unicode'),
266 268 ('realm', 'rhodecode_realm', 'unicode'),
267 269 ('pre_code', 'rhodecode_pre_code', 'unicode'),
268 270 ('post_code', 'rhodecode_post_code', 'unicode'),
269 271 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
270 272 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
271 273 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
272 274 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
273 275 ]
274 276 for setting, form_key, type_ in settings:
275 277 sett = SettingsModel().create_or_update_setting(
276 278 setting, form_result[form_key], type_)
277 279 Session().add(sett)
278 280
279 281 Session().commit()
280 282 SettingsModel().invalidate_settings_cache()
281 283 h.flash(_('Updated application settings'), category='success')
282 284 except Exception:
283 285 log.exception("Exception while updating application settings")
284 286 h.flash(
285 287 _('Error occurred during updating application settings'),
286 288 category='error')
287 289
288 290 return redirect(url('admin_settings_global'))
289 291
290 292 @HasPermissionAllDecorator('hg.admin')
291 293 def settings_global(self):
292 294 """GET /admin/settings/global: All items in the collection"""
293 295 # url('admin_settings_global')
294 296 c.active = 'global'
295 297 c.personal_repo_group_default_pattern = RepoGroupModel()\
296 298 .get_personal_group_name_pattern()
297 299
298 300 return htmlfill.render(
299 301 render('admin/settings/settings.mako'),
300 302 defaults=self._form_defaults(),
301 303 encoding="UTF-8",
302 304 force_defaults=False)
303 305
304 306 @HasPermissionAllDecorator('hg.admin')
305 307 @auth.CSRFRequired()
306 308 def settings_visual_update(self):
307 309 """POST /admin/settings/visual: All items in the collection"""
308 310 # url('admin_settings_visual')
309 311 c.active = 'visual'
310 312 application_form = ApplicationVisualisationForm()()
311 313 try:
312 314 form_result = application_form.to_python(dict(request.POST))
313 315 except formencode.Invalid as errors:
314 316 return htmlfill.render(
315 317 render('admin/settings/settings.mako'),
316 318 defaults=errors.value,
317 319 errors=errors.error_dict or {},
318 320 prefix_error=False,
319 321 encoding="UTF-8",
320 322 force_defaults=False
321 323 )
322 324
323 325 try:
324 326 settings = [
325 327 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
326 328 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
327 329 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
328 330 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
329 331 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
330 332 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
331 333 ('show_version', 'rhodecode_show_version', 'bool'),
332 334 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
333 335 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
334 336 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
335 337 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
336 338 ('support_url', 'rhodecode_support_url', 'unicode'),
337 339 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
338 340 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
339 341 ]
340 342 for setting, form_key, type_ in settings:
341 343 sett = SettingsModel().create_or_update_setting(
342 344 setting, form_result[form_key], type_)
343 345 Session().add(sett)
344 346
345 347 Session().commit()
346 348 SettingsModel().invalidate_settings_cache()
347 349 h.flash(_('Updated visualisation settings'), category='success')
348 350 except Exception:
349 351 log.exception("Exception updating visualization settings")
350 352 h.flash(_('Error occurred during updating '
351 353 'visualisation settings'),
352 354 category='error')
353 355
354 356 return redirect(url('admin_settings_visual'))
355 357
356 358 @HasPermissionAllDecorator('hg.admin')
357 359 def settings_visual(self):
358 360 """GET /admin/settings/visual: All items in the collection"""
359 361 # url('admin_settings_visual')
360 362 c.active = 'visual'
361 363
362 364 return htmlfill.render(
363 365 render('admin/settings/settings.mako'),
364 366 defaults=self._form_defaults(),
365 367 encoding="UTF-8",
366 368 force_defaults=False)
367 369
368 370 @HasPermissionAllDecorator('hg.admin')
369 371 @auth.CSRFRequired()
370 372 def settings_issuetracker_test(self):
371 373 if request.is_xhr:
372 374 return h.urlify_commit_message(
373 375 request.POST.get('test_text', ''),
374 376 'repo_group/test_repo1')
375 377 else:
376 378 raise HTTPBadRequest()
377 379
378 380 @HasPermissionAllDecorator('hg.admin')
379 381 @auth.CSRFRequired()
380 382 def settings_issuetracker_delete(self):
381 383 uid = request.POST.get('uid')
382 384 IssueTrackerSettingsModel().delete_entries(uid)
383 385 h.flash(_('Removed issue tracker entry'), category='success')
384 386 return redirect(url('admin_settings_issuetracker'))
385 387
386 388 @HasPermissionAllDecorator('hg.admin')
387 389 def settings_issuetracker(self):
388 390 """GET /admin/settings/issue-tracker: All items in the collection"""
389 391 # url('admin_settings_issuetracker')
390 392 c.active = 'issuetracker'
391 393 defaults = SettingsModel().get_all_settings()
392 394
393 395 entry_key = 'rhodecode_issuetracker_pat_'
394 396
395 397 c.issuetracker_entries = {}
396 398 for k, v in defaults.items():
397 399 if k.startswith(entry_key):
398 400 uid = k[len(entry_key):]
399 401 c.issuetracker_entries[uid] = None
400 402
401 403 for uid in c.issuetracker_entries:
402 404 c.issuetracker_entries[uid] = AttributeDict({
403 405 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
404 406 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
405 407 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
406 408 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
407 409 })
408 410
409 411 return render('admin/settings/settings.mako')
410 412
411 413 @HasPermissionAllDecorator('hg.admin')
412 414 @auth.CSRFRequired()
413 415 def settings_issuetracker_save(self):
414 416 settings_model = IssueTrackerSettingsModel()
415 417
416 418 form = IssueTrackerPatternsForm()().to_python(request.POST)
417 419 if form:
418 420 for uid in form.get('delete_patterns', []):
419 421 settings_model.delete_entries(uid)
420 422
421 423 for pattern in form.get('patterns', []):
422 424 for setting, value, type_ in pattern:
423 425 sett = settings_model.create_or_update_setting(
424 426 setting, value, type_)
425 427 Session().add(sett)
426 428
427 429 Session().commit()
428 430
429 431 SettingsModel().invalidate_settings_cache()
430 432 h.flash(_('Updated issue tracker entries'), category='success')
431 433 return redirect(url('admin_settings_issuetracker'))
432 434
433 435 @HasPermissionAllDecorator('hg.admin')
434 436 @auth.CSRFRequired()
435 437 def settings_email_update(self):
436 438 """POST /admin/settings/email: All items in the collection"""
437 439 # url('admin_settings_email')
438 440 c.active = 'email'
439 441
440 442 test_email = request.POST.get('test_email')
441 443
442 444 if not test_email:
443 445 h.flash(_('Please enter email address'), category='error')
444 446 return redirect(url('admin_settings_email'))
445 447
446 448 email_kwargs = {
447 449 'date': datetime.datetime.now(),
448 450 'user': c.rhodecode_user,
449 451 'rhodecode_version': c.rhodecode_version
450 452 }
451 453
452 454 (subject, headers, email_body,
453 455 email_body_plaintext) = EmailNotificationModel().render_email(
454 456 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
455 457
456 458 recipients = [test_email] if test_email else None
457 459
458 460 run_task(tasks.send_email, recipients, subject,
459 461 email_body_plaintext, email_body)
460 462
461 463 h.flash(_('Send email task created'), category='success')
462 464 return redirect(url('admin_settings_email'))
463 465
464 466 @HasPermissionAllDecorator('hg.admin')
465 467 def settings_email(self):
466 468 """GET /admin/settings/email: All items in the collection"""
467 469 # url('admin_settings_email')
468 470 c.active = 'email'
469 471 c.rhodecode_ini = rhodecode.CONFIG
470 472
471 473 return htmlfill.render(
472 474 render('admin/settings/settings.mako'),
473 475 defaults=self._form_defaults(),
474 476 encoding="UTF-8",
475 477 force_defaults=False)
476 478
477 479 @HasPermissionAllDecorator('hg.admin')
478 480 @auth.CSRFRequired()
479 481 def settings_hooks_update(self):
480 482 """POST or DELETE /admin/settings/hooks: All items in the collection"""
481 483 # url('admin_settings_hooks')
482 484 c.active = 'hooks'
483 485 if c.visual.allow_custom_hooks_settings:
484 486 ui_key = request.POST.get('new_hook_ui_key')
485 487 ui_value = request.POST.get('new_hook_ui_value')
486 488
487 489 hook_id = request.POST.get('hook_id')
488 490 new_hook = False
489 491
490 492 model = SettingsModel()
491 493 try:
492 494 if ui_value and ui_key:
493 495 model.create_or_update_hook(ui_key, ui_value)
494 496 h.flash(_('Added new hook'), category='success')
495 497 new_hook = True
496 498 elif hook_id:
497 499 RhodeCodeUi.delete(hook_id)
498 500 Session().commit()
499 501
500 502 # check for edits
501 503 update = False
502 504 _d = request.POST.dict_of_lists()
503 505 for k, v in zip(_d.get('hook_ui_key', []),
504 506 _d.get('hook_ui_value_new', [])):
505 507 model.create_or_update_hook(k, v)
506 508 update = True
507 509
508 510 if update and not new_hook:
509 511 h.flash(_('Updated hooks'), category='success')
510 512 Session().commit()
511 513 except Exception:
512 514 log.exception("Exception during hook creation")
513 515 h.flash(_('Error occurred during hook creation'),
514 516 category='error')
515 517
516 518 return redirect(url('admin_settings_hooks'))
517 519
518 520 @HasPermissionAllDecorator('hg.admin')
519 521 def settings_hooks(self):
520 522 """GET /admin/settings/hooks: All items in the collection"""
521 523 # url('admin_settings_hooks')
522 524 c.active = 'hooks'
523 525
524 526 model = SettingsModel()
525 527 c.hooks = model.get_builtin_hooks()
526 528 c.custom_hooks = model.get_custom_hooks()
527 529
528 530 return htmlfill.render(
529 531 render('admin/settings/settings.mako'),
530 532 defaults=self._form_defaults(),
531 533 encoding="UTF-8",
532 534 force_defaults=False)
533 535
534 536 @HasPermissionAllDecorator('hg.admin')
535 537 def settings_search(self):
536 538 """GET /admin/settings/search: All items in the collection"""
537 539 # url('admin_settings_search')
538 540 c.active = 'search'
539 541
540 542 from rhodecode.lib.index import searcher_from_config
541 543 searcher = searcher_from_config(config)
542 544 c.statistics = searcher.statistics()
543 545
544 546 return render('admin/settings/settings.mako')
545 547
546 548 @HasPermissionAllDecorator('hg.admin')
547 549 def settings_system(self):
548 550 """GET /admin/settings/system: All items in the collection"""
549 551 # url('admin_settings_system')
550 552 snapshot = str2bool(request.GET.get('snapshot'))
551 553 defaults = self._form_defaults()
552 554
553 555 c.active = 'system'
554 556 c.rhodecode_update_url = defaults.get('rhodecode_update_url')
555 557 server_info = ScmModel().get_server_info(request.environ)
556 558
557 559 for key, val in server_info.iteritems():
558 560 setattr(c, key, val)
559 561
560 562 def val(name, subkey='human_value'):
561 563 return server_info[name][subkey]
562 564
563 565 def state(name):
564 566 return server_info[name]['state']
565 567
566 568 def val2(name):
567 569 val = server_info[name]['human_value']
568 570 state = server_info[name]['state']
569 571 return val, state
570 572
571 573 c.data_items = [
572 574 # update info
573 575 (_('Update info'), h.literal(
574 576 '<span class="link" id="check_for_update" >%s.</span>' % (
575 577 _('Check for updates')) +
576 578 '<br/> <span >%s.</span>' % (_('Note: please make sure this server can access `%s` for the update link to work') % c.rhodecode_update_url)
577 579 ), ''),
578 580
579 581 # RhodeCode specific
580 582 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
581 583 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
582 584 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
583 585 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
584 586 ('', '', ''), # spacer
585 587
586 588 # Database
587 589 (_('Database'), val('database')['url'], state('database')),
588 590 (_('Database version'), val('database')['version'], state('database')),
589 591 ('', '', ''), # spacer
590 592
591 593 # Platform/Python
592 594 (_('Platform'), val('platform')['name'], state('platform')),
593 595 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
594 596 (_('Python version'), val('python')['version'], state('python')),
595 597 (_('Python path'), val('python')['executable'], state('python')),
596 598 ('', '', ''), # spacer
597 599
598 600 # Systems stats
599 601 (_('CPU'), val('cpu'), state('cpu')),
600 602 (_('Load'), val('load')['text'], state('load')),
601 603 (_('Memory'), val('memory')['text'], state('memory')),
602 604 (_('Uptime'), val('uptime')['text'], state('uptime')),
603 605 ('', '', ''), # spacer
604 606
605 607 # Repo storage
606 608 (_('Storage location'), val('storage')['path'], state('storage')),
607 609 (_('Storage info'), val('storage')['text'], state('storage')),
608 610 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
609 611
610 612 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
611 613 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
612 614
613 615 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
614 616 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
615 617
616 618 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
617 619 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
618 620
619 621 (_('Search info'), val('search')['text'], state('search')),
620 622 (_('Search location'), val('search')['location'], state('search')),
621 623 ('', '', ''), # spacer
622 624
623 625 # VCS specific
624 626 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
625 627 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
626 628 (_('GIT'), val('git'), state('git')),
627 629 (_('HG'), val('hg'), state('hg')),
628 630 (_('SVN'), val('svn'), state('svn')),
629 631
630 632 ]
631 633
632 634 # TODO: marcink, figure out how to allow only selected users to do this
633 635 c.allowed_to_snapshot = c.rhodecode_user.admin
634 636
635 637 if snapshot:
636 638 if c.allowed_to_snapshot:
637 639 c.data_items.pop(0) # remove server info
638 640 return render('admin/settings/settings_system_snapshot.mako')
639 641 else:
640 642 h.flash('You are not allowed to do this', category='warning')
641 643
642 644 return htmlfill.render(
643 645 render('admin/settings/settings.mako'),
644 646 defaults=defaults,
645 647 encoding="UTF-8",
646 648 force_defaults=False)
647 649
648 650 @staticmethod
649 651 def get_update_data(update_url):
650 652 """Return the JSON update data."""
651 653 ver = rhodecode.__version__
652 654 log.debug('Checking for upgrade on `%s` server', update_url)
653 655 opener = urllib2.build_opener()
654 656 opener.addheaders = [('User-agent', 'RhodeCode-SCM/%s' % ver)]
655 657 response = opener.open(update_url)
656 658 response_data = response.read()
657 659 data = json.loads(response_data)
658 660
659 661 return data
660 662
661 663 @HasPermissionAllDecorator('hg.admin')
662 664 def settings_system_update(self):
663 665 """GET /admin/settings/system/updates: All items in the collection"""
664 666 # url('admin_settings_system_update')
665 667 defaults = self._form_defaults()
666 668 update_url = defaults.get('rhodecode_update_url', '')
667 669
668 670 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">%s</div>' % (s)
669 671 try:
670 672 data = self.get_update_data(update_url)
671 673 except urllib2.URLError as e:
672 674 log.exception("Exception contacting upgrade server")
673 675 return _err('Failed to contact upgrade server: %r' % e)
674 676 except ValueError as e:
675 677 log.exception("Bad data sent from update server")
676 678 return _err('Bad data sent from update server')
677 679
678 680 latest = data['versions'][0]
679 681
680 682 c.update_url = update_url
681 683 c.latest_data = latest
682 684 c.latest_ver = latest['version']
683 685 c.cur_ver = rhodecode.__version__
684 686 c.should_upgrade = False
685 687
686 688 if (packaging.version.Version(c.latest_ver) >
687 689 packaging.version.Version(c.cur_ver)):
688 690 c.should_upgrade = True
689 691 c.important_notices = latest['general']
690 692
691 693 return render('admin/settings/settings_system_update.mako')
692 694
693 695 @HasPermissionAllDecorator('hg.admin')
696 def settings_sessions(self):
697 # url('admin_settings_sessions')
698
699 c.active = 'sessions'
700 c.cleanup_older_days = 60
701 older_than_seconds = 24 * 60 * 60 * 24 * c.cleanup_older_days
702
703 config = system_info.rhodecode_config().get_value()['value']['config']
704 c.session_model = user_sessions.get_session_handler(
705 config.get('beaker.session.type', 'memory'))(config)
706
707 c.session_conf = c.session_model.config
708 c.session_count = c.session_model.get_count()
709 c.session_expired_count = c.session_model.get_expired_count(
710 older_than_seconds)
711
712 return render('admin/settings/settings.mako')
713
714 @HasPermissionAllDecorator('hg.admin')
715 def settings_sessions_cleanup(self):
716 # url('admin_settings_sessions_update')
717
718 expire_days = safe_int(request.POST.get('expire_days'))
719
720 if expire_days is None:
721 expire_days = 60
722
723 older_than_seconds = 24 * 60 * 60 * 24 * expire_days
724
725 config = system_info.rhodecode_config().get_value()['value']['config']
726 session_model = user_sessions.get_session_handler(
727 config.get('beaker.session.type', 'memory'))(config)
728
729 try:
730 session_model.clean_sessions(
731 older_than_seconds=older_than_seconds)
732 h.flash(_('Cleaned up old sessions'), category='success')
733 except user_sessions.CleanupCommand as msg:
734 h.flash(msg, category='warning')
735 except Exception as e:
736 log.exception('Failed session cleanup')
737 h.flash(_('Failed to cleanup up old sessions'), category='error')
738
739 return redirect(url('admin_settings_sessions'))
740
741 @HasPermissionAllDecorator('hg.admin')
694 742 def settings_supervisor(self):
695 743 c.rhodecode_ini = rhodecode.CONFIG
696 744 c.active = 'supervisor'
697 745
698 746 c.supervisor_procs = OrderedDict([
699 747 (SUPERVISOR_MASTER, {}),
700 748 ])
701 749
702 750 c.log_size = 10240
703 751 supervisor = SupervisorModel()
704 752
705 753 _connection = supervisor.get_connection(
706 754 c.rhodecode_ini.get('supervisor.uri'))
707 755 c.connection_error = None
708 756 try:
709 757 _connection.supervisor.getAllProcessInfo()
710 758 except Exception as e:
711 759 c.connection_error = str(e)
712 760 log.exception("Exception reading supervisor data")
713 761 return render('admin/settings/settings.mako')
714 762
715 763 groupid = c.rhodecode_ini.get('supervisor.group_id')
716 764
717 765 # feed our group processes to the main
718 766 for proc in supervisor.get_group_processes(_connection, groupid):
719 767 c.supervisor_procs[proc['name']] = {}
720 768
721 769 for k in c.supervisor_procs.keys():
722 770 try:
723 771 # master process info
724 772 if k == SUPERVISOR_MASTER:
725 773 _data = supervisor.get_master_state(_connection)
726 774 _data['name'] = 'supervisor master'
727 775 _data['description'] = 'pid %s, id: %s, ver: %s' % (
728 776 _data['pid'], _data['id'], _data['ver'])
729 777 c.supervisor_procs[k] = _data
730 778 else:
731 779 procid = groupid + ":" + k
732 780 c.supervisor_procs[k] = supervisor.get_process_info(_connection, procid)
733 781 except Exception as e:
734 782 log.exception("Exception reading supervisor data")
735 783 c.supervisor_procs[k] = {'_rhodecode_error': str(e)}
736 784
737 785 return render('admin/settings/settings.mako')
738 786
739 787 @HasPermissionAllDecorator('hg.admin')
740 788 def settings_supervisor_log(self, procid):
741 789 import rhodecode
742 790 c.rhodecode_ini = rhodecode.CONFIG
743 791 c.active = 'supervisor_tail'
744 792
745 793 supervisor = SupervisorModel()
746 794 _connection = supervisor.get_connection(c.rhodecode_ini.get('supervisor.uri'))
747 795 groupid = c.rhodecode_ini.get('supervisor.group_id')
748 796 procid = groupid + ":" + procid if procid != SUPERVISOR_MASTER else procid
749 797
750 798 c.log_size = 10240
751 799 offset = abs(safe_int(request.GET.get('offset', c.log_size))) * -1
752 800 c.log = supervisor.read_process_log(_connection, procid, offset, 0)
753 801
754 802 return render('admin/settings/settings.mako')
755 803
756 804 @HasPermissionAllDecorator('hg.admin')
757 805 @auth.CSRFRequired()
758 806 def settings_labs_update(self):
759 807 """POST /admin/settings/labs: All items in the collection"""
760 808 # url('admin_settings/labs', method={'POST'})
761 809 c.active = 'labs'
762 810
763 811 application_form = LabsSettingsForm()()
764 812 try:
765 813 form_result = application_form.to_python(dict(request.POST))
766 814 except formencode.Invalid as errors:
767 815 h.flash(
768 816 _('Some form inputs contain invalid data.'),
769 817 category='error')
770 818 return htmlfill.render(
771 819 render('admin/settings/settings.mako'),
772 820 defaults=errors.value,
773 821 errors=errors.error_dict or {},
774 822 prefix_error=False,
775 823 encoding='UTF-8',
776 824 force_defaults=False
777 825 )
778 826
779 827 try:
780 828 session = Session()
781 829 for setting in _LAB_SETTINGS:
782 830 setting_name = setting.key[len('rhodecode_'):]
783 831 sett = SettingsModel().create_or_update_setting(
784 832 setting_name, form_result[setting.key], setting.type)
785 833 session.add(sett)
786 834
787 835 except Exception:
788 836 log.exception('Exception while updating lab settings')
789 837 h.flash(_('Error occurred during updating labs settings'),
790 838 category='error')
791 839 else:
792 840 Session().commit()
793 841 SettingsModel().invalidate_settings_cache()
794 842 h.flash(_('Updated Labs settings'), category='success')
795 843 return redirect(url('admin_settings_labs'))
796 844
797 845 return htmlfill.render(
798 846 render('admin/settings/settings.mako'),
799 847 defaults=self._form_defaults(),
800 848 encoding='UTF-8',
801 849 force_defaults=False)
802 850
803 851 @HasPermissionAllDecorator('hg.admin')
804 852 def settings_labs(self):
805 853 """GET /admin/settings/labs: All items in the collection"""
806 854 # url('admin_settings_labs')
807 855 if not c.labs_active:
808 856 redirect(url('admin_settings'))
809 857
810 858 c.active = 'labs'
811 859 c.lab_settings = _LAB_SETTINGS
812 860
813 861 return htmlfill.render(
814 862 render('admin/settings/settings.mako'),
815 863 defaults=self._form_defaults(),
816 864 encoding='UTF-8',
817 865 force_defaults=False)
818 866
819 867 def _form_defaults(self):
820 868 defaults = SettingsModel().get_all_settings()
821 869 defaults.update(self._get_hg_ui_settings())
822 870 defaults.update({
823 871 'new_svn_branch': '',
824 872 'new_svn_tag': '',
825 873 })
826 874 return defaults
827 875
828 876
829 877 # :param key: name of the setting including the 'rhodecode_' prefix
830 878 # :param type: the RhodeCodeSetting type to use.
831 879 # :param group: the i18ned group in which we should dispaly this setting
832 880 # :param label: the i18ned label we should display for this setting
833 881 # :param help: the i18ned help we should dispaly for this setting
834 882 LabSetting = collections.namedtuple(
835 883 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
836 884
837 885
838 886 # This list has to be kept in sync with the form
839 887 # rhodecode.model.forms.LabsSettingsForm.
840 888 _LAB_SETTINGS = [
841 889
842 890 ]
@@ -1,639 +1,642 b''
1 1 import os
2 2 import sys
3 3 import time
4 4 import platform
5 5 import pkg_resources
6 6 import logging
7 7 import string
8 8
9 9
10 10 log = logging.getLogger(__name__)
11 11
12 12
13 13 psutil = None
14 14
15 15 try:
16 16 # cygwin cannot have yet psutil support.
17 17 import psutil as psutil
18 18 except ImportError:
19 19 pass
20 20
21 21
22 22 _NA = 'NOT AVAILABLE'
23 23
24 24 STATE_OK = 'ok'
25 25 STATE_ERR = 'error'
26 26 STATE_WARN = 'warning'
27 27
28 28 STATE_OK_DEFAULT = {'message': '', 'type': STATE_OK}
29 29
30 30
31 31 # HELPERS
32 32 def percentage(part, whole):
33 33 whole = float(whole)
34 34 if whole > 0:
35 35 return round(100 * float(part) / whole, 1)
36 36 return 0.0
37 37
38 38
39 39 def get_storage_size(storage_path):
40 40 sizes = []
41 41 for file_ in os.listdir(storage_path):
42 42 storage_file = os.path.join(storage_path, file_)
43 43 if os.path.isfile(storage_file):
44 44 try:
45 45 sizes.append(os.path.getsize(storage_file))
46 46 except OSError:
47 47 log.exception('Failed to get size of storage file %s',
48 48 storage_file)
49 49 pass
50 50
51 51 return sum(sizes)
52 52
53 53
54 54 class SysInfoRes(object):
55 55 def __init__(self, value, state=STATE_OK_DEFAULT, human_value=None):
56 56 self.value = value
57 57 self.state = state
58 58 self.human_value = human_value or value
59 59
60 60 def __json__(self):
61 61 return {
62 62 'value': self.value,
63 63 'state': self.state,
64 64 'human_value': self.human_value,
65 65 }
66 66
67 def get_value(self):
68 return self.__json__()
69
67 70 def __str__(self):
68 71 return '<SysInfoRes({})>'.format(self.__json__())
69 72
70 73
71 74 class SysInfo(object):
72 75
73 76 def __init__(self, func_name, **kwargs):
74 77 self.func_name = func_name
75 78 self.value = _NA
76 79 self.state = None
77 80 self.kwargs = kwargs or {}
78 81
79 82 def __call__(self):
80 83 computed = self.compute(**self.kwargs)
81 84 if not isinstance(computed, SysInfoRes):
82 85 raise ValueError(
83 86 'computed value for {} is not instance of '
84 87 '{}, got {} instead'.format(
85 88 self.func_name, SysInfoRes, type(computed)))
86 89 return computed.__json__()
87 90
88 91 def __str__(self):
89 92 return '<SysInfo({})>'.format(self.func_name)
90 93
91 94 def compute(self, **kwargs):
92 95 return self.func_name(**kwargs)
93 96
94 97
95 98 # SysInfo functions
96 99 def python_info():
97 100 value = dict(version=' '.join(platform._sys_version()),
98 101 executable=sys.executable)
99 102 return SysInfoRes(value=value)
100 103
101 104
102 105 def py_modules():
103 106 mods = dict([(p.project_name, p.version)
104 107 for p in pkg_resources.working_set])
105 108 value = sorted(mods.items(), key=lambda k: k[0].lower())
106 109 return SysInfoRes(value=value)
107 110
108 111
109 112 def platform_type():
110 113 from rhodecode.lib.utils import safe_unicode, generate_platform_uuid
111 114
112 115 value = dict(
113 116 name=safe_unicode(platform.platform()),
114 117 uuid=generate_platform_uuid()
115 118 )
116 119 return SysInfoRes(value=value)
117 120
118 121
119 122 def uptime():
120 123 from rhodecode.lib.helpers import age, time_to_datetime
121 124
122 125 value = dict(boot_time=0, uptime=0, text='')
123 126 state = STATE_OK_DEFAULT
124 127 if not psutil:
125 128 return SysInfoRes(value=value, state=state)
126 129
127 130 boot_time = psutil.boot_time()
128 131 value['boot_time'] = boot_time
129 132 value['uptime'] = time.time() - boot_time
130 133
131 134 human_value = value.copy()
132 135 human_value['boot_time'] = time_to_datetime(boot_time)
133 136 human_value['uptime'] = age(time_to_datetime(boot_time), show_suffix=False)
134 137 human_value['text'] = 'Server started {}'.format(
135 138 age(time_to_datetime(boot_time)))
136 139
137 140 return SysInfoRes(value=value, human_value=human_value)
138 141
139 142
140 143 def memory():
141 144 from rhodecode.lib.helpers import format_byte_size_binary
142 145 value = dict(available=0, used=0, used_real=0, cached=0, percent=0,
143 146 percent_used=0, free=0, inactive=0, active=0, shared=0,
144 147 total=0, buffers=0, text='')
145 148
146 149 state = STATE_OK_DEFAULT
147 150 if not psutil:
148 151 return SysInfoRes(value=value, state=state)
149 152
150 153 value.update(dict(psutil.virtual_memory()._asdict()))
151 154 value['used_real'] = value['total'] - value['available']
152 155 value['percent_used'] = psutil._common.usage_percent(
153 156 value['used_real'], value['total'], 1)
154 157
155 158 human_value = value.copy()
156 159 human_value['text'] = '%s/%s, %s%% used' % (
157 160 format_byte_size_binary(value['used_real']),
158 161 format_byte_size_binary(value['total']),
159 162 value['percent_used'],)
160 163
161 164 keys = value.keys()[::]
162 165 keys.pop(keys.index('percent'))
163 166 keys.pop(keys.index('percent_used'))
164 167 keys.pop(keys.index('text'))
165 168 for k in keys:
166 169 human_value[k] = format_byte_size_binary(value[k])
167 170
168 171 if state['type'] == STATE_OK and value['percent_used'] > 90:
169 172 msg = 'Critical: your available RAM memory is very low.'
170 173 state = {'message': msg, 'type': STATE_ERR}
171 174
172 175 elif state['type'] == STATE_OK and value['percent_used'] > 70:
173 176 msg = 'Warning: your available RAM memory is running low.'
174 177 state = {'message': msg, 'type': STATE_WARN}
175 178
176 179 return SysInfoRes(value=value, state=state, human_value=human_value)
177 180
178 181
179 182 def machine_load():
180 183 value = {'1_min': _NA, '5_min': _NA, '15_min': _NA, 'text': ''}
181 184 state = STATE_OK_DEFAULT
182 185 if not psutil:
183 186 return SysInfoRes(value=value, state=state)
184 187
185 188 # load averages
186 189 if hasattr(psutil.os, 'getloadavg'):
187 190 value.update(dict(
188 191 zip(['1_min', '5_min', '15_min'], psutil.os.getloadavg())))
189 192
190 193 human_value = value.copy()
191 194 human_value['text'] = '1min: {}, 5min: {}, 15min: {}'.format(
192 195 value['1_min'], value['5_min'], value['15_min'])
193 196
194 197 if state['type'] == STATE_OK and value['15_min'] > 5:
195 198 msg = 'Warning: your machine load is very high.'
196 199 state = {'message': msg, 'type': STATE_WARN}
197 200
198 201 return SysInfoRes(value=value, state=state, human_value=human_value)
199 202
200 203
201 204 def cpu():
202 205 value = 0
203 206 state = STATE_OK_DEFAULT
204 207
205 208 if not psutil:
206 209 return SysInfoRes(value=value, state=state)
207 210
208 211 value = psutil.cpu_percent(0.5)
209 212 human_value = '{} %'.format(value)
210 213 return SysInfoRes(value=value, state=state, human_value=human_value)
211 214
212 215
213 216 def storage():
214 217 from rhodecode.lib.helpers import format_byte_size_binary
215 218 from rhodecode.model.settings import VcsSettingsModel
216 219 path = VcsSettingsModel().get_repos_location()
217 220
218 221 value = dict(percent=0, used=0, total=0, path=path, text='')
219 222 state = STATE_OK_DEFAULT
220 223 if not psutil:
221 224 return SysInfoRes(value=value, state=state)
222 225
223 226 try:
224 227 value.update(dict(psutil.disk_usage(path)._asdict()))
225 228 except Exception as e:
226 229 log.exception('Failed to fetch disk info')
227 230 state = {'message': str(e), 'type': STATE_ERR}
228 231
229 232 human_value = value.copy()
230 233 human_value['used'] = format_byte_size_binary(value['used'])
231 234 human_value['total'] = format_byte_size_binary(value['total'])
232 235 human_value['text'] = "{}/{}, {}% used".format(
233 236 format_byte_size_binary(value['used']),
234 237 format_byte_size_binary(value['total']),
235 238 value['percent'])
236 239
237 240 if state['type'] == STATE_OK and value['percent'] > 90:
238 241 msg = 'Critical: your disk space is very low.'
239 242 state = {'message': msg, 'type': STATE_ERR}
240 243
241 244 elif state['type'] == STATE_OK and value['percent'] > 70:
242 245 msg = 'Warning: your disk space is running low.'
243 246 state = {'message': msg, 'type': STATE_WARN}
244 247
245 248 return SysInfoRes(value=value, state=state, human_value=human_value)
246 249
247 250
248 251 def storage_inodes():
249 252 from rhodecode.model.settings import VcsSettingsModel
250 253 path = VcsSettingsModel().get_repos_location()
251 254
252 255 value = dict(percent=0, free=0, used=0, total=0, path=path, text='')
253 256 state = STATE_OK_DEFAULT
254 257 if not psutil:
255 258 return SysInfoRes(value=value, state=state)
256 259
257 260 try:
258 261 i_stat = os.statvfs(path)
259 262 value['free'] = i_stat.f_ffree
260 263 value['used'] = i_stat.f_files-i_stat.f_favail
261 264 value['total'] = i_stat.f_files
262 265 value['percent'] = percentage(value['used'], value['total'])
263 266 except Exception as e:
264 267 log.exception('Failed to fetch disk inodes info')
265 268 state = {'message': str(e), 'type': STATE_ERR}
266 269
267 270 human_value = value.copy()
268 271 human_value['text'] = "{}/{}, {}% used".format(
269 272 value['used'], value['total'], value['percent'])
270 273
271 274 if state['type'] == STATE_OK and value['percent'] > 90:
272 275 msg = 'Critical: your disk free inodes are very low.'
273 276 state = {'message': msg, 'type': STATE_ERR}
274 277
275 278 elif state['type'] == STATE_OK and value['percent'] > 70:
276 279 msg = 'Warning: your disk free inodes are running low.'
277 280 state = {'message': msg, 'type': STATE_WARN}
278 281
279 282 return SysInfoRes(value=value, state=state, human_value=human_value)
280 283
281 284
282 285 def storage_archives():
283 286 import rhodecode
284 287 from rhodecode.lib.utils import safe_str
285 288 from rhodecode.lib.helpers import format_byte_size_binary
286 289
287 290 msg = 'Enable this by setting ' \
288 291 'archive_cache_dir=/path/to/cache option in the .ini file'
289 292 path = safe_str(rhodecode.CONFIG.get('archive_cache_dir', msg))
290 293
291 294 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
292 295 state = STATE_OK_DEFAULT
293 296 try:
294 297 items_count = 0
295 298 used = 0
296 299 for root, dirs, files in os.walk(path):
297 300 if root == path:
298 301 items_count = len(files)
299 302
300 303 for f in files:
301 304 try:
302 305 used += os.path.getsize(os.path.join(root, f))
303 306 except OSError:
304 307 pass
305 308 value.update({
306 309 'percent': 100,
307 310 'used': used,
308 311 'total': used,
309 312 'items': items_count
310 313 })
311 314
312 315 except Exception as e:
313 316 log.exception('failed to fetch archive cache storage')
314 317 state = {'message': str(e), 'type': STATE_ERR}
315 318
316 319 human_value = value.copy()
317 320 human_value['used'] = format_byte_size_binary(value['used'])
318 321 human_value['total'] = format_byte_size_binary(value['total'])
319 322 human_value['text'] = "{} ({} items)".format(
320 323 human_value['used'], value['items'])
321 324
322 325 return SysInfoRes(value=value, state=state, human_value=human_value)
323 326
324 327
325 328 def storage_gist():
326 329 from rhodecode.model.gist import GIST_STORE_LOC
327 330 from rhodecode.model.settings import VcsSettingsModel
328 331 from rhodecode.lib.utils import safe_str
329 332 from rhodecode.lib.helpers import format_byte_size_binary
330 333 path = safe_str(os.path.join(
331 334 VcsSettingsModel().get_repos_location(), GIST_STORE_LOC))
332 335
333 336 # gist storage
334 337 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
335 338 state = STATE_OK_DEFAULT
336 339
337 340 try:
338 341 items_count = 0
339 342 used = 0
340 343 for root, dirs, files in os.walk(path):
341 344 if root == path:
342 345 items_count = len(dirs)
343 346
344 347 for f in files:
345 348 try:
346 349 used += os.path.getsize(os.path.join(root, f))
347 350 except OSError:
348 351 pass
349 352 value.update({
350 353 'percent': 100,
351 354 'used': used,
352 355 'total': used,
353 356 'items': items_count
354 357 })
355 358 except Exception as e:
356 359 log.exception('failed to fetch gist storage items')
357 360 state = {'message': str(e), 'type': STATE_ERR}
358 361
359 362 human_value = value.copy()
360 363 human_value['used'] = format_byte_size_binary(value['used'])
361 364 human_value['total'] = format_byte_size_binary(value['total'])
362 365 human_value['text'] = "{} ({} items)".format(
363 366 human_value['used'], value['items'])
364 367
365 368 return SysInfoRes(value=value, state=state, human_value=human_value)
366 369
367 370
368 371 def storage_temp():
369 372 import tempfile
370 373 from rhodecode.lib.helpers import format_byte_size_binary
371 374
372 375 path = tempfile.gettempdir()
373 376 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
374 377 state = STATE_OK_DEFAULT
375 378
376 379 if not psutil:
377 380 return SysInfoRes(value=value, state=state)
378 381
379 382 try:
380 383 value.update(dict(psutil.disk_usage(path)._asdict()))
381 384 except Exception as e:
382 385 log.exception('Failed to fetch temp dir info')
383 386 state = {'message': str(e), 'type': STATE_ERR}
384 387
385 388 human_value = value.copy()
386 389 human_value['used'] = format_byte_size_binary(value['used'])
387 390 human_value['total'] = format_byte_size_binary(value['total'])
388 391 human_value['text'] = "{}/{}, {}% used".format(
389 392 format_byte_size_binary(value['used']),
390 393 format_byte_size_binary(value['total']),
391 394 value['percent'])
392 395
393 396 return SysInfoRes(value=value, state=state, human_value=human_value)
394 397
395 398
396 399 def search_info():
397 400 import rhodecode
398 401 from rhodecode.lib.index import searcher_from_config
399 402
400 403 backend = rhodecode.CONFIG.get('search.module', '')
401 404 location = rhodecode.CONFIG.get('search.location', '')
402 405
403 406 try:
404 407 searcher = searcher_from_config(rhodecode.CONFIG)
405 408 searcher = searcher.__class__.__name__
406 409 except Exception:
407 410 searcher = None
408 411
409 412 value = dict(
410 413 backend=backend, searcher=searcher, location=location, text='')
411 414 state = STATE_OK_DEFAULT
412 415
413 416 human_value = value.copy()
414 417 human_value['text'] = "backend:`{}`".format(human_value['backend'])
415 418
416 419 return SysInfoRes(value=value, state=state, human_value=human_value)
417 420
418 421
419 422 def git_info():
420 423 from rhodecode.lib.vcs.backends import git
421 424 state = STATE_OK_DEFAULT
422 425 value = human_value = ''
423 426 try:
424 427 value = git.discover_git_version(raise_on_exc=True)
425 428 human_value = 'version reported from VCSServer: {}'.format(value)
426 429 except Exception as e:
427 430 state = {'message': str(e), 'type': STATE_ERR}
428 431
429 432 return SysInfoRes(value=value, state=state, human_value=human_value)
430 433
431 434
432 435 def hg_info():
433 436 from rhodecode.lib.vcs.backends import hg
434 437 state = STATE_OK_DEFAULT
435 438 value = human_value = ''
436 439 try:
437 440 value = hg.discover_hg_version(raise_on_exc=True)
438 441 human_value = 'version reported from VCSServer: {}'.format(value)
439 442 except Exception as e:
440 443 state = {'message': str(e), 'type': STATE_ERR}
441 444 return SysInfoRes(value=value, state=state, human_value=human_value)
442 445
443 446
444 447 def svn_info():
445 448 from rhodecode.lib.vcs.backends import svn
446 449 state = STATE_OK_DEFAULT
447 450 value = human_value = ''
448 451 try:
449 452 value = svn.discover_svn_version(raise_on_exc=True)
450 453 human_value = 'version reported from VCSServer: {}'.format(value)
451 454 except Exception as e:
452 455 state = {'message': str(e), 'type': STATE_ERR}
453 456 return SysInfoRes(value=value, state=state, human_value=human_value)
454 457
455 458
456 459 def vcs_backends():
457 460 import rhodecode
458 461 value = map(
459 462 string.strip, rhodecode.CONFIG.get('vcs.backends', '').split(','))
460 463 human_value = 'Enabled backends in order: {}'.format(','.join(value))
461 464 return SysInfoRes(value=value, human_value=human_value)
462 465
463 466
464 467 def vcs_server():
465 468 import rhodecode
466 469 from rhodecode.lib.vcs.backends import get_vcsserver_version
467 470
468 471 server_url = rhodecode.CONFIG.get('vcs.server')
469 472 enabled = rhodecode.CONFIG.get('vcs.server.enable')
470 473 protocol = rhodecode.CONFIG.get('vcs.server.protocol') or 'http'
471 474 state = STATE_OK_DEFAULT
472 475 version = None
473 476
474 477 try:
475 478 version = get_vcsserver_version()
476 479 connection = 'connected'
477 480 except Exception as e:
478 481 connection = 'failed'
479 482 state = {'message': str(e), 'type': STATE_ERR}
480 483
481 484 value = dict(
482 485 url=server_url,
483 486 enabled=enabled,
484 487 protocol=protocol,
485 488 connection=connection,
486 489 version=version,
487 490 text='',
488 491 )
489 492
490 493 human_value = value.copy()
491 494 human_value['text'] = \
492 495 '{url}@ver:{ver} via {mode} mode, connection:{conn}'.format(
493 496 url=server_url, ver=version, mode=protocol, conn=connection)
494 497
495 498 return SysInfoRes(value=value, state=state, human_value=human_value)
496 499
497 500
498 501 def rhodecode_app_info():
499 502 import rhodecode
500 503 edition = rhodecode.CONFIG.get('rhodecode.edition')
501 504
502 505 value = dict(
503 506 rhodecode_version=rhodecode.__version__,
504 507 rhodecode_lib_path=os.path.abspath(rhodecode.__file__),
505 508 text=''
506 509 )
507 510 human_value = value.copy()
508 511 human_value['text'] = 'RhodeCode {edition}, version {ver}'.format(
509 512 edition=edition, ver=value['rhodecode_version']
510 513 )
511 514 return SysInfoRes(value=value, human_value=human_value)
512 515
513 516
514 517 def rhodecode_config():
515 518 import rhodecode
516 519 path = rhodecode.CONFIG.get('__file__')
517 520 rhodecode_ini_safe = rhodecode.CONFIG.copy()
518 521
519 522 blacklist = [
520 523 'rhodecode_license_key',
521 524 'routes.map',
522 525 'pylons.h',
523 526 'pylons.app_globals',
524 527 'pylons.environ_config',
525 528 'sqlalchemy.db1.url',
526 529 'channelstream.secret',
527 530 'beaker.session.secret',
528 531 'rhodecode.encrypted_values.secret',
529 532 'rhodecode_auth_github_consumer_key',
530 533 'rhodecode_auth_github_consumer_secret',
531 534 'rhodecode_auth_google_consumer_key',
532 535 'rhodecode_auth_google_consumer_secret',
533 536 'rhodecode_auth_bitbucket_consumer_secret',
534 537 'rhodecode_auth_bitbucket_consumer_key',
535 538 'rhodecode_auth_twitter_consumer_secret',
536 539 'rhodecode_auth_twitter_consumer_key',
537 540
538 541 'rhodecode_auth_twitter_secret',
539 542 'rhodecode_auth_github_secret',
540 543 'rhodecode_auth_google_secret',
541 544 'rhodecode_auth_bitbucket_secret',
542 545
543 546 'appenlight.api_key',
544 547 ('app_conf', 'sqlalchemy.db1.url')
545 548 ]
546 549 for k in blacklist:
547 550 if isinstance(k, tuple):
548 551 section, key = k
549 552 if section in rhodecode_ini_safe:
550 553 rhodecode_ini_safe[section] = '**OBFUSCATED**'
551 554 else:
552 555 rhodecode_ini_safe.pop(k, None)
553 556
554 557 # TODO: maybe put some CONFIG checks here ?
555 558 return SysInfoRes(value={'config': rhodecode_ini_safe, 'path': path})
556 559
557 560
558 561 def database_info():
559 562 import rhodecode
560 563 from sqlalchemy.engine import url as engine_url
561 564 from rhodecode.model.meta import Base as sql_base, Session
562 565 from rhodecode.model.db import DbMigrateVersion
563 566
564 567 state = STATE_OK_DEFAULT
565 568
566 569 db_migrate = DbMigrateVersion.query().filter(
567 570 DbMigrateVersion.repository_id == 'rhodecode_db_migrations').one()
568 571
569 572 db_url_obj = engine_url.make_url(rhodecode.CONFIG['sqlalchemy.db1.url'])
570 573
571 574 try:
572 575 engine = sql_base.metadata.bind
573 576 db_server_info = engine.dialect._get_server_version_info(
574 577 Session.connection(bind=engine))
575 578 db_version = '.'.join(map(str, db_server_info))
576 579 except Exception:
577 580 log.exception('failed to fetch db version')
578 581 db_version = 'UNKNOWN'
579 582
580 583 db_info = dict(
581 584 migrate_version=db_migrate.version,
582 585 type=db_url_obj.get_backend_name(),
583 586 version=db_version,
584 587 url=repr(db_url_obj)
585 588 )
586 589
587 590 human_value = db_info.copy()
588 591 human_value['url'] = "{} @ migration version: {}".format(
589 592 db_info['url'], db_info['migrate_version'])
590 593 human_value['version'] = "{} {}".format(db_info['type'], db_info['version'])
591 594 return SysInfoRes(value=db_info, state=state, human_value=human_value)
592 595
593 596
594 597 def server_info(environ):
595 598 import rhodecode
596 599 from rhodecode.lib.base import get_server_ip_addr, get_server_port
597 600
598 601 value = {
599 602 'server_ip': '%s:%s' % (
600 603 get_server_ip_addr(environ, log_errors=False),
601 604 get_server_port(environ)
602 605 ),
603 606 'server_id': rhodecode.CONFIG.get('instance_id'),
604 607 }
605 608 return SysInfoRes(value=value)
606 609
607 610
608 611 def get_system_info(environ):
609 612 environ = environ or {}
610 613 return {
611 614 'rhodecode_app': SysInfo(rhodecode_app_info)(),
612 615 'rhodecode_config': SysInfo(rhodecode_config)(),
613 616 'python': SysInfo(python_info)(),
614 617 'py_modules': SysInfo(py_modules)(),
615 618
616 619 'platform': SysInfo(platform_type)(),
617 620 'server': SysInfo(server_info, environ=environ)(),
618 621 'database': SysInfo(database_info)(),
619 622
620 623 'storage': SysInfo(storage)(),
621 624 'storage_inodes': SysInfo(storage_inodes)(),
622 625 'storage_archive': SysInfo(storage_archives)(),
623 626 'storage_gist': SysInfo(storage_gist)(),
624 627 'storage_temp': SysInfo(storage_temp)(),
625 628
626 629 'search': SysInfo(search_info)(),
627 630
628 631 'uptime': SysInfo(uptime)(),
629 632 'load': SysInfo(machine_load)(),
630 633 'cpu': SysInfo(cpu)(),
631 634 'memory': SysInfo(memory)(),
632 635
633 636 'vcs_backends': SysInfo(vcs_backends)(),
634 637 'vcs_server': SysInfo(vcs_server)(),
635 638
636 639 'git': SysInfo(git_info)(),
637 640 'hg': SysInfo(hg_info)(),
638 641 'svn': SysInfo(svn_info)(),
639 642 }
@@ -1,3818 +1,3823 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 44 from beaker.cache import cache_region
45 45 from webob.exc import HTTPNotFound
46 46 from zope.cachedescriptors.property import Lazy as LazyProperty
47 47
48 48 from pylons import url
49 49 from pylons.i18n.translation import lazy_ugettext as _
50 50
51 51 from rhodecode.lib.vcs import get_vcs_instance
52 52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 53 from rhodecode.lib.utils2 import (
54 54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 56 glob2re, StrictAttributeDict)
57 57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 58 from rhodecode.lib.ext_json import json
59 59 from rhodecode.lib.caching_query import FromCache
60 60 from rhodecode.lib.encrypt import AESCipher
61 61
62 62 from rhodecode.model.meta import Base, Session
63 63
64 64 URL_SEP = '/'
65 65 log = logging.getLogger(__name__)
66 66
67 67 # =============================================================================
68 68 # BASE CLASSES
69 69 # =============================================================================
70 70
71 71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 72 # beaker.session.secret if first is not set.
73 73 # and initialized at environment.py
74 74 ENCRYPTION_KEY = None
75 75
76 76 # used to sort permissions by types, '#' used here is not allowed to be in
77 77 # usernames, and it's very early in sorted string.printable table.
78 78 PERMISSION_TYPE_SORT = {
79 79 'admin': '####',
80 80 'write': '###',
81 81 'read': '##',
82 82 'none': '#',
83 83 }
84 84
85 85
86 86 def display_sort(obj):
87 87 """
88 88 Sort function used to sort permissions in .permissions() function of
89 89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 90 of all other resources
91 91 """
92 92
93 93 if obj.username == User.DEFAULT_USER:
94 94 return '#####'
95 95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 96 return prefix + obj.username
97 97
98 98
99 99 def _hash_key(k):
100 100 return md5_safe(k)
101 101
102 102
103 103 class EncryptedTextValue(TypeDecorator):
104 104 """
105 105 Special column for encrypted long text data, use like::
106 106
107 107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108 108
109 109 This column is intelligent so if value is in unencrypted form it return
110 110 unencrypted form, but on save it always encrypts
111 111 """
112 112 impl = Text
113 113
114 114 def process_bind_param(self, value, dialect):
115 115 if not value:
116 116 return value
117 117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 118 # protect against double encrypting if someone manually starts
119 119 # doing
120 120 raise ValueError('value needs to be in unencrypted format, ie. '
121 121 'not starting with enc$aes')
122 122 return 'enc$aes_hmac$%s' % AESCipher(
123 123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124 124
125 125 def process_result_value(self, value, dialect):
126 126 import rhodecode
127 127
128 128 if not value:
129 129 return value
130 130
131 131 parts = value.split('$', 3)
132 132 if not len(parts) == 3:
133 133 # probably not encrypted values
134 134 return value
135 135 else:
136 136 if parts[0] != 'enc':
137 137 # parts ok but without our header ?
138 138 return value
139 139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 140 'rhodecode.encrypted_values.strict') or True)
141 141 # at that stage we know it's our encryption
142 142 if parts[1] == 'aes':
143 143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 144 elif parts[1] == 'aes_hmac':
145 145 decrypted_data = AESCipher(
146 146 ENCRYPTION_KEY, hmac=True,
147 147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 148 else:
149 149 raise ValueError(
150 150 'Encryption type part is wrong, must be `aes` '
151 151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 152 return decrypted_data
153 153
154 154
155 155 class BaseModel(object):
156 156 """
157 157 Base Model for all classes
158 158 """
159 159
160 160 @classmethod
161 161 def _get_keys(cls):
162 162 """return column names for this model """
163 163 return class_mapper(cls).c.keys()
164 164
165 165 def get_dict(self):
166 166 """
167 167 return dict with keys and values corresponding
168 168 to this model data """
169 169
170 170 d = {}
171 171 for k in self._get_keys():
172 172 d[k] = getattr(self, k)
173 173
174 174 # also use __json__() if present to get additional fields
175 175 _json_attr = getattr(self, '__json__', None)
176 176 if _json_attr:
177 177 # update with attributes from __json__
178 178 if callable(_json_attr):
179 179 _json_attr = _json_attr()
180 180 for k, val in _json_attr.iteritems():
181 181 d[k] = val
182 182 return d
183 183
184 184 def get_appstruct(self):
185 185 """return list with keys and values tuples corresponding
186 186 to this model data """
187 187
188 188 l = []
189 189 for k in self._get_keys():
190 190 l.append((k, getattr(self, k),))
191 191 return l
192 192
193 193 def populate_obj(self, populate_dict):
194 194 """populate model with data from given populate_dict"""
195 195
196 196 for k in self._get_keys():
197 197 if k in populate_dict:
198 198 setattr(self, k, populate_dict[k])
199 199
200 200 @classmethod
201 201 def query(cls):
202 202 return Session().query(cls)
203 203
204 204 @classmethod
205 205 def get(cls, id_):
206 206 if id_:
207 207 return cls.query().get(id_)
208 208
209 209 @classmethod
210 210 def get_or_404(cls, id_):
211 211 try:
212 212 id_ = int(id_)
213 213 except (TypeError, ValueError):
214 214 raise HTTPNotFound
215 215
216 216 res = cls.query().get(id_)
217 217 if not res:
218 218 raise HTTPNotFound
219 219 return res
220 220
221 221 @classmethod
222 222 def getAll(cls):
223 223 # deprecated and left for backward compatibility
224 224 return cls.get_all()
225 225
226 226 @classmethod
227 227 def get_all(cls):
228 228 return cls.query().all()
229 229
230 230 @classmethod
231 231 def delete(cls, id_):
232 232 obj = cls.query().get(id_)
233 233 Session().delete(obj)
234 234
235 235 @classmethod
236 236 def identity_cache(cls, session, attr_name, value):
237 237 exist_in_session = []
238 238 for (item_cls, pkey), instance in session.identity_map.items():
239 239 if cls == item_cls and getattr(instance, attr_name) == value:
240 240 exist_in_session.append(instance)
241 241 if exist_in_session:
242 242 if len(exist_in_session) == 1:
243 243 return exist_in_session[0]
244 244 log.exception(
245 245 'multiple objects with attr %s and '
246 246 'value %s found with same name: %r',
247 247 attr_name, value, exist_in_session)
248 248
249 249 def __repr__(self):
250 250 if hasattr(self, '__unicode__'):
251 251 # python repr needs to return str
252 252 try:
253 253 return safe_str(self.__unicode__())
254 254 except UnicodeDecodeError:
255 255 pass
256 256 return '<DB:%s>' % (self.__class__.__name__)
257 257
258 258
259 259 class RhodeCodeSetting(Base, BaseModel):
260 260 __tablename__ = 'rhodecode_settings'
261 261 __table_args__ = (
262 262 UniqueConstraint('app_settings_name'),
263 263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 265 )
266 266
267 267 SETTINGS_TYPES = {
268 268 'str': safe_str,
269 269 'int': safe_int,
270 270 'unicode': safe_unicode,
271 271 'bool': str2bool,
272 272 'list': functools.partial(aslist, sep=',')
273 273 }
274 274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 275 GLOBAL_CONF_KEY = 'app_settings'
276 276
277 277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281 281
282 282 def __init__(self, key='', val='', type='unicode'):
283 283 self.app_settings_name = key
284 284 self.app_settings_type = type
285 285 self.app_settings_value = val
286 286
287 287 @validates('_app_settings_value')
288 288 def validate_settings_value(self, key, val):
289 289 assert type(val) == unicode
290 290 return val
291 291
292 292 @hybrid_property
293 293 def app_settings_value(self):
294 294 v = self._app_settings_value
295 295 _type = self.app_settings_type
296 296 if _type:
297 297 _type = self.app_settings_type.split('.')[0]
298 298 # decode the encrypted value
299 299 if 'encrypted' in self.app_settings_type:
300 300 cipher = EncryptedTextValue()
301 301 v = safe_unicode(cipher.process_result_value(v, None))
302 302
303 303 converter = self.SETTINGS_TYPES.get(_type) or \
304 304 self.SETTINGS_TYPES['unicode']
305 305 return converter(v)
306 306
307 307 @app_settings_value.setter
308 308 def app_settings_value(self, val):
309 309 """
310 310 Setter that will always make sure we use unicode in app_settings_value
311 311
312 312 :param val:
313 313 """
314 314 val = safe_unicode(val)
315 315 # encode the encrypted value
316 316 if 'encrypted' in self.app_settings_type:
317 317 cipher = EncryptedTextValue()
318 318 val = safe_unicode(cipher.process_bind_param(val, None))
319 319 self._app_settings_value = val
320 320
321 321 @hybrid_property
322 322 def app_settings_type(self):
323 323 return self._app_settings_type
324 324
325 325 @app_settings_type.setter
326 326 def app_settings_type(self, val):
327 327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 328 raise Exception('type must be one of %s got %s'
329 329 % (self.SETTINGS_TYPES.keys(), val))
330 330 self._app_settings_type = val
331 331
332 332 def __unicode__(self):
333 333 return u"<%s('%s:%s[%s]')>" % (
334 334 self.__class__.__name__,
335 335 self.app_settings_name, self.app_settings_value,
336 336 self.app_settings_type
337 337 )
338 338
339 339
340 340 class RhodeCodeUi(Base, BaseModel):
341 341 __tablename__ = 'rhodecode_ui'
342 342 __table_args__ = (
343 343 UniqueConstraint('ui_key'),
344 344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 346 )
347 347
348 348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 349 # HG
350 350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 351 HOOK_PULL = 'outgoing.pull_logger'
352 352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 353 HOOK_PUSH = 'changegroup.push_logger'
354 354
355 355 # TODO: johbo: Unify way how hooks are configured for git and hg,
356 356 # git part is currently hardcoded.
357 357
358 358 # SVN PATTERNS
359 359 SVN_BRANCH_ID = 'vcs_svn_branch'
360 360 SVN_TAG_ID = 'vcs_svn_tag'
361 361
362 362 ui_id = Column(
363 363 "ui_id", Integer(), nullable=False, unique=True, default=None,
364 364 primary_key=True)
365 365 ui_section = Column(
366 366 "ui_section", String(255), nullable=True, unique=None, default=None)
367 367 ui_key = Column(
368 368 "ui_key", String(255), nullable=True, unique=None, default=None)
369 369 ui_value = Column(
370 370 "ui_value", String(255), nullable=True, unique=None, default=None)
371 371 ui_active = Column(
372 372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
373 373
374 374 def __repr__(self):
375 375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
376 376 self.ui_key, self.ui_value)
377 377
378 378
379 379 class RepoRhodeCodeSetting(Base, BaseModel):
380 380 __tablename__ = 'repo_rhodecode_settings'
381 381 __table_args__ = (
382 382 UniqueConstraint(
383 383 'app_settings_name', 'repository_id',
384 384 name='uq_repo_rhodecode_setting_name_repo_id'),
385 385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
386 386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
387 387 )
388 388
389 389 repository_id = Column(
390 390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
391 391 nullable=False)
392 392 app_settings_id = Column(
393 393 "app_settings_id", Integer(), nullable=False, unique=True,
394 394 default=None, primary_key=True)
395 395 app_settings_name = Column(
396 396 "app_settings_name", String(255), nullable=True, unique=None,
397 397 default=None)
398 398 _app_settings_value = Column(
399 399 "app_settings_value", String(4096), nullable=True, unique=None,
400 400 default=None)
401 401 _app_settings_type = Column(
402 402 "app_settings_type", String(255), nullable=True, unique=None,
403 403 default=None)
404 404
405 405 repository = relationship('Repository')
406 406
407 407 def __init__(self, repository_id, key='', val='', type='unicode'):
408 408 self.repository_id = repository_id
409 409 self.app_settings_name = key
410 410 self.app_settings_type = type
411 411 self.app_settings_value = val
412 412
413 413 @validates('_app_settings_value')
414 414 def validate_settings_value(self, key, val):
415 415 assert type(val) == unicode
416 416 return val
417 417
418 418 @hybrid_property
419 419 def app_settings_value(self):
420 420 v = self._app_settings_value
421 421 type_ = self.app_settings_type
422 422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
423 423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
424 424 return converter(v)
425 425
426 426 @app_settings_value.setter
427 427 def app_settings_value(self, val):
428 428 """
429 429 Setter that will always make sure we use unicode in app_settings_value
430 430
431 431 :param val:
432 432 """
433 433 self._app_settings_value = safe_unicode(val)
434 434
435 435 @hybrid_property
436 436 def app_settings_type(self):
437 437 return self._app_settings_type
438 438
439 439 @app_settings_type.setter
440 440 def app_settings_type(self, val):
441 441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
442 442 if val not in SETTINGS_TYPES:
443 443 raise Exception('type must be one of %s got %s'
444 444 % (SETTINGS_TYPES.keys(), val))
445 445 self._app_settings_type = val
446 446
447 447 def __unicode__(self):
448 448 return u"<%s('%s:%s:%s[%s]')>" % (
449 449 self.__class__.__name__, self.repository.repo_name,
450 450 self.app_settings_name, self.app_settings_value,
451 451 self.app_settings_type
452 452 )
453 453
454 454
455 455 class RepoRhodeCodeUi(Base, BaseModel):
456 456 __tablename__ = 'repo_rhodecode_ui'
457 457 __table_args__ = (
458 458 UniqueConstraint(
459 459 'repository_id', 'ui_section', 'ui_key',
460 460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
461 461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
462 462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
463 463 )
464 464
465 465 repository_id = Column(
466 466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 467 nullable=False)
468 468 ui_id = Column(
469 469 "ui_id", Integer(), nullable=False, unique=True, default=None,
470 470 primary_key=True)
471 471 ui_section = Column(
472 472 "ui_section", String(255), nullable=True, unique=None, default=None)
473 473 ui_key = Column(
474 474 "ui_key", String(255), nullable=True, unique=None, default=None)
475 475 ui_value = Column(
476 476 "ui_value", String(255), nullable=True, unique=None, default=None)
477 477 ui_active = Column(
478 478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
479 479
480 480 repository = relationship('Repository')
481 481
482 482 def __repr__(self):
483 483 return '<%s[%s:%s]%s=>%s]>' % (
484 484 self.__class__.__name__, self.repository.repo_name,
485 485 self.ui_section, self.ui_key, self.ui_value)
486 486
487 487
488 488 class User(Base, BaseModel):
489 489 __tablename__ = 'users'
490 490 __table_args__ = (
491 491 UniqueConstraint('username'), UniqueConstraint('email'),
492 492 Index('u_username_idx', 'username'),
493 493 Index('u_email_idx', 'email'),
494 494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
495 495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
496 496 )
497 497 DEFAULT_USER = 'default'
498 498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
499 499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
500 500
501 501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
502 502 username = Column("username", String(255), nullable=True, unique=None, default=None)
503 503 password = Column("password", String(255), nullable=True, unique=None, default=None)
504 504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
505 505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
506 506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
507 507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
508 508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
509 509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
510 510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
511 511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
512 512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
513 513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
514 514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
515 515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
516 516
517 517 user_log = relationship('UserLog')
518 518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
519 519
520 520 repositories = relationship('Repository')
521 521 repository_groups = relationship('RepoGroup')
522 522 user_groups = relationship('UserGroup')
523 523
524 524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
525 525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
526 526
527 527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
528 528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
529 529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
530 530
531 531 group_member = relationship('UserGroupMember', cascade='all')
532 532
533 533 notifications = relationship('UserNotification', cascade='all')
534 534 # notifications assigned to this user
535 535 user_created_notifications = relationship('Notification', cascade='all')
536 536 # comments created by this user
537 537 user_comments = relationship('ChangesetComment', cascade='all')
538 538 # user profile extra info
539 539 user_emails = relationship('UserEmailMap', cascade='all')
540 540 user_ip_map = relationship('UserIpMap', cascade='all')
541 541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
542 542 # gists
543 543 user_gists = relationship('Gist', cascade='all')
544 544 # user pull requests
545 545 user_pull_requests = relationship('PullRequest', cascade='all')
546 546 # external identities
547 547 extenal_identities = relationship(
548 548 'ExternalIdentity',
549 549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
550 550 cascade='all')
551 551
552 552 def __unicode__(self):
553 553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
554 554 self.user_id, self.username)
555 555
556 556 @hybrid_property
557 557 def email(self):
558 558 return self._email
559 559
560 560 @email.setter
561 561 def email(self, val):
562 562 self._email = val.lower() if val else None
563 563
564 564 @property
565 565 def firstname(self):
566 566 # alias for future
567 567 return self.name
568 568
569 569 @property
570 570 def emails(self):
571 571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
572 572 return [self.email] + [x.email for x in other]
573 573
574 574 @property
575 575 def auth_tokens(self):
576 576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
577 577
578 578 @property
579 579 def extra_auth_tokens(self):
580 580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
581 581
582 582 @property
583 583 def feed_token(self):
584 584 feed_tokens = UserApiKeys.query()\
585 585 .filter(UserApiKeys.user == self)\
586 586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
587 587 .all()
588 588 if feed_tokens:
589 589 return feed_tokens[0].api_key
590 590 else:
591 591 # use the main token so we don't end up with nothing...
592 592 return self.api_key
593 593
594 594 @classmethod
595 595 def extra_valid_auth_tokens(cls, user, role=None):
596 596 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
597 597 .filter(or_(UserApiKeys.expires == -1,
598 598 UserApiKeys.expires >= time.time()))
599 599 if role:
600 600 tokens = tokens.filter(or_(UserApiKeys.role == role,
601 601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
602 602 return tokens.all()
603 603
604 604 @property
605 605 def builtin_token_roles(self):
606 606 return map(UserApiKeys._get_role_name, [
607 607 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
608 608 ])
609 609
610 610 @property
611 611 def ip_addresses(self):
612 612 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
613 613 return [x.ip_addr for x in ret]
614 614
615 615 @property
616 616 def username_and_name(self):
617 617 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
618 618
619 619 @property
620 620 def username_or_name_or_email(self):
621 621 full_name = self.full_name if self.full_name is not ' ' else None
622 622 return self.username or full_name or self.email
623 623
624 624 @property
625 625 def full_name(self):
626 626 return '%s %s' % (self.firstname, self.lastname)
627 627
628 628 @property
629 629 def full_name_or_username(self):
630 630 return ('%s %s' % (self.firstname, self.lastname)
631 631 if (self.firstname and self.lastname) else self.username)
632 632
633 633 @property
634 634 def full_contact(self):
635 635 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
636 636
637 637 @property
638 638 def short_contact(self):
639 639 return '%s %s' % (self.firstname, self.lastname)
640 640
641 641 @property
642 642 def is_admin(self):
643 643 return self.admin
644 644
645 645 @property
646 646 def AuthUser(self):
647 647 """
648 648 Returns instance of AuthUser for this user
649 649 """
650 650 from rhodecode.lib.auth import AuthUser
651 651 return AuthUser(user_id=self.user_id, api_key=self.api_key,
652 652 username=self.username)
653 653
654 654 @hybrid_property
655 655 def user_data(self):
656 656 if not self._user_data:
657 657 return {}
658 658
659 659 try:
660 660 return json.loads(self._user_data)
661 661 except TypeError:
662 662 return {}
663 663
664 664 @user_data.setter
665 665 def user_data(self, val):
666 666 if not isinstance(val, dict):
667 667 raise Exception('user_data must be dict, got %s' % type(val))
668 668 try:
669 669 self._user_data = json.dumps(val)
670 670 except Exception:
671 671 log.error(traceback.format_exc())
672 672
673 673 @classmethod
674 674 def get_by_username(cls, username, case_insensitive=False,
675 675 cache=False, identity_cache=False):
676 676 session = Session()
677 677
678 678 if case_insensitive:
679 679 q = cls.query().filter(
680 680 func.lower(cls.username) == func.lower(username))
681 681 else:
682 682 q = cls.query().filter(cls.username == username)
683 683
684 684 if cache:
685 685 if identity_cache:
686 686 val = cls.identity_cache(session, 'username', username)
687 687 if val:
688 688 return val
689 689 else:
690 690 q = q.options(
691 691 FromCache("sql_cache_short",
692 692 "get_user_by_name_%s" % _hash_key(username)))
693 693
694 694 return q.scalar()
695 695
696 696 @classmethod
697 697 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
698 698 q = cls.query().filter(cls.api_key == auth_token)
699 699
700 700 if cache:
701 701 q = q.options(FromCache("sql_cache_short",
702 702 "get_auth_token_%s" % auth_token))
703 703 res = q.scalar()
704 704
705 705 if fallback and not res:
706 706 #fallback to additional keys
707 707 _res = UserApiKeys.query()\
708 708 .filter(UserApiKeys.api_key == auth_token)\
709 709 .filter(or_(UserApiKeys.expires == -1,
710 710 UserApiKeys.expires >= time.time()))\
711 711 .first()
712 712 if _res:
713 713 res = _res.user
714 714 return res
715 715
716 716 @classmethod
717 717 def get_by_email(cls, email, case_insensitive=False, cache=False):
718 718
719 719 if case_insensitive:
720 720 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
721 721
722 722 else:
723 723 q = cls.query().filter(cls.email == email)
724 724
725 725 if cache:
726 726 q = q.options(FromCache("sql_cache_short",
727 727 "get_email_key_%s" % _hash_key(email)))
728 728
729 729 ret = q.scalar()
730 730 if ret is None:
731 731 q = UserEmailMap.query()
732 732 # try fetching in alternate email map
733 733 if case_insensitive:
734 734 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
735 735 else:
736 736 q = q.filter(UserEmailMap.email == email)
737 737 q = q.options(joinedload(UserEmailMap.user))
738 738 if cache:
739 739 q = q.options(FromCache("sql_cache_short",
740 740 "get_email_map_key_%s" % email))
741 741 ret = getattr(q.scalar(), 'user', None)
742 742
743 743 return ret
744 744
745 745 @classmethod
746 746 def get_from_cs_author(cls, author):
747 747 """
748 748 Tries to get User objects out of commit author string
749 749
750 750 :param author:
751 751 """
752 752 from rhodecode.lib.helpers import email, author_name
753 753 # Valid email in the attribute passed, see if they're in the system
754 754 _email = email(author)
755 755 if _email:
756 756 user = cls.get_by_email(_email, case_insensitive=True)
757 757 if user:
758 758 return user
759 759 # Maybe we can match by username?
760 760 _author = author_name(author)
761 761 user = cls.get_by_username(_author, case_insensitive=True)
762 762 if user:
763 763 return user
764 764
765 765 def update_userdata(self, **kwargs):
766 766 usr = self
767 767 old = usr.user_data
768 768 old.update(**kwargs)
769 769 usr.user_data = old
770 770 Session().add(usr)
771 771 log.debug('updated userdata with ', kwargs)
772 772
773 773 def update_lastlogin(self):
774 774 """Update user lastlogin"""
775 775 self.last_login = datetime.datetime.now()
776 776 Session().add(self)
777 777 log.debug('updated user %s lastlogin', self.username)
778 778
779 779 def update_lastactivity(self):
780 780 """Update user lastactivity"""
781 781 usr = self
782 782 old = usr.user_data
783 783 old.update({'last_activity': time.time()})
784 784 usr.user_data = old
785 785 Session().add(usr)
786 786 log.debug('updated user %s lastactivity', usr.username)
787 787
788 788 def update_password(self, new_password, change_api_key=False):
789 789 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
790 790
791 791 self.password = get_crypt_password(new_password)
792 792 if change_api_key:
793 793 self.api_key = generate_auth_token(self.username)
794 794 Session().add(self)
795 795
796 796 @classmethod
797 797 def get_first_super_admin(cls):
798 798 user = User.query().filter(User.admin == true()).first()
799 799 if user is None:
800 800 raise Exception('FATAL: Missing administrative account!')
801 801 return user
802 802
803 803 @classmethod
804 804 def get_all_super_admins(cls):
805 805 """
806 806 Returns all admin accounts sorted by username
807 807 """
808 808 return User.query().filter(User.admin == true())\
809 809 .order_by(User.username.asc()).all()
810 810
811 811 @classmethod
812 812 def get_default_user(cls, cache=False):
813 813 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
814 814 if user is None:
815 815 raise Exception('FATAL: Missing default account!')
816 816 return user
817 817
818 818 def _get_default_perms(self, user, suffix=''):
819 819 from rhodecode.model.permission import PermissionModel
820 820 return PermissionModel().get_default_perms(user.user_perms, suffix)
821 821
822 822 def get_default_perms(self, suffix=''):
823 823 return self._get_default_perms(self, suffix)
824 824
825 825 def get_api_data(self, include_secrets=False, details='full'):
826 826 """
827 827 Common function for generating user related data for API
828 828
829 829 :param include_secrets: By default secrets in the API data will be replaced
830 830 by a placeholder value to prevent exposing this data by accident. In case
831 831 this data shall be exposed, set this flag to ``True``.
832 832
833 833 :param details: details can be 'basic|full' basic gives only a subset of
834 834 the available user information that includes user_id, name and emails.
835 835 """
836 836 user = self
837 837 user_data = self.user_data
838 838 data = {
839 839 'user_id': user.user_id,
840 840 'username': user.username,
841 841 'firstname': user.name,
842 842 'lastname': user.lastname,
843 843 'email': user.email,
844 844 'emails': user.emails,
845 845 }
846 846 if details == 'basic':
847 847 return data
848 848
849 849 api_key_length = 40
850 850 api_key_replacement = '*' * api_key_length
851 851
852 852 extras = {
853 853 'api_key': api_key_replacement,
854 854 'api_keys': [api_key_replacement],
855 855 'active': user.active,
856 856 'admin': user.admin,
857 857 'extern_type': user.extern_type,
858 858 'extern_name': user.extern_name,
859 859 'last_login': user.last_login,
860 860 'ip_addresses': user.ip_addresses,
861 861 'language': user_data.get('language')
862 862 }
863 863 data.update(extras)
864 864
865 865 if include_secrets:
866 866 data['api_key'] = user.api_key
867 867 data['api_keys'] = user.auth_tokens
868 868 return data
869 869
870 870 def __json__(self):
871 871 data = {
872 872 'full_name': self.full_name,
873 873 'full_name_or_username': self.full_name_or_username,
874 874 'short_contact': self.short_contact,
875 875 'full_contact': self.full_contact,
876 876 }
877 877 data.update(self.get_api_data())
878 878 return data
879 879
880 880
881 881 class UserApiKeys(Base, BaseModel):
882 882 __tablename__ = 'user_api_keys'
883 883 __table_args__ = (
884 884 Index('uak_api_key_idx', 'api_key'),
885 885 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
886 886 UniqueConstraint('api_key'),
887 887 {'extend_existing': True, 'mysql_engine': 'InnoDB',
888 888 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
889 889 )
890 890 __mapper_args__ = {}
891 891
892 892 # ApiKey role
893 893 ROLE_ALL = 'token_role_all'
894 894 ROLE_HTTP = 'token_role_http'
895 895 ROLE_VCS = 'token_role_vcs'
896 896 ROLE_API = 'token_role_api'
897 897 ROLE_FEED = 'token_role_feed'
898 898 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
899 899
900 900 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
901 901 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
902 902 api_key = Column("api_key", String(255), nullable=False, unique=True)
903 903 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
904 904 expires = Column('expires', Float(53), nullable=False)
905 905 role = Column('role', String(255), nullable=True)
906 906 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
907 907
908 908 user = relationship('User', lazy='joined')
909 909
910 910 @classmethod
911 911 def _get_role_name(cls, role):
912 912 return {
913 913 cls.ROLE_ALL: _('all'),
914 914 cls.ROLE_HTTP: _('http/web interface'),
915 915 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
916 916 cls.ROLE_API: _('api calls'),
917 917 cls.ROLE_FEED: _('feed access'),
918 918 }.get(role, role)
919 919
920 920 @property
921 921 def expired(self):
922 922 if self.expires == -1:
923 923 return False
924 924 return time.time() > self.expires
925 925
926 926 @property
927 927 def role_humanized(self):
928 928 return self._get_role_name(self.role)
929 929
930 930
931 931 class UserEmailMap(Base, BaseModel):
932 932 __tablename__ = 'user_email_map'
933 933 __table_args__ = (
934 934 Index('uem_email_idx', 'email'),
935 935 UniqueConstraint('email'),
936 936 {'extend_existing': True, 'mysql_engine': 'InnoDB',
937 937 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
938 938 )
939 939 __mapper_args__ = {}
940 940
941 941 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
942 942 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
943 943 _email = Column("email", String(255), nullable=True, unique=False, default=None)
944 944 user = relationship('User', lazy='joined')
945 945
946 946 @validates('_email')
947 947 def validate_email(self, key, email):
948 948 # check if this email is not main one
949 949 main_email = Session().query(User).filter(User.email == email).scalar()
950 950 if main_email is not None:
951 951 raise AttributeError('email %s is present is user table' % email)
952 952 return email
953 953
954 954 @hybrid_property
955 955 def email(self):
956 956 return self._email
957 957
958 958 @email.setter
959 959 def email(self, val):
960 960 self._email = val.lower() if val else None
961 961
962 962
963 963 class UserIpMap(Base, BaseModel):
964 964 __tablename__ = 'user_ip_map'
965 965 __table_args__ = (
966 966 UniqueConstraint('user_id', 'ip_addr'),
967 967 {'extend_existing': True, 'mysql_engine': 'InnoDB',
968 968 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
969 969 )
970 970 __mapper_args__ = {}
971 971
972 972 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
973 973 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
974 974 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
975 975 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
976 976 description = Column("description", String(10000), nullable=True, unique=None, default=None)
977 977 user = relationship('User', lazy='joined')
978 978
979 979 @classmethod
980 980 def _get_ip_range(cls, ip_addr):
981 981 net = ipaddress.ip_network(ip_addr, strict=False)
982 982 return [str(net.network_address), str(net.broadcast_address)]
983 983
984 984 def __json__(self):
985 985 return {
986 986 'ip_addr': self.ip_addr,
987 987 'ip_range': self._get_ip_range(self.ip_addr),
988 988 }
989 989
990 990 def __unicode__(self):
991 991 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
992 992 self.user_id, self.ip_addr)
993 993
994 994 class UserLog(Base, BaseModel):
995 995 __tablename__ = 'user_logs'
996 996 __table_args__ = (
997 997 {'extend_existing': True, 'mysql_engine': 'InnoDB',
998 998 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
999 999 )
1000 1000 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1001 1001 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1002 1002 username = Column("username", String(255), nullable=True, unique=None, default=None)
1003 1003 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1004 1004 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1005 1005 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1006 1006 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1007 1007 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1008 1008
1009 1009 def __unicode__(self):
1010 1010 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1011 1011 self.repository_name,
1012 1012 self.action)
1013 1013
1014 1014 @property
1015 1015 def action_as_day(self):
1016 1016 return datetime.date(*self.action_date.timetuple()[:3])
1017 1017
1018 1018 user = relationship('User')
1019 1019 repository = relationship('Repository', cascade='')
1020 1020
1021 1021
1022 1022 class UserGroup(Base, BaseModel):
1023 1023 __tablename__ = 'users_groups'
1024 1024 __table_args__ = (
1025 1025 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1026 1026 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1027 1027 )
1028 1028
1029 1029 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1030 1030 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1031 1031 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1032 1032 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1033 1033 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1034 1034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1035 1035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1036 1036 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1037 1037
1038 1038 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1039 1039 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1040 1040 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1041 1041 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1042 1042 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1043 1043 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1044 1044
1045 1045 user = relationship('User')
1046 1046
1047 1047 @hybrid_property
1048 1048 def group_data(self):
1049 1049 if not self._group_data:
1050 1050 return {}
1051 1051
1052 1052 try:
1053 1053 return json.loads(self._group_data)
1054 1054 except TypeError:
1055 1055 return {}
1056 1056
1057 1057 @group_data.setter
1058 1058 def group_data(self, val):
1059 1059 try:
1060 1060 self._group_data = json.dumps(val)
1061 1061 except Exception:
1062 1062 log.error(traceback.format_exc())
1063 1063
1064 1064 def __unicode__(self):
1065 1065 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1066 1066 self.users_group_id,
1067 1067 self.users_group_name)
1068 1068
1069 1069 @classmethod
1070 1070 def get_by_group_name(cls, group_name, cache=False,
1071 1071 case_insensitive=False):
1072 1072 if case_insensitive:
1073 1073 q = cls.query().filter(func.lower(cls.users_group_name) ==
1074 1074 func.lower(group_name))
1075 1075
1076 1076 else:
1077 1077 q = cls.query().filter(cls.users_group_name == group_name)
1078 1078 if cache:
1079 1079 q = q.options(FromCache(
1080 1080 "sql_cache_short",
1081 1081 "get_group_%s" % _hash_key(group_name)))
1082 1082 return q.scalar()
1083 1083
1084 1084 @classmethod
1085 1085 def get(cls, user_group_id, cache=False):
1086 1086 user_group = cls.query()
1087 1087 if cache:
1088 1088 user_group = user_group.options(FromCache("sql_cache_short",
1089 1089 "get_users_group_%s" % user_group_id))
1090 1090 return user_group.get(user_group_id)
1091 1091
1092 1092 def permissions(self, with_admins=True, with_owner=True):
1093 1093 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1094 1094 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1095 1095 joinedload(UserUserGroupToPerm.user),
1096 1096 joinedload(UserUserGroupToPerm.permission),)
1097 1097
1098 1098 # get owners and admins and permissions. We do a trick of re-writing
1099 1099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1100 1100 # has a global reference and changing one object propagates to all
1101 1101 # others. This means if admin is also an owner admin_row that change
1102 1102 # would propagate to both objects
1103 1103 perm_rows = []
1104 1104 for _usr in q.all():
1105 1105 usr = AttributeDict(_usr.user.get_dict())
1106 1106 usr.permission = _usr.permission.permission_name
1107 1107 perm_rows.append(usr)
1108 1108
1109 1109 # filter the perm rows by 'default' first and then sort them by
1110 1110 # admin,write,read,none permissions sorted again alphabetically in
1111 1111 # each group
1112 1112 perm_rows = sorted(perm_rows, key=display_sort)
1113 1113
1114 1114 _admin_perm = 'usergroup.admin'
1115 1115 owner_row = []
1116 1116 if with_owner:
1117 1117 usr = AttributeDict(self.user.get_dict())
1118 1118 usr.owner_row = True
1119 1119 usr.permission = _admin_perm
1120 1120 owner_row.append(usr)
1121 1121
1122 1122 super_admin_rows = []
1123 1123 if with_admins:
1124 1124 for usr in User.get_all_super_admins():
1125 1125 # if this admin is also owner, don't double the record
1126 1126 if usr.user_id == owner_row[0].user_id:
1127 1127 owner_row[0].admin_row = True
1128 1128 else:
1129 1129 usr = AttributeDict(usr.get_dict())
1130 1130 usr.admin_row = True
1131 1131 usr.permission = _admin_perm
1132 1132 super_admin_rows.append(usr)
1133 1133
1134 1134 return super_admin_rows + owner_row + perm_rows
1135 1135
1136 1136 def permission_user_groups(self):
1137 1137 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1138 1138 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1139 1139 joinedload(UserGroupUserGroupToPerm.target_user_group),
1140 1140 joinedload(UserGroupUserGroupToPerm.permission),)
1141 1141
1142 1142 perm_rows = []
1143 1143 for _user_group in q.all():
1144 1144 usr = AttributeDict(_user_group.user_group.get_dict())
1145 1145 usr.permission = _user_group.permission.permission_name
1146 1146 perm_rows.append(usr)
1147 1147
1148 1148 return perm_rows
1149 1149
1150 1150 def _get_default_perms(self, user_group, suffix=''):
1151 1151 from rhodecode.model.permission import PermissionModel
1152 1152 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1153 1153
1154 1154 def get_default_perms(self, suffix=''):
1155 1155 return self._get_default_perms(self, suffix)
1156 1156
1157 1157 def get_api_data(self, with_group_members=True, include_secrets=False):
1158 1158 """
1159 1159 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1160 1160 basically forwarded.
1161 1161
1162 1162 """
1163 1163 user_group = self
1164 1164
1165 1165 data = {
1166 1166 'users_group_id': user_group.users_group_id,
1167 1167 'group_name': user_group.users_group_name,
1168 1168 'group_description': user_group.user_group_description,
1169 1169 'active': user_group.users_group_active,
1170 1170 'owner': user_group.user.username,
1171 1171 }
1172 1172 if with_group_members:
1173 1173 users = []
1174 1174 for user in user_group.members:
1175 1175 user = user.user
1176 1176 users.append(user.get_api_data(include_secrets=include_secrets))
1177 1177 data['users'] = users
1178 1178
1179 1179 return data
1180 1180
1181 1181
1182 1182 class UserGroupMember(Base, BaseModel):
1183 1183 __tablename__ = 'users_groups_members'
1184 1184 __table_args__ = (
1185 1185 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1186 1186 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1187 1187 )
1188 1188
1189 1189 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1190 1190 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1191 1191 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1192 1192
1193 1193 user = relationship('User', lazy='joined')
1194 1194 users_group = relationship('UserGroup')
1195 1195
1196 1196 def __init__(self, gr_id='', u_id=''):
1197 1197 self.users_group_id = gr_id
1198 1198 self.user_id = u_id
1199 1199
1200 1200
1201 1201 class RepositoryField(Base, BaseModel):
1202 1202 __tablename__ = 'repositories_fields'
1203 1203 __table_args__ = (
1204 1204 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1205 1205 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1206 1206 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1207 1207 )
1208 1208 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1209 1209
1210 1210 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1211 1211 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1212 1212 field_key = Column("field_key", String(250))
1213 1213 field_label = Column("field_label", String(1024), nullable=False)
1214 1214 field_value = Column("field_value", String(10000), nullable=False)
1215 1215 field_desc = Column("field_desc", String(1024), nullable=False)
1216 1216 field_type = Column("field_type", String(255), nullable=False, unique=None)
1217 1217 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1218 1218
1219 1219 repository = relationship('Repository')
1220 1220
1221 1221 @property
1222 1222 def field_key_prefixed(self):
1223 1223 return 'ex_%s' % self.field_key
1224 1224
1225 1225 @classmethod
1226 1226 def un_prefix_key(cls, key):
1227 1227 if key.startswith(cls.PREFIX):
1228 1228 return key[len(cls.PREFIX):]
1229 1229 return key
1230 1230
1231 1231 @classmethod
1232 1232 def get_by_key_name(cls, key, repo):
1233 1233 row = cls.query()\
1234 1234 .filter(cls.repository == repo)\
1235 1235 .filter(cls.field_key == key).scalar()
1236 1236 return row
1237 1237
1238 1238
1239 1239 class Repository(Base, BaseModel):
1240 1240 __tablename__ = 'repositories'
1241 1241 __table_args__ = (
1242 1242 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1243 1243 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1244 1244 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1245 1245 )
1246 1246 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1247 1247 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1248 1248
1249 1249 STATE_CREATED = 'repo_state_created'
1250 1250 STATE_PENDING = 'repo_state_pending'
1251 1251 STATE_ERROR = 'repo_state_error'
1252 1252
1253 1253 LOCK_AUTOMATIC = 'lock_auto'
1254 1254 LOCK_API = 'lock_api'
1255 1255 LOCK_WEB = 'lock_web'
1256 1256 LOCK_PULL = 'lock_pull'
1257 1257
1258 1258 NAME_SEP = URL_SEP
1259 1259
1260 1260 repo_id = Column(
1261 1261 "repo_id", Integer(), nullable=False, unique=True, default=None,
1262 1262 primary_key=True)
1263 1263 _repo_name = Column(
1264 1264 "repo_name", Text(), nullable=False, default=None)
1265 1265 _repo_name_hash = Column(
1266 1266 "repo_name_hash", String(255), nullable=False, unique=True)
1267 1267 repo_state = Column("repo_state", String(255), nullable=True)
1268 1268
1269 1269 clone_uri = Column(
1270 1270 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1271 1271 default=None)
1272 1272 repo_type = Column(
1273 1273 "repo_type", String(255), nullable=False, unique=False, default=None)
1274 1274 user_id = Column(
1275 1275 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1276 1276 unique=False, default=None)
1277 1277 private = Column(
1278 1278 "private", Boolean(), nullable=True, unique=None, default=None)
1279 1279 enable_statistics = Column(
1280 1280 "statistics", Boolean(), nullable=True, unique=None, default=True)
1281 1281 enable_downloads = Column(
1282 1282 "downloads", Boolean(), nullable=True, unique=None, default=True)
1283 1283 description = Column(
1284 1284 "description", String(10000), nullable=True, unique=None, default=None)
1285 1285 created_on = Column(
1286 1286 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1287 1287 default=datetime.datetime.now)
1288 1288 updated_on = Column(
1289 1289 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1290 1290 default=datetime.datetime.now)
1291 1291 _landing_revision = Column(
1292 1292 "landing_revision", String(255), nullable=False, unique=False,
1293 1293 default=None)
1294 1294 enable_locking = Column(
1295 1295 "enable_locking", Boolean(), nullable=False, unique=None,
1296 1296 default=False)
1297 1297 _locked = Column(
1298 1298 "locked", String(255), nullable=True, unique=False, default=None)
1299 1299 _changeset_cache = Column(
1300 1300 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1301 1301
1302 1302 fork_id = Column(
1303 1303 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1304 1304 nullable=True, unique=False, default=None)
1305 1305 group_id = Column(
1306 1306 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1307 1307 unique=False, default=None)
1308 1308
1309 1309 user = relationship('User', lazy='joined')
1310 1310 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1311 1311 group = relationship('RepoGroup', lazy='joined')
1312 1312 repo_to_perm = relationship(
1313 1313 'UserRepoToPerm', cascade='all',
1314 1314 order_by='UserRepoToPerm.repo_to_perm_id')
1315 1315 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1316 1316 stats = relationship('Statistics', cascade='all', uselist=False)
1317 1317
1318 1318 followers = relationship(
1319 1319 'UserFollowing',
1320 1320 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1321 1321 cascade='all')
1322 1322 extra_fields = relationship(
1323 1323 'RepositoryField', cascade="all, delete, delete-orphan")
1324 1324 logs = relationship('UserLog')
1325 1325 comments = relationship(
1326 1326 'ChangesetComment', cascade="all, delete, delete-orphan")
1327 1327 pull_requests_source = relationship(
1328 1328 'PullRequest',
1329 1329 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1330 1330 cascade="all, delete, delete-orphan")
1331 1331 pull_requests_target = relationship(
1332 1332 'PullRequest',
1333 1333 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1334 1334 cascade="all, delete, delete-orphan")
1335 1335 ui = relationship('RepoRhodeCodeUi', cascade="all")
1336 1336 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1337 1337 integrations = relationship('Integration',
1338 1338 cascade="all, delete, delete-orphan")
1339 1339
1340 1340 def __unicode__(self):
1341 1341 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1342 1342 safe_unicode(self.repo_name))
1343 1343
1344 1344 @hybrid_property
1345 1345 def landing_rev(self):
1346 1346 # always should return [rev_type, rev]
1347 1347 if self._landing_revision:
1348 1348 _rev_info = self._landing_revision.split(':')
1349 1349 if len(_rev_info) < 2:
1350 1350 _rev_info.insert(0, 'rev')
1351 1351 return [_rev_info[0], _rev_info[1]]
1352 1352 return [None, None]
1353 1353
1354 1354 @landing_rev.setter
1355 1355 def landing_rev(self, val):
1356 1356 if ':' not in val:
1357 1357 raise ValueError('value must be delimited with `:` and consist '
1358 1358 'of <rev_type>:<rev>, got %s instead' % val)
1359 1359 self._landing_revision = val
1360 1360
1361 1361 @hybrid_property
1362 1362 def locked(self):
1363 1363 if self._locked:
1364 1364 user_id, timelocked, reason = self._locked.split(':')
1365 1365 lock_values = int(user_id), timelocked, reason
1366 1366 else:
1367 1367 lock_values = [None, None, None]
1368 1368 return lock_values
1369 1369
1370 1370 @locked.setter
1371 1371 def locked(self, val):
1372 1372 if val and isinstance(val, (list, tuple)):
1373 1373 self._locked = ':'.join(map(str, val))
1374 1374 else:
1375 1375 self._locked = None
1376 1376
1377 1377 @hybrid_property
1378 1378 def changeset_cache(self):
1379 1379 from rhodecode.lib.vcs.backends.base import EmptyCommit
1380 1380 dummy = EmptyCommit().__json__()
1381 1381 if not self._changeset_cache:
1382 1382 return dummy
1383 1383 try:
1384 1384 return json.loads(self._changeset_cache)
1385 1385 except TypeError:
1386 1386 return dummy
1387 1387 except Exception:
1388 1388 log.error(traceback.format_exc())
1389 1389 return dummy
1390 1390
1391 1391 @changeset_cache.setter
1392 1392 def changeset_cache(self, val):
1393 1393 try:
1394 1394 self._changeset_cache = json.dumps(val)
1395 1395 except Exception:
1396 1396 log.error(traceback.format_exc())
1397 1397
1398 1398 @hybrid_property
1399 1399 def repo_name(self):
1400 1400 return self._repo_name
1401 1401
1402 1402 @repo_name.setter
1403 1403 def repo_name(self, value):
1404 1404 self._repo_name = value
1405 1405 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1406 1406
1407 1407 @classmethod
1408 1408 def normalize_repo_name(cls, repo_name):
1409 1409 """
1410 1410 Normalizes os specific repo_name to the format internally stored inside
1411 1411 database using URL_SEP
1412 1412
1413 1413 :param cls:
1414 1414 :param repo_name:
1415 1415 """
1416 1416 return cls.NAME_SEP.join(repo_name.split(os.sep))
1417 1417
1418 1418 @classmethod
1419 1419 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1420 1420 session = Session()
1421 1421 q = session.query(cls).filter(cls.repo_name == repo_name)
1422 1422
1423 1423 if cache:
1424 1424 if identity_cache:
1425 1425 val = cls.identity_cache(session, 'repo_name', repo_name)
1426 1426 if val:
1427 1427 return val
1428 1428 else:
1429 1429 q = q.options(
1430 1430 FromCache("sql_cache_short",
1431 1431 "get_repo_by_name_%s" % _hash_key(repo_name)))
1432 1432
1433 1433 return q.scalar()
1434 1434
1435 1435 @classmethod
1436 1436 def get_by_full_path(cls, repo_full_path):
1437 1437 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1438 1438 repo_name = cls.normalize_repo_name(repo_name)
1439 1439 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1440 1440
1441 1441 @classmethod
1442 1442 def get_repo_forks(cls, repo_id):
1443 1443 return cls.query().filter(Repository.fork_id == repo_id)
1444 1444
1445 1445 @classmethod
1446 1446 def base_path(cls):
1447 1447 """
1448 1448 Returns base path when all repos are stored
1449 1449
1450 1450 :param cls:
1451 1451 """
1452 1452 q = Session().query(RhodeCodeUi)\
1453 1453 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1454 1454 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1455 1455 return q.one().ui_value
1456 1456
1457 1457 @classmethod
1458 1458 def is_valid(cls, repo_name):
1459 1459 """
1460 1460 returns True if given repo name is a valid filesystem repository
1461 1461
1462 1462 :param cls:
1463 1463 :param repo_name:
1464 1464 """
1465 1465 from rhodecode.lib.utils import is_valid_repo
1466 1466
1467 1467 return is_valid_repo(repo_name, cls.base_path())
1468 1468
1469 1469 @classmethod
1470 1470 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1471 1471 case_insensitive=True):
1472 1472 q = Repository.query()
1473 1473
1474 1474 if not isinstance(user_id, Optional):
1475 1475 q = q.filter(Repository.user_id == user_id)
1476 1476
1477 1477 if not isinstance(group_id, Optional):
1478 1478 q = q.filter(Repository.group_id == group_id)
1479 1479
1480 1480 if case_insensitive:
1481 1481 q = q.order_by(func.lower(Repository.repo_name))
1482 1482 else:
1483 1483 q = q.order_by(Repository.repo_name)
1484 1484 return q.all()
1485 1485
1486 1486 @property
1487 1487 def forks(self):
1488 1488 """
1489 1489 Return forks of this repo
1490 1490 """
1491 1491 return Repository.get_repo_forks(self.repo_id)
1492 1492
1493 1493 @property
1494 1494 def parent(self):
1495 1495 """
1496 1496 Returns fork parent
1497 1497 """
1498 1498 return self.fork
1499 1499
1500 1500 @property
1501 1501 def just_name(self):
1502 1502 return self.repo_name.split(self.NAME_SEP)[-1]
1503 1503
1504 1504 @property
1505 1505 def groups_with_parents(self):
1506 1506 groups = []
1507 1507 if self.group is None:
1508 1508 return groups
1509 1509
1510 1510 cur_gr = self.group
1511 1511 groups.insert(0, cur_gr)
1512 1512 while 1:
1513 1513 gr = getattr(cur_gr, 'parent_group', None)
1514 1514 cur_gr = cur_gr.parent_group
1515 1515 if gr is None:
1516 1516 break
1517 1517 groups.insert(0, gr)
1518 1518
1519 1519 return groups
1520 1520
1521 1521 @property
1522 1522 def groups_and_repo(self):
1523 1523 return self.groups_with_parents, self
1524 1524
1525 1525 @LazyProperty
1526 1526 def repo_path(self):
1527 1527 """
1528 1528 Returns base full path for that repository means where it actually
1529 1529 exists on a filesystem
1530 1530 """
1531 1531 q = Session().query(RhodeCodeUi).filter(
1532 1532 RhodeCodeUi.ui_key == self.NAME_SEP)
1533 1533 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1534 1534 return q.one().ui_value
1535 1535
1536 1536 @property
1537 1537 def repo_full_path(self):
1538 1538 p = [self.repo_path]
1539 1539 # we need to split the name by / since this is how we store the
1540 1540 # names in the database, but that eventually needs to be converted
1541 1541 # into a valid system path
1542 1542 p += self.repo_name.split(self.NAME_SEP)
1543 1543 return os.path.join(*map(safe_unicode, p))
1544 1544
1545 1545 @property
1546 1546 def cache_keys(self):
1547 1547 """
1548 1548 Returns associated cache keys for that repo
1549 1549 """
1550 1550 return CacheKey.query()\
1551 1551 .filter(CacheKey.cache_args == self.repo_name)\
1552 1552 .order_by(CacheKey.cache_key)\
1553 1553 .all()
1554 1554
1555 1555 def get_new_name(self, repo_name):
1556 1556 """
1557 1557 returns new full repository name based on assigned group and new new
1558 1558
1559 1559 :param group_name:
1560 1560 """
1561 1561 path_prefix = self.group.full_path_splitted if self.group else []
1562 1562 return self.NAME_SEP.join(path_prefix + [repo_name])
1563 1563
1564 1564 @property
1565 1565 def _config(self):
1566 1566 """
1567 1567 Returns db based config object.
1568 1568 """
1569 1569 from rhodecode.lib.utils import make_db_config
1570 1570 return make_db_config(clear_session=False, repo=self)
1571 1571
1572 1572 def permissions(self, with_admins=True, with_owner=True):
1573 1573 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1574 1574 q = q.options(joinedload(UserRepoToPerm.repository),
1575 1575 joinedload(UserRepoToPerm.user),
1576 1576 joinedload(UserRepoToPerm.permission),)
1577 1577
1578 1578 # get owners and admins and permissions. We do a trick of re-writing
1579 1579 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1580 1580 # has a global reference and changing one object propagates to all
1581 1581 # others. This means if admin is also an owner admin_row that change
1582 1582 # would propagate to both objects
1583 1583 perm_rows = []
1584 1584 for _usr in q.all():
1585 1585 usr = AttributeDict(_usr.user.get_dict())
1586 1586 usr.permission = _usr.permission.permission_name
1587 1587 perm_rows.append(usr)
1588 1588
1589 1589 # filter the perm rows by 'default' first and then sort them by
1590 1590 # admin,write,read,none permissions sorted again alphabetically in
1591 1591 # each group
1592 1592 perm_rows = sorted(perm_rows, key=display_sort)
1593 1593
1594 1594 _admin_perm = 'repository.admin'
1595 1595 owner_row = []
1596 1596 if with_owner:
1597 1597 usr = AttributeDict(self.user.get_dict())
1598 1598 usr.owner_row = True
1599 1599 usr.permission = _admin_perm
1600 1600 owner_row.append(usr)
1601 1601
1602 1602 super_admin_rows = []
1603 1603 if with_admins:
1604 1604 for usr in User.get_all_super_admins():
1605 1605 # if this admin is also owner, don't double the record
1606 1606 if usr.user_id == owner_row[0].user_id:
1607 1607 owner_row[0].admin_row = True
1608 1608 else:
1609 1609 usr = AttributeDict(usr.get_dict())
1610 1610 usr.admin_row = True
1611 1611 usr.permission = _admin_perm
1612 1612 super_admin_rows.append(usr)
1613 1613
1614 1614 return super_admin_rows + owner_row + perm_rows
1615 1615
1616 1616 def permission_user_groups(self):
1617 1617 q = UserGroupRepoToPerm.query().filter(
1618 1618 UserGroupRepoToPerm.repository == self)
1619 1619 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1620 1620 joinedload(UserGroupRepoToPerm.users_group),
1621 1621 joinedload(UserGroupRepoToPerm.permission),)
1622 1622
1623 1623 perm_rows = []
1624 1624 for _user_group in q.all():
1625 1625 usr = AttributeDict(_user_group.users_group.get_dict())
1626 1626 usr.permission = _user_group.permission.permission_name
1627 1627 perm_rows.append(usr)
1628 1628
1629 1629 return perm_rows
1630 1630
1631 1631 def get_api_data(self, include_secrets=False):
1632 1632 """
1633 1633 Common function for generating repo api data
1634 1634
1635 1635 :param include_secrets: See :meth:`User.get_api_data`.
1636 1636
1637 1637 """
1638 1638 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1639 1639 # move this methods on models level.
1640 1640 from rhodecode.model.settings import SettingsModel
1641 1641
1642 1642 repo = self
1643 1643 _user_id, _time, _reason = self.locked
1644 1644
1645 1645 data = {
1646 1646 'repo_id': repo.repo_id,
1647 1647 'repo_name': repo.repo_name,
1648 1648 'repo_type': repo.repo_type,
1649 1649 'clone_uri': repo.clone_uri or '',
1650 1650 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1651 1651 'private': repo.private,
1652 1652 'created_on': repo.created_on,
1653 1653 'description': repo.description,
1654 1654 'landing_rev': repo.landing_rev,
1655 1655 'owner': repo.user.username,
1656 1656 'fork_of': repo.fork.repo_name if repo.fork else None,
1657 1657 'enable_statistics': repo.enable_statistics,
1658 1658 'enable_locking': repo.enable_locking,
1659 1659 'enable_downloads': repo.enable_downloads,
1660 1660 'last_changeset': repo.changeset_cache,
1661 1661 'locked_by': User.get(_user_id).get_api_data(
1662 1662 include_secrets=include_secrets) if _user_id else None,
1663 1663 'locked_date': time_to_datetime(_time) if _time else None,
1664 1664 'lock_reason': _reason if _reason else None,
1665 1665 }
1666 1666
1667 1667 # TODO: mikhail: should be per-repo settings here
1668 1668 rc_config = SettingsModel().get_all_settings()
1669 1669 repository_fields = str2bool(
1670 1670 rc_config.get('rhodecode_repository_fields'))
1671 1671 if repository_fields:
1672 1672 for f in self.extra_fields:
1673 1673 data[f.field_key_prefixed] = f.field_value
1674 1674
1675 1675 return data
1676 1676
1677 1677 @classmethod
1678 1678 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1679 1679 if not lock_time:
1680 1680 lock_time = time.time()
1681 1681 if not lock_reason:
1682 1682 lock_reason = cls.LOCK_AUTOMATIC
1683 1683 repo.locked = [user_id, lock_time, lock_reason]
1684 1684 Session().add(repo)
1685 1685 Session().commit()
1686 1686
1687 1687 @classmethod
1688 1688 def unlock(cls, repo):
1689 1689 repo.locked = None
1690 1690 Session().add(repo)
1691 1691 Session().commit()
1692 1692
1693 1693 @classmethod
1694 1694 def getlock(cls, repo):
1695 1695 return repo.locked
1696 1696
1697 1697 def is_user_lock(self, user_id):
1698 1698 if self.lock[0]:
1699 1699 lock_user_id = safe_int(self.lock[0])
1700 1700 user_id = safe_int(user_id)
1701 1701 # both are ints, and they are equal
1702 1702 return all([lock_user_id, user_id]) and lock_user_id == user_id
1703 1703
1704 1704 return False
1705 1705
1706 1706 def get_locking_state(self, action, user_id, only_when_enabled=True):
1707 1707 """
1708 1708 Checks locking on this repository, if locking is enabled and lock is
1709 1709 present returns a tuple of make_lock, locked, locked_by.
1710 1710 make_lock can have 3 states None (do nothing) True, make lock
1711 1711 False release lock, This value is later propagated to hooks, which
1712 1712 do the locking. Think about this as signals passed to hooks what to do.
1713 1713
1714 1714 """
1715 1715 # TODO: johbo: This is part of the business logic and should be moved
1716 1716 # into the RepositoryModel.
1717 1717
1718 1718 if action not in ('push', 'pull'):
1719 1719 raise ValueError("Invalid action value: %s" % repr(action))
1720 1720
1721 1721 # defines if locked error should be thrown to user
1722 1722 currently_locked = False
1723 1723 # defines if new lock should be made, tri-state
1724 1724 make_lock = None
1725 1725 repo = self
1726 1726 user = User.get(user_id)
1727 1727
1728 1728 lock_info = repo.locked
1729 1729
1730 1730 if repo and (repo.enable_locking or not only_when_enabled):
1731 1731 if action == 'push':
1732 1732 # check if it's already locked !, if it is compare users
1733 1733 locked_by_user_id = lock_info[0]
1734 1734 if user.user_id == locked_by_user_id:
1735 1735 log.debug(
1736 1736 'Got `push` action from user %s, now unlocking', user)
1737 1737 # unlock if we have push from user who locked
1738 1738 make_lock = False
1739 1739 else:
1740 1740 # we're not the same user who locked, ban with
1741 1741 # code defined in settings (default is 423 HTTP Locked) !
1742 1742 log.debug('Repo %s is currently locked by %s', repo, user)
1743 1743 currently_locked = True
1744 1744 elif action == 'pull':
1745 1745 # [0] user [1] date
1746 1746 if lock_info[0] and lock_info[1]:
1747 1747 log.debug('Repo %s is currently locked by %s', repo, user)
1748 1748 currently_locked = True
1749 1749 else:
1750 1750 log.debug('Setting lock on repo %s by %s', repo, user)
1751 1751 make_lock = True
1752 1752
1753 1753 else:
1754 1754 log.debug('Repository %s do not have locking enabled', repo)
1755 1755
1756 1756 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1757 1757 make_lock, currently_locked, lock_info)
1758 1758
1759 1759 from rhodecode.lib.auth import HasRepoPermissionAny
1760 1760 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1761 1761 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1762 1762 # if we don't have at least write permission we cannot make a lock
1763 1763 log.debug('lock state reset back to FALSE due to lack '
1764 1764 'of at least read permission')
1765 1765 make_lock = False
1766 1766
1767 1767 return make_lock, currently_locked, lock_info
1768 1768
1769 1769 @property
1770 1770 def last_db_change(self):
1771 1771 return self.updated_on
1772 1772
1773 1773 @property
1774 1774 def clone_uri_hidden(self):
1775 1775 clone_uri = self.clone_uri
1776 1776 if clone_uri:
1777 1777 import urlobject
1778 1778 url_obj = urlobject.URLObject(clone_uri)
1779 1779 if url_obj.password:
1780 1780 clone_uri = url_obj.with_password('*****')
1781 1781 return clone_uri
1782 1782
1783 1783 def clone_url(self, **override):
1784 1784 qualified_home_url = url('home', qualified=True)
1785 1785
1786 1786 uri_tmpl = None
1787 1787 if 'with_id' in override:
1788 1788 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1789 1789 del override['with_id']
1790 1790
1791 1791 if 'uri_tmpl' in override:
1792 1792 uri_tmpl = override['uri_tmpl']
1793 1793 del override['uri_tmpl']
1794 1794
1795 1795 # we didn't override our tmpl from **overrides
1796 1796 if not uri_tmpl:
1797 1797 uri_tmpl = self.DEFAULT_CLONE_URI
1798 1798 try:
1799 1799 from pylons import tmpl_context as c
1800 1800 uri_tmpl = c.clone_uri_tmpl
1801 1801 except Exception:
1802 1802 # in any case if we call this outside of request context,
1803 1803 # ie, not having tmpl_context set up
1804 1804 pass
1805 1805
1806 1806 return get_clone_url(uri_tmpl=uri_tmpl,
1807 1807 qualifed_home_url=qualified_home_url,
1808 1808 repo_name=self.repo_name,
1809 1809 repo_id=self.repo_id, **override)
1810 1810
1811 1811 def set_state(self, state):
1812 1812 self.repo_state = state
1813 1813 Session().add(self)
1814 1814 #==========================================================================
1815 1815 # SCM PROPERTIES
1816 1816 #==========================================================================
1817 1817
1818 1818 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1819 1819 return get_commit_safe(
1820 1820 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1821 1821
1822 1822 def get_changeset(self, rev=None, pre_load=None):
1823 1823 warnings.warn("Use get_commit", DeprecationWarning)
1824 1824 commit_id = None
1825 1825 commit_idx = None
1826 1826 if isinstance(rev, basestring):
1827 1827 commit_id = rev
1828 1828 else:
1829 1829 commit_idx = rev
1830 1830 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1831 1831 pre_load=pre_load)
1832 1832
1833 1833 def get_landing_commit(self):
1834 1834 """
1835 1835 Returns landing commit, or if that doesn't exist returns the tip
1836 1836 """
1837 1837 _rev_type, _rev = self.landing_rev
1838 1838 commit = self.get_commit(_rev)
1839 1839 if isinstance(commit, EmptyCommit):
1840 1840 return self.get_commit()
1841 1841 return commit
1842 1842
1843 1843 def update_commit_cache(self, cs_cache=None, config=None):
1844 1844 """
1845 1845 Update cache of last changeset for repository, keys should be::
1846 1846
1847 1847 short_id
1848 1848 raw_id
1849 1849 revision
1850 1850 parents
1851 1851 message
1852 1852 date
1853 1853 author
1854 1854
1855 1855 :param cs_cache:
1856 1856 """
1857 1857 from rhodecode.lib.vcs.backends.base import BaseChangeset
1858 1858 if cs_cache is None:
1859 1859 # use no-cache version here
1860 1860 scm_repo = self.scm_instance(cache=False, config=config)
1861 1861 if scm_repo:
1862 1862 cs_cache = scm_repo.get_commit(
1863 1863 pre_load=["author", "date", "message", "parents"])
1864 1864 else:
1865 1865 cs_cache = EmptyCommit()
1866 1866
1867 1867 if isinstance(cs_cache, BaseChangeset):
1868 1868 cs_cache = cs_cache.__json__()
1869 1869
1870 1870 def is_outdated(new_cs_cache):
1871 1871 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1872 1872 new_cs_cache['revision'] != self.changeset_cache['revision']):
1873 1873 return True
1874 1874 return False
1875 1875
1876 1876 # check if we have maybe already latest cached revision
1877 1877 if is_outdated(cs_cache) or not self.changeset_cache:
1878 1878 _default = datetime.datetime.fromtimestamp(0)
1879 1879 last_change = cs_cache.get('date') or _default
1880 1880 log.debug('updated repo %s with new cs cache %s',
1881 1881 self.repo_name, cs_cache)
1882 1882 self.updated_on = last_change
1883 1883 self.changeset_cache = cs_cache
1884 1884 Session().add(self)
1885 1885 Session().commit()
1886 1886 else:
1887 1887 log.debug('Skipping update_commit_cache for repo:`%s` '
1888 1888 'commit already with latest changes', self.repo_name)
1889 1889
1890 1890 @property
1891 1891 def tip(self):
1892 1892 return self.get_commit('tip')
1893 1893
1894 1894 @property
1895 1895 def author(self):
1896 1896 return self.tip.author
1897 1897
1898 1898 @property
1899 1899 def last_change(self):
1900 1900 return self.scm_instance().last_change
1901 1901
1902 1902 def get_comments(self, revisions=None):
1903 1903 """
1904 1904 Returns comments for this repository grouped by revisions
1905 1905
1906 1906 :param revisions: filter query by revisions only
1907 1907 """
1908 1908 cmts = ChangesetComment.query()\
1909 1909 .filter(ChangesetComment.repo == self)
1910 1910 if revisions:
1911 1911 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1912 1912 grouped = collections.defaultdict(list)
1913 1913 for cmt in cmts.all():
1914 1914 grouped[cmt.revision].append(cmt)
1915 1915 return grouped
1916 1916
1917 1917 def statuses(self, revisions=None):
1918 1918 """
1919 1919 Returns statuses for this repository
1920 1920
1921 1921 :param revisions: list of revisions to get statuses for
1922 1922 """
1923 1923 statuses = ChangesetStatus.query()\
1924 1924 .filter(ChangesetStatus.repo == self)\
1925 1925 .filter(ChangesetStatus.version == 0)
1926 1926
1927 1927 if revisions:
1928 1928 # Try doing the filtering in chunks to avoid hitting limits
1929 1929 size = 500
1930 1930 status_results = []
1931 1931 for chunk in xrange(0, len(revisions), size):
1932 1932 status_results += statuses.filter(
1933 1933 ChangesetStatus.revision.in_(
1934 1934 revisions[chunk: chunk+size])
1935 1935 ).all()
1936 1936 else:
1937 1937 status_results = statuses.all()
1938 1938
1939 1939 grouped = {}
1940 1940
1941 1941 # maybe we have open new pullrequest without a status?
1942 1942 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1943 1943 status_lbl = ChangesetStatus.get_status_lbl(stat)
1944 1944 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1945 1945 for rev in pr.revisions:
1946 1946 pr_id = pr.pull_request_id
1947 1947 pr_repo = pr.target_repo.repo_name
1948 1948 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1949 1949
1950 1950 for stat in status_results:
1951 1951 pr_id = pr_repo = None
1952 1952 if stat.pull_request:
1953 1953 pr_id = stat.pull_request.pull_request_id
1954 1954 pr_repo = stat.pull_request.target_repo.repo_name
1955 1955 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1956 1956 pr_id, pr_repo]
1957 1957 return grouped
1958 1958
1959 1959 # ==========================================================================
1960 1960 # SCM CACHE INSTANCE
1961 1961 # ==========================================================================
1962 1962
1963 1963 def scm_instance(self, **kwargs):
1964 1964 import rhodecode
1965 1965
1966 1966 # Passing a config will not hit the cache currently only used
1967 1967 # for repo2dbmapper
1968 1968 config = kwargs.pop('config', None)
1969 1969 cache = kwargs.pop('cache', None)
1970 1970 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1971 1971 # if cache is NOT defined use default global, else we have a full
1972 1972 # control over cache behaviour
1973 1973 if cache is None and full_cache and not config:
1974 1974 return self._get_instance_cached()
1975 1975 return self._get_instance(cache=bool(cache), config=config)
1976 1976
1977 1977 def _get_instance_cached(self):
1978 1978 @cache_region('long_term')
1979 1979 def _get_repo(cache_key):
1980 1980 return self._get_instance()
1981 1981
1982 1982 invalidator_context = CacheKey.repo_context_cache(
1983 1983 _get_repo, self.repo_name, None, thread_scoped=True)
1984 1984
1985 1985 with invalidator_context as context:
1986 1986 context.invalidate()
1987 1987 repo = context.compute()
1988 1988
1989 1989 return repo
1990 1990
1991 1991 def _get_instance(self, cache=True, config=None):
1992 1992 config = config or self._config
1993 1993 custom_wire = {
1994 1994 'cache': cache # controls the vcs.remote cache
1995 1995 }
1996 1996 repo = get_vcs_instance(
1997 1997 repo_path=safe_str(self.repo_full_path),
1998 1998 config=config,
1999 1999 with_wire=custom_wire,
2000 2000 create=False,
2001 2001 _vcs_alias=self.repo_type)
2002 2002
2003 2003 return repo
2004 2004
2005 2005 def __json__(self):
2006 2006 return {'landing_rev': self.landing_rev}
2007 2007
2008 2008 def get_dict(self):
2009 2009
2010 2010 # Since we transformed `repo_name` to a hybrid property, we need to
2011 2011 # keep compatibility with the code which uses `repo_name` field.
2012 2012
2013 2013 result = super(Repository, self).get_dict()
2014 2014 result['repo_name'] = result.pop('_repo_name', None)
2015 2015 return result
2016 2016
2017 2017
2018 2018 class RepoGroup(Base, BaseModel):
2019 2019 __tablename__ = 'groups'
2020 2020 __table_args__ = (
2021 2021 UniqueConstraint('group_name', 'group_parent_id'),
2022 2022 CheckConstraint('group_id != group_parent_id'),
2023 2023 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2024 2024 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2025 2025 )
2026 2026 __mapper_args__ = {'order_by': 'group_name'}
2027 2027
2028 2028 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2029 2029
2030 2030 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2031 2031 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2032 2032 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2033 2033 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2034 2034 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2035 2035 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2036 2036 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2037 2037 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2038 2038
2039 2039 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2040 2040 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2041 2041 parent_group = relationship('RepoGroup', remote_side=group_id)
2042 2042 user = relationship('User')
2043 2043 integrations = relationship('Integration',
2044 2044 cascade="all, delete, delete-orphan")
2045 2045
2046 2046 def __init__(self, group_name='', parent_group=None):
2047 2047 self.group_name = group_name
2048 2048 self.parent_group = parent_group
2049 2049
2050 2050 def __unicode__(self):
2051 2051 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2052 2052 self.group_name)
2053 2053
2054 2054 @classmethod
2055 2055 def _generate_choice(cls, repo_group):
2056 2056 from webhelpers.html import literal as _literal
2057 2057 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2058 2058 return repo_group.group_id, _name(repo_group.full_path_splitted)
2059 2059
2060 2060 @classmethod
2061 2061 def groups_choices(cls, groups=None, show_empty_group=True):
2062 2062 if not groups:
2063 2063 groups = cls.query().all()
2064 2064
2065 2065 repo_groups = []
2066 2066 if show_empty_group:
2067 2067 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2068 2068
2069 2069 repo_groups.extend([cls._generate_choice(x) for x in groups])
2070 2070
2071 2071 repo_groups = sorted(
2072 2072 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2073 2073 return repo_groups
2074 2074
2075 2075 @classmethod
2076 2076 def url_sep(cls):
2077 2077 return URL_SEP
2078 2078
2079 2079 @classmethod
2080 2080 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2081 2081 if case_insensitive:
2082 2082 gr = cls.query().filter(func.lower(cls.group_name)
2083 2083 == func.lower(group_name))
2084 2084 else:
2085 2085 gr = cls.query().filter(cls.group_name == group_name)
2086 2086 if cache:
2087 2087 gr = gr.options(FromCache(
2088 2088 "sql_cache_short",
2089 2089 "get_group_%s" % _hash_key(group_name)))
2090 2090 return gr.scalar()
2091 2091
2092 2092 @classmethod
2093 2093 def get_user_personal_repo_group(cls, user_id):
2094 2094 user = User.get(user_id)
2095 2095 return cls.query()\
2096 2096 .filter(cls.personal == true())\
2097 2097 .filter(cls.user == user).scalar()
2098 2098
2099 2099 @classmethod
2100 2100 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2101 2101 case_insensitive=True):
2102 2102 q = RepoGroup.query()
2103 2103
2104 2104 if not isinstance(user_id, Optional):
2105 2105 q = q.filter(RepoGroup.user_id == user_id)
2106 2106
2107 2107 if not isinstance(group_id, Optional):
2108 2108 q = q.filter(RepoGroup.group_parent_id == group_id)
2109 2109
2110 2110 if case_insensitive:
2111 2111 q = q.order_by(func.lower(RepoGroup.group_name))
2112 2112 else:
2113 2113 q = q.order_by(RepoGroup.group_name)
2114 2114 return q.all()
2115 2115
2116 2116 @property
2117 2117 def parents(self):
2118 2118 parents_recursion_limit = 10
2119 2119 groups = []
2120 2120 if self.parent_group is None:
2121 2121 return groups
2122 2122 cur_gr = self.parent_group
2123 2123 groups.insert(0, cur_gr)
2124 2124 cnt = 0
2125 2125 while 1:
2126 2126 cnt += 1
2127 2127 gr = getattr(cur_gr, 'parent_group', None)
2128 2128 cur_gr = cur_gr.parent_group
2129 2129 if gr is None:
2130 2130 break
2131 2131 if cnt == parents_recursion_limit:
2132 2132 # this will prevent accidental infinit loops
2133 2133 log.error(('more than %s parents found for group %s, stopping '
2134 2134 'recursive parent fetching' % (parents_recursion_limit, self)))
2135 2135 break
2136 2136
2137 2137 groups.insert(0, gr)
2138 2138 return groups
2139 2139
2140 2140 @property
2141 2141 def children(self):
2142 2142 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2143 2143
2144 2144 @property
2145 2145 def name(self):
2146 2146 return self.group_name.split(RepoGroup.url_sep())[-1]
2147 2147
2148 2148 @property
2149 2149 def full_path(self):
2150 2150 return self.group_name
2151 2151
2152 2152 @property
2153 2153 def full_path_splitted(self):
2154 2154 return self.group_name.split(RepoGroup.url_sep())
2155 2155
2156 2156 @property
2157 2157 def repositories(self):
2158 2158 return Repository.query()\
2159 2159 .filter(Repository.group == self)\
2160 2160 .order_by(Repository.repo_name)
2161 2161
2162 2162 @property
2163 2163 def repositories_recursive_count(self):
2164 2164 cnt = self.repositories.count()
2165 2165
2166 2166 def children_count(group):
2167 2167 cnt = 0
2168 2168 for child in group.children:
2169 2169 cnt += child.repositories.count()
2170 2170 cnt += children_count(child)
2171 2171 return cnt
2172 2172
2173 2173 return cnt + children_count(self)
2174 2174
2175 2175 def _recursive_objects(self, include_repos=True):
2176 2176 all_ = []
2177 2177
2178 2178 def _get_members(root_gr):
2179 2179 if include_repos:
2180 2180 for r in root_gr.repositories:
2181 2181 all_.append(r)
2182 2182 childs = root_gr.children.all()
2183 2183 if childs:
2184 2184 for gr in childs:
2185 2185 all_.append(gr)
2186 2186 _get_members(gr)
2187 2187
2188 2188 _get_members(self)
2189 2189 return [self] + all_
2190 2190
2191 2191 def recursive_groups_and_repos(self):
2192 2192 """
2193 2193 Recursive return all groups, with repositories in those groups
2194 2194 """
2195 2195 return self._recursive_objects()
2196 2196
2197 2197 def recursive_groups(self):
2198 2198 """
2199 2199 Returns all children groups for this group including children of children
2200 2200 """
2201 2201 return self._recursive_objects(include_repos=False)
2202 2202
2203 2203 def get_new_name(self, group_name):
2204 2204 """
2205 2205 returns new full group name based on parent and new name
2206 2206
2207 2207 :param group_name:
2208 2208 """
2209 2209 path_prefix = (self.parent_group.full_path_splitted if
2210 2210 self.parent_group else [])
2211 2211 return RepoGroup.url_sep().join(path_prefix + [group_name])
2212 2212
2213 2213 def permissions(self, with_admins=True, with_owner=True):
2214 2214 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2215 2215 q = q.options(joinedload(UserRepoGroupToPerm.group),
2216 2216 joinedload(UserRepoGroupToPerm.user),
2217 2217 joinedload(UserRepoGroupToPerm.permission),)
2218 2218
2219 2219 # get owners and admins and permissions. We do a trick of re-writing
2220 2220 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2221 2221 # has a global reference and changing one object propagates to all
2222 2222 # others. This means if admin is also an owner admin_row that change
2223 2223 # would propagate to both objects
2224 2224 perm_rows = []
2225 2225 for _usr in q.all():
2226 2226 usr = AttributeDict(_usr.user.get_dict())
2227 2227 usr.permission = _usr.permission.permission_name
2228 2228 perm_rows.append(usr)
2229 2229
2230 2230 # filter the perm rows by 'default' first and then sort them by
2231 2231 # admin,write,read,none permissions sorted again alphabetically in
2232 2232 # each group
2233 2233 perm_rows = sorted(perm_rows, key=display_sort)
2234 2234
2235 2235 _admin_perm = 'group.admin'
2236 2236 owner_row = []
2237 2237 if with_owner:
2238 2238 usr = AttributeDict(self.user.get_dict())
2239 2239 usr.owner_row = True
2240 2240 usr.permission = _admin_perm
2241 2241 owner_row.append(usr)
2242 2242
2243 2243 super_admin_rows = []
2244 2244 if with_admins:
2245 2245 for usr in User.get_all_super_admins():
2246 2246 # if this admin is also owner, don't double the record
2247 2247 if usr.user_id == owner_row[0].user_id:
2248 2248 owner_row[0].admin_row = True
2249 2249 else:
2250 2250 usr = AttributeDict(usr.get_dict())
2251 2251 usr.admin_row = True
2252 2252 usr.permission = _admin_perm
2253 2253 super_admin_rows.append(usr)
2254 2254
2255 2255 return super_admin_rows + owner_row + perm_rows
2256 2256
2257 2257 def permission_user_groups(self):
2258 2258 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2259 2259 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2260 2260 joinedload(UserGroupRepoGroupToPerm.users_group),
2261 2261 joinedload(UserGroupRepoGroupToPerm.permission),)
2262 2262
2263 2263 perm_rows = []
2264 2264 for _user_group in q.all():
2265 2265 usr = AttributeDict(_user_group.users_group.get_dict())
2266 2266 usr.permission = _user_group.permission.permission_name
2267 2267 perm_rows.append(usr)
2268 2268
2269 2269 return perm_rows
2270 2270
2271 2271 def get_api_data(self):
2272 2272 """
2273 2273 Common function for generating api data
2274 2274
2275 2275 """
2276 2276 group = self
2277 2277 data = {
2278 2278 'group_id': group.group_id,
2279 2279 'group_name': group.group_name,
2280 2280 'group_description': group.group_description,
2281 2281 'parent_group': group.parent_group.group_name if group.parent_group else None,
2282 2282 'repositories': [x.repo_name for x in group.repositories],
2283 2283 'owner': group.user.username,
2284 2284 }
2285 2285 return data
2286 2286
2287 2287
2288 2288 class Permission(Base, BaseModel):
2289 2289 __tablename__ = 'permissions'
2290 2290 __table_args__ = (
2291 2291 Index('p_perm_name_idx', 'permission_name'),
2292 2292 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2293 2293 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2294 2294 )
2295 2295 PERMS = [
2296 2296 ('hg.admin', _('RhodeCode Super Administrator')),
2297 2297
2298 2298 ('repository.none', _('Repository no access')),
2299 2299 ('repository.read', _('Repository read access')),
2300 2300 ('repository.write', _('Repository write access')),
2301 2301 ('repository.admin', _('Repository admin access')),
2302 2302
2303 2303 ('group.none', _('Repository group no access')),
2304 2304 ('group.read', _('Repository group read access')),
2305 2305 ('group.write', _('Repository group write access')),
2306 2306 ('group.admin', _('Repository group admin access')),
2307 2307
2308 2308 ('usergroup.none', _('User group no access')),
2309 2309 ('usergroup.read', _('User group read access')),
2310 2310 ('usergroup.write', _('User group write access')),
2311 2311 ('usergroup.admin', _('User group admin access')),
2312 2312
2313 2313 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2314 2314 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2315 2315
2316 2316 ('hg.usergroup.create.false', _('User Group creation disabled')),
2317 2317 ('hg.usergroup.create.true', _('User Group creation enabled')),
2318 2318
2319 2319 ('hg.create.none', _('Repository creation disabled')),
2320 2320 ('hg.create.repository', _('Repository creation enabled')),
2321 2321 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2322 2322 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2323 2323
2324 2324 ('hg.fork.none', _('Repository forking disabled')),
2325 2325 ('hg.fork.repository', _('Repository forking enabled')),
2326 2326
2327 2327 ('hg.register.none', _('Registration disabled')),
2328 2328 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2329 2329 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2330 2330
2331 2331 ('hg.password_reset.enabled', _('Password reset enabled')),
2332 2332 ('hg.password_reset.hidden', _('Password reset hidden')),
2333 2333 ('hg.password_reset.disabled', _('Password reset disabled')),
2334 2334
2335 2335 ('hg.extern_activate.manual', _('Manual activation of external account')),
2336 2336 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2337 2337
2338 2338 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2339 2339 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2340 2340 ]
2341 2341
2342 2342 # definition of system default permissions for DEFAULT user
2343 2343 DEFAULT_USER_PERMISSIONS = [
2344 2344 'repository.read',
2345 2345 'group.read',
2346 2346 'usergroup.read',
2347 2347 'hg.create.repository',
2348 2348 'hg.repogroup.create.false',
2349 2349 'hg.usergroup.create.false',
2350 2350 'hg.create.write_on_repogroup.true',
2351 2351 'hg.fork.repository',
2352 2352 'hg.register.manual_activate',
2353 2353 'hg.password_reset.enabled',
2354 2354 'hg.extern_activate.auto',
2355 2355 'hg.inherit_default_perms.true',
2356 2356 ]
2357 2357
2358 2358 # defines which permissions are more important higher the more important
2359 2359 # Weight defines which permissions are more important.
2360 2360 # The higher number the more important.
2361 2361 PERM_WEIGHTS = {
2362 2362 'repository.none': 0,
2363 2363 'repository.read': 1,
2364 2364 'repository.write': 3,
2365 2365 'repository.admin': 4,
2366 2366
2367 2367 'group.none': 0,
2368 2368 'group.read': 1,
2369 2369 'group.write': 3,
2370 2370 'group.admin': 4,
2371 2371
2372 2372 'usergroup.none': 0,
2373 2373 'usergroup.read': 1,
2374 2374 'usergroup.write': 3,
2375 2375 'usergroup.admin': 4,
2376 2376
2377 2377 'hg.repogroup.create.false': 0,
2378 2378 'hg.repogroup.create.true': 1,
2379 2379
2380 2380 'hg.usergroup.create.false': 0,
2381 2381 'hg.usergroup.create.true': 1,
2382 2382
2383 2383 'hg.fork.none': 0,
2384 2384 'hg.fork.repository': 1,
2385 2385 'hg.create.none': 0,
2386 2386 'hg.create.repository': 1
2387 2387 }
2388 2388
2389 2389 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2390 2390 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2391 2391 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2392 2392
2393 2393 def __unicode__(self):
2394 2394 return u"<%s('%s:%s')>" % (
2395 2395 self.__class__.__name__, self.permission_id, self.permission_name
2396 2396 )
2397 2397
2398 2398 @classmethod
2399 2399 def get_by_key(cls, key):
2400 2400 return cls.query().filter(cls.permission_name == key).scalar()
2401 2401
2402 2402 @classmethod
2403 2403 def get_default_repo_perms(cls, user_id, repo_id=None):
2404 2404 q = Session().query(UserRepoToPerm, Repository, Permission)\
2405 2405 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2406 2406 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2407 2407 .filter(UserRepoToPerm.user_id == user_id)
2408 2408 if repo_id:
2409 2409 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2410 2410 return q.all()
2411 2411
2412 2412 @classmethod
2413 2413 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2414 2414 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2415 2415 .join(
2416 2416 Permission,
2417 2417 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2418 2418 .join(
2419 2419 Repository,
2420 2420 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2421 2421 .join(
2422 2422 UserGroup,
2423 2423 UserGroupRepoToPerm.users_group_id ==
2424 2424 UserGroup.users_group_id)\
2425 2425 .join(
2426 2426 UserGroupMember,
2427 2427 UserGroupRepoToPerm.users_group_id ==
2428 2428 UserGroupMember.users_group_id)\
2429 2429 .filter(
2430 2430 UserGroupMember.user_id == user_id,
2431 2431 UserGroup.users_group_active == true())
2432 2432 if repo_id:
2433 2433 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2434 2434 return q.all()
2435 2435
2436 2436 @classmethod
2437 2437 def get_default_group_perms(cls, user_id, repo_group_id=None):
2438 2438 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2439 2439 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2440 2440 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2441 2441 .filter(UserRepoGroupToPerm.user_id == user_id)
2442 2442 if repo_group_id:
2443 2443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2444 2444 return q.all()
2445 2445
2446 2446 @classmethod
2447 2447 def get_default_group_perms_from_user_group(
2448 2448 cls, user_id, repo_group_id=None):
2449 2449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2450 2450 .join(
2451 2451 Permission,
2452 2452 UserGroupRepoGroupToPerm.permission_id ==
2453 2453 Permission.permission_id)\
2454 2454 .join(
2455 2455 RepoGroup,
2456 2456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2457 2457 .join(
2458 2458 UserGroup,
2459 2459 UserGroupRepoGroupToPerm.users_group_id ==
2460 2460 UserGroup.users_group_id)\
2461 2461 .join(
2462 2462 UserGroupMember,
2463 2463 UserGroupRepoGroupToPerm.users_group_id ==
2464 2464 UserGroupMember.users_group_id)\
2465 2465 .filter(
2466 2466 UserGroupMember.user_id == user_id,
2467 2467 UserGroup.users_group_active == true())
2468 2468 if repo_group_id:
2469 2469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2470 2470 return q.all()
2471 2471
2472 2472 @classmethod
2473 2473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2474 2474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2475 2475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2476 2476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2477 2477 .filter(UserUserGroupToPerm.user_id == user_id)
2478 2478 if user_group_id:
2479 2479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2480 2480 return q.all()
2481 2481
2482 2482 @classmethod
2483 2483 def get_default_user_group_perms_from_user_group(
2484 2484 cls, user_id, user_group_id=None):
2485 2485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2486 2486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2487 2487 .join(
2488 2488 Permission,
2489 2489 UserGroupUserGroupToPerm.permission_id ==
2490 2490 Permission.permission_id)\
2491 2491 .join(
2492 2492 TargetUserGroup,
2493 2493 UserGroupUserGroupToPerm.target_user_group_id ==
2494 2494 TargetUserGroup.users_group_id)\
2495 2495 .join(
2496 2496 UserGroup,
2497 2497 UserGroupUserGroupToPerm.user_group_id ==
2498 2498 UserGroup.users_group_id)\
2499 2499 .join(
2500 2500 UserGroupMember,
2501 2501 UserGroupUserGroupToPerm.user_group_id ==
2502 2502 UserGroupMember.users_group_id)\
2503 2503 .filter(
2504 2504 UserGroupMember.user_id == user_id,
2505 2505 UserGroup.users_group_active == true())
2506 2506 if user_group_id:
2507 2507 q = q.filter(
2508 2508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2509 2509
2510 2510 return q.all()
2511 2511
2512 2512
2513 2513 class UserRepoToPerm(Base, BaseModel):
2514 2514 __tablename__ = 'repo_to_perm'
2515 2515 __table_args__ = (
2516 2516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2517 2517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2518 2518 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2519 2519 )
2520 2520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2521 2521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2522 2522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2523 2523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2524 2524
2525 2525 user = relationship('User')
2526 2526 repository = relationship('Repository')
2527 2527 permission = relationship('Permission')
2528 2528
2529 2529 @classmethod
2530 2530 def create(cls, user, repository, permission):
2531 2531 n = cls()
2532 2532 n.user = user
2533 2533 n.repository = repository
2534 2534 n.permission = permission
2535 2535 Session().add(n)
2536 2536 return n
2537 2537
2538 2538 def __unicode__(self):
2539 2539 return u'<%s => %s >' % (self.user, self.repository)
2540 2540
2541 2541
2542 2542 class UserUserGroupToPerm(Base, BaseModel):
2543 2543 __tablename__ = 'user_user_group_to_perm'
2544 2544 __table_args__ = (
2545 2545 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2546 2546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2547 2547 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2548 2548 )
2549 2549 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2550 2550 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2551 2551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2552 2552 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2553 2553
2554 2554 user = relationship('User')
2555 2555 user_group = relationship('UserGroup')
2556 2556 permission = relationship('Permission')
2557 2557
2558 2558 @classmethod
2559 2559 def create(cls, user, user_group, permission):
2560 2560 n = cls()
2561 2561 n.user = user
2562 2562 n.user_group = user_group
2563 2563 n.permission = permission
2564 2564 Session().add(n)
2565 2565 return n
2566 2566
2567 2567 def __unicode__(self):
2568 2568 return u'<%s => %s >' % (self.user, self.user_group)
2569 2569
2570 2570
2571 2571 class UserToPerm(Base, BaseModel):
2572 2572 __tablename__ = 'user_to_perm'
2573 2573 __table_args__ = (
2574 2574 UniqueConstraint('user_id', 'permission_id'),
2575 2575 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2576 2576 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2577 2577 )
2578 2578 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2579 2579 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2580 2580 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2581 2581
2582 2582 user = relationship('User')
2583 2583 permission = relationship('Permission', lazy='joined')
2584 2584
2585 2585 def __unicode__(self):
2586 2586 return u'<%s => %s >' % (self.user, self.permission)
2587 2587
2588 2588
2589 2589 class UserGroupRepoToPerm(Base, BaseModel):
2590 2590 __tablename__ = 'users_group_repo_to_perm'
2591 2591 __table_args__ = (
2592 2592 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2593 2593 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2594 2594 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2595 2595 )
2596 2596 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2597 2597 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2598 2598 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2599 2599 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2600 2600
2601 2601 users_group = relationship('UserGroup')
2602 2602 permission = relationship('Permission')
2603 2603 repository = relationship('Repository')
2604 2604
2605 2605 @classmethod
2606 2606 def create(cls, users_group, repository, permission):
2607 2607 n = cls()
2608 2608 n.users_group = users_group
2609 2609 n.repository = repository
2610 2610 n.permission = permission
2611 2611 Session().add(n)
2612 2612 return n
2613 2613
2614 2614 def __unicode__(self):
2615 2615 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2616 2616
2617 2617
2618 2618 class UserGroupUserGroupToPerm(Base, BaseModel):
2619 2619 __tablename__ = 'user_group_user_group_to_perm'
2620 2620 __table_args__ = (
2621 2621 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2622 2622 CheckConstraint('target_user_group_id != user_group_id'),
2623 2623 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2624 2624 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2625 2625 )
2626 2626 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)
2627 2627 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2628 2628 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2629 2629 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2630 2630
2631 2631 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2632 2632 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2633 2633 permission = relationship('Permission')
2634 2634
2635 2635 @classmethod
2636 2636 def create(cls, target_user_group, user_group, permission):
2637 2637 n = cls()
2638 2638 n.target_user_group = target_user_group
2639 2639 n.user_group = user_group
2640 2640 n.permission = permission
2641 2641 Session().add(n)
2642 2642 return n
2643 2643
2644 2644 def __unicode__(self):
2645 2645 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2646 2646
2647 2647
2648 2648 class UserGroupToPerm(Base, BaseModel):
2649 2649 __tablename__ = 'users_group_to_perm'
2650 2650 __table_args__ = (
2651 2651 UniqueConstraint('users_group_id', 'permission_id',),
2652 2652 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2653 2653 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2654 2654 )
2655 2655 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2656 2656 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2657 2657 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2658 2658
2659 2659 users_group = relationship('UserGroup')
2660 2660 permission = relationship('Permission')
2661 2661
2662 2662
2663 2663 class UserRepoGroupToPerm(Base, BaseModel):
2664 2664 __tablename__ = 'user_repo_group_to_perm'
2665 2665 __table_args__ = (
2666 2666 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2667 2667 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2668 2668 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2669 2669 )
2670 2670
2671 2671 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2672 2672 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2673 2673 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2674 2674 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2675 2675
2676 2676 user = relationship('User')
2677 2677 group = relationship('RepoGroup')
2678 2678 permission = relationship('Permission')
2679 2679
2680 2680 @classmethod
2681 2681 def create(cls, user, repository_group, permission):
2682 2682 n = cls()
2683 2683 n.user = user
2684 2684 n.group = repository_group
2685 2685 n.permission = permission
2686 2686 Session().add(n)
2687 2687 return n
2688 2688
2689 2689
2690 2690 class UserGroupRepoGroupToPerm(Base, BaseModel):
2691 2691 __tablename__ = 'users_group_repo_group_to_perm'
2692 2692 __table_args__ = (
2693 2693 UniqueConstraint('users_group_id', 'group_id'),
2694 2694 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2695 2695 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2696 2696 )
2697 2697
2698 2698 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)
2699 2699 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2700 2700 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2701 2701 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2702 2702
2703 2703 users_group = relationship('UserGroup')
2704 2704 permission = relationship('Permission')
2705 2705 group = relationship('RepoGroup')
2706 2706
2707 2707 @classmethod
2708 2708 def create(cls, user_group, repository_group, permission):
2709 2709 n = cls()
2710 2710 n.users_group = user_group
2711 2711 n.group = repository_group
2712 2712 n.permission = permission
2713 2713 Session().add(n)
2714 2714 return n
2715 2715
2716 2716 def __unicode__(self):
2717 2717 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2718 2718
2719 2719
2720 2720 class Statistics(Base, BaseModel):
2721 2721 __tablename__ = 'statistics'
2722 2722 __table_args__ = (
2723 2723 UniqueConstraint('repository_id'),
2724 2724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2725 2725 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2726 2726 )
2727 2727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2728 2728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2729 2729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2730 2730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2731 2731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2732 2732 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2733 2733
2734 2734 repository = relationship('Repository', single_parent=True)
2735 2735
2736 2736
2737 2737 class UserFollowing(Base, BaseModel):
2738 2738 __tablename__ = 'user_followings'
2739 2739 __table_args__ = (
2740 2740 UniqueConstraint('user_id', 'follows_repository_id'),
2741 2741 UniqueConstraint('user_id', 'follows_user_id'),
2742 2742 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2743 2743 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2744 2744 )
2745 2745
2746 2746 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2747 2747 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2748 2748 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2749 2749 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2750 2750 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2751 2751
2752 2752 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2753 2753
2754 2754 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2755 2755 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2756 2756
2757 2757 @classmethod
2758 2758 def get_repo_followers(cls, repo_id):
2759 2759 return cls.query().filter(cls.follows_repo_id == repo_id)
2760 2760
2761 2761
2762 2762 class CacheKey(Base, BaseModel):
2763 2763 __tablename__ = 'cache_invalidation'
2764 2764 __table_args__ = (
2765 2765 UniqueConstraint('cache_key'),
2766 2766 Index('key_idx', 'cache_key'),
2767 2767 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2768 2768 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2769 2769 )
2770 2770 CACHE_TYPE_ATOM = 'ATOM'
2771 2771 CACHE_TYPE_RSS = 'RSS'
2772 2772 CACHE_TYPE_README = 'README'
2773 2773
2774 2774 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2775 2775 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2776 2776 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2777 2777 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2778 2778
2779 2779 def __init__(self, cache_key, cache_args=''):
2780 2780 self.cache_key = cache_key
2781 2781 self.cache_args = cache_args
2782 2782 self.cache_active = False
2783 2783
2784 2784 def __unicode__(self):
2785 2785 return u"<%s('%s:%s[%s]')>" % (
2786 2786 self.__class__.__name__,
2787 2787 self.cache_id, self.cache_key, self.cache_active)
2788 2788
2789 2789 def _cache_key_partition(self):
2790 2790 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2791 2791 return prefix, repo_name, suffix
2792 2792
2793 2793 def get_prefix(self):
2794 2794 """
2795 2795 Try to extract prefix from existing cache key. The key could consist
2796 2796 of prefix, repo_name, suffix
2797 2797 """
2798 2798 # this returns prefix, repo_name, suffix
2799 2799 return self._cache_key_partition()[0]
2800 2800
2801 2801 def get_suffix(self):
2802 2802 """
2803 2803 get suffix that might have been used in _get_cache_key to
2804 2804 generate self.cache_key. Only used for informational purposes
2805 2805 in repo_edit.mako.
2806 2806 """
2807 2807 # prefix, repo_name, suffix
2808 2808 return self._cache_key_partition()[2]
2809 2809
2810 2810 @classmethod
2811 2811 def delete_all_cache(cls):
2812 2812 """
2813 2813 Delete all cache keys from database.
2814 2814 Should only be run when all instances are down and all entries
2815 2815 thus stale.
2816 2816 """
2817 2817 cls.query().delete()
2818 2818 Session().commit()
2819 2819
2820 2820 @classmethod
2821 2821 def get_cache_key(cls, repo_name, cache_type):
2822 2822 """
2823 2823
2824 2824 Generate a cache key for this process of RhodeCode instance.
2825 2825 Prefix most likely will be process id or maybe explicitly set
2826 2826 instance_id from .ini file.
2827 2827 """
2828 2828 import rhodecode
2829 2829 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2830 2830
2831 2831 repo_as_unicode = safe_unicode(repo_name)
2832 2832 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2833 2833 if cache_type else repo_as_unicode
2834 2834
2835 2835 return u'{}{}'.format(prefix, key)
2836 2836
2837 2837 @classmethod
2838 2838 def set_invalidate(cls, repo_name, delete=False):
2839 2839 """
2840 2840 Mark all caches of a repo as invalid in the database.
2841 2841 """
2842 2842
2843 2843 try:
2844 2844 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2845 2845 if delete:
2846 2846 log.debug('cache objects deleted for repo %s',
2847 2847 safe_str(repo_name))
2848 2848 qry.delete()
2849 2849 else:
2850 2850 log.debug('cache objects marked as invalid for repo %s',
2851 2851 safe_str(repo_name))
2852 2852 qry.update({"cache_active": False})
2853 2853
2854 2854 Session().commit()
2855 2855 except Exception:
2856 2856 log.exception(
2857 2857 'Cache key invalidation failed for repository %s',
2858 2858 safe_str(repo_name))
2859 2859 Session().rollback()
2860 2860
2861 2861 @classmethod
2862 2862 def get_active_cache(cls, cache_key):
2863 2863 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2864 2864 if inv_obj:
2865 2865 return inv_obj
2866 2866 return None
2867 2867
2868 2868 @classmethod
2869 2869 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2870 2870 thread_scoped=False):
2871 2871 """
2872 2872 @cache_region('long_term')
2873 2873 def _heavy_calculation(cache_key):
2874 2874 return 'result'
2875 2875
2876 2876 cache_context = CacheKey.repo_context_cache(
2877 2877 _heavy_calculation, repo_name, cache_type)
2878 2878
2879 2879 with cache_context as context:
2880 2880 context.invalidate()
2881 2881 computed = context.compute()
2882 2882
2883 2883 assert computed == 'result'
2884 2884 """
2885 2885 from rhodecode.lib import caches
2886 2886 return caches.InvalidationContext(
2887 2887 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2888 2888
2889 2889
2890 2890 class ChangesetComment(Base, BaseModel):
2891 2891 __tablename__ = 'changeset_comments'
2892 2892 __table_args__ = (
2893 2893 Index('cc_revision_idx', 'revision'),
2894 2894 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2895 2895 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2896 2896 )
2897 2897
2898 2898 COMMENT_OUTDATED = u'comment_outdated'
2899 2899
2900 2900 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2901 2901 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2902 2902 revision = Column('revision', String(40), nullable=True)
2903 2903 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2904 2904 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2905 2905 line_no = Column('line_no', Unicode(10), nullable=True)
2906 2906 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2907 2907 f_path = Column('f_path', Unicode(1000), nullable=True)
2908 2908 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2909 2909 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2910 2910 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2911 2911 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2912 2912 renderer = Column('renderer', Unicode(64), nullable=True)
2913 2913 display_state = Column('display_state', Unicode(128), nullable=True)
2914 2914
2915 2915 author = relationship('User', lazy='joined')
2916 2916 repo = relationship('Repository')
2917 2917 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2918 2918 pull_request = relationship('PullRequest', lazy='joined')
2919 2919 pull_request_version = relationship('PullRequestVersion')
2920 2920
2921 2921 @classmethod
2922 2922 def get_users(cls, revision=None, pull_request_id=None):
2923 2923 """
2924 2924 Returns user associated with this ChangesetComment. ie those
2925 2925 who actually commented
2926 2926
2927 2927 :param cls:
2928 2928 :param revision:
2929 2929 """
2930 2930 q = Session().query(User)\
2931 2931 .join(ChangesetComment.author)
2932 2932 if revision:
2933 2933 q = q.filter(cls.revision == revision)
2934 2934 elif pull_request_id:
2935 2935 q = q.filter(cls.pull_request_id == pull_request_id)
2936 2936 return q.all()
2937 2937
2938 2938 @classmethod
2939 2939 def get_index_from_version(cls, pr_version, versions):
2940 2940 num_versions = [x.pull_request_version_id for x in versions]
2941 2941 try:
2942 2942 return num_versions.index(pr_version) +1
2943 2943 except (IndexError, ValueError):
2944 2944 return
2945 2945
2946 2946 @property
2947 2947 def outdated(self):
2948 2948 return self.display_state == self.COMMENT_OUTDATED
2949 2949
2950 2950 def outdated_at_version(self, version):
2951 2951 """
2952 2952 Checks if comment is outdated for given pull request version
2953 2953 """
2954 2954 return self.outdated and self.pull_request_version_id != version
2955 2955
2956 2956 def get_index_version(self, versions):
2957 2957 return self.get_index_from_version(
2958 2958 self.pull_request_version_id, versions)
2959 2959
2960 2960 def render(self, mentions=False):
2961 2961 from rhodecode.lib import helpers as h
2962 2962 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2963 2963
2964 2964 def __repr__(self):
2965 2965 if self.comment_id:
2966 2966 return '<DB:ChangesetComment #%s>' % self.comment_id
2967 2967 else:
2968 2968 return '<DB:ChangesetComment at %#x>' % id(self)
2969 2969
2970 2970
2971 2971 class ChangesetStatus(Base, BaseModel):
2972 2972 __tablename__ = 'changeset_statuses'
2973 2973 __table_args__ = (
2974 2974 Index('cs_revision_idx', 'revision'),
2975 2975 Index('cs_version_idx', 'version'),
2976 2976 UniqueConstraint('repo_id', 'revision', 'version'),
2977 2977 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2978 2978 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2979 2979 )
2980 2980 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2981 2981 STATUS_APPROVED = 'approved'
2982 2982 STATUS_REJECTED = 'rejected'
2983 2983 STATUS_UNDER_REVIEW = 'under_review'
2984 2984
2985 2985 STATUSES = [
2986 2986 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2987 2987 (STATUS_APPROVED, _("Approved")),
2988 2988 (STATUS_REJECTED, _("Rejected")),
2989 2989 (STATUS_UNDER_REVIEW, _("Under Review")),
2990 2990 ]
2991 2991
2992 2992 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2993 2993 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2994 2994 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2995 2995 revision = Column('revision', String(40), nullable=False)
2996 2996 status = Column('status', String(128), nullable=False, default=DEFAULT)
2997 2997 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2998 2998 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2999 2999 version = Column('version', Integer(), nullable=False, default=0)
3000 3000 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3001 3001
3002 3002 author = relationship('User', lazy='joined')
3003 3003 repo = relationship('Repository')
3004 3004 comment = relationship('ChangesetComment', lazy='joined')
3005 3005 pull_request = relationship('PullRequest', lazy='joined')
3006 3006
3007 3007 def __unicode__(self):
3008 3008 return u"<%s('%s[%s]:%s')>" % (
3009 3009 self.__class__.__name__,
3010 3010 self.status, self.version, self.author
3011 3011 )
3012 3012
3013 3013 @classmethod
3014 3014 def get_status_lbl(cls, value):
3015 3015 return dict(cls.STATUSES).get(value)
3016 3016
3017 3017 @property
3018 3018 def status_lbl(self):
3019 3019 return ChangesetStatus.get_status_lbl(self.status)
3020 3020
3021 3021
3022 3022 class _PullRequestBase(BaseModel):
3023 3023 """
3024 3024 Common attributes of pull request and version entries.
3025 3025 """
3026 3026
3027 3027 # .status values
3028 3028 STATUS_NEW = u'new'
3029 3029 STATUS_OPEN = u'open'
3030 3030 STATUS_CLOSED = u'closed'
3031 3031
3032 3032 title = Column('title', Unicode(255), nullable=True)
3033 3033 description = Column(
3034 3034 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3035 3035 nullable=True)
3036 3036 # new/open/closed status of pull request (not approve/reject/etc)
3037 3037 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3038 3038 created_on = Column(
3039 3039 'created_on', DateTime(timezone=False), nullable=False,
3040 3040 default=datetime.datetime.now)
3041 3041 updated_on = Column(
3042 3042 'updated_on', DateTime(timezone=False), nullable=False,
3043 3043 default=datetime.datetime.now)
3044 3044
3045 3045 @declared_attr
3046 3046 def user_id(cls):
3047 3047 return Column(
3048 3048 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3049 3049 unique=None)
3050 3050
3051 3051 # 500 revisions max
3052 3052 _revisions = Column(
3053 3053 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3054 3054
3055 3055 @declared_attr
3056 3056 def source_repo_id(cls):
3057 3057 # TODO: dan: rename column to source_repo_id
3058 3058 return Column(
3059 3059 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3060 3060 nullable=False)
3061 3061
3062 3062 source_ref = Column('org_ref', Unicode(255), nullable=False)
3063 3063
3064 3064 @declared_attr
3065 3065 def target_repo_id(cls):
3066 3066 # TODO: dan: rename column to target_repo_id
3067 3067 return Column(
3068 3068 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3069 3069 nullable=False)
3070 3070
3071 3071 target_ref = Column('other_ref', Unicode(255), nullable=False)
3072 3072 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3073 3073
3074 3074 # TODO: dan: rename column to last_merge_source_rev
3075 3075 _last_merge_source_rev = Column(
3076 3076 'last_merge_org_rev', String(40), nullable=True)
3077 3077 # TODO: dan: rename column to last_merge_target_rev
3078 3078 _last_merge_target_rev = Column(
3079 3079 'last_merge_other_rev', String(40), nullable=True)
3080 3080 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3081 3081 merge_rev = Column('merge_rev', String(40), nullable=True)
3082 3082
3083 3083 @hybrid_property
3084 3084 def revisions(self):
3085 3085 return self._revisions.split(':') if self._revisions else []
3086 3086
3087 3087 @revisions.setter
3088 3088 def revisions(self, val):
3089 3089 self._revisions = ':'.join(val)
3090 3090
3091 3091 @declared_attr
3092 3092 def author(cls):
3093 3093 return relationship('User', lazy='joined')
3094 3094
3095 3095 @declared_attr
3096 3096 def source_repo(cls):
3097 3097 return relationship(
3098 3098 'Repository',
3099 3099 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3100 3100
3101 3101 @property
3102 3102 def source_ref_parts(self):
3103 3103 return self.unicode_to_reference(self.source_ref)
3104 3104
3105 3105 @declared_attr
3106 3106 def target_repo(cls):
3107 3107 return relationship(
3108 3108 'Repository',
3109 3109 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3110 3110
3111 3111 @property
3112 3112 def target_ref_parts(self):
3113 3113 return self.unicode_to_reference(self.target_ref)
3114 3114
3115 3115 @property
3116 3116 def shadow_merge_ref(self):
3117 3117 return self.unicode_to_reference(self._shadow_merge_ref)
3118 3118
3119 3119 @shadow_merge_ref.setter
3120 3120 def shadow_merge_ref(self, ref):
3121 3121 self._shadow_merge_ref = self.reference_to_unicode(ref)
3122 3122
3123 3123 def unicode_to_reference(self, raw):
3124 3124 """
3125 3125 Convert a unicode (or string) to a reference object.
3126 3126 If unicode evaluates to False it returns None.
3127 3127 """
3128 3128 if raw:
3129 3129 refs = raw.split(':')
3130 3130 return Reference(*refs)
3131 3131 else:
3132 3132 return None
3133 3133
3134 3134 def reference_to_unicode(self, ref):
3135 3135 """
3136 3136 Convert a reference object to unicode.
3137 3137 If reference is None it returns None.
3138 3138 """
3139 3139 if ref:
3140 3140 return u':'.join(ref)
3141 3141 else:
3142 3142 return None
3143 3143
3144 3144 def get_api_data(self):
3145 3145 from rhodecode.model.pull_request import PullRequestModel
3146 3146 pull_request = self
3147 3147 merge_status = PullRequestModel().merge_status(pull_request)
3148 3148
3149 3149 pull_request_url = url(
3150 3150 'pullrequest_show', repo_name=self.target_repo.repo_name,
3151 3151 pull_request_id=self.pull_request_id, qualified=True)
3152 3152
3153 3153 merge_data = {
3154 3154 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3155 3155 'reference': (
3156 3156 pull_request.shadow_merge_ref._asdict()
3157 3157 if pull_request.shadow_merge_ref else None),
3158 3158 }
3159 3159
3160 3160 data = {
3161 3161 'pull_request_id': pull_request.pull_request_id,
3162 3162 'url': pull_request_url,
3163 3163 'title': pull_request.title,
3164 3164 'description': pull_request.description,
3165 3165 'status': pull_request.status,
3166 3166 'created_on': pull_request.created_on,
3167 3167 'updated_on': pull_request.updated_on,
3168 3168 'commit_ids': pull_request.revisions,
3169 3169 'review_status': pull_request.calculated_review_status(),
3170 3170 'mergeable': {
3171 3171 'status': merge_status[0],
3172 3172 'message': unicode(merge_status[1]),
3173 3173 },
3174 3174 'source': {
3175 3175 'clone_url': pull_request.source_repo.clone_url(),
3176 3176 'repository': pull_request.source_repo.repo_name,
3177 3177 'reference': {
3178 3178 'name': pull_request.source_ref_parts.name,
3179 3179 'type': pull_request.source_ref_parts.type,
3180 3180 'commit_id': pull_request.source_ref_parts.commit_id,
3181 3181 },
3182 3182 },
3183 3183 'target': {
3184 3184 'clone_url': pull_request.target_repo.clone_url(),
3185 3185 'repository': pull_request.target_repo.repo_name,
3186 3186 'reference': {
3187 3187 'name': pull_request.target_ref_parts.name,
3188 3188 'type': pull_request.target_ref_parts.type,
3189 3189 'commit_id': pull_request.target_ref_parts.commit_id,
3190 3190 },
3191 3191 },
3192 3192 'merge': merge_data,
3193 3193 'author': pull_request.author.get_api_data(include_secrets=False,
3194 3194 details='basic'),
3195 3195 'reviewers': [
3196 3196 {
3197 3197 'user': reviewer.get_api_data(include_secrets=False,
3198 3198 details='basic'),
3199 3199 'reasons': reasons,
3200 3200 'review_status': st[0][1].status if st else 'not_reviewed',
3201 3201 }
3202 3202 for reviewer, reasons, st in pull_request.reviewers_statuses()
3203 3203 ]
3204 3204 }
3205 3205
3206 3206 return data
3207 3207
3208 3208
3209 3209 class PullRequest(Base, _PullRequestBase):
3210 3210 __tablename__ = 'pull_requests'
3211 3211 __table_args__ = (
3212 3212 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3213 3213 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3214 3214 )
3215 3215
3216 3216 pull_request_id = Column(
3217 3217 'pull_request_id', Integer(), nullable=False, primary_key=True)
3218 3218
3219 3219 def __repr__(self):
3220 3220 if self.pull_request_id:
3221 3221 return '<DB:PullRequest #%s>' % self.pull_request_id
3222 3222 else:
3223 3223 return '<DB:PullRequest at %#x>' % id(self)
3224 3224
3225 3225 reviewers = relationship('PullRequestReviewers',
3226 3226 cascade="all, delete, delete-orphan")
3227 3227 statuses = relationship('ChangesetStatus')
3228 3228 comments = relationship('ChangesetComment',
3229 3229 cascade="all, delete, delete-orphan")
3230 3230 versions = relationship('PullRequestVersion',
3231 3231 cascade="all, delete, delete-orphan",
3232 3232 lazy='dynamic')
3233 3233
3234 3234
3235 3235 @classmethod
3236 3236 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3237 3237 internal_methods=None):
3238 3238
3239 3239 class PullRequestDisplay(object):
3240 3240 """
3241 3241 Special object wrapper for showing PullRequest data via Versions
3242 3242 It mimics PR object as close as possible. This is read only object
3243 3243 just for display
3244 3244 """
3245 3245
3246 3246 def __init__(self, attrs, internal=None):
3247 3247 self.attrs = attrs
3248 3248 # internal have priority over the given ones via attrs
3249 3249 self.internal = internal or ['versions']
3250 3250
3251 3251 def __getattr__(self, item):
3252 3252 if item in self.internal:
3253 3253 return getattr(self, item)
3254 3254 try:
3255 3255 return self.attrs[item]
3256 3256 except KeyError:
3257 3257 raise AttributeError(
3258 3258 '%s object has no attribute %s' % (self, item))
3259 3259
3260 3260 def __repr__(self):
3261 3261 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3262 3262
3263 3263 def versions(self):
3264 3264 return pull_request_obj.versions.order_by(
3265 3265 PullRequestVersion.pull_request_version_id).all()
3266 3266
3267 3267 def is_closed(self):
3268 3268 return pull_request_obj.is_closed()
3269 3269
3270 3270 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3271 3271
3272 3272 attrs.author = StrictAttributeDict(
3273 3273 pull_request_obj.author.get_api_data())
3274 3274 if pull_request_obj.target_repo:
3275 3275 attrs.target_repo = StrictAttributeDict(
3276 3276 pull_request_obj.target_repo.get_api_data())
3277 3277 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3278 3278
3279 3279 if pull_request_obj.source_repo:
3280 3280 attrs.source_repo = StrictAttributeDict(
3281 3281 pull_request_obj.source_repo.get_api_data())
3282 3282 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3283 3283
3284 3284 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3285 3285 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3286 3286 attrs.revisions = pull_request_obj.revisions
3287 3287
3288 3288 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3289 3289
3290 3290 return PullRequestDisplay(attrs, internal=internal_methods)
3291 3291
3292 3292 def is_closed(self):
3293 3293 return self.status == self.STATUS_CLOSED
3294 3294
3295 3295 def __json__(self):
3296 3296 return {
3297 3297 'revisions': self.revisions,
3298 3298 }
3299 3299
3300 3300 def calculated_review_status(self):
3301 3301 from rhodecode.model.changeset_status import ChangesetStatusModel
3302 3302 return ChangesetStatusModel().calculated_review_status(self)
3303 3303
3304 3304 def reviewers_statuses(self):
3305 3305 from rhodecode.model.changeset_status import ChangesetStatusModel
3306 3306 return ChangesetStatusModel().reviewers_statuses(self)
3307 3307
3308 3308
3309 3309 class PullRequestVersion(Base, _PullRequestBase):
3310 3310 __tablename__ = 'pull_request_versions'
3311 3311 __table_args__ = (
3312 3312 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3313 3313 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3314 3314 )
3315 3315
3316 3316 pull_request_version_id = Column(
3317 3317 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3318 3318 pull_request_id = Column(
3319 3319 'pull_request_id', Integer(),
3320 3320 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3321 3321 pull_request = relationship('PullRequest')
3322 3322
3323 3323 def __repr__(self):
3324 3324 if self.pull_request_version_id:
3325 3325 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3326 3326 else:
3327 3327 return '<DB:PullRequestVersion at %#x>' % id(self)
3328 3328
3329 3329 @property
3330 3330 def reviewers(self):
3331 3331 return self.pull_request.reviewers
3332 3332
3333 3333 @property
3334 3334 def versions(self):
3335 3335 return self.pull_request.versions
3336 3336
3337 3337 def is_closed(self):
3338 3338 # calculate from original
3339 3339 return self.pull_request.status == self.STATUS_CLOSED
3340 3340
3341 3341 def calculated_review_status(self):
3342 3342 return self.pull_request.calculated_review_status()
3343 3343
3344 3344 def reviewers_statuses(self):
3345 3345 return self.pull_request.reviewers_statuses()
3346 3346
3347 3347
3348 3348 class PullRequestReviewers(Base, BaseModel):
3349 3349 __tablename__ = 'pull_request_reviewers'
3350 3350 __table_args__ = (
3351 3351 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3352 3352 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3353 3353 )
3354 3354
3355 3355 def __init__(self, user=None, pull_request=None, reasons=None):
3356 3356 self.user = user
3357 3357 self.pull_request = pull_request
3358 3358 self.reasons = reasons or []
3359 3359
3360 3360 @hybrid_property
3361 3361 def reasons(self):
3362 3362 if not self._reasons:
3363 3363 return []
3364 3364 return self._reasons
3365 3365
3366 3366 @reasons.setter
3367 3367 def reasons(self, val):
3368 3368 val = val or []
3369 3369 if any(not isinstance(x, basestring) for x in val):
3370 3370 raise Exception('invalid reasons type, must be list of strings')
3371 3371 self._reasons = val
3372 3372
3373 3373 pull_requests_reviewers_id = Column(
3374 3374 'pull_requests_reviewers_id', Integer(), nullable=False,
3375 3375 primary_key=True)
3376 3376 pull_request_id = Column(
3377 3377 "pull_request_id", Integer(),
3378 3378 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3379 3379 user_id = Column(
3380 3380 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3381 3381 _reasons = Column(
3382 3382 'reason', MutationList.as_mutable(
3383 3383 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3384 3384
3385 3385 user = relationship('User')
3386 3386 pull_request = relationship('PullRequest')
3387 3387
3388 3388
3389 3389 class Notification(Base, BaseModel):
3390 3390 __tablename__ = 'notifications'
3391 3391 __table_args__ = (
3392 3392 Index('notification_type_idx', 'type'),
3393 3393 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3394 3394 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3395 3395 )
3396 3396
3397 3397 TYPE_CHANGESET_COMMENT = u'cs_comment'
3398 3398 TYPE_MESSAGE = u'message'
3399 3399 TYPE_MENTION = u'mention'
3400 3400 TYPE_REGISTRATION = u'registration'
3401 3401 TYPE_PULL_REQUEST = u'pull_request'
3402 3402 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3403 3403
3404 3404 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3405 3405 subject = Column('subject', Unicode(512), nullable=True)
3406 3406 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3407 3407 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3408 3408 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3409 3409 type_ = Column('type', Unicode(255))
3410 3410
3411 3411 created_by_user = relationship('User')
3412 3412 notifications_to_users = relationship('UserNotification', lazy='joined',
3413 3413 cascade="all, delete, delete-orphan")
3414 3414
3415 3415 @property
3416 3416 def recipients(self):
3417 3417 return [x.user for x in UserNotification.query()\
3418 3418 .filter(UserNotification.notification == self)\
3419 3419 .order_by(UserNotification.user_id.asc()).all()]
3420 3420
3421 3421 @classmethod
3422 3422 def create(cls, created_by, subject, body, recipients, type_=None):
3423 3423 if type_ is None:
3424 3424 type_ = Notification.TYPE_MESSAGE
3425 3425
3426 3426 notification = cls()
3427 3427 notification.created_by_user = created_by
3428 3428 notification.subject = subject
3429 3429 notification.body = body
3430 3430 notification.type_ = type_
3431 3431 notification.created_on = datetime.datetime.now()
3432 3432
3433 3433 for u in recipients:
3434 3434 assoc = UserNotification()
3435 3435 assoc.notification = notification
3436 3436
3437 3437 # if created_by is inside recipients mark his notification
3438 3438 # as read
3439 3439 if u.user_id == created_by.user_id:
3440 3440 assoc.read = True
3441 3441
3442 3442 u.notifications.append(assoc)
3443 3443 Session().add(notification)
3444 3444
3445 3445 return notification
3446 3446
3447 3447 @property
3448 3448 def description(self):
3449 3449 from rhodecode.model.notification import NotificationModel
3450 3450 return NotificationModel().make_description(self)
3451 3451
3452 3452
3453 3453 class UserNotification(Base, BaseModel):
3454 3454 __tablename__ = 'user_to_notification'
3455 3455 __table_args__ = (
3456 3456 UniqueConstraint('user_id', 'notification_id'),
3457 3457 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3458 3458 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3459 3459 )
3460 3460 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3461 3461 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3462 3462 read = Column('read', Boolean, default=False)
3463 3463 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3464 3464
3465 3465 user = relationship('User', lazy="joined")
3466 3466 notification = relationship('Notification', lazy="joined",
3467 3467 order_by=lambda: Notification.created_on.desc(),)
3468 3468
3469 3469 def mark_as_read(self):
3470 3470 self.read = True
3471 3471 Session().add(self)
3472 3472
3473 3473
3474 3474 class Gist(Base, BaseModel):
3475 3475 __tablename__ = 'gists'
3476 3476 __table_args__ = (
3477 3477 Index('g_gist_access_id_idx', 'gist_access_id'),
3478 3478 Index('g_created_on_idx', 'created_on'),
3479 3479 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3480 3480 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3481 3481 )
3482 3482 GIST_PUBLIC = u'public'
3483 3483 GIST_PRIVATE = u'private'
3484 3484 DEFAULT_FILENAME = u'gistfile1.txt'
3485 3485
3486 3486 ACL_LEVEL_PUBLIC = u'acl_public'
3487 3487 ACL_LEVEL_PRIVATE = u'acl_private'
3488 3488
3489 3489 gist_id = Column('gist_id', Integer(), primary_key=True)
3490 3490 gist_access_id = Column('gist_access_id', Unicode(250))
3491 3491 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3492 3492 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3493 3493 gist_expires = Column('gist_expires', Float(53), nullable=False)
3494 3494 gist_type = Column('gist_type', Unicode(128), nullable=False)
3495 3495 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3496 3496 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3497 3497 acl_level = Column('acl_level', Unicode(128), nullable=True)
3498 3498
3499 3499 owner = relationship('User')
3500 3500
3501 3501 def __repr__(self):
3502 3502 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3503 3503
3504 3504 @classmethod
3505 3505 def get_or_404(cls, id_):
3506 3506 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3507 3507 if not res:
3508 3508 raise HTTPNotFound
3509 3509 return res
3510 3510
3511 3511 @classmethod
3512 3512 def get_by_access_id(cls, gist_access_id):
3513 3513 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3514 3514
3515 3515 def gist_url(self):
3516 3516 import rhodecode
3517 3517 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3518 3518 if alias_url:
3519 3519 return alias_url.replace('{gistid}', self.gist_access_id)
3520 3520
3521 3521 return url('gist', gist_id=self.gist_access_id, qualified=True)
3522 3522
3523 3523 @classmethod
3524 3524 def base_path(cls):
3525 3525 """
3526 3526 Returns base path when all gists are stored
3527 3527
3528 3528 :param cls:
3529 3529 """
3530 3530 from rhodecode.model.gist import GIST_STORE_LOC
3531 3531 q = Session().query(RhodeCodeUi)\
3532 3532 .filter(RhodeCodeUi.ui_key == URL_SEP)
3533 3533 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3534 3534 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3535 3535
3536 3536 def get_api_data(self):
3537 3537 """
3538 3538 Common function for generating gist related data for API
3539 3539 """
3540 3540 gist = self
3541 3541 data = {
3542 3542 'gist_id': gist.gist_id,
3543 3543 'type': gist.gist_type,
3544 3544 'access_id': gist.gist_access_id,
3545 3545 'description': gist.gist_description,
3546 3546 'url': gist.gist_url(),
3547 3547 'expires': gist.gist_expires,
3548 3548 'created_on': gist.created_on,
3549 3549 'modified_at': gist.modified_at,
3550 3550 'content': None,
3551 3551 'acl_level': gist.acl_level,
3552 3552 }
3553 3553 return data
3554 3554
3555 3555 def __json__(self):
3556 3556 data = dict(
3557 3557 )
3558 3558 data.update(self.get_api_data())
3559 3559 return data
3560 3560 # SCM functions
3561 3561
3562 3562 def scm_instance(self, **kwargs):
3563 3563 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3564 3564 return get_vcs_instance(
3565 3565 repo_path=safe_str(full_repo_path), create=False)
3566 3566
3567 3567
3568 3568 class ExternalIdentity(Base, BaseModel):
3569 3569 __tablename__ = 'external_identities'
3570 3570 __table_args__ = (
3571 3571 Index('local_user_id_idx', 'local_user_id'),
3572 3572 Index('external_id_idx', 'external_id'),
3573 3573 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3574 3574 'mysql_charset': 'utf8'})
3575 3575
3576 3576 external_id = Column('external_id', Unicode(255), default=u'',
3577 3577 primary_key=True)
3578 3578 external_username = Column('external_username', Unicode(1024), default=u'')
3579 3579 local_user_id = Column('local_user_id', Integer(),
3580 3580 ForeignKey('users.user_id'), primary_key=True)
3581 3581 provider_name = Column('provider_name', Unicode(255), default=u'',
3582 3582 primary_key=True)
3583 3583 access_token = Column('access_token', String(1024), default=u'')
3584 3584 alt_token = Column('alt_token', String(1024), default=u'')
3585 3585 token_secret = Column('token_secret', String(1024), default=u'')
3586 3586
3587 3587 @classmethod
3588 3588 def by_external_id_and_provider(cls, external_id, provider_name,
3589 3589 local_user_id=None):
3590 3590 """
3591 3591 Returns ExternalIdentity instance based on search params
3592 3592
3593 3593 :param external_id:
3594 3594 :param provider_name:
3595 3595 :return: ExternalIdentity
3596 3596 """
3597 3597 query = cls.query()
3598 3598 query = query.filter(cls.external_id == external_id)
3599 3599 query = query.filter(cls.provider_name == provider_name)
3600 3600 if local_user_id:
3601 3601 query = query.filter(cls.local_user_id == local_user_id)
3602 3602 return query.first()
3603 3603
3604 3604 @classmethod
3605 3605 def user_by_external_id_and_provider(cls, external_id, provider_name):
3606 3606 """
3607 3607 Returns User instance based on search params
3608 3608
3609 3609 :param external_id:
3610 3610 :param provider_name:
3611 3611 :return: User
3612 3612 """
3613 3613 query = User.query()
3614 3614 query = query.filter(cls.external_id == external_id)
3615 3615 query = query.filter(cls.provider_name == provider_name)
3616 3616 query = query.filter(User.user_id == cls.local_user_id)
3617 3617 return query.first()
3618 3618
3619 3619 @classmethod
3620 3620 def by_local_user_id(cls, local_user_id):
3621 3621 """
3622 3622 Returns all tokens for user
3623 3623
3624 3624 :param local_user_id:
3625 3625 :return: ExternalIdentity
3626 3626 """
3627 3627 query = cls.query()
3628 3628 query = query.filter(cls.local_user_id == local_user_id)
3629 3629 return query
3630 3630
3631 3631
3632 3632 class Integration(Base, BaseModel):
3633 3633 __tablename__ = 'integrations'
3634 3634 __table_args__ = (
3635 3635 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3636 3636 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3637 3637 )
3638 3638
3639 3639 integration_id = Column('integration_id', Integer(), primary_key=True)
3640 3640 integration_type = Column('integration_type', String(255))
3641 3641 enabled = Column('enabled', Boolean(), nullable=False)
3642 3642 name = Column('name', String(255), nullable=False)
3643 3643 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3644 3644 default=False)
3645 3645
3646 3646 settings = Column(
3647 3647 'settings_json', MutationObj.as_mutable(
3648 3648 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3649 3649 repo_id = Column(
3650 3650 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3651 3651 nullable=True, unique=None, default=None)
3652 3652 repo = relationship('Repository', lazy='joined')
3653 3653
3654 3654 repo_group_id = Column(
3655 3655 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3656 3656 nullable=True, unique=None, default=None)
3657 3657 repo_group = relationship('RepoGroup', lazy='joined')
3658 3658
3659 3659 @property
3660 3660 def scope(self):
3661 3661 if self.repo:
3662 3662 return repr(self.repo)
3663 3663 if self.repo_group:
3664 3664 if self.child_repos_only:
3665 3665 return repr(self.repo_group) + ' (child repos only)'
3666 3666 else:
3667 3667 return repr(self.repo_group) + ' (recursive)'
3668 3668 if self.child_repos_only:
3669 3669 return 'root_repos'
3670 3670 return 'global'
3671 3671
3672 3672 def __repr__(self):
3673 3673 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3674 3674
3675 3675
3676 3676 class RepoReviewRuleUser(Base, BaseModel):
3677 3677 __tablename__ = 'repo_review_rules_users'
3678 3678 __table_args__ = (
3679 3679 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3680 3680 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3681 3681 )
3682 3682 repo_review_rule_user_id = Column(
3683 3683 'repo_review_rule_user_id', Integer(), primary_key=True)
3684 3684 repo_review_rule_id = Column("repo_review_rule_id",
3685 3685 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3686 3686 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3687 3687 nullable=False)
3688 3688 user = relationship('User')
3689 3689
3690 3690
3691 3691 class RepoReviewRuleUserGroup(Base, BaseModel):
3692 3692 __tablename__ = 'repo_review_rules_users_groups'
3693 3693 __table_args__ = (
3694 3694 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3695 3695 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3696 3696 )
3697 3697 repo_review_rule_users_group_id = Column(
3698 3698 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3699 3699 repo_review_rule_id = Column("repo_review_rule_id",
3700 3700 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3701 3701 users_group_id = Column("users_group_id", Integer(),
3702 3702 ForeignKey('users_groups.users_group_id'), nullable=False)
3703 3703 users_group = relationship('UserGroup')
3704 3704
3705 3705
3706 3706 class RepoReviewRule(Base, BaseModel):
3707 3707 __tablename__ = 'repo_review_rules'
3708 3708 __table_args__ = (
3709 3709 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3710 3710 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3711 3711 )
3712 3712
3713 3713 repo_review_rule_id = Column(
3714 3714 'repo_review_rule_id', Integer(), primary_key=True)
3715 3715 repo_id = Column(
3716 3716 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3717 3717 repo = relationship('Repository', backref='review_rules')
3718 3718
3719 3719 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3720 3720 default=u'*') # glob
3721 3721 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3722 3722 default=u'*') # glob
3723 3723
3724 3724 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3725 3725 nullable=False, default=False)
3726 3726 rule_users = relationship('RepoReviewRuleUser')
3727 3727 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3728 3728
3729 3729 @hybrid_property
3730 3730 def branch_pattern(self):
3731 3731 return self._branch_pattern or '*'
3732 3732
3733 3733 def _validate_glob(self, value):
3734 3734 re.compile('^' + glob2re(value) + '$')
3735 3735
3736 3736 @branch_pattern.setter
3737 3737 def branch_pattern(self, value):
3738 3738 self._validate_glob(value)
3739 3739 self._branch_pattern = value or '*'
3740 3740
3741 3741 @hybrid_property
3742 3742 def file_pattern(self):
3743 3743 return self._file_pattern or '*'
3744 3744
3745 3745 @file_pattern.setter
3746 3746 def file_pattern(self, value):
3747 3747 self._validate_glob(value)
3748 3748 self._file_pattern = value or '*'
3749 3749
3750 3750 def matches(self, branch, files_changed):
3751 3751 """
3752 3752 Check if this review rule matches a branch/files in a pull request
3753 3753
3754 3754 :param branch: branch name for the commit
3755 3755 :param files_changed: list of file paths changed in the pull request
3756 3756 """
3757 3757
3758 3758 branch = branch or ''
3759 3759 files_changed = files_changed or []
3760 3760
3761 3761 branch_matches = True
3762 3762 if branch:
3763 3763 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3764 3764 branch_matches = bool(branch_regex.search(branch))
3765 3765
3766 3766 files_matches = True
3767 3767 if self.file_pattern != '*':
3768 3768 files_matches = False
3769 3769 file_regex = re.compile(glob2re(self.file_pattern))
3770 3770 for filename in files_changed:
3771 3771 if file_regex.search(filename):
3772 3772 files_matches = True
3773 3773 break
3774 3774
3775 3775 return branch_matches and files_matches
3776 3776
3777 3777 @property
3778 3778 def review_users(self):
3779 3779 """ Returns the users which this rule applies to """
3780 3780
3781 3781 users = set()
3782 3782 users |= set([
3783 3783 rule_user.user for rule_user in self.rule_users
3784 3784 if rule_user.user.active])
3785 3785 users |= set(
3786 3786 member.user
3787 3787 for rule_user_group in self.rule_user_groups
3788 3788 for member in rule_user_group.users_group.members
3789 3789 if member.user.active
3790 3790 )
3791 3791 return users
3792 3792
3793 3793 def __repr__(self):
3794 3794 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3795 3795 self.repo_review_rule_id, self.repo)
3796 3796
3797 3797
3798 3798 class DbMigrateVersion(Base, BaseModel):
3799 3799 __tablename__ = 'db_migrate_version'
3800 3800 __table_args__ = (
3801 3801 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3802 3802 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3803 3803 )
3804 3804 repository_id = Column('repository_id', String(250), primary_key=True)
3805 3805 repository_path = Column('repository_path', Text)
3806 3806 version = Column('version', Integer)
3807 3807
3808 3808
3809 3809 class DbSession(Base, BaseModel):
3810 3810 __tablename__ = 'db_session'
3811 3811 __table_args__ = (
3812 3812 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3813 3813 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3814 3814 )
3815
3816 def __repr__(self):
3817 return '<DB:DbSession({})>'.format(self.id)
3818
3819 id = Column('id', Integer())
3815 3820 namespace = Column('namespace', String(255), primary_key=True)
3816 3821 accessed = Column('accessed', DateTime, nullable=False)
3817 3822 created = Column('created', DateTime, nullable=False)
3818 data = Column('data', PickleType, nullable=False) No newline at end of file
3823 data = Column('data', PickleType, nullable=False)
@@ -1,56 +1,56 b''
1 1 <div class="panel panel-default">
2 2 <div class="panel-heading">
3 3 <h3 class="panel-title">${_('Email Configuration')}</h3>
4 4 </div>
5 5 <div class="panel-body">
6 6 <%
7 7 elems = [
8 8 (_('Email prefix'), c.rhodecode_ini.get('email_prefix'), ''),
9 9 (_('RhodeCode email from'), c.rhodecode_ini.get('app_email_from'), ''),
10 10 (_('Error email from'), c.rhodecode_ini.get('error_email_from'), ''),
11 11 (_('Error email recipients'), c.rhodecode_ini.get('email_to'), ''),
12 12
13 13 (_('SMTP server'), c.rhodecode_ini.get('smtp_server'), ''),
14 14 (_('SMTP username'), c.rhodecode_ini.get('smtp_username'), ''),
15 15 (_('SMTP password'), '%s chars' % len(c.rhodecode_ini.get('smtp_password', '')), ''),
16 16 (_('SMTP port'), c.rhodecode_ini.get('smtp_port'), ''),
17 17
18 18 (_('SMTP use TLS'), c.rhodecode_ini.get('smtp_use_tls'), ''),
19 19 (_('SMTP use SSL'), c.rhodecode_ini.get('smtp_use_ssl'), ''),
20 20 (_('SMTP auth'), c.rhodecode_ini.get('smtp_auth'), ''),
21 21 ]
22 22 %>
23 <dl class="dl-horizontal">
23 <dl class="dl-horizontal settings">
24 24 %for dt, dd, tt in elems:
25 25 <dt >${dt}:</dt>
26 26 <dd title="${tt}">${dd}</dd>
27 27 %endfor
28 28 </dl>
29 29 </div>
30 30 </div>
31 31
32 32 <div class="panel panel-default">
33 33 <div class="panel-heading">
34 34 <h3 class="panel-title">${_('Test Email')}</h3>
35 35 </div>
36 36 <div class="panel-body">
37 37 ${h.secure_form(url('admin_settings_email'), method='post')}
38 38
39 39 <div class="field input">
40 40 ${h.text('test_email', size=60, placeholder=_('enter valid email'))}
41 41 </div>
42 42 <div class="field">
43 43 <span class="help-block">
44 44 ${_('Send an auto-generated email from this server to above email...')}
45 45 </span>
46 46 </div>
47 47 <div class="buttons">
48 48 ${h.submit('send',_('Send'),class_="btn")}
49 49 </div>
50 50 ${h.end_form()}
51 51 </div>
52 52 </div>
53 53
54 54
55 55
56 56
General Comments 0
You need to be logged in to leave comments. Login now