##// END OF EJS Templates
comments: allow submitting id of comment which submitted comment resolved....
marcink -
r1325:b4535cc7 default
parent child Browse files
Show More
@@ -1,1167 +1,1167 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Routes configuration
22 Routes configuration
23
23
24 The more specific and detailed routes should be defined first so they
24 The more specific and detailed routes should be defined first so they
25 may take precedent over the more generic routes. For more information
25 may take precedent over the more generic routes. For more information
26 refer to the routes manual at http://routes.groovie.org/docs/
26 refer to the routes manual at http://routes.groovie.org/docs/
27
27
28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 and _route_name variable which uses some of stored naming here to do redirects.
29 and _route_name variable which uses some of stored naming here to do redirects.
30 """
30 """
31 import os
31 import os
32 import re
32 import re
33 from routes import Mapper
33 from routes import Mapper
34
34
35 from rhodecode.config import routing_links
35 from rhodecode.config import routing_links
36
36
37 # prefix for non repository related links needs to be prefixed with `/`
37 # prefix for non repository related links needs to be prefixed with `/`
38 ADMIN_PREFIX = '/_admin'
38 ADMIN_PREFIX = '/_admin'
39 STATIC_FILE_PREFIX = '/_static'
39 STATIC_FILE_PREFIX = '/_static'
40
40
41 # Default requirements for URL parts
41 # Default requirements for URL parts
42 URL_NAME_REQUIREMENTS = {
42 URL_NAME_REQUIREMENTS = {
43 # group name can have a slash in them, but they must not end with a slash
43 # group name can have a slash in them, but they must not end with a slash
44 'group_name': r'.*?[^/]',
44 'group_name': r'.*?[^/]',
45 'repo_group_name': r'.*?[^/]',
45 'repo_group_name': r'.*?[^/]',
46 # repo names can have a slash in them, but they must not end with a slash
46 # repo names can have a slash in them, but they must not end with a slash
47 'repo_name': r'.*?[^/]',
47 'repo_name': r'.*?[^/]',
48 # file path eats up everything at the end
48 # file path eats up everything at the end
49 'f_path': r'.*',
49 'f_path': r'.*',
50 # reference types
50 # reference types
51 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
51 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
52 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
52 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
53 }
53 }
54
54
55
55
56 def add_route_requirements(route_path, requirements):
56 def add_route_requirements(route_path, requirements):
57 """
57 """
58 Adds regex requirements to pyramid routes using a mapping dict
58 Adds regex requirements to pyramid routes using a mapping dict
59
59
60 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
60 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
61 '/{action}/{id:\d+}'
61 '/{action}/{id:\d+}'
62
62
63 """
63 """
64 for key, regex in requirements.items():
64 for key, regex in requirements.items():
65 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
65 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
66 return route_path
66 return route_path
67
67
68
68
69 class JSRoutesMapper(Mapper):
69 class JSRoutesMapper(Mapper):
70 """
70 """
71 Wrapper for routes.Mapper to make pyroutes compatible url definitions
71 Wrapper for routes.Mapper to make pyroutes compatible url definitions
72 """
72 """
73 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
73 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
74 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
74 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
75 def __init__(self, *args, **kw):
75 def __init__(self, *args, **kw):
76 super(JSRoutesMapper, self).__init__(*args, **kw)
76 super(JSRoutesMapper, self).__init__(*args, **kw)
77 self._jsroutes = []
77 self._jsroutes = []
78
78
79 def connect(self, *args, **kw):
79 def connect(self, *args, **kw):
80 """
80 """
81 Wrapper for connect to take an extra argument jsroute=True
81 Wrapper for connect to take an extra argument jsroute=True
82
82
83 :param jsroute: boolean, if True will add the route to the pyroutes list
83 :param jsroute: boolean, if True will add the route to the pyroutes list
84 """
84 """
85 if kw.pop('jsroute', False):
85 if kw.pop('jsroute', False):
86 if not self._named_route_regex.match(args[0]):
86 if not self._named_route_regex.match(args[0]):
87 raise Exception('only named routes can be added to pyroutes')
87 raise Exception('only named routes can be added to pyroutes')
88 self._jsroutes.append(args[0])
88 self._jsroutes.append(args[0])
89
89
90 super(JSRoutesMapper, self).connect(*args, **kw)
90 super(JSRoutesMapper, self).connect(*args, **kw)
91
91
92 def _extract_route_information(self, route):
92 def _extract_route_information(self, route):
93 """
93 """
94 Convert a route into tuple(name, path, args), eg:
94 Convert a route into tuple(name, path, args), eg:
95 ('user_profile', '/profile/%(username)s', ['username'])
95 ('user_profile', '/profile/%(username)s', ['username'])
96 """
96 """
97 routepath = route.routepath
97 routepath = route.routepath
98 def replace(matchobj):
98 def replace(matchobj):
99 if matchobj.group(1):
99 if matchobj.group(1):
100 return "%%(%s)s" % matchobj.group(1).split(':')[0]
100 return "%%(%s)s" % matchobj.group(1).split(':')[0]
101 else:
101 else:
102 return "%%(%s)s" % matchobj.group(2)
102 return "%%(%s)s" % matchobj.group(2)
103
103
104 routepath = self._argument_prog.sub(replace, routepath)
104 routepath = self._argument_prog.sub(replace, routepath)
105 return (
105 return (
106 route.name,
106 route.name,
107 routepath,
107 routepath,
108 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
108 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
109 for arg in self._argument_prog.findall(route.routepath)]
109 for arg in self._argument_prog.findall(route.routepath)]
110 )
110 )
111
111
112 def jsroutes(self):
112 def jsroutes(self):
113 """
113 """
114 Return a list of pyroutes.js compatible routes
114 Return a list of pyroutes.js compatible routes
115 """
115 """
116 for route_name in self._jsroutes:
116 for route_name in self._jsroutes:
117 yield self._extract_route_information(self._routenames[route_name])
117 yield self._extract_route_information(self._routenames[route_name])
118
118
119
119
120 def make_map(config):
120 def make_map(config):
121 """Create, configure and return the routes Mapper"""
121 """Create, configure and return the routes Mapper"""
122 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
122 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
123 always_scan=config['debug'])
123 always_scan=config['debug'])
124 rmap.minimization = False
124 rmap.minimization = False
125 rmap.explicit = False
125 rmap.explicit = False
126
126
127 from rhodecode.lib.utils2 import str2bool
127 from rhodecode.lib.utils2 import str2bool
128 from rhodecode.model import repo, repo_group
128 from rhodecode.model import repo, repo_group
129
129
130 def check_repo(environ, match_dict):
130 def check_repo(environ, match_dict):
131 """
131 """
132 check for valid repository for proper 404 handling
132 check for valid repository for proper 404 handling
133
133
134 :param environ:
134 :param environ:
135 :param match_dict:
135 :param match_dict:
136 """
136 """
137 repo_name = match_dict.get('repo_name')
137 repo_name = match_dict.get('repo_name')
138
138
139 if match_dict.get('f_path'):
139 if match_dict.get('f_path'):
140 # fix for multiple initial slashes that causes errors
140 # fix for multiple initial slashes that causes errors
141 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
141 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
142 repo_model = repo.RepoModel()
142 repo_model = repo.RepoModel()
143 by_name_match = repo_model.get_by_repo_name(repo_name)
143 by_name_match = repo_model.get_by_repo_name(repo_name)
144 # if we match quickly from database, short circuit the operation,
144 # if we match quickly from database, short circuit the operation,
145 # and validate repo based on the type.
145 # and validate repo based on the type.
146 if by_name_match:
146 if by_name_match:
147 return True
147 return True
148
148
149 by_id_match = repo_model.get_repo_by_id(repo_name)
149 by_id_match = repo_model.get_repo_by_id(repo_name)
150 if by_id_match:
150 if by_id_match:
151 repo_name = by_id_match.repo_name
151 repo_name = by_id_match.repo_name
152 match_dict['repo_name'] = repo_name
152 match_dict['repo_name'] = repo_name
153 return True
153 return True
154
154
155 return False
155 return False
156
156
157 def check_group(environ, match_dict):
157 def check_group(environ, match_dict):
158 """
158 """
159 check for valid repository group path for proper 404 handling
159 check for valid repository group path for proper 404 handling
160
160
161 :param environ:
161 :param environ:
162 :param match_dict:
162 :param match_dict:
163 """
163 """
164 repo_group_name = match_dict.get('group_name')
164 repo_group_name = match_dict.get('group_name')
165 repo_group_model = repo_group.RepoGroupModel()
165 repo_group_model = repo_group.RepoGroupModel()
166 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
166 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
167 if by_name_match:
167 if by_name_match:
168 return True
168 return True
169
169
170 return False
170 return False
171
171
172 def check_user_group(environ, match_dict):
172 def check_user_group(environ, match_dict):
173 """
173 """
174 check for valid user group for proper 404 handling
174 check for valid user group for proper 404 handling
175
175
176 :param environ:
176 :param environ:
177 :param match_dict:
177 :param match_dict:
178 """
178 """
179 return True
179 return True
180
180
181 def check_int(environ, match_dict):
181 def check_int(environ, match_dict):
182 return match_dict.get('id').isdigit()
182 return match_dict.get('id').isdigit()
183
183
184
184
185 #==========================================================================
185 #==========================================================================
186 # CUSTOM ROUTES HERE
186 # CUSTOM ROUTES HERE
187 #==========================================================================
187 #==========================================================================
188
188
189 # MAIN PAGE
189 # MAIN PAGE
190 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
190 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
191 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
191 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
192 action='goto_switcher_data')
192 action='goto_switcher_data')
193 rmap.connect('repo_list_data', '/_repos', controller='home',
193 rmap.connect('repo_list_data', '/_repos', controller='home',
194 action='repo_list_data')
194 action='repo_list_data')
195
195
196 rmap.connect('user_autocomplete_data', '/_users', controller='home',
196 rmap.connect('user_autocomplete_data', '/_users', controller='home',
197 action='user_autocomplete_data', jsroute=True)
197 action='user_autocomplete_data', jsroute=True)
198 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
198 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
199 action='user_group_autocomplete_data', jsroute=True)
199 action='user_group_autocomplete_data', jsroute=True)
200
200
201 rmap.connect(
201 rmap.connect(
202 'user_profile', '/_profiles/{username}', controller='users',
202 'user_profile', '/_profiles/{username}', controller='users',
203 action='user_profile')
203 action='user_profile')
204
204
205 # TODO: johbo: Static links, to be replaced by our redirection mechanism
205 # TODO: johbo: Static links, to be replaced by our redirection mechanism
206 rmap.connect('rst_help',
206 rmap.connect('rst_help',
207 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
207 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
208 _static=True)
208 _static=True)
209 rmap.connect('markdown_help',
209 rmap.connect('markdown_help',
210 'http://daringfireball.net/projects/markdown/syntax',
210 'http://daringfireball.net/projects/markdown/syntax',
211 _static=True)
211 _static=True)
212 rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True)
212 rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True)
213 rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True)
213 rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True)
214 rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True)
214 rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True)
215 # TODO: anderson - making this a static link since redirect won't play
215 # TODO: anderson - making this a static link since redirect won't play
216 # nice with POST requests
216 # nice with POST requests
217 rmap.connect('enterprise_license_convert_from_old',
217 rmap.connect('enterprise_license_convert_from_old',
218 'https://rhodecode.com/u/license-upgrade',
218 'https://rhodecode.com/u/license-upgrade',
219 _static=True)
219 _static=True)
220
220
221 routing_links.connect_redirection_links(rmap)
221 routing_links.connect_redirection_links(rmap)
222
222
223 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
223 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
224 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
224 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
225
225
226 # ADMIN REPOSITORY ROUTES
226 # ADMIN REPOSITORY ROUTES
227 with rmap.submapper(path_prefix=ADMIN_PREFIX,
227 with rmap.submapper(path_prefix=ADMIN_PREFIX,
228 controller='admin/repos') as m:
228 controller='admin/repos') as m:
229 m.connect('repos', '/repos',
229 m.connect('repos', '/repos',
230 action='create', conditions={'method': ['POST']})
230 action='create', conditions={'method': ['POST']})
231 m.connect('repos', '/repos',
231 m.connect('repos', '/repos',
232 action='index', conditions={'method': ['GET']})
232 action='index', conditions={'method': ['GET']})
233 m.connect('new_repo', '/create_repository', jsroute=True,
233 m.connect('new_repo', '/create_repository', jsroute=True,
234 action='create_repository', conditions={'method': ['GET']})
234 action='create_repository', conditions={'method': ['GET']})
235 m.connect('/repos/{repo_name}',
235 m.connect('/repos/{repo_name}',
236 action='update', conditions={'method': ['PUT'],
236 action='update', conditions={'method': ['PUT'],
237 'function': check_repo},
237 'function': check_repo},
238 requirements=URL_NAME_REQUIREMENTS)
238 requirements=URL_NAME_REQUIREMENTS)
239 m.connect('delete_repo', '/repos/{repo_name}',
239 m.connect('delete_repo', '/repos/{repo_name}',
240 action='delete', conditions={'method': ['DELETE']},
240 action='delete', conditions={'method': ['DELETE']},
241 requirements=URL_NAME_REQUIREMENTS)
241 requirements=URL_NAME_REQUIREMENTS)
242 m.connect('repo', '/repos/{repo_name}',
242 m.connect('repo', '/repos/{repo_name}',
243 action='show', conditions={'method': ['GET'],
243 action='show', conditions={'method': ['GET'],
244 'function': check_repo},
244 'function': check_repo},
245 requirements=URL_NAME_REQUIREMENTS)
245 requirements=URL_NAME_REQUIREMENTS)
246
246
247 # ADMIN REPOSITORY GROUPS ROUTES
247 # ADMIN REPOSITORY GROUPS ROUTES
248 with rmap.submapper(path_prefix=ADMIN_PREFIX,
248 with rmap.submapper(path_prefix=ADMIN_PREFIX,
249 controller='admin/repo_groups') as m:
249 controller='admin/repo_groups') as m:
250 m.connect('repo_groups', '/repo_groups',
250 m.connect('repo_groups', '/repo_groups',
251 action='create', conditions={'method': ['POST']})
251 action='create', conditions={'method': ['POST']})
252 m.connect('repo_groups', '/repo_groups',
252 m.connect('repo_groups', '/repo_groups',
253 action='index', conditions={'method': ['GET']})
253 action='index', conditions={'method': ['GET']})
254 m.connect('new_repo_group', '/repo_groups/new',
254 m.connect('new_repo_group', '/repo_groups/new',
255 action='new', conditions={'method': ['GET']})
255 action='new', conditions={'method': ['GET']})
256 m.connect('update_repo_group', '/repo_groups/{group_name}',
256 m.connect('update_repo_group', '/repo_groups/{group_name}',
257 action='update', conditions={'method': ['PUT'],
257 action='update', conditions={'method': ['PUT'],
258 'function': check_group},
258 'function': check_group},
259 requirements=URL_NAME_REQUIREMENTS)
259 requirements=URL_NAME_REQUIREMENTS)
260
260
261 # EXTRAS REPO GROUP ROUTES
261 # EXTRAS REPO GROUP ROUTES
262 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
262 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
263 action='edit',
263 action='edit',
264 conditions={'method': ['GET'], 'function': check_group},
264 conditions={'method': ['GET'], 'function': check_group},
265 requirements=URL_NAME_REQUIREMENTS)
265 requirements=URL_NAME_REQUIREMENTS)
266 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
266 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
267 action='edit',
267 action='edit',
268 conditions={'method': ['PUT'], 'function': check_group},
268 conditions={'method': ['PUT'], 'function': check_group},
269 requirements=URL_NAME_REQUIREMENTS)
269 requirements=URL_NAME_REQUIREMENTS)
270
270
271 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
271 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
272 action='edit_repo_group_advanced',
272 action='edit_repo_group_advanced',
273 conditions={'method': ['GET'], 'function': check_group},
273 conditions={'method': ['GET'], 'function': check_group},
274 requirements=URL_NAME_REQUIREMENTS)
274 requirements=URL_NAME_REQUIREMENTS)
275 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
275 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
276 action='edit_repo_group_advanced',
276 action='edit_repo_group_advanced',
277 conditions={'method': ['PUT'], 'function': check_group},
277 conditions={'method': ['PUT'], 'function': check_group},
278 requirements=URL_NAME_REQUIREMENTS)
278 requirements=URL_NAME_REQUIREMENTS)
279
279
280 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
280 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
281 action='edit_repo_group_perms',
281 action='edit_repo_group_perms',
282 conditions={'method': ['GET'], 'function': check_group},
282 conditions={'method': ['GET'], 'function': check_group},
283 requirements=URL_NAME_REQUIREMENTS)
283 requirements=URL_NAME_REQUIREMENTS)
284 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
284 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
285 action='update_perms',
285 action='update_perms',
286 conditions={'method': ['PUT'], 'function': check_group},
286 conditions={'method': ['PUT'], 'function': check_group},
287 requirements=URL_NAME_REQUIREMENTS)
287 requirements=URL_NAME_REQUIREMENTS)
288
288
289 m.connect('delete_repo_group', '/repo_groups/{group_name}',
289 m.connect('delete_repo_group', '/repo_groups/{group_name}',
290 action='delete', conditions={'method': ['DELETE'],
290 action='delete', conditions={'method': ['DELETE'],
291 'function': check_group},
291 'function': check_group},
292 requirements=URL_NAME_REQUIREMENTS)
292 requirements=URL_NAME_REQUIREMENTS)
293
293
294 # ADMIN USER ROUTES
294 # ADMIN USER ROUTES
295 with rmap.submapper(path_prefix=ADMIN_PREFIX,
295 with rmap.submapper(path_prefix=ADMIN_PREFIX,
296 controller='admin/users') as m:
296 controller='admin/users') as m:
297 m.connect('users', '/users',
297 m.connect('users', '/users',
298 action='create', conditions={'method': ['POST']})
298 action='create', conditions={'method': ['POST']})
299 m.connect('users', '/users',
299 m.connect('users', '/users',
300 action='index', conditions={'method': ['GET']})
300 action='index', conditions={'method': ['GET']})
301 m.connect('new_user', '/users/new',
301 m.connect('new_user', '/users/new',
302 action='new', conditions={'method': ['GET']})
302 action='new', conditions={'method': ['GET']})
303 m.connect('update_user', '/users/{user_id}',
303 m.connect('update_user', '/users/{user_id}',
304 action='update', conditions={'method': ['PUT']})
304 action='update', conditions={'method': ['PUT']})
305 m.connect('delete_user', '/users/{user_id}',
305 m.connect('delete_user', '/users/{user_id}',
306 action='delete', conditions={'method': ['DELETE']})
306 action='delete', conditions={'method': ['DELETE']})
307 m.connect('edit_user', '/users/{user_id}/edit',
307 m.connect('edit_user', '/users/{user_id}/edit',
308 action='edit', conditions={'method': ['GET']}, jsroute=True)
308 action='edit', conditions={'method': ['GET']}, jsroute=True)
309 m.connect('user', '/users/{user_id}',
309 m.connect('user', '/users/{user_id}',
310 action='show', conditions={'method': ['GET']})
310 action='show', conditions={'method': ['GET']})
311 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
311 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
312 action='reset_password', conditions={'method': ['POST']})
312 action='reset_password', conditions={'method': ['POST']})
313 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
313 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
314 action='create_personal_repo_group', conditions={'method': ['POST']})
314 action='create_personal_repo_group', conditions={'method': ['POST']})
315
315
316 # EXTRAS USER ROUTES
316 # EXTRAS USER ROUTES
317 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
317 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
318 action='edit_advanced', conditions={'method': ['GET']})
318 action='edit_advanced', conditions={'method': ['GET']})
319 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
319 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
320 action='update_advanced', conditions={'method': ['PUT']})
320 action='update_advanced', conditions={'method': ['PUT']})
321
321
322 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
322 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
323 action='edit_auth_tokens', conditions={'method': ['GET']})
323 action='edit_auth_tokens', conditions={'method': ['GET']})
324 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
324 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
325 action='add_auth_token', conditions={'method': ['PUT']})
325 action='add_auth_token', conditions={'method': ['PUT']})
326 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
326 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
327 action='delete_auth_token', conditions={'method': ['DELETE']})
327 action='delete_auth_token', conditions={'method': ['DELETE']})
328
328
329 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
329 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
330 action='edit_global_perms', conditions={'method': ['GET']})
330 action='edit_global_perms', conditions={'method': ['GET']})
331 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
331 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
332 action='update_global_perms', conditions={'method': ['PUT']})
332 action='update_global_perms', conditions={'method': ['PUT']})
333
333
334 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
334 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
335 action='edit_perms_summary', conditions={'method': ['GET']})
335 action='edit_perms_summary', conditions={'method': ['GET']})
336
336
337 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
337 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
338 action='edit_emails', conditions={'method': ['GET']})
338 action='edit_emails', conditions={'method': ['GET']})
339 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
339 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
340 action='add_email', conditions={'method': ['PUT']})
340 action='add_email', conditions={'method': ['PUT']})
341 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
341 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
342 action='delete_email', conditions={'method': ['DELETE']})
342 action='delete_email', conditions={'method': ['DELETE']})
343
343
344 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
344 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
345 action='edit_ips', conditions={'method': ['GET']})
345 action='edit_ips', conditions={'method': ['GET']})
346 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
346 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
347 action='add_ip', conditions={'method': ['PUT']})
347 action='add_ip', conditions={'method': ['PUT']})
348 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
348 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
349 action='delete_ip', conditions={'method': ['DELETE']})
349 action='delete_ip', conditions={'method': ['DELETE']})
350
350
351 # ADMIN USER GROUPS REST ROUTES
351 # ADMIN USER GROUPS REST ROUTES
352 with rmap.submapper(path_prefix=ADMIN_PREFIX,
352 with rmap.submapper(path_prefix=ADMIN_PREFIX,
353 controller='admin/user_groups') as m:
353 controller='admin/user_groups') as m:
354 m.connect('users_groups', '/user_groups',
354 m.connect('users_groups', '/user_groups',
355 action='create', conditions={'method': ['POST']})
355 action='create', conditions={'method': ['POST']})
356 m.connect('users_groups', '/user_groups',
356 m.connect('users_groups', '/user_groups',
357 action='index', conditions={'method': ['GET']})
357 action='index', conditions={'method': ['GET']})
358 m.connect('new_users_group', '/user_groups/new',
358 m.connect('new_users_group', '/user_groups/new',
359 action='new', conditions={'method': ['GET']})
359 action='new', conditions={'method': ['GET']})
360 m.connect('update_users_group', '/user_groups/{user_group_id}',
360 m.connect('update_users_group', '/user_groups/{user_group_id}',
361 action='update', conditions={'method': ['PUT']})
361 action='update', conditions={'method': ['PUT']})
362 m.connect('delete_users_group', '/user_groups/{user_group_id}',
362 m.connect('delete_users_group', '/user_groups/{user_group_id}',
363 action='delete', conditions={'method': ['DELETE']})
363 action='delete', conditions={'method': ['DELETE']})
364 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
364 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
365 action='edit', conditions={'method': ['GET']},
365 action='edit', conditions={'method': ['GET']},
366 function=check_user_group)
366 function=check_user_group)
367
367
368 # EXTRAS USER GROUP ROUTES
368 # EXTRAS USER GROUP ROUTES
369 m.connect('edit_user_group_global_perms',
369 m.connect('edit_user_group_global_perms',
370 '/user_groups/{user_group_id}/edit/global_permissions',
370 '/user_groups/{user_group_id}/edit/global_permissions',
371 action='edit_global_perms', conditions={'method': ['GET']})
371 action='edit_global_perms', conditions={'method': ['GET']})
372 m.connect('edit_user_group_global_perms',
372 m.connect('edit_user_group_global_perms',
373 '/user_groups/{user_group_id}/edit/global_permissions',
373 '/user_groups/{user_group_id}/edit/global_permissions',
374 action='update_global_perms', conditions={'method': ['PUT']})
374 action='update_global_perms', conditions={'method': ['PUT']})
375 m.connect('edit_user_group_perms_summary',
375 m.connect('edit_user_group_perms_summary',
376 '/user_groups/{user_group_id}/edit/permissions_summary',
376 '/user_groups/{user_group_id}/edit/permissions_summary',
377 action='edit_perms_summary', conditions={'method': ['GET']})
377 action='edit_perms_summary', conditions={'method': ['GET']})
378
378
379 m.connect('edit_user_group_perms',
379 m.connect('edit_user_group_perms',
380 '/user_groups/{user_group_id}/edit/permissions',
380 '/user_groups/{user_group_id}/edit/permissions',
381 action='edit_perms', conditions={'method': ['GET']})
381 action='edit_perms', conditions={'method': ['GET']})
382 m.connect('edit_user_group_perms',
382 m.connect('edit_user_group_perms',
383 '/user_groups/{user_group_id}/edit/permissions',
383 '/user_groups/{user_group_id}/edit/permissions',
384 action='update_perms', conditions={'method': ['PUT']})
384 action='update_perms', conditions={'method': ['PUT']})
385
385
386 m.connect('edit_user_group_advanced',
386 m.connect('edit_user_group_advanced',
387 '/user_groups/{user_group_id}/edit/advanced',
387 '/user_groups/{user_group_id}/edit/advanced',
388 action='edit_advanced', conditions={'method': ['GET']})
388 action='edit_advanced', conditions={'method': ['GET']})
389
389
390 m.connect('edit_user_group_members',
390 m.connect('edit_user_group_members',
391 '/user_groups/{user_group_id}/edit/members', jsroute=True,
391 '/user_groups/{user_group_id}/edit/members', jsroute=True,
392 action='user_group_members', conditions={'method': ['GET']})
392 action='user_group_members', conditions={'method': ['GET']})
393
393
394 # ADMIN PERMISSIONS ROUTES
394 # ADMIN PERMISSIONS ROUTES
395 with rmap.submapper(path_prefix=ADMIN_PREFIX,
395 with rmap.submapper(path_prefix=ADMIN_PREFIX,
396 controller='admin/permissions') as m:
396 controller='admin/permissions') as m:
397 m.connect('admin_permissions_application', '/permissions/application',
397 m.connect('admin_permissions_application', '/permissions/application',
398 action='permission_application_update', conditions={'method': ['POST']})
398 action='permission_application_update', conditions={'method': ['POST']})
399 m.connect('admin_permissions_application', '/permissions/application',
399 m.connect('admin_permissions_application', '/permissions/application',
400 action='permission_application', conditions={'method': ['GET']})
400 action='permission_application', conditions={'method': ['GET']})
401
401
402 m.connect('admin_permissions_global', '/permissions/global',
402 m.connect('admin_permissions_global', '/permissions/global',
403 action='permission_global_update', conditions={'method': ['POST']})
403 action='permission_global_update', conditions={'method': ['POST']})
404 m.connect('admin_permissions_global', '/permissions/global',
404 m.connect('admin_permissions_global', '/permissions/global',
405 action='permission_global', conditions={'method': ['GET']})
405 action='permission_global', conditions={'method': ['GET']})
406
406
407 m.connect('admin_permissions_object', '/permissions/object',
407 m.connect('admin_permissions_object', '/permissions/object',
408 action='permission_objects_update', conditions={'method': ['POST']})
408 action='permission_objects_update', conditions={'method': ['POST']})
409 m.connect('admin_permissions_object', '/permissions/object',
409 m.connect('admin_permissions_object', '/permissions/object',
410 action='permission_objects', conditions={'method': ['GET']})
410 action='permission_objects', conditions={'method': ['GET']})
411
411
412 m.connect('admin_permissions_ips', '/permissions/ips',
412 m.connect('admin_permissions_ips', '/permissions/ips',
413 action='permission_ips', conditions={'method': ['POST']})
413 action='permission_ips', conditions={'method': ['POST']})
414 m.connect('admin_permissions_ips', '/permissions/ips',
414 m.connect('admin_permissions_ips', '/permissions/ips',
415 action='permission_ips', conditions={'method': ['GET']})
415 action='permission_ips', conditions={'method': ['GET']})
416
416
417 m.connect('admin_permissions_overview', '/permissions/overview',
417 m.connect('admin_permissions_overview', '/permissions/overview',
418 action='permission_perms', conditions={'method': ['GET']})
418 action='permission_perms', conditions={'method': ['GET']})
419
419
420 # ADMIN DEFAULTS REST ROUTES
420 # ADMIN DEFAULTS REST ROUTES
421 with rmap.submapper(path_prefix=ADMIN_PREFIX,
421 with rmap.submapper(path_prefix=ADMIN_PREFIX,
422 controller='admin/defaults') as m:
422 controller='admin/defaults') as m:
423 m.connect('admin_defaults_repositories', '/defaults/repositories',
423 m.connect('admin_defaults_repositories', '/defaults/repositories',
424 action='update_repository_defaults', conditions={'method': ['POST']})
424 action='update_repository_defaults', conditions={'method': ['POST']})
425 m.connect('admin_defaults_repositories', '/defaults/repositories',
425 m.connect('admin_defaults_repositories', '/defaults/repositories',
426 action='index', conditions={'method': ['GET']})
426 action='index', conditions={'method': ['GET']})
427
427
428 # ADMIN DEBUG STYLE ROUTES
428 # ADMIN DEBUG STYLE ROUTES
429 if str2bool(config.get('debug_style')):
429 if str2bool(config.get('debug_style')):
430 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
430 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
431 controller='debug_style') as m:
431 controller='debug_style') as m:
432 m.connect('debug_style_home', '',
432 m.connect('debug_style_home', '',
433 action='index', conditions={'method': ['GET']})
433 action='index', conditions={'method': ['GET']})
434 m.connect('debug_style_template', '/t/{t_path}',
434 m.connect('debug_style_template', '/t/{t_path}',
435 action='template', conditions={'method': ['GET']})
435 action='template', conditions={'method': ['GET']})
436
436
437 # ADMIN SETTINGS ROUTES
437 # ADMIN SETTINGS ROUTES
438 with rmap.submapper(path_prefix=ADMIN_PREFIX,
438 with rmap.submapper(path_prefix=ADMIN_PREFIX,
439 controller='admin/settings') as m:
439 controller='admin/settings') as m:
440
440
441 # default
441 # default
442 m.connect('admin_settings', '/settings',
442 m.connect('admin_settings', '/settings',
443 action='settings_global_update',
443 action='settings_global_update',
444 conditions={'method': ['POST']})
444 conditions={'method': ['POST']})
445 m.connect('admin_settings', '/settings',
445 m.connect('admin_settings', '/settings',
446 action='settings_global', conditions={'method': ['GET']})
446 action='settings_global', conditions={'method': ['GET']})
447
447
448 m.connect('admin_settings_vcs', '/settings/vcs',
448 m.connect('admin_settings_vcs', '/settings/vcs',
449 action='settings_vcs_update',
449 action='settings_vcs_update',
450 conditions={'method': ['POST']})
450 conditions={'method': ['POST']})
451 m.connect('admin_settings_vcs', '/settings/vcs',
451 m.connect('admin_settings_vcs', '/settings/vcs',
452 action='settings_vcs',
452 action='settings_vcs',
453 conditions={'method': ['GET']})
453 conditions={'method': ['GET']})
454 m.connect('admin_settings_vcs', '/settings/vcs',
454 m.connect('admin_settings_vcs', '/settings/vcs',
455 action='delete_svn_pattern',
455 action='delete_svn_pattern',
456 conditions={'method': ['DELETE']})
456 conditions={'method': ['DELETE']})
457
457
458 m.connect('admin_settings_mapping', '/settings/mapping',
458 m.connect('admin_settings_mapping', '/settings/mapping',
459 action='settings_mapping_update',
459 action='settings_mapping_update',
460 conditions={'method': ['POST']})
460 conditions={'method': ['POST']})
461 m.connect('admin_settings_mapping', '/settings/mapping',
461 m.connect('admin_settings_mapping', '/settings/mapping',
462 action='settings_mapping', conditions={'method': ['GET']})
462 action='settings_mapping', conditions={'method': ['GET']})
463
463
464 m.connect('admin_settings_global', '/settings/global',
464 m.connect('admin_settings_global', '/settings/global',
465 action='settings_global_update',
465 action='settings_global_update',
466 conditions={'method': ['POST']})
466 conditions={'method': ['POST']})
467 m.connect('admin_settings_global', '/settings/global',
467 m.connect('admin_settings_global', '/settings/global',
468 action='settings_global', conditions={'method': ['GET']})
468 action='settings_global', conditions={'method': ['GET']})
469
469
470 m.connect('admin_settings_visual', '/settings/visual',
470 m.connect('admin_settings_visual', '/settings/visual',
471 action='settings_visual_update',
471 action='settings_visual_update',
472 conditions={'method': ['POST']})
472 conditions={'method': ['POST']})
473 m.connect('admin_settings_visual', '/settings/visual',
473 m.connect('admin_settings_visual', '/settings/visual',
474 action='settings_visual', conditions={'method': ['GET']})
474 action='settings_visual', conditions={'method': ['GET']})
475
475
476 m.connect('admin_settings_issuetracker',
476 m.connect('admin_settings_issuetracker',
477 '/settings/issue-tracker', action='settings_issuetracker',
477 '/settings/issue-tracker', action='settings_issuetracker',
478 conditions={'method': ['GET']})
478 conditions={'method': ['GET']})
479 m.connect('admin_settings_issuetracker_save',
479 m.connect('admin_settings_issuetracker_save',
480 '/settings/issue-tracker/save',
480 '/settings/issue-tracker/save',
481 action='settings_issuetracker_save',
481 action='settings_issuetracker_save',
482 conditions={'method': ['POST']})
482 conditions={'method': ['POST']})
483 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
483 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
484 action='settings_issuetracker_test',
484 action='settings_issuetracker_test',
485 conditions={'method': ['POST']})
485 conditions={'method': ['POST']})
486 m.connect('admin_issuetracker_delete',
486 m.connect('admin_issuetracker_delete',
487 '/settings/issue-tracker/delete',
487 '/settings/issue-tracker/delete',
488 action='settings_issuetracker_delete',
488 action='settings_issuetracker_delete',
489 conditions={'method': ['DELETE']})
489 conditions={'method': ['DELETE']})
490
490
491 m.connect('admin_settings_email', '/settings/email',
491 m.connect('admin_settings_email', '/settings/email',
492 action='settings_email_update',
492 action='settings_email_update',
493 conditions={'method': ['POST']})
493 conditions={'method': ['POST']})
494 m.connect('admin_settings_email', '/settings/email',
494 m.connect('admin_settings_email', '/settings/email',
495 action='settings_email', conditions={'method': ['GET']})
495 action='settings_email', conditions={'method': ['GET']})
496
496
497 m.connect('admin_settings_hooks', '/settings/hooks',
497 m.connect('admin_settings_hooks', '/settings/hooks',
498 action='settings_hooks_update',
498 action='settings_hooks_update',
499 conditions={'method': ['POST', 'DELETE']})
499 conditions={'method': ['POST', 'DELETE']})
500 m.connect('admin_settings_hooks', '/settings/hooks',
500 m.connect('admin_settings_hooks', '/settings/hooks',
501 action='settings_hooks', conditions={'method': ['GET']})
501 action='settings_hooks', conditions={'method': ['GET']})
502
502
503 m.connect('admin_settings_search', '/settings/search',
503 m.connect('admin_settings_search', '/settings/search',
504 action='settings_search', conditions={'method': ['GET']})
504 action='settings_search', conditions={'method': ['GET']})
505
505
506 m.connect('admin_settings_supervisor', '/settings/supervisor',
506 m.connect('admin_settings_supervisor', '/settings/supervisor',
507 action='settings_supervisor', conditions={'method': ['GET']})
507 action='settings_supervisor', conditions={'method': ['GET']})
508 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
508 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
509 action='settings_supervisor_log', conditions={'method': ['GET']})
509 action='settings_supervisor_log', conditions={'method': ['GET']})
510
510
511 m.connect('admin_settings_labs', '/settings/labs',
511 m.connect('admin_settings_labs', '/settings/labs',
512 action='settings_labs_update',
512 action='settings_labs_update',
513 conditions={'method': ['POST']})
513 conditions={'method': ['POST']})
514 m.connect('admin_settings_labs', '/settings/labs',
514 m.connect('admin_settings_labs', '/settings/labs',
515 action='settings_labs', conditions={'method': ['GET']})
515 action='settings_labs', conditions={'method': ['GET']})
516
516
517 # ADMIN MY ACCOUNT
517 # ADMIN MY ACCOUNT
518 with rmap.submapper(path_prefix=ADMIN_PREFIX,
518 with rmap.submapper(path_prefix=ADMIN_PREFIX,
519 controller='admin/my_account') as m:
519 controller='admin/my_account') as m:
520
520
521 m.connect('my_account', '/my_account',
521 m.connect('my_account', '/my_account',
522 action='my_account', conditions={'method': ['GET']})
522 action='my_account', conditions={'method': ['GET']})
523 m.connect('my_account_edit', '/my_account/edit',
523 m.connect('my_account_edit', '/my_account/edit',
524 action='my_account_edit', conditions={'method': ['GET']})
524 action='my_account_edit', conditions={'method': ['GET']})
525 m.connect('my_account', '/my_account',
525 m.connect('my_account', '/my_account',
526 action='my_account_update', conditions={'method': ['POST']})
526 action='my_account_update', conditions={'method': ['POST']})
527
527
528 m.connect('my_account_password', '/my_account/password',
528 m.connect('my_account_password', '/my_account/password',
529 action='my_account_password', conditions={'method': ['GET', 'POST']})
529 action='my_account_password', conditions={'method': ['GET', 'POST']})
530
530
531 m.connect('my_account_repos', '/my_account/repos',
531 m.connect('my_account_repos', '/my_account/repos',
532 action='my_account_repos', conditions={'method': ['GET']})
532 action='my_account_repos', conditions={'method': ['GET']})
533
533
534 m.connect('my_account_watched', '/my_account/watched',
534 m.connect('my_account_watched', '/my_account/watched',
535 action='my_account_watched', conditions={'method': ['GET']})
535 action='my_account_watched', conditions={'method': ['GET']})
536
536
537 m.connect('my_account_pullrequests', '/my_account/pull_requests',
537 m.connect('my_account_pullrequests', '/my_account/pull_requests',
538 action='my_account_pullrequests', conditions={'method': ['GET']})
538 action='my_account_pullrequests', conditions={'method': ['GET']})
539
539
540 m.connect('my_account_perms', '/my_account/perms',
540 m.connect('my_account_perms', '/my_account/perms',
541 action='my_account_perms', conditions={'method': ['GET']})
541 action='my_account_perms', conditions={'method': ['GET']})
542
542
543 m.connect('my_account_emails', '/my_account/emails',
543 m.connect('my_account_emails', '/my_account/emails',
544 action='my_account_emails', conditions={'method': ['GET']})
544 action='my_account_emails', conditions={'method': ['GET']})
545 m.connect('my_account_emails', '/my_account/emails',
545 m.connect('my_account_emails', '/my_account/emails',
546 action='my_account_emails_add', conditions={'method': ['POST']})
546 action='my_account_emails_add', conditions={'method': ['POST']})
547 m.connect('my_account_emails', '/my_account/emails',
547 m.connect('my_account_emails', '/my_account/emails',
548 action='my_account_emails_delete', conditions={'method': ['DELETE']})
548 action='my_account_emails_delete', conditions={'method': ['DELETE']})
549
549
550 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
550 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
551 action='my_account_auth_tokens', conditions={'method': ['GET']})
551 action='my_account_auth_tokens', conditions={'method': ['GET']})
552 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
552 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
553 action='my_account_auth_tokens_add', conditions={'method': ['POST']})
553 action='my_account_auth_tokens_add', conditions={'method': ['POST']})
554 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
554 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
555 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
555 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
556 m.connect('my_account_notifications', '/my_account/notifications',
556 m.connect('my_account_notifications', '/my_account/notifications',
557 action='my_notifications',
557 action='my_notifications',
558 conditions={'method': ['GET']})
558 conditions={'method': ['GET']})
559 m.connect('my_account_notifications_toggle_visibility',
559 m.connect('my_account_notifications_toggle_visibility',
560 '/my_account/toggle_visibility',
560 '/my_account/toggle_visibility',
561 action='my_notifications_toggle_visibility',
561 action='my_notifications_toggle_visibility',
562 conditions={'method': ['POST']})
562 conditions={'method': ['POST']})
563 m.connect('my_account_notifications_test_channelstream',
563 m.connect('my_account_notifications_test_channelstream',
564 '/my_account/test_channelstream',
564 '/my_account/test_channelstream',
565 action='my_account_notifications_test_channelstream',
565 action='my_account_notifications_test_channelstream',
566 conditions={'method': ['POST']})
566 conditions={'method': ['POST']})
567
567
568 # NOTIFICATION REST ROUTES
568 # NOTIFICATION REST ROUTES
569 with rmap.submapper(path_prefix=ADMIN_PREFIX,
569 with rmap.submapper(path_prefix=ADMIN_PREFIX,
570 controller='admin/notifications') as m:
570 controller='admin/notifications') as m:
571 m.connect('notifications', '/notifications',
571 m.connect('notifications', '/notifications',
572 action='index', conditions={'method': ['GET']})
572 action='index', conditions={'method': ['GET']})
573 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
573 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
574 action='mark_all_read', conditions={'method': ['POST']})
574 action='mark_all_read', conditions={'method': ['POST']})
575 m.connect('/notifications/{notification_id}',
575 m.connect('/notifications/{notification_id}',
576 action='update', conditions={'method': ['PUT']})
576 action='update', conditions={'method': ['PUT']})
577 m.connect('/notifications/{notification_id}',
577 m.connect('/notifications/{notification_id}',
578 action='delete', conditions={'method': ['DELETE']})
578 action='delete', conditions={'method': ['DELETE']})
579 m.connect('notification', '/notifications/{notification_id}',
579 m.connect('notification', '/notifications/{notification_id}',
580 action='show', conditions={'method': ['GET']})
580 action='show', conditions={'method': ['GET']})
581
581
582 # ADMIN GIST
582 # ADMIN GIST
583 with rmap.submapper(path_prefix=ADMIN_PREFIX,
583 with rmap.submapper(path_prefix=ADMIN_PREFIX,
584 controller='admin/gists') as m:
584 controller='admin/gists') as m:
585 m.connect('gists', '/gists',
585 m.connect('gists', '/gists',
586 action='create', conditions={'method': ['POST']})
586 action='create', conditions={'method': ['POST']})
587 m.connect('gists', '/gists', jsroute=True,
587 m.connect('gists', '/gists', jsroute=True,
588 action='index', conditions={'method': ['GET']})
588 action='index', conditions={'method': ['GET']})
589 m.connect('new_gist', '/gists/new', jsroute=True,
589 m.connect('new_gist', '/gists/new', jsroute=True,
590 action='new', conditions={'method': ['GET']})
590 action='new', conditions={'method': ['GET']})
591
591
592 m.connect('/gists/{gist_id}',
592 m.connect('/gists/{gist_id}',
593 action='delete', conditions={'method': ['DELETE']})
593 action='delete', conditions={'method': ['DELETE']})
594 m.connect('edit_gist', '/gists/{gist_id}/edit',
594 m.connect('edit_gist', '/gists/{gist_id}/edit',
595 action='edit_form', conditions={'method': ['GET']})
595 action='edit_form', conditions={'method': ['GET']})
596 m.connect('edit_gist', '/gists/{gist_id}/edit',
596 m.connect('edit_gist', '/gists/{gist_id}/edit',
597 action='edit', conditions={'method': ['POST']})
597 action='edit', conditions={'method': ['POST']})
598 m.connect(
598 m.connect(
599 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
599 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
600 action='check_revision', conditions={'method': ['GET']})
600 action='check_revision', conditions={'method': ['GET']})
601
601
602 m.connect('gist', '/gists/{gist_id}',
602 m.connect('gist', '/gists/{gist_id}',
603 action='show', conditions={'method': ['GET']})
603 action='show', conditions={'method': ['GET']})
604 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
604 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
605 revision='tip',
605 revision='tip',
606 action='show', conditions={'method': ['GET']})
606 action='show', conditions={'method': ['GET']})
607 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
607 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
608 revision='tip',
608 revision='tip',
609 action='show', conditions={'method': ['GET']})
609 action='show', conditions={'method': ['GET']})
610 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
610 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
611 revision='tip',
611 revision='tip',
612 action='show', conditions={'method': ['GET']},
612 action='show', conditions={'method': ['GET']},
613 requirements=URL_NAME_REQUIREMENTS)
613 requirements=URL_NAME_REQUIREMENTS)
614
614
615 # ADMIN MAIN PAGES
615 # ADMIN MAIN PAGES
616 with rmap.submapper(path_prefix=ADMIN_PREFIX,
616 with rmap.submapper(path_prefix=ADMIN_PREFIX,
617 controller='admin/admin') as m:
617 controller='admin/admin') as m:
618 m.connect('admin_home', '', action='index')
618 m.connect('admin_home', '', action='index')
619 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
619 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
620 action='add_repo')
620 action='add_repo')
621 m.connect(
621 m.connect(
622 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
622 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
623 action='pull_requests')
623 action='pull_requests')
624 m.connect(
624 m.connect(
625 'pull_requests_global_1', '/pull-requests/{pull_request_id:[0-9]+}',
625 'pull_requests_global_1', '/pull-requests/{pull_request_id:[0-9]+}',
626 action='pull_requests')
626 action='pull_requests')
627 m.connect(
627 m.connect(
628 'pull_requests_global', '/pull-request/{pull_request_id:[0-9]+}',
628 'pull_requests_global', '/pull-request/{pull_request_id:[0-9]+}',
629 action='pull_requests')
629 action='pull_requests')
630
630
631 # USER JOURNAL
631 # USER JOURNAL
632 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
632 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
633 controller='journal', action='index')
633 controller='journal', action='index')
634 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
634 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
635 controller='journal', action='journal_rss')
635 controller='journal', action='journal_rss')
636 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
636 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
637 controller='journal', action='journal_atom')
637 controller='journal', action='journal_atom')
638
638
639 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
639 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
640 controller='journal', action='public_journal')
640 controller='journal', action='public_journal')
641
641
642 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
642 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
643 controller='journal', action='public_journal_rss')
643 controller='journal', action='public_journal_rss')
644
644
645 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
645 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
646 controller='journal', action='public_journal_rss')
646 controller='journal', action='public_journal_rss')
647
647
648 rmap.connect('public_journal_atom',
648 rmap.connect('public_journal_atom',
649 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
649 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
650 action='public_journal_atom')
650 action='public_journal_atom')
651
651
652 rmap.connect('public_journal_atom_old',
652 rmap.connect('public_journal_atom_old',
653 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
653 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
654 action='public_journal_atom')
654 action='public_journal_atom')
655
655
656 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
656 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
657 controller='journal', action='toggle_following', jsroute=True,
657 controller='journal', action='toggle_following', jsroute=True,
658 conditions={'method': ['POST']})
658 conditions={'method': ['POST']})
659
659
660 # FULL TEXT SEARCH
660 # FULL TEXT SEARCH
661 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
661 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
662 controller='search')
662 controller='search')
663 rmap.connect('search_repo_home', '/{repo_name}/search',
663 rmap.connect('search_repo_home', '/{repo_name}/search',
664 controller='search',
664 controller='search',
665 action='index',
665 action='index',
666 conditions={'function': check_repo},
666 conditions={'function': check_repo},
667 requirements=URL_NAME_REQUIREMENTS)
667 requirements=URL_NAME_REQUIREMENTS)
668
668
669 # FEEDS
669 # FEEDS
670 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
670 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
671 controller='feed', action='rss',
671 controller='feed', action='rss',
672 conditions={'function': check_repo},
672 conditions={'function': check_repo},
673 requirements=URL_NAME_REQUIREMENTS)
673 requirements=URL_NAME_REQUIREMENTS)
674
674
675 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
675 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
676 controller='feed', action='atom',
676 controller='feed', action='atom',
677 conditions={'function': check_repo},
677 conditions={'function': check_repo},
678 requirements=URL_NAME_REQUIREMENTS)
678 requirements=URL_NAME_REQUIREMENTS)
679
679
680 #==========================================================================
680 #==========================================================================
681 # REPOSITORY ROUTES
681 # REPOSITORY ROUTES
682 #==========================================================================
682 #==========================================================================
683
683
684 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
684 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
685 controller='admin/repos', action='repo_creating',
685 controller='admin/repos', action='repo_creating',
686 requirements=URL_NAME_REQUIREMENTS)
686 requirements=URL_NAME_REQUIREMENTS)
687 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
687 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
688 controller='admin/repos', action='repo_check',
688 controller='admin/repos', action='repo_check',
689 requirements=URL_NAME_REQUIREMENTS)
689 requirements=URL_NAME_REQUIREMENTS)
690
690
691 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
691 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
692 controller='summary', action='repo_stats',
692 controller='summary', action='repo_stats',
693 conditions={'function': check_repo},
693 conditions={'function': check_repo},
694 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
694 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
695
695
696 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
696 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
697 controller='summary', action='repo_refs_data', jsroute=True,
697 controller='summary', action='repo_refs_data',
698 requirements=URL_NAME_REQUIREMENTS)
698 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
699 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
699 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
700 controller='summary', action='repo_refs_changelog_data',
700 controller='summary', action='repo_refs_changelog_data',
701 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
701 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
702 rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers',
702 rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers',
703 controller='summary', action='repo_default_reviewers_data',
703 controller='summary', action='repo_default_reviewers_data',
704 jsroute=True, requirements=URL_NAME_REQUIREMENTS)
704 jsroute=True, requirements=URL_NAME_REQUIREMENTS)
705
705
706 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
706 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
707 controller='changeset', revision='tip', jsroute=True,
707 controller='changeset', revision='tip',
708 conditions={'function': check_repo},
708 conditions={'function': check_repo},
709 requirements=URL_NAME_REQUIREMENTS)
709 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
710 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
710 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
711 controller='changeset', revision='tip', action='changeset_children',
711 controller='changeset', revision='tip', action='changeset_children',
712 conditions={'function': check_repo},
712 conditions={'function': check_repo},
713 requirements=URL_NAME_REQUIREMENTS)
713 requirements=URL_NAME_REQUIREMENTS)
714 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
714 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
715 controller='changeset', revision='tip', action='changeset_parents',
715 controller='changeset', revision='tip', action='changeset_parents',
716 conditions={'function': check_repo},
716 conditions={'function': check_repo},
717 requirements=URL_NAME_REQUIREMENTS)
717 requirements=URL_NAME_REQUIREMENTS)
718
718
719 # repo edit options
719 # repo edit options
720 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
720 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
721 controller='admin/repos', action='edit',
721 controller='admin/repos', action='edit',
722 conditions={'method': ['GET'], 'function': check_repo},
722 conditions={'method': ['GET'], 'function': check_repo},
723 requirements=URL_NAME_REQUIREMENTS)
723 requirements=URL_NAME_REQUIREMENTS)
724
724
725 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
725 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
726 jsroute=True,
726 jsroute=True,
727 controller='admin/repos', action='edit_permissions',
727 controller='admin/repos', action='edit_permissions',
728 conditions={'method': ['GET'], 'function': check_repo},
728 conditions={'method': ['GET'], 'function': check_repo},
729 requirements=URL_NAME_REQUIREMENTS)
729 requirements=URL_NAME_REQUIREMENTS)
730 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
730 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
731 controller='admin/repos', action='edit_permissions_update',
731 controller='admin/repos', action='edit_permissions_update',
732 conditions={'method': ['PUT'], 'function': check_repo},
732 conditions={'method': ['PUT'], 'function': check_repo},
733 requirements=URL_NAME_REQUIREMENTS)
733 requirements=URL_NAME_REQUIREMENTS)
734
734
735 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
735 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
736 controller='admin/repos', action='edit_fields',
736 controller='admin/repos', action='edit_fields',
737 conditions={'method': ['GET'], 'function': check_repo},
737 conditions={'method': ['GET'], 'function': check_repo},
738 requirements=URL_NAME_REQUIREMENTS)
738 requirements=URL_NAME_REQUIREMENTS)
739 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
739 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
740 controller='admin/repos', action='create_repo_field',
740 controller='admin/repos', action='create_repo_field',
741 conditions={'method': ['PUT'], 'function': check_repo},
741 conditions={'method': ['PUT'], 'function': check_repo},
742 requirements=URL_NAME_REQUIREMENTS)
742 requirements=URL_NAME_REQUIREMENTS)
743 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
743 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
744 controller='admin/repos', action='delete_repo_field',
744 controller='admin/repos', action='delete_repo_field',
745 conditions={'method': ['DELETE'], 'function': check_repo},
745 conditions={'method': ['DELETE'], 'function': check_repo},
746 requirements=URL_NAME_REQUIREMENTS)
746 requirements=URL_NAME_REQUIREMENTS)
747
747
748 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
748 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
749 controller='admin/repos', action='edit_advanced',
749 controller='admin/repos', action='edit_advanced',
750 conditions={'method': ['GET'], 'function': check_repo},
750 conditions={'method': ['GET'], 'function': check_repo},
751 requirements=URL_NAME_REQUIREMENTS)
751 requirements=URL_NAME_REQUIREMENTS)
752
752
753 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
753 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
754 controller='admin/repos', action='edit_advanced_locking',
754 controller='admin/repos', action='edit_advanced_locking',
755 conditions={'method': ['PUT'], 'function': check_repo},
755 conditions={'method': ['PUT'], 'function': check_repo},
756 requirements=URL_NAME_REQUIREMENTS)
756 requirements=URL_NAME_REQUIREMENTS)
757 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
757 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
758 controller='admin/repos', action='toggle_locking',
758 controller='admin/repos', action='toggle_locking',
759 conditions={'method': ['GET'], 'function': check_repo},
759 conditions={'method': ['GET'], 'function': check_repo},
760 requirements=URL_NAME_REQUIREMENTS)
760 requirements=URL_NAME_REQUIREMENTS)
761
761
762 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
762 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
763 controller='admin/repos', action='edit_advanced_journal',
763 controller='admin/repos', action='edit_advanced_journal',
764 conditions={'method': ['PUT'], 'function': check_repo},
764 conditions={'method': ['PUT'], 'function': check_repo},
765 requirements=URL_NAME_REQUIREMENTS)
765 requirements=URL_NAME_REQUIREMENTS)
766
766
767 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
767 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
768 controller='admin/repos', action='edit_advanced_fork',
768 controller='admin/repos', action='edit_advanced_fork',
769 conditions={'method': ['PUT'], 'function': check_repo},
769 conditions={'method': ['PUT'], 'function': check_repo},
770 requirements=URL_NAME_REQUIREMENTS)
770 requirements=URL_NAME_REQUIREMENTS)
771
771
772 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
772 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
773 controller='admin/repos', action='edit_caches_form',
773 controller='admin/repos', action='edit_caches_form',
774 conditions={'method': ['GET'], 'function': check_repo},
774 conditions={'method': ['GET'], 'function': check_repo},
775 requirements=URL_NAME_REQUIREMENTS)
775 requirements=URL_NAME_REQUIREMENTS)
776 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
776 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
777 controller='admin/repos', action='edit_caches',
777 controller='admin/repos', action='edit_caches',
778 conditions={'method': ['PUT'], 'function': check_repo},
778 conditions={'method': ['PUT'], 'function': check_repo},
779 requirements=URL_NAME_REQUIREMENTS)
779 requirements=URL_NAME_REQUIREMENTS)
780
780
781 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
781 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
782 controller='admin/repos', action='edit_remote_form',
782 controller='admin/repos', action='edit_remote_form',
783 conditions={'method': ['GET'], 'function': check_repo},
783 conditions={'method': ['GET'], 'function': check_repo},
784 requirements=URL_NAME_REQUIREMENTS)
784 requirements=URL_NAME_REQUIREMENTS)
785 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
785 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
786 controller='admin/repos', action='edit_remote',
786 controller='admin/repos', action='edit_remote',
787 conditions={'method': ['PUT'], 'function': check_repo},
787 conditions={'method': ['PUT'], 'function': check_repo},
788 requirements=URL_NAME_REQUIREMENTS)
788 requirements=URL_NAME_REQUIREMENTS)
789
789
790 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
790 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
791 controller='admin/repos', action='edit_statistics_form',
791 controller='admin/repos', action='edit_statistics_form',
792 conditions={'method': ['GET'], 'function': check_repo},
792 conditions={'method': ['GET'], 'function': check_repo},
793 requirements=URL_NAME_REQUIREMENTS)
793 requirements=URL_NAME_REQUIREMENTS)
794 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
794 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
795 controller='admin/repos', action='edit_statistics',
795 controller='admin/repos', action='edit_statistics',
796 conditions={'method': ['PUT'], 'function': check_repo},
796 conditions={'method': ['PUT'], 'function': check_repo},
797 requirements=URL_NAME_REQUIREMENTS)
797 requirements=URL_NAME_REQUIREMENTS)
798 rmap.connect('repo_settings_issuetracker',
798 rmap.connect('repo_settings_issuetracker',
799 '/{repo_name}/settings/issue-tracker',
799 '/{repo_name}/settings/issue-tracker',
800 controller='admin/repos', action='repo_issuetracker',
800 controller='admin/repos', action='repo_issuetracker',
801 conditions={'method': ['GET'], 'function': check_repo},
801 conditions={'method': ['GET'], 'function': check_repo},
802 requirements=URL_NAME_REQUIREMENTS)
802 requirements=URL_NAME_REQUIREMENTS)
803 rmap.connect('repo_issuetracker_test',
803 rmap.connect('repo_issuetracker_test',
804 '/{repo_name}/settings/issue-tracker/test',
804 '/{repo_name}/settings/issue-tracker/test',
805 controller='admin/repos', action='repo_issuetracker_test',
805 controller='admin/repos', action='repo_issuetracker_test',
806 conditions={'method': ['POST'], 'function': check_repo},
806 conditions={'method': ['POST'], 'function': check_repo},
807 requirements=URL_NAME_REQUIREMENTS)
807 requirements=URL_NAME_REQUIREMENTS)
808 rmap.connect('repo_issuetracker_delete',
808 rmap.connect('repo_issuetracker_delete',
809 '/{repo_name}/settings/issue-tracker/delete',
809 '/{repo_name}/settings/issue-tracker/delete',
810 controller='admin/repos', action='repo_issuetracker_delete',
810 controller='admin/repos', action='repo_issuetracker_delete',
811 conditions={'method': ['DELETE'], 'function': check_repo},
811 conditions={'method': ['DELETE'], 'function': check_repo},
812 requirements=URL_NAME_REQUIREMENTS)
812 requirements=URL_NAME_REQUIREMENTS)
813 rmap.connect('repo_issuetracker_save',
813 rmap.connect('repo_issuetracker_save',
814 '/{repo_name}/settings/issue-tracker/save',
814 '/{repo_name}/settings/issue-tracker/save',
815 controller='admin/repos', action='repo_issuetracker_save',
815 controller='admin/repos', action='repo_issuetracker_save',
816 conditions={'method': ['POST'], 'function': check_repo},
816 conditions={'method': ['POST'], 'function': check_repo},
817 requirements=URL_NAME_REQUIREMENTS)
817 requirements=URL_NAME_REQUIREMENTS)
818 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
818 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
819 controller='admin/repos', action='repo_settings_vcs_update',
819 controller='admin/repos', action='repo_settings_vcs_update',
820 conditions={'method': ['POST'], 'function': check_repo},
820 conditions={'method': ['POST'], 'function': check_repo},
821 requirements=URL_NAME_REQUIREMENTS)
821 requirements=URL_NAME_REQUIREMENTS)
822 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
822 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
823 controller='admin/repos', action='repo_settings_vcs',
823 controller='admin/repos', action='repo_settings_vcs',
824 conditions={'method': ['GET'], 'function': check_repo},
824 conditions={'method': ['GET'], 'function': check_repo},
825 requirements=URL_NAME_REQUIREMENTS)
825 requirements=URL_NAME_REQUIREMENTS)
826 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
826 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
827 controller='admin/repos', action='repo_delete_svn_pattern',
827 controller='admin/repos', action='repo_delete_svn_pattern',
828 conditions={'method': ['DELETE'], 'function': check_repo},
828 conditions={'method': ['DELETE'], 'function': check_repo},
829 requirements=URL_NAME_REQUIREMENTS)
829 requirements=URL_NAME_REQUIREMENTS)
830 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
830 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
831 controller='admin/repos', action='repo_settings_pullrequest',
831 controller='admin/repos', action='repo_settings_pullrequest',
832 conditions={'method': ['GET', 'POST'], 'function': check_repo},
832 conditions={'method': ['GET', 'POST'], 'function': check_repo},
833 requirements=URL_NAME_REQUIREMENTS)
833 requirements=URL_NAME_REQUIREMENTS)
834
834
835 # still working url for backward compat.
835 # still working url for backward compat.
836 rmap.connect('raw_changeset_home_depraced',
836 rmap.connect('raw_changeset_home_depraced',
837 '/{repo_name}/raw-changeset/{revision}',
837 '/{repo_name}/raw-changeset/{revision}',
838 controller='changeset', action='changeset_raw',
838 controller='changeset', action='changeset_raw',
839 revision='tip', conditions={'function': check_repo},
839 revision='tip', conditions={'function': check_repo},
840 requirements=URL_NAME_REQUIREMENTS)
840 requirements=URL_NAME_REQUIREMENTS)
841
841
842 # new URLs
842 # new URLs
843 rmap.connect('changeset_raw_home',
843 rmap.connect('changeset_raw_home',
844 '/{repo_name}/changeset-diff/{revision}',
844 '/{repo_name}/changeset-diff/{revision}',
845 controller='changeset', action='changeset_raw',
845 controller='changeset', action='changeset_raw',
846 revision='tip', conditions={'function': check_repo},
846 revision='tip', conditions={'function': check_repo},
847 requirements=URL_NAME_REQUIREMENTS)
847 requirements=URL_NAME_REQUIREMENTS)
848
848
849 rmap.connect('changeset_patch_home',
849 rmap.connect('changeset_patch_home',
850 '/{repo_name}/changeset-patch/{revision}',
850 '/{repo_name}/changeset-patch/{revision}',
851 controller='changeset', action='changeset_patch',
851 controller='changeset', action='changeset_patch',
852 revision='tip', conditions={'function': check_repo},
852 revision='tip', conditions={'function': check_repo},
853 requirements=URL_NAME_REQUIREMENTS)
853 requirements=URL_NAME_REQUIREMENTS)
854
854
855 rmap.connect('changeset_download_home',
855 rmap.connect('changeset_download_home',
856 '/{repo_name}/changeset-download/{revision}',
856 '/{repo_name}/changeset-download/{revision}',
857 controller='changeset', action='changeset_download',
857 controller='changeset', action='changeset_download',
858 revision='tip', conditions={'function': check_repo},
858 revision='tip', conditions={'function': check_repo},
859 requirements=URL_NAME_REQUIREMENTS)
859 requirements=URL_NAME_REQUIREMENTS)
860
860
861 rmap.connect('changeset_comment',
861 rmap.connect('changeset_comment',
862 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
862 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
863 controller='changeset', revision='tip', action='comment',
863 controller='changeset', revision='tip', action='comment',
864 conditions={'function': check_repo},
864 conditions={'function': check_repo},
865 requirements=URL_NAME_REQUIREMENTS)
865 requirements=URL_NAME_REQUIREMENTS)
866
866
867 rmap.connect('changeset_comment_preview',
867 rmap.connect('changeset_comment_preview',
868 '/{repo_name}/changeset/comment/preview', jsroute=True,
868 '/{repo_name}/changeset/comment/preview', jsroute=True,
869 controller='changeset', action='preview_comment',
869 controller='changeset', action='preview_comment',
870 conditions={'function': check_repo, 'method': ['POST']},
870 conditions={'function': check_repo, 'method': ['POST']},
871 requirements=URL_NAME_REQUIREMENTS)
871 requirements=URL_NAME_REQUIREMENTS)
872
872
873 rmap.connect('changeset_comment_delete',
873 rmap.connect('changeset_comment_delete',
874 '/{repo_name}/changeset/comment/{comment_id}/delete',
874 '/{repo_name}/changeset/comment/{comment_id}/delete',
875 controller='changeset', action='delete_comment',
875 controller='changeset', action='delete_comment',
876 conditions={'function': check_repo, 'method': ['DELETE']},
876 conditions={'function': check_repo, 'method': ['DELETE']},
877 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
877 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
878
878
879 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
879 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
880 controller='changeset', action='changeset_info',
880 controller='changeset', action='changeset_info',
881 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
881 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
882
882
883 rmap.connect('compare_home',
883 rmap.connect('compare_home',
884 '/{repo_name}/compare',
884 '/{repo_name}/compare',
885 controller='compare', action='index',
885 controller='compare', action='index',
886 conditions={'function': check_repo},
886 conditions={'function': check_repo},
887 requirements=URL_NAME_REQUIREMENTS)
887 requirements=URL_NAME_REQUIREMENTS)
888
888
889 rmap.connect('compare_url',
889 rmap.connect('compare_url',
890 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
890 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
891 controller='compare', action='compare',
891 controller='compare', action='compare',
892 conditions={'function': check_repo},
892 conditions={'function': check_repo},
893 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
893 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
894
894
895 rmap.connect('pullrequest_home',
895 rmap.connect('pullrequest_home',
896 '/{repo_name}/pull-request/new', controller='pullrequests',
896 '/{repo_name}/pull-request/new', controller='pullrequests',
897 action='index', conditions={'function': check_repo,
897 action='index', conditions={'function': check_repo,
898 'method': ['GET']},
898 'method': ['GET']},
899 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
899 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
900
900
901 rmap.connect('pullrequest',
901 rmap.connect('pullrequest',
902 '/{repo_name}/pull-request/new', controller='pullrequests',
902 '/{repo_name}/pull-request/new', controller='pullrequests',
903 action='create', conditions={'function': check_repo,
903 action='create', conditions={'function': check_repo,
904 'method': ['POST']},
904 'method': ['POST']},
905 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
905 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
906
906
907 rmap.connect('pullrequest_repo_refs',
907 rmap.connect('pullrequest_repo_refs',
908 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
908 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
909 controller='pullrequests',
909 controller='pullrequests',
910 action='get_repo_refs',
910 action='get_repo_refs',
911 conditions={'function': check_repo, 'method': ['GET']},
911 conditions={'function': check_repo, 'method': ['GET']},
912 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
912 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
913
913
914 rmap.connect('pullrequest_repo_destinations',
914 rmap.connect('pullrequest_repo_destinations',
915 '/{repo_name}/pull-request/repo-destinations',
915 '/{repo_name}/pull-request/repo-destinations',
916 controller='pullrequests',
916 controller='pullrequests',
917 action='get_repo_destinations',
917 action='get_repo_destinations',
918 conditions={'function': check_repo, 'method': ['GET']},
918 conditions={'function': check_repo, 'method': ['GET']},
919 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
919 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
920
920
921 rmap.connect('pullrequest_show',
921 rmap.connect('pullrequest_show',
922 '/{repo_name}/pull-request/{pull_request_id}',
922 '/{repo_name}/pull-request/{pull_request_id}',
923 controller='pullrequests',
923 controller='pullrequests',
924 action='show', conditions={'function': check_repo,
924 action='show', conditions={'function': check_repo,
925 'method': ['GET']},
925 'method': ['GET']},
926 requirements=URL_NAME_REQUIREMENTS)
926 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
927
927
928 rmap.connect('pullrequest_update',
928 rmap.connect('pullrequest_update',
929 '/{repo_name}/pull-request/{pull_request_id}',
929 '/{repo_name}/pull-request/{pull_request_id}',
930 controller='pullrequests',
930 controller='pullrequests',
931 action='update', conditions={'function': check_repo,
931 action='update', conditions={'function': check_repo,
932 'method': ['PUT']},
932 'method': ['PUT']},
933 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
933 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
934
934
935 rmap.connect('pullrequest_merge',
935 rmap.connect('pullrequest_merge',
936 '/{repo_name}/pull-request/{pull_request_id}',
936 '/{repo_name}/pull-request/{pull_request_id}',
937 controller='pullrequests',
937 controller='pullrequests',
938 action='merge', conditions={'function': check_repo,
938 action='merge', conditions={'function': check_repo,
939 'method': ['POST']},
939 'method': ['POST']},
940 requirements=URL_NAME_REQUIREMENTS)
940 requirements=URL_NAME_REQUIREMENTS)
941
941
942 rmap.connect('pullrequest_delete',
942 rmap.connect('pullrequest_delete',
943 '/{repo_name}/pull-request/{pull_request_id}',
943 '/{repo_name}/pull-request/{pull_request_id}',
944 controller='pullrequests',
944 controller='pullrequests',
945 action='delete', conditions={'function': check_repo,
945 action='delete', conditions={'function': check_repo,
946 'method': ['DELETE']},
946 'method': ['DELETE']},
947 requirements=URL_NAME_REQUIREMENTS)
947 requirements=URL_NAME_REQUIREMENTS)
948
948
949 rmap.connect('pullrequest_show_all',
949 rmap.connect('pullrequest_show_all',
950 '/{repo_name}/pull-request',
950 '/{repo_name}/pull-request',
951 controller='pullrequests',
951 controller='pullrequests',
952 action='show_all', conditions={'function': check_repo,
952 action='show_all', conditions={'function': check_repo,
953 'method': ['GET']},
953 'method': ['GET']},
954 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
954 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
955
955
956 rmap.connect('pullrequest_comment',
956 rmap.connect('pullrequest_comment',
957 '/{repo_name}/pull-request-comment/{pull_request_id}',
957 '/{repo_name}/pull-request-comment/{pull_request_id}',
958 controller='pullrequests',
958 controller='pullrequests',
959 action='comment', conditions={'function': check_repo,
959 action='comment', conditions={'function': check_repo,
960 'method': ['POST']},
960 'method': ['POST']},
961 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
961 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
962
962
963 rmap.connect('pullrequest_comment_delete',
963 rmap.connect('pullrequest_comment_delete',
964 '/{repo_name}/pull-request-comment/{comment_id}/delete',
964 '/{repo_name}/pull-request-comment/{comment_id}/delete',
965 controller='pullrequests', action='delete_comment',
965 controller='pullrequests', action='delete_comment',
966 conditions={'function': check_repo, 'method': ['DELETE']},
966 conditions={'function': check_repo, 'method': ['DELETE']},
967 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
967 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
968
968
969 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
969 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
970 controller='summary', conditions={'function': check_repo},
970 controller='summary', conditions={'function': check_repo},
971 requirements=URL_NAME_REQUIREMENTS)
971 requirements=URL_NAME_REQUIREMENTS)
972
972
973 rmap.connect('branches_home', '/{repo_name}/branches',
973 rmap.connect('branches_home', '/{repo_name}/branches',
974 controller='branches', conditions={'function': check_repo},
974 controller='branches', conditions={'function': check_repo},
975 requirements=URL_NAME_REQUIREMENTS)
975 requirements=URL_NAME_REQUIREMENTS)
976
976
977 rmap.connect('tags_home', '/{repo_name}/tags',
977 rmap.connect('tags_home', '/{repo_name}/tags',
978 controller='tags', conditions={'function': check_repo},
978 controller='tags', conditions={'function': check_repo},
979 requirements=URL_NAME_REQUIREMENTS)
979 requirements=URL_NAME_REQUIREMENTS)
980
980
981 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
981 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
982 controller='bookmarks', conditions={'function': check_repo},
982 controller='bookmarks', conditions={'function': check_repo},
983 requirements=URL_NAME_REQUIREMENTS)
983 requirements=URL_NAME_REQUIREMENTS)
984
984
985 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
985 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
986 controller='changelog', conditions={'function': check_repo},
986 controller='changelog', conditions={'function': check_repo},
987 requirements=URL_NAME_REQUIREMENTS)
987 requirements=URL_NAME_REQUIREMENTS)
988
988
989 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
989 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
990 controller='changelog', action='changelog_summary',
990 controller='changelog', action='changelog_summary',
991 conditions={'function': check_repo},
991 conditions={'function': check_repo},
992 requirements=URL_NAME_REQUIREMENTS)
992 requirements=URL_NAME_REQUIREMENTS)
993
993
994 rmap.connect('changelog_file_home',
994 rmap.connect('changelog_file_home',
995 '/{repo_name}/changelog/{revision}/{f_path}',
995 '/{repo_name}/changelog/{revision}/{f_path}',
996 controller='changelog', f_path=None,
996 controller='changelog', f_path=None,
997 conditions={'function': check_repo},
997 conditions={'function': check_repo},
998 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
998 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
999
999
1000 rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}',
1000 rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}',
1001 controller='changelog', action='changelog_details',
1001 controller='changelog', action='changelog_details',
1002 conditions={'function': check_repo},
1002 conditions={'function': check_repo},
1003 requirements=URL_NAME_REQUIREMENTS)
1003 requirements=URL_NAME_REQUIREMENTS)
1004
1004
1005 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
1005 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
1006 controller='files', revision='tip', f_path='',
1006 controller='files', revision='tip', f_path='',
1007 conditions={'function': check_repo},
1007 conditions={'function': check_repo},
1008 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1008 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1009
1009
1010 rmap.connect('files_home_simple_catchrev',
1010 rmap.connect('files_home_simple_catchrev',
1011 '/{repo_name}/files/{revision}',
1011 '/{repo_name}/files/{revision}',
1012 controller='files', revision='tip', f_path='',
1012 controller='files', revision='tip', f_path='',
1013 conditions={'function': check_repo},
1013 conditions={'function': check_repo},
1014 requirements=URL_NAME_REQUIREMENTS)
1014 requirements=URL_NAME_REQUIREMENTS)
1015
1015
1016 rmap.connect('files_home_simple_catchall',
1016 rmap.connect('files_home_simple_catchall',
1017 '/{repo_name}/files',
1017 '/{repo_name}/files',
1018 controller='files', revision='tip', f_path='',
1018 controller='files', revision='tip', f_path='',
1019 conditions={'function': check_repo},
1019 conditions={'function': check_repo},
1020 requirements=URL_NAME_REQUIREMENTS)
1020 requirements=URL_NAME_REQUIREMENTS)
1021
1021
1022 rmap.connect('files_history_home',
1022 rmap.connect('files_history_home',
1023 '/{repo_name}/history/{revision}/{f_path}',
1023 '/{repo_name}/history/{revision}/{f_path}',
1024 controller='files', action='history', revision='tip', f_path='',
1024 controller='files', action='history', revision='tip', f_path='',
1025 conditions={'function': check_repo},
1025 conditions={'function': check_repo},
1026 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1026 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1027
1027
1028 rmap.connect('files_authors_home',
1028 rmap.connect('files_authors_home',
1029 '/{repo_name}/authors/{revision}/{f_path}',
1029 '/{repo_name}/authors/{revision}/{f_path}',
1030 controller='files', action='authors', revision='tip', f_path='',
1030 controller='files', action='authors', revision='tip', f_path='',
1031 conditions={'function': check_repo},
1031 conditions={'function': check_repo},
1032 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1032 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1033
1033
1034 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1034 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1035 controller='files', action='diff', f_path='',
1035 controller='files', action='diff', f_path='',
1036 conditions={'function': check_repo},
1036 conditions={'function': check_repo},
1037 requirements=URL_NAME_REQUIREMENTS)
1037 requirements=URL_NAME_REQUIREMENTS)
1038
1038
1039 rmap.connect('files_diff_2way_home',
1039 rmap.connect('files_diff_2way_home',
1040 '/{repo_name}/diff-2way/{f_path}',
1040 '/{repo_name}/diff-2way/{f_path}',
1041 controller='files', action='diff_2way', f_path='',
1041 controller='files', action='diff_2way', f_path='',
1042 conditions={'function': check_repo},
1042 conditions={'function': check_repo},
1043 requirements=URL_NAME_REQUIREMENTS)
1043 requirements=URL_NAME_REQUIREMENTS)
1044
1044
1045 rmap.connect('files_rawfile_home',
1045 rmap.connect('files_rawfile_home',
1046 '/{repo_name}/rawfile/{revision}/{f_path}',
1046 '/{repo_name}/rawfile/{revision}/{f_path}',
1047 controller='files', action='rawfile', revision='tip',
1047 controller='files', action='rawfile', revision='tip',
1048 f_path='', conditions={'function': check_repo},
1048 f_path='', conditions={'function': check_repo},
1049 requirements=URL_NAME_REQUIREMENTS)
1049 requirements=URL_NAME_REQUIREMENTS)
1050
1050
1051 rmap.connect('files_raw_home',
1051 rmap.connect('files_raw_home',
1052 '/{repo_name}/raw/{revision}/{f_path}',
1052 '/{repo_name}/raw/{revision}/{f_path}',
1053 controller='files', action='raw', revision='tip', f_path='',
1053 controller='files', action='raw', revision='tip', f_path='',
1054 conditions={'function': check_repo},
1054 conditions={'function': check_repo},
1055 requirements=URL_NAME_REQUIREMENTS)
1055 requirements=URL_NAME_REQUIREMENTS)
1056
1056
1057 rmap.connect('files_render_home',
1057 rmap.connect('files_render_home',
1058 '/{repo_name}/render/{revision}/{f_path}',
1058 '/{repo_name}/render/{revision}/{f_path}',
1059 controller='files', action='index', revision='tip', f_path='',
1059 controller='files', action='index', revision='tip', f_path='',
1060 rendered=True, conditions={'function': check_repo},
1060 rendered=True, conditions={'function': check_repo},
1061 requirements=URL_NAME_REQUIREMENTS)
1061 requirements=URL_NAME_REQUIREMENTS)
1062
1062
1063 rmap.connect('files_annotate_home',
1063 rmap.connect('files_annotate_home',
1064 '/{repo_name}/annotate/{revision}/{f_path}',
1064 '/{repo_name}/annotate/{revision}/{f_path}',
1065 controller='files', action='index', revision='tip',
1065 controller='files', action='index', revision='tip',
1066 f_path='', annotate=True, conditions={'function': check_repo},
1066 f_path='', annotate=True, conditions={'function': check_repo},
1067 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1067 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1068
1068
1069 rmap.connect('files_edit',
1069 rmap.connect('files_edit',
1070 '/{repo_name}/edit/{revision}/{f_path}',
1070 '/{repo_name}/edit/{revision}/{f_path}',
1071 controller='files', action='edit', revision='tip',
1071 controller='files', action='edit', revision='tip',
1072 f_path='',
1072 f_path='',
1073 conditions={'function': check_repo, 'method': ['POST']},
1073 conditions={'function': check_repo, 'method': ['POST']},
1074 requirements=URL_NAME_REQUIREMENTS)
1074 requirements=URL_NAME_REQUIREMENTS)
1075
1075
1076 rmap.connect('files_edit_home',
1076 rmap.connect('files_edit_home',
1077 '/{repo_name}/edit/{revision}/{f_path}',
1077 '/{repo_name}/edit/{revision}/{f_path}',
1078 controller='files', action='edit_home', revision='tip',
1078 controller='files', action='edit_home', revision='tip',
1079 f_path='', conditions={'function': check_repo},
1079 f_path='', conditions={'function': check_repo},
1080 requirements=URL_NAME_REQUIREMENTS)
1080 requirements=URL_NAME_REQUIREMENTS)
1081
1081
1082 rmap.connect('files_add',
1082 rmap.connect('files_add',
1083 '/{repo_name}/add/{revision}/{f_path}',
1083 '/{repo_name}/add/{revision}/{f_path}',
1084 controller='files', action='add', revision='tip',
1084 controller='files', action='add', revision='tip',
1085 f_path='',
1085 f_path='',
1086 conditions={'function': check_repo, 'method': ['POST']},
1086 conditions={'function': check_repo, 'method': ['POST']},
1087 requirements=URL_NAME_REQUIREMENTS)
1087 requirements=URL_NAME_REQUIREMENTS)
1088
1088
1089 rmap.connect('files_add_home',
1089 rmap.connect('files_add_home',
1090 '/{repo_name}/add/{revision}/{f_path}',
1090 '/{repo_name}/add/{revision}/{f_path}',
1091 controller='files', action='add_home', revision='tip',
1091 controller='files', action='add_home', revision='tip',
1092 f_path='', conditions={'function': check_repo},
1092 f_path='', conditions={'function': check_repo},
1093 requirements=URL_NAME_REQUIREMENTS)
1093 requirements=URL_NAME_REQUIREMENTS)
1094
1094
1095 rmap.connect('files_delete',
1095 rmap.connect('files_delete',
1096 '/{repo_name}/delete/{revision}/{f_path}',
1096 '/{repo_name}/delete/{revision}/{f_path}',
1097 controller='files', action='delete', revision='tip',
1097 controller='files', action='delete', revision='tip',
1098 f_path='',
1098 f_path='',
1099 conditions={'function': check_repo, 'method': ['POST']},
1099 conditions={'function': check_repo, 'method': ['POST']},
1100 requirements=URL_NAME_REQUIREMENTS)
1100 requirements=URL_NAME_REQUIREMENTS)
1101
1101
1102 rmap.connect('files_delete_home',
1102 rmap.connect('files_delete_home',
1103 '/{repo_name}/delete/{revision}/{f_path}',
1103 '/{repo_name}/delete/{revision}/{f_path}',
1104 controller='files', action='delete_home', revision='tip',
1104 controller='files', action='delete_home', revision='tip',
1105 f_path='', conditions={'function': check_repo},
1105 f_path='', conditions={'function': check_repo},
1106 requirements=URL_NAME_REQUIREMENTS)
1106 requirements=URL_NAME_REQUIREMENTS)
1107
1107
1108 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1108 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1109 controller='files', action='archivefile',
1109 controller='files', action='archivefile',
1110 conditions={'function': check_repo},
1110 conditions={'function': check_repo},
1111 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1111 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1112
1112
1113 rmap.connect('files_nodelist_home',
1113 rmap.connect('files_nodelist_home',
1114 '/{repo_name}/nodelist/{revision}/{f_path}',
1114 '/{repo_name}/nodelist/{revision}/{f_path}',
1115 controller='files', action='nodelist',
1115 controller='files', action='nodelist',
1116 conditions={'function': check_repo},
1116 conditions={'function': check_repo},
1117 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1117 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1118
1118
1119 rmap.connect('files_nodetree_full',
1119 rmap.connect('files_nodetree_full',
1120 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
1120 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
1121 controller='files', action='nodetree_full',
1121 controller='files', action='nodetree_full',
1122 conditions={'function': check_repo},
1122 conditions={'function': check_repo},
1123 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1123 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1124
1124
1125 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1125 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1126 controller='forks', action='fork_create',
1126 controller='forks', action='fork_create',
1127 conditions={'function': check_repo, 'method': ['POST']},
1127 conditions={'function': check_repo, 'method': ['POST']},
1128 requirements=URL_NAME_REQUIREMENTS)
1128 requirements=URL_NAME_REQUIREMENTS)
1129
1129
1130 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1130 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1131 controller='forks', action='fork',
1131 controller='forks', action='fork',
1132 conditions={'function': check_repo},
1132 conditions={'function': check_repo},
1133 requirements=URL_NAME_REQUIREMENTS)
1133 requirements=URL_NAME_REQUIREMENTS)
1134
1134
1135 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1135 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1136 controller='forks', action='forks',
1136 controller='forks', action='forks',
1137 conditions={'function': check_repo},
1137 conditions={'function': check_repo},
1138 requirements=URL_NAME_REQUIREMENTS)
1138 requirements=URL_NAME_REQUIREMENTS)
1139
1139
1140 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1140 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1141 controller='followers', action='followers',
1141 controller='followers', action='followers',
1142 conditions={'function': check_repo},
1142 conditions={'function': check_repo},
1143 requirements=URL_NAME_REQUIREMENTS)
1143 requirements=URL_NAME_REQUIREMENTS)
1144
1144
1145 # must be here for proper group/repo catching pattern
1145 # must be here for proper group/repo catching pattern
1146 _connect_with_slash(
1146 _connect_with_slash(
1147 rmap, 'repo_group_home', '/{group_name}',
1147 rmap, 'repo_group_home', '/{group_name}',
1148 controller='home', action='index_repo_group',
1148 controller='home', action='index_repo_group',
1149 conditions={'function': check_group},
1149 conditions={'function': check_group},
1150 requirements=URL_NAME_REQUIREMENTS)
1150 requirements=URL_NAME_REQUIREMENTS)
1151
1151
1152 # catch all, at the end
1152 # catch all, at the end
1153 _connect_with_slash(
1153 _connect_with_slash(
1154 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1154 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1155 controller='summary', action='index',
1155 controller='summary', action='index',
1156 conditions={'function': check_repo},
1156 conditions={'function': check_repo},
1157 requirements=URL_NAME_REQUIREMENTS)
1157 requirements=URL_NAME_REQUIREMENTS)
1158
1158
1159 return rmap
1159 return rmap
1160
1160
1161
1161
1162 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1162 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1163 """
1163 """
1164 Connect a route with an optional trailing slash in `path`.
1164 Connect a route with an optional trailing slash in `path`.
1165 """
1165 """
1166 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1166 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1167 mapper.connect(name, path, *args, **kwargs)
1167 mapper.connect(name, path, *args, **kwargs)
@@ -1,1026 +1,1029 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.comment import CommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel
63 from rhodecode.model.pull_request import PullRequestModel
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69 def __before__(self):
69 def __before__(self):
70 super(PullrequestsController, self).__before__()
70 super(PullrequestsController, self).__before__()
71
71
72 def _load_compare_data(self, pull_request, inline_comments):
72 def _load_compare_data(self, pull_request, inline_comments):
73 """
73 """
74 Load context data needed for generating compare diff
74 Load context data needed for generating compare diff
75
75
76 :param pull_request: object related to the request
76 :param pull_request: object related to the request
77 :param enable_comments: flag to determine if comments are included
77 :param enable_comments: flag to determine if comments are included
78 """
78 """
79 source_repo = pull_request.source_repo
79 source_repo = pull_request.source_repo
80 source_ref_id = pull_request.source_ref_parts.commit_id
80 source_ref_id = pull_request.source_ref_parts.commit_id
81
81
82 target_repo = pull_request.target_repo
82 target_repo = pull_request.target_repo
83 target_ref_id = pull_request.target_ref_parts.commit_id
83 target_ref_id = pull_request.target_ref_parts.commit_id
84
84
85 # despite opening commits for bookmarks/branches/tags, we always
85 # despite opening commits for bookmarks/branches/tags, we always
86 # convert this to rev to prevent changes after bookmark or branch change
86 # convert this to rev to prevent changes after bookmark or branch change
87 c.source_ref_type = 'rev'
87 c.source_ref_type = 'rev'
88 c.source_ref = source_ref_id
88 c.source_ref = source_ref_id
89
89
90 c.target_ref_type = 'rev'
90 c.target_ref_type = 'rev'
91 c.target_ref = target_ref_id
91 c.target_ref = target_ref_id
92
92
93 c.source_repo = source_repo
93 c.source_repo = source_repo
94 c.target_repo = target_repo
94 c.target_repo = target_repo
95
95
96 c.fulldiff = bool(request.GET.get('fulldiff'))
96 c.fulldiff = bool(request.GET.get('fulldiff'))
97
97
98 # diff_limit is the old behavior, will cut off the whole diff
98 # diff_limit is the old behavior, will cut off the whole diff
99 # if the limit is applied otherwise will just hide the
99 # if the limit is applied otherwise will just hide the
100 # big files from the front-end
100 # big files from the front-end
101 diff_limit = self.cut_off_limit_diff
101 diff_limit = self.cut_off_limit_diff
102 file_limit = self.cut_off_limit_file
102 file_limit = self.cut_off_limit_file
103
103
104 pre_load = ["author", "branch", "date", "message"]
104 pre_load = ["author", "branch", "date", "message"]
105
105
106 c.commit_ranges = []
106 c.commit_ranges = []
107 source_commit = EmptyCommit()
107 source_commit = EmptyCommit()
108 target_commit = EmptyCommit()
108 target_commit = EmptyCommit()
109 c.missing_requirements = False
109 c.missing_requirements = False
110 try:
110 try:
111 c.commit_ranges = [
111 c.commit_ranges = [
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 for rev in pull_request.revisions]
113 for rev in pull_request.revisions]
114
114
115 c.statuses = source_repo.statuses(
115 c.statuses = source_repo.statuses(
116 [x.raw_id for x in c.commit_ranges])
116 [x.raw_id for x in c.commit_ranges])
117
117
118 target_commit = source_repo.get_commit(
118 target_commit = source_repo.get_commit(
119 commit_id=safe_str(target_ref_id))
119 commit_id=safe_str(target_ref_id))
120 source_commit = source_repo.get_commit(
120 source_commit = source_repo.get_commit(
121 commit_id=safe_str(source_ref_id))
121 commit_id=safe_str(source_ref_id))
122 except RepositoryRequirementError:
122 except RepositoryRequirementError:
123 c.missing_requirements = True
123 c.missing_requirements = True
124
124
125 # auto collapse if we have more than limit
125 # auto collapse if we have more than limit
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128
128
129 c.changes = {}
129 c.changes = {}
130 c.missing_commits = False
130 c.missing_commits = False
131 if (c.missing_requirements or
131 if (c.missing_requirements or
132 isinstance(source_commit, EmptyCommit) or
132 isinstance(source_commit, EmptyCommit) or
133 source_commit == target_commit):
133 source_commit == target_commit):
134 _parsed = []
134 _parsed = []
135 c.missing_commits = True
135 c.missing_commits = True
136 else:
136 else:
137 vcs_diff = PullRequestModel().get_diff(pull_request)
137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 diff_processor = diffs.DiffProcessor(
138 diff_processor = diffs.DiffProcessor(
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 file_limit=file_limit, show_full_diff=c.fulldiff)
140 file_limit=file_limit, show_full_diff=c.fulldiff)
141
141
142 _parsed = diff_processor.prepare()
142 _parsed = diff_processor.prepare()
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144
144
145 included_files = {}
145 included_files = {}
146 for f in _parsed:
146 for f in _parsed:
147 included_files[f['filename']] = f['stats']
147 included_files[f['filename']] = f['stats']
148
148
149 c.deleted_files = [fname for fname in inline_comments if
149 c.deleted_files = [fname for fname in inline_comments if
150 fname not in included_files]
150 fname not in included_files]
151
151
152 c.deleted_files_comments = collections.defaultdict(dict)
152 c.deleted_files_comments = collections.defaultdict(dict)
153 for fname, per_line_comments in inline_comments.items():
153 for fname, per_line_comments in inline_comments.items():
154 if fname in c.deleted_files:
154 if fname in c.deleted_files:
155 c.deleted_files_comments[fname]['stats'] = 0
155 c.deleted_files_comments[fname]['stats'] = 0
156 c.deleted_files_comments[fname]['comments'] = list()
156 c.deleted_files_comments[fname]['comments'] = list()
157 for lno, comments in per_line_comments.items():
157 for lno, comments in per_line_comments.items():
158 c.deleted_files_comments[fname]['comments'].extend(comments)
158 c.deleted_files_comments[fname]['comments'].extend(comments)
159
159
160 def _node_getter(commit):
160 def _node_getter(commit):
161 def get_node(fname):
161 def get_node(fname):
162 try:
162 try:
163 return commit.get_node(fname)
163 return commit.get_node(fname)
164 except NodeDoesNotExistError:
164 except NodeDoesNotExistError:
165 return None
165 return None
166 return get_node
166 return get_node
167
167
168 c.diffset = codeblocks.DiffSet(
168 c.diffset = codeblocks.DiffSet(
169 repo_name=c.repo_name,
169 repo_name=c.repo_name,
170 source_repo_name=c.source_repo.repo_name,
170 source_repo_name=c.source_repo.repo_name,
171 source_node_getter=_node_getter(target_commit),
171 source_node_getter=_node_getter(target_commit),
172 target_node_getter=_node_getter(source_commit),
172 target_node_getter=_node_getter(source_commit),
173 comments=inline_comments
173 comments=inline_comments
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175
175
176 def _extract_ordering(self, request):
176 def _extract_ordering(self, request):
177 column_index = safe_int(request.GET.get('order[0][column]'))
177 column_index = safe_int(request.GET.get('order[0][column]'))
178 order_dir = request.GET.get('order[0][dir]', 'desc')
178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 order_by = request.GET.get(
179 order_by = request.GET.get(
180 'columns[%s][data][sort]' % column_index, 'name_raw')
180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 return order_by, order_dir
181 return order_by, order_dir
182
182
183 @LoginRequired()
183 @LoginRequired()
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 'repository.admin')
185 'repository.admin')
186 @HasAcceptedRepoType('git', 'hg')
186 @HasAcceptedRepoType('git', 'hg')
187 def show_all(self, repo_name):
187 def show_all(self, repo_name):
188 # filter types
188 # filter types
189 c.active = 'open'
189 c.active = 'open'
190 c.source = str2bool(request.GET.get('source'))
190 c.source = str2bool(request.GET.get('source'))
191 c.closed = str2bool(request.GET.get('closed'))
191 c.closed = str2bool(request.GET.get('closed'))
192 c.my = str2bool(request.GET.get('my'))
192 c.my = str2bool(request.GET.get('my'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 c.repo_name = repo_name
195 c.repo_name = repo_name
196
196
197 opened_by = None
197 opened_by = None
198 if c.my:
198 if c.my:
199 c.active = 'my'
199 c.active = 'my'
200 opened_by = [c.rhodecode_user.user_id]
200 opened_by = [c.rhodecode_user.user_id]
201
201
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 if c.closed:
203 if c.closed:
204 c.active = 'closed'
204 c.active = 'closed'
205 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
206
206
207 if c.awaiting_review and not c.source:
207 if c.awaiting_review and not c.source:
208 c.active = 'awaiting'
208 c.active = 'awaiting'
209 if c.source and not c.awaiting_review:
209 if c.source and not c.awaiting_review:
210 c.active = 'source'
210 c.active = 'source'
211 if c.awaiting_my_review:
211 if c.awaiting_my_review:
212 c.active = 'awaiting_my'
212 c.active = 'awaiting_my'
213
213
214 data = self._get_pull_requests_list(
214 data = self._get_pull_requests_list(
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 if not request.is_xhr:
216 if not request.is_xhr:
217 c.data = json.dumps(data['data'])
217 c.data = json.dumps(data['data'])
218 c.records_total = data['recordsTotal']
218 c.records_total = data['recordsTotal']
219 return render('/pullrequests/pullrequests.mako')
219 return render('/pullrequests/pullrequests.mako')
220 else:
220 else:
221 return json.dumps(data)
221 return json.dumps(data)
222
222
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 # pagination
224 # pagination
225 start = safe_int(request.GET.get('start'), 0)
225 start = safe_int(request.GET.get('start'), 0)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 order_by, order_dir = self._extract_ordering(request)
227 order_by, order_dir = self._extract_ordering(request)
228
228
229 if c.awaiting_review:
229 if c.awaiting_review:
230 pull_requests = PullRequestModel().get_awaiting_review(
230 pull_requests = PullRequestModel().get_awaiting_review(
231 repo_name, source=c.source, opened_by=opened_by,
231 repo_name, source=c.source, opened_by=opened_by,
232 statuses=statuses, offset=start, length=length,
232 statuses=statuses, offset=start, length=length,
233 order_by=order_by, order_dir=order_dir)
233 order_by=order_by, order_dir=order_dir)
234 pull_requests_total_count = PullRequestModel(
234 pull_requests_total_count = PullRequestModel(
235 ).count_awaiting_review(
235 ).count_awaiting_review(
236 repo_name, source=c.source, statuses=statuses,
236 repo_name, source=c.source, statuses=statuses,
237 opened_by=opened_by)
237 opened_by=opened_by)
238 elif c.awaiting_my_review:
238 elif c.awaiting_my_review:
239 pull_requests = PullRequestModel().get_awaiting_my_review(
239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 repo_name, source=c.source, opened_by=opened_by,
240 repo_name, source=c.source, opened_by=opened_by,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 offset=start, length=length, order_by=order_by,
242 offset=start, length=length, order_by=order_by,
243 order_dir=order_dir)
243 order_dir=order_dir)
244 pull_requests_total_count = PullRequestModel(
244 pull_requests_total_count = PullRequestModel(
245 ).count_awaiting_my_review(
245 ).count_awaiting_my_review(
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 statuses=statuses, opened_by=opened_by)
247 statuses=statuses, opened_by=opened_by)
248 else:
248 else:
249 pull_requests = PullRequestModel().get_all(
249 pull_requests = PullRequestModel().get_all(
250 repo_name, source=c.source, opened_by=opened_by,
250 repo_name, source=c.source, opened_by=opened_by,
251 statuses=statuses, offset=start, length=length,
251 statuses=statuses, offset=start, length=length,
252 order_by=order_by, order_dir=order_dir)
252 order_by=order_by, order_dir=order_dir)
253 pull_requests_total_count = PullRequestModel().count_all(
253 pull_requests_total_count = PullRequestModel().count_all(
254 repo_name, source=c.source, statuses=statuses,
254 repo_name, source=c.source, statuses=statuses,
255 opened_by=opened_by)
255 opened_by=opened_by)
256
256
257 from rhodecode.lib.utils import PartialRenderer
257 from rhodecode.lib.utils import PartialRenderer
258 _render = PartialRenderer('data_table/_dt_elements.mako')
258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 data = []
259 data = []
260 for pr in pull_requests:
260 for pr in pull_requests:
261 comments = CommentsModel().get_all_comments(
261 comments = CommentsModel().get_all_comments(
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263
263
264 data.append({
264 data.append({
265 'name': _render('pullrequest_name',
265 'name': _render('pullrequest_name',
266 pr.pull_request_id, pr.target_repo.repo_name),
266 pr.pull_request_id, pr.target_repo.repo_name),
267 'name_raw': pr.pull_request_id,
267 'name_raw': pr.pull_request_id,
268 'status': _render('pullrequest_status',
268 'status': _render('pullrequest_status',
269 pr.calculated_review_status()),
269 pr.calculated_review_status()),
270 'title': _render(
270 'title': _render(
271 'pullrequest_title', pr.title, pr.description),
271 'pullrequest_title', pr.title, pr.description),
272 'description': h.escape(pr.description),
272 'description': h.escape(pr.description),
273 'updated_on': _render('pullrequest_updated_on',
273 'updated_on': _render('pullrequest_updated_on',
274 h.datetime_to_time(pr.updated_on)),
274 h.datetime_to_time(pr.updated_on)),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 'created_on': _render('pullrequest_updated_on',
276 'created_on': _render('pullrequest_updated_on',
277 h.datetime_to_time(pr.created_on)),
277 h.datetime_to_time(pr.created_on)),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 'author': _render('pullrequest_author',
279 'author': _render('pullrequest_author',
280 pr.author.full_contact, ),
280 pr.author.full_contact, ),
281 'author_raw': pr.author.full_name,
281 'author_raw': pr.author.full_name,
282 'comments': _render('pullrequest_comments', len(comments)),
282 'comments': _render('pullrequest_comments', len(comments)),
283 'comments_raw': len(comments),
283 'comments_raw': len(comments),
284 'closed': pr.is_closed(),
284 'closed': pr.is_closed(),
285 })
285 })
286 # json used to render the grid
286 # json used to render the grid
287 data = ({
287 data = ({
288 'data': data,
288 'data': data,
289 'recordsTotal': pull_requests_total_count,
289 'recordsTotal': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
291 })
291 })
292 return data
292 return data
293
293
294 @LoginRequired()
294 @LoginRequired()
295 @NotAnonymous()
295 @NotAnonymous()
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 'repository.admin')
297 'repository.admin')
298 @HasAcceptedRepoType('git', 'hg')
298 @HasAcceptedRepoType('git', 'hg')
299 def index(self):
299 def index(self):
300 source_repo = c.rhodecode_db_repo
300 source_repo = c.rhodecode_db_repo
301
301
302 try:
302 try:
303 source_repo.scm_instance().get_commit()
303 source_repo.scm_instance().get_commit()
304 except EmptyRepositoryError:
304 except EmptyRepositoryError:
305 h.flash(h.literal(_('There are no commits yet')),
305 h.flash(h.literal(_('There are no commits yet')),
306 category='warning')
306 category='warning')
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308
308
309 commit_id = request.GET.get('commit')
309 commit_id = request.GET.get('commit')
310 branch_ref = request.GET.get('branch')
310 branch_ref = request.GET.get('branch')
311 bookmark_ref = request.GET.get('bookmark')
311 bookmark_ref = request.GET.get('bookmark')
312
312
313 try:
313 try:
314 source_repo_data = PullRequestModel().generate_repo_data(
314 source_repo_data = PullRequestModel().generate_repo_data(
315 source_repo, commit_id=commit_id,
315 source_repo, commit_id=commit_id,
316 branch=branch_ref, bookmark=bookmark_ref)
316 branch=branch_ref, bookmark=bookmark_ref)
317 except CommitDoesNotExistError as e:
317 except CommitDoesNotExistError as e:
318 log.exception(e)
318 log.exception(e)
319 h.flash(_('Commit does not exist'), 'error')
319 h.flash(_('Commit does not exist'), 'error')
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321
321
322 default_target_repo = source_repo
322 default_target_repo = source_repo
323
323
324 if source_repo.parent:
324 if source_repo.parent:
325 parent_vcs_obj = source_repo.parent.scm_instance()
325 parent_vcs_obj = source_repo.parent.scm_instance()
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 # change default if we have a parent repo
327 # change default if we have a parent repo
328 default_target_repo = source_repo.parent
328 default_target_repo = source_repo.parent
329
329
330 target_repo_data = PullRequestModel().generate_repo_data(
330 target_repo_data = PullRequestModel().generate_repo_data(
331 default_target_repo)
331 default_target_repo)
332
332
333 selected_source_ref = source_repo_data['refs']['selected_ref']
333 selected_source_ref = source_repo_data['refs']['selected_ref']
334
334
335 title_source_ref = selected_source_ref.split(':', 2)[1]
335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 c.default_title = PullRequestModel().generate_pullrequest_title(
336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 source=source_repo.repo_name,
337 source=source_repo.repo_name,
338 source_ref=title_source_ref,
338 source_ref=title_source_ref,
339 target=default_target_repo.repo_name
339 target=default_target_repo.repo_name
340 )
340 )
341
341
342 c.default_repo_data = {
342 c.default_repo_data = {
343 'source_repo_name': source_repo.repo_name,
343 'source_repo_name': source_repo.repo_name,
344 'source_refs_json': json.dumps(source_repo_data),
344 'source_refs_json': json.dumps(source_repo_data),
345 'target_repo_name': default_target_repo.repo_name,
345 'target_repo_name': default_target_repo.repo_name,
346 'target_refs_json': json.dumps(target_repo_data),
346 'target_refs_json': json.dumps(target_repo_data),
347 }
347 }
348 c.default_source_ref = selected_source_ref
348 c.default_source_ref = selected_source_ref
349
349
350 return render('/pullrequests/pullrequest.mako')
350 return render('/pullrequests/pullrequest.mako')
351
351
352 @LoginRequired()
352 @LoginRequired()
353 @NotAnonymous()
353 @NotAnonymous()
354 @XHRRequired()
354 @XHRRequired()
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 'repository.admin')
356 'repository.admin')
357 @jsonify
357 @jsonify
358 def get_repo_refs(self, repo_name, target_repo_name):
358 def get_repo_refs(self, repo_name, target_repo_name):
359 repo = Repository.get_by_repo_name(target_repo_name)
359 repo = Repository.get_by_repo_name(target_repo_name)
360 if not repo:
360 if not repo:
361 raise HTTPNotFound
361 raise HTTPNotFound
362 return PullRequestModel().generate_repo_data(repo)
362 return PullRequestModel().generate_repo_data(repo)
363
363
364 @LoginRequired()
364 @LoginRequired()
365 @NotAnonymous()
365 @NotAnonymous()
366 @XHRRequired()
366 @XHRRequired()
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 'repository.admin')
368 'repository.admin')
369 @jsonify
369 @jsonify
370 def get_repo_destinations(self, repo_name):
370 def get_repo_destinations(self, repo_name):
371 repo = Repository.get_by_repo_name(repo_name)
371 repo = Repository.get_by_repo_name(repo_name)
372 if not repo:
372 if not repo:
373 raise HTTPNotFound
373 raise HTTPNotFound
374 filter_query = request.GET.get('query')
374 filter_query = request.GET.get('query')
375
375
376 query = Repository.query() \
376 query = Repository.query() \
377 .order_by(func.length(Repository.repo_name)) \
377 .order_by(func.length(Repository.repo_name)) \
378 .filter(or_(
378 .filter(or_(
379 Repository.repo_name == repo.repo_name,
379 Repository.repo_name == repo.repo_name,
380 Repository.fork_id == repo.repo_id))
380 Repository.fork_id == repo.repo_id))
381
381
382 if filter_query:
382 if filter_query:
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 query = query.filter(
384 query = query.filter(
385 Repository.repo_name.ilike(ilike_expression))
385 Repository.repo_name.ilike(ilike_expression))
386
386
387 add_parent = False
387 add_parent = False
388 if repo.parent:
388 if repo.parent:
389 if filter_query in repo.parent.repo_name:
389 if filter_query in repo.parent.repo_name:
390 parent_vcs_obj = repo.parent.scm_instance()
390 parent_vcs_obj = repo.parent.scm_instance()
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 add_parent = True
392 add_parent = True
393
393
394 limit = 20 - 1 if add_parent else 20
394 limit = 20 - 1 if add_parent else 20
395 all_repos = query.limit(limit).all()
395 all_repos = query.limit(limit).all()
396 if add_parent:
396 if add_parent:
397 all_repos += [repo.parent]
397 all_repos += [repo.parent]
398
398
399 repos = []
399 repos = []
400 for obj in self.scm_model.get_repos(all_repos):
400 for obj in self.scm_model.get_repos(all_repos):
401 repos.append({
401 repos.append({
402 'id': obj['name'],
402 'id': obj['name'],
403 'text': obj['name'],
403 'text': obj['name'],
404 'type': 'repo',
404 'type': 'repo',
405 'obj': obj['dbrepo']
405 'obj': obj['dbrepo']
406 })
406 })
407
407
408 data = {
408 data = {
409 'more': False,
409 'more': False,
410 'results': [{
410 'results': [{
411 'text': _('Repositories'),
411 'text': _('Repositories'),
412 'children': repos
412 'children': repos
413 }] if repos else []
413 }] if repos else []
414 }
414 }
415 return data
415 return data
416
416
417 @LoginRequired()
417 @LoginRequired()
418 @NotAnonymous()
418 @NotAnonymous()
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 'repository.admin')
420 'repository.admin')
421 @HasAcceptedRepoType('git', 'hg')
421 @HasAcceptedRepoType('git', 'hg')
422 @auth.CSRFRequired()
422 @auth.CSRFRequired()
423 def create(self, repo_name):
423 def create(self, repo_name):
424 repo = Repository.get_by_repo_name(repo_name)
424 repo = Repository.get_by_repo_name(repo_name)
425 if not repo:
425 if not repo:
426 raise HTTPNotFound
426 raise HTTPNotFound
427
427
428 controls = peppercorn.parse(request.POST.items())
428 controls = peppercorn.parse(request.POST.items())
429
429
430 try:
430 try:
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 except formencode.Invalid as errors:
432 except formencode.Invalid as errors:
433 if errors.error_dict.get('revisions'):
433 if errors.error_dict.get('revisions'):
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 elif errors.error_dict.get('pullrequest_title'):
435 elif errors.error_dict.get('pullrequest_title'):
436 msg = _('Pull request requires a title with min. 3 chars')
436 msg = _('Pull request requires a title with min. 3 chars')
437 else:
437 else:
438 msg = _('Error creating pull request: {}').format(errors)
438 msg = _('Error creating pull request: {}').format(errors)
439 log.exception(msg)
439 log.exception(msg)
440 h.flash(msg, 'error')
440 h.flash(msg, 'error')
441
441
442 # would rather just go back to form ...
442 # would rather just go back to form ...
443 return redirect(url('pullrequest_home', repo_name=repo_name))
443 return redirect(url('pullrequest_home', repo_name=repo_name))
444
444
445 source_repo = _form['source_repo']
445 source_repo = _form['source_repo']
446 source_ref = _form['source_ref']
446 source_ref = _form['source_ref']
447 target_repo = _form['target_repo']
447 target_repo = _form['target_repo']
448 target_ref = _form['target_ref']
448 target_ref = _form['target_ref']
449 commit_ids = _form['revisions'][::-1]
449 commit_ids = _form['revisions'][::-1]
450 reviewers = [
450 reviewers = [
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452
452
453 # find the ancestor for this pr
453 # find the ancestor for this pr
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456
456
457 source_scm = source_db_repo.scm_instance()
457 source_scm = source_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
459
459
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462
462
463 ancestor = source_scm.get_common_ancestor(
463 ancestor = source_scm.get_common_ancestor(
464 source_commit.raw_id, target_commit.raw_id, target_scm)
464 source_commit.raw_id, target_commit.raw_id, target_scm)
465
465
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468
468
469 pullrequest_title = _form['pullrequest_title']
469 pullrequest_title = _form['pullrequest_title']
470 title_source_ref = source_ref.split(':', 2)[1]
470 title_source_ref = source_ref.split(':', 2)[1]
471 if not pullrequest_title:
471 if not pullrequest_title:
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 source=source_repo,
473 source=source_repo,
474 source_ref=title_source_ref,
474 source_ref=title_source_ref,
475 target=target_repo
475 target=target_repo
476 )
476 )
477
477
478 description = _form['pullrequest_desc']
478 description = _form['pullrequest_desc']
479 try:
479 try:
480 pull_request = PullRequestModel().create(
480 pull_request = PullRequestModel().create(
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 target_ref, commit_ids, reviewers, pullrequest_title,
482 target_ref, commit_ids, reviewers, pullrequest_title,
483 description
483 description
484 )
484 )
485 Session().commit()
485 Session().commit()
486 h.flash(_('Successfully opened new pull request'),
486 h.flash(_('Successfully opened new pull request'),
487 category='success')
487 category='success')
488 except Exception as e:
488 except Exception as e:
489 msg = _('Error occurred during sending pull request')
489 msg = _('Error occurred during sending pull request')
490 log.exception(msg)
490 log.exception(msg)
491 h.flash(msg, category='error')
491 h.flash(msg, category='error')
492 return redirect(url('pullrequest_home', repo_name=repo_name))
492 return redirect(url('pullrequest_home', repo_name=repo_name))
493
493
494 return redirect(url('pullrequest_show', repo_name=target_repo,
494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 pull_request_id=pull_request.pull_request_id))
495 pull_request_id=pull_request.pull_request_id))
496
496
497 @LoginRequired()
497 @LoginRequired()
498 @NotAnonymous()
498 @NotAnonymous()
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 'repository.admin')
500 'repository.admin')
501 @auth.CSRFRequired()
501 @auth.CSRFRequired()
502 @jsonify
502 @jsonify
503 def update(self, repo_name, pull_request_id):
503 def update(self, repo_name, pull_request_id):
504 pull_request_id = safe_int(pull_request_id)
504 pull_request_id = safe_int(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
506 # only owner or admin can update it
506 # only owner or admin can update it
507 allowed_to_update = PullRequestModel().check_user_update(
507 allowed_to_update = PullRequestModel().check_user_update(
508 pull_request, c.rhodecode_user)
508 pull_request, c.rhodecode_user)
509 if allowed_to_update:
509 if allowed_to_update:
510 controls = peppercorn.parse(request.POST.items())
510 controls = peppercorn.parse(request.POST.items())
511
511
512 if 'review_members' in controls:
512 if 'review_members' in controls:
513 self._update_reviewers(
513 self._update_reviewers(
514 pull_request_id, controls['review_members'])
514 pull_request_id, controls['review_members'])
515 elif str2bool(request.POST.get('update_commits', 'false')):
515 elif str2bool(request.POST.get('update_commits', 'false')):
516 self._update_commits(pull_request)
516 self._update_commits(pull_request)
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 self._reject_close(pull_request)
518 self._reject_close(pull_request)
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 self._edit_pull_request(pull_request)
520 self._edit_pull_request(pull_request)
521 else:
521 else:
522 raise HTTPBadRequest()
522 raise HTTPBadRequest()
523 return True
523 return True
524 raise HTTPForbidden()
524 raise HTTPForbidden()
525
525
526 def _edit_pull_request(self, pull_request):
526 def _edit_pull_request(self, pull_request):
527 try:
527 try:
528 PullRequestModel().edit(
528 PullRequestModel().edit(
529 pull_request, request.POST.get('title'),
529 pull_request, request.POST.get('title'),
530 request.POST.get('description'))
530 request.POST.get('description'))
531 except ValueError:
531 except ValueError:
532 msg = _(u'Cannot update closed pull requests.')
532 msg = _(u'Cannot update closed pull requests.')
533 h.flash(msg, category='error')
533 h.flash(msg, category='error')
534 return
534 return
535 else:
535 else:
536 Session().commit()
536 Session().commit()
537
537
538 msg = _(u'Pull request title & description updated.')
538 msg = _(u'Pull request title & description updated.')
539 h.flash(msg, category='success')
539 h.flash(msg, category='success')
540 return
540 return
541
541
542 def _update_commits(self, pull_request):
542 def _update_commits(self, pull_request):
543 resp = PullRequestModel().update_commits(pull_request)
543 resp = PullRequestModel().update_commits(pull_request)
544
544
545 if resp.executed:
545 if resp.executed:
546 msg = _(
546 msg = _(
547 u'Pull request updated to "{source_commit_id}" with '
547 u'Pull request updated to "{source_commit_id}" with '
548 u'{count_added} added, {count_removed} removed commits.')
548 u'{count_added} added, {count_removed} removed commits.')
549 msg = msg.format(
549 msg = msg.format(
550 source_commit_id=pull_request.source_ref_parts.commit_id,
550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 count_added=len(resp.changes.added),
551 count_added=len(resp.changes.added),
552 count_removed=len(resp.changes.removed))
552 count_removed=len(resp.changes.removed))
553 h.flash(msg, category='success')
553 h.flash(msg, category='success')
554
554
555 registry = get_current_registry()
555 registry = get_current_registry()
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 if channelstream_config.get('enabled'):
558 if channelstream_config.get('enabled'):
559 message = msg + (
559 message = msg + (
560 ' - <a onclick="window.location.reload()">'
560 ' - <a onclick="window.location.reload()">'
561 '<strong>{}</strong></a>'.format(_('Reload page')))
561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 channel = '/repo${}$/pr/{}'.format(
562 channel = '/repo${}$/pr/{}'.format(
563 pull_request.target_repo.repo_name,
563 pull_request.target_repo.repo_name,
564 pull_request.pull_request_id
564 pull_request.pull_request_id
565 )
565 )
566 payload = {
566 payload = {
567 'type': 'message',
567 'type': 'message',
568 'user': 'system',
568 'user': 'system',
569 'exclude_users': [request.user.username],
569 'exclude_users': [request.user.username],
570 'channel': channel,
570 'channel': channel,
571 'message': {
571 'message': {
572 'message': message,
572 'message': message,
573 'level': 'success',
573 'level': 'success',
574 'topic': '/notifications'
574 'topic': '/notifications'
575 }
575 }
576 }
576 }
577 channelstream_request(
577 channelstream_request(
578 channelstream_config, [payload], '/message',
578 channelstream_config, [payload], '/message',
579 raise_exc=False)
579 raise_exc=False)
580 else:
580 else:
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 warning_reasons = [
582 warning_reasons = [
583 UpdateFailureReason.NO_CHANGE,
583 UpdateFailureReason.NO_CHANGE,
584 UpdateFailureReason.WRONG_REF_TPYE,
584 UpdateFailureReason.WRONG_REF_TPYE,
585 ]
585 ]
586 category = 'warning' if resp.reason in warning_reasons else 'error'
586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 h.flash(msg, category=category)
587 h.flash(msg, category=category)
588
588
589 @auth.CSRFRequired()
589 @auth.CSRFRequired()
590 @LoginRequired()
590 @LoginRequired()
591 @NotAnonymous()
591 @NotAnonymous()
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 'repository.admin')
593 'repository.admin')
594 def merge(self, repo_name, pull_request_id):
594 def merge(self, repo_name, pull_request_id):
595 """
595 """
596 POST /{repo_name}/pull-request/{pull_request_id}
596 POST /{repo_name}/pull-request/{pull_request_id}
597
597
598 Merge will perform a server-side merge of the specified
598 Merge will perform a server-side merge of the specified
599 pull request, if the pull request is approved and mergeable.
599 pull request, if the pull request is approved and mergeable.
600 After succesfull merging, the pull request is automatically
600 After succesfull merging, the pull request is automatically
601 closed, with a relevant comment.
601 closed, with a relevant comment.
602 """
602 """
603 pull_request_id = safe_int(pull_request_id)
603 pull_request_id = safe_int(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
605 user = c.rhodecode_user
605 user = c.rhodecode_user
606
606
607 if self._meets_merge_pre_conditions(pull_request, user):
607 if self._meets_merge_pre_conditions(pull_request, user):
608 log.debug("Pre-conditions checked, trying to merge.")
608 log.debug("Pre-conditions checked, trying to merge.")
609 extras = vcs_operation_context(
609 extras = vcs_operation_context(
610 request.environ, repo_name=pull_request.target_repo.repo_name,
610 request.environ, repo_name=pull_request.target_repo.repo_name,
611 username=user.username, action='push',
611 username=user.username, action='push',
612 scm=pull_request.target_repo.repo_type)
612 scm=pull_request.target_repo.repo_type)
613 self._merge_pull_request(pull_request, user, extras)
613 self._merge_pull_request(pull_request, user, extras)
614
614
615 return redirect(url(
615 return redirect(url(
616 'pullrequest_show',
616 'pullrequest_show',
617 repo_name=pull_request.target_repo.repo_name,
617 repo_name=pull_request.target_repo.repo_name,
618 pull_request_id=pull_request.pull_request_id))
618 pull_request_id=pull_request.pull_request_id))
619
619
620 def _meets_merge_pre_conditions(self, pull_request, user):
620 def _meets_merge_pre_conditions(self, pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
622 raise HTTPForbidden()
622 raise HTTPForbidden()
623
623
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 if not merge_status:
625 if not merge_status:
626 log.debug("Cannot merge, not mergeable.")
626 log.debug("Cannot merge, not mergeable.")
627 h.flash(msg, category='error')
627 h.flash(msg, category='error')
628 return False
628 return False
629
629
630 if (pull_request.calculated_review_status()
630 if (pull_request.calculated_review_status()
631 is not ChangesetStatus.STATUS_APPROVED):
631 is not ChangesetStatus.STATUS_APPROVED):
632 log.debug("Cannot merge, approval is pending.")
632 log.debug("Cannot merge, approval is pending.")
633 msg = _('Pull request reviewer approval is pending.')
633 msg = _('Pull request reviewer approval is pending.')
634 h.flash(msg, category='error')
634 h.flash(msg, category='error')
635 return False
635 return False
636 return True
636 return True
637
637
638 def _merge_pull_request(self, pull_request, user, extras):
638 def _merge_pull_request(self, pull_request, user, extras):
639 merge_resp = PullRequestModel().merge(
639 merge_resp = PullRequestModel().merge(
640 pull_request, user, extras=extras)
640 pull_request, user, extras=extras)
641
641
642 if merge_resp.executed:
642 if merge_resp.executed:
643 log.debug("The merge was successful, closing the pull request.")
643 log.debug("The merge was successful, closing the pull request.")
644 PullRequestModel().close_pull_request(
644 PullRequestModel().close_pull_request(
645 pull_request.pull_request_id, user)
645 pull_request.pull_request_id, user)
646 Session().commit()
646 Session().commit()
647 msg = _('Pull request was successfully merged and closed.')
647 msg = _('Pull request was successfully merged and closed.')
648 h.flash(msg, category='success')
648 h.flash(msg, category='success')
649 else:
649 else:
650 log.debug(
650 log.debug(
651 "The merge was not successful. Merge response: %s",
651 "The merge was not successful. Merge response: %s",
652 merge_resp)
652 merge_resp)
653 msg = PullRequestModel().merge_status_message(
653 msg = PullRequestModel().merge_status_message(
654 merge_resp.failure_reason)
654 merge_resp.failure_reason)
655 h.flash(msg, category='error')
655 h.flash(msg, category='error')
656
656
657 def _update_reviewers(self, pull_request_id, review_members):
657 def _update_reviewers(self, pull_request_id, review_members):
658 reviewers = [
658 reviewers = [
659 (int(r['user_id']), r['reasons']) for r in review_members]
659 (int(r['user_id']), r['reasons']) for r in review_members]
660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
661 Session().commit()
661 Session().commit()
662
662
663 def _reject_close(self, pull_request):
663 def _reject_close(self, pull_request):
664 if pull_request.is_closed():
664 if pull_request.is_closed():
665 raise HTTPForbidden()
665 raise HTTPForbidden()
666
666
667 PullRequestModel().close_pull_request_with_comment(
667 PullRequestModel().close_pull_request_with_comment(
668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
669 Session().commit()
669 Session().commit()
670
670
671 @LoginRequired()
671 @LoginRequired()
672 @NotAnonymous()
672 @NotAnonymous()
673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 'repository.admin')
674 'repository.admin')
675 @auth.CSRFRequired()
675 @auth.CSRFRequired()
676 @jsonify
676 @jsonify
677 def delete(self, repo_name, pull_request_id):
677 def delete(self, repo_name, pull_request_id):
678 pull_request_id = safe_int(pull_request_id)
678 pull_request_id = safe_int(pull_request_id)
679 pull_request = PullRequest.get_or_404(pull_request_id)
679 pull_request = PullRequest.get_or_404(pull_request_id)
680 # only owner can delete it !
680 # only owner can delete it !
681 if pull_request.author.user_id == c.rhodecode_user.user_id:
681 if pull_request.author.user_id == c.rhodecode_user.user_id:
682 PullRequestModel().delete(pull_request)
682 PullRequestModel().delete(pull_request)
683 Session().commit()
683 Session().commit()
684 h.flash(_('Successfully deleted pull request'),
684 h.flash(_('Successfully deleted pull request'),
685 category='success')
685 category='success')
686 return redirect(url('my_account_pullrequests'))
686 return redirect(url('my_account_pullrequests'))
687 raise HTTPForbidden()
687 raise HTTPForbidden()
688
688
689 def _get_pr_version(self, pull_request_id, version=None):
689 def _get_pr_version(self, pull_request_id, version=None):
690 pull_request_id = safe_int(pull_request_id)
690 pull_request_id = safe_int(pull_request_id)
691 at_version = None
691 at_version = None
692
692
693 if version and version == 'latest':
693 if version and version == 'latest':
694 pull_request_ver = PullRequest.get(pull_request_id)
694 pull_request_ver = PullRequest.get(pull_request_id)
695 pull_request_obj = pull_request_ver
695 pull_request_obj = pull_request_ver
696 _org_pull_request_obj = pull_request_obj
696 _org_pull_request_obj = pull_request_obj
697 at_version = 'latest'
697 at_version = 'latest'
698 elif version:
698 elif version:
699 pull_request_ver = PullRequestVersion.get_or_404(version)
699 pull_request_ver = PullRequestVersion.get_or_404(version)
700 pull_request_obj = pull_request_ver
700 pull_request_obj = pull_request_ver
701 _org_pull_request_obj = pull_request_ver.pull_request
701 _org_pull_request_obj = pull_request_ver.pull_request
702 at_version = pull_request_ver.pull_request_version_id
702 at_version = pull_request_ver.pull_request_version_id
703 else:
703 else:
704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
705
705
706 pull_request_display_obj = PullRequest.get_pr_display_object(
706 pull_request_display_obj = PullRequest.get_pr_display_object(
707 pull_request_obj, _org_pull_request_obj)
707 pull_request_obj, _org_pull_request_obj)
708 return _org_pull_request_obj, pull_request_obj, \
708 return _org_pull_request_obj, pull_request_obj, \
709 pull_request_display_obj, at_version
709 pull_request_display_obj, at_version
710
710
711 def _get_pr_version_changes(self, version, pull_request_latest):
711 def _get_pr_version_changes(self, version, pull_request_latest):
712 """
712 """
713 Generate changes commits, and diff data based on the current pr version
713 Generate changes commits, and diff data based on the current pr version
714 """
714 """
715
715
716 #TODO(marcink): save those changes as JSON metadata for chaching later.
716 #TODO(marcink): save those changes as JSON metadata for chaching later.
717
717
718 # fake the version to add the "initial" state object
718 # fake the version to add the "initial" state object
719 pull_request_initial = PullRequest.get_pr_display_object(
719 pull_request_initial = PullRequest.get_pr_display_object(
720 pull_request_latest, pull_request_latest,
720 pull_request_latest, pull_request_latest,
721 internal_methods=['get_commit', 'versions'])
721 internal_methods=['get_commit', 'versions'])
722 pull_request_initial.revisions = []
722 pull_request_initial.revisions = []
723 pull_request_initial.source_repo.get_commit = types.MethodType(
723 pull_request_initial.source_repo.get_commit = types.MethodType(
724 lambda *a, **k: EmptyCommit(), pull_request_initial)
724 lambda *a, **k: EmptyCommit(), pull_request_initial)
725 pull_request_initial.source_repo.scm_instance = types.MethodType(
725 pull_request_initial.source_repo.scm_instance = types.MethodType(
726 lambda *a, **k: EmptyRepository(), pull_request_initial)
726 lambda *a, **k: EmptyRepository(), pull_request_initial)
727
727
728 _changes_versions = [pull_request_latest] + \
728 _changes_versions = [pull_request_latest] + \
729 list(reversed(c.versions)) + \
729 list(reversed(c.versions)) + \
730 [pull_request_initial]
730 [pull_request_initial]
731
731
732 if version == 'latest':
732 if version == 'latest':
733 index = 0
733 index = 0
734 else:
734 else:
735 for pos, prver in enumerate(_changes_versions):
735 for pos, prver in enumerate(_changes_versions):
736 ver = getattr(prver, 'pull_request_version_id', -1)
736 ver = getattr(prver, 'pull_request_version_id', -1)
737 if ver == safe_int(version):
737 if ver == safe_int(version):
738 index = pos
738 index = pos
739 break
739 break
740 else:
740 else:
741 index = 0
741 index = 0
742
742
743 cur_obj = _changes_versions[index]
743 cur_obj = _changes_versions[index]
744 prev_obj = _changes_versions[index + 1]
744 prev_obj = _changes_versions[index + 1]
745
745
746 old_commit_ids = set(prev_obj.revisions)
746 old_commit_ids = set(prev_obj.revisions)
747 new_commit_ids = set(cur_obj.revisions)
747 new_commit_ids = set(cur_obj.revisions)
748
748
749 changes = PullRequestModel()._calculate_commit_id_changes(
749 changes = PullRequestModel()._calculate_commit_id_changes(
750 old_commit_ids, new_commit_ids)
750 old_commit_ids, new_commit_ids)
751
751
752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
753 cur_obj, prev_obj)
753 cur_obj, prev_obj)
754 file_changes = PullRequestModel()._calculate_file_changes(
754 file_changes = PullRequestModel()._calculate_file_changes(
755 old_diff_data, new_diff_data)
755 old_diff_data, new_diff_data)
756 return changes, file_changes
756 return changes, file_changes
757
757
758 @LoginRequired()
758 @LoginRequired()
759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
760 'repository.admin')
760 'repository.admin')
761 def show(self, repo_name, pull_request_id):
761 def show(self, repo_name, pull_request_id):
762 pull_request_id = safe_int(pull_request_id)
762 pull_request_id = safe_int(pull_request_id)
763 version = request.GET.get('version')
763 version = request.GET.get('version')
764
764
765 (pull_request_latest,
765 (pull_request_latest,
766 pull_request_at_ver,
766 pull_request_at_ver,
767 pull_request_display_obj,
767 pull_request_display_obj,
768 at_version) = self._get_pr_version(pull_request_id, version=version)
768 at_version) = self._get_pr_version(pull_request_id, version=version)
769
769
770 c.template_context['pull_request_data']['pull_request_id'] = \
770 c.template_context['pull_request_data']['pull_request_id'] = \
771 pull_request_id
771 pull_request_id
772
772
773 # pull_requests repo_name we opened it against
773 # pull_requests repo_name we opened it against
774 # ie. target_repo must match
774 # ie. target_repo must match
775 if repo_name != pull_request_at_ver.target_repo.repo_name:
775 if repo_name != pull_request_at_ver.target_repo.repo_name:
776 raise HTTPNotFound
776 raise HTTPNotFound
777
777
778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
779 pull_request_at_ver)
779 pull_request_at_ver)
780
780
781 pr_closed = pull_request_latest.is_closed()
781 pr_closed = pull_request_latest.is_closed()
782 if at_version and not at_version == 'latest':
782 if at_version and not at_version == 'latest':
783 c.allowed_to_change_status = False
783 c.allowed_to_change_status = False
784 c.allowed_to_update = False
784 c.allowed_to_update = False
785 c.allowed_to_merge = False
785 c.allowed_to_merge = False
786 c.allowed_to_delete = False
786 c.allowed_to_delete = False
787 c.allowed_to_comment = False
787 c.allowed_to_comment = False
788 else:
788 else:
789 c.allowed_to_change_status = PullRequestModel(). \
789 c.allowed_to_change_status = PullRequestModel(). \
790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
791 c.allowed_to_update = PullRequestModel().check_user_update(
791 c.allowed_to_update = PullRequestModel().check_user_update(
792 pull_request_latest, c.rhodecode_user) and not pr_closed
792 pull_request_latest, c.rhodecode_user) and not pr_closed
793 c.allowed_to_merge = PullRequestModel().check_user_merge(
793 c.allowed_to_merge = PullRequestModel().check_user_merge(
794 pull_request_latest, c.rhodecode_user) and not pr_closed
794 pull_request_latest, c.rhodecode_user) and not pr_closed
795 c.allowed_to_delete = PullRequestModel().check_user_delete(
795 c.allowed_to_delete = PullRequestModel().check_user_delete(
796 pull_request_latest, c.rhodecode_user) and not pr_closed
796 pull_request_latest, c.rhodecode_user) and not pr_closed
797 c.allowed_to_comment = not pr_closed
797 c.allowed_to_comment = not pr_closed
798
798
799 cc_model = CommentsModel()
799 cc_model = CommentsModel()
800
800
801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
804 pull_request_at_ver)
804 pull_request_at_ver)
805 c.approval_msg = None
805 c.approval_msg = None
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
807 c.approval_msg = _('Reviewer approval is pending.')
807 c.approval_msg = _('Reviewer approval is pending.')
808 c.pr_merge_status = False
808 c.pr_merge_status = False
809
809
810 # inline comments
810 # inline comments
811 inline_comments = cc_model.get_inline_comments(
811 inline_comments = cc_model.get_inline_comments(
812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
813
813
814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
815 inline_comments, version=at_version, include_aggregates=True)
815 inline_comments, version=at_version, include_aggregates=True)
816
816
817 c.versions = pull_request_display_obj.versions()
817 c.versions = pull_request_display_obj.versions()
818 c.at_version_num = at_version if at_version and at_version != 'latest' else None
818 c.at_version_num = at_version if at_version and at_version != 'latest' else None
819 c.at_version_pos = ChangesetComment.get_index_from_version(
819 c.at_version_pos = ChangesetComment.get_index_from_version(
820 c.at_version_num, c.versions)
820 c.at_version_num, c.versions)
821
821
822 is_outdated = lambda co: \
822 is_outdated = lambda co: \
823 not c.at_version_num \
823 not c.at_version_num \
824 or co.pull_request_version_id <= c.at_version_num
824 or co.pull_request_version_id <= c.at_version_num
825
825
826 # inline_comments_until_version
826 # inline_comments_until_version
827 if c.at_version_num:
827 if c.at_version_num:
828 # if we use version, then do not show later comments
828 # if we use version, then do not show later comments
829 # than current version
829 # than current version
830 paths = collections.defaultdict(lambda: collections.defaultdict(list))
830 paths = collections.defaultdict(lambda: collections.defaultdict(list))
831 for fname, per_line_comments in inline_comments.iteritems():
831 for fname, per_line_comments in inline_comments.iteritems():
832 for lno, comments in per_line_comments.iteritems():
832 for lno, comments in per_line_comments.iteritems():
833 for co in comments:
833 for co in comments:
834 if co.pull_request_version_id and is_outdated(co):
834 if co.pull_request_version_id and is_outdated(co):
835 paths[co.f_path][co.line_no].append(co)
835 paths[co.f_path][co.line_no].append(co)
836 inline_comments = paths
836 inline_comments = paths
837
837
838 # outdated comments
838 # outdated comments
839 c.outdated_cnt = 0
839 c.outdated_cnt = 0
840 if CommentsModel.use_outdated_comments(pull_request_latest):
840 if CommentsModel.use_outdated_comments(pull_request_latest):
841 outdated_comments = cc_model.get_outdated_comments(
841 outdated_comments = cc_model.get_outdated_comments(
842 c.rhodecode_db_repo.repo_id,
842 c.rhodecode_db_repo.repo_id,
843 pull_request=pull_request_at_ver)
843 pull_request=pull_request_at_ver)
844
844
845 # Count outdated comments and check for deleted files
845 # Count outdated comments and check for deleted files
846 is_outdated = lambda co: \
846 is_outdated = lambda co: \
847 not c.at_version_num \
847 not c.at_version_num \
848 or co.pull_request_version_id < c.at_version_num
848 or co.pull_request_version_id < c.at_version_num
849 for file_name, lines in outdated_comments.iteritems():
849 for file_name, lines in outdated_comments.iteritems():
850 for comments in lines.values():
850 for comments in lines.values():
851 comments = [comm for comm in comments if is_outdated(comm)]
851 comments = [comm for comm in comments if is_outdated(comm)]
852 c.outdated_cnt += len(comments)
852 c.outdated_cnt += len(comments)
853
853
854 # load compare data into template context
854 # load compare data into template context
855 self._load_compare_data(pull_request_at_ver, inline_comments)
855 self._load_compare_data(pull_request_at_ver, inline_comments)
856
856
857 # this is a hack to properly display links, when creating PR, the
857 # this is a hack to properly display links, when creating PR, the
858 # compare view and others uses different notation, and
858 # compare view and others uses different notation, and
859 # compare_commits.mako renders links based on the target_repo.
859 # compare_commits.mako renders links based on the target_repo.
860 # We need to swap that here to generate it properly on the html side
860 # We need to swap that here to generate it properly on the html side
861 c.target_repo = c.source_repo
861 c.target_repo = c.source_repo
862
862
863 # general comments
863 # general comments
864 c.comments = cc_model.get_comments(
864 c.comments = cc_model.get_comments(
865 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
865 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
866
866
867 if c.allowed_to_update:
867 if c.allowed_to_update:
868 force_close = ('forced_closed', _('Close Pull Request'))
868 force_close = ('forced_closed', _('Close Pull Request'))
869 statuses = ChangesetStatus.STATUSES + [force_close]
869 statuses = ChangesetStatus.STATUSES + [force_close]
870 else:
870 else:
871 statuses = ChangesetStatus.STATUSES
871 statuses = ChangesetStatus.STATUSES
872 c.commit_statuses = statuses
872 c.commit_statuses = statuses
873
873
874 c.ancestor = None # TODO: add ancestor here
874 c.ancestor = None # TODO: add ancestor here
875 c.pull_request = pull_request_display_obj
875 c.pull_request = pull_request_display_obj
876 c.pull_request_latest = pull_request_latest
876 c.pull_request_latest = pull_request_latest
877 c.at_version = at_version
877 c.at_version = at_version
878
878
879 c.changes = None
879 c.changes = None
880 c.file_changes = None
880 c.file_changes = None
881
881
882 c.show_version_changes = 1 # control flag, not used yet
882 c.show_version_changes = 1 # control flag, not used yet
883
883
884 if at_version and c.show_version_changes:
884 if at_version and c.show_version_changes:
885 c.changes, c.file_changes = self._get_pr_version_changes(
885 c.changes, c.file_changes = self._get_pr_version_changes(
886 version, pull_request_latest)
886 version, pull_request_latest)
887
887
888 return render('/pullrequests/pullrequest_show.mako')
888 return render('/pullrequests/pullrequest_show.mako')
889
889
890 @LoginRequired()
890 @LoginRequired()
891 @NotAnonymous()
891 @NotAnonymous()
892 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
892 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
893 'repository.admin')
893 'repository.admin')
894 @auth.CSRFRequired()
894 @auth.CSRFRequired()
895 @jsonify
895 @jsonify
896 def comment(self, repo_name, pull_request_id):
896 def comment(self, repo_name, pull_request_id):
897 pull_request_id = safe_int(pull_request_id)
897 pull_request_id = safe_int(pull_request_id)
898 pull_request = PullRequest.get_or_404(pull_request_id)
898 pull_request = PullRequest.get_or_404(pull_request_id)
899 if pull_request.is_closed():
899 if pull_request.is_closed():
900 raise HTTPForbidden()
900 raise HTTPForbidden()
901
901
902 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
902 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
903 # as a changeset status, still we want to send it in one value.
903 # as a changeset status, still we want to send it in one value.
904 status = request.POST.get('changeset_status', None)
904 status = request.POST.get('changeset_status', None)
905 text = request.POST.get('text')
905 text = request.POST.get('text')
906 comment_type = request.POST.get('comment_type')
906 comment_type = request.POST.get('comment_type')
907 resolves_comment_id = request.POST.get('resolves_comment_id')
908
907 if status and '_closed' in status:
909 if status and '_closed' in status:
908 close_pr = True
910 close_pr = True
909 status = status.replace('_closed', '')
911 status = status.replace('_closed', '')
910 else:
912 else:
911 close_pr = False
913 close_pr = False
912
914
913 forced = (status == 'forced')
915 forced = (status == 'forced')
914 if forced:
916 if forced:
915 status = 'rejected'
917 status = 'rejected'
916
918
917 allowed_to_change_status = PullRequestModel().check_user_change_status(
919 allowed_to_change_status = PullRequestModel().check_user_change_status(
918 pull_request, c.rhodecode_user)
920 pull_request, c.rhodecode_user)
919
921
920 if status and allowed_to_change_status:
922 if status and allowed_to_change_status:
921 message = (_('Status change %(transition_icon)s %(status)s')
923 message = (_('Status change %(transition_icon)s %(status)s')
922 % {'transition_icon': '>',
924 % {'transition_icon': '>',
923 'status': ChangesetStatus.get_status_lbl(status)})
925 'status': ChangesetStatus.get_status_lbl(status)})
924 if close_pr:
926 if close_pr:
925 message = _('Closing with') + ' ' + message
927 message = _('Closing with') + ' ' + message
926 text = text or message
928 text = text or message
927 comm = CommentsModel().create(
929 comm = CommentsModel().create(
928 text=text,
930 text=text,
929 repo=c.rhodecode_db_repo.repo_id,
931 repo=c.rhodecode_db_repo.repo_id,
930 user=c.rhodecode_user.user_id,
932 user=c.rhodecode_user.user_id,
931 pull_request=pull_request_id,
933 pull_request=pull_request_id,
932 f_path=request.POST.get('f_path'),
934 f_path=request.POST.get('f_path'),
933 line_no=request.POST.get('line'),
935 line_no=request.POST.get('line'),
934 status_change=(ChangesetStatus.get_status_lbl(status)
936 status_change=(ChangesetStatus.get_status_lbl(status)
935 if status and allowed_to_change_status else None),
937 if status and allowed_to_change_status else None),
936 status_change_type=(status
938 status_change_type=(status
937 if status and allowed_to_change_status else None),
939 if status and allowed_to_change_status else None),
938 closing_pr=close_pr,
940 closing_pr=close_pr,
939 comment_type=comment_type
941 comment_type=comment_type,
942 resolves_comment_id=resolves_comment_id
940 )
943 )
941
944
942 if allowed_to_change_status:
945 if allowed_to_change_status:
943 old_calculated_status = pull_request.calculated_review_status()
946 old_calculated_status = pull_request.calculated_review_status()
944 # get status if set !
947 # get status if set !
945 if status:
948 if status:
946 ChangesetStatusModel().set_status(
949 ChangesetStatusModel().set_status(
947 c.rhodecode_db_repo.repo_id,
950 c.rhodecode_db_repo.repo_id,
948 status,
951 status,
949 c.rhodecode_user.user_id,
952 c.rhodecode_user.user_id,
950 comm,
953 comm,
951 pull_request=pull_request_id
954 pull_request=pull_request_id
952 )
955 )
953
956
954 Session().flush()
957 Session().flush()
955 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
958 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
956 # we now calculate the status of pull request, and based on that
959 # we now calculate the status of pull request, and based on that
957 # calculation we set the commits status
960 # calculation we set the commits status
958 calculated_status = pull_request.calculated_review_status()
961 calculated_status = pull_request.calculated_review_status()
959 if old_calculated_status != calculated_status:
962 if old_calculated_status != calculated_status:
960 PullRequestModel()._trigger_pull_request_hook(
963 PullRequestModel()._trigger_pull_request_hook(
961 pull_request, c.rhodecode_user, 'review_status_change')
964 pull_request, c.rhodecode_user, 'review_status_change')
962
965
963 calculated_status_lbl = ChangesetStatus.get_status_lbl(
966 calculated_status_lbl = ChangesetStatus.get_status_lbl(
964 calculated_status)
967 calculated_status)
965
968
966 if close_pr:
969 if close_pr:
967 status_completed = (
970 status_completed = (
968 calculated_status in [ChangesetStatus.STATUS_APPROVED,
971 calculated_status in [ChangesetStatus.STATUS_APPROVED,
969 ChangesetStatus.STATUS_REJECTED])
972 ChangesetStatus.STATUS_REJECTED])
970 if forced or status_completed:
973 if forced or status_completed:
971 PullRequestModel().close_pull_request(
974 PullRequestModel().close_pull_request(
972 pull_request_id, c.rhodecode_user)
975 pull_request_id, c.rhodecode_user)
973 else:
976 else:
974 h.flash(_('Closing pull request on other statuses than '
977 h.flash(_('Closing pull request on other statuses than '
975 'rejected or approved is forbidden. '
978 'rejected or approved is forbidden. '
976 'Calculated status from all reviewers '
979 'Calculated status from all reviewers '
977 'is currently: %s') % calculated_status_lbl,
980 'is currently: %s') % calculated_status_lbl,
978 category='warning')
981 category='warning')
979
982
980 Session().commit()
983 Session().commit()
981
984
982 if not request.is_xhr:
985 if not request.is_xhr:
983 return redirect(h.url('pullrequest_show', repo_name=repo_name,
986 return redirect(h.url('pullrequest_show', repo_name=repo_name,
984 pull_request_id=pull_request_id))
987 pull_request_id=pull_request_id))
985
988
986 data = {
989 data = {
987 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
990 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
988 }
991 }
989 if comm:
992 if comm:
990 c.co = comm
993 c.co = comm
991 c.inline_comment = True if comm.line_no else False
994 c.inline_comment = True if comm.line_no else False
992 data.update(comm.get_dict())
995 data.update(comm.get_dict())
993 data.update({'rendered_text':
996 data.update({'rendered_text':
994 render('changeset/changeset_comment_block.mako')})
997 render('changeset/changeset_comment_block.mako')})
995
998
996 return data
999 return data
997
1000
998 @LoginRequired()
1001 @LoginRequired()
999 @NotAnonymous()
1002 @NotAnonymous()
1000 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1003 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1001 'repository.admin')
1004 'repository.admin')
1002 @auth.CSRFRequired()
1005 @auth.CSRFRequired()
1003 @jsonify
1006 @jsonify
1004 def delete_comment(self, repo_name, comment_id):
1007 def delete_comment(self, repo_name, comment_id):
1005 return self._delete_comment(comment_id)
1008 return self._delete_comment(comment_id)
1006
1009
1007 def _delete_comment(self, comment_id):
1010 def _delete_comment(self, comment_id):
1008 comment_id = safe_int(comment_id)
1011 comment_id = safe_int(comment_id)
1009 co = ChangesetComment.get_or_404(comment_id)
1012 co = ChangesetComment.get_or_404(comment_id)
1010 if co.pull_request.is_closed():
1013 if co.pull_request.is_closed():
1011 # don't allow deleting comments on closed pull request
1014 # don't allow deleting comments on closed pull request
1012 raise HTTPForbidden()
1015 raise HTTPForbidden()
1013
1016
1014 is_owner = co.author.user_id == c.rhodecode_user.user_id
1017 is_owner = co.author.user_id == c.rhodecode_user.user_id
1015 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1018 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1016 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1019 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1017 old_calculated_status = co.pull_request.calculated_review_status()
1020 old_calculated_status = co.pull_request.calculated_review_status()
1018 CommentsModel().delete(comment=co)
1021 CommentsModel().delete(comment=co)
1019 Session().commit()
1022 Session().commit()
1020 calculated_status = co.pull_request.calculated_review_status()
1023 calculated_status = co.pull_request.calculated_review_status()
1021 if old_calculated_status != calculated_status:
1024 if old_calculated_status != calculated_status:
1022 PullRequestModel()._trigger_pull_request_hook(
1025 PullRequestModel()._trigger_pull_request_hook(
1023 co.pull_request, c.rhodecode_user, 'review_status_change')
1026 co.pull_request, c.rhodecode_user, 'review_status_change')
1024 return True
1027 return True
1025 else:
1028 else:
1026 raise HTTPForbidden()
1029 raise HTTPForbidden()
@@ -1,549 +1,551 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from datetime import datetime
29 from datetime import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry
32 from pyramid.threadlocal import get_current_registry
33 from sqlalchemy.sql.expression import null
33 from sqlalchemy.sql.expression import null
34 from sqlalchemy.sql.functions import coalesce
34 from sqlalchemy.sql.functions import coalesce
35
35
36 from rhodecode.lib import helpers as h, diffs
36 from rhodecode.lib import helpers as h, diffs
37 from rhodecode.lib.channelstream import channelstream_request
37 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.utils import action_logger
38 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils2 import extract_mentioned_users
39 from rhodecode.lib.utils2 import extract_mentioned_users
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 ChangesetComment, User, Notification, PullRequest)
42 ChangesetComment, User, Notification, PullRequest)
43 from rhodecode.model.notification import NotificationModel
43 from rhodecode.model.notification import NotificationModel
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import VcsSettingsModel
45 from rhodecode.model.settings import VcsSettingsModel
46 from rhodecode.model.notification import EmailNotificationModel
46 from rhodecode.model.notification import EmailNotificationModel
47 from rhodecode.model.validation_schema.schemas import comment_schema
47 from rhodecode.model.validation_schema.schemas import comment_schema
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class CommentsModel(BaseModel):
53 class CommentsModel(BaseModel):
54
54
55 cls = ChangesetComment
55 cls = ChangesetComment
56
56
57 DIFF_CONTEXT_BEFORE = 3
57 DIFF_CONTEXT_BEFORE = 3
58 DIFF_CONTEXT_AFTER = 3
58 DIFF_CONTEXT_AFTER = 3
59
59
60 def __get_commit_comment(self, changeset_comment):
60 def __get_commit_comment(self, changeset_comment):
61 return self._get_instance(ChangesetComment, changeset_comment)
61 return self._get_instance(ChangesetComment, changeset_comment)
62
62
63 def __get_pull_request(self, pull_request):
63 def __get_pull_request(self, pull_request):
64 return self._get_instance(PullRequest, pull_request)
64 return self._get_instance(PullRequest, pull_request)
65
65
66 def _extract_mentions(self, s):
66 def _extract_mentions(self, s):
67 user_objects = []
67 user_objects = []
68 for username in extract_mentioned_users(s):
68 for username in extract_mentioned_users(s):
69 user_obj = User.get_by_username(username, case_insensitive=True)
69 user_obj = User.get_by_username(username, case_insensitive=True)
70 if user_obj:
70 if user_obj:
71 user_objects.append(user_obj)
71 user_objects.append(user_obj)
72 return user_objects
72 return user_objects
73
73
74 def _get_renderer(self, global_renderer='rst'):
74 def _get_renderer(self, global_renderer='rst'):
75 try:
75 try:
76 # try reading from visual context
76 # try reading from visual context
77 from pylons import tmpl_context
77 from pylons import tmpl_context
78 global_renderer = tmpl_context.visual.default_renderer
78 global_renderer = tmpl_context.visual.default_renderer
79 except AttributeError:
79 except AttributeError:
80 log.debug("Renderer not set, falling back "
80 log.debug("Renderer not set, falling back "
81 "to default renderer '%s'", global_renderer)
81 "to default renderer '%s'", global_renderer)
82 except Exception:
82 except Exception:
83 log.error(traceback.format_exc())
83 log.error(traceback.format_exc())
84 return global_renderer
84 return global_renderer
85
85
86 def create(self, text, repo, user, commit_id=None, pull_request=None,
86 def create(self, text, repo, user, commit_id=None, pull_request=None,
87 f_path=None, line_no=None, status_change=None, comment_type=None,
87 f_path=None, line_no=None, status_change=None,
88 status_change_type=None, closing_pr=False,
88 status_change_type=None, comment_type=None,
89 send_email=True, renderer=None):
89 resolves_comment_id=None, closing_pr=False, send_email=True,
90 renderer=None):
90 """
91 """
91 Creates new comment for commit or pull request.
92 Creates new comment for commit or pull request.
92 IF status_change is not none this comment is associated with a
93 IF status_change is not none this comment is associated with a
93 status change of commit or commit associated with pull request
94 status change of commit or commit associated with pull request
94
95
95 :param text:
96 :param text:
96 :param repo:
97 :param repo:
97 :param user:
98 :param user:
98 :param commit_id:
99 :param commit_id:
99 :param pull_request:
100 :param pull_request:
100 :param f_path:
101 :param f_path:
101 :param line_no:
102 :param line_no:
102 :param status_change: Label for status change
103 :param status_change: Label for status change
103 :param comment_type: Type of comment
104 :param comment_type: Type of comment
104 :param status_change_type: type of status change
105 :param status_change_type: type of status change
105 :param closing_pr:
106 :param closing_pr:
106 :param send_email:
107 :param send_email:
107 :param renderer: pick renderer for this comment
108 :param renderer: pick renderer for this comment
108 """
109 """
109 if not text:
110 if not text:
110 log.warning('Missing text for comment, skipping...')
111 log.warning('Missing text for comment, skipping...')
111 return
112 return
112
113
113 if not renderer:
114 if not renderer:
114 renderer = self._get_renderer()
115 renderer = self._get_renderer()
115
116
117 repo = self._get_repo(repo)
118 user = self._get_user(user)
116
119
117 schema = comment_schema.CommentSchema()
120 schema = comment_schema.CommentSchema()
118 validated_kwargs = schema.deserialize(dict(
121 validated_kwargs = schema.deserialize(dict(
119 comment_body=text,
122 comment_body=text,
120 comment_type=comment_type,
123 comment_type=comment_type,
121 comment_file=f_path,
124 comment_file=f_path,
122 comment_line=line_no,
125 comment_line=line_no,
123 renderer_type=renderer,
126 renderer_type=renderer,
124 status_change=status_change,
127 status_change=status_change_type,
125
128 resolves_comment_id=resolves_comment_id,
126 repo=repo,
129 repo=repo.repo_id,
127 user=user,
130 user=user.user_id,
128 ))
131 ))
129
132
130 repo = self._get_repo(validated_kwargs['repo'])
131 user = self._get_user(validated_kwargs['user'])
132
133 comment = ChangesetComment()
133 comment = ChangesetComment()
134 comment.renderer = validated_kwargs['renderer_type']
134 comment.renderer = validated_kwargs['renderer_type']
135 comment.text = validated_kwargs['comment_body']
135 comment.text = validated_kwargs['comment_body']
136 comment.f_path = validated_kwargs['comment_file']
136 comment.f_path = validated_kwargs['comment_file']
137 comment.line_no = validated_kwargs['comment_line']
137 comment.line_no = validated_kwargs['comment_line']
138 comment.comment_type = validated_kwargs['comment_type']
138 comment.comment_type = validated_kwargs['comment_type']
139
139
140 comment.repo = repo
140 comment.repo = repo
141 comment.author = user
141 comment.author = user
142 comment.resolved_comment = self.__get_commit_comment(
143 validated_kwargs['resolves_comment_id'])
142
144
143 pull_request_id = pull_request
145 pull_request_id = pull_request
144
146
145 commit_obj = None
147 commit_obj = None
146 pull_request_obj = None
148 pull_request_obj = None
147
149
148 if commit_id:
150 if commit_id:
149 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
151 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
150 # do a lookup, so we don't pass something bad here
152 # do a lookup, so we don't pass something bad here
151 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
153 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
152 comment.revision = commit_obj.raw_id
154 comment.revision = commit_obj.raw_id
153
155
154 elif pull_request_id:
156 elif pull_request_id:
155 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
157 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
156 pull_request_obj = self.__get_pull_request(pull_request_id)
158 pull_request_obj = self.__get_pull_request(pull_request_id)
157 comment.pull_request = pull_request_obj
159 comment.pull_request = pull_request_obj
158 else:
160 else:
159 raise Exception('Please specify commit or pull_request_id')
161 raise Exception('Please specify commit or pull_request_id')
160
162
161 Session().add(comment)
163 Session().add(comment)
162 Session().flush()
164 Session().flush()
163 kwargs = {
165 kwargs = {
164 'user': user,
166 'user': user,
165 'renderer_type': renderer,
167 'renderer_type': renderer,
166 'repo_name': repo.repo_name,
168 'repo_name': repo.repo_name,
167 'status_change': status_change,
169 'status_change': status_change,
168 'status_change_type': status_change_type,
170 'status_change_type': status_change_type,
169 'comment_body': text,
171 'comment_body': text,
170 'comment_file': f_path,
172 'comment_file': f_path,
171 'comment_line': line_no,
173 'comment_line': line_no,
172 }
174 }
173
175
174 if commit_obj:
176 if commit_obj:
175 recipients = ChangesetComment.get_users(
177 recipients = ChangesetComment.get_users(
176 revision=commit_obj.raw_id)
178 revision=commit_obj.raw_id)
177 # add commit author if it's in RhodeCode system
179 # add commit author if it's in RhodeCode system
178 cs_author = User.get_from_cs_author(commit_obj.author)
180 cs_author = User.get_from_cs_author(commit_obj.author)
179 if not cs_author:
181 if not cs_author:
180 # use repo owner if we cannot extract the author correctly
182 # use repo owner if we cannot extract the author correctly
181 cs_author = repo.user
183 cs_author = repo.user
182 recipients += [cs_author]
184 recipients += [cs_author]
183
185
184 commit_comment_url = self.get_url(comment)
186 commit_comment_url = self.get_url(comment)
185
187
186 target_repo_url = h.link_to(
188 target_repo_url = h.link_to(
187 repo.repo_name,
189 repo.repo_name,
188 h.url('summary_home',
190 h.url('summary_home',
189 repo_name=repo.repo_name, qualified=True))
191 repo_name=repo.repo_name, qualified=True))
190
192
191 # commit specifics
193 # commit specifics
192 kwargs.update({
194 kwargs.update({
193 'commit': commit_obj,
195 'commit': commit_obj,
194 'commit_message': commit_obj.message,
196 'commit_message': commit_obj.message,
195 'commit_target_repo': target_repo_url,
197 'commit_target_repo': target_repo_url,
196 'commit_comment_url': commit_comment_url,
198 'commit_comment_url': commit_comment_url,
197 })
199 })
198
200
199 elif pull_request_obj:
201 elif pull_request_obj:
200 # get the current participants of this pull request
202 # get the current participants of this pull request
201 recipients = ChangesetComment.get_users(
203 recipients = ChangesetComment.get_users(
202 pull_request_id=pull_request_obj.pull_request_id)
204 pull_request_id=pull_request_obj.pull_request_id)
203 # add pull request author
205 # add pull request author
204 recipients += [pull_request_obj.author]
206 recipients += [pull_request_obj.author]
205
207
206 # add the reviewers to notification
208 # add the reviewers to notification
207 recipients += [x.user for x in pull_request_obj.reviewers]
209 recipients += [x.user for x in pull_request_obj.reviewers]
208
210
209 pr_target_repo = pull_request_obj.target_repo
211 pr_target_repo = pull_request_obj.target_repo
210 pr_source_repo = pull_request_obj.source_repo
212 pr_source_repo = pull_request_obj.source_repo
211
213
212 pr_comment_url = h.url(
214 pr_comment_url = h.url(
213 'pullrequest_show',
215 'pullrequest_show',
214 repo_name=pr_target_repo.repo_name,
216 repo_name=pr_target_repo.repo_name,
215 pull_request_id=pull_request_obj.pull_request_id,
217 pull_request_id=pull_request_obj.pull_request_id,
216 anchor='comment-%s' % comment.comment_id,
218 anchor='comment-%s' % comment.comment_id,
217 qualified=True,)
219 qualified=True,)
218
220
219 # set some variables for email notification
221 # set some variables for email notification
220 pr_target_repo_url = h.url(
222 pr_target_repo_url = h.url(
221 'summary_home', repo_name=pr_target_repo.repo_name,
223 'summary_home', repo_name=pr_target_repo.repo_name,
222 qualified=True)
224 qualified=True)
223
225
224 pr_source_repo_url = h.url(
226 pr_source_repo_url = h.url(
225 'summary_home', repo_name=pr_source_repo.repo_name,
227 'summary_home', repo_name=pr_source_repo.repo_name,
226 qualified=True)
228 qualified=True)
227
229
228 # pull request specifics
230 # pull request specifics
229 kwargs.update({
231 kwargs.update({
230 'pull_request': pull_request_obj,
232 'pull_request': pull_request_obj,
231 'pr_id': pull_request_obj.pull_request_id,
233 'pr_id': pull_request_obj.pull_request_id,
232 'pr_target_repo': pr_target_repo,
234 'pr_target_repo': pr_target_repo,
233 'pr_target_repo_url': pr_target_repo_url,
235 'pr_target_repo_url': pr_target_repo_url,
234 'pr_source_repo': pr_source_repo,
236 'pr_source_repo': pr_source_repo,
235 'pr_source_repo_url': pr_source_repo_url,
237 'pr_source_repo_url': pr_source_repo_url,
236 'pr_comment_url': pr_comment_url,
238 'pr_comment_url': pr_comment_url,
237 'pr_closing': closing_pr,
239 'pr_closing': closing_pr,
238 })
240 })
239 if send_email:
241 if send_email:
240 # pre-generate the subject for notification itself
242 # pre-generate the subject for notification itself
241 (subject,
243 (subject,
242 _h, _e, # we don't care about those
244 _h, _e, # we don't care about those
243 body_plaintext) = EmailNotificationModel().render_email(
245 body_plaintext) = EmailNotificationModel().render_email(
244 notification_type, **kwargs)
246 notification_type, **kwargs)
245
247
246 mention_recipients = set(
248 mention_recipients = set(
247 self._extract_mentions(text)).difference(recipients)
249 self._extract_mentions(text)).difference(recipients)
248
250
249 # create notification objects, and emails
251 # create notification objects, and emails
250 NotificationModel().create(
252 NotificationModel().create(
251 created_by=user,
253 created_by=user,
252 notification_subject=subject,
254 notification_subject=subject,
253 notification_body=body_plaintext,
255 notification_body=body_plaintext,
254 notification_type=notification_type,
256 notification_type=notification_type,
255 recipients=recipients,
257 recipients=recipients,
256 mention_recipients=mention_recipients,
258 mention_recipients=mention_recipients,
257 email_kwargs=kwargs,
259 email_kwargs=kwargs,
258 )
260 )
259
261
260 action = (
262 action = (
261 'user_commented_pull_request:{}'.format(
263 'user_commented_pull_request:{}'.format(
262 comment.pull_request.pull_request_id)
264 comment.pull_request.pull_request_id)
263 if comment.pull_request
265 if comment.pull_request
264 else 'user_commented_revision:{}'.format(comment.revision)
266 else 'user_commented_revision:{}'.format(comment.revision)
265 )
267 )
266 action_logger(user, action, comment.repo)
268 action_logger(user, action, comment.repo)
267
269
268 registry = get_current_registry()
270 registry = get_current_registry()
269 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
271 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
270 channelstream_config = rhodecode_plugins.get('channelstream', {})
272 channelstream_config = rhodecode_plugins.get('channelstream', {})
271 msg_url = ''
273 msg_url = ''
272 if commit_obj:
274 if commit_obj:
273 msg_url = commit_comment_url
275 msg_url = commit_comment_url
274 repo_name = repo.repo_name
276 repo_name = repo.repo_name
275 elif pull_request_obj:
277 elif pull_request_obj:
276 msg_url = pr_comment_url
278 msg_url = pr_comment_url
277 repo_name = pr_target_repo.repo_name
279 repo_name = pr_target_repo.repo_name
278
280
279 if channelstream_config.get('enabled'):
281 if channelstream_config.get('enabled'):
280 message = '<strong>{}</strong> {} - ' \
282 message = '<strong>{}</strong> {} - ' \
281 '<a onclick="window.location=\'{}\';' \
283 '<a onclick="window.location=\'{}\';' \
282 'window.location.reload()">' \
284 'window.location.reload()">' \
283 '<strong>{}</strong></a>'
285 '<strong>{}</strong></a>'
284 message = message.format(
286 message = message.format(
285 user.username, _('made a comment'), msg_url,
287 user.username, _('made a comment'), msg_url,
286 _('Show it now'))
288 _('Show it now'))
287 channel = '/repo${}$/pr/{}'.format(
289 channel = '/repo${}$/pr/{}'.format(
288 repo_name,
290 repo_name,
289 pull_request_id
291 pull_request_id
290 )
292 )
291 payload = {
293 payload = {
292 'type': 'message',
294 'type': 'message',
293 'timestamp': datetime.utcnow(),
295 'timestamp': datetime.utcnow(),
294 'user': 'system',
296 'user': 'system',
295 'exclude_users': [user.username],
297 'exclude_users': [user.username],
296 'channel': channel,
298 'channel': channel,
297 'message': {
299 'message': {
298 'message': message,
300 'message': message,
299 'level': 'info',
301 'level': 'info',
300 'topic': '/notifications'
302 'topic': '/notifications'
301 }
303 }
302 }
304 }
303 channelstream_request(channelstream_config, [payload],
305 channelstream_request(channelstream_config, [payload],
304 '/message', raise_exc=False)
306 '/message', raise_exc=False)
305
307
306 return comment
308 return comment
307
309
308 def delete(self, comment):
310 def delete(self, comment):
309 """
311 """
310 Deletes given comment
312 Deletes given comment
311
313
312 :param comment_id:
314 :param comment_id:
313 """
315 """
314 comment = self.__get_commit_comment(comment)
316 comment = self.__get_commit_comment(comment)
315 Session().delete(comment)
317 Session().delete(comment)
316
318
317 return comment
319 return comment
318
320
319 def get_all_comments(self, repo_id, revision=None, pull_request=None):
321 def get_all_comments(self, repo_id, revision=None, pull_request=None):
320 q = ChangesetComment.query()\
322 q = ChangesetComment.query()\
321 .filter(ChangesetComment.repo_id == repo_id)
323 .filter(ChangesetComment.repo_id == repo_id)
322 if revision:
324 if revision:
323 q = q.filter(ChangesetComment.revision == revision)
325 q = q.filter(ChangesetComment.revision == revision)
324 elif pull_request:
326 elif pull_request:
325 pull_request = self.__get_pull_request(pull_request)
327 pull_request = self.__get_pull_request(pull_request)
326 q = q.filter(ChangesetComment.pull_request == pull_request)
328 q = q.filter(ChangesetComment.pull_request == pull_request)
327 else:
329 else:
328 raise Exception('Please specify commit or pull_request')
330 raise Exception('Please specify commit or pull_request')
329 q = q.order_by(ChangesetComment.created_on)
331 q = q.order_by(ChangesetComment.created_on)
330 return q.all()
332 return q.all()
331
333
332 def get_url(self, comment):
334 def get_url(self, comment):
333 comment = self.__get_commit_comment(comment)
335 comment = self.__get_commit_comment(comment)
334 if comment.pull_request:
336 if comment.pull_request:
335 return h.url(
337 return h.url(
336 'pullrequest_show',
338 'pullrequest_show',
337 repo_name=comment.pull_request.target_repo.repo_name,
339 repo_name=comment.pull_request.target_repo.repo_name,
338 pull_request_id=comment.pull_request.pull_request_id,
340 pull_request_id=comment.pull_request.pull_request_id,
339 anchor='comment-%s' % comment.comment_id,
341 anchor='comment-%s' % comment.comment_id,
340 qualified=True,)
342 qualified=True,)
341 else:
343 else:
342 return h.url(
344 return h.url(
343 'changeset_home',
345 'changeset_home',
344 repo_name=comment.repo.repo_name,
346 repo_name=comment.repo.repo_name,
345 revision=comment.revision,
347 revision=comment.revision,
346 anchor='comment-%s' % comment.comment_id,
348 anchor='comment-%s' % comment.comment_id,
347 qualified=True,)
349 qualified=True,)
348
350
349 def get_comments(self, repo_id, revision=None, pull_request=None):
351 def get_comments(self, repo_id, revision=None, pull_request=None):
350 """
352 """
351 Gets main comments based on revision or pull_request_id
353 Gets main comments based on revision or pull_request_id
352
354
353 :param repo_id:
355 :param repo_id:
354 :param revision:
356 :param revision:
355 :param pull_request:
357 :param pull_request:
356 """
358 """
357
359
358 q = ChangesetComment.query()\
360 q = ChangesetComment.query()\
359 .filter(ChangesetComment.repo_id == repo_id)\
361 .filter(ChangesetComment.repo_id == repo_id)\
360 .filter(ChangesetComment.line_no == None)\
362 .filter(ChangesetComment.line_no == None)\
361 .filter(ChangesetComment.f_path == None)
363 .filter(ChangesetComment.f_path == None)
362 if revision:
364 if revision:
363 q = q.filter(ChangesetComment.revision == revision)
365 q = q.filter(ChangesetComment.revision == revision)
364 elif pull_request:
366 elif pull_request:
365 pull_request = self.__get_pull_request(pull_request)
367 pull_request = self.__get_pull_request(pull_request)
366 q = q.filter(ChangesetComment.pull_request == pull_request)
368 q = q.filter(ChangesetComment.pull_request == pull_request)
367 else:
369 else:
368 raise Exception('Please specify commit or pull_request')
370 raise Exception('Please specify commit or pull_request')
369 q = q.order_by(ChangesetComment.created_on)
371 q = q.order_by(ChangesetComment.created_on)
370 return q.all()
372 return q.all()
371
373
372 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
374 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
373 q = self._get_inline_comments_query(repo_id, revision, pull_request)
375 q = self._get_inline_comments_query(repo_id, revision, pull_request)
374 return self._group_comments_by_path_and_line_number(q)
376 return self._group_comments_by_path_and_line_number(q)
375
377
376 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
378 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
377 version=None, include_aggregates=False):
379 version=None, include_aggregates=False):
378 version_aggregates = collections.defaultdict(list)
380 version_aggregates = collections.defaultdict(list)
379 inline_cnt = 0
381 inline_cnt = 0
380 for fname, per_line_comments in inline_comments.iteritems():
382 for fname, per_line_comments in inline_comments.iteritems():
381 for lno, comments in per_line_comments.iteritems():
383 for lno, comments in per_line_comments.iteritems():
382 for comm in comments:
384 for comm in comments:
383 version_aggregates[comm.pull_request_version_id].append(comm)
385 version_aggregates[comm.pull_request_version_id].append(comm)
384 if not comm.outdated_at_version(version) and skip_outdated:
386 if not comm.outdated_at_version(version) and skip_outdated:
385 inline_cnt += 1
387 inline_cnt += 1
386
388
387 if include_aggregates:
389 if include_aggregates:
388 return inline_cnt, version_aggregates
390 return inline_cnt, version_aggregates
389 return inline_cnt
391 return inline_cnt
390
392
391 def get_outdated_comments(self, repo_id, pull_request):
393 def get_outdated_comments(self, repo_id, pull_request):
392 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
394 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
393 # of a pull request.
395 # of a pull request.
394 q = self._all_inline_comments_of_pull_request(pull_request)
396 q = self._all_inline_comments_of_pull_request(pull_request)
395 q = q.filter(
397 q = q.filter(
396 ChangesetComment.display_state ==
398 ChangesetComment.display_state ==
397 ChangesetComment.COMMENT_OUTDATED
399 ChangesetComment.COMMENT_OUTDATED
398 ).order_by(ChangesetComment.comment_id.asc())
400 ).order_by(ChangesetComment.comment_id.asc())
399
401
400 return self._group_comments_by_path_and_line_number(q)
402 return self._group_comments_by_path_and_line_number(q)
401
403
402 def _get_inline_comments_query(self, repo_id, revision, pull_request):
404 def _get_inline_comments_query(self, repo_id, revision, pull_request):
403 # TODO: johbo: Split this into two methods: One for PR and one for
405 # TODO: johbo: Split this into two methods: One for PR and one for
404 # commit.
406 # commit.
405 if revision:
407 if revision:
406 q = Session().query(ChangesetComment).filter(
408 q = Session().query(ChangesetComment).filter(
407 ChangesetComment.repo_id == repo_id,
409 ChangesetComment.repo_id == repo_id,
408 ChangesetComment.line_no != null(),
410 ChangesetComment.line_no != null(),
409 ChangesetComment.f_path != null(),
411 ChangesetComment.f_path != null(),
410 ChangesetComment.revision == revision)
412 ChangesetComment.revision == revision)
411
413
412 elif pull_request:
414 elif pull_request:
413 pull_request = self.__get_pull_request(pull_request)
415 pull_request = self.__get_pull_request(pull_request)
414 if not CommentsModel.use_outdated_comments(pull_request):
416 if not CommentsModel.use_outdated_comments(pull_request):
415 q = self._visible_inline_comments_of_pull_request(pull_request)
417 q = self._visible_inline_comments_of_pull_request(pull_request)
416 else:
418 else:
417 q = self._all_inline_comments_of_pull_request(pull_request)
419 q = self._all_inline_comments_of_pull_request(pull_request)
418
420
419 else:
421 else:
420 raise Exception('Please specify commit or pull_request_id')
422 raise Exception('Please specify commit or pull_request_id')
421 q = q.order_by(ChangesetComment.comment_id.asc())
423 q = q.order_by(ChangesetComment.comment_id.asc())
422 return q
424 return q
423
425
424 def _group_comments_by_path_and_line_number(self, q):
426 def _group_comments_by_path_and_line_number(self, q):
425 comments = q.all()
427 comments = q.all()
426 paths = collections.defaultdict(lambda: collections.defaultdict(list))
428 paths = collections.defaultdict(lambda: collections.defaultdict(list))
427 for co in comments:
429 for co in comments:
428 paths[co.f_path][co.line_no].append(co)
430 paths[co.f_path][co.line_no].append(co)
429 return paths
431 return paths
430
432
431 @classmethod
433 @classmethod
432 def needed_extra_diff_context(cls):
434 def needed_extra_diff_context(cls):
433 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
435 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
434
436
435 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
437 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
436 if not CommentsModel.use_outdated_comments(pull_request):
438 if not CommentsModel.use_outdated_comments(pull_request):
437 return
439 return
438
440
439 comments = self._visible_inline_comments_of_pull_request(pull_request)
441 comments = self._visible_inline_comments_of_pull_request(pull_request)
440 comments_to_outdate = comments.all()
442 comments_to_outdate = comments.all()
441
443
442 for comment in comments_to_outdate:
444 for comment in comments_to_outdate:
443 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
445 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
444
446
445 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
447 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
446 diff_line = _parse_comment_line_number(comment.line_no)
448 diff_line = _parse_comment_line_number(comment.line_no)
447
449
448 try:
450 try:
449 old_context = old_diff_proc.get_context_of_line(
451 old_context = old_diff_proc.get_context_of_line(
450 path=comment.f_path, diff_line=diff_line)
452 path=comment.f_path, diff_line=diff_line)
451 new_context = new_diff_proc.get_context_of_line(
453 new_context = new_diff_proc.get_context_of_line(
452 path=comment.f_path, diff_line=diff_line)
454 path=comment.f_path, diff_line=diff_line)
453 except (diffs.LineNotInDiffException,
455 except (diffs.LineNotInDiffException,
454 diffs.FileNotInDiffException):
456 diffs.FileNotInDiffException):
455 comment.display_state = ChangesetComment.COMMENT_OUTDATED
457 comment.display_state = ChangesetComment.COMMENT_OUTDATED
456 return
458 return
457
459
458 if old_context == new_context:
460 if old_context == new_context:
459 return
461 return
460
462
461 if self._should_relocate_diff_line(diff_line):
463 if self._should_relocate_diff_line(diff_line):
462 new_diff_lines = new_diff_proc.find_context(
464 new_diff_lines = new_diff_proc.find_context(
463 path=comment.f_path, context=old_context,
465 path=comment.f_path, context=old_context,
464 offset=self.DIFF_CONTEXT_BEFORE)
466 offset=self.DIFF_CONTEXT_BEFORE)
465 if not new_diff_lines:
467 if not new_diff_lines:
466 comment.display_state = ChangesetComment.COMMENT_OUTDATED
468 comment.display_state = ChangesetComment.COMMENT_OUTDATED
467 else:
469 else:
468 new_diff_line = self._choose_closest_diff_line(
470 new_diff_line = self._choose_closest_diff_line(
469 diff_line, new_diff_lines)
471 diff_line, new_diff_lines)
470 comment.line_no = _diff_to_comment_line_number(new_diff_line)
472 comment.line_no = _diff_to_comment_line_number(new_diff_line)
471 else:
473 else:
472 comment.display_state = ChangesetComment.COMMENT_OUTDATED
474 comment.display_state = ChangesetComment.COMMENT_OUTDATED
473
475
474 def _should_relocate_diff_line(self, diff_line):
476 def _should_relocate_diff_line(self, diff_line):
475 """
477 """
476 Checks if relocation shall be tried for the given `diff_line`.
478 Checks if relocation shall be tried for the given `diff_line`.
477
479
478 If a comment points into the first lines, then we can have a situation
480 If a comment points into the first lines, then we can have a situation
479 that after an update another line has been added on top. In this case
481 that after an update another line has been added on top. In this case
480 we would find the context still and move the comment around. This
482 we would find the context still and move the comment around. This
481 would be wrong.
483 would be wrong.
482 """
484 """
483 should_relocate = (
485 should_relocate = (
484 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
486 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
485 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
487 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
486 return should_relocate
488 return should_relocate
487
489
488 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
490 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
489 candidate = new_diff_lines[0]
491 candidate = new_diff_lines[0]
490 best_delta = _diff_line_delta(diff_line, candidate)
492 best_delta = _diff_line_delta(diff_line, candidate)
491 for new_diff_line in new_diff_lines[1:]:
493 for new_diff_line in new_diff_lines[1:]:
492 delta = _diff_line_delta(diff_line, new_diff_line)
494 delta = _diff_line_delta(diff_line, new_diff_line)
493 if delta < best_delta:
495 if delta < best_delta:
494 candidate = new_diff_line
496 candidate = new_diff_line
495 best_delta = delta
497 best_delta = delta
496 return candidate
498 return candidate
497
499
498 def _visible_inline_comments_of_pull_request(self, pull_request):
500 def _visible_inline_comments_of_pull_request(self, pull_request):
499 comments = self._all_inline_comments_of_pull_request(pull_request)
501 comments = self._all_inline_comments_of_pull_request(pull_request)
500 comments = comments.filter(
502 comments = comments.filter(
501 coalesce(ChangesetComment.display_state, '') !=
503 coalesce(ChangesetComment.display_state, '') !=
502 ChangesetComment.COMMENT_OUTDATED)
504 ChangesetComment.COMMENT_OUTDATED)
503 return comments
505 return comments
504
506
505 def _all_inline_comments_of_pull_request(self, pull_request):
507 def _all_inline_comments_of_pull_request(self, pull_request):
506 comments = Session().query(ChangesetComment)\
508 comments = Session().query(ChangesetComment)\
507 .filter(ChangesetComment.line_no != None)\
509 .filter(ChangesetComment.line_no != None)\
508 .filter(ChangesetComment.f_path != None)\
510 .filter(ChangesetComment.f_path != None)\
509 .filter(ChangesetComment.pull_request == pull_request)
511 .filter(ChangesetComment.pull_request == pull_request)
510 return comments
512 return comments
511
513
512 @staticmethod
514 @staticmethod
513 def use_outdated_comments(pull_request):
515 def use_outdated_comments(pull_request):
514 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
516 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
515 settings = settings_model.get_general_settings()
517 settings = settings_model.get_general_settings()
516 return settings.get('rhodecode_use_outdated_comments', False)
518 return settings.get('rhodecode_use_outdated_comments', False)
517
519
518
520
519 def _parse_comment_line_number(line_no):
521 def _parse_comment_line_number(line_no):
520 """
522 """
521 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
523 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
522 """
524 """
523 old_line = None
525 old_line = None
524 new_line = None
526 new_line = None
525 if line_no.startswith('o'):
527 if line_no.startswith('o'):
526 old_line = int(line_no[1:])
528 old_line = int(line_no[1:])
527 elif line_no.startswith('n'):
529 elif line_no.startswith('n'):
528 new_line = int(line_no[1:])
530 new_line = int(line_no[1:])
529 else:
531 else:
530 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
532 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
531 return diffs.DiffLineNumber(old_line, new_line)
533 return diffs.DiffLineNumber(old_line, new_line)
532
534
533
535
534 def _diff_to_comment_line_number(diff_line):
536 def _diff_to_comment_line_number(diff_line):
535 if diff_line.new is not None:
537 if diff_line.new is not None:
536 return u'n{}'.format(diff_line.new)
538 return u'n{}'.format(diff_line.new)
537 elif diff_line.old is not None:
539 elif diff_line.old is not None:
538 return u'o{}'.format(diff_line.old)
540 return u'o{}'.format(diff_line.old)
539 return u''
541 return u''
540
542
541
543
542 def _diff_line_delta(a, b):
544 def _diff_line_delta(a, b):
543 if None not in (a.new, b.new):
545 if None not in (a.new, b.new):
544 return abs(a.new - b.new)
546 return abs(a.new - b.new)
545 elif None not in (a.old, b.old):
547 elif None not in (a.old, b.old):
546 return abs(a.old - b.old)
548 return abs(a.old - b.old)
547 else:
549 else:
548 raise ValueError(
550 raise ValueError(
549 "Cannot compute delta between {} and {}".format(a, b))
551 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,3829 +1,3833 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37
37
38 from sqlalchemy import *
38 from sqlalchemy import *
39 from sqlalchemy.ext.declarative import declared_attr
39 from sqlalchemy.ext.declarative import declared_attr
40 from sqlalchemy.ext.hybrid import hybrid_property
40 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.orm import (
41 from sqlalchemy.orm import (
42 relationship, joinedload, class_mapper, validates, aliased)
42 relationship, joinedload, class_mapper, validates, aliased)
43 from sqlalchemy.sql.expression import true
43 from sqlalchemy.sql.expression import true
44 from beaker.cache import cache_region
44 from beaker.cache import cache_region
45 from webob.exc import HTTPNotFound
45 from webob.exc import HTTPNotFound
46 from zope.cachedescriptors.property import Lazy as LazyProperty
46 from zope.cachedescriptors.property import Lazy as LazyProperty
47
47
48 from pylons import url
48 from pylons import url
49 from pylons.i18n.translation import lazy_ugettext as _
49 from pylons.i18n.translation import lazy_ugettext as _
50
50
51 from rhodecode.lib.vcs import get_vcs_instance
51 from rhodecode.lib.vcs import get_vcs_instance
52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 from rhodecode.lib.utils2 import (
53 from rhodecode.lib.utils2 import (
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 glob2re, StrictAttributeDict)
56 glob2re, StrictAttributeDict)
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 from rhodecode.lib.ext_json import json
58 from rhodecode.lib.ext_json import json
59 from rhodecode.lib.caching_query import FromCache
59 from rhodecode.lib.caching_query import FromCache
60 from rhodecode.lib.encrypt import AESCipher
60 from rhodecode.lib.encrypt import AESCipher
61
61
62 from rhodecode.model.meta import Base, Session
62 from rhodecode.model.meta import Base, Session
63
63
64 URL_SEP = '/'
64 URL_SEP = '/'
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67 # =============================================================================
67 # =============================================================================
68 # BASE CLASSES
68 # BASE CLASSES
69 # =============================================================================
69 # =============================================================================
70
70
71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 # beaker.session.secret if first is not set.
72 # beaker.session.secret if first is not set.
73 # and initialized at environment.py
73 # and initialized at environment.py
74 ENCRYPTION_KEY = None
74 ENCRYPTION_KEY = None
75
75
76 # used to sort permissions by types, '#' used here is not allowed to be in
76 # used to sort permissions by types, '#' used here is not allowed to be in
77 # usernames, and it's very early in sorted string.printable table.
77 # usernames, and it's very early in sorted string.printable table.
78 PERMISSION_TYPE_SORT = {
78 PERMISSION_TYPE_SORT = {
79 'admin': '####',
79 'admin': '####',
80 'write': '###',
80 'write': '###',
81 'read': '##',
81 'read': '##',
82 'none': '#',
82 'none': '#',
83 }
83 }
84
84
85
85
86 def display_sort(obj):
86 def display_sort(obj):
87 """
87 """
88 Sort function used to sort permissions in .permissions() function of
88 Sort function used to sort permissions in .permissions() function of
89 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 of all other resources
90 of all other resources
91 """
91 """
92
92
93 if obj.username == User.DEFAULT_USER:
93 if obj.username == User.DEFAULT_USER:
94 return '#####'
94 return '#####'
95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 return prefix + obj.username
96 return prefix + obj.username
97
97
98
98
99 def _hash_key(k):
99 def _hash_key(k):
100 return md5_safe(k)
100 return md5_safe(k)
101
101
102
102
103 class EncryptedTextValue(TypeDecorator):
103 class EncryptedTextValue(TypeDecorator):
104 """
104 """
105 Special column for encrypted long text data, use like::
105 Special column for encrypted long text data, use like::
106
106
107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108
108
109 This column is intelligent so if value is in unencrypted form it return
109 This column is intelligent so if value is in unencrypted form it return
110 unencrypted form, but on save it always encrypts
110 unencrypted form, but on save it always encrypts
111 """
111 """
112 impl = Text
112 impl = Text
113
113
114 def process_bind_param(self, value, dialect):
114 def process_bind_param(self, value, dialect):
115 if not value:
115 if not value:
116 return value
116 return value
117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 # protect against double encrypting if someone manually starts
118 # protect against double encrypting if someone manually starts
119 # doing
119 # doing
120 raise ValueError('value needs to be in unencrypted format, ie. '
120 raise ValueError('value needs to be in unencrypted format, ie. '
121 'not starting with enc$aes')
121 'not starting with enc$aes')
122 return 'enc$aes_hmac$%s' % AESCipher(
122 return 'enc$aes_hmac$%s' % AESCipher(
123 ENCRYPTION_KEY, hmac=True).encrypt(value)
123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124
124
125 def process_result_value(self, value, dialect):
125 def process_result_value(self, value, dialect):
126 import rhodecode
126 import rhodecode
127
127
128 if not value:
128 if not value:
129 return value
129 return value
130
130
131 parts = value.split('$', 3)
131 parts = value.split('$', 3)
132 if not len(parts) == 3:
132 if not len(parts) == 3:
133 # probably not encrypted values
133 # probably not encrypted values
134 return value
134 return value
135 else:
135 else:
136 if parts[0] != 'enc':
136 if parts[0] != 'enc':
137 # parts ok but without our header ?
137 # parts ok but without our header ?
138 return value
138 return value
139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 'rhodecode.encrypted_values.strict') or True)
140 'rhodecode.encrypted_values.strict') or True)
141 # at that stage we know it's our encryption
141 # at that stage we know it's our encryption
142 if parts[1] == 'aes':
142 if parts[1] == 'aes':
143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 elif parts[1] == 'aes_hmac':
144 elif parts[1] == 'aes_hmac':
145 decrypted_data = AESCipher(
145 decrypted_data = AESCipher(
146 ENCRYPTION_KEY, hmac=True,
146 ENCRYPTION_KEY, hmac=True,
147 strict_verification=enc_strict_mode).decrypt(parts[2])
147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 else:
148 else:
149 raise ValueError(
149 raise ValueError(
150 'Encryption type part is wrong, must be `aes` '
150 'Encryption type part is wrong, must be `aes` '
151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 return decrypted_data
152 return decrypted_data
153
153
154
154
155 class BaseModel(object):
155 class BaseModel(object):
156 """
156 """
157 Base Model for all classes
157 Base Model for all classes
158 """
158 """
159
159
160 @classmethod
160 @classmethod
161 def _get_keys(cls):
161 def _get_keys(cls):
162 """return column names for this model """
162 """return column names for this model """
163 return class_mapper(cls).c.keys()
163 return class_mapper(cls).c.keys()
164
164
165 def get_dict(self):
165 def get_dict(self):
166 """
166 """
167 return dict with keys and values corresponding
167 return dict with keys and values corresponding
168 to this model data """
168 to this model data """
169
169
170 d = {}
170 d = {}
171 for k in self._get_keys():
171 for k in self._get_keys():
172 d[k] = getattr(self, k)
172 d[k] = getattr(self, k)
173
173
174 # also use __json__() if present to get additional fields
174 # also use __json__() if present to get additional fields
175 _json_attr = getattr(self, '__json__', None)
175 _json_attr = getattr(self, '__json__', None)
176 if _json_attr:
176 if _json_attr:
177 # update with attributes from __json__
177 # update with attributes from __json__
178 if callable(_json_attr):
178 if callable(_json_attr):
179 _json_attr = _json_attr()
179 _json_attr = _json_attr()
180 for k, val in _json_attr.iteritems():
180 for k, val in _json_attr.iteritems():
181 d[k] = val
181 d[k] = val
182 return d
182 return d
183
183
184 def get_appstruct(self):
184 def get_appstruct(self):
185 """return list with keys and values tuples corresponding
185 """return list with keys and values tuples corresponding
186 to this model data """
186 to this model data """
187
187
188 l = []
188 l = []
189 for k in self._get_keys():
189 for k in self._get_keys():
190 l.append((k, getattr(self, k),))
190 l.append((k, getattr(self, k),))
191 return l
191 return l
192
192
193 def populate_obj(self, populate_dict):
193 def populate_obj(self, populate_dict):
194 """populate model with data from given populate_dict"""
194 """populate model with data from given populate_dict"""
195
195
196 for k in self._get_keys():
196 for k in self._get_keys():
197 if k in populate_dict:
197 if k in populate_dict:
198 setattr(self, k, populate_dict[k])
198 setattr(self, k, populate_dict[k])
199
199
200 @classmethod
200 @classmethod
201 def query(cls):
201 def query(cls):
202 return Session().query(cls)
202 return Session().query(cls)
203
203
204 @classmethod
204 @classmethod
205 def get(cls, id_):
205 def get(cls, id_):
206 if id_:
206 if id_:
207 return cls.query().get(id_)
207 return cls.query().get(id_)
208
208
209 @classmethod
209 @classmethod
210 def get_or_404(cls, id_):
210 def get_or_404(cls, id_):
211 try:
211 try:
212 id_ = int(id_)
212 id_ = int(id_)
213 except (TypeError, ValueError):
213 except (TypeError, ValueError):
214 raise HTTPNotFound
214 raise HTTPNotFound
215
215
216 res = cls.query().get(id_)
216 res = cls.query().get(id_)
217 if not res:
217 if not res:
218 raise HTTPNotFound
218 raise HTTPNotFound
219 return res
219 return res
220
220
221 @classmethod
221 @classmethod
222 def getAll(cls):
222 def getAll(cls):
223 # deprecated and left for backward compatibility
223 # deprecated and left for backward compatibility
224 return cls.get_all()
224 return cls.get_all()
225
225
226 @classmethod
226 @classmethod
227 def get_all(cls):
227 def get_all(cls):
228 return cls.query().all()
228 return cls.query().all()
229
229
230 @classmethod
230 @classmethod
231 def delete(cls, id_):
231 def delete(cls, id_):
232 obj = cls.query().get(id_)
232 obj = cls.query().get(id_)
233 Session().delete(obj)
233 Session().delete(obj)
234
234
235 @classmethod
235 @classmethod
236 def identity_cache(cls, session, attr_name, value):
236 def identity_cache(cls, session, attr_name, value):
237 exist_in_session = []
237 exist_in_session = []
238 for (item_cls, pkey), instance in session.identity_map.items():
238 for (item_cls, pkey), instance in session.identity_map.items():
239 if cls == item_cls and getattr(instance, attr_name) == value:
239 if cls == item_cls and getattr(instance, attr_name) == value:
240 exist_in_session.append(instance)
240 exist_in_session.append(instance)
241 if exist_in_session:
241 if exist_in_session:
242 if len(exist_in_session) == 1:
242 if len(exist_in_session) == 1:
243 return exist_in_session[0]
243 return exist_in_session[0]
244 log.exception(
244 log.exception(
245 'multiple objects with attr %s and '
245 'multiple objects with attr %s and '
246 'value %s found with same name: %r',
246 'value %s found with same name: %r',
247 attr_name, value, exist_in_session)
247 attr_name, value, exist_in_session)
248
248
249 def __repr__(self):
249 def __repr__(self):
250 if hasattr(self, '__unicode__'):
250 if hasattr(self, '__unicode__'):
251 # python repr needs to return str
251 # python repr needs to return str
252 try:
252 try:
253 return safe_str(self.__unicode__())
253 return safe_str(self.__unicode__())
254 except UnicodeDecodeError:
254 except UnicodeDecodeError:
255 pass
255 pass
256 return '<DB:%s>' % (self.__class__.__name__)
256 return '<DB:%s>' % (self.__class__.__name__)
257
257
258
258
259 class RhodeCodeSetting(Base, BaseModel):
259 class RhodeCodeSetting(Base, BaseModel):
260 __tablename__ = 'rhodecode_settings'
260 __tablename__ = 'rhodecode_settings'
261 __table_args__ = (
261 __table_args__ = (
262 UniqueConstraint('app_settings_name'),
262 UniqueConstraint('app_settings_name'),
263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 )
265 )
266
266
267 SETTINGS_TYPES = {
267 SETTINGS_TYPES = {
268 'str': safe_str,
268 'str': safe_str,
269 'int': safe_int,
269 'int': safe_int,
270 'unicode': safe_unicode,
270 'unicode': safe_unicode,
271 'bool': str2bool,
271 'bool': str2bool,
272 'list': functools.partial(aslist, sep=',')
272 'list': functools.partial(aslist, sep=',')
273 }
273 }
274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 GLOBAL_CONF_KEY = 'app_settings'
275 GLOBAL_CONF_KEY = 'app_settings'
276
276
277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281
281
282 def __init__(self, key='', val='', type='unicode'):
282 def __init__(self, key='', val='', type='unicode'):
283 self.app_settings_name = key
283 self.app_settings_name = key
284 self.app_settings_type = type
284 self.app_settings_type = type
285 self.app_settings_value = val
285 self.app_settings_value = val
286
286
287 @validates('_app_settings_value')
287 @validates('_app_settings_value')
288 def validate_settings_value(self, key, val):
288 def validate_settings_value(self, key, val):
289 assert type(val) == unicode
289 assert type(val) == unicode
290 return val
290 return val
291
291
292 @hybrid_property
292 @hybrid_property
293 def app_settings_value(self):
293 def app_settings_value(self):
294 v = self._app_settings_value
294 v = self._app_settings_value
295 _type = self.app_settings_type
295 _type = self.app_settings_type
296 if _type:
296 if _type:
297 _type = self.app_settings_type.split('.')[0]
297 _type = self.app_settings_type.split('.')[0]
298 # decode the encrypted value
298 # decode the encrypted value
299 if 'encrypted' in self.app_settings_type:
299 if 'encrypted' in self.app_settings_type:
300 cipher = EncryptedTextValue()
300 cipher = EncryptedTextValue()
301 v = safe_unicode(cipher.process_result_value(v, None))
301 v = safe_unicode(cipher.process_result_value(v, None))
302
302
303 converter = self.SETTINGS_TYPES.get(_type) or \
303 converter = self.SETTINGS_TYPES.get(_type) or \
304 self.SETTINGS_TYPES['unicode']
304 self.SETTINGS_TYPES['unicode']
305 return converter(v)
305 return converter(v)
306
306
307 @app_settings_value.setter
307 @app_settings_value.setter
308 def app_settings_value(self, val):
308 def app_settings_value(self, val):
309 """
309 """
310 Setter that will always make sure we use unicode in app_settings_value
310 Setter that will always make sure we use unicode in app_settings_value
311
311
312 :param val:
312 :param val:
313 """
313 """
314 val = safe_unicode(val)
314 val = safe_unicode(val)
315 # encode the encrypted value
315 # encode the encrypted value
316 if 'encrypted' in self.app_settings_type:
316 if 'encrypted' in self.app_settings_type:
317 cipher = EncryptedTextValue()
317 cipher = EncryptedTextValue()
318 val = safe_unicode(cipher.process_bind_param(val, None))
318 val = safe_unicode(cipher.process_bind_param(val, None))
319 self._app_settings_value = val
319 self._app_settings_value = val
320
320
321 @hybrid_property
321 @hybrid_property
322 def app_settings_type(self):
322 def app_settings_type(self):
323 return self._app_settings_type
323 return self._app_settings_type
324
324
325 @app_settings_type.setter
325 @app_settings_type.setter
326 def app_settings_type(self, val):
326 def app_settings_type(self, val):
327 if val.split('.')[0] not in self.SETTINGS_TYPES:
327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 raise Exception('type must be one of %s got %s'
328 raise Exception('type must be one of %s got %s'
329 % (self.SETTINGS_TYPES.keys(), val))
329 % (self.SETTINGS_TYPES.keys(), val))
330 self._app_settings_type = val
330 self._app_settings_type = val
331
331
332 def __unicode__(self):
332 def __unicode__(self):
333 return u"<%s('%s:%s[%s]')>" % (
333 return u"<%s('%s:%s[%s]')>" % (
334 self.__class__.__name__,
334 self.__class__.__name__,
335 self.app_settings_name, self.app_settings_value,
335 self.app_settings_name, self.app_settings_value,
336 self.app_settings_type
336 self.app_settings_type
337 )
337 )
338
338
339
339
340 class RhodeCodeUi(Base, BaseModel):
340 class RhodeCodeUi(Base, BaseModel):
341 __tablename__ = 'rhodecode_ui'
341 __tablename__ = 'rhodecode_ui'
342 __table_args__ = (
342 __table_args__ = (
343 UniqueConstraint('ui_key'),
343 UniqueConstraint('ui_key'),
344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 )
346 )
347
347
348 HOOK_REPO_SIZE = 'changegroup.repo_size'
348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 # HG
349 # HG
350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 HOOK_PULL = 'outgoing.pull_logger'
351 HOOK_PULL = 'outgoing.pull_logger'
352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 HOOK_PUSH = 'changegroup.push_logger'
353 HOOK_PUSH = 'changegroup.push_logger'
354
354
355 # TODO: johbo: Unify way how hooks are configured for git and hg,
355 # TODO: johbo: Unify way how hooks are configured for git and hg,
356 # git part is currently hardcoded.
356 # git part is currently hardcoded.
357
357
358 # SVN PATTERNS
358 # SVN PATTERNS
359 SVN_BRANCH_ID = 'vcs_svn_branch'
359 SVN_BRANCH_ID = 'vcs_svn_branch'
360 SVN_TAG_ID = 'vcs_svn_tag'
360 SVN_TAG_ID = 'vcs_svn_tag'
361
361
362 ui_id = Column(
362 ui_id = Column(
363 "ui_id", Integer(), nullable=False, unique=True, default=None,
363 "ui_id", Integer(), nullable=False, unique=True, default=None,
364 primary_key=True)
364 primary_key=True)
365 ui_section = Column(
365 ui_section = Column(
366 "ui_section", String(255), nullable=True, unique=None, default=None)
366 "ui_section", String(255), nullable=True, unique=None, default=None)
367 ui_key = Column(
367 ui_key = Column(
368 "ui_key", String(255), nullable=True, unique=None, default=None)
368 "ui_key", String(255), nullable=True, unique=None, default=None)
369 ui_value = Column(
369 ui_value = Column(
370 "ui_value", String(255), nullable=True, unique=None, default=None)
370 "ui_value", String(255), nullable=True, unique=None, default=None)
371 ui_active = Column(
371 ui_active = Column(
372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
373
373
374 def __repr__(self):
374 def __repr__(self):
375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
376 self.ui_key, self.ui_value)
376 self.ui_key, self.ui_value)
377
377
378
378
379 class RepoRhodeCodeSetting(Base, BaseModel):
379 class RepoRhodeCodeSetting(Base, BaseModel):
380 __tablename__ = 'repo_rhodecode_settings'
380 __tablename__ = 'repo_rhodecode_settings'
381 __table_args__ = (
381 __table_args__ = (
382 UniqueConstraint(
382 UniqueConstraint(
383 'app_settings_name', 'repository_id',
383 'app_settings_name', 'repository_id',
384 name='uq_repo_rhodecode_setting_name_repo_id'),
384 name='uq_repo_rhodecode_setting_name_repo_id'),
385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
387 )
387 )
388
388
389 repository_id = Column(
389 repository_id = Column(
390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
391 nullable=False)
391 nullable=False)
392 app_settings_id = Column(
392 app_settings_id = Column(
393 "app_settings_id", Integer(), nullable=False, unique=True,
393 "app_settings_id", Integer(), nullable=False, unique=True,
394 default=None, primary_key=True)
394 default=None, primary_key=True)
395 app_settings_name = Column(
395 app_settings_name = Column(
396 "app_settings_name", String(255), nullable=True, unique=None,
396 "app_settings_name", String(255), nullable=True, unique=None,
397 default=None)
397 default=None)
398 _app_settings_value = Column(
398 _app_settings_value = Column(
399 "app_settings_value", String(4096), nullable=True, unique=None,
399 "app_settings_value", String(4096), nullable=True, unique=None,
400 default=None)
400 default=None)
401 _app_settings_type = Column(
401 _app_settings_type = Column(
402 "app_settings_type", String(255), nullable=True, unique=None,
402 "app_settings_type", String(255), nullable=True, unique=None,
403 default=None)
403 default=None)
404
404
405 repository = relationship('Repository')
405 repository = relationship('Repository')
406
406
407 def __init__(self, repository_id, key='', val='', type='unicode'):
407 def __init__(self, repository_id, key='', val='', type='unicode'):
408 self.repository_id = repository_id
408 self.repository_id = repository_id
409 self.app_settings_name = key
409 self.app_settings_name = key
410 self.app_settings_type = type
410 self.app_settings_type = type
411 self.app_settings_value = val
411 self.app_settings_value = val
412
412
413 @validates('_app_settings_value')
413 @validates('_app_settings_value')
414 def validate_settings_value(self, key, val):
414 def validate_settings_value(self, key, val):
415 assert type(val) == unicode
415 assert type(val) == unicode
416 return val
416 return val
417
417
418 @hybrid_property
418 @hybrid_property
419 def app_settings_value(self):
419 def app_settings_value(self):
420 v = self._app_settings_value
420 v = self._app_settings_value
421 type_ = self.app_settings_type
421 type_ = self.app_settings_type
422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
424 return converter(v)
424 return converter(v)
425
425
426 @app_settings_value.setter
426 @app_settings_value.setter
427 def app_settings_value(self, val):
427 def app_settings_value(self, val):
428 """
428 """
429 Setter that will always make sure we use unicode in app_settings_value
429 Setter that will always make sure we use unicode in app_settings_value
430
430
431 :param val:
431 :param val:
432 """
432 """
433 self._app_settings_value = safe_unicode(val)
433 self._app_settings_value = safe_unicode(val)
434
434
435 @hybrid_property
435 @hybrid_property
436 def app_settings_type(self):
436 def app_settings_type(self):
437 return self._app_settings_type
437 return self._app_settings_type
438
438
439 @app_settings_type.setter
439 @app_settings_type.setter
440 def app_settings_type(self, val):
440 def app_settings_type(self, val):
441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
442 if val not in SETTINGS_TYPES:
442 if val not in SETTINGS_TYPES:
443 raise Exception('type must be one of %s got %s'
443 raise Exception('type must be one of %s got %s'
444 % (SETTINGS_TYPES.keys(), val))
444 % (SETTINGS_TYPES.keys(), val))
445 self._app_settings_type = val
445 self._app_settings_type = val
446
446
447 def __unicode__(self):
447 def __unicode__(self):
448 return u"<%s('%s:%s:%s[%s]')>" % (
448 return u"<%s('%s:%s:%s[%s]')>" % (
449 self.__class__.__name__, self.repository.repo_name,
449 self.__class__.__name__, self.repository.repo_name,
450 self.app_settings_name, self.app_settings_value,
450 self.app_settings_name, self.app_settings_value,
451 self.app_settings_type
451 self.app_settings_type
452 )
452 )
453
453
454
454
455 class RepoRhodeCodeUi(Base, BaseModel):
455 class RepoRhodeCodeUi(Base, BaseModel):
456 __tablename__ = 'repo_rhodecode_ui'
456 __tablename__ = 'repo_rhodecode_ui'
457 __table_args__ = (
457 __table_args__ = (
458 UniqueConstraint(
458 UniqueConstraint(
459 'repository_id', 'ui_section', 'ui_key',
459 'repository_id', 'ui_section', 'ui_key',
460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
463 )
463 )
464
464
465 repository_id = Column(
465 repository_id = Column(
466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 nullable=False)
467 nullable=False)
468 ui_id = Column(
468 ui_id = Column(
469 "ui_id", Integer(), nullable=False, unique=True, default=None,
469 "ui_id", Integer(), nullable=False, unique=True, default=None,
470 primary_key=True)
470 primary_key=True)
471 ui_section = Column(
471 ui_section = Column(
472 "ui_section", String(255), nullable=True, unique=None, default=None)
472 "ui_section", String(255), nullable=True, unique=None, default=None)
473 ui_key = Column(
473 ui_key = Column(
474 "ui_key", String(255), nullable=True, unique=None, default=None)
474 "ui_key", String(255), nullable=True, unique=None, default=None)
475 ui_value = Column(
475 ui_value = Column(
476 "ui_value", String(255), nullable=True, unique=None, default=None)
476 "ui_value", String(255), nullable=True, unique=None, default=None)
477 ui_active = Column(
477 ui_active = Column(
478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
479
479
480 repository = relationship('Repository')
480 repository = relationship('Repository')
481
481
482 def __repr__(self):
482 def __repr__(self):
483 return '<%s[%s:%s]%s=>%s]>' % (
483 return '<%s[%s:%s]%s=>%s]>' % (
484 self.__class__.__name__, self.repository.repo_name,
484 self.__class__.__name__, self.repository.repo_name,
485 self.ui_section, self.ui_key, self.ui_value)
485 self.ui_section, self.ui_key, self.ui_value)
486
486
487
487
488 class User(Base, BaseModel):
488 class User(Base, BaseModel):
489 __tablename__ = 'users'
489 __tablename__ = 'users'
490 __table_args__ = (
490 __table_args__ = (
491 UniqueConstraint('username'), UniqueConstraint('email'),
491 UniqueConstraint('username'), UniqueConstraint('email'),
492 Index('u_username_idx', 'username'),
492 Index('u_username_idx', 'username'),
493 Index('u_email_idx', 'email'),
493 Index('u_email_idx', 'email'),
494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
496 )
496 )
497 DEFAULT_USER = 'default'
497 DEFAULT_USER = 'default'
498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
500
500
501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
502 username = Column("username", String(255), nullable=True, unique=None, default=None)
502 username = Column("username", String(255), nullable=True, unique=None, default=None)
503 password = Column("password", String(255), nullable=True, unique=None, default=None)
503 password = Column("password", String(255), nullable=True, unique=None, default=None)
504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
516
516
517 user_log = relationship('UserLog')
517 user_log = relationship('UserLog')
518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
519
519
520 repositories = relationship('Repository')
520 repositories = relationship('Repository')
521 repository_groups = relationship('RepoGroup')
521 repository_groups = relationship('RepoGroup')
522 user_groups = relationship('UserGroup')
522 user_groups = relationship('UserGroup')
523
523
524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
526
526
527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
530
530
531 group_member = relationship('UserGroupMember', cascade='all')
531 group_member = relationship('UserGroupMember', cascade='all')
532
532
533 notifications = relationship('UserNotification', cascade='all')
533 notifications = relationship('UserNotification', cascade='all')
534 # notifications assigned to this user
534 # notifications assigned to this user
535 user_created_notifications = relationship('Notification', cascade='all')
535 user_created_notifications = relationship('Notification', cascade='all')
536 # comments created by this user
536 # comments created by this user
537 user_comments = relationship('ChangesetComment', cascade='all')
537 user_comments = relationship('ChangesetComment', cascade='all')
538 # user profile extra info
538 # user profile extra info
539 user_emails = relationship('UserEmailMap', cascade='all')
539 user_emails = relationship('UserEmailMap', cascade='all')
540 user_ip_map = relationship('UserIpMap', cascade='all')
540 user_ip_map = relationship('UserIpMap', cascade='all')
541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
542 # gists
542 # gists
543 user_gists = relationship('Gist', cascade='all')
543 user_gists = relationship('Gist', cascade='all')
544 # user pull requests
544 # user pull requests
545 user_pull_requests = relationship('PullRequest', cascade='all')
545 user_pull_requests = relationship('PullRequest', cascade='all')
546 # external identities
546 # external identities
547 extenal_identities = relationship(
547 extenal_identities = relationship(
548 'ExternalIdentity',
548 'ExternalIdentity',
549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
550 cascade='all')
550 cascade='all')
551
551
552 def __unicode__(self):
552 def __unicode__(self):
553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
554 self.user_id, self.username)
554 self.user_id, self.username)
555
555
556 @hybrid_property
556 @hybrid_property
557 def email(self):
557 def email(self):
558 return self._email
558 return self._email
559
559
560 @email.setter
560 @email.setter
561 def email(self, val):
561 def email(self, val):
562 self._email = val.lower() if val else None
562 self._email = val.lower() if val else None
563
563
564 @property
564 @property
565 def firstname(self):
565 def firstname(self):
566 # alias for future
566 # alias for future
567 return self.name
567 return self.name
568
568
569 @property
569 @property
570 def emails(self):
570 def emails(self):
571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
572 return [self.email] + [x.email for x in other]
572 return [self.email] + [x.email for x in other]
573
573
574 @property
574 @property
575 def auth_tokens(self):
575 def auth_tokens(self):
576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
577
577
578 @property
578 @property
579 def extra_auth_tokens(self):
579 def extra_auth_tokens(self):
580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
581
581
582 @property
582 @property
583 def feed_token(self):
583 def feed_token(self):
584 feed_tokens = UserApiKeys.query()\
584 feed_tokens = UserApiKeys.query()\
585 .filter(UserApiKeys.user == self)\
585 .filter(UserApiKeys.user == self)\
586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
587 .all()
587 .all()
588 if feed_tokens:
588 if feed_tokens:
589 return feed_tokens[0].api_key
589 return feed_tokens[0].api_key
590 else:
590 else:
591 # use the main token so we don't end up with nothing...
591 # use the main token so we don't end up with nothing...
592 return self.api_key
592 return self.api_key
593
593
594 @classmethod
594 @classmethod
595 def extra_valid_auth_tokens(cls, user, role=None):
595 def extra_valid_auth_tokens(cls, user, role=None):
596 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
596 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
597 .filter(or_(UserApiKeys.expires == -1,
597 .filter(or_(UserApiKeys.expires == -1,
598 UserApiKeys.expires >= time.time()))
598 UserApiKeys.expires >= time.time()))
599 if role:
599 if role:
600 tokens = tokens.filter(or_(UserApiKeys.role == role,
600 tokens = tokens.filter(or_(UserApiKeys.role == role,
601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
602 return tokens.all()
602 return tokens.all()
603
603
604 @property
604 @property
605 def builtin_token_roles(self):
605 def builtin_token_roles(self):
606 return map(UserApiKeys._get_role_name, [
606 return map(UserApiKeys._get_role_name, [
607 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
607 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
608 ])
608 ])
609
609
610 @property
610 @property
611 def ip_addresses(self):
611 def ip_addresses(self):
612 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
612 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
613 return [x.ip_addr for x in ret]
613 return [x.ip_addr for x in ret]
614
614
615 @property
615 @property
616 def username_and_name(self):
616 def username_and_name(self):
617 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
617 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
618
618
619 @property
619 @property
620 def username_or_name_or_email(self):
620 def username_or_name_or_email(self):
621 full_name = self.full_name if self.full_name is not ' ' else None
621 full_name = self.full_name if self.full_name is not ' ' else None
622 return self.username or full_name or self.email
622 return self.username or full_name or self.email
623
623
624 @property
624 @property
625 def full_name(self):
625 def full_name(self):
626 return '%s %s' % (self.firstname, self.lastname)
626 return '%s %s' % (self.firstname, self.lastname)
627
627
628 @property
628 @property
629 def full_name_or_username(self):
629 def full_name_or_username(self):
630 return ('%s %s' % (self.firstname, self.lastname)
630 return ('%s %s' % (self.firstname, self.lastname)
631 if (self.firstname and self.lastname) else self.username)
631 if (self.firstname and self.lastname) else self.username)
632
632
633 @property
633 @property
634 def full_contact(self):
634 def full_contact(self):
635 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
635 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
636
636
637 @property
637 @property
638 def short_contact(self):
638 def short_contact(self):
639 return '%s %s' % (self.firstname, self.lastname)
639 return '%s %s' % (self.firstname, self.lastname)
640
640
641 @property
641 @property
642 def is_admin(self):
642 def is_admin(self):
643 return self.admin
643 return self.admin
644
644
645 @property
645 @property
646 def AuthUser(self):
646 def AuthUser(self):
647 """
647 """
648 Returns instance of AuthUser for this user
648 Returns instance of AuthUser for this user
649 """
649 """
650 from rhodecode.lib.auth import AuthUser
650 from rhodecode.lib.auth import AuthUser
651 return AuthUser(user_id=self.user_id, api_key=self.api_key,
651 return AuthUser(user_id=self.user_id, api_key=self.api_key,
652 username=self.username)
652 username=self.username)
653
653
654 @hybrid_property
654 @hybrid_property
655 def user_data(self):
655 def user_data(self):
656 if not self._user_data:
656 if not self._user_data:
657 return {}
657 return {}
658
658
659 try:
659 try:
660 return json.loads(self._user_data)
660 return json.loads(self._user_data)
661 except TypeError:
661 except TypeError:
662 return {}
662 return {}
663
663
664 @user_data.setter
664 @user_data.setter
665 def user_data(self, val):
665 def user_data(self, val):
666 if not isinstance(val, dict):
666 if not isinstance(val, dict):
667 raise Exception('user_data must be dict, got %s' % type(val))
667 raise Exception('user_data must be dict, got %s' % type(val))
668 try:
668 try:
669 self._user_data = json.dumps(val)
669 self._user_data = json.dumps(val)
670 except Exception:
670 except Exception:
671 log.error(traceback.format_exc())
671 log.error(traceback.format_exc())
672
672
673 @classmethod
673 @classmethod
674 def get_by_username(cls, username, case_insensitive=False,
674 def get_by_username(cls, username, case_insensitive=False,
675 cache=False, identity_cache=False):
675 cache=False, identity_cache=False):
676 session = Session()
676 session = Session()
677
677
678 if case_insensitive:
678 if case_insensitive:
679 q = cls.query().filter(
679 q = cls.query().filter(
680 func.lower(cls.username) == func.lower(username))
680 func.lower(cls.username) == func.lower(username))
681 else:
681 else:
682 q = cls.query().filter(cls.username == username)
682 q = cls.query().filter(cls.username == username)
683
683
684 if cache:
684 if cache:
685 if identity_cache:
685 if identity_cache:
686 val = cls.identity_cache(session, 'username', username)
686 val = cls.identity_cache(session, 'username', username)
687 if val:
687 if val:
688 return val
688 return val
689 else:
689 else:
690 q = q.options(
690 q = q.options(
691 FromCache("sql_cache_short",
691 FromCache("sql_cache_short",
692 "get_user_by_name_%s" % _hash_key(username)))
692 "get_user_by_name_%s" % _hash_key(username)))
693
693
694 return q.scalar()
694 return q.scalar()
695
695
696 @classmethod
696 @classmethod
697 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
697 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
698 q = cls.query().filter(cls.api_key == auth_token)
698 q = cls.query().filter(cls.api_key == auth_token)
699
699
700 if cache:
700 if cache:
701 q = q.options(FromCache("sql_cache_short",
701 q = q.options(FromCache("sql_cache_short",
702 "get_auth_token_%s" % auth_token))
702 "get_auth_token_%s" % auth_token))
703 res = q.scalar()
703 res = q.scalar()
704
704
705 if fallback and not res:
705 if fallback and not res:
706 #fallback to additional keys
706 #fallback to additional keys
707 _res = UserApiKeys.query()\
707 _res = UserApiKeys.query()\
708 .filter(UserApiKeys.api_key == auth_token)\
708 .filter(UserApiKeys.api_key == auth_token)\
709 .filter(or_(UserApiKeys.expires == -1,
709 .filter(or_(UserApiKeys.expires == -1,
710 UserApiKeys.expires >= time.time()))\
710 UserApiKeys.expires >= time.time()))\
711 .first()
711 .first()
712 if _res:
712 if _res:
713 res = _res.user
713 res = _res.user
714 return res
714 return res
715
715
716 @classmethod
716 @classmethod
717 def get_by_email(cls, email, case_insensitive=False, cache=False):
717 def get_by_email(cls, email, case_insensitive=False, cache=False):
718
718
719 if case_insensitive:
719 if case_insensitive:
720 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
720 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
721
721
722 else:
722 else:
723 q = cls.query().filter(cls.email == email)
723 q = cls.query().filter(cls.email == email)
724
724
725 if cache:
725 if cache:
726 q = q.options(FromCache("sql_cache_short",
726 q = q.options(FromCache("sql_cache_short",
727 "get_email_key_%s" % _hash_key(email)))
727 "get_email_key_%s" % _hash_key(email)))
728
728
729 ret = q.scalar()
729 ret = q.scalar()
730 if ret is None:
730 if ret is None:
731 q = UserEmailMap.query()
731 q = UserEmailMap.query()
732 # try fetching in alternate email map
732 # try fetching in alternate email map
733 if case_insensitive:
733 if case_insensitive:
734 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
734 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
735 else:
735 else:
736 q = q.filter(UserEmailMap.email == email)
736 q = q.filter(UserEmailMap.email == email)
737 q = q.options(joinedload(UserEmailMap.user))
737 q = q.options(joinedload(UserEmailMap.user))
738 if cache:
738 if cache:
739 q = q.options(FromCache("sql_cache_short",
739 q = q.options(FromCache("sql_cache_short",
740 "get_email_map_key_%s" % email))
740 "get_email_map_key_%s" % email))
741 ret = getattr(q.scalar(), 'user', None)
741 ret = getattr(q.scalar(), 'user', None)
742
742
743 return ret
743 return ret
744
744
745 @classmethod
745 @classmethod
746 def get_from_cs_author(cls, author):
746 def get_from_cs_author(cls, author):
747 """
747 """
748 Tries to get User objects out of commit author string
748 Tries to get User objects out of commit author string
749
749
750 :param author:
750 :param author:
751 """
751 """
752 from rhodecode.lib.helpers import email, author_name
752 from rhodecode.lib.helpers import email, author_name
753 # Valid email in the attribute passed, see if they're in the system
753 # Valid email in the attribute passed, see if they're in the system
754 _email = email(author)
754 _email = email(author)
755 if _email:
755 if _email:
756 user = cls.get_by_email(_email, case_insensitive=True)
756 user = cls.get_by_email(_email, case_insensitive=True)
757 if user:
757 if user:
758 return user
758 return user
759 # Maybe we can match by username?
759 # Maybe we can match by username?
760 _author = author_name(author)
760 _author = author_name(author)
761 user = cls.get_by_username(_author, case_insensitive=True)
761 user = cls.get_by_username(_author, case_insensitive=True)
762 if user:
762 if user:
763 return user
763 return user
764
764
765 def update_userdata(self, **kwargs):
765 def update_userdata(self, **kwargs):
766 usr = self
766 usr = self
767 old = usr.user_data
767 old = usr.user_data
768 old.update(**kwargs)
768 old.update(**kwargs)
769 usr.user_data = old
769 usr.user_data = old
770 Session().add(usr)
770 Session().add(usr)
771 log.debug('updated userdata with ', kwargs)
771 log.debug('updated userdata with ', kwargs)
772
772
773 def update_lastlogin(self):
773 def update_lastlogin(self):
774 """Update user lastlogin"""
774 """Update user lastlogin"""
775 self.last_login = datetime.datetime.now()
775 self.last_login = datetime.datetime.now()
776 Session().add(self)
776 Session().add(self)
777 log.debug('updated user %s lastlogin', self.username)
777 log.debug('updated user %s lastlogin', self.username)
778
778
779 def update_lastactivity(self):
779 def update_lastactivity(self):
780 """Update user lastactivity"""
780 """Update user lastactivity"""
781 usr = self
781 usr = self
782 old = usr.user_data
782 old = usr.user_data
783 old.update({'last_activity': time.time()})
783 old.update({'last_activity': time.time()})
784 usr.user_data = old
784 usr.user_data = old
785 Session().add(usr)
785 Session().add(usr)
786 log.debug('updated user %s lastactivity', usr.username)
786 log.debug('updated user %s lastactivity', usr.username)
787
787
788 def update_password(self, new_password, change_api_key=False):
788 def update_password(self, new_password, change_api_key=False):
789 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
789 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
790
790
791 self.password = get_crypt_password(new_password)
791 self.password = get_crypt_password(new_password)
792 if change_api_key:
792 if change_api_key:
793 self.api_key = generate_auth_token(self.username)
793 self.api_key = generate_auth_token(self.username)
794 Session().add(self)
794 Session().add(self)
795
795
796 @classmethod
796 @classmethod
797 def get_first_super_admin(cls):
797 def get_first_super_admin(cls):
798 user = User.query().filter(User.admin == true()).first()
798 user = User.query().filter(User.admin == true()).first()
799 if user is None:
799 if user is None:
800 raise Exception('FATAL: Missing administrative account!')
800 raise Exception('FATAL: Missing administrative account!')
801 return user
801 return user
802
802
803 @classmethod
803 @classmethod
804 def get_all_super_admins(cls):
804 def get_all_super_admins(cls):
805 """
805 """
806 Returns all admin accounts sorted by username
806 Returns all admin accounts sorted by username
807 """
807 """
808 return User.query().filter(User.admin == true())\
808 return User.query().filter(User.admin == true())\
809 .order_by(User.username.asc()).all()
809 .order_by(User.username.asc()).all()
810
810
811 @classmethod
811 @classmethod
812 def get_default_user(cls, cache=False):
812 def get_default_user(cls, cache=False):
813 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
813 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
814 if user is None:
814 if user is None:
815 raise Exception('FATAL: Missing default account!')
815 raise Exception('FATAL: Missing default account!')
816 return user
816 return user
817
817
818 def _get_default_perms(self, user, suffix=''):
818 def _get_default_perms(self, user, suffix=''):
819 from rhodecode.model.permission import PermissionModel
819 from rhodecode.model.permission import PermissionModel
820 return PermissionModel().get_default_perms(user.user_perms, suffix)
820 return PermissionModel().get_default_perms(user.user_perms, suffix)
821
821
822 def get_default_perms(self, suffix=''):
822 def get_default_perms(self, suffix=''):
823 return self._get_default_perms(self, suffix)
823 return self._get_default_perms(self, suffix)
824
824
825 def get_api_data(self, include_secrets=False, details='full'):
825 def get_api_data(self, include_secrets=False, details='full'):
826 """
826 """
827 Common function for generating user related data for API
827 Common function for generating user related data for API
828
828
829 :param include_secrets: By default secrets in the API data will be replaced
829 :param include_secrets: By default secrets in the API data will be replaced
830 by a placeholder value to prevent exposing this data by accident. In case
830 by a placeholder value to prevent exposing this data by accident. In case
831 this data shall be exposed, set this flag to ``True``.
831 this data shall be exposed, set this flag to ``True``.
832
832
833 :param details: details can be 'basic|full' basic gives only a subset of
833 :param details: details can be 'basic|full' basic gives only a subset of
834 the available user information that includes user_id, name and emails.
834 the available user information that includes user_id, name and emails.
835 """
835 """
836 user = self
836 user = self
837 user_data = self.user_data
837 user_data = self.user_data
838 data = {
838 data = {
839 'user_id': user.user_id,
839 'user_id': user.user_id,
840 'username': user.username,
840 'username': user.username,
841 'firstname': user.name,
841 'firstname': user.name,
842 'lastname': user.lastname,
842 'lastname': user.lastname,
843 'email': user.email,
843 'email': user.email,
844 'emails': user.emails,
844 'emails': user.emails,
845 }
845 }
846 if details == 'basic':
846 if details == 'basic':
847 return data
847 return data
848
848
849 api_key_length = 40
849 api_key_length = 40
850 api_key_replacement = '*' * api_key_length
850 api_key_replacement = '*' * api_key_length
851
851
852 extras = {
852 extras = {
853 'api_key': api_key_replacement,
853 'api_key': api_key_replacement,
854 'api_keys': [api_key_replacement],
854 'api_keys': [api_key_replacement],
855 'active': user.active,
855 'active': user.active,
856 'admin': user.admin,
856 'admin': user.admin,
857 'extern_type': user.extern_type,
857 'extern_type': user.extern_type,
858 'extern_name': user.extern_name,
858 'extern_name': user.extern_name,
859 'last_login': user.last_login,
859 'last_login': user.last_login,
860 'ip_addresses': user.ip_addresses,
860 'ip_addresses': user.ip_addresses,
861 'language': user_data.get('language')
861 'language': user_data.get('language')
862 }
862 }
863 data.update(extras)
863 data.update(extras)
864
864
865 if include_secrets:
865 if include_secrets:
866 data['api_key'] = user.api_key
866 data['api_key'] = user.api_key
867 data['api_keys'] = user.auth_tokens
867 data['api_keys'] = user.auth_tokens
868 return data
868 return data
869
869
870 def __json__(self):
870 def __json__(self):
871 data = {
871 data = {
872 'full_name': self.full_name,
872 'full_name': self.full_name,
873 'full_name_or_username': self.full_name_or_username,
873 'full_name_or_username': self.full_name_or_username,
874 'short_contact': self.short_contact,
874 'short_contact': self.short_contact,
875 'full_contact': self.full_contact,
875 'full_contact': self.full_contact,
876 }
876 }
877 data.update(self.get_api_data())
877 data.update(self.get_api_data())
878 return data
878 return data
879
879
880
880
881 class UserApiKeys(Base, BaseModel):
881 class UserApiKeys(Base, BaseModel):
882 __tablename__ = 'user_api_keys'
882 __tablename__ = 'user_api_keys'
883 __table_args__ = (
883 __table_args__ = (
884 Index('uak_api_key_idx', 'api_key'),
884 Index('uak_api_key_idx', 'api_key'),
885 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
885 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
886 UniqueConstraint('api_key'),
886 UniqueConstraint('api_key'),
887 {'extend_existing': True, 'mysql_engine': 'InnoDB',
887 {'extend_existing': True, 'mysql_engine': 'InnoDB',
888 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
888 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
889 )
889 )
890 __mapper_args__ = {}
890 __mapper_args__ = {}
891
891
892 # ApiKey role
892 # ApiKey role
893 ROLE_ALL = 'token_role_all'
893 ROLE_ALL = 'token_role_all'
894 ROLE_HTTP = 'token_role_http'
894 ROLE_HTTP = 'token_role_http'
895 ROLE_VCS = 'token_role_vcs'
895 ROLE_VCS = 'token_role_vcs'
896 ROLE_API = 'token_role_api'
896 ROLE_API = 'token_role_api'
897 ROLE_FEED = 'token_role_feed'
897 ROLE_FEED = 'token_role_feed'
898 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
898 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
899
899
900 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
900 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
901 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
901 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
902 api_key = Column("api_key", String(255), nullable=False, unique=True)
902 api_key = Column("api_key", String(255), nullable=False, unique=True)
903 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
903 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
904 expires = Column('expires', Float(53), nullable=False)
904 expires = Column('expires', Float(53), nullable=False)
905 role = Column('role', String(255), nullable=True)
905 role = Column('role', String(255), nullable=True)
906 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
906 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
907
907
908 user = relationship('User', lazy='joined')
908 user = relationship('User', lazy='joined')
909
909
910 @classmethod
910 @classmethod
911 def _get_role_name(cls, role):
911 def _get_role_name(cls, role):
912 return {
912 return {
913 cls.ROLE_ALL: _('all'),
913 cls.ROLE_ALL: _('all'),
914 cls.ROLE_HTTP: _('http/web interface'),
914 cls.ROLE_HTTP: _('http/web interface'),
915 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
915 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
916 cls.ROLE_API: _('api calls'),
916 cls.ROLE_API: _('api calls'),
917 cls.ROLE_FEED: _('feed access'),
917 cls.ROLE_FEED: _('feed access'),
918 }.get(role, role)
918 }.get(role, role)
919
919
920 @property
920 @property
921 def expired(self):
921 def expired(self):
922 if self.expires == -1:
922 if self.expires == -1:
923 return False
923 return False
924 return time.time() > self.expires
924 return time.time() > self.expires
925
925
926 @property
926 @property
927 def role_humanized(self):
927 def role_humanized(self):
928 return self._get_role_name(self.role)
928 return self._get_role_name(self.role)
929
929
930
930
931 class UserEmailMap(Base, BaseModel):
931 class UserEmailMap(Base, BaseModel):
932 __tablename__ = 'user_email_map'
932 __tablename__ = 'user_email_map'
933 __table_args__ = (
933 __table_args__ = (
934 Index('uem_email_idx', 'email'),
934 Index('uem_email_idx', 'email'),
935 UniqueConstraint('email'),
935 UniqueConstraint('email'),
936 {'extend_existing': True, 'mysql_engine': 'InnoDB',
936 {'extend_existing': True, 'mysql_engine': 'InnoDB',
937 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
937 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
938 )
938 )
939 __mapper_args__ = {}
939 __mapper_args__ = {}
940
940
941 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
941 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
942 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
942 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
943 _email = Column("email", String(255), nullable=True, unique=False, default=None)
943 _email = Column("email", String(255), nullable=True, unique=False, default=None)
944 user = relationship('User', lazy='joined')
944 user = relationship('User', lazy='joined')
945
945
946 @validates('_email')
946 @validates('_email')
947 def validate_email(self, key, email):
947 def validate_email(self, key, email):
948 # check if this email is not main one
948 # check if this email is not main one
949 main_email = Session().query(User).filter(User.email == email).scalar()
949 main_email = Session().query(User).filter(User.email == email).scalar()
950 if main_email is not None:
950 if main_email is not None:
951 raise AttributeError('email %s is present is user table' % email)
951 raise AttributeError('email %s is present is user table' % email)
952 return email
952 return email
953
953
954 @hybrid_property
954 @hybrid_property
955 def email(self):
955 def email(self):
956 return self._email
956 return self._email
957
957
958 @email.setter
958 @email.setter
959 def email(self, val):
959 def email(self, val):
960 self._email = val.lower() if val else None
960 self._email = val.lower() if val else None
961
961
962
962
963 class UserIpMap(Base, BaseModel):
963 class UserIpMap(Base, BaseModel):
964 __tablename__ = 'user_ip_map'
964 __tablename__ = 'user_ip_map'
965 __table_args__ = (
965 __table_args__ = (
966 UniqueConstraint('user_id', 'ip_addr'),
966 UniqueConstraint('user_id', 'ip_addr'),
967 {'extend_existing': True, 'mysql_engine': 'InnoDB',
967 {'extend_existing': True, 'mysql_engine': 'InnoDB',
968 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
968 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
969 )
969 )
970 __mapper_args__ = {}
970 __mapper_args__ = {}
971
971
972 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
972 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
973 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
973 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
974 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
974 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
975 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
975 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
976 description = Column("description", String(10000), nullable=True, unique=None, default=None)
976 description = Column("description", String(10000), nullable=True, unique=None, default=None)
977 user = relationship('User', lazy='joined')
977 user = relationship('User', lazy='joined')
978
978
979 @classmethod
979 @classmethod
980 def _get_ip_range(cls, ip_addr):
980 def _get_ip_range(cls, ip_addr):
981 net = ipaddress.ip_network(ip_addr, strict=False)
981 net = ipaddress.ip_network(ip_addr, strict=False)
982 return [str(net.network_address), str(net.broadcast_address)]
982 return [str(net.network_address), str(net.broadcast_address)]
983
983
984 def __json__(self):
984 def __json__(self):
985 return {
985 return {
986 'ip_addr': self.ip_addr,
986 'ip_addr': self.ip_addr,
987 'ip_range': self._get_ip_range(self.ip_addr),
987 'ip_range': self._get_ip_range(self.ip_addr),
988 }
988 }
989
989
990 def __unicode__(self):
990 def __unicode__(self):
991 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
991 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
992 self.user_id, self.ip_addr)
992 self.user_id, self.ip_addr)
993
993
994 class UserLog(Base, BaseModel):
994 class UserLog(Base, BaseModel):
995 __tablename__ = 'user_logs'
995 __tablename__ = 'user_logs'
996 __table_args__ = (
996 __table_args__ = (
997 {'extend_existing': True, 'mysql_engine': 'InnoDB',
997 {'extend_existing': True, 'mysql_engine': 'InnoDB',
998 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
998 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
999 )
999 )
1000 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1000 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1001 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1001 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1002 username = Column("username", String(255), nullable=True, unique=None, default=None)
1002 username = Column("username", String(255), nullable=True, unique=None, default=None)
1003 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1003 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1004 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1004 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1005 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1005 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1006 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1006 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1007 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1007 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1008
1008
1009 def __unicode__(self):
1009 def __unicode__(self):
1010 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1010 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1011 self.repository_name,
1011 self.repository_name,
1012 self.action)
1012 self.action)
1013
1013
1014 @property
1014 @property
1015 def action_as_day(self):
1015 def action_as_day(self):
1016 return datetime.date(*self.action_date.timetuple()[:3])
1016 return datetime.date(*self.action_date.timetuple()[:3])
1017
1017
1018 user = relationship('User')
1018 user = relationship('User')
1019 repository = relationship('Repository', cascade='')
1019 repository = relationship('Repository', cascade='')
1020
1020
1021
1021
1022 class UserGroup(Base, BaseModel):
1022 class UserGroup(Base, BaseModel):
1023 __tablename__ = 'users_groups'
1023 __tablename__ = 'users_groups'
1024 __table_args__ = (
1024 __table_args__ = (
1025 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1025 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1026 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1026 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1027 )
1027 )
1028
1028
1029 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1029 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1030 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1030 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1031 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1031 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1032 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1032 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1033 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1033 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1036 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1036 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1037
1037
1038 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1038 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1039 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1039 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1040 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1040 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1041 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1041 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1042 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1042 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1043 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1043 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1044
1044
1045 user = relationship('User')
1045 user = relationship('User')
1046
1046
1047 @hybrid_property
1047 @hybrid_property
1048 def group_data(self):
1048 def group_data(self):
1049 if not self._group_data:
1049 if not self._group_data:
1050 return {}
1050 return {}
1051
1051
1052 try:
1052 try:
1053 return json.loads(self._group_data)
1053 return json.loads(self._group_data)
1054 except TypeError:
1054 except TypeError:
1055 return {}
1055 return {}
1056
1056
1057 @group_data.setter
1057 @group_data.setter
1058 def group_data(self, val):
1058 def group_data(self, val):
1059 try:
1059 try:
1060 self._group_data = json.dumps(val)
1060 self._group_data = json.dumps(val)
1061 except Exception:
1061 except Exception:
1062 log.error(traceback.format_exc())
1062 log.error(traceback.format_exc())
1063
1063
1064 def __unicode__(self):
1064 def __unicode__(self):
1065 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1065 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1066 self.users_group_id,
1066 self.users_group_id,
1067 self.users_group_name)
1067 self.users_group_name)
1068
1068
1069 @classmethod
1069 @classmethod
1070 def get_by_group_name(cls, group_name, cache=False,
1070 def get_by_group_name(cls, group_name, cache=False,
1071 case_insensitive=False):
1071 case_insensitive=False):
1072 if case_insensitive:
1072 if case_insensitive:
1073 q = cls.query().filter(func.lower(cls.users_group_name) ==
1073 q = cls.query().filter(func.lower(cls.users_group_name) ==
1074 func.lower(group_name))
1074 func.lower(group_name))
1075
1075
1076 else:
1076 else:
1077 q = cls.query().filter(cls.users_group_name == group_name)
1077 q = cls.query().filter(cls.users_group_name == group_name)
1078 if cache:
1078 if cache:
1079 q = q.options(FromCache(
1079 q = q.options(FromCache(
1080 "sql_cache_short",
1080 "sql_cache_short",
1081 "get_group_%s" % _hash_key(group_name)))
1081 "get_group_%s" % _hash_key(group_name)))
1082 return q.scalar()
1082 return q.scalar()
1083
1083
1084 @classmethod
1084 @classmethod
1085 def get(cls, user_group_id, cache=False):
1085 def get(cls, user_group_id, cache=False):
1086 user_group = cls.query()
1086 user_group = cls.query()
1087 if cache:
1087 if cache:
1088 user_group = user_group.options(FromCache("sql_cache_short",
1088 user_group = user_group.options(FromCache("sql_cache_short",
1089 "get_users_group_%s" % user_group_id))
1089 "get_users_group_%s" % user_group_id))
1090 return user_group.get(user_group_id)
1090 return user_group.get(user_group_id)
1091
1091
1092 def permissions(self, with_admins=True, with_owner=True):
1092 def permissions(self, with_admins=True, with_owner=True):
1093 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1093 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1094 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1094 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1095 joinedload(UserUserGroupToPerm.user),
1095 joinedload(UserUserGroupToPerm.user),
1096 joinedload(UserUserGroupToPerm.permission),)
1096 joinedload(UserUserGroupToPerm.permission),)
1097
1097
1098 # get owners and admins and permissions. We do a trick of re-writing
1098 # get owners and admins and permissions. We do a trick of re-writing
1099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1100 # has a global reference and changing one object propagates to all
1100 # has a global reference and changing one object propagates to all
1101 # others. This means if admin is also an owner admin_row that change
1101 # others. This means if admin is also an owner admin_row that change
1102 # would propagate to both objects
1102 # would propagate to both objects
1103 perm_rows = []
1103 perm_rows = []
1104 for _usr in q.all():
1104 for _usr in q.all():
1105 usr = AttributeDict(_usr.user.get_dict())
1105 usr = AttributeDict(_usr.user.get_dict())
1106 usr.permission = _usr.permission.permission_name
1106 usr.permission = _usr.permission.permission_name
1107 perm_rows.append(usr)
1107 perm_rows.append(usr)
1108
1108
1109 # filter the perm rows by 'default' first and then sort them by
1109 # filter the perm rows by 'default' first and then sort them by
1110 # admin,write,read,none permissions sorted again alphabetically in
1110 # admin,write,read,none permissions sorted again alphabetically in
1111 # each group
1111 # each group
1112 perm_rows = sorted(perm_rows, key=display_sort)
1112 perm_rows = sorted(perm_rows, key=display_sort)
1113
1113
1114 _admin_perm = 'usergroup.admin'
1114 _admin_perm = 'usergroup.admin'
1115 owner_row = []
1115 owner_row = []
1116 if with_owner:
1116 if with_owner:
1117 usr = AttributeDict(self.user.get_dict())
1117 usr = AttributeDict(self.user.get_dict())
1118 usr.owner_row = True
1118 usr.owner_row = True
1119 usr.permission = _admin_perm
1119 usr.permission = _admin_perm
1120 owner_row.append(usr)
1120 owner_row.append(usr)
1121
1121
1122 super_admin_rows = []
1122 super_admin_rows = []
1123 if with_admins:
1123 if with_admins:
1124 for usr in User.get_all_super_admins():
1124 for usr in User.get_all_super_admins():
1125 # if this admin is also owner, don't double the record
1125 # if this admin is also owner, don't double the record
1126 if usr.user_id == owner_row[0].user_id:
1126 if usr.user_id == owner_row[0].user_id:
1127 owner_row[0].admin_row = True
1127 owner_row[0].admin_row = True
1128 else:
1128 else:
1129 usr = AttributeDict(usr.get_dict())
1129 usr = AttributeDict(usr.get_dict())
1130 usr.admin_row = True
1130 usr.admin_row = True
1131 usr.permission = _admin_perm
1131 usr.permission = _admin_perm
1132 super_admin_rows.append(usr)
1132 super_admin_rows.append(usr)
1133
1133
1134 return super_admin_rows + owner_row + perm_rows
1134 return super_admin_rows + owner_row + perm_rows
1135
1135
1136 def permission_user_groups(self):
1136 def permission_user_groups(self):
1137 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1137 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1138 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1138 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1139 joinedload(UserGroupUserGroupToPerm.target_user_group),
1139 joinedload(UserGroupUserGroupToPerm.target_user_group),
1140 joinedload(UserGroupUserGroupToPerm.permission),)
1140 joinedload(UserGroupUserGroupToPerm.permission),)
1141
1141
1142 perm_rows = []
1142 perm_rows = []
1143 for _user_group in q.all():
1143 for _user_group in q.all():
1144 usr = AttributeDict(_user_group.user_group.get_dict())
1144 usr = AttributeDict(_user_group.user_group.get_dict())
1145 usr.permission = _user_group.permission.permission_name
1145 usr.permission = _user_group.permission.permission_name
1146 perm_rows.append(usr)
1146 perm_rows.append(usr)
1147
1147
1148 return perm_rows
1148 return perm_rows
1149
1149
1150 def _get_default_perms(self, user_group, suffix=''):
1150 def _get_default_perms(self, user_group, suffix=''):
1151 from rhodecode.model.permission import PermissionModel
1151 from rhodecode.model.permission import PermissionModel
1152 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1152 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1153
1153
1154 def get_default_perms(self, suffix=''):
1154 def get_default_perms(self, suffix=''):
1155 return self._get_default_perms(self, suffix)
1155 return self._get_default_perms(self, suffix)
1156
1156
1157 def get_api_data(self, with_group_members=True, include_secrets=False):
1157 def get_api_data(self, with_group_members=True, include_secrets=False):
1158 """
1158 """
1159 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1159 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1160 basically forwarded.
1160 basically forwarded.
1161
1161
1162 """
1162 """
1163 user_group = self
1163 user_group = self
1164
1164
1165 data = {
1165 data = {
1166 'users_group_id': user_group.users_group_id,
1166 'users_group_id': user_group.users_group_id,
1167 'group_name': user_group.users_group_name,
1167 'group_name': user_group.users_group_name,
1168 'group_description': user_group.user_group_description,
1168 'group_description': user_group.user_group_description,
1169 'active': user_group.users_group_active,
1169 'active': user_group.users_group_active,
1170 'owner': user_group.user.username,
1170 'owner': user_group.user.username,
1171 }
1171 }
1172 if with_group_members:
1172 if with_group_members:
1173 users = []
1173 users = []
1174 for user in user_group.members:
1174 for user in user_group.members:
1175 user = user.user
1175 user = user.user
1176 users.append(user.get_api_data(include_secrets=include_secrets))
1176 users.append(user.get_api_data(include_secrets=include_secrets))
1177 data['users'] = users
1177 data['users'] = users
1178
1178
1179 return data
1179 return data
1180
1180
1181
1181
1182 class UserGroupMember(Base, BaseModel):
1182 class UserGroupMember(Base, BaseModel):
1183 __tablename__ = 'users_groups_members'
1183 __tablename__ = 'users_groups_members'
1184 __table_args__ = (
1184 __table_args__ = (
1185 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1185 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1186 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1186 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1187 )
1187 )
1188
1188
1189 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1189 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1190 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1190 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1191 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1191 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1192
1192
1193 user = relationship('User', lazy='joined')
1193 user = relationship('User', lazy='joined')
1194 users_group = relationship('UserGroup')
1194 users_group = relationship('UserGroup')
1195
1195
1196 def __init__(self, gr_id='', u_id=''):
1196 def __init__(self, gr_id='', u_id=''):
1197 self.users_group_id = gr_id
1197 self.users_group_id = gr_id
1198 self.user_id = u_id
1198 self.user_id = u_id
1199
1199
1200
1200
1201 class RepositoryField(Base, BaseModel):
1201 class RepositoryField(Base, BaseModel):
1202 __tablename__ = 'repositories_fields'
1202 __tablename__ = 'repositories_fields'
1203 __table_args__ = (
1203 __table_args__ = (
1204 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1204 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1205 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1205 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1206 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1206 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1207 )
1207 )
1208 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1208 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1209
1209
1210 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1210 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1211 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1211 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1212 field_key = Column("field_key", String(250))
1212 field_key = Column("field_key", String(250))
1213 field_label = Column("field_label", String(1024), nullable=False)
1213 field_label = Column("field_label", String(1024), nullable=False)
1214 field_value = Column("field_value", String(10000), nullable=False)
1214 field_value = Column("field_value", String(10000), nullable=False)
1215 field_desc = Column("field_desc", String(1024), nullable=False)
1215 field_desc = Column("field_desc", String(1024), nullable=False)
1216 field_type = Column("field_type", String(255), nullable=False, unique=None)
1216 field_type = Column("field_type", String(255), nullable=False, unique=None)
1217 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1217 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1218
1218
1219 repository = relationship('Repository')
1219 repository = relationship('Repository')
1220
1220
1221 @property
1221 @property
1222 def field_key_prefixed(self):
1222 def field_key_prefixed(self):
1223 return 'ex_%s' % self.field_key
1223 return 'ex_%s' % self.field_key
1224
1224
1225 @classmethod
1225 @classmethod
1226 def un_prefix_key(cls, key):
1226 def un_prefix_key(cls, key):
1227 if key.startswith(cls.PREFIX):
1227 if key.startswith(cls.PREFIX):
1228 return key[len(cls.PREFIX):]
1228 return key[len(cls.PREFIX):]
1229 return key
1229 return key
1230
1230
1231 @classmethod
1231 @classmethod
1232 def get_by_key_name(cls, key, repo):
1232 def get_by_key_name(cls, key, repo):
1233 row = cls.query()\
1233 row = cls.query()\
1234 .filter(cls.repository == repo)\
1234 .filter(cls.repository == repo)\
1235 .filter(cls.field_key == key).scalar()
1235 .filter(cls.field_key == key).scalar()
1236 return row
1236 return row
1237
1237
1238
1238
1239 class Repository(Base, BaseModel):
1239 class Repository(Base, BaseModel):
1240 __tablename__ = 'repositories'
1240 __tablename__ = 'repositories'
1241 __table_args__ = (
1241 __table_args__ = (
1242 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1242 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1243 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1243 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1244 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1244 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1245 )
1245 )
1246 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1246 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1247 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1247 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1248
1248
1249 STATE_CREATED = 'repo_state_created'
1249 STATE_CREATED = 'repo_state_created'
1250 STATE_PENDING = 'repo_state_pending'
1250 STATE_PENDING = 'repo_state_pending'
1251 STATE_ERROR = 'repo_state_error'
1251 STATE_ERROR = 'repo_state_error'
1252
1252
1253 LOCK_AUTOMATIC = 'lock_auto'
1253 LOCK_AUTOMATIC = 'lock_auto'
1254 LOCK_API = 'lock_api'
1254 LOCK_API = 'lock_api'
1255 LOCK_WEB = 'lock_web'
1255 LOCK_WEB = 'lock_web'
1256 LOCK_PULL = 'lock_pull'
1256 LOCK_PULL = 'lock_pull'
1257
1257
1258 NAME_SEP = URL_SEP
1258 NAME_SEP = URL_SEP
1259
1259
1260 repo_id = Column(
1260 repo_id = Column(
1261 "repo_id", Integer(), nullable=False, unique=True, default=None,
1261 "repo_id", Integer(), nullable=False, unique=True, default=None,
1262 primary_key=True)
1262 primary_key=True)
1263 _repo_name = Column(
1263 _repo_name = Column(
1264 "repo_name", Text(), nullable=False, default=None)
1264 "repo_name", Text(), nullable=False, default=None)
1265 _repo_name_hash = Column(
1265 _repo_name_hash = Column(
1266 "repo_name_hash", String(255), nullable=False, unique=True)
1266 "repo_name_hash", String(255), nullable=False, unique=True)
1267 repo_state = Column("repo_state", String(255), nullable=True)
1267 repo_state = Column("repo_state", String(255), nullable=True)
1268
1268
1269 clone_uri = Column(
1269 clone_uri = Column(
1270 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1270 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1271 default=None)
1271 default=None)
1272 repo_type = Column(
1272 repo_type = Column(
1273 "repo_type", String(255), nullable=False, unique=False, default=None)
1273 "repo_type", String(255), nullable=False, unique=False, default=None)
1274 user_id = Column(
1274 user_id = Column(
1275 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1275 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1276 unique=False, default=None)
1276 unique=False, default=None)
1277 private = Column(
1277 private = Column(
1278 "private", Boolean(), nullable=True, unique=None, default=None)
1278 "private", Boolean(), nullable=True, unique=None, default=None)
1279 enable_statistics = Column(
1279 enable_statistics = Column(
1280 "statistics", Boolean(), nullable=True, unique=None, default=True)
1280 "statistics", Boolean(), nullable=True, unique=None, default=True)
1281 enable_downloads = Column(
1281 enable_downloads = Column(
1282 "downloads", Boolean(), nullable=True, unique=None, default=True)
1282 "downloads", Boolean(), nullable=True, unique=None, default=True)
1283 description = Column(
1283 description = Column(
1284 "description", String(10000), nullable=True, unique=None, default=None)
1284 "description", String(10000), nullable=True, unique=None, default=None)
1285 created_on = Column(
1285 created_on = Column(
1286 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1286 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1287 default=datetime.datetime.now)
1287 default=datetime.datetime.now)
1288 updated_on = Column(
1288 updated_on = Column(
1289 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1289 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1290 default=datetime.datetime.now)
1290 default=datetime.datetime.now)
1291 _landing_revision = Column(
1291 _landing_revision = Column(
1292 "landing_revision", String(255), nullable=False, unique=False,
1292 "landing_revision", String(255), nullable=False, unique=False,
1293 default=None)
1293 default=None)
1294 enable_locking = Column(
1294 enable_locking = Column(
1295 "enable_locking", Boolean(), nullable=False, unique=None,
1295 "enable_locking", Boolean(), nullable=False, unique=None,
1296 default=False)
1296 default=False)
1297 _locked = Column(
1297 _locked = Column(
1298 "locked", String(255), nullable=True, unique=False, default=None)
1298 "locked", String(255), nullable=True, unique=False, default=None)
1299 _changeset_cache = Column(
1299 _changeset_cache = Column(
1300 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1300 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1301
1301
1302 fork_id = Column(
1302 fork_id = Column(
1303 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1303 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1304 nullable=True, unique=False, default=None)
1304 nullable=True, unique=False, default=None)
1305 group_id = Column(
1305 group_id = Column(
1306 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1306 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1307 unique=False, default=None)
1307 unique=False, default=None)
1308
1308
1309 user = relationship('User', lazy='joined')
1309 user = relationship('User', lazy='joined')
1310 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1310 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1311 group = relationship('RepoGroup', lazy='joined')
1311 group = relationship('RepoGroup', lazy='joined')
1312 repo_to_perm = relationship(
1312 repo_to_perm = relationship(
1313 'UserRepoToPerm', cascade='all',
1313 'UserRepoToPerm', cascade='all',
1314 order_by='UserRepoToPerm.repo_to_perm_id')
1314 order_by='UserRepoToPerm.repo_to_perm_id')
1315 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1315 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1316 stats = relationship('Statistics', cascade='all', uselist=False)
1316 stats = relationship('Statistics', cascade='all', uselist=False)
1317
1317
1318 followers = relationship(
1318 followers = relationship(
1319 'UserFollowing',
1319 'UserFollowing',
1320 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1320 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1321 cascade='all')
1321 cascade='all')
1322 extra_fields = relationship(
1322 extra_fields = relationship(
1323 'RepositoryField', cascade="all, delete, delete-orphan")
1323 'RepositoryField', cascade="all, delete, delete-orphan")
1324 logs = relationship('UserLog')
1324 logs = relationship('UserLog')
1325 comments = relationship(
1325 comments = relationship(
1326 'ChangesetComment', cascade="all, delete, delete-orphan")
1326 'ChangesetComment', cascade="all, delete, delete-orphan")
1327 pull_requests_source = relationship(
1327 pull_requests_source = relationship(
1328 'PullRequest',
1328 'PullRequest',
1329 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1329 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1330 cascade="all, delete, delete-orphan")
1330 cascade="all, delete, delete-orphan")
1331 pull_requests_target = relationship(
1331 pull_requests_target = relationship(
1332 'PullRequest',
1332 'PullRequest',
1333 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1333 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1334 cascade="all, delete, delete-orphan")
1334 cascade="all, delete, delete-orphan")
1335 ui = relationship('RepoRhodeCodeUi', cascade="all")
1335 ui = relationship('RepoRhodeCodeUi', cascade="all")
1336 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1336 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1337 integrations = relationship('Integration',
1337 integrations = relationship('Integration',
1338 cascade="all, delete, delete-orphan")
1338 cascade="all, delete, delete-orphan")
1339
1339
1340 def __unicode__(self):
1340 def __unicode__(self):
1341 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1341 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1342 safe_unicode(self.repo_name))
1342 safe_unicode(self.repo_name))
1343
1343
1344 @hybrid_property
1344 @hybrid_property
1345 def landing_rev(self):
1345 def landing_rev(self):
1346 # always should return [rev_type, rev]
1346 # always should return [rev_type, rev]
1347 if self._landing_revision:
1347 if self._landing_revision:
1348 _rev_info = self._landing_revision.split(':')
1348 _rev_info = self._landing_revision.split(':')
1349 if len(_rev_info) < 2:
1349 if len(_rev_info) < 2:
1350 _rev_info.insert(0, 'rev')
1350 _rev_info.insert(0, 'rev')
1351 return [_rev_info[0], _rev_info[1]]
1351 return [_rev_info[0], _rev_info[1]]
1352 return [None, None]
1352 return [None, None]
1353
1353
1354 @landing_rev.setter
1354 @landing_rev.setter
1355 def landing_rev(self, val):
1355 def landing_rev(self, val):
1356 if ':' not in val:
1356 if ':' not in val:
1357 raise ValueError('value must be delimited with `:` and consist '
1357 raise ValueError('value must be delimited with `:` and consist '
1358 'of <rev_type>:<rev>, got %s instead' % val)
1358 'of <rev_type>:<rev>, got %s instead' % val)
1359 self._landing_revision = val
1359 self._landing_revision = val
1360
1360
1361 @hybrid_property
1361 @hybrid_property
1362 def locked(self):
1362 def locked(self):
1363 if self._locked:
1363 if self._locked:
1364 user_id, timelocked, reason = self._locked.split(':')
1364 user_id, timelocked, reason = self._locked.split(':')
1365 lock_values = int(user_id), timelocked, reason
1365 lock_values = int(user_id), timelocked, reason
1366 else:
1366 else:
1367 lock_values = [None, None, None]
1367 lock_values = [None, None, None]
1368 return lock_values
1368 return lock_values
1369
1369
1370 @locked.setter
1370 @locked.setter
1371 def locked(self, val):
1371 def locked(self, val):
1372 if val and isinstance(val, (list, tuple)):
1372 if val and isinstance(val, (list, tuple)):
1373 self._locked = ':'.join(map(str, val))
1373 self._locked = ':'.join(map(str, val))
1374 else:
1374 else:
1375 self._locked = None
1375 self._locked = None
1376
1376
1377 @hybrid_property
1377 @hybrid_property
1378 def changeset_cache(self):
1378 def changeset_cache(self):
1379 from rhodecode.lib.vcs.backends.base import EmptyCommit
1379 from rhodecode.lib.vcs.backends.base import EmptyCommit
1380 dummy = EmptyCommit().__json__()
1380 dummy = EmptyCommit().__json__()
1381 if not self._changeset_cache:
1381 if not self._changeset_cache:
1382 return dummy
1382 return dummy
1383 try:
1383 try:
1384 return json.loads(self._changeset_cache)
1384 return json.loads(self._changeset_cache)
1385 except TypeError:
1385 except TypeError:
1386 return dummy
1386 return dummy
1387 except Exception:
1387 except Exception:
1388 log.error(traceback.format_exc())
1388 log.error(traceback.format_exc())
1389 return dummy
1389 return dummy
1390
1390
1391 @changeset_cache.setter
1391 @changeset_cache.setter
1392 def changeset_cache(self, val):
1392 def changeset_cache(self, val):
1393 try:
1393 try:
1394 self._changeset_cache = json.dumps(val)
1394 self._changeset_cache = json.dumps(val)
1395 except Exception:
1395 except Exception:
1396 log.error(traceback.format_exc())
1396 log.error(traceback.format_exc())
1397
1397
1398 @hybrid_property
1398 @hybrid_property
1399 def repo_name(self):
1399 def repo_name(self):
1400 return self._repo_name
1400 return self._repo_name
1401
1401
1402 @repo_name.setter
1402 @repo_name.setter
1403 def repo_name(self, value):
1403 def repo_name(self, value):
1404 self._repo_name = value
1404 self._repo_name = value
1405 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1405 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1406
1406
1407 @classmethod
1407 @classmethod
1408 def normalize_repo_name(cls, repo_name):
1408 def normalize_repo_name(cls, repo_name):
1409 """
1409 """
1410 Normalizes os specific repo_name to the format internally stored inside
1410 Normalizes os specific repo_name to the format internally stored inside
1411 database using URL_SEP
1411 database using URL_SEP
1412
1412
1413 :param cls:
1413 :param cls:
1414 :param repo_name:
1414 :param repo_name:
1415 """
1415 """
1416 return cls.NAME_SEP.join(repo_name.split(os.sep))
1416 return cls.NAME_SEP.join(repo_name.split(os.sep))
1417
1417
1418 @classmethod
1418 @classmethod
1419 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1419 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1420 session = Session()
1420 session = Session()
1421 q = session.query(cls).filter(cls.repo_name == repo_name)
1421 q = session.query(cls).filter(cls.repo_name == repo_name)
1422
1422
1423 if cache:
1423 if cache:
1424 if identity_cache:
1424 if identity_cache:
1425 val = cls.identity_cache(session, 'repo_name', repo_name)
1425 val = cls.identity_cache(session, 'repo_name', repo_name)
1426 if val:
1426 if val:
1427 return val
1427 return val
1428 else:
1428 else:
1429 q = q.options(
1429 q = q.options(
1430 FromCache("sql_cache_short",
1430 FromCache("sql_cache_short",
1431 "get_repo_by_name_%s" % _hash_key(repo_name)))
1431 "get_repo_by_name_%s" % _hash_key(repo_name)))
1432
1432
1433 return q.scalar()
1433 return q.scalar()
1434
1434
1435 @classmethod
1435 @classmethod
1436 def get_by_full_path(cls, repo_full_path):
1436 def get_by_full_path(cls, repo_full_path):
1437 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1437 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1438 repo_name = cls.normalize_repo_name(repo_name)
1438 repo_name = cls.normalize_repo_name(repo_name)
1439 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1439 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1440
1440
1441 @classmethod
1441 @classmethod
1442 def get_repo_forks(cls, repo_id):
1442 def get_repo_forks(cls, repo_id):
1443 return cls.query().filter(Repository.fork_id == repo_id)
1443 return cls.query().filter(Repository.fork_id == repo_id)
1444
1444
1445 @classmethod
1445 @classmethod
1446 def base_path(cls):
1446 def base_path(cls):
1447 """
1447 """
1448 Returns base path when all repos are stored
1448 Returns base path when all repos are stored
1449
1449
1450 :param cls:
1450 :param cls:
1451 """
1451 """
1452 q = Session().query(RhodeCodeUi)\
1452 q = Session().query(RhodeCodeUi)\
1453 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1453 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1454 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1454 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1455 return q.one().ui_value
1455 return q.one().ui_value
1456
1456
1457 @classmethod
1457 @classmethod
1458 def is_valid(cls, repo_name):
1458 def is_valid(cls, repo_name):
1459 """
1459 """
1460 returns True if given repo name is a valid filesystem repository
1460 returns True if given repo name is a valid filesystem repository
1461
1461
1462 :param cls:
1462 :param cls:
1463 :param repo_name:
1463 :param repo_name:
1464 """
1464 """
1465 from rhodecode.lib.utils import is_valid_repo
1465 from rhodecode.lib.utils import is_valid_repo
1466
1466
1467 return is_valid_repo(repo_name, cls.base_path())
1467 return is_valid_repo(repo_name, cls.base_path())
1468
1468
1469 @classmethod
1469 @classmethod
1470 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1470 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1471 case_insensitive=True):
1471 case_insensitive=True):
1472 q = Repository.query()
1472 q = Repository.query()
1473
1473
1474 if not isinstance(user_id, Optional):
1474 if not isinstance(user_id, Optional):
1475 q = q.filter(Repository.user_id == user_id)
1475 q = q.filter(Repository.user_id == user_id)
1476
1476
1477 if not isinstance(group_id, Optional):
1477 if not isinstance(group_id, Optional):
1478 q = q.filter(Repository.group_id == group_id)
1478 q = q.filter(Repository.group_id == group_id)
1479
1479
1480 if case_insensitive:
1480 if case_insensitive:
1481 q = q.order_by(func.lower(Repository.repo_name))
1481 q = q.order_by(func.lower(Repository.repo_name))
1482 else:
1482 else:
1483 q = q.order_by(Repository.repo_name)
1483 q = q.order_by(Repository.repo_name)
1484 return q.all()
1484 return q.all()
1485
1485
1486 @property
1486 @property
1487 def forks(self):
1487 def forks(self):
1488 """
1488 """
1489 Return forks of this repo
1489 Return forks of this repo
1490 """
1490 """
1491 return Repository.get_repo_forks(self.repo_id)
1491 return Repository.get_repo_forks(self.repo_id)
1492
1492
1493 @property
1493 @property
1494 def parent(self):
1494 def parent(self):
1495 """
1495 """
1496 Returns fork parent
1496 Returns fork parent
1497 """
1497 """
1498 return self.fork
1498 return self.fork
1499
1499
1500 @property
1500 @property
1501 def just_name(self):
1501 def just_name(self):
1502 return self.repo_name.split(self.NAME_SEP)[-1]
1502 return self.repo_name.split(self.NAME_SEP)[-1]
1503
1503
1504 @property
1504 @property
1505 def groups_with_parents(self):
1505 def groups_with_parents(self):
1506 groups = []
1506 groups = []
1507 if self.group is None:
1507 if self.group is None:
1508 return groups
1508 return groups
1509
1509
1510 cur_gr = self.group
1510 cur_gr = self.group
1511 groups.insert(0, cur_gr)
1511 groups.insert(0, cur_gr)
1512 while 1:
1512 while 1:
1513 gr = getattr(cur_gr, 'parent_group', None)
1513 gr = getattr(cur_gr, 'parent_group', None)
1514 cur_gr = cur_gr.parent_group
1514 cur_gr = cur_gr.parent_group
1515 if gr is None:
1515 if gr is None:
1516 break
1516 break
1517 groups.insert(0, gr)
1517 groups.insert(0, gr)
1518
1518
1519 return groups
1519 return groups
1520
1520
1521 @property
1521 @property
1522 def groups_and_repo(self):
1522 def groups_and_repo(self):
1523 return self.groups_with_parents, self
1523 return self.groups_with_parents, self
1524
1524
1525 @LazyProperty
1525 @LazyProperty
1526 def repo_path(self):
1526 def repo_path(self):
1527 """
1527 """
1528 Returns base full path for that repository means where it actually
1528 Returns base full path for that repository means where it actually
1529 exists on a filesystem
1529 exists on a filesystem
1530 """
1530 """
1531 q = Session().query(RhodeCodeUi).filter(
1531 q = Session().query(RhodeCodeUi).filter(
1532 RhodeCodeUi.ui_key == self.NAME_SEP)
1532 RhodeCodeUi.ui_key == self.NAME_SEP)
1533 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1533 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1534 return q.one().ui_value
1534 return q.one().ui_value
1535
1535
1536 @property
1536 @property
1537 def repo_full_path(self):
1537 def repo_full_path(self):
1538 p = [self.repo_path]
1538 p = [self.repo_path]
1539 # we need to split the name by / since this is how we store the
1539 # we need to split the name by / since this is how we store the
1540 # names in the database, but that eventually needs to be converted
1540 # names in the database, but that eventually needs to be converted
1541 # into a valid system path
1541 # into a valid system path
1542 p += self.repo_name.split(self.NAME_SEP)
1542 p += self.repo_name.split(self.NAME_SEP)
1543 return os.path.join(*map(safe_unicode, p))
1543 return os.path.join(*map(safe_unicode, p))
1544
1544
1545 @property
1545 @property
1546 def cache_keys(self):
1546 def cache_keys(self):
1547 """
1547 """
1548 Returns associated cache keys for that repo
1548 Returns associated cache keys for that repo
1549 """
1549 """
1550 return CacheKey.query()\
1550 return CacheKey.query()\
1551 .filter(CacheKey.cache_args == self.repo_name)\
1551 .filter(CacheKey.cache_args == self.repo_name)\
1552 .order_by(CacheKey.cache_key)\
1552 .order_by(CacheKey.cache_key)\
1553 .all()
1553 .all()
1554
1554
1555 def get_new_name(self, repo_name):
1555 def get_new_name(self, repo_name):
1556 """
1556 """
1557 returns new full repository name based on assigned group and new new
1557 returns new full repository name based on assigned group and new new
1558
1558
1559 :param group_name:
1559 :param group_name:
1560 """
1560 """
1561 path_prefix = self.group.full_path_splitted if self.group else []
1561 path_prefix = self.group.full_path_splitted if self.group else []
1562 return self.NAME_SEP.join(path_prefix + [repo_name])
1562 return self.NAME_SEP.join(path_prefix + [repo_name])
1563
1563
1564 @property
1564 @property
1565 def _config(self):
1565 def _config(self):
1566 """
1566 """
1567 Returns db based config object.
1567 Returns db based config object.
1568 """
1568 """
1569 from rhodecode.lib.utils import make_db_config
1569 from rhodecode.lib.utils import make_db_config
1570 return make_db_config(clear_session=False, repo=self)
1570 return make_db_config(clear_session=False, repo=self)
1571
1571
1572 def permissions(self, with_admins=True, with_owner=True):
1572 def permissions(self, with_admins=True, with_owner=True):
1573 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1573 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1574 q = q.options(joinedload(UserRepoToPerm.repository),
1574 q = q.options(joinedload(UserRepoToPerm.repository),
1575 joinedload(UserRepoToPerm.user),
1575 joinedload(UserRepoToPerm.user),
1576 joinedload(UserRepoToPerm.permission),)
1576 joinedload(UserRepoToPerm.permission),)
1577
1577
1578 # get owners and admins and permissions. We do a trick of re-writing
1578 # get owners and admins and permissions. We do a trick of re-writing
1579 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1579 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1580 # has a global reference and changing one object propagates to all
1580 # has a global reference and changing one object propagates to all
1581 # others. This means if admin is also an owner admin_row that change
1581 # others. This means if admin is also an owner admin_row that change
1582 # would propagate to both objects
1582 # would propagate to both objects
1583 perm_rows = []
1583 perm_rows = []
1584 for _usr in q.all():
1584 for _usr in q.all():
1585 usr = AttributeDict(_usr.user.get_dict())
1585 usr = AttributeDict(_usr.user.get_dict())
1586 usr.permission = _usr.permission.permission_name
1586 usr.permission = _usr.permission.permission_name
1587 perm_rows.append(usr)
1587 perm_rows.append(usr)
1588
1588
1589 # filter the perm rows by 'default' first and then sort them by
1589 # filter the perm rows by 'default' first and then sort them by
1590 # admin,write,read,none permissions sorted again alphabetically in
1590 # admin,write,read,none permissions sorted again alphabetically in
1591 # each group
1591 # each group
1592 perm_rows = sorted(perm_rows, key=display_sort)
1592 perm_rows = sorted(perm_rows, key=display_sort)
1593
1593
1594 _admin_perm = 'repository.admin'
1594 _admin_perm = 'repository.admin'
1595 owner_row = []
1595 owner_row = []
1596 if with_owner:
1596 if with_owner:
1597 usr = AttributeDict(self.user.get_dict())
1597 usr = AttributeDict(self.user.get_dict())
1598 usr.owner_row = True
1598 usr.owner_row = True
1599 usr.permission = _admin_perm
1599 usr.permission = _admin_perm
1600 owner_row.append(usr)
1600 owner_row.append(usr)
1601
1601
1602 super_admin_rows = []
1602 super_admin_rows = []
1603 if with_admins:
1603 if with_admins:
1604 for usr in User.get_all_super_admins():
1604 for usr in User.get_all_super_admins():
1605 # if this admin is also owner, don't double the record
1605 # if this admin is also owner, don't double the record
1606 if usr.user_id == owner_row[0].user_id:
1606 if usr.user_id == owner_row[0].user_id:
1607 owner_row[0].admin_row = True
1607 owner_row[0].admin_row = True
1608 else:
1608 else:
1609 usr = AttributeDict(usr.get_dict())
1609 usr = AttributeDict(usr.get_dict())
1610 usr.admin_row = True
1610 usr.admin_row = True
1611 usr.permission = _admin_perm
1611 usr.permission = _admin_perm
1612 super_admin_rows.append(usr)
1612 super_admin_rows.append(usr)
1613
1613
1614 return super_admin_rows + owner_row + perm_rows
1614 return super_admin_rows + owner_row + perm_rows
1615
1615
1616 def permission_user_groups(self):
1616 def permission_user_groups(self):
1617 q = UserGroupRepoToPerm.query().filter(
1617 q = UserGroupRepoToPerm.query().filter(
1618 UserGroupRepoToPerm.repository == self)
1618 UserGroupRepoToPerm.repository == self)
1619 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1619 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1620 joinedload(UserGroupRepoToPerm.users_group),
1620 joinedload(UserGroupRepoToPerm.users_group),
1621 joinedload(UserGroupRepoToPerm.permission),)
1621 joinedload(UserGroupRepoToPerm.permission),)
1622
1622
1623 perm_rows = []
1623 perm_rows = []
1624 for _user_group in q.all():
1624 for _user_group in q.all():
1625 usr = AttributeDict(_user_group.users_group.get_dict())
1625 usr = AttributeDict(_user_group.users_group.get_dict())
1626 usr.permission = _user_group.permission.permission_name
1626 usr.permission = _user_group.permission.permission_name
1627 perm_rows.append(usr)
1627 perm_rows.append(usr)
1628
1628
1629 return perm_rows
1629 return perm_rows
1630
1630
1631 def get_api_data(self, include_secrets=False):
1631 def get_api_data(self, include_secrets=False):
1632 """
1632 """
1633 Common function for generating repo api data
1633 Common function for generating repo api data
1634
1634
1635 :param include_secrets: See :meth:`User.get_api_data`.
1635 :param include_secrets: See :meth:`User.get_api_data`.
1636
1636
1637 """
1637 """
1638 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1638 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1639 # move this methods on models level.
1639 # move this methods on models level.
1640 from rhodecode.model.settings import SettingsModel
1640 from rhodecode.model.settings import SettingsModel
1641
1641
1642 repo = self
1642 repo = self
1643 _user_id, _time, _reason = self.locked
1643 _user_id, _time, _reason = self.locked
1644
1644
1645 data = {
1645 data = {
1646 'repo_id': repo.repo_id,
1646 'repo_id': repo.repo_id,
1647 'repo_name': repo.repo_name,
1647 'repo_name': repo.repo_name,
1648 'repo_type': repo.repo_type,
1648 'repo_type': repo.repo_type,
1649 'clone_uri': repo.clone_uri or '',
1649 'clone_uri': repo.clone_uri or '',
1650 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1650 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1651 'private': repo.private,
1651 'private': repo.private,
1652 'created_on': repo.created_on,
1652 'created_on': repo.created_on,
1653 'description': repo.description,
1653 'description': repo.description,
1654 'landing_rev': repo.landing_rev,
1654 'landing_rev': repo.landing_rev,
1655 'owner': repo.user.username,
1655 'owner': repo.user.username,
1656 'fork_of': repo.fork.repo_name if repo.fork else None,
1656 'fork_of': repo.fork.repo_name if repo.fork else None,
1657 'enable_statistics': repo.enable_statistics,
1657 'enable_statistics': repo.enable_statistics,
1658 'enable_locking': repo.enable_locking,
1658 'enable_locking': repo.enable_locking,
1659 'enable_downloads': repo.enable_downloads,
1659 'enable_downloads': repo.enable_downloads,
1660 'last_changeset': repo.changeset_cache,
1660 'last_changeset': repo.changeset_cache,
1661 'locked_by': User.get(_user_id).get_api_data(
1661 'locked_by': User.get(_user_id).get_api_data(
1662 include_secrets=include_secrets) if _user_id else None,
1662 include_secrets=include_secrets) if _user_id else None,
1663 'locked_date': time_to_datetime(_time) if _time else None,
1663 'locked_date': time_to_datetime(_time) if _time else None,
1664 'lock_reason': _reason if _reason else None,
1664 'lock_reason': _reason if _reason else None,
1665 }
1665 }
1666
1666
1667 # TODO: mikhail: should be per-repo settings here
1667 # TODO: mikhail: should be per-repo settings here
1668 rc_config = SettingsModel().get_all_settings()
1668 rc_config = SettingsModel().get_all_settings()
1669 repository_fields = str2bool(
1669 repository_fields = str2bool(
1670 rc_config.get('rhodecode_repository_fields'))
1670 rc_config.get('rhodecode_repository_fields'))
1671 if repository_fields:
1671 if repository_fields:
1672 for f in self.extra_fields:
1672 for f in self.extra_fields:
1673 data[f.field_key_prefixed] = f.field_value
1673 data[f.field_key_prefixed] = f.field_value
1674
1674
1675 return data
1675 return data
1676
1676
1677 @classmethod
1677 @classmethod
1678 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1678 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1679 if not lock_time:
1679 if not lock_time:
1680 lock_time = time.time()
1680 lock_time = time.time()
1681 if not lock_reason:
1681 if not lock_reason:
1682 lock_reason = cls.LOCK_AUTOMATIC
1682 lock_reason = cls.LOCK_AUTOMATIC
1683 repo.locked = [user_id, lock_time, lock_reason]
1683 repo.locked = [user_id, lock_time, lock_reason]
1684 Session().add(repo)
1684 Session().add(repo)
1685 Session().commit()
1685 Session().commit()
1686
1686
1687 @classmethod
1687 @classmethod
1688 def unlock(cls, repo):
1688 def unlock(cls, repo):
1689 repo.locked = None
1689 repo.locked = None
1690 Session().add(repo)
1690 Session().add(repo)
1691 Session().commit()
1691 Session().commit()
1692
1692
1693 @classmethod
1693 @classmethod
1694 def getlock(cls, repo):
1694 def getlock(cls, repo):
1695 return repo.locked
1695 return repo.locked
1696
1696
1697 def is_user_lock(self, user_id):
1697 def is_user_lock(self, user_id):
1698 if self.lock[0]:
1698 if self.lock[0]:
1699 lock_user_id = safe_int(self.lock[0])
1699 lock_user_id = safe_int(self.lock[0])
1700 user_id = safe_int(user_id)
1700 user_id = safe_int(user_id)
1701 # both are ints, and they are equal
1701 # both are ints, and they are equal
1702 return all([lock_user_id, user_id]) and lock_user_id == user_id
1702 return all([lock_user_id, user_id]) and lock_user_id == user_id
1703
1703
1704 return False
1704 return False
1705
1705
1706 def get_locking_state(self, action, user_id, only_when_enabled=True):
1706 def get_locking_state(self, action, user_id, only_when_enabled=True):
1707 """
1707 """
1708 Checks locking on this repository, if locking is enabled and lock is
1708 Checks locking on this repository, if locking is enabled and lock is
1709 present returns a tuple of make_lock, locked, locked_by.
1709 present returns a tuple of make_lock, locked, locked_by.
1710 make_lock can have 3 states None (do nothing) True, make lock
1710 make_lock can have 3 states None (do nothing) True, make lock
1711 False release lock, This value is later propagated to hooks, which
1711 False release lock, This value is later propagated to hooks, which
1712 do the locking. Think about this as signals passed to hooks what to do.
1712 do the locking. Think about this as signals passed to hooks what to do.
1713
1713
1714 """
1714 """
1715 # TODO: johbo: This is part of the business logic and should be moved
1715 # TODO: johbo: This is part of the business logic and should be moved
1716 # into the RepositoryModel.
1716 # into the RepositoryModel.
1717
1717
1718 if action not in ('push', 'pull'):
1718 if action not in ('push', 'pull'):
1719 raise ValueError("Invalid action value: %s" % repr(action))
1719 raise ValueError("Invalid action value: %s" % repr(action))
1720
1720
1721 # defines if locked error should be thrown to user
1721 # defines if locked error should be thrown to user
1722 currently_locked = False
1722 currently_locked = False
1723 # defines if new lock should be made, tri-state
1723 # defines if new lock should be made, tri-state
1724 make_lock = None
1724 make_lock = None
1725 repo = self
1725 repo = self
1726 user = User.get(user_id)
1726 user = User.get(user_id)
1727
1727
1728 lock_info = repo.locked
1728 lock_info = repo.locked
1729
1729
1730 if repo and (repo.enable_locking or not only_when_enabled):
1730 if repo and (repo.enable_locking or not only_when_enabled):
1731 if action == 'push':
1731 if action == 'push':
1732 # check if it's already locked !, if it is compare users
1732 # check if it's already locked !, if it is compare users
1733 locked_by_user_id = lock_info[0]
1733 locked_by_user_id = lock_info[0]
1734 if user.user_id == locked_by_user_id:
1734 if user.user_id == locked_by_user_id:
1735 log.debug(
1735 log.debug(
1736 'Got `push` action from user %s, now unlocking', user)
1736 'Got `push` action from user %s, now unlocking', user)
1737 # unlock if we have push from user who locked
1737 # unlock if we have push from user who locked
1738 make_lock = False
1738 make_lock = False
1739 else:
1739 else:
1740 # we're not the same user who locked, ban with
1740 # we're not the same user who locked, ban with
1741 # code defined in settings (default is 423 HTTP Locked) !
1741 # code defined in settings (default is 423 HTTP Locked) !
1742 log.debug('Repo %s is currently locked by %s', repo, user)
1742 log.debug('Repo %s is currently locked by %s', repo, user)
1743 currently_locked = True
1743 currently_locked = True
1744 elif action == 'pull':
1744 elif action == 'pull':
1745 # [0] user [1] date
1745 # [0] user [1] date
1746 if lock_info[0] and lock_info[1]:
1746 if lock_info[0] and lock_info[1]:
1747 log.debug('Repo %s is currently locked by %s', repo, user)
1747 log.debug('Repo %s is currently locked by %s', repo, user)
1748 currently_locked = True
1748 currently_locked = True
1749 else:
1749 else:
1750 log.debug('Setting lock on repo %s by %s', repo, user)
1750 log.debug('Setting lock on repo %s by %s', repo, user)
1751 make_lock = True
1751 make_lock = True
1752
1752
1753 else:
1753 else:
1754 log.debug('Repository %s do not have locking enabled', repo)
1754 log.debug('Repository %s do not have locking enabled', repo)
1755
1755
1756 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1756 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1757 make_lock, currently_locked, lock_info)
1757 make_lock, currently_locked, lock_info)
1758
1758
1759 from rhodecode.lib.auth import HasRepoPermissionAny
1759 from rhodecode.lib.auth import HasRepoPermissionAny
1760 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1760 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1761 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1761 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1762 # if we don't have at least write permission we cannot make a lock
1762 # if we don't have at least write permission we cannot make a lock
1763 log.debug('lock state reset back to FALSE due to lack '
1763 log.debug('lock state reset back to FALSE due to lack '
1764 'of at least read permission')
1764 'of at least read permission')
1765 make_lock = False
1765 make_lock = False
1766
1766
1767 return make_lock, currently_locked, lock_info
1767 return make_lock, currently_locked, lock_info
1768
1768
1769 @property
1769 @property
1770 def last_db_change(self):
1770 def last_db_change(self):
1771 return self.updated_on
1771 return self.updated_on
1772
1772
1773 @property
1773 @property
1774 def clone_uri_hidden(self):
1774 def clone_uri_hidden(self):
1775 clone_uri = self.clone_uri
1775 clone_uri = self.clone_uri
1776 if clone_uri:
1776 if clone_uri:
1777 import urlobject
1777 import urlobject
1778 url_obj = urlobject.URLObject(clone_uri)
1778 url_obj = urlobject.URLObject(clone_uri)
1779 if url_obj.password:
1779 if url_obj.password:
1780 clone_uri = url_obj.with_password('*****')
1780 clone_uri = url_obj.with_password('*****')
1781 return clone_uri
1781 return clone_uri
1782
1782
1783 def clone_url(self, **override):
1783 def clone_url(self, **override):
1784 qualified_home_url = url('home', qualified=True)
1784 qualified_home_url = url('home', qualified=True)
1785
1785
1786 uri_tmpl = None
1786 uri_tmpl = None
1787 if 'with_id' in override:
1787 if 'with_id' in override:
1788 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1788 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1789 del override['with_id']
1789 del override['with_id']
1790
1790
1791 if 'uri_tmpl' in override:
1791 if 'uri_tmpl' in override:
1792 uri_tmpl = override['uri_tmpl']
1792 uri_tmpl = override['uri_tmpl']
1793 del override['uri_tmpl']
1793 del override['uri_tmpl']
1794
1794
1795 # we didn't override our tmpl from **overrides
1795 # we didn't override our tmpl from **overrides
1796 if not uri_tmpl:
1796 if not uri_tmpl:
1797 uri_tmpl = self.DEFAULT_CLONE_URI
1797 uri_tmpl = self.DEFAULT_CLONE_URI
1798 try:
1798 try:
1799 from pylons import tmpl_context as c
1799 from pylons import tmpl_context as c
1800 uri_tmpl = c.clone_uri_tmpl
1800 uri_tmpl = c.clone_uri_tmpl
1801 except Exception:
1801 except Exception:
1802 # in any case if we call this outside of request context,
1802 # in any case if we call this outside of request context,
1803 # ie, not having tmpl_context set up
1803 # ie, not having tmpl_context set up
1804 pass
1804 pass
1805
1805
1806 return get_clone_url(uri_tmpl=uri_tmpl,
1806 return get_clone_url(uri_tmpl=uri_tmpl,
1807 qualifed_home_url=qualified_home_url,
1807 qualifed_home_url=qualified_home_url,
1808 repo_name=self.repo_name,
1808 repo_name=self.repo_name,
1809 repo_id=self.repo_id, **override)
1809 repo_id=self.repo_id, **override)
1810
1810
1811 def set_state(self, state):
1811 def set_state(self, state):
1812 self.repo_state = state
1812 self.repo_state = state
1813 Session().add(self)
1813 Session().add(self)
1814 #==========================================================================
1814 #==========================================================================
1815 # SCM PROPERTIES
1815 # SCM PROPERTIES
1816 #==========================================================================
1816 #==========================================================================
1817
1817
1818 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1818 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1819 return get_commit_safe(
1819 return get_commit_safe(
1820 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1820 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1821
1821
1822 def get_changeset(self, rev=None, pre_load=None):
1822 def get_changeset(self, rev=None, pre_load=None):
1823 warnings.warn("Use get_commit", DeprecationWarning)
1823 warnings.warn("Use get_commit", DeprecationWarning)
1824 commit_id = None
1824 commit_id = None
1825 commit_idx = None
1825 commit_idx = None
1826 if isinstance(rev, basestring):
1826 if isinstance(rev, basestring):
1827 commit_id = rev
1827 commit_id = rev
1828 else:
1828 else:
1829 commit_idx = rev
1829 commit_idx = rev
1830 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1830 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1831 pre_load=pre_load)
1831 pre_load=pre_load)
1832
1832
1833 def get_landing_commit(self):
1833 def get_landing_commit(self):
1834 """
1834 """
1835 Returns landing commit, or if that doesn't exist returns the tip
1835 Returns landing commit, or if that doesn't exist returns the tip
1836 """
1836 """
1837 _rev_type, _rev = self.landing_rev
1837 _rev_type, _rev = self.landing_rev
1838 commit = self.get_commit(_rev)
1838 commit = self.get_commit(_rev)
1839 if isinstance(commit, EmptyCommit):
1839 if isinstance(commit, EmptyCommit):
1840 return self.get_commit()
1840 return self.get_commit()
1841 return commit
1841 return commit
1842
1842
1843 def update_commit_cache(self, cs_cache=None, config=None):
1843 def update_commit_cache(self, cs_cache=None, config=None):
1844 """
1844 """
1845 Update cache of last changeset for repository, keys should be::
1845 Update cache of last changeset for repository, keys should be::
1846
1846
1847 short_id
1847 short_id
1848 raw_id
1848 raw_id
1849 revision
1849 revision
1850 parents
1850 parents
1851 message
1851 message
1852 date
1852 date
1853 author
1853 author
1854
1854
1855 :param cs_cache:
1855 :param cs_cache:
1856 """
1856 """
1857 from rhodecode.lib.vcs.backends.base import BaseChangeset
1857 from rhodecode.lib.vcs.backends.base import BaseChangeset
1858 if cs_cache is None:
1858 if cs_cache is None:
1859 # use no-cache version here
1859 # use no-cache version here
1860 scm_repo = self.scm_instance(cache=False, config=config)
1860 scm_repo = self.scm_instance(cache=False, config=config)
1861 if scm_repo:
1861 if scm_repo:
1862 cs_cache = scm_repo.get_commit(
1862 cs_cache = scm_repo.get_commit(
1863 pre_load=["author", "date", "message", "parents"])
1863 pre_load=["author", "date", "message", "parents"])
1864 else:
1864 else:
1865 cs_cache = EmptyCommit()
1865 cs_cache = EmptyCommit()
1866
1866
1867 if isinstance(cs_cache, BaseChangeset):
1867 if isinstance(cs_cache, BaseChangeset):
1868 cs_cache = cs_cache.__json__()
1868 cs_cache = cs_cache.__json__()
1869
1869
1870 def is_outdated(new_cs_cache):
1870 def is_outdated(new_cs_cache):
1871 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1871 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1872 new_cs_cache['revision'] != self.changeset_cache['revision']):
1872 new_cs_cache['revision'] != self.changeset_cache['revision']):
1873 return True
1873 return True
1874 return False
1874 return False
1875
1875
1876 # check if we have maybe already latest cached revision
1876 # check if we have maybe already latest cached revision
1877 if is_outdated(cs_cache) or not self.changeset_cache:
1877 if is_outdated(cs_cache) or not self.changeset_cache:
1878 _default = datetime.datetime.fromtimestamp(0)
1878 _default = datetime.datetime.fromtimestamp(0)
1879 last_change = cs_cache.get('date') or _default
1879 last_change = cs_cache.get('date') or _default
1880 log.debug('updated repo %s with new cs cache %s',
1880 log.debug('updated repo %s with new cs cache %s',
1881 self.repo_name, cs_cache)
1881 self.repo_name, cs_cache)
1882 self.updated_on = last_change
1882 self.updated_on = last_change
1883 self.changeset_cache = cs_cache
1883 self.changeset_cache = cs_cache
1884 Session().add(self)
1884 Session().add(self)
1885 Session().commit()
1885 Session().commit()
1886 else:
1886 else:
1887 log.debug('Skipping update_commit_cache for repo:`%s` '
1887 log.debug('Skipping update_commit_cache for repo:`%s` '
1888 'commit already with latest changes', self.repo_name)
1888 'commit already with latest changes', self.repo_name)
1889
1889
1890 @property
1890 @property
1891 def tip(self):
1891 def tip(self):
1892 return self.get_commit('tip')
1892 return self.get_commit('tip')
1893
1893
1894 @property
1894 @property
1895 def author(self):
1895 def author(self):
1896 return self.tip.author
1896 return self.tip.author
1897
1897
1898 @property
1898 @property
1899 def last_change(self):
1899 def last_change(self):
1900 return self.scm_instance().last_change
1900 return self.scm_instance().last_change
1901
1901
1902 def get_comments(self, revisions=None):
1902 def get_comments(self, revisions=None):
1903 """
1903 """
1904 Returns comments for this repository grouped by revisions
1904 Returns comments for this repository grouped by revisions
1905
1905
1906 :param revisions: filter query by revisions only
1906 :param revisions: filter query by revisions only
1907 """
1907 """
1908 cmts = ChangesetComment.query()\
1908 cmts = ChangesetComment.query()\
1909 .filter(ChangesetComment.repo == self)
1909 .filter(ChangesetComment.repo == self)
1910 if revisions:
1910 if revisions:
1911 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1911 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1912 grouped = collections.defaultdict(list)
1912 grouped = collections.defaultdict(list)
1913 for cmt in cmts.all():
1913 for cmt in cmts.all():
1914 grouped[cmt.revision].append(cmt)
1914 grouped[cmt.revision].append(cmt)
1915 return grouped
1915 return grouped
1916
1916
1917 def statuses(self, revisions=None):
1917 def statuses(self, revisions=None):
1918 """
1918 """
1919 Returns statuses for this repository
1919 Returns statuses for this repository
1920
1920
1921 :param revisions: list of revisions to get statuses for
1921 :param revisions: list of revisions to get statuses for
1922 """
1922 """
1923 statuses = ChangesetStatus.query()\
1923 statuses = ChangesetStatus.query()\
1924 .filter(ChangesetStatus.repo == self)\
1924 .filter(ChangesetStatus.repo == self)\
1925 .filter(ChangesetStatus.version == 0)
1925 .filter(ChangesetStatus.version == 0)
1926
1926
1927 if revisions:
1927 if revisions:
1928 # Try doing the filtering in chunks to avoid hitting limits
1928 # Try doing the filtering in chunks to avoid hitting limits
1929 size = 500
1929 size = 500
1930 status_results = []
1930 status_results = []
1931 for chunk in xrange(0, len(revisions), size):
1931 for chunk in xrange(0, len(revisions), size):
1932 status_results += statuses.filter(
1932 status_results += statuses.filter(
1933 ChangesetStatus.revision.in_(
1933 ChangesetStatus.revision.in_(
1934 revisions[chunk: chunk+size])
1934 revisions[chunk: chunk+size])
1935 ).all()
1935 ).all()
1936 else:
1936 else:
1937 status_results = statuses.all()
1937 status_results = statuses.all()
1938
1938
1939 grouped = {}
1939 grouped = {}
1940
1940
1941 # maybe we have open new pullrequest without a status?
1941 # maybe we have open new pullrequest without a status?
1942 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1942 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1943 status_lbl = ChangesetStatus.get_status_lbl(stat)
1943 status_lbl = ChangesetStatus.get_status_lbl(stat)
1944 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1944 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1945 for rev in pr.revisions:
1945 for rev in pr.revisions:
1946 pr_id = pr.pull_request_id
1946 pr_id = pr.pull_request_id
1947 pr_repo = pr.target_repo.repo_name
1947 pr_repo = pr.target_repo.repo_name
1948 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1948 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1949
1949
1950 for stat in status_results:
1950 for stat in status_results:
1951 pr_id = pr_repo = None
1951 pr_id = pr_repo = None
1952 if stat.pull_request:
1952 if stat.pull_request:
1953 pr_id = stat.pull_request.pull_request_id
1953 pr_id = stat.pull_request.pull_request_id
1954 pr_repo = stat.pull_request.target_repo.repo_name
1954 pr_repo = stat.pull_request.target_repo.repo_name
1955 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1955 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1956 pr_id, pr_repo]
1956 pr_id, pr_repo]
1957 return grouped
1957 return grouped
1958
1958
1959 # ==========================================================================
1959 # ==========================================================================
1960 # SCM CACHE INSTANCE
1960 # SCM CACHE INSTANCE
1961 # ==========================================================================
1961 # ==========================================================================
1962
1962
1963 def scm_instance(self, **kwargs):
1963 def scm_instance(self, **kwargs):
1964 import rhodecode
1964 import rhodecode
1965
1965
1966 # Passing a config will not hit the cache currently only used
1966 # Passing a config will not hit the cache currently only used
1967 # for repo2dbmapper
1967 # for repo2dbmapper
1968 config = kwargs.pop('config', None)
1968 config = kwargs.pop('config', None)
1969 cache = kwargs.pop('cache', None)
1969 cache = kwargs.pop('cache', None)
1970 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1970 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1971 # if cache is NOT defined use default global, else we have a full
1971 # if cache is NOT defined use default global, else we have a full
1972 # control over cache behaviour
1972 # control over cache behaviour
1973 if cache is None and full_cache and not config:
1973 if cache is None and full_cache and not config:
1974 return self._get_instance_cached()
1974 return self._get_instance_cached()
1975 return self._get_instance(cache=bool(cache), config=config)
1975 return self._get_instance(cache=bool(cache), config=config)
1976
1976
1977 def _get_instance_cached(self):
1977 def _get_instance_cached(self):
1978 @cache_region('long_term')
1978 @cache_region('long_term')
1979 def _get_repo(cache_key):
1979 def _get_repo(cache_key):
1980 return self._get_instance()
1980 return self._get_instance()
1981
1981
1982 invalidator_context = CacheKey.repo_context_cache(
1982 invalidator_context = CacheKey.repo_context_cache(
1983 _get_repo, self.repo_name, None, thread_scoped=True)
1983 _get_repo, self.repo_name, None, thread_scoped=True)
1984
1984
1985 with invalidator_context as context:
1985 with invalidator_context as context:
1986 context.invalidate()
1986 context.invalidate()
1987 repo = context.compute()
1987 repo = context.compute()
1988
1988
1989 return repo
1989 return repo
1990
1990
1991 def _get_instance(self, cache=True, config=None):
1991 def _get_instance(self, cache=True, config=None):
1992 config = config or self._config
1992 config = config or self._config
1993 custom_wire = {
1993 custom_wire = {
1994 'cache': cache # controls the vcs.remote cache
1994 'cache': cache # controls the vcs.remote cache
1995 }
1995 }
1996 repo = get_vcs_instance(
1996 repo = get_vcs_instance(
1997 repo_path=safe_str(self.repo_full_path),
1997 repo_path=safe_str(self.repo_full_path),
1998 config=config,
1998 config=config,
1999 with_wire=custom_wire,
1999 with_wire=custom_wire,
2000 create=False,
2000 create=False,
2001 _vcs_alias=self.repo_type)
2001 _vcs_alias=self.repo_type)
2002
2002
2003 return repo
2003 return repo
2004
2004
2005 def __json__(self):
2005 def __json__(self):
2006 return {'landing_rev': self.landing_rev}
2006 return {'landing_rev': self.landing_rev}
2007
2007
2008 def get_dict(self):
2008 def get_dict(self):
2009
2009
2010 # Since we transformed `repo_name` to a hybrid property, we need to
2010 # Since we transformed `repo_name` to a hybrid property, we need to
2011 # keep compatibility with the code which uses `repo_name` field.
2011 # keep compatibility with the code which uses `repo_name` field.
2012
2012
2013 result = super(Repository, self).get_dict()
2013 result = super(Repository, self).get_dict()
2014 result['repo_name'] = result.pop('_repo_name', None)
2014 result['repo_name'] = result.pop('_repo_name', None)
2015 return result
2015 return result
2016
2016
2017
2017
2018 class RepoGroup(Base, BaseModel):
2018 class RepoGroup(Base, BaseModel):
2019 __tablename__ = 'groups'
2019 __tablename__ = 'groups'
2020 __table_args__ = (
2020 __table_args__ = (
2021 UniqueConstraint('group_name', 'group_parent_id'),
2021 UniqueConstraint('group_name', 'group_parent_id'),
2022 CheckConstraint('group_id != group_parent_id'),
2022 CheckConstraint('group_id != group_parent_id'),
2023 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2023 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2024 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2024 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2025 )
2025 )
2026 __mapper_args__ = {'order_by': 'group_name'}
2026 __mapper_args__ = {'order_by': 'group_name'}
2027
2027
2028 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2028 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2029
2029
2030 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2030 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2031 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2031 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2032 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2032 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2033 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2033 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2034 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2034 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2035 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2035 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2036 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2036 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2037 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2037 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2038
2038
2039 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2039 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2040 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2040 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2041 parent_group = relationship('RepoGroup', remote_side=group_id)
2041 parent_group = relationship('RepoGroup', remote_side=group_id)
2042 user = relationship('User')
2042 user = relationship('User')
2043 integrations = relationship('Integration',
2043 integrations = relationship('Integration',
2044 cascade="all, delete, delete-orphan")
2044 cascade="all, delete, delete-orphan")
2045
2045
2046 def __init__(self, group_name='', parent_group=None):
2046 def __init__(self, group_name='', parent_group=None):
2047 self.group_name = group_name
2047 self.group_name = group_name
2048 self.parent_group = parent_group
2048 self.parent_group = parent_group
2049
2049
2050 def __unicode__(self):
2050 def __unicode__(self):
2051 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2051 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2052 self.group_name)
2052 self.group_name)
2053
2053
2054 @classmethod
2054 @classmethod
2055 def _generate_choice(cls, repo_group):
2055 def _generate_choice(cls, repo_group):
2056 from webhelpers.html import literal as _literal
2056 from webhelpers.html import literal as _literal
2057 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2057 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2058 return repo_group.group_id, _name(repo_group.full_path_splitted)
2058 return repo_group.group_id, _name(repo_group.full_path_splitted)
2059
2059
2060 @classmethod
2060 @classmethod
2061 def groups_choices(cls, groups=None, show_empty_group=True):
2061 def groups_choices(cls, groups=None, show_empty_group=True):
2062 if not groups:
2062 if not groups:
2063 groups = cls.query().all()
2063 groups = cls.query().all()
2064
2064
2065 repo_groups = []
2065 repo_groups = []
2066 if show_empty_group:
2066 if show_empty_group:
2067 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2067 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2068
2068
2069 repo_groups.extend([cls._generate_choice(x) for x in groups])
2069 repo_groups.extend([cls._generate_choice(x) for x in groups])
2070
2070
2071 repo_groups = sorted(
2071 repo_groups = sorted(
2072 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2072 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2073 return repo_groups
2073 return repo_groups
2074
2074
2075 @classmethod
2075 @classmethod
2076 def url_sep(cls):
2076 def url_sep(cls):
2077 return URL_SEP
2077 return URL_SEP
2078
2078
2079 @classmethod
2079 @classmethod
2080 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2080 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2081 if case_insensitive:
2081 if case_insensitive:
2082 gr = cls.query().filter(func.lower(cls.group_name)
2082 gr = cls.query().filter(func.lower(cls.group_name)
2083 == func.lower(group_name))
2083 == func.lower(group_name))
2084 else:
2084 else:
2085 gr = cls.query().filter(cls.group_name == group_name)
2085 gr = cls.query().filter(cls.group_name == group_name)
2086 if cache:
2086 if cache:
2087 gr = gr.options(FromCache(
2087 gr = gr.options(FromCache(
2088 "sql_cache_short",
2088 "sql_cache_short",
2089 "get_group_%s" % _hash_key(group_name)))
2089 "get_group_%s" % _hash_key(group_name)))
2090 return gr.scalar()
2090 return gr.scalar()
2091
2091
2092 @classmethod
2092 @classmethod
2093 def get_user_personal_repo_group(cls, user_id):
2093 def get_user_personal_repo_group(cls, user_id):
2094 user = User.get(user_id)
2094 user = User.get(user_id)
2095 return cls.query()\
2095 return cls.query()\
2096 .filter(cls.personal == true())\
2096 .filter(cls.personal == true())\
2097 .filter(cls.user == user).scalar()
2097 .filter(cls.user == user).scalar()
2098
2098
2099 @classmethod
2099 @classmethod
2100 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2100 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2101 case_insensitive=True):
2101 case_insensitive=True):
2102 q = RepoGroup.query()
2102 q = RepoGroup.query()
2103
2103
2104 if not isinstance(user_id, Optional):
2104 if not isinstance(user_id, Optional):
2105 q = q.filter(RepoGroup.user_id == user_id)
2105 q = q.filter(RepoGroup.user_id == user_id)
2106
2106
2107 if not isinstance(group_id, Optional):
2107 if not isinstance(group_id, Optional):
2108 q = q.filter(RepoGroup.group_parent_id == group_id)
2108 q = q.filter(RepoGroup.group_parent_id == group_id)
2109
2109
2110 if case_insensitive:
2110 if case_insensitive:
2111 q = q.order_by(func.lower(RepoGroup.group_name))
2111 q = q.order_by(func.lower(RepoGroup.group_name))
2112 else:
2112 else:
2113 q = q.order_by(RepoGroup.group_name)
2113 q = q.order_by(RepoGroup.group_name)
2114 return q.all()
2114 return q.all()
2115
2115
2116 @property
2116 @property
2117 def parents(self):
2117 def parents(self):
2118 parents_recursion_limit = 10
2118 parents_recursion_limit = 10
2119 groups = []
2119 groups = []
2120 if self.parent_group is None:
2120 if self.parent_group is None:
2121 return groups
2121 return groups
2122 cur_gr = self.parent_group
2122 cur_gr = self.parent_group
2123 groups.insert(0, cur_gr)
2123 groups.insert(0, cur_gr)
2124 cnt = 0
2124 cnt = 0
2125 while 1:
2125 while 1:
2126 cnt += 1
2126 cnt += 1
2127 gr = getattr(cur_gr, 'parent_group', None)
2127 gr = getattr(cur_gr, 'parent_group', None)
2128 cur_gr = cur_gr.parent_group
2128 cur_gr = cur_gr.parent_group
2129 if gr is None:
2129 if gr is None:
2130 break
2130 break
2131 if cnt == parents_recursion_limit:
2131 if cnt == parents_recursion_limit:
2132 # this will prevent accidental infinit loops
2132 # this will prevent accidental infinit loops
2133 log.error(('more than %s parents found for group %s, stopping '
2133 log.error(('more than %s parents found for group %s, stopping '
2134 'recursive parent fetching' % (parents_recursion_limit, self)))
2134 'recursive parent fetching' % (parents_recursion_limit, self)))
2135 break
2135 break
2136
2136
2137 groups.insert(0, gr)
2137 groups.insert(0, gr)
2138 return groups
2138 return groups
2139
2139
2140 @property
2140 @property
2141 def children(self):
2141 def children(self):
2142 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2142 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2143
2143
2144 @property
2144 @property
2145 def name(self):
2145 def name(self):
2146 return self.group_name.split(RepoGroup.url_sep())[-1]
2146 return self.group_name.split(RepoGroup.url_sep())[-1]
2147
2147
2148 @property
2148 @property
2149 def full_path(self):
2149 def full_path(self):
2150 return self.group_name
2150 return self.group_name
2151
2151
2152 @property
2152 @property
2153 def full_path_splitted(self):
2153 def full_path_splitted(self):
2154 return self.group_name.split(RepoGroup.url_sep())
2154 return self.group_name.split(RepoGroup.url_sep())
2155
2155
2156 @property
2156 @property
2157 def repositories(self):
2157 def repositories(self):
2158 return Repository.query()\
2158 return Repository.query()\
2159 .filter(Repository.group == self)\
2159 .filter(Repository.group == self)\
2160 .order_by(Repository.repo_name)
2160 .order_by(Repository.repo_name)
2161
2161
2162 @property
2162 @property
2163 def repositories_recursive_count(self):
2163 def repositories_recursive_count(self):
2164 cnt = self.repositories.count()
2164 cnt = self.repositories.count()
2165
2165
2166 def children_count(group):
2166 def children_count(group):
2167 cnt = 0
2167 cnt = 0
2168 for child in group.children:
2168 for child in group.children:
2169 cnt += child.repositories.count()
2169 cnt += child.repositories.count()
2170 cnt += children_count(child)
2170 cnt += children_count(child)
2171 return cnt
2171 return cnt
2172
2172
2173 return cnt + children_count(self)
2173 return cnt + children_count(self)
2174
2174
2175 def _recursive_objects(self, include_repos=True):
2175 def _recursive_objects(self, include_repos=True):
2176 all_ = []
2176 all_ = []
2177
2177
2178 def _get_members(root_gr):
2178 def _get_members(root_gr):
2179 if include_repos:
2179 if include_repos:
2180 for r in root_gr.repositories:
2180 for r in root_gr.repositories:
2181 all_.append(r)
2181 all_.append(r)
2182 childs = root_gr.children.all()
2182 childs = root_gr.children.all()
2183 if childs:
2183 if childs:
2184 for gr in childs:
2184 for gr in childs:
2185 all_.append(gr)
2185 all_.append(gr)
2186 _get_members(gr)
2186 _get_members(gr)
2187
2187
2188 _get_members(self)
2188 _get_members(self)
2189 return [self] + all_
2189 return [self] + all_
2190
2190
2191 def recursive_groups_and_repos(self):
2191 def recursive_groups_and_repos(self):
2192 """
2192 """
2193 Recursive return all groups, with repositories in those groups
2193 Recursive return all groups, with repositories in those groups
2194 """
2194 """
2195 return self._recursive_objects()
2195 return self._recursive_objects()
2196
2196
2197 def recursive_groups(self):
2197 def recursive_groups(self):
2198 """
2198 """
2199 Returns all children groups for this group including children of children
2199 Returns all children groups for this group including children of children
2200 """
2200 """
2201 return self._recursive_objects(include_repos=False)
2201 return self._recursive_objects(include_repos=False)
2202
2202
2203 def get_new_name(self, group_name):
2203 def get_new_name(self, group_name):
2204 """
2204 """
2205 returns new full group name based on parent and new name
2205 returns new full group name based on parent and new name
2206
2206
2207 :param group_name:
2207 :param group_name:
2208 """
2208 """
2209 path_prefix = (self.parent_group.full_path_splitted if
2209 path_prefix = (self.parent_group.full_path_splitted if
2210 self.parent_group else [])
2210 self.parent_group else [])
2211 return RepoGroup.url_sep().join(path_prefix + [group_name])
2211 return RepoGroup.url_sep().join(path_prefix + [group_name])
2212
2212
2213 def permissions(self, with_admins=True, with_owner=True):
2213 def permissions(self, with_admins=True, with_owner=True):
2214 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2214 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2215 q = q.options(joinedload(UserRepoGroupToPerm.group),
2215 q = q.options(joinedload(UserRepoGroupToPerm.group),
2216 joinedload(UserRepoGroupToPerm.user),
2216 joinedload(UserRepoGroupToPerm.user),
2217 joinedload(UserRepoGroupToPerm.permission),)
2217 joinedload(UserRepoGroupToPerm.permission),)
2218
2218
2219 # get owners and admins and permissions. We do a trick of re-writing
2219 # get owners and admins and permissions. We do a trick of re-writing
2220 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2220 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2221 # has a global reference and changing one object propagates to all
2221 # has a global reference and changing one object propagates to all
2222 # others. This means if admin is also an owner admin_row that change
2222 # others. This means if admin is also an owner admin_row that change
2223 # would propagate to both objects
2223 # would propagate to both objects
2224 perm_rows = []
2224 perm_rows = []
2225 for _usr in q.all():
2225 for _usr in q.all():
2226 usr = AttributeDict(_usr.user.get_dict())
2226 usr = AttributeDict(_usr.user.get_dict())
2227 usr.permission = _usr.permission.permission_name
2227 usr.permission = _usr.permission.permission_name
2228 perm_rows.append(usr)
2228 perm_rows.append(usr)
2229
2229
2230 # filter the perm rows by 'default' first and then sort them by
2230 # filter the perm rows by 'default' first and then sort them by
2231 # admin,write,read,none permissions sorted again alphabetically in
2231 # admin,write,read,none permissions sorted again alphabetically in
2232 # each group
2232 # each group
2233 perm_rows = sorted(perm_rows, key=display_sort)
2233 perm_rows = sorted(perm_rows, key=display_sort)
2234
2234
2235 _admin_perm = 'group.admin'
2235 _admin_perm = 'group.admin'
2236 owner_row = []
2236 owner_row = []
2237 if with_owner:
2237 if with_owner:
2238 usr = AttributeDict(self.user.get_dict())
2238 usr = AttributeDict(self.user.get_dict())
2239 usr.owner_row = True
2239 usr.owner_row = True
2240 usr.permission = _admin_perm
2240 usr.permission = _admin_perm
2241 owner_row.append(usr)
2241 owner_row.append(usr)
2242
2242
2243 super_admin_rows = []
2243 super_admin_rows = []
2244 if with_admins:
2244 if with_admins:
2245 for usr in User.get_all_super_admins():
2245 for usr in User.get_all_super_admins():
2246 # if this admin is also owner, don't double the record
2246 # if this admin is also owner, don't double the record
2247 if usr.user_id == owner_row[0].user_id:
2247 if usr.user_id == owner_row[0].user_id:
2248 owner_row[0].admin_row = True
2248 owner_row[0].admin_row = True
2249 else:
2249 else:
2250 usr = AttributeDict(usr.get_dict())
2250 usr = AttributeDict(usr.get_dict())
2251 usr.admin_row = True
2251 usr.admin_row = True
2252 usr.permission = _admin_perm
2252 usr.permission = _admin_perm
2253 super_admin_rows.append(usr)
2253 super_admin_rows.append(usr)
2254
2254
2255 return super_admin_rows + owner_row + perm_rows
2255 return super_admin_rows + owner_row + perm_rows
2256
2256
2257 def permission_user_groups(self):
2257 def permission_user_groups(self):
2258 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2258 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2259 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2259 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2260 joinedload(UserGroupRepoGroupToPerm.users_group),
2260 joinedload(UserGroupRepoGroupToPerm.users_group),
2261 joinedload(UserGroupRepoGroupToPerm.permission),)
2261 joinedload(UserGroupRepoGroupToPerm.permission),)
2262
2262
2263 perm_rows = []
2263 perm_rows = []
2264 for _user_group in q.all():
2264 for _user_group in q.all():
2265 usr = AttributeDict(_user_group.users_group.get_dict())
2265 usr = AttributeDict(_user_group.users_group.get_dict())
2266 usr.permission = _user_group.permission.permission_name
2266 usr.permission = _user_group.permission.permission_name
2267 perm_rows.append(usr)
2267 perm_rows.append(usr)
2268
2268
2269 return perm_rows
2269 return perm_rows
2270
2270
2271 def get_api_data(self):
2271 def get_api_data(self):
2272 """
2272 """
2273 Common function for generating api data
2273 Common function for generating api data
2274
2274
2275 """
2275 """
2276 group = self
2276 group = self
2277 data = {
2277 data = {
2278 'group_id': group.group_id,
2278 'group_id': group.group_id,
2279 'group_name': group.group_name,
2279 'group_name': group.group_name,
2280 'group_description': group.group_description,
2280 'group_description': group.group_description,
2281 'parent_group': group.parent_group.group_name if group.parent_group else None,
2281 'parent_group': group.parent_group.group_name if group.parent_group else None,
2282 'repositories': [x.repo_name for x in group.repositories],
2282 'repositories': [x.repo_name for x in group.repositories],
2283 'owner': group.user.username,
2283 'owner': group.user.username,
2284 }
2284 }
2285 return data
2285 return data
2286
2286
2287
2287
2288 class Permission(Base, BaseModel):
2288 class Permission(Base, BaseModel):
2289 __tablename__ = 'permissions'
2289 __tablename__ = 'permissions'
2290 __table_args__ = (
2290 __table_args__ = (
2291 Index('p_perm_name_idx', 'permission_name'),
2291 Index('p_perm_name_idx', 'permission_name'),
2292 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2292 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2293 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2293 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2294 )
2294 )
2295 PERMS = [
2295 PERMS = [
2296 ('hg.admin', _('RhodeCode Super Administrator')),
2296 ('hg.admin', _('RhodeCode Super Administrator')),
2297
2297
2298 ('repository.none', _('Repository no access')),
2298 ('repository.none', _('Repository no access')),
2299 ('repository.read', _('Repository read access')),
2299 ('repository.read', _('Repository read access')),
2300 ('repository.write', _('Repository write access')),
2300 ('repository.write', _('Repository write access')),
2301 ('repository.admin', _('Repository admin access')),
2301 ('repository.admin', _('Repository admin access')),
2302
2302
2303 ('group.none', _('Repository group no access')),
2303 ('group.none', _('Repository group no access')),
2304 ('group.read', _('Repository group read access')),
2304 ('group.read', _('Repository group read access')),
2305 ('group.write', _('Repository group write access')),
2305 ('group.write', _('Repository group write access')),
2306 ('group.admin', _('Repository group admin access')),
2306 ('group.admin', _('Repository group admin access')),
2307
2307
2308 ('usergroup.none', _('User group no access')),
2308 ('usergroup.none', _('User group no access')),
2309 ('usergroup.read', _('User group read access')),
2309 ('usergroup.read', _('User group read access')),
2310 ('usergroup.write', _('User group write access')),
2310 ('usergroup.write', _('User group write access')),
2311 ('usergroup.admin', _('User group admin access')),
2311 ('usergroup.admin', _('User group admin access')),
2312
2312
2313 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2313 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2314 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2314 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2315
2315
2316 ('hg.usergroup.create.false', _('User Group creation disabled')),
2316 ('hg.usergroup.create.false', _('User Group creation disabled')),
2317 ('hg.usergroup.create.true', _('User Group creation enabled')),
2317 ('hg.usergroup.create.true', _('User Group creation enabled')),
2318
2318
2319 ('hg.create.none', _('Repository creation disabled')),
2319 ('hg.create.none', _('Repository creation disabled')),
2320 ('hg.create.repository', _('Repository creation enabled')),
2320 ('hg.create.repository', _('Repository creation enabled')),
2321 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2321 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2322 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2322 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2323
2323
2324 ('hg.fork.none', _('Repository forking disabled')),
2324 ('hg.fork.none', _('Repository forking disabled')),
2325 ('hg.fork.repository', _('Repository forking enabled')),
2325 ('hg.fork.repository', _('Repository forking enabled')),
2326
2326
2327 ('hg.register.none', _('Registration disabled')),
2327 ('hg.register.none', _('Registration disabled')),
2328 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2328 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2329 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2329 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2330
2330
2331 ('hg.password_reset.enabled', _('Password reset enabled')),
2331 ('hg.password_reset.enabled', _('Password reset enabled')),
2332 ('hg.password_reset.hidden', _('Password reset hidden')),
2332 ('hg.password_reset.hidden', _('Password reset hidden')),
2333 ('hg.password_reset.disabled', _('Password reset disabled')),
2333 ('hg.password_reset.disabled', _('Password reset disabled')),
2334
2334
2335 ('hg.extern_activate.manual', _('Manual activation of external account')),
2335 ('hg.extern_activate.manual', _('Manual activation of external account')),
2336 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2336 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2337
2337
2338 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2338 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2339 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2339 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2340 ]
2340 ]
2341
2341
2342 # definition of system default permissions for DEFAULT user
2342 # definition of system default permissions for DEFAULT user
2343 DEFAULT_USER_PERMISSIONS = [
2343 DEFAULT_USER_PERMISSIONS = [
2344 'repository.read',
2344 'repository.read',
2345 'group.read',
2345 'group.read',
2346 'usergroup.read',
2346 'usergroup.read',
2347 'hg.create.repository',
2347 'hg.create.repository',
2348 'hg.repogroup.create.false',
2348 'hg.repogroup.create.false',
2349 'hg.usergroup.create.false',
2349 'hg.usergroup.create.false',
2350 'hg.create.write_on_repogroup.true',
2350 'hg.create.write_on_repogroup.true',
2351 'hg.fork.repository',
2351 'hg.fork.repository',
2352 'hg.register.manual_activate',
2352 'hg.register.manual_activate',
2353 'hg.password_reset.enabled',
2353 'hg.password_reset.enabled',
2354 'hg.extern_activate.auto',
2354 'hg.extern_activate.auto',
2355 'hg.inherit_default_perms.true',
2355 'hg.inherit_default_perms.true',
2356 ]
2356 ]
2357
2357
2358 # defines which permissions are more important higher the more important
2358 # defines which permissions are more important higher the more important
2359 # Weight defines which permissions are more important.
2359 # Weight defines which permissions are more important.
2360 # The higher number the more important.
2360 # The higher number the more important.
2361 PERM_WEIGHTS = {
2361 PERM_WEIGHTS = {
2362 'repository.none': 0,
2362 'repository.none': 0,
2363 'repository.read': 1,
2363 'repository.read': 1,
2364 'repository.write': 3,
2364 'repository.write': 3,
2365 'repository.admin': 4,
2365 'repository.admin': 4,
2366
2366
2367 'group.none': 0,
2367 'group.none': 0,
2368 'group.read': 1,
2368 'group.read': 1,
2369 'group.write': 3,
2369 'group.write': 3,
2370 'group.admin': 4,
2370 'group.admin': 4,
2371
2371
2372 'usergroup.none': 0,
2372 'usergroup.none': 0,
2373 'usergroup.read': 1,
2373 'usergroup.read': 1,
2374 'usergroup.write': 3,
2374 'usergroup.write': 3,
2375 'usergroup.admin': 4,
2375 'usergroup.admin': 4,
2376
2376
2377 'hg.repogroup.create.false': 0,
2377 'hg.repogroup.create.false': 0,
2378 'hg.repogroup.create.true': 1,
2378 'hg.repogroup.create.true': 1,
2379
2379
2380 'hg.usergroup.create.false': 0,
2380 'hg.usergroup.create.false': 0,
2381 'hg.usergroup.create.true': 1,
2381 'hg.usergroup.create.true': 1,
2382
2382
2383 'hg.fork.none': 0,
2383 'hg.fork.none': 0,
2384 'hg.fork.repository': 1,
2384 'hg.fork.repository': 1,
2385 'hg.create.none': 0,
2385 'hg.create.none': 0,
2386 'hg.create.repository': 1
2386 'hg.create.repository': 1
2387 }
2387 }
2388
2388
2389 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2389 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2390 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2390 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2391 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2391 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2392
2392
2393 def __unicode__(self):
2393 def __unicode__(self):
2394 return u"<%s('%s:%s')>" % (
2394 return u"<%s('%s:%s')>" % (
2395 self.__class__.__name__, self.permission_id, self.permission_name
2395 self.__class__.__name__, self.permission_id, self.permission_name
2396 )
2396 )
2397
2397
2398 @classmethod
2398 @classmethod
2399 def get_by_key(cls, key):
2399 def get_by_key(cls, key):
2400 return cls.query().filter(cls.permission_name == key).scalar()
2400 return cls.query().filter(cls.permission_name == key).scalar()
2401
2401
2402 @classmethod
2402 @classmethod
2403 def get_default_repo_perms(cls, user_id, repo_id=None):
2403 def get_default_repo_perms(cls, user_id, repo_id=None):
2404 q = Session().query(UserRepoToPerm, Repository, Permission)\
2404 q = Session().query(UserRepoToPerm, Repository, Permission)\
2405 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2405 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2406 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2406 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2407 .filter(UserRepoToPerm.user_id == user_id)
2407 .filter(UserRepoToPerm.user_id == user_id)
2408 if repo_id:
2408 if repo_id:
2409 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2409 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2410 return q.all()
2410 return q.all()
2411
2411
2412 @classmethod
2412 @classmethod
2413 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2413 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2414 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2414 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2415 .join(
2415 .join(
2416 Permission,
2416 Permission,
2417 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2417 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2418 .join(
2418 .join(
2419 Repository,
2419 Repository,
2420 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2420 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2421 .join(
2421 .join(
2422 UserGroup,
2422 UserGroup,
2423 UserGroupRepoToPerm.users_group_id ==
2423 UserGroupRepoToPerm.users_group_id ==
2424 UserGroup.users_group_id)\
2424 UserGroup.users_group_id)\
2425 .join(
2425 .join(
2426 UserGroupMember,
2426 UserGroupMember,
2427 UserGroupRepoToPerm.users_group_id ==
2427 UserGroupRepoToPerm.users_group_id ==
2428 UserGroupMember.users_group_id)\
2428 UserGroupMember.users_group_id)\
2429 .filter(
2429 .filter(
2430 UserGroupMember.user_id == user_id,
2430 UserGroupMember.user_id == user_id,
2431 UserGroup.users_group_active == true())
2431 UserGroup.users_group_active == true())
2432 if repo_id:
2432 if repo_id:
2433 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2433 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2434 return q.all()
2434 return q.all()
2435
2435
2436 @classmethod
2436 @classmethod
2437 def get_default_group_perms(cls, user_id, repo_group_id=None):
2437 def get_default_group_perms(cls, user_id, repo_group_id=None):
2438 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2438 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2439 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2439 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2440 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2440 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2441 .filter(UserRepoGroupToPerm.user_id == user_id)
2441 .filter(UserRepoGroupToPerm.user_id == user_id)
2442 if repo_group_id:
2442 if repo_group_id:
2443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2444 return q.all()
2444 return q.all()
2445
2445
2446 @classmethod
2446 @classmethod
2447 def get_default_group_perms_from_user_group(
2447 def get_default_group_perms_from_user_group(
2448 cls, user_id, repo_group_id=None):
2448 cls, user_id, repo_group_id=None):
2449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2450 .join(
2450 .join(
2451 Permission,
2451 Permission,
2452 UserGroupRepoGroupToPerm.permission_id ==
2452 UserGroupRepoGroupToPerm.permission_id ==
2453 Permission.permission_id)\
2453 Permission.permission_id)\
2454 .join(
2454 .join(
2455 RepoGroup,
2455 RepoGroup,
2456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2457 .join(
2457 .join(
2458 UserGroup,
2458 UserGroup,
2459 UserGroupRepoGroupToPerm.users_group_id ==
2459 UserGroupRepoGroupToPerm.users_group_id ==
2460 UserGroup.users_group_id)\
2460 UserGroup.users_group_id)\
2461 .join(
2461 .join(
2462 UserGroupMember,
2462 UserGroupMember,
2463 UserGroupRepoGroupToPerm.users_group_id ==
2463 UserGroupRepoGroupToPerm.users_group_id ==
2464 UserGroupMember.users_group_id)\
2464 UserGroupMember.users_group_id)\
2465 .filter(
2465 .filter(
2466 UserGroupMember.user_id == user_id,
2466 UserGroupMember.user_id == user_id,
2467 UserGroup.users_group_active == true())
2467 UserGroup.users_group_active == true())
2468 if repo_group_id:
2468 if repo_group_id:
2469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2470 return q.all()
2470 return q.all()
2471
2471
2472 @classmethod
2472 @classmethod
2473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2477 .filter(UserUserGroupToPerm.user_id == user_id)
2477 .filter(UserUserGroupToPerm.user_id == user_id)
2478 if user_group_id:
2478 if user_group_id:
2479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2480 return q.all()
2480 return q.all()
2481
2481
2482 @classmethod
2482 @classmethod
2483 def get_default_user_group_perms_from_user_group(
2483 def get_default_user_group_perms_from_user_group(
2484 cls, user_id, user_group_id=None):
2484 cls, user_id, user_group_id=None):
2485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2487 .join(
2487 .join(
2488 Permission,
2488 Permission,
2489 UserGroupUserGroupToPerm.permission_id ==
2489 UserGroupUserGroupToPerm.permission_id ==
2490 Permission.permission_id)\
2490 Permission.permission_id)\
2491 .join(
2491 .join(
2492 TargetUserGroup,
2492 TargetUserGroup,
2493 UserGroupUserGroupToPerm.target_user_group_id ==
2493 UserGroupUserGroupToPerm.target_user_group_id ==
2494 TargetUserGroup.users_group_id)\
2494 TargetUserGroup.users_group_id)\
2495 .join(
2495 .join(
2496 UserGroup,
2496 UserGroup,
2497 UserGroupUserGroupToPerm.user_group_id ==
2497 UserGroupUserGroupToPerm.user_group_id ==
2498 UserGroup.users_group_id)\
2498 UserGroup.users_group_id)\
2499 .join(
2499 .join(
2500 UserGroupMember,
2500 UserGroupMember,
2501 UserGroupUserGroupToPerm.user_group_id ==
2501 UserGroupUserGroupToPerm.user_group_id ==
2502 UserGroupMember.users_group_id)\
2502 UserGroupMember.users_group_id)\
2503 .filter(
2503 .filter(
2504 UserGroupMember.user_id == user_id,
2504 UserGroupMember.user_id == user_id,
2505 UserGroup.users_group_active == true())
2505 UserGroup.users_group_active == true())
2506 if user_group_id:
2506 if user_group_id:
2507 q = q.filter(
2507 q = q.filter(
2508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2509
2509
2510 return q.all()
2510 return q.all()
2511
2511
2512
2512
2513 class UserRepoToPerm(Base, BaseModel):
2513 class UserRepoToPerm(Base, BaseModel):
2514 __tablename__ = 'repo_to_perm'
2514 __tablename__ = 'repo_to_perm'
2515 __table_args__ = (
2515 __table_args__ = (
2516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2518 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2518 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2519 )
2519 )
2520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2524
2524
2525 user = relationship('User')
2525 user = relationship('User')
2526 repository = relationship('Repository')
2526 repository = relationship('Repository')
2527 permission = relationship('Permission')
2527 permission = relationship('Permission')
2528
2528
2529 @classmethod
2529 @classmethod
2530 def create(cls, user, repository, permission):
2530 def create(cls, user, repository, permission):
2531 n = cls()
2531 n = cls()
2532 n.user = user
2532 n.user = user
2533 n.repository = repository
2533 n.repository = repository
2534 n.permission = permission
2534 n.permission = permission
2535 Session().add(n)
2535 Session().add(n)
2536 return n
2536 return n
2537
2537
2538 def __unicode__(self):
2538 def __unicode__(self):
2539 return u'<%s => %s >' % (self.user, self.repository)
2539 return u'<%s => %s >' % (self.user, self.repository)
2540
2540
2541
2541
2542 class UserUserGroupToPerm(Base, BaseModel):
2542 class UserUserGroupToPerm(Base, BaseModel):
2543 __tablename__ = 'user_user_group_to_perm'
2543 __tablename__ = 'user_user_group_to_perm'
2544 __table_args__ = (
2544 __table_args__ = (
2545 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2545 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2547 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2547 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2548 )
2548 )
2549 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
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 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2550 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2552 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2552 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2553
2553
2554 user = relationship('User')
2554 user = relationship('User')
2555 user_group = relationship('UserGroup')
2555 user_group = relationship('UserGroup')
2556 permission = relationship('Permission')
2556 permission = relationship('Permission')
2557
2557
2558 @classmethod
2558 @classmethod
2559 def create(cls, user, user_group, permission):
2559 def create(cls, user, user_group, permission):
2560 n = cls()
2560 n = cls()
2561 n.user = user
2561 n.user = user
2562 n.user_group = user_group
2562 n.user_group = user_group
2563 n.permission = permission
2563 n.permission = permission
2564 Session().add(n)
2564 Session().add(n)
2565 return n
2565 return n
2566
2566
2567 def __unicode__(self):
2567 def __unicode__(self):
2568 return u'<%s => %s >' % (self.user, self.user_group)
2568 return u'<%s => %s >' % (self.user, self.user_group)
2569
2569
2570
2570
2571 class UserToPerm(Base, BaseModel):
2571 class UserToPerm(Base, BaseModel):
2572 __tablename__ = 'user_to_perm'
2572 __tablename__ = 'user_to_perm'
2573 __table_args__ = (
2573 __table_args__ = (
2574 UniqueConstraint('user_id', 'permission_id'),
2574 UniqueConstraint('user_id', 'permission_id'),
2575 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2575 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2576 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2576 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2577 )
2577 )
2578 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2578 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2579 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2579 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2580 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2580 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2581
2581
2582 user = relationship('User')
2582 user = relationship('User')
2583 permission = relationship('Permission', lazy='joined')
2583 permission = relationship('Permission', lazy='joined')
2584
2584
2585 def __unicode__(self):
2585 def __unicode__(self):
2586 return u'<%s => %s >' % (self.user, self.permission)
2586 return u'<%s => %s >' % (self.user, self.permission)
2587
2587
2588
2588
2589 class UserGroupRepoToPerm(Base, BaseModel):
2589 class UserGroupRepoToPerm(Base, BaseModel):
2590 __tablename__ = 'users_group_repo_to_perm'
2590 __tablename__ = 'users_group_repo_to_perm'
2591 __table_args__ = (
2591 __table_args__ = (
2592 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2592 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2593 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2593 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2594 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2594 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2595 )
2595 )
2596 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2596 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2597 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2597 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2598 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2598 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2599 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2599 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2600
2600
2601 users_group = relationship('UserGroup')
2601 users_group = relationship('UserGroup')
2602 permission = relationship('Permission')
2602 permission = relationship('Permission')
2603 repository = relationship('Repository')
2603 repository = relationship('Repository')
2604
2604
2605 @classmethod
2605 @classmethod
2606 def create(cls, users_group, repository, permission):
2606 def create(cls, users_group, repository, permission):
2607 n = cls()
2607 n = cls()
2608 n.users_group = users_group
2608 n.users_group = users_group
2609 n.repository = repository
2609 n.repository = repository
2610 n.permission = permission
2610 n.permission = permission
2611 Session().add(n)
2611 Session().add(n)
2612 return n
2612 return n
2613
2613
2614 def __unicode__(self):
2614 def __unicode__(self):
2615 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2615 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2616
2616
2617
2617
2618 class UserGroupUserGroupToPerm(Base, BaseModel):
2618 class UserGroupUserGroupToPerm(Base, BaseModel):
2619 __tablename__ = 'user_group_user_group_to_perm'
2619 __tablename__ = 'user_group_user_group_to_perm'
2620 __table_args__ = (
2620 __table_args__ = (
2621 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2621 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2622 CheckConstraint('target_user_group_id != user_group_id'),
2622 CheckConstraint('target_user_group_id != user_group_id'),
2623 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2623 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2624 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2624 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2625 )
2625 )
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)
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 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2627 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2628 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2628 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2629 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2629 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2630
2630
2631 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2631 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2632 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2632 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2633 permission = relationship('Permission')
2633 permission = relationship('Permission')
2634
2634
2635 @classmethod
2635 @classmethod
2636 def create(cls, target_user_group, user_group, permission):
2636 def create(cls, target_user_group, user_group, permission):
2637 n = cls()
2637 n = cls()
2638 n.target_user_group = target_user_group
2638 n.target_user_group = target_user_group
2639 n.user_group = user_group
2639 n.user_group = user_group
2640 n.permission = permission
2640 n.permission = permission
2641 Session().add(n)
2641 Session().add(n)
2642 return n
2642 return n
2643
2643
2644 def __unicode__(self):
2644 def __unicode__(self):
2645 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2645 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2646
2646
2647
2647
2648 class UserGroupToPerm(Base, BaseModel):
2648 class UserGroupToPerm(Base, BaseModel):
2649 __tablename__ = 'users_group_to_perm'
2649 __tablename__ = 'users_group_to_perm'
2650 __table_args__ = (
2650 __table_args__ = (
2651 UniqueConstraint('users_group_id', 'permission_id',),
2651 UniqueConstraint('users_group_id', 'permission_id',),
2652 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2652 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2653 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2653 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2654 )
2654 )
2655 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2655 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2656 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2656 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2657 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2657 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2658
2658
2659 users_group = relationship('UserGroup')
2659 users_group = relationship('UserGroup')
2660 permission = relationship('Permission')
2660 permission = relationship('Permission')
2661
2661
2662
2662
2663 class UserRepoGroupToPerm(Base, BaseModel):
2663 class UserRepoGroupToPerm(Base, BaseModel):
2664 __tablename__ = 'user_repo_group_to_perm'
2664 __tablename__ = 'user_repo_group_to_perm'
2665 __table_args__ = (
2665 __table_args__ = (
2666 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2666 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2667 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2667 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2668 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2668 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2669 )
2669 )
2670
2670
2671 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2671 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2672 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2672 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2673 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2673 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2674 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2674 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2675
2675
2676 user = relationship('User')
2676 user = relationship('User')
2677 group = relationship('RepoGroup')
2677 group = relationship('RepoGroup')
2678 permission = relationship('Permission')
2678 permission = relationship('Permission')
2679
2679
2680 @classmethod
2680 @classmethod
2681 def create(cls, user, repository_group, permission):
2681 def create(cls, user, repository_group, permission):
2682 n = cls()
2682 n = cls()
2683 n.user = user
2683 n.user = user
2684 n.group = repository_group
2684 n.group = repository_group
2685 n.permission = permission
2685 n.permission = permission
2686 Session().add(n)
2686 Session().add(n)
2687 return n
2687 return n
2688
2688
2689
2689
2690 class UserGroupRepoGroupToPerm(Base, BaseModel):
2690 class UserGroupRepoGroupToPerm(Base, BaseModel):
2691 __tablename__ = 'users_group_repo_group_to_perm'
2691 __tablename__ = 'users_group_repo_group_to_perm'
2692 __table_args__ = (
2692 __table_args__ = (
2693 UniqueConstraint('users_group_id', 'group_id'),
2693 UniqueConstraint('users_group_id', 'group_id'),
2694 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2694 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2695 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2695 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2696 )
2696 )
2697
2697
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)
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 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2699 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2700 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2700 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2701 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2701 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2702
2702
2703 users_group = relationship('UserGroup')
2703 users_group = relationship('UserGroup')
2704 permission = relationship('Permission')
2704 permission = relationship('Permission')
2705 group = relationship('RepoGroup')
2705 group = relationship('RepoGroup')
2706
2706
2707 @classmethod
2707 @classmethod
2708 def create(cls, user_group, repository_group, permission):
2708 def create(cls, user_group, repository_group, permission):
2709 n = cls()
2709 n = cls()
2710 n.users_group = user_group
2710 n.users_group = user_group
2711 n.group = repository_group
2711 n.group = repository_group
2712 n.permission = permission
2712 n.permission = permission
2713 Session().add(n)
2713 Session().add(n)
2714 return n
2714 return n
2715
2715
2716 def __unicode__(self):
2716 def __unicode__(self):
2717 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2717 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2718
2718
2719
2719
2720 class Statistics(Base, BaseModel):
2720 class Statistics(Base, BaseModel):
2721 __tablename__ = 'statistics'
2721 __tablename__ = 'statistics'
2722 __table_args__ = (
2722 __table_args__ = (
2723 UniqueConstraint('repository_id'),
2723 UniqueConstraint('repository_id'),
2724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2725 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2725 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2726 )
2726 )
2727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2732 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2732 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2733
2733
2734 repository = relationship('Repository', single_parent=True)
2734 repository = relationship('Repository', single_parent=True)
2735
2735
2736
2736
2737 class UserFollowing(Base, BaseModel):
2737 class UserFollowing(Base, BaseModel):
2738 __tablename__ = 'user_followings'
2738 __tablename__ = 'user_followings'
2739 __table_args__ = (
2739 __table_args__ = (
2740 UniqueConstraint('user_id', 'follows_repository_id'),
2740 UniqueConstraint('user_id', 'follows_repository_id'),
2741 UniqueConstraint('user_id', 'follows_user_id'),
2741 UniqueConstraint('user_id', 'follows_user_id'),
2742 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2742 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2743 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2743 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2744 )
2744 )
2745
2745
2746 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2746 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2747 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2747 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2748 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2748 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2749 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2749 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2750 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2750 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2751
2751
2752 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2752 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2753
2753
2754 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2754 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2755 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2755 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2756
2756
2757 @classmethod
2757 @classmethod
2758 def get_repo_followers(cls, repo_id):
2758 def get_repo_followers(cls, repo_id):
2759 return cls.query().filter(cls.follows_repo_id == repo_id)
2759 return cls.query().filter(cls.follows_repo_id == repo_id)
2760
2760
2761
2761
2762 class CacheKey(Base, BaseModel):
2762 class CacheKey(Base, BaseModel):
2763 __tablename__ = 'cache_invalidation'
2763 __tablename__ = 'cache_invalidation'
2764 __table_args__ = (
2764 __table_args__ = (
2765 UniqueConstraint('cache_key'),
2765 UniqueConstraint('cache_key'),
2766 Index('key_idx', 'cache_key'),
2766 Index('key_idx', 'cache_key'),
2767 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2767 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2768 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2768 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2769 )
2769 )
2770 CACHE_TYPE_ATOM = 'ATOM'
2770 CACHE_TYPE_ATOM = 'ATOM'
2771 CACHE_TYPE_RSS = 'RSS'
2771 CACHE_TYPE_RSS = 'RSS'
2772 CACHE_TYPE_README = 'README'
2772 CACHE_TYPE_README = 'README'
2773
2773
2774 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2774 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2775 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2775 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2776 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2776 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2777 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2777 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2778
2778
2779 def __init__(self, cache_key, cache_args=''):
2779 def __init__(self, cache_key, cache_args=''):
2780 self.cache_key = cache_key
2780 self.cache_key = cache_key
2781 self.cache_args = cache_args
2781 self.cache_args = cache_args
2782 self.cache_active = False
2782 self.cache_active = False
2783
2783
2784 def __unicode__(self):
2784 def __unicode__(self):
2785 return u"<%s('%s:%s[%s]')>" % (
2785 return u"<%s('%s:%s[%s]')>" % (
2786 self.__class__.__name__,
2786 self.__class__.__name__,
2787 self.cache_id, self.cache_key, self.cache_active)
2787 self.cache_id, self.cache_key, self.cache_active)
2788
2788
2789 def _cache_key_partition(self):
2789 def _cache_key_partition(self):
2790 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2790 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2791 return prefix, repo_name, suffix
2791 return prefix, repo_name, suffix
2792
2792
2793 def get_prefix(self):
2793 def get_prefix(self):
2794 """
2794 """
2795 Try to extract prefix from existing cache key. The key could consist
2795 Try to extract prefix from existing cache key. The key could consist
2796 of prefix, repo_name, suffix
2796 of prefix, repo_name, suffix
2797 """
2797 """
2798 # this returns prefix, repo_name, suffix
2798 # this returns prefix, repo_name, suffix
2799 return self._cache_key_partition()[0]
2799 return self._cache_key_partition()[0]
2800
2800
2801 def get_suffix(self):
2801 def get_suffix(self):
2802 """
2802 """
2803 get suffix that might have been used in _get_cache_key to
2803 get suffix that might have been used in _get_cache_key to
2804 generate self.cache_key. Only used for informational purposes
2804 generate self.cache_key. Only used for informational purposes
2805 in repo_edit.mako.
2805 in repo_edit.mako.
2806 """
2806 """
2807 # prefix, repo_name, suffix
2807 # prefix, repo_name, suffix
2808 return self._cache_key_partition()[2]
2808 return self._cache_key_partition()[2]
2809
2809
2810 @classmethod
2810 @classmethod
2811 def delete_all_cache(cls):
2811 def delete_all_cache(cls):
2812 """
2812 """
2813 Delete all cache keys from database.
2813 Delete all cache keys from database.
2814 Should only be run when all instances are down and all entries
2814 Should only be run when all instances are down and all entries
2815 thus stale.
2815 thus stale.
2816 """
2816 """
2817 cls.query().delete()
2817 cls.query().delete()
2818 Session().commit()
2818 Session().commit()
2819
2819
2820 @classmethod
2820 @classmethod
2821 def get_cache_key(cls, repo_name, cache_type):
2821 def get_cache_key(cls, repo_name, cache_type):
2822 """
2822 """
2823
2823
2824 Generate a cache key for this process of RhodeCode instance.
2824 Generate a cache key for this process of RhodeCode instance.
2825 Prefix most likely will be process id or maybe explicitly set
2825 Prefix most likely will be process id or maybe explicitly set
2826 instance_id from .ini file.
2826 instance_id from .ini file.
2827 """
2827 """
2828 import rhodecode
2828 import rhodecode
2829 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2829 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2830
2830
2831 repo_as_unicode = safe_unicode(repo_name)
2831 repo_as_unicode = safe_unicode(repo_name)
2832 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2832 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2833 if cache_type else repo_as_unicode
2833 if cache_type else repo_as_unicode
2834
2834
2835 return u'{}{}'.format(prefix, key)
2835 return u'{}{}'.format(prefix, key)
2836
2836
2837 @classmethod
2837 @classmethod
2838 def set_invalidate(cls, repo_name, delete=False):
2838 def set_invalidate(cls, repo_name, delete=False):
2839 """
2839 """
2840 Mark all caches of a repo as invalid in the database.
2840 Mark all caches of a repo as invalid in the database.
2841 """
2841 """
2842
2842
2843 try:
2843 try:
2844 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2844 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2845 if delete:
2845 if delete:
2846 log.debug('cache objects deleted for repo %s',
2846 log.debug('cache objects deleted for repo %s',
2847 safe_str(repo_name))
2847 safe_str(repo_name))
2848 qry.delete()
2848 qry.delete()
2849 else:
2849 else:
2850 log.debug('cache objects marked as invalid for repo %s',
2850 log.debug('cache objects marked as invalid for repo %s',
2851 safe_str(repo_name))
2851 safe_str(repo_name))
2852 qry.update({"cache_active": False})
2852 qry.update({"cache_active": False})
2853
2853
2854 Session().commit()
2854 Session().commit()
2855 except Exception:
2855 except Exception:
2856 log.exception(
2856 log.exception(
2857 'Cache key invalidation failed for repository %s',
2857 'Cache key invalidation failed for repository %s',
2858 safe_str(repo_name))
2858 safe_str(repo_name))
2859 Session().rollback()
2859 Session().rollback()
2860
2860
2861 @classmethod
2861 @classmethod
2862 def get_active_cache(cls, cache_key):
2862 def get_active_cache(cls, cache_key):
2863 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2863 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2864 if inv_obj:
2864 if inv_obj:
2865 return inv_obj
2865 return inv_obj
2866 return None
2866 return None
2867
2867
2868 @classmethod
2868 @classmethod
2869 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2869 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2870 thread_scoped=False):
2870 thread_scoped=False):
2871 """
2871 """
2872 @cache_region('long_term')
2872 @cache_region('long_term')
2873 def _heavy_calculation(cache_key):
2873 def _heavy_calculation(cache_key):
2874 return 'result'
2874 return 'result'
2875
2875
2876 cache_context = CacheKey.repo_context_cache(
2876 cache_context = CacheKey.repo_context_cache(
2877 _heavy_calculation, repo_name, cache_type)
2877 _heavy_calculation, repo_name, cache_type)
2878
2878
2879 with cache_context as context:
2879 with cache_context as context:
2880 context.invalidate()
2880 context.invalidate()
2881 computed = context.compute()
2881 computed = context.compute()
2882
2882
2883 assert computed == 'result'
2883 assert computed == 'result'
2884 """
2884 """
2885 from rhodecode.lib import caches
2885 from rhodecode.lib import caches
2886 return caches.InvalidationContext(
2886 return caches.InvalidationContext(
2887 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2887 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2888
2888
2889
2889
2890 class ChangesetComment(Base, BaseModel):
2890 class ChangesetComment(Base, BaseModel):
2891 __tablename__ = 'changeset_comments'
2891 __tablename__ = 'changeset_comments'
2892 __table_args__ = (
2892 __table_args__ = (
2893 Index('cc_revision_idx', 'revision'),
2893 Index('cc_revision_idx', 'revision'),
2894 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2894 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2895 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2895 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2896 )
2896 )
2897
2897
2898 COMMENT_OUTDATED = u'comment_outdated'
2898 COMMENT_OUTDATED = u'comment_outdated'
2899 COMMENT_TYPE_NOTE = u'note'
2899 COMMENT_TYPE_NOTE = u'note'
2900 COMMENT_TYPE_TODO = u'todo'
2900 COMMENT_TYPE_TODO = u'todo'
2901 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
2901 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
2902
2902
2903 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2903 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2904 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2904 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2905 revision = Column('revision', String(40), nullable=True)
2905 revision = Column('revision', String(40), nullable=True)
2906 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2906 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2907 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2907 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2908 line_no = Column('line_no', Unicode(10), nullable=True)
2908 line_no = Column('line_no', Unicode(10), nullable=True)
2909 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2909 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2910 f_path = Column('f_path', Unicode(1000), nullable=True)
2910 f_path = Column('f_path', Unicode(1000), nullable=True)
2911 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2911 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2912 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2912 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2913 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2913 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2914 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2914 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2915 renderer = Column('renderer', Unicode(64), nullable=True)
2915 renderer = Column('renderer', Unicode(64), nullable=True)
2916 display_state = Column('display_state', Unicode(128), nullable=True)
2916 display_state = Column('display_state', Unicode(128), nullable=True)
2917
2917
2918 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
2918 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
2919 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
2919 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
2920 resolved_comment = relationship('ChangesetComment', remote_side=comment_id)
2920 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
2921 author = relationship('User', lazy='joined')
2921 author = relationship('User', lazy='joined')
2922 repo = relationship('Repository')
2922 repo = relationship('Repository')
2923 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2923 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2924 pull_request = relationship('PullRequest', lazy='joined')
2924 pull_request = relationship('PullRequest', lazy='joined')
2925 pull_request_version = relationship('PullRequestVersion')
2925 pull_request_version = relationship('PullRequestVersion')
2926
2926
2927 @classmethod
2927 @classmethod
2928 def get_users(cls, revision=None, pull_request_id=None):
2928 def get_users(cls, revision=None, pull_request_id=None):
2929 """
2929 """
2930 Returns user associated with this ChangesetComment. ie those
2930 Returns user associated with this ChangesetComment. ie those
2931 who actually commented
2931 who actually commented
2932
2932
2933 :param cls:
2933 :param cls:
2934 :param revision:
2934 :param revision:
2935 """
2935 """
2936 q = Session().query(User)\
2936 q = Session().query(User)\
2937 .join(ChangesetComment.author)
2937 .join(ChangesetComment.author)
2938 if revision:
2938 if revision:
2939 q = q.filter(cls.revision == revision)
2939 q = q.filter(cls.revision == revision)
2940 elif pull_request_id:
2940 elif pull_request_id:
2941 q = q.filter(cls.pull_request_id == pull_request_id)
2941 q = q.filter(cls.pull_request_id == pull_request_id)
2942 return q.all()
2942 return q.all()
2943
2943
2944 @classmethod
2944 @classmethod
2945 def get_index_from_version(cls, pr_version, versions):
2945 def get_index_from_version(cls, pr_version, versions):
2946 num_versions = [x.pull_request_version_id for x in versions]
2946 num_versions = [x.pull_request_version_id for x in versions]
2947 try:
2947 try:
2948 return num_versions.index(pr_version) +1
2948 return num_versions.index(pr_version) +1
2949 except (IndexError, ValueError):
2949 except (IndexError, ValueError):
2950 return
2950 return
2951
2951
2952 @property
2952 @property
2953 def outdated(self):
2953 def outdated(self):
2954 return self.display_state == self.COMMENT_OUTDATED
2954 return self.display_state == self.COMMENT_OUTDATED
2955
2955
2956 def outdated_at_version(self, version):
2956 def outdated_at_version(self, version):
2957 """
2957 """
2958 Checks if comment is outdated for given pull request version
2958 Checks if comment is outdated for given pull request version
2959 """
2959 """
2960 return self.outdated and self.pull_request_version_id != version
2960 return self.outdated and self.pull_request_version_id != version
2961
2961
2962 @property
2963 def resolved(self):
2964 return self.resolved_by[0] if self.resolved_by else None
2965
2962 def get_index_version(self, versions):
2966 def get_index_version(self, versions):
2963 return self.get_index_from_version(
2967 return self.get_index_from_version(
2964 self.pull_request_version_id, versions)
2968 self.pull_request_version_id, versions)
2965
2969
2966 def render(self, mentions=False):
2970 def render(self, mentions=False):
2967 from rhodecode.lib import helpers as h
2971 from rhodecode.lib import helpers as h
2968 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2972 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2969
2973
2970 def __repr__(self):
2974 def __repr__(self):
2971 if self.comment_id:
2975 if self.comment_id:
2972 return '<DB:ChangesetComment #%s>' % self.comment_id
2976 return '<DB:ChangesetComment #%s>' % self.comment_id
2973 else:
2977 else:
2974 return '<DB:ChangesetComment at %#x>' % id(self)
2978 return '<DB:ChangesetComment at %#x>' % id(self)
2975
2979
2976
2980
2977 class ChangesetStatus(Base, BaseModel):
2981 class ChangesetStatus(Base, BaseModel):
2978 __tablename__ = 'changeset_statuses'
2982 __tablename__ = 'changeset_statuses'
2979 __table_args__ = (
2983 __table_args__ = (
2980 Index('cs_revision_idx', 'revision'),
2984 Index('cs_revision_idx', 'revision'),
2981 Index('cs_version_idx', 'version'),
2985 Index('cs_version_idx', 'version'),
2982 UniqueConstraint('repo_id', 'revision', 'version'),
2986 UniqueConstraint('repo_id', 'revision', 'version'),
2983 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2987 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2984 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2988 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2985 )
2989 )
2986 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2990 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2987 STATUS_APPROVED = 'approved'
2991 STATUS_APPROVED = 'approved'
2988 STATUS_REJECTED = 'rejected'
2992 STATUS_REJECTED = 'rejected'
2989 STATUS_UNDER_REVIEW = 'under_review'
2993 STATUS_UNDER_REVIEW = 'under_review'
2990
2994
2991 STATUSES = [
2995 STATUSES = [
2992 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2996 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2993 (STATUS_APPROVED, _("Approved")),
2997 (STATUS_APPROVED, _("Approved")),
2994 (STATUS_REJECTED, _("Rejected")),
2998 (STATUS_REJECTED, _("Rejected")),
2995 (STATUS_UNDER_REVIEW, _("Under Review")),
2999 (STATUS_UNDER_REVIEW, _("Under Review")),
2996 ]
3000 ]
2997
3001
2998 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3002 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2999 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3003 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3000 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3004 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3001 revision = Column('revision', String(40), nullable=False)
3005 revision = Column('revision', String(40), nullable=False)
3002 status = Column('status', String(128), nullable=False, default=DEFAULT)
3006 status = Column('status', String(128), nullable=False, default=DEFAULT)
3003 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3007 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3004 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3008 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3005 version = Column('version', Integer(), nullable=False, default=0)
3009 version = Column('version', Integer(), nullable=False, default=0)
3006 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3010 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3007
3011
3008 author = relationship('User', lazy='joined')
3012 author = relationship('User', lazy='joined')
3009 repo = relationship('Repository')
3013 repo = relationship('Repository')
3010 comment = relationship('ChangesetComment', lazy='joined')
3014 comment = relationship('ChangesetComment', lazy='joined')
3011 pull_request = relationship('PullRequest', lazy='joined')
3015 pull_request = relationship('PullRequest', lazy='joined')
3012
3016
3013 def __unicode__(self):
3017 def __unicode__(self):
3014 return u"<%s('%s[%s]:%s')>" % (
3018 return u"<%s('%s[%s]:%s')>" % (
3015 self.__class__.__name__,
3019 self.__class__.__name__,
3016 self.status, self.version, self.author
3020 self.status, self.version, self.author
3017 )
3021 )
3018
3022
3019 @classmethod
3023 @classmethod
3020 def get_status_lbl(cls, value):
3024 def get_status_lbl(cls, value):
3021 return dict(cls.STATUSES).get(value)
3025 return dict(cls.STATUSES).get(value)
3022
3026
3023 @property
3027 @property
3024 def status_lbl(self):
3028 def status_lbl(self):
3025 return ChangesetStatus.get_status_lbl(self.status)
3029 return ChangesetStatus.get_status_lbl(self.status)
3026
3030
3027
3031
3028 class _PullRequestBase(BaseModel):
3032 class _PullRequestBase(BaseModel):
3029 """
3033 """
3030 Common attributes of pull request and version entries.
3034 Common attributes of pull request and version entries.
3031 """
3035 """
3032
3036
3033 # .status values
3037 # .status values
3034 STATUS_NEW = u'new'
3038 STATUS_NEW = u'new'
3035 STATUS_OPEN = u'open'
3039 STATUS_OPEN = u'open'
3036 STATUS_CLOSED = u'closed'
3040 STATUS_CLOSED = u'closed'
3037
3041
3038 title = Column('title', Unicode(255), nullable=True)
3042 title = Column('title', Unicode(255), nullable=True)
3039 description = Column(
3043 description = Column(
3040 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3044 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3041 nullable=True)
3045 nullable=True)
3042 # new/open/closed status of pull request (not approve/reject/etc)
3046 # new/open/closed status of pull request (not approve/reject/etc)
3043 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3047 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3044 created_on = Column(
3048 created_on = Column(
3045 'created_on', DateTime(timezone=False), nullable=False,
3049 'created_on', DateTime(timezone=False), nullable=False,
3046 default=datetime.datetime.now)
3050 default=datetime.datetime.now)
3047 updated_on = Column(
3051 updated_on = Column(
3048 'updated_on', DateTime(timezone=False), nullable=False,
3052 'updated_on', DateTime(timezone=False), nullable=False,
3049 default=datetime.datetime.now)
3053 default=datetime.datetime.now)
3050
3054
3051 @declared_attr
3055 @declared_attr
3052 def user_id(cls):
3056 def user_id(cls):
3053 return Column(
3057 return Column(
3054 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3058 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3055 unique=None)
3059 unique=None)
3056
3060
3057 # 500 revisions max
3061 # 500 revisions max
3058 _revisions = Column(
3062 _revisions = Column(
3059 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3063 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3060
3064
3061 @declared_attr
3065 @declared_attr
3062 def source_repo_id(cls):
3066 def source_repo_id(cls):
3063 # TODO: dan: rename column to source_repo_id
3067 # TODO: dan: rename column to source_repo_id
3064 return Column(
3068 return Column(
3065 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3069 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3066 nullable=False)
3070 nullable=False)
3067
3071
3068 source_ref = Column('org_ref', Unicode(255), nullable=False)
3072 source_ref = Column('org_ref', Unicode(255), nullable=False)
3069
3073
3070 @declared_attr
3074 @declared_attr
3071 def target_repo_id(cls):
3075 def target_repo_id(cls):
3072 # TODO: dan: rename column to target_repo_id
3076 # TODO: dan: rename column to target_repo_id
3073 return Column(
3077 return Column(
3074 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3078 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3075 nullable=False)
3079 nullable=False)
3076
3080
3077 target_ref = Column('other_ref', Unicode(255), nullable=False)
3081 target_ref = Column('other_ref', Unicode(255), nullable=False)
3078 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3082 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3079
3083
3080 # TODO: dan: rename column to last_merge_source_rev
3084 # TODO: dan: rename column to last_merge_source_rev
3081 _last_merge_source_rev = Column(
3085 _last_merge_source_rev = Column(
3082 'last_merge_org_rev', String(40), nullable=True)
3086 'last_merge_org_rev', String(40), nullable=True)
3083 # TODO: dan: rename column to last_merge_target_rev
3087 # TODO: dan: rename column to last_merge_target_rev
3084 _last_merge_target_rev = Column(
3088 _last_merge_target_rev = Column(
3085 'last_merge_other_rev', String(40), nullable=True)
3089 'last_merge_other_rev', String(40), nullable=True)
3086 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3090 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3087 merge_rev = Column('merge_rev', String(40), nullable=True)
3091 merge_rev = Column('merge_rev', String(40), nullable=True)
3088
3092
3089 @hybrid_property
3093 @hybrid_property
3090 def revisions(self):
3094 def revisions(self):
3091 return self._revisions.split(':') if self._revisions else []
3095 return self._revisions.split(':') if self._revisions else []
3092
3096
3093 @revisions.setter
3097 @revisions.setter
3094 def revisions(self, val):
3098 def revisions(self, val):
3095 self._revisions = ':'.join(val)
3099 self._revisions = ':'.join(val)
3096
3100
3097 @declared_attr
3101 @declared_attr
3098 def author(cls):
3102 def author(cls):
3099 return relationship('User', lazy='joined')
3103 return relationship('User', lazy='joined')
3100
3104
3101 @declared_attr
3105 @declared_attr
3102 def source_repo(cls):
3106 def source_repo(cls):
3103 return relationship(
3107 return relationship(
3104 'Repository',
3108 'Repository',
3105 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3109 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3106
3110
3107 @property
3111 @property
3108 def source_ref_parts(self):
3112 def source_ref_parts(self):
3109 return self.unicode_to_reference(self.source_ref)
3113 return self.unicode_to_reference(self.source_ref)
3110
3114
3111 @declared_attr
3115 @declared_attr
3112 def target_repo(cls):
3116 def target_repo(cls):
3113 return relationship(
3117 return relationship(
3114 'Repository',
3118 'Repository',
3115 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3119 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3116
3120
3117 @property
3121 @property
3118 def target_ref_parts(self):
3122 def target_ref_parts(self):
3119 return self.unicode_to_reference(self.target_ref)
3123 return self.unicode_to_reference(self.target_ref)
3120
3124
3121 @property
3125 @property
3122 def shadow_merge_ref(self):
3126 def shadow_merge_ref(self):
3123 return self.unicode_to_reference(self._shadow_merge_ref)
3127 return self.unicode_to_reference(self._shadow_merge_ref)
3124
3128
3125 @shadow_merge_ref.setter
3129 @shadow_merge_ref.setter
3126 def shadow_merge_ref(self, ref):
3130 def shadow_merge_ref(self, ref):
3127 self._shadow_merge_ref = self.reference_to_unicode(ref)
3131 self._shadow_merge_ref = self.reference_to_unicode(ref)
3128
3132
3129 def unicode_to_reference(self, raw):
3133 def unicode_to_reference(self, raw):
3130 """
3134 """
3131 Convert a unicode (or string) to a reference object.
3135 Convert a unicode (or string) to a reference object.
3132 If unicode evaluates to False it returns None.
3136 If unicode evaluates to False it returns None.
3133 """
3137 """
3134 if raw:
3138 if raw:
3135 refs = raw.split(':')
3139 refs = raw.split(':')
3136 return Reference(*refs)
3140 return Reference(*refs)
3137 else:
3141 else:
3138 return None
3142 return None
3139
3143
3140 def reference_to_unicode(self, ref):
3144 def reference_to_unicode(self, ref):
3141 """
3145 """
3142 Convert a reference object to unicode.
3146 Convert a reference object to unicode.
3143 If reference is None it returns None.
3147 If reference is None it returns None.
3144 """
3148 """
3145 if ref:
3149 if ref:
3146 return u':'.join(ref)
3150 return u':'.join(ref)
3147 else:
3151 else:
3148 return None
3152 return None
3149
3153
3150 def get_api_data(self):
3154 def get_api_data(self):
3151 from rhodecode.model.pull_request import PullRequestModel
3155 from rhodecode.model.pull_request import PullRequestModel
3152 pull_request = self
3156 pull_request = self
3153 merge_status = PullRequestModel().merge_status(pull_request)
3157 merge_status = PullRequestModel().merge_status(pull_request)
3154
3158
3155 pull_request_url = url(
3159 pull_request_url = url(
3156 'pullrequest_show', repo_name=self.target_repo.repo_name,
3160 'pullrequest_show', repo_name=self.target_repo.repo_name,
3157 pull_request_id=self.pull_request_id, qualified=True)
3161 pull_request_id=self.pull_request_id, qualified=True)
3158
3162
3159 merge_data = {
3163 merge_data = {
3160 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3164 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3161 'reference': (
3165 'reference': (
3162 pull_request.shadow_merge_ref._asdict()
3166 pull_request.shadow_merge_ref._asdict()
3163 if pull_request.shadow_merge_ref else None),
3167 if pull_request.shadow_merge_ref else None),
3164 }
3168 }
3165
3169
3166 data = {
3170 data = {
3167 'pull_request_id': pull_request.pull_request_id,
3171 'pull_request_id': pull_request.pull_request_id,
3168 'url': pull_request_url,
3172 'url': pull_request_url,
3169 'title': pull_request.title,
3173 'title': pull_request.title,
3170 'description': pull_request.description,
3174 'description': pull_request.description,
3171 'status': pull_request.status,
3175 'status': pull_request.status,
3172 'created_on': pull_request.created_on,
3176 'created_on': pull_request.created_on,
3173 'updated_on': pull_request.updated_on,
3177 'updated_on': pull_request.updated_on,
3174 'commit_ids': pull_request.revisions,
3178 'commit_ids': pull_request.revisions,
3175 'review_status': pull_request.calculated_review_status(),
3179 'review_status': pull_request.calculated_review_status(),
3176 'mergeable': {
3180 'mergeable': {
3177 'status': merge_status[0],
3181 'status': merge_status[0],
3178 'message': unicode(merge_status[1]),
3182 'message': unicode(merge_status[1]),
3179 },
3183 },
3180 'source': {
3184 'source': {
3181 'clone_url': pull_request.source_repo.clone_url(),
3185 'clone_url': pull_request.source_repo.clone_url(),
3182 'repository': pull_request.source_repo.repo_name,
3186 'repository': pull_request.source_repo.repo_name,
3183 'reference': {
3187 'reference': {
3184 'name': pull_request.source_ref_parts.name,
3188 'name': pull_request.source_ref_parts.name,
3185 'type': pull_request.source_ref_parts.type,
3189 'type': pull_request.source_ref_parts.type,
3186 'commit_id': pull_request.source_ref_parts.commit_id,
3190 'commit_id': pull_request.source_ref_parts.commit_id,
3187 },
3191 },
3188 },
3192 },
3189 'target': {
3193 'target': {
3190 'clone_url': pull_request.target_repo.clone_url(),
3194 'clone_url': pull_request.target_repo.clone_url(),
3191 'repository': pull_request.target_repo.repo_name,
3195 'repository': pull_request.target_repo.repo_name,
3192 'reference': {
3196 'reference': {
3193 'name': pull_request.target_ref_parts.name,
3197 'name': pull_request.target_ref_parts.name,
3194 'type': pull_request.target_ref_parts.type,
3198 'type': pull_request.target_ref_parts.type,
3195 'commit_id': pull_request.target_ref_parts.commit_id,
3199 'commit_id': pull_request.target_ref_parts.commit_id,
3196 },
3200 },
3197 },
3201 },
3198 'merge': merge_data,
3202 'merge': merge_data,
3199 'author': pull_request.author.get_api_data(include_secrets=False,
3203 'author': pull_request.author.get_api_data(include_secrets=False,
3200 details='basic'),
3204 details='basic'),
3201 'reviewers': [
3205 'reviewers': [
3202 {
3206 {
3203 'user': reviewer.get_api_data(include_secrets=False,
3207 'user': reviewer.get_api_data(include_secrets=False,
3204 details='basic'),
3208 details='basic'),
3205 'reasons': reasons,
3209 'reasons': reasons,
3206 'review_status': st[0][1].status if st else 'not_reviewed',
3210 'review_status': st[0][1].status if st else 'not_reviewed',
3207 }
3211 }
3208 for reviewer, reasons, st in pull_request.reviewers_statuses()
3212 for reviewer, reasons, st in pull_request.reviewers_statuses()
3209 ]
3213 ]
3210 }
3214 }
3211
3215
3212 return data
3216 return data
3213
3217
3214
3218
3215 class PullRequest(Base, _PullRequestBase):
3219 class PullRequest(Base, _PullRequestBase):
3216 __tablename__ = 'pull_requests'
3220 __tablename__ = 'pull_requests'
3217 __table_args__ = (
3221 __table_args__ = (
3218 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3222 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3219 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3223 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3220 )
3224 )
3221
3225
3222 pull_request_id = Column(
3226 pull_request_id = Column(
3223 'pull_request_id', Integer(), nullable=False, primary_key=True)
3227 'pull_request_id', Integer(), nullable=False, primary_key=True)
3224
3228
3225 def __repr__(self):
3229 def __repr__(self):
3226 if self.pull_request_id:
3230 if self.pull_request_id:
3227 return '<DB:PullRequest #%s>' % self.pull_request_id
3231 return '<DB:PullRequest #%s>' % self.pull_request_id
3228 else:
3232 else:
3229 return '<DB:PullRequest at %#x>' % id(self)
3233 return '<DB:PullRequest at %#x>' % id(self)
3230
3234
3231 reviewers = relationship('PullRequestReviewers',
3235 reviewers = relationship('PullRequestReviewers',
3232 cascade="all, delete, delete-orphan")
3236 cascade="all, delete, delete-orphan")
3233 statuses = relationship('ChangesetStatus')
3237 statuses = relationship('ChangesetStatus')
3234 comments = relationship('ChangesetComment',
3238 comments = relationship('ChangesetComment',
3235 cascade="all, delete, delete-orphan")
3239 cascade="all, delete, delete-orphan")
3236 versions = relationship('PullRequestVersion',
3240 versions = relationship('PullRequestVersion',
3237 cascade="all, delete, delete-orphan",
3241 cascade="all, delete, delete-orphan",
3238 lazy='dynamic')
3242 lazy='dynamic')
3239
3243
3240
3244
3241 @classmethod
3245 @classmethod
3242 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3246 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3243 internal_methods=None):
3247 internal_methods=None):
3244
3248
3245 class PullRequestDisplay(object):
3249 class PullRequestDisplay(object):
3246 """
3250 """
3247 Special object wrapper for showing PullRequest data via Versions
3251 Special object wrapper for showing PullRequest data via Versions
3248 It mimics PR object as close as possible. This is read only object
3252 It mimics PR object as close as possible. This is read only object
3249 just for display
3253 just for display
3250 """
3254 """
3251
3255
3252 def __init__(self, attrs, internal=None):
3256 def __init__(self, attrs, internal=None):
3253 self.attrs = attrs
3257 self.attrs = attrs
3254 # internal have priority over the given ones via attrs
3258 # internal have priority over the given ones via attrs
3255 self.internal = internal or ['versions']
3259 self.internal = internal or ['versions']
3256
3260
3257 def __getattr__(self, item):
3261 def __getattr__(self, item):
3258 if item in self.internal:
3262 if item in self.internal:
3259 return getattr(self, item)
3263 return getattr(self, item)
3260 try:
3264 try:
3261 return self.attrs[item]
3265 return self.attrs[item]
3262 except KeyError:
3266 except KeyError:
3263 raise AttributeError(
3267 raise AttributeError(
3264 '%s object has no attribute %s' % (self, item))
3268 '%s object has no attribute %s' % (self, item))
3265
3269
3266 def __repr__(self):
3270 def __repr__(self):
3267 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3271 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3268
3272
3269 def versions(self):
3273 def versions(self):
3270 return pull_request_obj.versions.order_by(
3274 return pull_request_obj.versions.order_by(
3271 PullRequestVersion.pull_request_version_id).all()
3275 PullRequestVersion.pull_request_version_id).all()
3272
3276
3273 def is_closed(self):
3277 def is_closed(self):
3274 return pull_request_obj.is_closed()
3278 return pull_request_obj.is_closed()
3275
3279
3276 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3280 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3277
3281
3278 attrs.author = StrictAttributeDict(
3282 attrs.author = StrictAttributeDict(
3279 pull_request_obj.author.get_api_data())
3283 pull_request_obj.author.get_api_data())
3280 if pull_request_obj.target_repo:
3284 if pull_request_obj.target_repo:
3281 attrs.target_repo = StrictAttributeDict(
3285 attrs.target_repo = StrictAttributeDict(
3282 pull_request_obj.target_repo.get_api_data())
3286 pull_request_obj.target_repo.get_api_data())
3283 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3287 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3284
3288
3285 if pull_request_obj.source_repo:
3289 if pull_request_obj.source_repo:
3286 attrs.source_repo = StrictAttributeDict(
3290 attrs.source_repo = StrictAttributeDict(
3287 pull_request_obj.source_repo.get_api_data())
3291 pull_request_obj.source_repo.get_api_data())
3288 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3292 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3289
3293
3290 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3294 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3291 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3295 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3292 attrs.revisions = pull_request_obj.revisions
3296 attrs.revisions = pull_request_obj.revisions
3293
3297
3294 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3298 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3295
3299
3296 return PullRequestDisplay(attrs, internal=internal_methods)
3300 return PullRequestDisplay(attrs, internal=internal_methods)
3297
3301
3298 def is_closed(self):
3302 def is_closed(self):
3299 return self.status == self.STATUS_CLOSED
3303 return self.status == self.STATUS_CLOSED
3300
3304
3301 def __json__(self):
3305 def __json__(self):
3302 return {
3306 return {
3303 'revisions': self.revisions,
3307 'revisions': self.revisions,
3304 }
3308 }
3305
3309
3306 def calculated_review_status(self):
3310 def calculated_review_status(self):
3307 from rhodecode.model.changeset_status import ChangesetStatusModel
3311 from rhodecode.model.changeset_status import ChangesetStatusModel
3308 return ChangesetStatusModel().calculated_review_status(self)
3312 return ChangesetStatusModel().calculated_review_status(self)
3309
3313
3310 def reviewers_statuses(self):
3314 def reviewers_statuses(self):
3311 from rhodecode.model.changeset_status import ChangesetStatusModel
3315 from rhodecode.model.changeset_status import ChangesetStatusModel
3312 return ChangesetStatusModel().reviewers_statuses(self)
3316 return ChangesetStatusModel().reviewers_statuses(self)
3313
3317
3314
3318
3315 class PullRequestVersion(Base, _PullRequestBase):
3319 class PullRequestVersion(Base, _PullRequestBase):
3316 __tablename__ = 'pull_request_versions'
3320 __tablename__ = 'pull_request_versions'
3317 __table_args__ = (
3321 __table_args__ = (
3318 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3322 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3319 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3323 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3320 )
3324 )
3321
3325
3322 pull_request_version_id = Column(
3326 pull_request_version_id = Column(
3323 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3327 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3324 pull_request_id = Column(
3328 pull_request_id = Column(
3325 'pull_request_id', Integer(),
3329 'pull_request_id', Integer(),
3326 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3330 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3327 pull_request = relationship('PullRequest')
3331 pull_request = relationship('PullRequest')
3328
3332
3329 def __repr__(self):
3333 def __repr__(self):
3330 if self.pull_request_version_id:
3334 if self.pull_request_version_id:
3331 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3335 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3332 else:
3336 else:
3333 return '<DB:PullRequestVersion at %#x>' % id(self)
3337 return '<DB:PullRequestVersion at %#x>' % id(self)
3334
3338
3335 @property
3339 @property
3336 def reviewers(self):
3340 def reviewers(self):
3337 return self.pull_request.reviewers
3341 return self.pull_request.reviewers
3338
3342
3339 @property
3343 @property
3340 def versions(self):
3344 def versions(self):
3341 return self.pull_request.versions
3345 return self.pull_request.versions
3342
3346
3343 def is_closed(self):
3347 def is_closed(self):
3344 # calculate from original
3348 # calculate from original
3345 return self.pull_request.status == self.STATUS_CLOSED
3349 return self.pull_request.status == self.STATUS_CLOSED
3346
3350
3347 def calculated_review_status(self):
3351 def calculated_review_status(self):
3348 return self.pull_request.calculated_review_status()
3352 return self.pull_request.calculated_review_status()
3349
3353
3350 def reviewers_statuses(self):
3354 def reviewers_statuses(self):
3351 return self.pull_request.reviewers_statuses()
3355 return self.pull_request.reviewers_statuses()
3352
3356
3353
3357
3354 class PullRequestReviewers(Base, BaseModel):
3358 class PullRequestReviewers(Base, BaseModel):
3355 __tablename__ = 'pull_request_reviewers'
3359 __tablename__ = 'pull_request_reviewers'
3356 __table_args__ = (
3360 __table_args__ = (
3357 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3361 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3358 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3362 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3359 )
3363 )
3360
3364
3361 def __init__(self, user=None, pull_request=None, reasons=None):
3365 def __init__(self, user=None, pull_request=None, reasons=None):
3362 self.user = user
3366 self.user = user
3363 self.pull_request = pull_request
3367 self.pull_request = pull_request
3364 self.reasons = reasons or []
3368 self.reasons = reasons or []
3365
3369
3366 @hybrid_property
3370 @hybrid_property
3367 def reasons(self):
3371 def reasons(self):
3368 if not self._reasons:
3372 if not self._reasons:
3369 return []
3373 return []
3370 return self._reasons
3374 return self._reasons
3371
3375
3372 @reasons.setter
3376 @reasons.setter
3373 def reasons(self, val):
3377 def reasons(self, val):
3374 val = val or []
3378 val = val or []
3375 if any(not isinstance(x, basestring) for x in val):
3379 if any(not isinstance(x, basestring) for x in val):
3376 raise Exception('invalid reasons type, must be list of strings')
3380 raise Exception('invalid reasons type, must be list of strings')
3377 self._reasons = val
3381 self._reasons = val
3378
3382
3379 pull_requests_reviewers_id = Column(
3383 pull_requests_reviewers_id = Column(
3380 'pull_requests_reviewers_id', Integer(), nullable=False,
3384 'pull_requests_reviewers_id', Integer(), nullable=False,
3381 primary_key=True)
3385 primary_key=True)
3382 pull_request_id = Column(
3386 pull_request_id = Column(
3383 "pull_request_id", Integer(),
3387 "pull_request_id", Integer(),
3384 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3388 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3385 user_id = Column(
3389 user_id = Column(
3386 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3390 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3387 _reasons = Column(
3391 _reasons = Column(
3388 'reason', MutationList.as_mutable(
3392 'reason', MutationList.as_mutable(
3389 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3393 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3390
3394
3391 user = relationship('User')
3395 user = relationship('User')
3392 pull_request = relationship('PullRequest')
3396 pull_request = relationship('PullRequest')
3393
3397
3394
3398
3395 class Notification(Base, BaseModel):
3399 class Notification(Base, BaseModel):
3396 __tablename__ = 'notifications'
3400 __tablename__ = 'notifications'
3397 __table_args__ = (
3401 __table_args__ = (
3398 Index('notification_type_idx', 'type'),
3402 Index('notification_type_idx', 'type'),
3399 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3403 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3400 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3404 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3401 )
3405 )
3402
3406
3403 TYPE_CHANGESET_COMMENT = u'cs_comment'
3407 TYPE_CHANGESET_COMMENT = u'cs_comment'
3404 TYPE_MESSAGE = u'message'
3408 TYPE_MESSAGE = u'message'
3405 TYPE_MENTION = u'mention'
3409 TYPE_MENTION = u'mention'
3406 TYPE_REGISTRATION = u'registration'
3410 TYPE_REGISTRATION = u'registration'
3407 TYPE_PULL_REQUEST = u'pull_request'
3411 TYPE_PULL_REQUEST = u'pull_request'
3408 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3412 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3409
3413
3410 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3414 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3411 subject = Column('subject', Unicode(512), nullable=True)
3415 subject = Column('subject', Unicode(512), nullable=True)
3412 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3416 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3413 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3417 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3414 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3418 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3415 type_ = Column('type', Unicode(255))
3419 type_ = Column('type', Unicode(255))
3416
3420
3417 created_by_user = relationship('User')
3421 created_by_user = relationship('User')
3418 notifications_to_users = relationship('UserNotification', lazy='joined',
3422 notifications_to_users = relationship('UserNotification', lazy='joined',
3419 cascade="all, delete, delete-orphan")
3423 cascade="all, delete, delete-orphan")
3420
3424
3421 @property
3425 @property
3422 def recipients(self):
3426 def recipients(self):
3423 return [x.user for x in UserNotification.query()\
3427 return [x.user for x in UserNotification.query()\
3424 .filter(UserNotification.notification == self)\
3428 .filter(UserNotification.notification == self)\
3425 .order_by(UserNotification.user_id.asc()).all()]
3429 .order_by(UserNotification.user_id.asc()).all()]
3426
3430
3427 @classmethod
3431 @classmethod
3428 def create(cls, created_by, subject, body, recipients, type_=None):
3432 def create(cls, created_by, subject, body, recipients, type_=None):
3429 if type_ is None:
3433 if type_ is None:
3430 type_ = Notification.TYPE_MESSAGE
3434 type_ = Notification.TYPE_MESSAGE
3431
3435
3432 notification = cls()
3436 notification = cls()
3433 notification.created_by_user = created_by
3437 notification.created_by_user = created_by
3434 notification.subject = subject
3438 notification.subject = subject
3435 notification.body = body
3439 notification.body = body
3436 notification.type_ = type_
3440 notification.type_ = type_
3437 notification.created_on = datetime.datetime.now()
3441 notification.created_on = datetime.datetime.now()
3438
3442
3439 for u in recipients:
3443 for u in recipients:
3440 assoc = UserNotification()
3444 assoc = UserNotification()
3441 assoc.notification = notification
3445 assoc.notification = notification
3442
3446
3443 # if created_by is inside recipients mark his notification
3447 # if created_by is inside recipients mark his notification
3444 # as read
3448 # as read
3445 if u.user_id == created_by.user_id:
3449 if u.user_id == created_by.user_id:
3446 assoc.read = True
3450 assoc.read = True
3447
3451
3448 u.notifications.append(assoc)
3452 u.notifications.append(assoc)
3449 Session().add(notification)
3453 Session().add(notification)
3450
3454
3451 return notification
3455 return notification
3452
3456
3453 @property
3457 @property
3454 def description(self):
3458 def description(self):
3455 from rhodecode.model.notification import NotificationModel
3459 from rhodecode.model.notification import NotificationModel
3456 return NotificationModel().make_description(self)
3460 return NotificationModel().make_description(self)
3457
3461
3458
3462
3459 class UserNotification(Base, BaseModel):
3463 class UserNotification(Base, BaseModel):
3460 __tablename__ = 'user_to_notification'
3464 __tablename__ = 'user_to_notification'
3461 __table_args__ = (
3465 __table_args__ = (
3462 UniqueConstraint('user_id', 'notification_id'),
3466 UniqueConstraint('user_id', 'notification_id'),
3463 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3467 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3464 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3468 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3465 )
3469 )
3466 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3470 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3467 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3471 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3468 read = Column('read', Boolean, default=False)
3472 read = Column('read', Boolean, default=False)
3469 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3473 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3470
3474
3471 user = relationship('User', lazy="joined")
3475 user = relationship('User', lazy="joined")
3472 notification = relationship('Notification', lazy="joined",
3476 notification = relationship('Notification', lazy="joined",
3473 order_by=lambda: Notification.created_on.desc(),)
3477 order_by=lambda: Notification.created_on.desc(),)
3474
3478
3475 def mark_as_read(self):
3479 def mark_as_read(self):
3476 self.read = True
3480 self.read = True
3477 Session().add(self)
3481 Session().add(self)
3478
3482
3479
3483
3480 class Gist(Base, BaseModel):
3484 class Gist(Base, BaseModel):
3481 __tablename__ = 'gists'
3485 __tablename__ = 'gists'
3482 __table_args__ = (
3486 __table_args__ = (
3483 Index('g_gist_access_id_idx', 'gist_access_id'),
3487 Index('g_gist_access_id_idx', 'gist_access_id'),
3484 Index('g_created_on_idx', 'created_on'),
3488 Index('g_created_on_idx', 'created_on'),
3485 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3489 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3486 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3490 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3487 )
3491 )
3488 GIST_PUBLIC = u'public'
3492 GIST_PUBLIC = u'public'
3489 GIST_PRIVATE = u'private'
3493 GIST_PRIVATE = u'private'
3490 DEFAULT_FILENAME = u'gistfile1.txt'
3494 DEFAULT_FILENAME = u'gistfile1.txt'
3491
3495
3492 ACL_LEVEL_PUBLIC = u'acl_public'
3496 ACL_LEVEL_PUBLIC = u'acl_public'
3493 ACL_LEVEL_PRIVATE = u'acl_private'
3497 ACL_LEVEL_PRIVATE = u'acl_private'
3494
3498
3495 gist_id = Column('gist_id', Integer(), primary_key=True)
3499 gist_id = Column('gist_id', Integer(), primary_key=True)
3496 gist_access_id = Column('gist_access_id', Unicode(250))
3500 gist_access_id = Column('gist_access_id', Unicode(250))
3497 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3501 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3498 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3502 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3499 gist_expires = Column('gist_expires', Float(53), nullable=False)
3503 gist_expires = Column('gist_expires', Float(53), nullable=False)
3500 gist_type = Column('gist_type', Unicode(128), nullable=False)
3504 gist_type = Column('gist_type', Unicode(128), nullable=False)
3501 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3505 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3502 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3506 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3503 acl_level = Column('acl_level', Unicode(128), nullable=True)
3507 acl_level = Column('acl_level', Unicode(128), nullable=True)
3504
3508
3505 owner = relationship('User')
3509 owner = relationship('User')
3506
3510
3507 def __repr__(self):
3511 def __repr__(self):
3508 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3512 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3509
3513
3510 @classmethod
3514 @classmethod
3511 def get_or_404(cls, id_):
3515 def get_or_404(cls, id_):
3512 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3516 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3513 if not res:
3517 if not res:
3514 raise HTTPNotFound
3518 raise HTTPNotFound
3515 return res
3519 return res
3516
3520
3517 @classmethod
3521 @classmethod
3518 def get_by_access_id(cls, gist_access_id):
3522 def get_by_access_id(cls, gist_access_id):
3519 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3523 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3520
3524
3521 def gist_url(self):
3525 def gist_url(self):
3522 import rhodecode
3526 import rhodecode
3523 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3527 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3524 if alias_url:
3528 if alias_url:
3525 return alias_url.replace('{gistid}', self.gist_access_id)
3529 return alias_url.replace('{gistid}', self.gist_access_id)
3526
3530
3527 return url('gist', gist_id=self.gist_access_id, qualified=True)
3531 return url('gist', gist_id=self.gist_access_id, qualified=True)
3528
3532
3529 @classmethod
3533 @classmethod
3530 def base_path(cls):
3534 def base_path(cls):
3531 """
3535 """
3532 Returns base path when all gists are stored
3536 Returns base path when all gists are stored
3533
3537
3534 :param cls:
3538 :param cls:
3535 """
3539 """
3536 from rhodecode.model.gist import GIST_STORE_LOC
3540 from rhodecode.model.gist import GIST_STORE_LOC
3537 q = Session().query(RhodeCodeUi)\
3541 q = Session().query(RhodeCodeUi)\
3538 .filter(RhodeCodeUi.ui_key == URL_SEP)
3542 .filter(RhodeCodeUi.ui_key == URL_SEP)
3539 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3543 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3540 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3544 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3541
3545
3542 def get_api_data(self):
3546 def get_api_data(self):
3543 """
3547 """
3544 Common function for generating gist related data for API
3548 Common function for generating gist related data for API
3545 """
3549 """
3546 gist = self
3550 gist = self
3547 data = {
3551 data = {
3548 'gist_id': gist.gist_id,
3552 'gist_id': gist.gist_id,
3549 'type': gist.gist_type,
3553 'type': gist.gist_type,
3550 'access_id': gist.gist_access_id,
3554 'access_id': gist.gist_access_id,
3551 'description': gist.gist_description,
3555 'description': gist.gist_description,
3552 'url': gist.gist_url(),
3556 'url': gist.gist_url(),
3553 'expires': gist.gist_expires,
3557 'expires': gist.gist_expires,
3554 'created_on': gist.created_on,
3558 'created_on': gist.created_on,
3555 'modified_at': gist.modified_at,
3559 'modified_at': gist.modified_at,
3556 'content': None,
3560 'content': None,
3557 'acl_level': gist.acl_level,
3561 'acl_level': gist.acl_level,
3558 }
3562 }
3559 return data
3563 return data
3560
3564
3561 def __json__(self):
3565 def __json__(self):
3562 data = dict(
3566 data = dict(
3563 )
3567 )
3564 data.update(self.get_api_data())
3568 data.update(self.get_api_data())
3565 return data
3569 return data
3566 # SCM functions
3570 # SCM functions
3567
3571
3568 def scm_instance(self, **kwargs):
3572 def scm_instance(self, **kwargs):
3569 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3573 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3570 return get_vcs_instance(
3574 return get_vcs_instance(
3571 repo_path=safe_str(full_repo_path), create=False)
3575 repo_path=safe_str(full_repo_path), create=False)
3572
3576
3573
3577
3574 class ExternalIdentity(Base, BaseModel):
3578 class ExternalIdentity(Base, BaseModel):
3575 __tablename__ = 'external_identities'
3579 __tablename__ = 'external_identities'
3576 __table_args__ = (
3580 __table_args__ = (
3577 Index('local_user_id_idx', 'local_user_id'),
3581 Index('local_user_id_idx', 'local_user_id'),
3578 Index('external_id_idx', 'external_id'),
3582 Index('external_id_idx', 'external_id'),
3579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3583 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3580 'mysql_charset': 'utf8'})
3584 'mysql_charset': 'utf8'})
3581
3585
3582 external_id = Column('external_id', Unicode(255), default=u'',
3586 external_id = Column('external_id', Unicode(255), default=u'',
3583 primary_key=True)
3587 primary_key=True)
3584 external_username = Column('external_username', Unicode(1024), default=u'')
3588 external_username = Column('external_username', Unicode(1024), default=u'')
3585 local_user_id = Column('local_user_id', Integer(),
3589 local_user_id = Column('local_user_id', Integer(),
3586 ForeignKey('users.user_id'), primary_key=True)
3590 ForeignKey('users.user_id'), primary_key=True)
3587 provider_name = Column('provider_name', Unicode(255), default=u'',
3591 provider_name = Column('provider_name', Unicode(255), default=u'',
3588 primary_key=True)
3592 primary_key=True)
3589 access_token = Column('access_token', String(1024), default=u'')
3593 access_token = Column('access_token', String(1024), default=u'')
3590 alt_token = Column('alt_token', String(1024), default=u'')
3594 alt_token = Column('alt_token', String(1024), default=u'')
3591 token_secret = Column('token_secret', String(1024), default=u'')
3595 token_secret = Column('token_secret', String(1024), default=u'')
3592
3596
3593 @classmethod
3597 @classmethod
3594 def by_external_id_and_provider(cls, external_id, provider_name,
3598 def by_external_id_and_provider(cls, external_id, provider_name,
3595 local_user_id=None):
3599 local_user_id=None):
3596 """
3600 """
3597 Returns ExternalIdentity instance based on search params
3601 Returns ExternalIdentity instance based on search params
3598
3602
3599 :param external_id:
3603 :param external_id:
3600 :param provider_name:
3604 :param provider_name:
3601 :return: ExternalIdentity
3605 :return: ExternalIdentity
3602 """
3606 """
3603 query = cls.query()
3607 query = cls.query()
3604 query = query.filter(cls.external_id == external_id)
3608 query = query.filter(cls.external_id == external_id)
3605 query = query.filter(cls.provider_name == provider_name)
3609 query = query.filter(cls.provider_name == provider_name)
3606 if local_user_id:
3610 if local_user_id:
3607 query = query.filter(cls.local_user_id == local_user_id)
3611 query = query.filter(cls.local_user_id == local_user_id)
3608 return query.first()
3612 return query.first()
3609
3613
3610 @classmethod
3614 @classmethod
3611 def user_by_external_id_and_provider(cls, external_id, provider_name):
3615 def user_by_external_id_and_provider(cls, external_id, provider_name):
3612 """
3616 """
3613 Returns User instance based on search params
3617 Returns User instance based on search params
3614
3618
3615 :param external_id:
3619 :param external_id:
3616 :param provider_name:
3620 :param provider_name:
3617 :return: User
3621 :return: User
3618 """
3622 """
3619 query = User.query()
3623 query = User.query()
3620 query = query.filter(cls.external_id == external_id)
3624 query = query.filter(cls.external_id == external_id)
3621 query = query.filter(cls.provider_name == provider_name)
3625 query = query.filter(cls.provider_name == provider_name)
3622 query = query.filter(User.user_id == cls.local_user_id)
3626 query = query.filter(User.user_id == cls.local_user_id)
3623 return query.first()
3627 return query.first()
3624
3628
3625 @classmethod
3629 @classmethod
3626 def by_local_user_id(cls, local_user_id):
3630 def by_local_user_id(cls, local_user_id):
3627 """
3631 """
3628 Returns all tokens for user
3632 Returns all tokens for user
3629
3633
3630 :param local_user_id:
3634 :param local_user_id:
3631 :return: ExternalIdentity
3635 :return: ExternalIdentity
3632 """
3636 """
3633 query = cls.query()
3637 query = cls.query()
3634 query = query.filter(cls.local_user_id == local_user_id)
3638 query = query.filter(cls.local_user_id == local_user_id)
3635 return query
3639 return query
3636
3640
3637
3641
3638 class Integration(Base, BaseModel):
3642 class Integration(Base, BaseModel):
3639 __tablename__ = 'integrations'
3643 __tablename__ = 'integrations'
3640 __table_args__ = (
3644 __table_args__ = (
3641 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3645 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3642 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3646 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3643 )
3647 )
3644
3648
3645 integration_id = Column('integration_id', Integer(), primary_key=True)
3649 integration_id = Column('integration_id', Integer(), primary_key=True)
3646 integration_type = Column('integration_type', String(255))
3650 integration_type = Column('integration_type', String(255))
3647 enabled = Column('enabled', Boolean(), nullable=False)
3651 enabled = Column('enabled', Boolean(), nullable=False)
3648 name = Column('name', String(255), nullable=False)
3652 name = Column('name', String(255), nullable=False)
3649 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3653 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3650 default=False)
3654 default=False)
3651
3655
3652 settings = Column(
3656 settings = Column(
3653 'settings_json', MutationObj.as_mutable(
3657 'settings_json', MutationObj.as_mutable(
3654 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3658 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3655 repo_id = Column(
3659 repo_id = Column(
3656 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3660 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3657 nullable=True, unique=None, default=None)
3661 nullable=True, unique=None, default=None)
3658 repo = relationship('Repository', lazy='joined')
3662 repo = relationship('Repository', lazy='joined')
3659
3663
3660 repo_group_id = Column(
3664 repo_group_id = Column(
3661 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3665 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3662 nullable=True, unique=None, default=None)
3666 nullable=True, unique=None, default=None)
3663 repo_group = relationship('RepoGroup', lazy='joined')
3667 repo_group = relationship('RepoGroup', lazy='joined')
3664
3668
3665 @property
3669 @property
3666 def scope(self):
3670 def scope(self):
3667 if self.repo:
3671 if self.repo:
3668 return repr(self.repo)
3672 return repr(self.repo)
3669 if self.repo_group:
3673 if self.repo_group:
3670 if self.child_repos_only:
3674 if self.child_repos_only:
3671 return repr(self.repo_group) + ' (child repos only)'
3675 return repr(self.repo_group) + ' (child repos only)'
3672 else:
3676 else:
3673 return repr(self.repo_group) + ' (recursive)'
3677 return repr(self.repo_group) + ' (recursive)'
3674 if self.child_repos_only:
3678 if self.child_repos_only:
3675 return 'root_repos'
3679 return 'root_repos'
3676 return 'global'
3680 return 'global'
3677
3681
3678 def __repr__(self):
3682 def __repr__(self):
3679 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3683 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3680
3684
3681
3685
3682 class RepoReviewRuleUser(Base, BaseModel):
3686 class RepoReviewRuleUser(Base, BaseModel):
3683 __tablename__ = 'repo_review_rules_users'
3687 __tablename__ = 'repo_review_rules_users'
3684 __table_args__ = (
3688 __table_args__ = (
3685 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3689 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3686 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3690 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3687 )
3691 )
3688 repo_review_rule_user_id = Column(
3692 repo_review_rule_user_id = Column(
3689 'repo_review_rule_user_id', Integer(), primary_key=True)
3693 'repo_review_rule_user_id', Integer(), primary_key=True)
3690 repo_review_rule_id = Column("repo_review_rule_id",
3694 repo_review_rule_id = Column("repo_review_rule_id",
3691 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3695 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3692 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3696 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3693 nullable=False)
3697 nullable=False)
3694 user = relationship('User')
3698 user = relationship('User')
3695
3699
3696
3700
3697 class RepoReviewRuleUserGroup(Base, BaseModel):
3701 class RepoReviewRuleUserGroup(Base, BaseModel):
3698 __tablename__ = 'repo_review_rules_users_groups'
3702 __tablename__ = 'repo_review_rules_users_groups'
3699 __table_args__ = (
3703 __table_args__ = (
3700 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3704 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3701 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3705 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3702 )
3706 )
3703 repo_review_rule_users_group_id = Column(
3707 repo_review_rule_users_group_id = Column(
3704 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3708 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3705 repo_review_rule_id = Column("repo_review_rule_id",
3709 repo_review_rule_id = Column("repo_review_rule_id",
3706 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3710 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3707 users_group_id = Column("users_group_id", Integer(),
3711 users_group_id = Column("users_group_id", Integer(),
3708 ForeignKey('users_groups.users_group_id'), nullable=False)
3712 ForeignKey('users_groups.users_group_id'), nullable=False)
3709 users_group = relationship('UserGroup')
3713 users_group = relationship('UserGroup')
3710
3714
3711
3715
3712 class RepoReviewRule(Base, BaseModel):
3716 class RepoReviewRule(Base, BaseModel):
3713 __tablename__ = 'repo_review_rules'
3717 __tablename__ = 'repo_review_rules'
3714 __table_args__ = (
3718 __table_args__ = (
3715 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3719 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3716 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3720 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3717 )
3721 )
3718
3722
3719 repo_review_rule_id = Column(
3723 repo_review_rule_id = Column(
3720 'repo_review_rule_id', Integer(), primary_key=True)
3724 'repo_review_rule_id', Integer(), primary_key=True)
3721 repo_id = Column(
3725 repo_id = Column(
3722 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3726 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3723 repo = relationship('Repository', backref='review_rules')
3727 repo = relationship('Repository', backref='review_rules')
3724
3728
3725 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3729 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3726 default=u'*') # glob
3730 default=u'*') # glob
3727 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3731 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3728 default=u'*') # glob
3732 default=u'*') # glob
3729
3733
3730 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3734 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3731 nullable=False, default=False)
3735 nullable=False, default=False)
3732 rule_users = relationship('RepoReviewRuleUser')
3736 rule_users = relationship('RepoReviewRuleUser')
3733 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3737 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3734
3738
3735 @hybrid_property
3739 @hybrid_property
3736 def branch_pattern(self):
3740 def branch_pattern(self):
3737 return self._branch_pattern or '*'
3741 return self._branch_pattern or '*'
3738
3742
3739 def _validate_glob(self, value):
3743 def _validate_glob(self, value):
3740 re.compile('^' + glob2re(value) + '$')
3744 re.compile('^' + glob2re(value) + '$')
3741
3745
3742 @branch_pattern.setter
3746 @branch_pattern.setter
3743 def branch_pattern(self, value):
3747 def branch_pattern(self, value):
3744 self._validate_glob(value)
3748 self._validate_glob(value)
3745 self._branch_pattern = value or '*'
3749 self._branch_pattern = value or '*'
3746
3750
3747 @hybrid_property
3751 @hybrid_property
3748 def file_pattern(self):
3752 def file_pattern(self):
3749 return self._file_pattern or '*'
3753 return self._file_pattern or '*'
3750
3754
3751 @file_pattern.setter
3755 @file_pattern.setter
3752 def file_pattern(self, value):
3756 def file_pattern(self, value):
3753 self._validate_glob(value)
3757 self._validate_glob(value)
3754 self._file_pattern = value or '*'
3758 self._file_pattern = value or '*'
3755
3759
3756 def matches(self, branch, files_changed):
3760 def matches(self, branch, files_changed):
3757 """
3761 """
3758 Check if this review rule matches a branch/files in a pull request
3762 Check if this review rule matches a branch/files in a pull request
3759
3763
3760 :param branch: branch name for the commit
3764 :param branch: branch name for the commit
3761 :param files_changed: list of file paths changed in the pull request
3765 :param files_changed: list of file paths changed in the pull request
3762 """
3766 """
3763
3767
3764 branch = branch or ''
3768 branch = branch or ''
3765 files_changed = files_changed or []
3769 files_changed = files_changed or []
3766
3770
3767 branch_matches = True
3771 branch_matches = True
3768 if branch:
3772 if branch:
3769 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3773 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3770 branch_matches = bool(branch_regex.search(branch))
3774 branch_matches = bool(branch_regex.search(branch))
3771
3775
3772 files_matches = True
3776 files_matches = True
3773 if self.file_pattern != '*':
3777 if self.file_pattern != '*':
3774 files_matches = False
3778 files_matches = False
3775 file_regex = re.compile(glob2re(self.file_pattern))
3779 file_regex = re.compile(glob2re(self.file_pattern))
3776 for filename in files_changed:
3780 for filename in files_changed:
3777 if file_regex.search(filename):
3781 if file_regex.search(filename):
3778 files_matches = True
3782 files_matches = True
3779 break
3783 break
3780
3784
3781 return branch_matches and files_matches
3785 return branch_matches and files_matches
3782
3786
3783 @property
3787 @property
3784 def review_users(self):
3788 def review_users(self):
3785 """ Returns the users which this rule applies to """
3789 """ Returns the users which this rule applies to """
3786
3790
3787 users = set()
3791 users = set()
3788 users |= set([
3792 users |= set([
3789 rule_user.user for rule_user in self.rule_users
3793 rule_user.user for rule_user in self.rule_users
3790 if rule_user.user.active])
3794 if rule_user.user.active])
3791 users |= set(
3795 users |= set(
3792 member.user
3796 member.user
3793 for rule_user_group in self.rule_user_groups
3797 for rule_user_group in self.rule_user_groups
3794 for member in rule_user_group.users_group.members
3798 for member in rule_user_group.users_group.members
3795 if member.user.active
3799 if member.user.active
3796 )
3800 )
3797 return users
3801 return users
3798
3802
3799 def __repr__(self):
3803 def __repr__(self):
3800 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3804 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3801 self.repo_review_rule_id, self.repo)
3805 self.repo_review_rule_id, self.repo)
3802
3806
3803
3807
3804 class DbMigrateVersion(Base, BaseModel):
3808 class DbMigrateVersion(Base, BaseModel):
3805 __tablename__ = 'db_migrate_version'
3809 __tablename__ = 'db_migrate_version'
3806 __table_args__ = (
3810 __table_args__ = (
3807 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3811 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3808 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3812 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3809 )
3813 )
3810 repository_id = Column('repository_id', String(250), primary_key=True)
3814 repository_id = Column('repository_id', String(250), primary_key=True)
3811 repository_path = Column('repository_path', Text)
3815 repository_path = Column('repository_path', Text)
3812 version = Column('version', Integer)
3816 version = Column('version', Integer)
3813
3817
3814
3818
3815 class DbSession(Base, BaseModel):
3819 class DbSession(Base, BaseModel):
3816 __tablename__ = 'db_session'
3820 __tablename__ = 'db_session'
3817 __table_args__ = (
3821 __table_args__ = (
3818 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3822 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3819 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3823 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3820 )
3824 )
3821
3825
3822 def __repr__(self):
3826 def __repr__(self):
3823 return '<DB:DbSession({})>'.format(self.id)
3827 return '<DB:DbSession({})>'.format(self.id)
3824
3828
3825 id = Column('id', Integer())
3829 id = Column('id', Integer())
3826 namespace = Column('namespace', String(255), primary_key=True)
3830 namespace = Column('namespace', String(255), primary_key=True)
3827 accessed = Column('accessed', DateTime, nullable=False)
3831 accessed = Column('accessed', DateTime, nullable=False)
3828 created = Column('created', DateTime, nullable=False)
3832 created = Column('created', DateTime, nullable=False)
3829 data = Column('data', PickleType, nullable=False)
3833 data = Column('data', PickleType, nullable=False)
@@ -1,70 +1,74 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2017-2017 RhodeCode GmbH
3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22
22
23 import colander
23 import colander
24
24
25 from rhodecode.translation import _
25 from rhodecode.translation import _
26 from rhodecode.model.validation_schema import preparers
26 from rhodecode.model.validation_schema import preparers
27 from rhodecode.model.validation_schema import types
27 from rhodecode.model.validation_schema import types
28
28
29
29
30 @colander.deferred
30 @colander.deferred
31 def deferred_lifetime_validator(node, kw):
31 def deferred_lifetime_validator(node, kw):
32 options = kw.get('lifetime_options', [])
32 options = kw.get('lifetime_options', [])
33 return colander.All(
33 return colander.All(
34 colander.Range(min=-1, max=60 * 24 * 30 * 12),
34 colander.Range(min=-1, max=60 * 24 * 30 * 12),
35 colander.OneOf([x for x in options]))
35 colander.OneOf([x for x in options]))
36
36
37
37
38 def unique_gist_validator(node, value):
38 def unique_gist_validator(node, value):
39 from rhodecode.model.db import Gist
39 from rhodecode.model.db import Gist
40 existing = Gist.get_by_access_id(value)
40 existing = Gist.get_by_access_id(value)
41 if existing:
41 if existing:
42 msg = _(u'Gist with name {} already exists').format(value)
42 msg = _(u'Gist with name {} already exists').format(value)
43 raise colander.Invalid(node, msg)
43 raise colander.Invalid(node, msg)
44
44
45
45
46 def filename_validator(node, value):
46 def filename_validator(node, value):
47 if value != os.path.basename(value):
47 if value != os.path.basename(value):
48 msg = _(u'Filename {} cannot be inside a directory').format(value)
48 msg = _(u'Filename {} cannot be inside a directory').format(value)
49 raise colander.Invalid(node, msg)
49 raise colander.Invalid(node, msg)
50
50
51
51
52 comment_types = ['note', 'todo']
52 comment_types = ['note', 'todo']
53
53
54
54
55 class CommentSchema(colander.MappingSchema):
55 class CommentSchema(colander.MappingSchema):
56 from rhodecode.model.db import ChangesetComment
56 from rhodecode.model.db import ChangesetComment, ChangesetStatus
57
57
58 comment_body = colander.SchemaNode(colander.String())
58 comment_body = colander.SchemaNode(colander.String())
59 comment_type = colander.SchemaNode(
59 comment_type = colander.SchemaNode(
60 colander.String(),
60 colander.String(),
61 validator=colander.OneOf(ChangesetComment.COMMENT_TYPES))
61 validator=colander.OneOf(ChangesetComment.COMMENT_TYPES),
62 missing=ChangesetComment.COMMENT_TYPE_NOTE)
62
63
63 comment_file = colander.SchemaNode(colander.String(), missing=None)
64 comment_file = colander.SchemaNode(colander.String(), missing=None)
64 comment_line = colander.SchemaNode(colander.String(), missing=None)
65 comment_line = colander.SchemaNode(colander.String(), missing=None)
65 status_change = colander.SchemaNode(colander.String(), missing=None)
66 status_change = colander.SchemaNode(
67 colander.String(), missing=None,
68 validator=colander.OneOf([x[0] for x in ChangesetStatus.STATUSES]))
66 renderer_type = colander.SchemaNode(colander.String())
69 renderer_type = colander.SchemaNode(colander.String())
67
70
68 # do those ?
71 resolves_comment_id = colander.SchemaNode(colander.Integer(), missing=None)
72
69 user = colander.SchemaNode(types.StrOrIntType())
73 user = colander.SchemaNode(types.StrOrIntType())
70 repo = colander.SchemaNode(types.StrOrIntType())
74 repo = colander.SchemaNode(types.StrOrIntType())
@@ -1,527 +1,528 b''
1 // comments.less
1 // comments.less
2 // For use in RhodeCode applications;
2 // For use in RhodeCode applications;
3 // see style guide documentation for guidelines.
3 // see style guide documentation for guidelines.
4
4
5
5
6 // Comments
6 // Comments
7 .comments {
7 .comments {
8 width: 100%;
8 width: 100%;
9 }
9 }
10
10
11 tr.inline-comments div {
11 tr.inline-comments div {
12 max-width: 100%;
12 max-width: 100%;
13
13
14 p {
14 p {
15 white-space: normal;
15 white-space: normal;
16 }
16 }
17
17
18 code, pre, .code, dd {
18 code, pre, .code, dd {
19 overflow-x: auto;
19 overflow-x: auto;
20 width: 1062px;
20 width: 1062px;
21 }
21 }
22
22
23 dd {
23 dd {
24 width: auto;
24 width: auto;
25 }
25 }
26 }
26 }
27
27
28 #injected_page_comments {
28 #injected_page_comments {
29 .comment-previous-link,
29 .comment-previous-link,
30 .comment-next-link,
30 .comment-next-link,
31 .comment-links-divider {
31 .comment-links-divider {
32 display: none;
32 display: none;
33 }
33 }
34 }
34 }
35
35
36 .add-comment {
36 .add-comment {
37 margin-bottom: 10px;
37 margin-bottom: 10px;
38 }
38 }
39 .hide-comment-button .add-comment {
39 .hide-comment-button .add-comment {
40 display: none;
40 display: none;
41 }
41 }
42
42
43 .comment-bubble {
43 .comment-bubble {
44 color: @grey4;
44 color: @grey4;
45 margin-top: 4px;
45 margin-top: 4px;
46 margin-right: 30px;
46 margin-right: 30px;
47 visibility: hidden;
47 visibility: hidden;
48 }
48 }
49
49
50 .comment-label {
50 .comment-label {
51 float: left;
51 float: left;
52
52
53 padding: 0.4em 0.4em;
53 padding: 0.4em 0.4em;
54 margin: 2px 5px 0px -10px;
54 margin: 2px 5px 0px -10px;
55 display: inline-block;
55 display: inline-block;
56 min-height: 0;
56 min-height: 0;
57
57
58 text-align: center;
58 text-align: center;
59 font-size: 10px;
59 font-size: 10px;
60 line-height: .8em;
60 line-height: .8em;
61
61
62 font-family: @text-italic;
62 font-family: @text-italic;
63 background: #fff none;
63 background: #fff none;
64 color: @grey4;
64 color: @grey4;
65 border: 1px solid @grey4;
65 border: 1px solid @grey4;
66 white-space: nowrap;
66 white-space: nowrap;
67
67
68 text-transform: uppercase;
68 text-transform: uppercase;
69 min-width: 40px;
69
70
70 &.todo {
71 &.todo {
71 color: @color5;
72 color: @color5;
72 font-family: @text-bold-italic;
73 font-family: @text-bold-italic;
73 }
74 }
74 }
75 }
75
76
76
77
77 .comment {
78 .comment {
78
79
79 &.comment-general {
80 &.comment-general {
80 border: 1px solid @grey5;
81 border: 1px solid @grey5;
81 padding: 5px 5px 5px 5px;
82 padding: 5px 5px 5px 5px;
82 }
83 }
83
84
84 margin: @padding 0;
85 margin: @padding 0;
85 padding: 4px 0 0 0;
86 padding: 4px 0 0 0;
86 line-height: 1em;
87 line-height: 1em;
87
88
88 .rc-user {
89 .rc-user {
89 min-width: 0;
90 min-width: 0;
90 margin: 0px .5em 0 0;
91 margin: 0px .5em 0 0;
91
92
92 .user {
93 .user {
93 display: inline;
94 display: inline;
94 }
95 }
95 }
96 }
96
97
97 .meta {
98 .meta {
98 position: relative;
99 position: relative;
99 width: 100%;
100 width: 100%;
100 border-bottom: 1px solid @grey5;
101 border-bottom: 1px solid @grey5;
101 margin: -5px 0px;
102 margin: -5px 0px;
102 line-height: 24px;
103 line-height: 24px;
103
104
104 &:hover .permalink {
105 &:hover .permalink {
105 visibility: visible;
106 visibility: visible;
106 color: @rcblue;
107 color: @rcblue;
107 }
108 }
108 }
109 }
109
110
110 .author,
111 .author,
111 .date {
112 .date {
112 display: inline;
113 display: inline;
113
114
114 &:after {
115 &:after {
115 content: ' | ';
116 content: ' | ';
116 color: @grey5;
117 color: @grey5;
117 }
118 }
118 }
119 }
119
120
120 .author-general img {
121 .author-general img {
121 top: 3px;
122 top: 3px;
122 }
123 }
123 .author-inline img {
124 .author-inline img {
124 top: 3px;
125 top: 3px;
125 }
126 }
126
127
127 .status-change,
128 .status-change,
128 .permalink,
129 .permalink,
129 .changeset-status-lbl {
130 .changeset-status-lbl {
130 display: inline;
131 display: inline;
131 }
132 }
132
133
133 .permalink {
134 .permalink {
134 visibility: hidden;
135 visibility: hidden;
135 }
136 }
136
137
137 .comment-links-divider {
138 .comment-links-divider {
138 display: inline;
139 display: inline;
139 }
140 }
140
141
141 .comment-links-block {
142 .comment-links-block {
142 float:right;
143 float:right;
143 text-align: right;
144 text-align: right;
144 min-width: 85px;
145 min-width: 85px;
145
146
146 [class^="icon-"]:before,
147 [class^="icon-"]:before,
147 [class*=" icon-"]:before {
148 [class*=" icon-"]:before {
148 margin-left: 0;
149 margin-left: 0;
149 margin-right: 0;
150 margin-right: 0;
150 }
151 }
151 }
152 }
152
153
153 .comment-previous-link {
154 .comment-previous-link {
154 display: inline-block;
155 display: inline-block;
155
156
156 .arrow_comment_link{
157 .arrow_comment_link{
157 cursor: pointer;
158 cursor: pointer;
158 i {
159 i {
159 font-size:10px;
160 font-size:10px;
160 }
161 }
161 }
162 }
162 .arrow_comment_link.disabled {
163 .arrow_comment_link.disabled {
163 cursor: default;
164 cursor: default;
164 color: @grey5;
165 color: @grey5;
165 }
166 }
166 }
167 }
167
168
168 .comment-next-link {
169 .comment-next-link {
169 display: inline-block;
170 display: inline-block;
170
171
171 .arrow_comment_link{
172 .arrow_comment_link{
172 cursor: pointer;
173 cursor: pointer;
173 i {
174 i {
174 font-size:10px;
175 font-size:10px;
175 }
176 }
176 }
177 }
177 .arrow_comment_link.disabled {
178 .arrow_comment_link.disabled {
178 cursor: default;
179 cursor: default;
179 color: @grey5;
180 color: @grey5;
180 }
181 }
181 }
182 }
182
183
183 .flag_status {
184 .flag_status {
184 display: inline-block;
185 display: inline-block;
185 margin: -2px .5em 0 .25em
186 margin: -2px .5em 0 .25em
186 }
187 }
187
188
188 .delete-comment {
189 .delete-comment {
189 display: inline-block;
190 display: inline-block;
190 color: @rcblue;
191 color: @rcblue;
191
192
192 &:hover {
193 &:hover {
193 cursor: pointer;
194 cursor: pointer;
194 }
195 }
195 }
196 }
196
197
197
198
198 .text {
199 .text {
199 clear: both;
200 clear: both;
200 .border-radius(@border-radius);
201 .border-radius(@border-radius);
201 .box-sizing(border-box);
202 .box-sizing(border-box);
202
203
203 .markdown-block p,
204 .markdown-block p,
204 .rst-block p {
205 .rst-block p {
205 margin: .5em 0 !important;
206 margin: .5em 0 !important;
206 // TODO: lisa: This is needed because of other rst !important rules :[
207 // TODO: lisa: This is needed because of other rst !important rules :[
207 }
208 }
208 }
209 }
209
210
210 .pr-version {
211 .pr-version {
211 float: left;
212 float: left;
212 margin: 0px 4px;
213 margin: 0px 4px;
213 }
214 }
214 .pr-version-inline {
215 .pr-version-inline {
215 float: left;
216 float: left;
216 margin: 0px 4px;
217 margin: 0px 4px;
217 }
218 }
218 .pr-version-num {
219 .pr-version-num {
219 font-size: 10px;
220 font-size: 10px;
220 }
221 }
221
222
222 }
223 }
223
224
224 @comment-padding: 5px;
225 @comment-padding: 5px;
225
226
226 .inline-comments {
227 .inline-comments {
227 border-radius: @border-radius;
228 border-radius: @border-radius;
228 .comment {
229 .comment {
229 margin: 0;
230 margin: 0;
230 border-radius: @border-radius;
231 border-radius: @border-radius;
231 }
232 }
232 .comment-outdated {
233 .comment-outdated {
233 opacity: 0.5;
234 opacity: 0.5;
234 }
235 }
235
236
236 .comment-inline {
237 .comment-inline {
237 background: white;
238 background: white;
238 padding: @comment-padding @comment-padding;
239 padding: @comment-padding @comment-padding;
239 border: @comment-padding solid @grey6;
240 border: @comment-padding solid @grey6;
240
241
241 .text {
242 .text {
242 border: none;
243 border: none;
243 }
244 }
244 .meta {
245 .meta {
245 border-bottom: 1px solid @grey6;
246 border-bottom: 1px solid @grey6;
246 margin: -5px 0px;
247 margin: -5px 0px;
247 line-height: 24px;
248 line-height: 24px;
248 }
249 }
249 }
250 }
250 .comment-selected {
251 .comment-selected {
251 border-left: 6px solid @comment-highlight-color;
252 border-left: 6px solid @comment-highlight-color;
252 }
253 }
253 .comment-inline-form {
254 .comment-inline-form {
254 padding: @comment-padding;
255 padding: @comment-padding;
255 display: none;
256 display: none;
256 }
257 }
257 .cb-comment-add-button {
258 .cb-comment-add-button {
258 margin: @comment-padding;
259 margin: @comment-padding;
259 }
260 }
260 /* hide add comment button when form is open */
261 /* hide add comment button when form is open */
261 .comment-inline-form-open ~ .cb-comment-add-button {
262 .comment-inline-form-open ~ .cb-comment-add-button {
262 display: none;
263 display: none;
263 }
264 }
264 .comment-inline-form-open {
265 .comment-inline-form-open {
265 display: block;
266 display: block;
266 }
267 }
267 /* hide add comment button when form but no comments */
268 /* hide add comment button when form but no comments */
268 .comment-inline-form:first-child + .cb-comment-add-button {
269 .comment-inline-form:first-child + .cb-comment-add-button {
269 display: none;
270 display: none;
270 }
271 }
271 /* hide add comment button when no comments or form */
272 /* hide add comment button when no comments or form */
272 .cb-comment-add-button:first-child {
273 .cb-comment-add-button:first-child {
273 display: none;
274 display: none;
274 }
275 }
275 /* hide add comment button when only comment is being deleted */
276 /* hide add comment button when only comment is being deleted */
276 .comment-deleting:first-child + .cb-comment-add-button {
277 .comment-deleting:first-child + .cb-comment-add-button {
277 display: none;
278 display: none;
278 }
279 }
279 }
280 }
280
281
281
282
282 .show-outdated-comments {
283 .show-outdated-comments {
283 display: inline;
284 display: inline;
284 color: @rcblue;
285 color: @rcblue;
285 }
286 }
286
287
287 // Comment Form
288 // Comment Form
288 div.comment-form {
289 div.comment-form {
289 margin-top: 20px;
290 margin-top: 20px;
290 }
291 }
291
292
292 .comment-form strong {
293 .comment-form strong {
293 display: block;
294 display: block;
294 margin-bottom: 15px;
295 margin-bottom: 15px;
295 }
296 }
296
297
297 .comment-form textarea {
298 .comment-form textarea {
298 width: 100%;
299 width: 100%;
299 height: 100px;
300 height: 100px;
300 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
301 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
301 }
302 }
302
303
303 form.comment-form {
304 form.comment-form {
304 margin-top: 10px;
305 margin-top: 10px;
305 margin-left: 10px;
306 margin-left: 10px;
306 }
307 }
307
308
308 .comment-inline-form .comment-block-ta,
309 .comment-inline-form .comment-block-ta,
309 .comment-form .comment-block-ta,
310 .comment-form .comment-block-ta,
310 .comment-form .preview-box {
311 .comment-form .preview-box {
311 .border-radius(@border-radius);
312 .border-radius(@border-radius);
312 .box-sizing(border-box);
313 .box-sizing(border-box);
313 background-color: white;
314 background-color: white;
314 }
315 }
315
316
316 .comment-form-submit {
317 .comment-form-submit {
317 margin-top: 5px;
318 margin-top: 5px;
318 margin-left: 525px;
319 margin-left: 525px;
319 }
320 }
320
321
321 .file-comments {
322 .file-comments {
322 display: none;
323 display: none;
323 }
324 }
324
325
325 .comment-form .preview-box.unloaded,
326 .comment-form .preview-box.unloaded,
326 .comment-inline-form .preview-box.unloaded {
327 .comment-inline-form .preview-box.unloaded {
327 height: 50px;
328 height: 50px;
328 text-align: center;
329 text-align: center;
329 padding: 20px;
330 padding: 20px;
330 background-color: white;
331 background-color: white;
331 }
332 }
332
333
333 .comment-footer {
334 .comment-footer {
334 position: relative;
335 position: relative;
335 width: 100%;
336 width: 100%;
336 min-height: 42px;
337 min-height: 42px;
337
338
338 .status_box,
339 .status_box,
339 .cancel-button {
340 .cancel-button {
340 float: left;
341 float: left;
341 display: inline-block;
342 display: inline-block;
342 }
343 }
343
344
344 .action-buttons {
345 .action-buttons {
345 float: right;
346 float: right;
346 display: inline-block;
347 display: inline-block;
347 }
348 }
348 }
349 }
349
350
350 .comment-form {
351 .comment-form {
351
352
352 .comment {
353 .comment {
353 margin-left: 10px;
354 margin-left: 10px;
354 }
355 }
355
356
356 .comment-help {
357 .comment-help {
357 color: @grey4;
358 color: @grey4;
358 padding: 5px 0 5px 0;
359 padding: 5px 0 5px 0;
359 }
360 }
360
361
361 .comment-title {
362 .comment-title {
362 padding: 5px 0 5px 0;
363 padding: 5px 0 5px 0;
363 }
364 }
364
365
365 .comment-button {
366 .comment-button {
366 display: inline-block;
367 display: inline-block;
367 }
368 }
368
369
369 .comment-button .comment-button-input {
370 .comment-button-input {
370 margin-right: 0;
371 margin-right: 0;
371 }
372 }
372
373
373 .comment-footer {
374 .comment-footer {
374 margin-bottom: 110px;
375 margin-bottom: 110px;
375 margin-top: 10px;
376 margin-top: 10px;
376 }
377 }
377 }
378 }
378
379
379
380
380 .comment-form-login {
381 .comment-form-login {
381 .comment-help {
382 .comment-help {
382 padding: 0.9em; //same as the button
383 padding: 0.9em; //same as the button
383 }
384 }
384
385
385 div.clearfix {
386 div.clearfix {
386 clear: both;
387 clear: both;
387 width: 100%;
388 width: 100%;
388 display: block;
389 display: block;
389 }
390 }
390 }
391 }
391
392
392 .comment-type {
393 .comment-type {
393 margin: 0px;
394 margin: 0px;
394 border-radius: inherit;
395 border-radius: inherit;
395 border-color: @grey6;
396 border-color: @grey6;
396 }
397 }
397
398
398 .preview-box {
399 .preview-box {
399 min-height: 105px;
400 min-height: 105px;
400 margin-bottom: 15px;
401 margin-bottom: 15px;
401 background-color: white;
402 background-color: white;
402 .border-radius(@border-radius);
403 .border-radius(@border-radius);
403 .box-sizing(border-box);
404 .box-sizing(border-box);
404 }
405 }
405
406
406 .add-another-button {
407 .add-another-button {
407 margin-left: 10px;
408 margin-left: 10px;
408 margin-top: 10px;
409 margin-top: 10px;
409 margin-bottom: 10px;
410 margin-bottom: 10px;
410 }
411 }
411
412
412 .comment .buttons {
413 .comment .buttons {
413 float: right;
414 float: right;
414 margin: -1px 0px 0px 0px;
415 margin: -1px 0px 0px 0px;
415 }
416 }
416
417
417 // Inline Comment Form
418 // Inline Comment Form
418 .injected_diff .comment-inline-form,
419 .injected_diff .comment-inline-form,
419 .comment-inline-form {
420 .comment-inline-form {
420 background-color: white;
421 background-color: white;
421 margin-top: 10px;
422 margin-top: 10px;
422 margin-bottom: 20px;
423 margin-bottom: 20px;
423 }
424 }
424
425
425 .inline-form {
426 .inline-form {
426 padding: 10px 7px;
427 padding: 10px 7px;
427 }
428 }
428
429
429 .inline-form div {
430 .inline-form div {
430 max-width: 100%;
431 max-width: 100%;
431 }
432 }
432
433
433 .overlay {
434 .overlay {
434 display: none;
435 display: none;
435 position: absolute;
436 position: absolute;
436 width: 100%;
437 width: 100%;
437 text-align: center;
438 text-align: center;
438 vertical-align: middle;
439 vertical-align: middle;
439 font-size: 16px;
440 font-size: 16px;
440 background: none repeat scroll 0 0 white;
441 background: none repeat scroll 0 0 white;
441
442
442 &.submitting {
443 &.submitting {
443 display: block;
444 display: block;
444 opacity: 0.5;
445 opacity: 0.5;
445 z-index: 100;
446 z-index: 100;
446 }
447 }
447 }
448 }
448 .comment-inline-form .overlay.submitting .overlay-text {
449 .comment-inline-form .overlay.submitting .overlay-text {
449 margin-top: 5%;
450 margin-top: 5%;
450 }
451 }
451
452
452 .comment-inline-form .clearfix,
453 .comment-inline-form .clearfix,
453 .comment-form .clearfix {
454 .comment-form .clearfix {
454 .border-radius(@border-radius);
455 .border-radius(@border-radius);
455 margin: 0px;
456 margin: 0px;
456 }
457 }
457
458
458 .comment-inline-form .comment-footer {
459 .comment-inline-form .comment-footer {
459 margin: 10px 0px 0px 0px;
460 margin: 10px 0px 0px 0px;
460 }
461 }
461
462
462 .hide-inline-form-button {
463 .hide-inline-form-button {
463 margin-left: 5px;
464 margin-left: 5px;
464 }
465 }
465 .comment-button .hide-inline-form {
466 .comment-button .hide-inline-form {
466 background: white;
467 background: white;
467 }
468 }
468
469
469 .comment-area {
470 .comment-area {
470 padding: 8px 12px;
471 padding: 8px 12px;
471 border: 1px solid @grey5;
472 border: 1px solid @grey5;
472 .border-radius(@border-radius);
473 .border-radius(@border-radius);
473 }
474 }
474
475
475 .comment-area-header .nav-links {
476 .comment-area-header .nav-links {
476 display: flex;
477 display: flex;
477 flex-flow: row wrap;
478 flex-flow: row wrap;
478 -webkit-flex-flow: row wrap;
479 -webkit-flex-flow: row wrap;
479 width: 100%;
480 width: 100%;
480 }
481 }
481
482
482 .comment-area-footer {
483 .comment-area-footer {
483 display: flex;
484 display: flex;
484 }
485 }
485
486
486 .comment-footer .toolbar {
487 .comment-footer .toolbar {
487
488
488 }
489 }
489
490
490 .nav-links {
491 .nav-links {
491 padding: 0;
492 padding: 0;
492 margin: 0;
493 margin: 0;
493 list-style: none;
494 list-style: none;
494 height: auto;
495 height: auto;
495 border-bottom: 1px solid @grey5;
496 border-bottom: 1px solid @grey5;
496 }
497 }
497 .nav-links li {
498 .nav-links li {
498 display: inline-block;
499 display: inline-block;
499 }
500 }
500 .nav-links li:before {
501 .nav-links li:before {
501 content: "";
502 content: "";
502 }
503 }
503 .nav-links li a.disabled {
504 .nav-links li a.disabled {
504 cursor: not-allowed;
505 cursor: not-allowed;
505 }
506 }
506
507
507 .nav-links li.active a {
508 .nav-links li.active a {
508 border-bottom: 2px solid @rcblue;
509 border-bottom: 2px solid @rcblue;
509 color: #000;
510 color: #000;
510 font-weight: 600;
511 font-weight: 600;
511 }
512 }
512 .nav-links li a {
513 .nav-links li a {
513 display: inline-block;
514 display: inline-block;
514 padding: 0px 10px 5px 10px;
515 padding: 0px 10px 5px 10px;
515 margin-bottom: -1px;
516 margin-bottom: -1px;
516 font-size: 14px;
517 font-size: 14px;
517 line-height: 28px;
518 line-height: 28px;
518 color: #8f8f8f;
519 color: #8f8f8f;
519 border-bottom: 2px solid transparent;
520 border-bottom: 2px solid transparent;
520 }
521 }
521
522
522 .toolbar-text {
523 .toolbar-text {
523 float: left;
524 float: left;
524 margin: -5px 0px 0px 0px;
525 margin: -5px 0px 0px 0px;
525 font-size: 12px;
526 font-size: 12px;
526 }
527 }
527
528
@@ -1,55 +1,56 b''
1
1
2 /******************************************************************************
2 /******************************************************************************
3 * *
3 * *
4 * DO NOT CHANGE THIS FILE MANUALLY *
4 * DO NOT CHANGE THIS FILE MANUALLY *
5 * *
5 * *
6 * *
6 * *
7 * This file is automatically generated when the app starts up with *
7 * This file is automatically generated when the app starts up with *
8 * generate_js_files = true *
8 * generate_js_files = true *
9 * *
9 * *
10 * To add a route here pass jsroute=True to the route definition in the app *
10 * To add a route here pass jsroute=True to the route definition in the app *
11 * *
11 * *
12 ******************************************************************************/
12 ******************************************************************************/
13 function registerRCRoutes() {
13 function registerRCRoutes() {
14 // routes registration
14 // routes registration
15 pyroutes.register('home', '/', []);
15 pyroutes.register('home', '/', []);
16 pyroutes.register('user_autocomplete_data', '/_users', []);
16 pyroutes.register('user_autocomplete_data', '/_users', []);
17 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
17 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
18 pyroutes.register('new_repo', '/_admin/create_repository', []);
18 pyroutes.register('new_repo', '/_admin/create_repository', []);
19 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
19 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
20 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
20 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
21 pyroutes.register('gists', '/_admin/gists', []);
21 pyroutes.register('gists', '/_admin/gists', []);
22 pyroutes.register('new_gist', '/_admin/gists/new', []);
22 pyroutes.register('new_gist', '/_admin/gists/new', []);
23 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
23 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
24 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
24 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
25 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
25 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
26 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
26 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
27 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/default-reviewers', ['repo_name']);
27 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/default-reviewers', ['repo_name']);
28 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
28 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
29 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
29 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
30 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
30 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
31 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
31 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
32 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
32 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
33 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
33 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
34 pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']);
34 pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']);
35 pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
35 pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
36 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
36 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
37 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
37 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
38 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
38 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
39 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
39 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
40 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
40 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
41 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
41 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
42 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
42 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
43 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
43 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
44 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
44 pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
45 pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
45 pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
46 pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
46 pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
47 pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
47 pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
48 pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
48 pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
49 pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
49 pyroutes.register('files_annotate_home', '/%(repo_name)s/annotate/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
50 pyroutes.register('files_annotate_home', '/%(repo_name)s/annotate/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
50 pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
51 pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
51 pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
52 pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
52 pyroutes.register('files_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
53 pyroutes.register('files_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
53 pyroutes.register('summary_home_slash', '/%(repo_name)s/', ['repo_name']);
54 pyroutes.register('summary_home_slash', '/%(repo_name)s/', ['repo_name']);
54 pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']);
55 pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']);
55 }
56 }
@@ -1,666 +1,798 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 // returns a node from given html;
28 // returns a node from given html;
29 var fromHTML = function(html){
29 var fromHTML = function(html){
30 var _html = document.createElement('element');
30 var _html = document.createElement('element');
31 _html.innerHTML = html;
31 _html.innerHTML = html;
32 return _html;
32 return _html;
33 };
33 };
34
34
35 var tableTr = function(cls, body){
35 var tableTr = function(cls, body){
36 var _el = document.createElement('div');
36 var _el = document.createElement('div');
37 var _body = $(body).attr('id');
37 var _body = $(body).attr('id');
38 var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
38 var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
39 var id = 'comment-tr-{0}'.format(comment_id);
39 var id = 'comment-tr-{0}'.format(comment_id);
40 var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
40 var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
41 '<td class="add-comment-line tooltip tooltip" title="Add Comment"><span class="add-comment-content"></span></td>'+
41 '<td class="add-comment-line tooltip tooltip" title="Add Comment"><span class="add-comment-content"></span></td>'+
42 '<td></td>'+
42 '<td></td>'+
43 '<td></td>'+
43 '<td></td>'+
44 '<td></td>'+
44 '<td></td>'+
45 '<td>{2}</td>'+
45 '<td>{2}</td>'+
46 '</tr></tbody></table>').format(id, cls, body);
46 '</tr></tbody></table>').format(id, cls, body);
47 $(_el).html(_html);
47 $(_el).html(_html);
48 return _el.children[0].children[0].children[0];
48 return _el.children[0].children[0].children[0];
49 };
49 };
50
50
51 function bindDeleteCommentButtons() {
51 function bindDeleteCommentButtons() {
52 $('.delete-comment').one('click', function() {
52 $('.delete-comment').one('click', function() {
53 var comment_id = $(this).data("comment-id");
53 var comment_id = $(this).data("comment-id");
54
54
55 if (comment_id){
55 if (comment_id){
56 deleteComment(comment_id);
56 deleteComment(comment_id);
57 }
57 }
58 });
58 });
59 }
59 }
60
60
61 var deleteComment = function(comment_id) {
61 var deleteComment = function(comment_id) {
62 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
62 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
63 var postData = {
63 var postData = {
64 '_method': 'delete',
64 '_method': 'delete',
65 'csrf_token': CSRF_TOKEN
65 'csrf_token': CSRF_TOKEN
66 };
66 };
67
67
68 var success = function(o) {
68 var success = function(o) {
69 window.location.reload();
69 window.location.reload();
70 };
70 };
71 ajaxPOST(url, postData, success);
71 ajaxPOST(url, postData, success);
72 };
72 };
73
73
74
74
75 var bindToggleButtons = function() {
75 var bindToggleButtons = function() {
76 $('.comment-toggle').on('click', function() {
76 $('.comment-toggle').on('click', function() {
77 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
77 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
78 });
78 });
79 };
79 };
80
80
81 var linkifyComments = function(comments) {
81 var linkifyComments = function(comments) {
82 /* TODO: dan: remove this - it should no longer needed */
82 /* TODO: marcink: remove this - it should no longer needed */
83 for (var i = 0; i < comments.length; i++) {
83 for (var i = 0; i < comments.length; i++) {
84 var comment_id = $(comments[i]).data('comment-id');
84 var comment_id = $(comments[i]).data('comment-id');
85 var prev_comment_id = $(comments[i - 1]).data('comment-id');
85 var prev_comment_id = $(comments[i - 1]).data('comment-id');
86 var next_comment_id = $(comments[i + 1]).data('comment-id');
86 var next_comment_id = $(comments[i + 1]).data('comment-id');
87
87
88 // place next/prev links
88 // place next/prev links
89 if (prev_comment_id) {
89 if (prev_comment_id) {
90 $('#prev_c_' + comment_id).show();
90 $('#prev_c_' + comment_id).show();
91 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
91 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
92 'href', '#comment-' + prev_comment_id).removeClass('disabled');
92 'href', '#comment-' + prev_comment_id).removeClass('disabled');
93 }
93 }
94 if (next_comment_id) {
94 if (next_comment_id) {
95 $('#next_c_' + comment_id).show();
95 $('#next_c_' + comment_id).show();
96 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
96 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
97 'href', '#comment-' + next_comment_id).removeClass('disabled');
97 'href', '#comment-' + next_comment_id).removeClass('disabled');
98 }
98 }
99 /* TODO(marcink): end removal here */
100
99 // place a first link to the total counter
101 // place a first link to the total counter
100 if (i === 0) {
102 if (i === 0) {
101 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
103 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
102 }
104 }
103 }
105 }
104
106
105 };
107 };
106
108
107
109
108 /* Comment form for main and inline comments */
110 /* Comment form for main and inline comments */
109 var CommentForm = (function() {
111
112 (function(mod) {
113 if (typeof exports == "object" && typeof module == "object") // CommonJS
114 module.exports = mod();
115 else // Plain browser env
116 (this || window).CommentForm = mod();
117
118 })(function() {
110 "use strict";
119 "use strict";
111
120
112 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
121 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
122 if (!(this instanceof CommentForm)) {
123 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
124 }
125
126 // bind the element instance to our Form
127 $(formElement).get(0).CommentForm = this;
113
128
114 this.withLineNo = function(selector) {
129 this.withLineNo = function(selector) {
115 var lineNo = this.lineNo;
130 var lineNo = this.lineNo;
116 if (lineNo === undefined) {
131 if (lineNo === undefined) {
117 return selector
132 return selector
118 } else {
133 } else {
119 return selector + '_' + lineNo;
134 return selector + '_' + lineNo;
120 }
135 }
121 };
136 };
122
137
123 this.commitId = commitId;
138 this.commitId = commitId;
124 this.pullRequestId = pullRequestId;
139 this.pullRequestId = pullRequestId;
125 this.lineNo = lineNo;
140 this.lineNo = lineNo;
126 this.initAutocompleteActions = initAutocompleteActions;
141 this.initAutocompleteActions = initAutocompleteActions;
127
142
128 this.previewButton = this.withLineNo('#preview-btn');
143 this.previewButton = this.withLineNo('#preview-btn');
129 this.previewContainer = this.withLineNo('#preview-container');
144 this.previewContainer = this.withLineNo('#preview-container');
130
145
131 this.previewBoxSelector = this.withLineNo('#preview-box');
146 this.previewBoxSelector = this.withLineNo('#preview-box');
132
147
133 this.editButton = this.withLineNo('#edit-btn');
148 this.editButton = this.withLineNo('#edit-btn');
134 this.editContainer = this.withLineNo('#edit-container');
149 this.editContainer = this.withLineNo('#edit-container');
135 this.cancelButton = this.withLineNo('#cancel-btn');
150 this.cancelButton = this.withLineNo('#cancel-btn');
136 this.commentType = this.withLineNo('#comment_type');
151 this.commentType = this.withLineNo('#comment_type');
137
152
153 this.resolvesId = null;
154 this.resolvesActionId = null;
155
138 this.cmBox = this.withLineNo('#text');
156 this.cmBox = this.withLineNo('#text');
139 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
157 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
140
158
141 this.statusChange = '#change_status';
159 this.statusChange = '#change_status';
142
160
143 this.submitForm = formElement;
161 this.submitForm = formElement;
144 this.submitButton = $(this.submitForm).find('input[type="submit"]');
162 this.submitButton = $(this.submitForm).find('input[type="submit"]');
145 this.submitButtonText = this.submitButton.val();
163 this.submitButtonText = this.submitButton.val();
146
164
147 this.previewUrl = pyroutes.url('changeset_comment_preview',
165 this.previewUrl = pyroutes.url('changeset_comment_preview',
148 {'repo_name': templateContext.repo_name});
166 {'repo_name': templateContext.repo_name});
149
167
150 // based on commitId, or pullReuqestId decide where do we submit
168 if (resolvesCommentId){
169 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
170 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
171 $(this.commentType).prop('disabled', true);
172 $(this.commentType).addClass('disabled');
173
174 var resolvedInfo = (
175 '<li class="">' +
176 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
177 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
178 '</li>'
179 ).format(resolvesCommentId, _gettext('resolve comment'));
180 $(resolvedInfo).insertAfter($(this.commentType).parent());
181 }
182
183 // based on commitId, or pullRequestId decide where do we submit
151 // out data
184 // out data
152 if (this.commitId){
185 if (this.commitId){
153 this.submitUrl = pyroutes.url('changeset_comment',
186 this.submitUrl = pyroutes.url('changeset_comment',
154 {'repo_name': templateContext.repo_name,
187 {'repo_name': templateContext.repo_name,
155 'revision': this.commitId});
188 'revision': this.commitId});
189 this.selfUrl = pyroutes.url('changeset_home',
190 {'repo_name': templateContext.repo_name,
191 'revision': this.commitId});
156
192
157 } else if (this.pullRequestId) {
193 } else if (this.pullRequestId) {
158 this.submitUrl = pyroutes.url('pullrequest_comment',
194 this.submitUrl = pyroutes.url('pullrequest_comment',
159 {'repo_name': templateContext.repo_name,
195 {'repo_name': templateContext.repo_name,
160 'pull_request_id': this.pullRequestId});
196 'pull_request_id': this.pullRequestId});
197 this.selfUrl = pyroutes.url('pullrequest_show',
198 {'repo_name': templateContext.repo_name,
199 'pull_request_id': this.pullRequestId});
161
200
162 } else {
201 } else {
163 throw new Error(
202 throw new Error(
164 'CommentForm requires pullRequestId, or commitId to be specified.')
203 'CommentForm requires pullRequestId, or commitId to be specified.')
165 }
204 }
166
205
167 this.getCmInstance = function(){
206 this.getCmInstance = function(){
168 return this.cm
207 return this.cm
169 };
208 };
170
209
210 this.setPlaceholder = function(placeholder) {
211 var cm = this.getCmInstance();
212 if (cm){
213 cm.setOption('placeholder', placeholder);
214 }
215 };
216
171 var self = this;
217 var self = this;
172
218
173 this.getCommentStatus = function() {
219 this.getCommentStatus = function() {
174 return $(this.submitForm).find(this.statusChange).val();
220 return $(this.submitForm).find(this.statusChange).val();
175 };
221 };
176 this.getCommentType = function() {
222 this.getCommentType = function() {
177 return $(this.submitForm).find(this.commentType).val();
223 return $(this.submitForm).find(this.commentType).val();
178 };
224 };
225
226 this.getResolvesId = function() {
227 return $(this.submitForm).find(this.resolvesId).val() || null;
228 };
229 this.markCommentResolved = function(resolvedCommentId){
230 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
231 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
232 };
233
179 this.isAllowedToSubmit = function() {
234 this.isAllowedToSubmit = function() {
180 return !$(this.submitButton).prop('disabled');
235 return !$(this.submitButton).prop('disabled');
181 };
236 };
182
237
183 this.initStatusChangeSelector = function(){
238 this.initStatusChangeSelector = function(){
184 var formatChangeStatus = function(state, escapeMarkup) {
239 var formatChangeStatus = function(state, escapeMarkup) {
185 var originalOption = state.element;
240 var originalOption = state.element;
186 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
241 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
187 '<span>' + escapeMarkup(state.text) + '</span>';
242 '<span>' + escapeMarkup(state.text) + '</span>';
188 };
243 };
189 var formatResult = function(result, container, query, escapeMarkup) {
244 var formatResult = function(result, container, query, escapeMarkup) {
190 return formatChangeStatus(result, escapeMarkup);
245 return formatChangeStatus(result, escapeMarkup);
191 };
246 };
192
247
193 var formatSelection = function(data, container, escapeMarkup) {
248 var formatSelection = function(data, container, escapeMarkup) {
194 return formatChangeStatus(data, escapeMarkup);
249 return formatChangeStatus(data, escapeMarkup);
195 };
250 };
196
251
197 $(this.submitForm).find(this.statusChange).select2({
252 $(this.submitForm).find(this.statusChange).select2({
198 placeholder: _gettext('Status Review'),
253 placeholder: _gettext('Status Review'),
199 formatResult: formatResult,
254 formatResult: formatResult,
200 formatSelection: formatSelection,
255 formatSelection: formatSelection,
201 containerCssClass: "drop-menu status_box_menu",
256 containerCssClass: "drop-menu status_box_menu",
202 dropdownCssClass: "drop-menu-dropdown",
257 dropdownCssClass: "drop-menu-dropdown",
203 dropdownAutoWidth: true,
258 dropdownAutoWidth: true,
204 minimumResultsForSearch: -1
259 minimumResultsForSearch: -1
205 });
260 });
206 $(this.submitForm).find(this.statusChange).on('change', function() {
261 $(this.submitForm).find(this.statusChange).on('change', function() {
207 var status = self.getCommentStatus();
262 var status = self.getCommentStatus();
208 if (status && !self.lineNo) {
263 if (status && self.lineNo == 'general') {
209 $(self.submitButton).prop('disabled', false);
264 $(self.submitButton).prop('disabled', false);
210 }
265 }
211 //todo, fix this name
266
212 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
267 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
213 self.cm.setOption('placeholder', placeholderText);
268 self.setPlaceholder(placeholderText)
214 })
269 })
215 };
270 };
216
271
217 // reset the comment form into it's original state
272 // reset the comment form into it's original state
218 this.resetCommentFormState = function(content) {
273 this.resetCommentFormState = function(content) {
219 content = content || '';
274 content = content || '';
220
275
221 $(this.editContainer).show();
276 $(this.editContainer).show();
222 $(this.editButton).parent().addClass('active');
277 $(this.editButton).parent().addClass('active');
223
278
224 $(this.previewContainer).hide();
279 $(this.previewContainer).hide();
225 $(this.previewButton).parent().removeClass('active');
280 $(this.previewButton).parent().removeClass('active');
226
281
227 this.setActionButtonsDisabled(true);
282 this.setActionButtonsDisabled(true);
228 self.cm.setValue(content);
283 self.cm.setValue(content);
229 self.cm.setOption("readOnly", false);
284 self.cm.setOption("readOnly", false);
230 };
285 };
231
286
232 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
287 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
233 failHandler = failHandler || function() {};
288 failHandler = failHandler || function() {};
234 var postData = toQueryString(postData);
289 var postData = toQueryString(postData);
235 var request = $.ajax({
290 var request = $.ajax({
236 url: url,
291 url: url,
237 type: 'POST',
292 type: 'POST',
238 data: postData,
293 data: postData,
239 headers: {'X-PARTIAL-XHR': true}
294 headers: {'X-PARTIAL-XHR': true}
240 })
295 })
241 .done(function(data) {
296 .done(function(data) {
242 successHandler(data);
297 successHandler(data);
243 })
298 })
244 .fail(function(data, textStatus, errorThrown){
299 .fail(function(data, textStatus, errorThrown){
245 alert(
300 alert(
246 "Error while submitting comment.\n" +
301 "Error while submitting comment.\n" +
247 "Error code {0} ({1}).".format(data.status, data.statusText));
302 "Error code {0} ({1}).".format(data.status, data.statusText));
248 failHandler()
303 failHandler()
249 });
304 });
250 return request;
305 return request;
251 };
306 };
252
307
253 // overwrite a submitHandler, we need to do it for inline comments
308 // overwrite a submitHandler, we need to do it for inline comments
254 this.setHandleFormSubmit = function(callback) {
309 this.setHandleFormSubmit = function(callback) {
255 this.handleFormSubmit = callback;
310 this.handleFormSubmit = callback;
256 };
311 };
257
312
258 // default handler for for submit for main comments
313 // default handler for for submit for main comments
259 this.handleFormSubmit = function() {
314 this.handleFormSubmit = function() {
260 var text = self.cm.getValue();
315 var text = self.cm.getValue();
261 var status = self.getCommentStatus();
316 var status = self.getCommentStatus();
262 var commentType = self.getCommentType();
317 var commentType = self.getCommentType();
318 var resolvesCommentId = self.getResolvesId();
263
319
264 if (text === "" && !status) {
320 if (text === "" && !status) {
265 return;
321 return;
266 }
322 }
267
323
268 var excludeCancelBtn = false;
324 var excludeCancelBtn = false;
269 var submitEvent = true;
325 var submitEvent = true;
270 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
326 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
271 self.cm.setOption("readOnly", true);
327 self.cm.setOption("readOnly", true);
272 var postData = {
328 var postData = {
273 'text': text,
329 'text': text,
274 'changeset_status': status,
330 'changeset_status': status,
275 'comment_type': commentType,
331 'comment_type': commentType,
276 'csrf_token': CSRF_TOKEN
332 'csrf_token': CSRF_TOKEN
277 };
333 };
278
334 if (resolvesCommentId){
335 postData['resolves_comment_id'] = resolvesCommentId;
336 }
279 var submitSuccessCallback = function(o) {
337 var submitSuccessCallback = function(o) {
280 if (status) {
338 if (status) {
281 location.reload(true);
339 location.reload(true);
282 } else {
340 } else {
283 $('#injected_page_comments').append(o.rendered_text);
341 $('#injected_page_comments').append(o.rendered_text);
284 self.resetCommentFormState();
342 self.resetCommentFormState();
285 bindDeleteCommentButtons();
343 bindDeleteCommentButtons();
286 timeagoActivate();
344 timeagoActivate();
345
346 //mark visually which comment was resolved
347 if (resolvesCommentId) {
348 this.markCommentResolved(resolvesCommentId);
349 }
287 }
350 }
288 };
351 };
289 var submitFailCallback = function(){
352 var submitFailCallback = function(){
290 self.resetCommentFormState(text)
353 self.resetCommentFormState(text);
291 };
354 };
292 self.submitAjaxPOST(
355 self.submitAjaxPOST(
293 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
356 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
294 };
357 };
295
358
296 this.previewSuccessCallback = function(o) {
359 this.previewSuccessCallback = function(o) {
297 $(self.previewBoxSelector).html(o);
360 $(self.previewBoxSelector).html(o);
298 $(self.previewBoxSelector).removeClass('unloaded');
361 $(self.previewBoxSelector).removeClass('unloaded');
299
362
300 // swap buttons, making preview active
363 // swap buttons, making preview active
301 $(self.previewButton).parent().addClass('active');
364 $(self.previewButton).parent().addClass('active');
302 $(self.editButton).parent().removeClass('active');
365 $(self.editButton).parent().removeClass('active');
303
366
304 // unlock buttons
367 // unlock buttons
305 self.setActionButtonsDisabled(false);
368 self.setActionButtonsDisabled(false);
306 };
369 };
307
370
308 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
371 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
309 excludeCancelBtn = excludeCancelBtn || false;
372 excludeCancelBtn = excludeCancelBtn || false;
310 submitEvent = submitEvent || false;
373 submitEvent = submitEvent || false;
311
374
312 $(this.editButton).prop('disabled', state);
375 $(this.editButton).prop('disabled', state);
313 $(this.previewButton).prop('disabled', state);
376 $(this.previewButton).prop('disabled', state);
314
377
315 if (!excludeCancelBtn) {
378 if (!excludeCancelBtn) {
316 $(this.cancelButton).prop('disabled', state);
379 $(this.cancelButton).prop('disabled', state);
317 }
380 }
318
381
319 var submitState = state;
382 var submitState = state;
320 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
383 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
321 // if the value of commit review status is set, we allow
384 // if the value of commit review status is set, we allow
322 // submit button, but only on Main form, lineNo means inline
385 // submit button, but only on Main form, lineNo means inline
323 submitState = false
386 submitState = false
324 }
387 }
325 $(this.submitButton).prop('disabled', submitState);
388 $(this.submitButton).prop('disabled', submitState);
326 if (submitEvent) {
389 if (submitEvent) {
327 $(this.submitButton).val(_gettext('Submitting...'));
390 $(this.submitButton).val(_gettext('Submitting...'));
328 } else {
391 } else {
329 $(this.submitButton).val(this.submitButtonText);
392 $(this.submitButton).val(this.submitButtonText);
330 }
393 }
331
394
332 };
395 };
333
396
334 // lock preview/edit/submit buttons on load, but exclude cancel button
397 // lock preview/edit/submit buttons on load, but exclude cancel button
335 var excludeCancelBtn = true;
398 var excludeCancelBtn = true;
336 this.setActionButtonsDisabled(true, excludeCancelBtn);
399 this.setActionButtonsDisabled(true, excludeCancelBtn);
337
400
338 // anonymous users don't have access to initialized CM instance
401 // anonymous users don't have access to initialized CM instance
339 if (this.cm !== undefined){
402 if (this.cm !== undefined){
340 this.cm.on('change', function(cMirror) {
403 this.cm.on('change', function(cMirror) {
341 if (cMirror.getValue() === "") {
404 if (cMirror.getValue() === "") {
342 self.setActionButtonsDisabled(true, excludeCancelBtn)
405 self.setActionButtonsDisabled(true, excludeCancelBtn)
343 } else {
406 } else {
344 self.setActionButtonsDisabled(false, excludeCancelBtn)
407 self.setActionButtonsDisabled(false, excludeCancelBtn)
345 }
408 }
346 });
409 });
347 }
410 }
348
411
349 $(this.editButton).on('click', function(e) {
412 $(this.editButton).on('click', function(e) {
350 e.preventDefault();
413 e.preventDefault();
351
414
352 $(self.previewButton).parent().removeClass('active');
415 $(self.previewButton).parent().removeClass('active');
353 $(self.previewContainer).hide();
416 $(self.previewContainer).hide();
354
417
355 $(self.editButton).parent().addClass('active');
418 $(self.editButton).parent().addClass('active');
356 $(self.editContainer).show();
419 $(self.editContainer).show();
357
420
358 });
421 });
359
422
360 $(this.previewButton).on('click', function(e) {
423 $(this.previewButton).on('click', function(e) {
361 e.preventDefault();
424 e.preventDefault();
362 var text = self.cm.getValue();
425 var text = self.cm.getValue();
363
426
364 if (text === "") {
427 if (text === "") {
365 return;
428 return;
366 }
429 }
367
430
368 var postData = {
431 var postData = {
369 'text': text,
432 'text': text,
370 'renderer': DEFAULT_RENDERER,
433 'renderer': templateContext.visual.default_renderer,
371 'csrf_token': CSRF_TOKEN
434 'csrf_token': CSRF_TOKEN
372 };
435 };
373
436
374 // lock ALL buttons on preview
437 // lock ALL buttons on preview
375 self.setActionButtonsDisabled(true);
438 self.setActionButtonsDisabled(true);
376
439
377 $(self.previewBoxSelector).addClass('unloaded');
440 $(self.previewBoxSelector).addClass('unloaded');
378 $(self.previewBoxSelector).html(_gettext('Loading ...'));
441 $(self.previewBoxSelector).html(_gettext('Loading ...'));
379
442
380 $(self.editContainer).hide();
443 $(self.editContainer).hide();
381 $(self.previewContainer).show();
444 $(self.previewContainer).show();
382
445
383 // by default we reset state of comment preserving the text
446 // by default we reset state of comment preserving the text
384 var previewFailCallback = function(){
447 var previewFailCallback = function(){
385 self.resetCommentFormState(text)
448 self.resetCommentFormState(text)
386 };
449 };
387 self.submitAjaxPOST(
450 self.submitAjaxPOST(
388 self.previewUrl, postData, self.previewSuccessCallback,
451 self.previewUrl, postData, self.previewSuccessCallback,
389 previewFailCallback);
452 previewFailCallback);
390
453
391 $(self.previewButton).parent().addClass('active');
454 $(self.previewButton).parent().addClass('active');
392 $(self.editButton).parent().removeClass('active');
455 $(self.editButton).parent().removeClass('active');
393 });
456 });
394
457
395 $(this.submitForm).submit(function(e) {
458 $(this.submitForm).submit(function(e) {
396 e.preventDefault();
459 e.preventDefault();
397 var allowedToSubmit = self.isAllowedToSubmit();
460 var allowedToSubmit = self.isAllowedToSubmit();
398 if (!allowedToSubmit){
461 if (!allowedToSubmit){
399 return false;
462 return false;
400 }
463 }
401 self.handleFormSubmit();
464 self.handleFormSubmit();
402 });
465 });
403
466
404 }
467 }
405
468
406 return CommentForm;
469 return CommentForm;
407 })();
470 });
408
471
409 var CommentsController = function() { /* comments controller */
472 /* comments controller */
473 var CommentsController = function() {
474 var mainComment = '#text';
410 var self = this;
475 var self = this;
411
476
412 this.cancelComment = function(node) {
477 this.cancelComment = function(node) {
413 var $node = $(node);
478 var $node = $(node);
414 var $td = $node.closest('td');
479 var $td = $node.closest('td');
415 $node.closest('.comment-inline-form').removeClass('comment-inline-form-open');
480 $node.closest('.comment-inline-form').remove();
416 return false;
481 return false;
417 };
482 };
418
483
419 this.getLineNumber = function(node) {
484 this.getLineNumber = function(node) {
420 var $node = $(node);
485 var $node = $(node);
421 return $node.closest('td').attr('data-line-number');
486 return $node.closest('td').attr('data-line-number');
422 };
487 };
423
488
424 this.scrollToComment = function(node, offset, outdated) {
489 this.scrollToComment = function(node, offset, outdated) {
425 var outdated = outdated || false;
490 var outdated = outdated || false;
426 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
491 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
427
492
428 if (!node) {
493 if (!node) {
429 node = $('.comment-selected');
494 node = $('.comment-selected');
430 if (!node.length) {
495 if (!node.length) {
431 node = $('comment-current')
496 node = $('comment-current')
432 }
497 }
433 }
498 }
434 $comment = $(node).closest(klass);
499 $comment = $(node).closest(klass);
435 $comments = $(klass);
500 $comments = $(klass);
436
501
437 $('.comment-selected').removeClass('comment-selected');
502 $('.comment-selected').removeClass('comment-selected');
438
503
439 var nextIdx = $(klass).index($comment) + offset;
504 var nextIdx = $(klass).index($comment) + offset;
440 if (nextIdx >= $comments.length) {
505 if (nextIdx >= $comments.length) {
441 nextIdx = 0;
506 nextIdx = 0;
442 }
507 }
443 var $next = $(klass).eq(nextIdx);
508 var $next = $(klass).eq(nextIdx);
444 var $cb = $next.closest('.cb');
509 var $cb = $next.closest('.cb');
445 $cb.removeClass('cb-collapsed');
510 $cb.removeClass('cb-collapsed');
446
511
447 var $filediffCollapseState = $cb.closest('.filediff').prev();
512 var $filediffCollapseState = $cb.closest('.filediff').prev();
448 $filediffCollapseState.prop('checked', false);
513 $filediffCollapseState.prop('checked', false);
449 $next.addClass('comment-selected');
514 $next.addClass('comment-selected');
450 scrollToElement($next);
515 scrollToElement($next);
451 return false;
516 return false;
452 };
517 };
453
518
454 this.nextComment = function(node) {
519 this.nextComment = function(node) {
455 return self.scrollToComment(node, 1);
520 return self.scrollToComment(node, 1);
456 };
521 };
457
522
458 this.prevComment = function(node) {
523 this.prevComment = function(node) {
459 return self.scrollToComment(node, -1);
524 return self.scrollToComment(node, -1);
460 };
525 };
461
526
462 this.nextOutdatedComment = function(node) {
527 this.nextOutdatedComment = function(node) {
463 return self.scrollToComment(node, 1, true);
528 return self.scrollToComment(node, 1, true);
464 };
529 };
465
530
466 this.prevOutdatedComment = function(node) {
531 this.prevOutdatedComment = function(node) {
467 return self.scrollToComment(node, -1, true);
532 return self.scrollToComment(node, -1, true);
468 };
533 };
469
534
470 this.deleteComment = function(node) {
535 this.deleteComment = function(node) {
471 if (!confirm(_gettext('Delete this comment?'))) {
536 if (!confirm(_gettext('Delete this comment?'))) {
472 return false;
537 return false;
473 }
538 }
474 var $node = $(node);
539 var $node = $(node);
475 var $td = $node.closest('td');
540 var $td = $node.closest('td');
476 var $comment = $node.closest('.comment');
541 var $comment = $node.closest('.comment');
477 var comment_id = $comment.attr('data-comment-id');
542 var comment_id = $comment.attr('data-comment-id');
478 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
543 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
479 var postData = {
544 var postData = {
480 '_method': 'delete',
545 '_method': 'delete',
481 'csrf_token': CSRF_TOKEN
546 'csrf_token': CSRF_TOKEN
482 };
547 };
483
548
484 $comment.addClass('comment-deleting');
549 $comment.addClass('comment-deleting');
485 $comment.hide('fast');
550 $comment.hide('fast');
486
551
487 var success = function(response) {
552 var success = function(response) {
488 $comment.remove();
553 $comment.remove();
489 return false;
554 return false;
490 };
555 };
491 var failure = function(data, textStatus, xhr) {
556 var failure = function(data, textStatus, xhr) {
492 alert("error processing request: " + textStatus);
557 alert("error processing request: " + textStatus);
493 $comment.show('fast');
558 $comment.show('fast');
494 $comment.removeClass('comment-deleting');
559 $comment.removeClass('comment-deleting');
495 return false;
560 return false;
496 };
561 };
497 ajaxPOST(url, postData, success, failure);
562 ajaxPOST(url, postData, success, failure);
498 };
563 };
499
564
500 this.toggleWideMode = function (node) {
565 this.toggleWideMode = function (node) {
501 if ($('#content').hasClass('wrapper')) {
566 if ($('#content').hasClass('wrapper')) {
502 $('#content').removeClass("wrapper");
567 $('#content').removeClass("wrapper");
503 $('#content').addClass("wide-mode-wrapper");
568 $('#content').addClass("wide-mode-wrapper");
504 $(node).addClass('btn-success');
569 $(node).addClass('btn-success');
505 } else {
570 } else {
506 $('#content').removeClass("wide-mode-wrapper");
571 $('#content').removeClass("wide-mode-wrapper");
507 $('#content').addClass("wrapper");
572 $('#content').addClass("wrapper");
508 $(node).removeClass('btn-success');
573 $(node).removeClass('btn-success');
509 }
574 }
510 return false;
575 return false;
511 };
576 };
512
577
513 this.toggleComments = function(node, show) {
578 this.toggleComments = function(node, show) {
514 var $filediff = $(node).closest('.filediff');
579 var $filediff = $(node).closest('.filediff');
515 if (show === true) {
580 if (show === true) {
516 $filediff.removeClass('hide-comments');
581 $filediff.removeClass('hide-comments');
517 } else if (show === false) {
582 } else if (show === false) {
518 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
583 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
519 $filediff.addClass('hide-comments');
584 $filediff.addClass('hide-comments');
520 } else {
585 } else {
521 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
586 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
522 $filediff.toggleClass('hide-comments');
587 $filediff.toggleClass('hide-comments');
523 }
588 }
524 return false;
589 return false;
525 };
590 };
526
591
527 this.toggleLineComments = function(node) {
592 this.toggleLineComments = function(node) {
528 self.toggleComments(node, true);
593 self.toggleComments(node, true);
529 var $node = $(node);
594 var $node = $(node);
530 $node.closest('tr').toggleClass('hide-line-comments');
595 $node.closest('tr').toggleClass('hide-line-comments');
531 };
596 };
532
597
533 this.createComment = function(node) {
598 this.createComment = function(node, resolutionComment) {
599 var resolvesCommentId = resolutionComment || null;
534 var $node = $(node);
600 var $node = $(node);
535 var $td = $node.closest('td');
601 var $td = $node.closest('td');
536 var $form = $td.find('.comment-inline-form');
602 var $form = $td.find('.comment-inline-form');
537
603
538 if (!$form.length) {
604 if (!$form.length) {
539 var tmpl = $('#cb-comment-inline-form-template').html();
605 var tmpl = $('#cb-comment-inline-form-template').html();
540 var $filediff = $node.closest('.filediff');
606 var $filediff = $node.closest('.filediff');
541 $filediff.removeClass('hide-comments');
607 $filediff.removeClass('hide-comments');
542 var f_path = $filediff.attr('data-f-path');
608 var f_path = $filediff.attr('data-f-path');
543 var lineno = self.getLineNumber(node);
609 var lineno = self.getLineNumber(node);
544
610
545 tmpl = tmpl.format(f_path, lineno);
611 tmpl = tmpl.format(f_path, lineno);
546 $form = $(tmpl);
612 $form = $(tmpl);
547
613
548 var $comments = $td.find('.inline-comments');
614 var $comments = $td.find('.inline-comments');
549 if (!$comments.length) {
615 if (!$comments.length) {
550 $comments = $(
616 $comments = $(
551 $('#cb-comments-inline-container-template').html());
617 $('#cb-comments-inline-container-template').html());
552 $td.append($comments);
618 $td.append($comments);
553 }
619 }
554
620
555 $td.find('.cb-comment-add-button').before($form);
621 $td.find('.cb-comment-add-button').before($form);
556
622
557 var pullRequestId = templateContext.pull_request_data.pull_request_id;
623 var pullRequestId = templateContext.pull_request_data.pull_request_id;
558 var commitId = templateContext.commit_data.commit_id;
624 var commitId = templateContext.commit_data.commit_id;
559 var _form = $form[0];
625 var _form = $($form[0]).find('form');
560 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
626
627 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false, resolvesCommentId);
561 var cm = commentForm.getCmInstance();
628 var cm = commentForm.getCmInstance();
562
629
563 // set a CUSTOM submit handler for inline comments.
630 // set a CUSTOM submit handler for inline comments.
564 commentForm.setHandleFormSubmit(function(o) {
631 commentForm.setHandleFormSubmit(function(o) {
565 var text = commentForm.cm.getValue();
632 var text = commentForm.cm.getValue();
566 var commentType = commentForm.getCommentType();
633 var commentType = commentForm.getCommentType();
634 var resolvesCommentId = commentForm.getResolvesId();
567
635
568 if (text === "") {
636 if (text === "") {
569 return;
637 return;
570 }
638 }
571
639
572 if (lineno === undefined) {
640 if (lineno === undefined) {
573 alert('missing line !');
641 alert('missing line !');
574 return;
642 return;
575 }
643 }
576 if (f_path === undefined) {
644 if (f_path === undefined) {
577 alert('missing file path !');
645 alert('missing file path !');
578 return;
646 return;
579 }
647 }
580
648
581 var excludeCancelBtn = false;
649 var excludeCancelBtn = false;
582 var submitEvent = true;
650 var submitEvent = true;
583 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
651 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
584 commentForm.cm.setOption("readOnly", true);
652 commentForm.cm.setOption("readOnly", true);
585 var postData = {
653 var postData = {
586 'text': text,
654 'text': text,
587 'f_path': f_path,
655 'f_path': f_path,
588 'line': lineno,
656 'line': lineno,
589 'comment_type': commentType,
657 'comment_type': commentType,
590 'csrf_token': CSRF_TOKEN
658 'csrf_token': CSRF_TOKEN
591 };
659 };
660 if (resolvesCommentId){
661 postData['resolves_comment_id'] = resolvesCommentId;
662 }
663
592 var submitSuccessCallback = function(json_data) {
664 var submitSuccessCallback = function(json_data) {
593 $form.remove();
665 $form.remove();
594 try {
666 try {
595 var html = json_data.rendered_text;
667 var html = json_data.rendered_text;
596 var lineno = json_data.line_no;
668 var lineno = json_data.line_no;
597 var target_id = json_data.target_id;
669 var target_id = json_data.target_id;
598
670
599 $comments.find('.cb-comment-add-button').before(html);
671 $comments.find('.cb-comment-add-button').before(html);
600
672
673 //mark visually which comment was resolved
674 if (resolvesCommentId) {
675 commentForm.markCommentResolved(resolvesCommentId);
676 }
677
601 } catch (e) {
678 } catch (e) {
602 console.error(e);
679 console.error(e);
603 }
680 }
604
681
605 // re trigger the linkification of next/prev navigation
682 // re trigger the linkification of next/prev navigation
606 linkifyComments($('.inline-comment-injected'));
683 linkifyComments($('.inline-comment-injected'));
607 timeagoActivate();
684 timeagoActivate();
608 bindDeleteCommentButtons();
685 bindDeleteCommentButtons();
609 commentForm.setActionButtonsDisabled(false);
686 commentForm.setActionButtonsDisabled(false);
610
687
611 };
688 };
612 var submitFailCallback = function(){
689 var submitFailCallback = function(){
613 commentForm.resetCommentFormState(text)
690 commentForm.resetCommentFormState(text)
614 };
691 };
615 commentForm.submitAjaxPOST(
692 commentForm.submitAjaxPOST(
616 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
693 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
617 });
694 });
618
695
696 if (resolvesCommentId){
697 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
698
699 } else {
700 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
701 }
702
619 setTimeout(function() {
703 setTimeout(function() {
620 // callbacks
704 // callbacks
621 if (cm !== undefined) {
705 if (cm !== undefined) {
622 cm.setOption('placeholder', _gettext('Leave a comment on line {0}.').format(lineno));
706 commentForm.setPlaceholder(placeholderText);
623 cm.focus();
707 cm.focus();
624 cm.refresh();
708 cm.refresh();
625 }
709 }
626 }, 10);
710 }, 10);
627
711
628 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
712 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
629 form: _form,
713 form: _form,
630 parent: $td[0],
714 parent: $td[0],
631 lineno: lineno,
715 lineno: lineno,
632 f_path: f_path}
716 f_path: f_path}
633 );
717 );
718
719 // trigger hash
720 if (resolvesCommentId){
721 var resolveAction = $(commentForm.resolvesActionId);
722 setTimeout(function() {
723 $('body, html').animate({ scrollTop: resolveAction.offset().top }, 10);
724 }, 100);
725 }
634 }
726 }
635
727
636 $form.addClass('comment-inline-form-open');
728 $form.addClass('comment-inline-form-open');
637 };
729 };
638
730
731 this.createResolutionComment = function(commentId){
732 // hide the trigger text
733 $('#resolve-comment-{0}'.format(commentId)).hide();
734
735 var comment = $('#comment-'+commentId);
736 var commentData = comment.data();
737
738 if (commentData.commentInline) {
739 var resolutionComment = true;
740 this.createComment(comment, commentId)
741 } else {
742
743 this.createComment(comment, commentId)
744
745 console.log('TODO')
746 console.log(commentId)
747 }
748
749 return false;
750 };
751
752 this.submitResolution = function(commentId){
753 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
754 var commentForm = form.get(0).CommentForm;
755
756 var cm = commentForm.getCmInstance();
757 var renderer = templateContext.visual.default_renderer;
758 if (renderer == 'rst'){
759 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
760 } else if (renderer == 'markdown') {
761 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
762 } else {
763 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
764 }
765
766 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
767 form.submit();
768 return false;
769 };
770
639 this.renderInlineComments = function(file_comments) {
771 this.renderInlineComments = function(file_comments) {
640 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
772 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
641
773
642 for (var i = 0; i < file_comments.length; i++) {
774 for (var i = 0; i < file_comments.length; i++) {
643 var box = file_comments[i];
775 var box = file_comments[i];
644
776
645 var target_id = $(box).attr('target_id');
777 var target_id = $(box).attr('target_id');
646
778
647 // actually comments with line numbers
779 // actually comments with line numbers
648 var comments = box.children;
780 var comments = box.children;
649
781
650 for (var j = 0; j < comments.length; j++) {
782 for (var j = 0; j < comments.length; j++) {
651 var data = {
783 var data = {
652 'rendered_text': comments[j].outerHTML,
784 'rendered_text': comments[j].outerHTML,
653 'line_no': $(comments[j]).attr('line'),
785 'line_no': $(comments[j]).attr('line'),
654 'target_id': target_id
786 'target_id': target_id
655 };
787 };
656 }
788 }
657 }
789 }
658
790
659 // since order of injection is random, we're now re-iterating
791 // since order of injection is random, we're now re-iterating
660 // from correct order and filling in links
792 // from correct order and filling in links
661 linkifyComments($('.inline-comment-injected'));
793 linkifyComments($('.inline-comment-injected'));
662 bindDeleteCommentButtons();
794 bindDeleteCommentButtons();
663 firefoxAnchorFix();
795 firefoxAnchorFix();
664 };
796 };
665
797
666 };
798 };
@@ -1,310 +1,353 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6 <%namespace name="base" file="/base/base.mako"/>
7
7
8 <%def name="comment_block(comment, inline=False)">
8 <%def name="comment_block(comment, inline=False)">
9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
10 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
11
11
12 <div class="comment
12 <div class="comment
13 ${'comment-inline' if inline else 'comment-general'}
13 ${'comment-inline' if inline else 'comment-general'}
14 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
14 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
15 id="comment-${comment.comment_id}"
15 id="comment-${comment.comment_id}"
16 line="${comment.line_no}"
16 line="${comment.line_no}"
17 data-comment-id="${comment.comment_id}"
17 data-comment-id="${comment.comment_id}"
18 data-comment-type="${comment.comment_type}"
19 data-comment-inline=${h.json.dumps(inline)}
18 style="${'display: none;' if outdated_at_ver else ''}">
20 style="${'display: none;' if outdated_at_ver else ''}">
19
21
20 <div class="meta">
22 <div class="meta">
21 <div class="comment-type-label tooltip">
23 <div class="comment-type-label">
22 <div class="comment-label ${comment.comment_type or 'note'}">
24 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
25 % if comment.comment_type == 'todo':
26 % if comment.resolved:
27 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
28 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
29 </div>
30 % else:
31 <div class="resolved tooltip" style="display: none">
32 <span>${comment.comment_type}</span>
33 </div>
34 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
35 ${comment.comment_type}
36 </div>
37 % endif
38 % else:
39 % if comment.resolved_comment:
40 fix
41 % else:
23 ${comment.comment_type or 'note'}
42 ${comment.comment_type or 'note'}
43 % endif
44 % endif
24 </div>
45 </div>
25 </div>
46 </div>
26
47
27 <div class="author ${'author-inline' if inline else 'author-general'}">
48 <div class="author ${'author-inline' if inline else 'author-general'}">
28 ${base.gravatar_with_user(comment.author.email, 16)}
49 ${base.gravatar_with_user(comment.author.email, 16)}
29 </div>
50 </div>
30 <div class="date">
51 <div class="date">
31 ${h.age_component(comment.modified_at, time_is_local=True)}
52 ${h.age_component(comment.modified_at, time_is_local=True)}
32 </div>
53 </div>
33 % if inline:
54 % if inline:
34 <span></span>
55 <span></span>
35 % else:
56 % else:
36 <div class="status-change">
57 <div class="status-change">
37 % if comment.pull_request:
58 % if comment.pull_request:
38 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
59 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
39 % if comment.status_change:
60 % if comment.status_change:
40 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
61 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
41 % else:
62 % else:
42 ${_('pull request #%s') % comment.pull_request.pull_request_id}
63 ${_('pull request #%s') % comment.pull_request.pull_request_id}
43 % endif
64 % endif
44 </a>
65 </a>
45 % else:
66 % else:
46 % if comment.status_change:
67 % if comment.status_change:
47 ${_('Status change on commit')}:
68 ${_('Status change on commit')}:
48 % endif
69 % endif
49 % endif
70 % endif
50 </div>
71 </div>
51 % endif
72 % endif
52
73
53 % if comment.status_change:
74 % if comment.status_change:
54 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
75 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
55 <div title="${_('Commit status')}" class="changeset-status-lbl">
76 <div title="${_('Commit status')}" class="changeset-status-lbl">
56 ${comment.status_change[0].status_lbl}
77 ${comment.status_change[0].status_lbl}
57 </div>
78 </div>
58 % endif
79 % endif
59
80
60 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
81 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
61
82
62 <div class="comment-links-block">
83 <div class="comment-links-block">
63
84
64 % if inline:
85 % if inline:
65 % if outdated_at_ver:
86 % if outdated_at_ver:
66 <div class="pr-version-inline">
87 <div class="pr-version-inline">
67 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
88 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
68 <code class="pr-version-num">
89 <code class="pr-version-num">
69 outdated ${'v{}'.format(pr_index_ver)}
90 outdated ${'v{}'.format(pr_index_ver)}
70 </code>
91 </code>
71 </a>
92 </a>
72 </div>
93 </div>
73 |
94 |
74 % endif
95 % endif
75 % else:
96 % else:
76 % if comment.pull_request_version_id and pr_index_ver:
97 % if comment.pull_request_version_id and pr_index_ver:
77 |
98 |
78 <div class="pr-version">
99 <div class="pr-version">
79 % if comment.outdated:
100 % if comment.outdated:
80 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
101 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
81 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
102 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
82 </a>
103 </a>
83 % else:
104 % else:
84 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
105 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
85 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
106 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
86 <code class="pr-version-num">
107 <code class="pr-version-num">
87 ${'v{}'.format(pr_index_ver)}
108 ${'v{}'.format(pr_index_ver)}
88 </code>
109 </code>
89 </a>
110 </a>
90 </div>
111 </div>
91 % endif
112 % endif
92 </div>
113 </div>
93 % endif
114 % endif
94 % endif
115 % endif
95
116
96 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
117 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
97 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
118 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
98 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
119 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
99 ## permissions to delete
120 ## permissions to delete
100 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
121 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
101 ## TODO: dan: add edit comment here
122 ## TODO: dan: add edit comment here
102 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
123 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
103 %else:
124 %else:
104 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
125 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
105 %endif
126 %endif
106 %else:
127 %else:
107 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
128 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
108 %endif
129 %endif
109
130
110 %if not outdated_at_ver:
131 %if not outdated_at_ver:
111 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
132 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
112 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
133 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
113 %endif
134 %endif
114
135
115 </div>
136 </div>
116 </div>
137 </div>
117 <div class="text">
138 <div class="text">
118 ${comment.render(mentions=True)|n}
139 ${comment.render(mentions=True)|n}
119 </div>
140 </div>
120
141
121 </div>
142 </div>
122 </%def>
143 </%def>
144
123 ## generate main comments
145 ## generate main comments
124 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
146 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
125 <div id="comments">
147 <div id="comments">
126 %for comment in c.comments:
148 %for comment in c.comments:
127 <div id="comment-tr-${comment.comment_id}">
149 <div id="comment-tr-${comment.comment_id}">
128 ## only render comments that are not from pull request, or from
150 ## only render comments that are not from pull request, or from
129 ## pull request and a status change
151 ## pull request and a status change
130 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
152 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
131 ${comment_block(comment)}
153 ${comment_block(comment)}
132 %endif
154 %endif
133 </div>
155 </div>
134 %endfor
156 %endfor
135 ## to anchor ajax comments
157 ## to anchor ajax comments
136 <div id="injected_page_comments"></div>
158 <div id="injected_page_comments"></div>
137 </div>
159 </div>
138 </%def>
160 </%def>
139
161
140 ## MAIN COMMENT FORM
162
141 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
163 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
142
164
143 %if is_compare:
165 ## merge status, and merge action
144 <% form_id = "comments_form_compare" %>
145 %else:
146 <% form_id = "comments_form" %>
147 %endif
148
149
150 %if is_pull_request:
166 %if is_pull_request:
151 <div class="pull-request-merge">
167 <div class="pull-request-merge">
152 %if c.allowed_to_merge:
168 %if c.allowed_to_merge:
153 <div class="pull-request-wrap">
169 <div class="pull-request-wrap">
154 <div class="pull-right">
170 <div class="pull-right">
155 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
171 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
156 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
172 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
157 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
173 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
158 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
174 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
159 ${h.end_form()}
175 ${h.end_form()}
160 </div>
176 </div>
161 </div>
177 </div>
162 %else:
178 %else:
163 <div class="pull-request-wrap">
179 <div class="pull-request-wrap">
164 <div class="pull-right">
180 <div class="pull-right">
165 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
181 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
166 </div>
182 </div>
167 </div>
183 </div>
168 %endif
184 %endif
169 </div>
185 </div>
170 %endif
186 %endif
187
171 <div class="comments">
188 <div class="comments">
172 <%
189 <%
173 if is_pull_request:
190 if is_pull_request:
174 placeholder = _('Leave a comment on this Pull Request.')
191 placeholder = _('Leave a comment on this Pull Request.')
175 elif is_compare:
192 elif is_compare:
176 placeholder = _('Leave a comment on all commits in this range.')
193 placeholder = _('Leave a comment on all commits in this range.')
177 else:
194 else:
178 placeholder = _('Leave a comment on this Commit.')
195 placeholder = _('Leave a comment on this Commit.')
179 %>
196 %>
197
180 % if c.rhodecode_user.username != h.DEFAULT_USER:
198 % if c.rhodecode_user.username != h.DEFAULT_USER:
181 <div class="comment-form ac">
199 <div class="comment-form ac">
182 ${h.secure_form(post_url, id_=form_id)}
200 ## inject form here
183 <div class="comment-area">
201 ${comment_form(form_type='general', form_id='general_comment', lineno_id='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
184 <div class="comment-area-header">
185 <ul class="nav-links clearfix">
186 <li class="active">
187 <a href="#edit-btn" tabindex="-1" id="edit-btn">${_('Write')}</a>
188 </li>
189 <li class="">
190 <a href="#preview-btn" tabindex="-1" id="preview-btn">${_('Preview')}</a>
191 </li>
192 <li class="pull-right">
193 <select class="comment-type" id="comment_type" name="comment_type">
194 % for val in c.visual.comment_types:
195 <option value="${val}">${val.upper()}</option>
196 % endfor
197 </select>
198 </li>
199 </ul>
200 </div>
202 </div>
201
203 <script type="text/javascript">
202 <div class="comment-area-write" style="display: block;">
204 // init active elements of commentForm
203 <div id="edit-container">
205 var commitId = templateContext.commit_data.commit_id;
204 <textarea id="text" name="text" class="comment-block-ta ac-input"></textarea>
206 var pullRequestId = templateContext.pull_request_data.pull_request_id;
205 </div>
207 var lineNo = 'general';
206 <div id="preview-container" class="clearfix" style="display: none;">
208 var resolvesCommitId = null;
207 <div id="preview-box" class="preview-box"></div>
208 </div>
209 </div>
210
209
211 <div class="comment-area-footer">
210 var mainCommentForm = new CommentForm(
212 <div class="toolbar">
211 "#general_comment", commitId, pullRequestId, lineNo, true, resolvesCommitId);
213 <div class="toolbar-text">
212 mainCommentForm.setPlaceholder("${placeholder}");
214 ${(_('Comments parsed using %s syntax with %s support.') % (
213 mainCommentForm.initStatusChangeSelector();
215 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
214 </script>
216 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
217 )
218 )|n}
219 </div>
220 </div>
221 </div>
222 </div>
223
215
224 <div id="comment_form_extras">
216
225 %if form_extras and isinstance(form_extras, (list, tuple)):
226 % for form_ex_el in form_extras:
227 ${form_ex_el|n}
228 % endfor
229 %endif
230 </div>
231 <div class="comment-footer">
232 %if change_status:
233 <div class="status_box">
234 <select id="change_status" name="changeset_status">
235 <option></option> # Placeholder
236 %for status,lbl in c.commit_statuses:
237 <option value="${status}" data-status="${status}">${lbl}</option>
238 %if is_pull_request and change_status and status in ('approved', 'rejected'):
239 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
240 %endif
241 %endfor
242 </select>
243 </div>
244 %endif
245 <div class="action-buttons">
246 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
247 </div>
248 </div>
249 ${h.end_form()}
250 </div>
251 % else:
217 % else:
218 ## form state when not logged in
252 <div class="comment-form ac">
219 <div class="comment-form ac">
253
220
254 <div class="comment-area">
221 <div class="comment-area">
255 <div class="comment-area-header">
222 <div class="comment-area-header">
256 <ul class="nav-links clearfix">
223 <ul class="nav-links clearfix">
257 <li class="active">
224 <li class="active">
258 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
225 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
259 </li>
226 </li>
260 <li class="">
227 <li class="">
261 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
228 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
262 </li>
229 </li>
263 </ul>
230 </ul>
264 </div>
231 </div>
265
232
266 <div class="comment-area-write" style="display: block;">
233 <div class="comment-area-write" style="display: block;">
267 <div id="edit-container">
234 <div id="edit-container">
268 <div style="padding: 40px 0">
235 <div style="padding: 40px 0">
269 ${_('You need to be logged in to leave comments.')}
236 ${_('You need to be logged in to leave comments.')}
270 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
237 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
271 </div>
238 </div>
272 </div>
239 </div>
273 <div id="preview-container" class="clearfix" style="display: none;">
240 <div id="preview-container" class="clearfix" style="display: none;">
274 <div id="preview-box" class="preview-box"></div>
241 <div id="preview-box" class="preview-box"></div>
275 </div>
242 </div>
276 </div>
243 </div>
277
244
278 <div class="comment-area-footer">
245 <div class="comment-area-footer">
279 <div class="toolbar">
246 <div class="toolbar">
280 <div class="toolbar-text">
247 <div class="toolbar-text">
281 </div>
248 </div>
282 </div>
249 </div>
283 </div>
250 </div>
284 </div>
251 </div>
285
252
286 <div class="comment-footer">
253 <div class="comment-footer">
287 </div>
254 </div>
288
255
289 </div>
256 </div>
290 % endif
257 % endif
291
258
259 <script type="text/javascript">
260 bindToggleButtons();
261 </script>
262 </div>
263 </%def>
264
265
266 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
267 ## comment injected based on assumption that user is logged in
268
269 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
270
271 <div class="comment-area">
272 <div class="comment-area-header">
273 <ul class="nav-links clearfix">
274 <li class="active">
275 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
276 </li>
277 <li class="">
278 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
279 </li>
280 <li class="pull-right">
281 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
282 % for val in c.visual.comment_types:
283 <option value="${val}">${val.upper()}</option>
284 % endfor
285 </select>
286 </li>
287 </ul>
288 </div>
289
290 <div class="comment-area-write" style="display: block;">
291 <div id="edit-container_${lineno_id}">
292 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
293 </div>
294 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
295 <div id="preview-box_${lineno_id}" class="preview-box"></div>
296 </div>
292 </div>
297 </div>
293
298
294 <script>
299 <div class="comment-area-footer">
295 // init active elements of commentForm
300 <div class="toolbar">
296 var commitId = templateContext.commit_data.commit_id;
301 <div class="toolbar-text">
297 var pullRequestId = templateContext.pull_request_data.pull_request_id;
302 ${(_('Comments parsed using %s syntax with %s support.') % (
298 var lineNo;
303 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
304 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
305 )
306 )|n}
307 </div>
308 </div>
309 </div>
310 </div>
299
311
300 var mainCommentForm = new CommentForm(
312 <div class="comment-footer">
301 "#${form_id}", commitId, pullRequestId, lineNo, true);
302
313
303 if (mainCommentForm.cm){
314 % if review_statuses:
304 mainCommentForm.cm.setOption('placeholder', "${placeholder}");
315 <div class="status_box">
305 }
316 <select id="change_status" name="changeset_status">
317 <option></option> ## Placeholder
318 % for status, lbl in review_statuses:
319 <option value="${status}" data-status="${status}">${lbl}</option>
320 %if is_pull_request and change_status and status in ('approved', 'rejected'):
321 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
322 %endif
323 % endfor
324 </select>
325 </div>
326 % endif
306
327
307 mainCommentForm.initStatusChangeSelector();
328 ## inject extra inputs into the form
308 bindToggleButtons();
329 % if form_extras and isinstance(form_extras, (list, tuple)):
309 </script>
330 <div id="comment_form_extras">
310 </%def>
331 % for form_ex_el in form_extras:
332 ${form_ex_el|n}
333 % endfor
334 </div>
335 % endif
336
337 <div class="action-buttons">
338 ## inline for has a file, and line-number together with cancel hide button.
339 % if form_type == 'inline':
340 <input type="hidden" name="f_path" value="{0}">
341 <input type="hidden" name="line" value="${lineno_id}">
342 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
343 ${_('Cancel')}
344 </button>
345 % endif
346 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
347
348 </div>
349 </div>
350
351 </form>
352
353 </%def> No newline at end of file
@@ -1,718 +1,668 b''
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2
1 <%def name="diff_line_anchor(filename, line, type)"><%
3 <%def name="diff_line_anchor(filename, line, type)"><%
2 return '%s_%s_%i' % (h.safeid(filename), type, line)
4 return '%s_%s_%i' % (h.safeid(filename), type, line)
3 %></%def>
5 %></%def>
4
6
5 <%def name="action_class(action)">
7 <%def name="action_class(action)">
6 <%
8 <%
7 return {
9 return {
8 '-': 'cb-deletion',
10 '-': 'cb-deletion',
9 '+': 'cb-addition',
11 '+': 'cb-addition',
10 ' ': 'cb-context',
12 ' ': 'cb-context',
11 }.get(action, 'cb-empty')
13 }.get(action, 'cb-empty')
12 %>
14 %>
13 </%def>
15 </%def>
14
16
15 <%def name="op_class(op_id)">
17 <%def name="op_class(op_id)">
16 <%
18 <%
17 return {
19 return {
18 DEL_FILENODE: 'deletion', # file deleted
20 DEL_FILENODE: 'deletion', # file deleted
19 BIN_FILENODE: 'warning' # binary diff hidden
21 BIN_FILENODE: 'warning' # binary diff hidden
20 }.get(op_id, 'addition')
22 }.get(op_id, 'addition')
21 %>
23 %>
22 </%def>
24 </%def>
23
25
24 <%def name="link_for(**kw)">
26 <%def name="link_for(**kw)">
25 <%
27 <%
26 new_args = request.GET.mixed()
28 new_args = request.GET.mixed()
27 new_args.update(kw)
29 new_args.update(kw)
28 return h.url('', **new_args)
30 return h.url('', **new_args)
29 %>
31 %>
30 </%def>
32 </%def>
31
33
32 <%def name="render_diffset(diffset, commit=None,
34 <%def name="render_diffset(diffset, commit=None,
33
35
34 # collapse all file diff entries when there are more than this amount of files in the diff
36 # collapse all file diff entries when there are more than this amount of files in the diff
35 collapse_when_files_over=20,
37 collapse_when_files_over=20,
36
38
37 # collapse lines in the diff when more than this amount of lines changed in the file diff
39 # collapse lines in the diff when more than this amount of lines changed in the file diff
38 lines_changed_limit=500,
40 lines_changed_limit=500,
39
41
40 # add a ruler at to the output
42 # add a ruler at to the output
41 ruler_at_chars=0,
43 ruler_at_chars=0,
42
44
43 # show inline comments
45 # show inline comments
44 use_comments=False,
46 use_comments=False,
45
47
46 # disable new comments
48 # disable new comments
47 disable_new_comments=False,
49 disable_new_comments=False,
48
50
49 # special file-comments that were deleted in previous versions
51 # special file-comments that were deleted in previous versions
50 # it's used for showing outdated comments for deleted files in a PR
52 # it's used for showing outdated comments for deleted files in a PR
51 deleted_files_comments=None
53 deleted_files_comments=None
52
54
53 )">
55 )">
54
56
55 %if use_comments:
57 %if use_comments:
56 <div id="cb-comments-inline-container-template" class="js-template">
58 <div id="cb-comments-inline-container-template" class="js-template">
57 ${inline_comments_container([])}
59 ${inline_comments_container([])}
58 </div>
60 </div>
59 <div class="js-template" id="cb-comment-inline-form-template">
61 <div class="js-template" id="cb-comment-inline-form-template">
60 <div class="comment-inline-form ac">
62 <div class="comment-inline-form ac">
61
63
62 %if c.rhodecode_user.username != h.DEFAULT_USER:
64 %if c.rhodecode_user.username != h.DEFAULT_USER:
63 ${h.form('#', method='get')}
65 ## render template for inline comments
64 <div class="comment-area">
66 ${commentblock.comment_form(form_type='inline')}
65 <div class="comment-area-header">
66 <ul class="nav-links clearfix">
67 <li class="active">
68 <a href="#edit-btn" tabindex="-1" id="edit-btn_{1}">${_('Write')}</a>
69 </li>
70 <li class="">
71 <a href="#preview-btn" tabindex="-1" id="preview-btn_{1}">${_('Preview')}</a>
72 </li>
73 <li class="pull-right">
74 <select class="comment-type" id="comment_type_{1}" name="comment_type">
75 % for val in c.visual.comment_types:
76 <option value="${val}">${val.upper()}</option>
77 % endfor
78 </select>
79 </li>
80 </ul>
81 </div>
82
83 <div class="comment-area-write" style="display: block;">
84 <div id="edit-container_{1}">
85 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
86 </div>
87 <div id="preview-container_{1}" class="clearfix" style="display: none;">
88 <div id="preview-box_{1}" class="preview-box"></div>
89 </div>
90 </div>
91
92 <div class="comment-area-footer">
93 <div class="toolbar">
94 <div class="toolbar-text">
95 ${(_('Comments parsed using %s syntax with %s support.') % (
96 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
97 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
98 )
99 )|n}
100 </div>
101 </div>
102 </div>
103 </div>
104
105 <div class="comment-footer">
106 <div class="action-buttons">
107 <input type="hidden" name="f_path" value="{0}">
108 <input type="hidden" name="line" value="{1}">
109 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
110 ${_('Cancel')}
111 </button>
112 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
113 </div>
114 ${h.end_form()}
115 </div>
116 %else:
67 %else:
117 ${h.form('', class_='inline-form comment-form-login', method='get')}
68 ${h.form('', class_='inline-form comment-form-login', method='get')}
118 <div class="pull-left">
69 <div class="pull-left">
119 <div class="comment-help pull-right">
70 <div class="comment-help pull-right">
120 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
71 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
121 </div>
72 </div>
122 </div>
73 </div>
123 <div class="comment-button pull-right">
74 <div class="comment-button pull-right">
124 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
75 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
125 ${_('Cancel')}
76 ${_('Cancel')}
126 </button>
77 </button>
127 </div>
78 </div>
128 <div class="clearfix"></div>
79 <div class="clearfix"></div>
129 ${h.end_form()}
80 ${h.end_form()}
130 %endif
81 %endif
131 </div>
82 </div>
132 </div>
83 </div>
133
84
134 %endif
85 %endif
135 <%
86 <%
136 collapse_all = len(diffset.files) > collapse_when_files_over
87 collapse_all = len(diffset.files) > collapse_when_files_over
137 %>
88 %>
138
89
139 %if c.diffmode == 'sideside':
90 %if c.diffmode == 'sideside':
140 <style>
91 <style>
141 .wrapper {
92 .wrapper {
142 max-width: 1600px !important;
93 max-width: 1600px !important;
143 }
94 }
144 </style>
95 </style>
145 %endif
96 %endif
146
97
147 %if ruler_at_chars:
98 %if ruler_at_chars:
148 <style>
99 <style>
149 .diff table.cb .cb-content:after {
100 .diff table.cb .cb-content:after {
150 content: "";
101 content: "";
151 border-left: 1px solid blue;
102 border-left: 1px solid blue;
152 position: absolute;
103 position: absolute;
153 top: 0;
104 top: 0;
154 height: 18px;
105 height: 18px;
155 opacity: .2;
106 opacity: .2;
156 z-index: 10;
107 z-index: 10;
157 //## +5 to account for diff action (+/-)
108 //## +5 to account for diff action (+/-)
158 left: ${ruler_at_chars + 5}ch;
109 left: ${ruler_at_chars + 5}ch;
159 </style>
110 </style>
160 %endif
111 %endif
161
112
162 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
113 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
163 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
114 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
164 %if commit:
115 %if commit:
165 <div class="pull-right">
116 <div class="pull-right">
166 <a class="btn tooltip" title="${_('Browse Files at revision {}').format(commit.raw_id)}" href="${h.url('files_home',repo_name=diffset.repo_name, revision=commit.raw_id, f_path='')}">
117 <a class="btn tooltip" title="${_('Browse Files at revision {}').format(commit.raw_id)}" href="${h.url('files_home',repo_name=diffset.repo_name, revision=commit.raw_id, f_path='')}">
167 ${_('Browse Files')}
118 ${_('Browse Files')}
168 </a>
119 </a>
169 </div>
120 </div>
170 %endif
121 %endif
171 <h2 class="clearinner">
122 <h2 class="clearinner">
172 %if commit:
123 %if commit:
173 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
124 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
174 ${h.age_component(commit.date)} -
125 ${h.age_component(commit.date)} -
175 %endif
126 %endif
176 %if diffset.limited_diff:
127 %if diffset.limited_diff:
177 ${_('The requested commit is too big and content was truncated.')}
128 ${_('The requested commit is too big and content was truncated.')}
178
129
179 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
130 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
180 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
131 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
181 %else:
132 %else:
182 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
133 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
183 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
134 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
184 %endif
135 %endif
185
136
186 <% at_ver = getattr(c, 'at_version_pos', None) %>
137 <% at_ver = getattr(c, 'at_version_pos', None) %>
187 % if at_ver:
138 % if at_ver:
188 <div class="pull-right">
139 <div class="pull-right">
189 ${_('Showing changes at version %d') % at_ver}
140 ${_('Showing changes at version %d') % at_ver}
190 </div>
141 </div>
191 % endif
142 % endif
192
143
193 </h2>
144 </h2>
194 </div>
145 </div>
195
146
196 %if not diffset.files:
147 %if not diffset.files:
197 <p class="empty_data">${_('No files')}</p>
148 <p class="empty_data">${_('No files')}</p>
198 %endif
149 %endif
199
150
200 <div class="filediffs">
151 <div class="filediffs">
201 %for i, filediff in enumerate(diffset.files):
152 %for i, filediff in enumerate(diffset.files):
202
153
203 <%
154 <%
204 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
155 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
205 over_lines_changed_limit = lines_changed > lines_changed_limit
156 over_lines_changed_limit = lines_changed > lines_changed_limit
206 %>
157 %>
207 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
158 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
208 <div
159 <div
209 class="filediff"
160 class="filediff"
210 data-f-path="${filediff['patch']['filename']}"
161 data-f-path="${filediff['patch']['filename']}"
211 id="a_${h.FID('', filediff['patch']['filename'])}">
162 id="a_${h.FID('', filediff['patch']['filename'])}">
212 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
163 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
213 <div class="filediff-collapse-indicator"></div>
164 <div class="filediff-collapse-indicator"></div>
214 ${diff_ops(filediff)}
165 ${diff_ops(filediff)}
215 </label>
166 </label>
216 ${diff_menu(filediff, use_comments=use_comments)}
167 ${diff_menu(filediff, use_comments=use_comments)}
217 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
168 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
218 %if not filediff.hunks:
169 %if not filediff.hunks:
219 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
170 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
220 <tr>
171 <tr>
221 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
172 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
222 %if op_id == DEL_FILENODE:
173 %if op_id == DEL_FILENODE:
223 ${_('File was deleted')}
174 ${_('File was deleted')}
224 %elif op_id == BIN_FILENODE:
175 %elif op_id == BIN_FILENODE:
225 ${_('Binary file hidden')}
176 ${_('Binary file hidden')}
226 %else:
177 %else:
227 ${op_text}
178 ${op_text}
228 %endif
179 %endif
229 </td>
180 </td>
230 </tr>
181 </tr>
231 %endfor
182 %endfor
232 %endif
183 %endif
233 %if filediff.patch['is_limited_diff']:
184 %if filediff.patch['is_limited_diff']:
234 <tr class="cb-warning cb-collapser">
185 <tr class="cb-warning cb-collapser">
235 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
186 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
236 ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
187 ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
237 </td>
188 </td>
238 </tr>
189 </tr>
239 %else:
190 %else:
240 %if over_lines_changed_limit:
191 %if over_lines_changed_limit:
241 <tr class="cb-warning cb-collapser">
192 <tr class="cb-warning cb-collapser">
242 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
193 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
243 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
194 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
244 <a href="#" class="cb-expand"
195 <a href="#" class="cb-expand"
245 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
196 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
246 </a>
197 </a>
247 <a href="#" class="cb-collapse"
198 <a href="#" class="cb-collapse"
248 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
199 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
249 </a>
200 </a>
250 </td>
201 </td>
251 </tr>
202 </tr>
252 %endif
203 %endif
253 %endif
204 %endif
254
205
255 %for hunk in filediff.hunks:
206 %for hunk in filediff.hunks:
256 <tr class="cb-hunk">
207 <tr class="cb-hunk">
257 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
208 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
258 ## TODO: dan: add ajax loading of more context here
209 ## TODO: dan: add ajax loading of more context here
259 ## <a href="#">
210 ## <a href="#">
260 <i class="icon-more"></i>
211 <i class="icon-more"></i>
261 ## </a>
212 ## </a>
262 </td>
213 </td>
263 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
214 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
264 @@
215 @@
265 -${hunk.source_start},${hunk.source_length}
216 -${hunk.source_start},${hunk.source_length}
266 +${hunk.target_start},${hunk.target_length}
217 +${hunk.target_start},${hunk.target_length}
267 ${hunk.section_header}
218 ${hunk.section_header}
268 </td>
219 </td>
269 </tr>
220 </tr>
270 %if c.diffmode == 'unified':
221 %if c.diffmode == 'unified':
271 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
222 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
272 %elif c.diffmode == 'sideside':
223 %elif c.diffmode == 'sideside':
273 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
224 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
274 %else:
225 %else:
275 <tr class="cb-line">
226 <tr class="cb-line">
276 <td>unknown diff mode</td>
227 <td>unknown diff mode</td>
277 </tr>
228 </tr>
278 %endif
229 %endif
279 %endfor
230 %endfor
280
231
281 ## outdated comments that do not fit into currently displayed lines
232 ## outdated comments that do not fit into currently displayed lines
282 % for lineno, comments in filediff.left_comments.items():
233 % for lineno, comments in filediff.left_comments.items():
283
234
284 %if c.diffmode == 'unified':
235 %if c.diffmode == 'unified':
285 <tr class="cb-line">
236 <tr class="cb-line">
286 <td class="cb-data cb-context"></td>
237 <td class="cb-data cb-context"></td>
287 <td class="cb-lineno cb-context"></td>
238 <td class="cb-lineno cb-context"></td>
288 <td class="cb-lineno cb-context"></td>
239 <td class="cb-lineno cb-context"></td>
289 <td class="cb-content cb-context">
240 <td class="cb-content cb-context">
290 ${inline_comments_container(comments)}
241 ${inline_comments_container(comments)}
291 </td>
242 </td>
292 </tr>
243 </tr>
293 %elif c.diffmode == 'sideside':
244 %elif c.diffmode == 'sideside':
294 <tr class="cb-line">
245 <tr class="cb-line">
295 <td class="cb-data cb-context"></td>
246 <td class="cb-data cb-context"></td>
296 <td class="cb-lineno cb-context"></td>
247 <td class="cb-lineno cb-context"></td>
297 <td class="cb-content cb-context"></td>
248 <td class="cb-content cb-context"></td>
298
249
299 <td class="cb-data cb-context"></td>
250 <td class="cb-data cb-context"></td>
300 <td class="cb-lineno cb-context"></td>
251 <td class="cb-lineno cb-context"></td>
301 <td class="cb-content cb-context">
252 <td class="cb-content cb-context">
302 ${inline_comments_container(comments)}
253 ${inline_comments_container(comments)}
303 </td>
254 </td>
304 </tr>
255 </tr>
305 %endif
256 %endif
306
257
307 % endfor
258 % endfor
308
259
309 </table>
260 </table>
310 </div>
261 </div>
311 %endfor
262 %endfor
312
263
313 ## outdated comments that are made for a file that has been deleted
264 ## outdated comments that are made for a file that has been deleted
314 % for filename, comments_dict in (deleted_files_comments or {}).items():
265 % for filename, comments_dict in (deleted_files_comments or {}).items():
315
266
316 <div class="filediffs filediff-outdated" style="display: none">
267 <div class="filediffs filediff-outdated" style="display: none">
317 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
268 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
318 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
269 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
319 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
270 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
320 <div class="filediff-collapse-indicator"></div>
271 <div class="filediff-collapse-indicator"></div>
321 <span class="pill">
272 <span class="pill">
322 ## file was deleted
273 ## file was deleted
323 <strong>${filename}</strong>
274 <strong>${filename}</strong>
324 </span>
275 </span>
325 <span class="pill-group" style="float: left">
276 <span class="pill-group" style="float: left">
326 ## file op, doesn't need translation
277 ## file op, doesn't need translation
327 <span class="pill" op="removed">removed in this version</span>
278 <span class="pill" op="removed">removed in this version</span>
328 </span>
279 </span>
329 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
280 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
330 <span class="pill-group" style="float: right">
281 <span class="pill-group" style="float: right">
331 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
282 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
332 </span>
283 </span>
333 </label>
284 </label>
334
285
335 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
286 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
336 <tr>
287 <tr>
337 % if c.diffmode == 'unified':
288 % if c.diffmode == 'unified':
338 <td></td>
289 <td></td>
339 %endif
290 %endif
340
291
341 <td></td>
292 <td></td>
342 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
293 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
343 ${_('File was deleted in this version, and outdated comments were made on it')}
294 ${_('File was deleted in this version, and outdated comments were made on it')}
344 </td>
295 </td>
345 </tr>
296 </tr>
346 %if c.diffmode == 'unified':
297 %if c.diffmode == 'unified':
347 <tr class="cb-line">
298 <tr class="cb-line">
348 <td class="cb-data cb-context"></td>
299 <td class="cb-data cb-context"></td>
349 <td class="cb-lineno cb-context"></td>
300 <td class="cb-lineno cb-context"></td>
350 <td class="cb-lineno cb-context"></td>
301 <td class="cb-lineno cb-context"></td>
351 <td class="cb-content cb-context">
302 <td class="cb-content cb-context">
352 ${inline_comments_container(comments_dict['comments'])}
303 ${inline_comments_container(comments_dict['comments'])}
353 </td>
304 </td>
354 </tr>
305 </tr>
355 %elif c.diffmode == 'sideside':
306 %elif c.diffmode == 'sideside':
356 <tr class="cb-line">
307 <tr class="cb-line">
357 <td class="cb-data cb-context"></td>
308 <td class="cb-data cb-context"></td>
358 <td class="cb-lineno cb-context"></td>
309 <td class="cb-lineno cb-context"></td>
359 <td class="cb-content cb-context"></td>
310 <td class="cb-content cb-context"></td>
360
311
361 <td class="cb-data cb-context"></td>
312 <td class="cb-data cb-context"></td>
362 <td class="cb-lineno cb-context"></td>
313 <td class="cb-lineno cb-context"></td>
363 <td class="cb-content cb-context">
314 <td class="cb-content cb-context">
364 ${inline_comments_container(comments_dict['comments'])}
315 ${inline_comments_container(comments_dict['comments'])}
365 </td>
316 </td>
366 </tr>
317 </tr>
367 %endif
318 %endif
368 </table>
319 </table>
369 </div>
320 </div>
370 </div>
321 </div>
371 % endfor
322 % endfor
372
323
373 </div>
324 </div>
374 </div>
325 </div>
375 </%def>
326 </%def>
376
327
377 <%def name="diff_ops(filediff)">
328 <%def name="diff_ops(filediff)">
378 <%
329 <%
379 stats = filediff['patch']['stats']
330 stats = filediff['patch']['stats']
380 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
331 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
381 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
332 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
382 %>
333 %>
383 <span class="pill">
334 <span class="pill">
384 %if filediff.source_file_path and filediff.target_file_path:
335 %if filediff.source_file_path and filediff.target_file_path:
385 %if filediff.source_file_path != filediff.target_file_path:
336 %if filediff.source_file_path != filediff.target_file_path:
386 ## file was renamed
337 ## file was renamed
387 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
338 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
388 %else:
339 %else:
389 ## file was modified
340 ## file was modified
390 <strong>${filediff.source_file_path}</strong>
341 <strong>${filediff.source_file_path}</strong>
391 %endif
342 %endif
392 %else:
343 %else:
393 %if filediff.source_file_path:
344 %if filediff.source_file_path:
394 ## file was deleted
345 ## file was deleted
395 <strong>${filediff.source_file_path}</strong>
346 <strong>${filediff.source_file_path}</strong>
396 %else:
347 %else:
397 ## file was added
348 ## file was added
398 <strong>${filediff.target_file_path}</strong>
349 <strong>${filediff.target_file_path}</strong>
399 %endif
350 %endif
400 %endif
351 %endif
401 </span>
352 </span>
402 <span class="pill-group" style="float: left">
353 <span class="pill-group" style="float: left">
403 %if filediff.patch['is_limited_diff']:
354 %if filediff.patch['is_limited_diff']:
404 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
355 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
405 %endif
356 %endif
406 %if RENAMED_FILENODE in stats['ops']:
357 %if RENAMED_FILENODE in stats['ops']:
407 <span class="pill" op="renamed">renamed</span>
358 <span class="pill" op="renamed">renamed</span>
408 %endif
359 %endif
409
360
410 %if NEW_FILENODE in stats['ops']:
361 %if NEW_FILENODE in stats['ops']:
411 <span class="pill" op="created">created</span>
362 <span class="pill" op="created">created</span>
412 %if filediff['target_mode'].startswith('120'):
363 %if filediff['target_mode'].startswith('120'):
413 <span class="pill" op="symlink">symlink</span>
364 <span class="pill" op="symlink">symlink</span>
414 %else:
365 %else:
415 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
366 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
416 %endif
367 %endif
417 %endif
368 %endif
418
369
419 %if DEL_FILENODE in stats['ops']:
370 %if DEL_FILENODE in stats['ops']:
420 <span class="pill" op="removed">removed</span>
371 <span class="pill" op="removed">removed</span>
421 %endif
372 %endif
422
373
423 %if CHMOD_FILENODE in stats['ops']:
374 %if CHMOD_FILENODE in stats['ops']:
424 <span class="pill" op="mode">
375 <span class="pill" op="mode">
425 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
376 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
426 </span>
377 </span>
427 %endif
378 %endif
428 </span>
379 </span>
429
380
430 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
381 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
431
382
432 <span class="pill-group" style="float: right">
383 <span class="pill-group" style="float: right">
433 %if BIN_FILENODE in stats['ops']:
384 %if BIN_FILENODE in stats['ops']:
434 <span class="pill" op="binary">binary</span>
385 <span class="pill" op="binary">binary</span>
435 %if MOD_FILENODE in stats['ops']:
386 %if MOD_FILENODE in stats['ops']:
436 <span class="pill" op="modified">modified</span>
387 <span class="pill" op="modified">modified</span>
437 %endif
388 %endif
438 %endif
389 %endif
439 %if stats['added']:
390 %if stats['added']:
440 <span class="pill" op="added">+${stats['added']}</span>
391 <span class="pill" op="added">+${stats['added']}</span>
441 %endif
392 %endif
442 %if stats['deleted']:
393 %if stats['deleted']:
443 <span class="pill" op="deleted">-${stats['deleted']}</span>
394 <span class="pill" op="deleted">-${stats['deleted']}</span>
444 %endif
395 %endif
445 </span>
396 </span>
446
397
447 </%def>
398 </%def>
448
399
449 <%def name="nice_mode(filemode)">
400 <%def name="nice_mode(filemode)">
450 ${filemode.startswith('100') and filemode[3:] or filemode}
401 ${filemode.startswith('100') and filemode[3:] or filemode}
451 </%def>
402 </%def>
452
403
453 <%def name="diff_menu(filediff, use_comments=False)">
404 <%def name="diff_menu(filediff, use_comments=False)">
454 <div class="filediff-menu">
405 <div class="filediff-menu">
455 %if filediff.diffset.source_ref:
406 %if filediff.diffset.source_ref:
456 %if filediff.patch['operation'] in ['D', 'M']:
407 %if filediff.patch['operation'] in ['D', 'M']:
457 <a
408 <a
458 class="tooltip"
409 class="tooltip"
459 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
410 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
460 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
411 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
461 >
412 >
462 ${_('Show file before')}
413 ${_('Show file before')}
463 </a> |
414 </a> |
464 %else:
415 %else:
465 <span
416 <span
466 class="tooltip"
417 class="tooltip"
467 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
418 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
468 >
419 >
469 ${_('Show file before')}
420 ${_('Show file before')}
470 </span> |
421 </span> |
471 %endif
422 %endif
472 %if filediff.patch['operation'] in ['A', 'M']:
423 %if filediff.patch['operation'] in ['A', 'M']:
473 <a
424 <a
474 class="tooltip"
425 class="tooltip"
475 href="${h.url('files_home',repo_name=filediff.diffset.source_repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
426 href="${h.url('files_home',repo_name=filediff.diffset.source_repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
476 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
427 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
477 >
428 >
478 ${_('Show file after')}
429 ${_('Show file after')}
479 </a> |
430 </a> |
480 %else:
431 %else:
481 <span
432 <span
482 class="tooltip"
433 class="tooltip"
483 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
434 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
484 >
435 >
485 ${_('Show file after')}
436 ${_('Show file after')}
486 </span> |
437 </span> |
487 %endif
438 %endif
488 <a
439 <a
489 class="tooltip"
440 class="tooltip"
490 title="${h.tooltip(_('Raw diff'))}"
441 title="${h.tooltip(_('Raw diff'))}"
491 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw')}"
442 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw')}"
492 >
443 >
493 ${_('Raw diff')}
444 ${_('Raw diff')}
494 </a> |
445 </a> |
495 <a
446 <a
496 class="tooltip"
447 class="tooltip"
497 title="${h.tooltip(_('Download diff'))}"
448 title="${h.tooltip(_('Download diff'))}"
498 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download')}"
449 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download')}"
499 >
450 >
500 ${_('Download diff')}
451 ${_('Download diff')}
501 </a>
452 </a>
502 % if use_comments:
453 % if use_comments:
503 |
454 |
504 % endif
455 % endif
505
456
506 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
457 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
507 %if hasattr(c, 'ignorews_url'):
458 %if hasattr(c, 'ignorews_url'):
508 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
459 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
509 %endif
460 %endif
510 %if hasattr(c, 'context_url'):
461 %if hasattr(c, 'context_url'):
511 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
462 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
512 %endif
463 %endif
513
464
514 %if use_comments:
465 %if use_comments:
515 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
466 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
516 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
467 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
517 </a>
468 </a>
518 %endif
469 %endif
519 %endif
470 %endif
520 </div>
471 </div>
521 </%def>
472 </%def>
522
473
523
474
524 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
525 <%def name="inline_comments_container(comments)">
475 <%def name="inline_comments_container(comments)">
526 <div class="inline-comments">
476 <div class="inline-comments">
527 %for comment in comments:
477 %for comment in comments:
528 ${commentblock.comment_block(comment, inline=True)}
478 ${commentblock.comment_block(comment, inline=True)}
529 %endfor
479 %endfor
530
480
531 % if comments and comments[-1].outdated:
481 % if comments and comments[-1].outdated:
532 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
482 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
533 style="display: none;}">
483 style="display: none;}">
534 ${_('Add another comment')}
484 ${_('Add another comment')}
535 </span>
485 </span>
536 % else:
486 % else:
537 <span onclick="return Rhodecode.comments.createComment(this)"
487 <span onclick="return Rhodecode.comments.createComment(this)"
538 class="btn btn-secondary cb-comment-add-button">
488 class="btn btn-secondary cb-comment-add-button">
539 ${_('Add another comment')}
489 ${_('Add another comment')}
540 </span>
490 </span>
541 % endif
491 % endif
542
492
543 </div>
493 </div>
544 </%def>
494 </%def>
545
495
546
496
547 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
497 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
548 %for i, line in enumerate(hunk.sideside):
498 %for i, line in enumerate(hunk.sideside):
549 <%
499 <%
550 old_line_anchor, new_line_anchor = None, None
500 old_line_anchor, new_line_anchor = None, None
551 if line.original.lineno:
501 if line.original.lineno:
552 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
502 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
553 if line.modified.lineno:
503 if line.modified.lineno:
554 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
504 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
555 %>
505 %>
556
506
557 <tr class="cb-line">
507 <tr class="cb-line">
558 <td class="cb-data ${action_class(line.original.action)}"
508 <td class="cb-data ${action_class(line.original.action)}"
559 data-line-number="${line.original.lineno}"
509 data-line-number="${line.original.lineno}"
560 >
510 >
561 <div>
511 <div>
562 %if line.original.comments:
512 %if line.original.comments:
563 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
513 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
564 %endif
514 %endif
565 </div>
515 </div>
566 </td>
516 </td>
567 <td class="cb-lineno ${action_class(line.original.action)}"
517 <td class="cb-lineno ${action_class(line.original.action)}"
568 data-line-number="${line.original.lineno}"
518 data-line-number="${line.original.lineno}"
569 %if old_line_anchor:
519 %if old_line_anchor:
570 id="${old_line_anchor}"
520 id="${old_line_anchor}"
571 %endif
521 %endif
572 >
522 >
573 %if line.original.lineno:
523 %if line.original.lineno:
574 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
524 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
575 %endif
525 %endif
576 </td>
526 </td>
577 <td class="cb-content ${action_class(line.original.action)}"
527 <td class="cb-content ${action_class(line.original.action)}"
578 data-line-number="o${line.original.lineno}"
528 data-line-number="o${line.original.lineno}"
579 >
529 >
580 %if use_comments and line.original.lineno:
530 %if use_comments and line.original.lineno:
581 ${render_add_comment_button()}
531 ${render_add_comment_button()}
582 %endif
532 %endif
583 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
533 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
584 %if use_comments and line.original.lineno and line.original.comments:
534 %if use_comments and line.original.lineno and line.original.comments:
585 ${inline_comments_container(line.original.comments)}
535 ${inline_comments_container(line.original.comments)}
586 %endif
536 %endif
587 </td>
537 </td>
588 <td class="cb-data ${action_class(line.modified.action)}"
538 <td class="cb-data ${action_class(line.modified.action)}"
589 data-line-number="${line.modified.lineno}"
539 data-line-number="${line.modified.lineno}"
590 >
540 >
591 <div>
541 <div>
592 %if line.modified.comments:
542 %if line.modified.comments:
593 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
543 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
594 %endif
544 %endif
595 </div>
545 </div>
596 </td>
546 </td>
597 <td class="cb-lineno ${action_class(line.modified.action)}"
547 <td class="cb-lineno ${action_class(line.modified.action)}"
598 data-line-number="${line.modified.lineno}"
548 data-line-number="${line.modified.lineno}"
599 %if new_line_anchor:
549 %if new_line_anchor:
600 id="${new_line_anchor}"
550 id="${new_line_anchor}"
601 %endif
551 %endif
602 >
552 >
603 %if line.modified.lineno:
553 %if line.modified.lineno:
604 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
554 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
605 %endif
555 %endif
606 </td>
556 </td>
607 <td class="cb-content ${action_class(line.modified.action)}"
557 <td class="cb-content ${action_class(line.modified.action)}"
608 data-line-number="n${line.modified.lineno}"
558 data-line-number="n${line.modified.lineno}"
609 >
559 >
610 %if use_comments and line.modified.lineno:
560 %if use_comments and line.modified.lineno:
611 ${render_add_comment_button()}
561 ${render_add_comment_button()}
612 %endif
562 %endif
613 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
563 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
614 %if use_comments and line.modified.lineno and line.modified.comments:
564 %if use_comments and line.modified.lineno and line.modified.comments:
615 ${inline_comments_container(line.modified.comments)}
565 ${inline_comments_container(line.modified.comments)}
616 %endif
566 %endif
617 </td>
567 </td>
618 </tr>
568 </tr>
619 %endfor
569 %endfor
620 </%def>
570 </%def>
621
571
622
572
623 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
573 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
624 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
574 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
625 <%
575 <%
626 old_line_anchor, new_line_anchor = None, None
576 old_line_anchor, new_line_anchor = None, None
627 if old_line_no:
577 if old_line_no:
628 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
578 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
629 if new_line_no:
579 if new_line_no:
630 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
580 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
631 %>
581 %>
632 <tr class="cb-line">
582 <tr class="cb-line">
633 <td class="cb-data ${action_class(action)}">
583 <td class="cb-data ${action_class(action)}">
634 <div>
584 <div>
635 %if comments:
585 %if comments:
636 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
586 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
637 %endif
587 %endif
638 </div>
588 </div>
639 </td>
589 </td>
640 <td class="cb-lineno ${action_class(action)}"
590 <td class="cb-lineno ${action_class(action)}"
641 data-line-number="${old_line_no}"
591 data-line-number="${old_line_no}"
642 %if old_line_anchor:
592 %if old_line_anchor:
643 id="${old_line_anchor}"
593 id="${old_line_anchor}"
644 %endif
594 %endif
645 >
595 >
646 %if old_line_anchor:
596 %if old_line_anchor:
647 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
597 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
648 %endif
598 %endif
649 </td>
599 </td>
650 <td class="cb-lineno ${action_class(action)}"
600 <td class="cb-lineno ${action_class(action)}"
651 data-line-number="${new_line_no}"
601 data-line-number="${new_line_no}"
652 %if new_line_anchor:
602 %if new_line_anchor:
653 id="${new_line_anchor}"
603 id="${new_line_anchor}"
654 %endif
604 %endif
655 >
605 >
656 %if new_line_anchor:
606 %if new_line_anchor:
657 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
607 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
658 %endif
608 %endif
659 </td>
609 </td>
660 <td class="cb-content ${action_class(action)}"
610 <td class="cb-content ${action_class(action)}"
661 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
611 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
662 >
612 >
663 %if use_comments:
613 %if use_comments:
664 ${render_add_comment_button()}
614 ${render_add_comment_button()}
665 %endif
615 %endif
666 <span class="cb-code">${action} ${content or '' | n}</span>
616 <span class="cb-code">${action} ${content or '' | n}</span>
667 %if use_comments and comments:
617 %if use_comments and comments:
668 ${inline_comments_container(comments)}
618 ${inline_comments_container(comments)}
669 %endif
619 %endif
670 </td>
620 </td>
671 </tr>
621 </tr>
672 %endfor
622 %endfor
673 </%def>
623 </%def>
674
624
675 <%def name="render_add_comment_button()">
625 <%def name="render_add_comment_button()">
676 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
626 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
677 <span><i class="icon-comment"></i></span>
627 <span><i class="icon-comment"></i></span>
678 </button>
628 </button>
679 </%def>
629 </%def>
680
630
681 <%def name="render_diffset_menu()">
631 <%def name="render_diffset_menu()">
682
632
683 <div class="diffset-menu clearinner">
633 <div class="diffset-menu clearinner">
684 <div class="pull-right">
634 <div class="pull-right">
685 <div class="btn-group">
635 <div class="btn-group">
686
636
687 <a
637 <a
688 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
638 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
689 title="${_('View side by side')}"
639 title="${_('View side by side')}"
690 href="${h.url_replace(diffmode='sideside')}">
640 href="${h.url_replace(diffmode='sideside')}">
691 <span>${_('Side by Side')}</span>
641 <span>${_('Side by Side')}</span>
692 </a>
642 </a>
693 <a
643 <a
694 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
644 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
695 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
645 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
696 <span>${_('Unified')}</span>
646 <span>${_('Unified')}</span>
697 </a>
647 </a>
698 </div>
648 </div>
699 </div>
649 </div>
700
650
701 <div class="pull-left">
651 <div class="pull-left">
702 <div class="btn-group">
652 <div class="btn-group">
703 <a
653 <a
704 class="btn"
654 class="btn"
705 href="#"
655 href="#"
706 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
656 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
707 <a
657 <a
708 class="btn"
658 class="btn"
709 href="#"
659 href="#"
710 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
660 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
711 <a
661 <a
712 class="btn"
662 class="btn"
713 href="#"
663 href="#"
714 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
664 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
715 </div>
665 </div>
716 </div>
666 </div>
717 </div>
667 </div>
718 </%def>
668 </%def>
General Comments 0
You need to be logged in to leave comments. Login now