##// END OF EJS Templates
autocomplete: add flags to show/hide active users, and also display this information...
marcink -
r222:c6ddf19a default
parent child Browse files
Show More
@@ -1,277 +1,284 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Home controller for RhodeCode Enterprise
23 23 """
24 24
25 25 import logging
26 26 import time
27 27 import re
28 28
29 29 from pylons import tmpl_context as c, request, url, config
30 30 from pylons.i18n.translation import _
31 31 from sqlalchemy.sql import func
32 32
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasPermissionAllDecorator, AuthUser,
35 35 HasRepoGroupPermissionAnyDecorator, XHRRequired)
36 36 from rhodecode.lib.base import BaseController, render
37 37 from rhodecode.lib.index import searcher_from_config
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.utils import jsonify
40 40 from rhodecode.lib.utils2 import safe_unicode
41 41 from rhodecode.model.db import Repository, RepoGroup
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.repo_group import RepoGroupModel
44 44 from rhodecode.model.scm import RepoList, RepoGroupList
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class HomeController(BaseController):
51 51 def __before__(self):
52 52 super(HomeController, self).__before__()
53 53
54 54 def ping(self):
55 55 """
56 56 Ping, doesn't require login, good for checking out the platform
57 57 """
58 58 instance_id = getattr(c, 'rhodecode_instanceid', '')
59 59 return 'pong[%s] => %s' % (instance_id, self.ip_addr,)
60 60
61 61 @LoginRequired()
62 62 @HasPermissionAllDecorator('hg.admin')
63 63 def error_test(self):
64 64 """
65 65 Test exception handling and emails on errors
66 66 """
67 67 class TestException(Exception):
68 68 pass
69 69
70 70 msg = ('RhodeCode Enterprise %s test exception. Generation time: %s'
71 71 % (c.rhodecode_name, time.time()))
72 72 raise TestException(msg)
73 73
74 74 def _get_groups_and_repos(self, repo_group_id=None):
75 75 # repo groups groups
76 76 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
77 77 _perms = ['group.read', 'group.write', 'group.admin']
78 78 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
79 79 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
80 80 repo_group_list=repo_group_list_acl, admin=False)
81 81
82 82 # repositories
83 83 repo_list = Repository.get_all_repos(group_id=repo_group_id)
84 84 _perms = ['repository.read', 'repository.write', 'repository.admin']
85 85 repo_list_acl = RepoList(repo_list, perm_set=_perms)
86 86 repo_data = RepoModel().get_repos_as_dict(
87 87 repo_list=repo_list_acl, admin=False)
88 88
89 89 return repo_data, repo_group_data
90 90
91 91 @LoginRequired()
92 92 def index(self):
93 93 c.repo_group = None
94 94
95 95 repo_data, repo_group_data = self._get_groups_and_repos()
96 96 # json used to render the grids
97 97 c.repos_data = json.dumps(repo_data)
98 98 c.repo_groups_data = json.dumps(repo_group_data)
99 99
100 100 return render('/index.html')
101 101
102 102 @LoginRequired()
103 103 @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write',
104 104 'group.admin')
105 105 def index_repo_group(self, group_name):
106 106 """GET /repo_group_name: Show a specific item"""
107 107 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
108 108 repo_data, repo_group_data = self._get_groups_and_repos(
109 109 c.repo_group.group_id)
110 110
111 111 # json used to render the grids
112 112 c.repos_data = json.dumps(repo_data)
113 113 c.repo_groups_data = json.dumps(repo_group_data)
114 114
115 115 return render('index_repo_group.html')
116 116
117 117 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
118 118 query = Repository.query()\
119 119 .order_by(func.length(Repository.repo_name))\
120 120 .order_by(Repository.repo_name)
121 121
122 122 if repo_type:
123 123 query = query.filter(Repository.repo_type == repo_type)
124 124
125 125 if name_contains:
126 126 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
127 127 query = query.filter(
128 128 Repository.repo_name.ilike(ilike_expression))
129 129 query = query.limit(limit)
130 130
131 131 all_repos = query.all()
132 132 repo_iter = self.scm_model.get_repos(all_repos)
133 133 return [
134 134 {
135 135 'id': obj['name'],
136 136 'text': obj['name'],
137 137 'type': 'repo',
138 138 'obj': obj['dbrepo'],
139 139 'url': url('summary_home', repo_name=obj['name'])
140 140 }
141 141 for obj in repo_iter]
142 142
143 143 def _get_repo_group_list(self, name_contains=None, limit=20):
144 144 query = RepoGroup.query()\
145 145 .order_by(func.length(RepoGroup.group_name))\
146 146 .order_by(RepoGroup.group_name)
147 147
148 148 if name_contains:
149 149 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
150 150 query = query.filter(
151 151 RepoGroup.group_name.ilike(ilike_expression))
152 152 query = query.limit(limit)
153 153
154 154 all_groups = query.all()
155 155 repo_groups_iter = self.scm_model.get_repo_groups(all_groups)
156 156 return [
157 157 {
158 158 'id': obj.group_name,
159 159 'text': obj.group_name,
160 160 'type': 'group',
161 161 'obj': {},
162 162 'url': url('repo_group_home', group_name=obj.group_name)
163 163 }
164 164 for obj in repo_groups_iter]
165 165
166 166 def _get_hash_commit_list(self, hash_starts_with=None, limit=20):
167 167 if not hash_starts_with or len(hash_starts_with) < 3:
168 168 return []
169 169
170 170 commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
171 171
172 172 if len(commit_hashes) != 1:
173 173 return []
174 174
175 175 commit_hash_prefix = commit_hashes[0]
176 176
177 177 auth_user = AuthUser(
178 178 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
179 179 searcher = searcher_from_config(config)
180 180 result = searcher.search(
181 181 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user)
182 182
183 183 return [
184 184 {
185 185 'id': entry['commit_id'],
186 186 'text': entry['commit_id'],
187 187 'type': 'commit',
188 188 'obj': {'repo': entry['repository']},
189 189 'url': url('changeset_home',
190 190 repo_name=entry['repository'], revision=entry['commit_id'])
191 191 }
192 192 for entry in result['results']]
193 193
194 194 @LoginRequired()
195 195 @XHRRequired()
196 196 @jsonify
197 197 def goto_switcher_data(self):
198 198 query = request.GET.get('query')
199 199 log.debug('generating goto switcher list, query %s', query)
200 200
201 201 res = []
202 202 repo_groups = self._get_repo_group_list(query)
203 203 if repo_groups:
204 204 res.append({
205 205 'text': _('Groups'),
206 206 'children': repo_groups
207 207 })
208 208
209 209 repos = self._get_repo_list(query)
210 210 if repos:
211 211 res.append({
212 212 'text': _('Repositories'),
213 213 'children': repos
214 214 })
215 215
216 216 commits = self._get_hash_commit_list(query)
217 217 if commits:
218 218 unique_repos = {}
219 219 for commit in commits:
220 220 unique_repos.setdefault(commit['obj']['repo'], []
221 221 ).append(commit)
222 222
223 223 for repo in unique_repos:
224 224 res.append({
225 225 'text': _('Commits in %(repo)s') % {'repo': repo},
226 226 'children': unique_repos[repo]
227 227 })
228 228
229 229 data = {
230 230 'more': False,
231 231 'results': res
232 232 }
233 233 return data
234 234
235 235 @LoginRequired()
236 236 @XHRRequired()
237 237 @jsonify
238 238 def repo_list_data(self):
239 239 query = request.GET.get('query')
240 240 repo_type = request.GET.get('repo_type')
241 241 log.debug('generating repo list, query:%s', query)
242 242
243 243 res = []
244 244 repos = self._get_repo_list(query, repo_type=repo_type)
245 245 if repos:
246 246 res.append({
247 247 'text': _('Repositories'),
248 248 'children': repos
249 249 })
250 250
251 251 data = {
252 252 'more': False,
253 253 'results': res
254 254 }
255 255 return data
256 256
257 257 @LoginRequired()
258 258 @XHRRequired()
259 259 @jsonify
260 260 def user_autocomplete_data(self):
261 261 query = request.GET.get('query')
262 262
263 263 repo_model = RepoModel()
264 264 _users = repo_model.get_users(name_contains=query)
265 265
266 266 if request.GET.get('user_groups'):
267 267 # extend with user groups
268 268 _user_groups = repo_model.get_user_groups(name_contains=query)
269 269 _users = _users + _user_groups
270 270
271 271 return {'suggestions': _users}
272 272
273 273 @LoginRequired()
274 274 @XHRRequired()
275 275 @jsonify
276 276 def user_group_autocomplete_data(self):
277 return {'suggestions': []}
277 query = request.GET.get('query')
278
279 repo_model = RepoModel()
280 _user_groups = repo_model.get_user_groups(name_contains=query)
281 _user_groups = _user_groups
282
283 return {'suggestions': _user_groups}
284
@@ -1,918 +1,924 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Repository model for rhodecode
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import re
28 28 import shutil
29 29 import time
30 30 import traceback
31 31 from datetime import datetime
32 32
33 33 from sqlalchemy.sql import func
34 34 from sqlalchemy.sql.expression import true, or_
35 35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 36
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib.auth import HasUserGroupPermissionAny
39 39 from rhodecode.lib.caching_query import FromCache
40 40 from rhodecode.lib.exceptions import AttachedForksError
41 41 from rhodecode.lib.hooks_base import log_delete_repository
42 42 from rhodecode.lib.utils import make_db_config
43 43 from rhodecode.lib.utils2 import (
44 44 safe_str, safe_unicode, remove_prefix, obfuscate_url_pw,
45 45 get_current_rhodecode_user, safe_int, datetime_to_time, action_logger_generic)
46 46 from rhodecode.lib.vcs.backends import get_backend
47 47 from rhodecode.model import BaseModel
48 48 from rhodecode.model.db import (
49 49 Repository, UserRepoToPerm, UserGroupRepoToPerm, UserRepoGroupToPerm,
50 50 UserGroupRepoGroupToPerm, User, Permission, Statistics, UserGroup,
51 51 RepoGroup, RepositoryField)
52 52 from rhodecode.model.scm import UserGroupList
53 53 from rhodecode.model.settings import VcsSettingsModel
54 54
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 class RepoModel(BaseModel):
60 60
61 61 cls = Repository
62 62
63 63 def _get_user_group(self, users_group):
64 64 return self._get_instance(UserGroup, users_group,
65 65 callback=UserGroup.get_by_group_name)
66 66
67 67 def _get_repo_group(self, repo_group):
68 68 return self._get_instance(RepoGroup, repo_group,
69 69 callback=RepoGroup.get_by_group_name)
70 70
71 71 def _create_default_perms(self, repository, private):
72 72 # create default permission
73 73 default = 'repository.read'
74 74 def_user = User.get_default_user()
75 75 for p in def_user.user_perms:
76 76 if p.permission.permission_name.startswith('repository.'):
77 77 default = p.permission.permission_name
78 78 break
79 79
80 80 default_perm = 'repository.none' if private else default
81 81
82 82 repo_to_perm = UserRepoToPerm()
83 83 repo_to_perm.permission = Permission.get_by_key(default_perm)
84 84
85 85 repo_to_perm.repository = repository
86 86 repo_to_perm.user_id = def_user.user_id
87 87
88 88 return repo_to_perm
89 89
90 90 @LazyProperty
91 91 def repos_path(self):
92 92 """
93 93 Gets the repositories root path from database
94 94 """
95 95 settings_model = VcsSettingsModel(sa=self.sa)
96 96 return settings_model.get_repos_location()
97 97
98 98 def get(self, repo_id, cache=False):
99 99 repo = self.sa.query(Repository) \
100 100 .filter(Repository.repo_id == repo_id)
101 101
102 102 if cache:
103 103 repo = repo.options(FromCache("sql_cache_short",
104 104 "get_repo_%s" % repo_id))
105 105 return repo.scalar()
106 106
107 107 def get_repo(self, repository):
108 108 return self._get_repo(repository)
109 109
110 110 def get_by_repo_name(self, repo_name, cache=False):
111 111 repo = self.sa.query(Repository) \
112 112 .filter(Repository.repo_name == repo_name)
113 113
114 114 if cache:
115 115 repo = repo.options(FromCache("sql_cache_short",
116 116 "get_repo_%s" % repo_name))
117 117 return repo.scalar()
118 118
119 119 def _extract_id_from_repo_name(self, repo_name):
120 120 if repo_name.startswith('/'):
121 121 repo_name = repo_name.lstrip('/')
122 122 by_id_match = re.match(r'^_(\d{1,})', repo_name)
123 123 if by_id_match:
124 124 return by_id_match.groups()[0]
125 125
126 126 def get_repo_by_id(self, repo_name):
127 127 """
128 128 Extracts repo_name by id from special urls.
129 129 Example url is _11/repo_name
130 130
131 131 :param repo_name:
132 132 :return: repo object if matched else None
133 133 """
134 134 try:
135 135 _repo_id = self._extract_id_from_repo_name(repo_name)
136 136 if _repo_id:
137 137 return self.get(_repo_id)
138 138 except Exception:
139 139 log.exception('Failed to extract repo_name from URL')
140 140
141 141 return None
142 142
143 def get_users(self, name_contains=None, limit=20):
143 def get_users(self, name_contains=None, limit=20, only_active=True):
144 144 # TODO: mikhail: move this method to the UserModel.
145 145 query = self.sa.query(User)
146 if only_active:
146 147 query = query.filter(User.active == true())
148
147 149 if name_contains:
148 150 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
149 151 query = query.filter(
150 152 or_(
151 153 User.name.ilike(ilike_expression),
152 154 User.lastname.ilike(ilike_expression),
153 155 User.username.ilike(ilike_expression)
154 156 )
155 157 )
156 158 query = query.limit(limit)
157 159 users = query.all()
158 160
159 161 _users = [
160 162 {
161 163 'id': user.user_id,
162 164 'first_name': user.name,
163 165 'last_name': user.lastname,
164 166 'username': user.username,
165 167 'icon_link': h.gravatar_url(user.email, 14),
166 168 'value_display': h.person(user.email),
167 169 'value': user.username,
168 'value_type': 'user'
170 'value_type': 'user',
171 'active': user.active,
169 172 }
170 173 for user in users
171 174 ]
172 175 return _users
173 176
174 def get_user_groups(self, name_contains=None, limit=20):
177 def get_user_groups(self, name_contains=None, limit=20, only_active=True):
175 178 # TODO: mikhail: move this method to the UserGroupModel.
176 179 query = self.sa.query(UserGroup)
180 if only_active:
177 181 query = query.filter(UserGroup.users_group_active == true())
182
178 183 if name_contains:
179 184 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
180 185 query = query.filter(
181 186 UserGroup.users_group_name.ilike(ilike_expression))\
182 187 .order_by(func.length(UserGroup.users_group_name))\
183 188 .order_by(UserGroup.users_group_name)
184 189
185 190 query = query.limit(limit)
186 191 user_groups = query.all()
187 192 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
188 193 user_groups = UserGroupList(user_groups, perm_set=perm_set)
189 194
190 195 _groups = [
191 196 {
192 197 'id': group.users_group_id,
193 198 # TODO: marcink figure out a way to generate the url for the
194 199 # icon
195 200 'icon_link': '',
196 201 'value_display': 'Group: %s (%d members)' % (
197 202 group.users_group_name, len(group.members),),
198 203 'value': group.users_group_name,
199 'value_type': 'user_group'
204 'value_type': 'user_group',
205 'active': group.users_group_active,
200 206 }
201 207 for group in user_groups
202 208 ]
203 209 return _groups
204 210
205 211 @classmethod
206 212 def update_repoinfo(cls, repositories=None):
207 213 if not repositories:
208 214 repositories = Repository.getAll()
209 215 for repo in repositories:
210 216 repo.update_commit_cache()
211 217
212 218 def get_repos_as_dict(self, repo_list=None, admin=False,
213 219 super_user_actions=False):
214 220
215 221 from rhodecode.lib.utils import PartialRenderer
216 222 _render = PartialRenderer('data_table/_dt_elements.html')
217 223 c = _render.c
218 224
219 225 def quick_menu(repo_name):
220 226 return _render('quick_menu', repo_name)
221 227
222 228 def repo_lnk(name, rtype, rstate, private, fork_of):
223 229 return _render('repo_name', name, rtype, rstate, private, fork_of,
224 230 short_name=not admin, admin=False)
225 231
226 232 def last_change(last_change):
227 233 return _render("last_change", last_change)
228 234
229 235 def rss_lnk(repo_name):
230 236 return _render("rss", repo_name)
231 237
232 238 def atom_lnk(repo_name):
233 239 return _render("atom", repo_name)
234 240
235 241 def last_rev(repo_name, cs_cache):
236 242 return _render('revision', repo_name, cs_cache.get('revision'),
237 243 cs_cache.get('raw_id'), cs_cache.get('author'),
238 244 cs_cache.get('message'))
239 245
240 246 def desc(desc):
241 247 if c.visual.stylify_metatags:
242 248 return h.urlify_text(h.escaped_stylize(h.truncate(desc, 60)))
243 249 else:
244 250 return h.urlify_text(h.html_escape(h.truncate(desc, 60)))
245 251
246 252 def state(repo_state):
247 253 return _render("repo_state", repo_state)
248 254
249 255 def repo_actions(repo_name):
250 256 return _render('repo_actions', repo_name, super_user_actions)
251 257
252 258 def user_profile(username):
253 259 return _render('user_profile', username)
254 260
255 261 repos_data = []
256 262 for repo in repo_list:
257 263 cs_cache = repo.changeset_cache
258 264 row = {
259 265 "menu": quick_menu(repo.repo_name),
260 266
261 267 "name": repo_lnk(repo.repo_name, repo.repo_type,
262 268 repo.repo_state, repo.private, repo.fork),
263 269 "name_raw": repo.repo_name.lower(),
264 270
265 271 "last_change": last_change(repo.last_db_change),
266 272 "last_change_raw": datetime_to_time(repo.last_db_change),
267 273
268 274 "last_changeset": last_rev(repo.repo_name, cs_cache),
269 275 "last_changeset_raw": cs_cache.get('revision'),
270 276
271 277 "desc": desc(repo.description),
272 278 "owner": user_profile(repo.user.username),
273 279
274 280 "state": state(repo.repo_state),
275 281 "rss": rss_lnk(repo.repo_name),
276 282
277 283 "atom": atom_lnk(repo.repo_name),
278 284 }
279 285 if admin:
280 286 row.update({
281 287 "action": repo_actions(repo.repo_name),
282 288 })
283 289 repos_data.append(row)
284 290
285 291 return repos_data
286 292
287 293 def _get_defaults(self, repo_name):
288 294 """
289 295 Gets information about repository, and returns a dict for
290 296 usage in forms
291 297
292 298 :param repo_name:
293 299 """
294 300
295 301 repo_info = Repository.get_by_repo_name(repo_name)
296 302
297 303 if repo_info is None:
298 304 return None
299 305
300 306 defaults = repo_info.get_dict()
301 307 defaults['repo_name'] = repo_info.just_name
302 308
303 309 groups = repo_info.groups_with_parents
304 310 parent_group = groups[-1] if groups else None
305 311
306 312 # we use -1 as this is how in HTML, we mark an empty group
307 313 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
308 314
309 315 keys_to_process = (
310 316 {'k': 'repo_type', 'strip': False},
311 317 {'k': 'repo_enable_downloads', 'strip': True},
312 318 {'k': 'repo_description', 'strip': True},
313 319 {'k': 'repo_enable_locking', 'strip': True},
314 320 {'k': 'repo_landing_rev', 'strip': True},
315 321 {'k': 'clone_uri', 'strip': False},
316 322 {'k': 'repo_private', 'strip': True},
317 323 {'k': 'repo_enable_statistics', 'strip': True}
318 324 )
319 325
320 326 for item in keys_to_process:
321 327 attr = item['k']
322 328 if item['strip']:
323 329 attr = remove_prefix(item['k'], 'repo_')
324 330
325 331 val = defaults[attr]
326 332 if item['k'] == 'repo_landing_rev':
327 333 val = ':'.join(defaults[attr])
328 334 defaults[item['k']] = val
329 335 if item['k'] == 'clone_uri':
330 336 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
331 337
332 338 # fill owner
333 339 if repo_info.user:
334 340 defaults.update({'user': repo_info.user.username})
335 341 else:
336 342 replacement_user = User.get_first_admin().username
337 343 defaults.update({'user': replacement_user})
338 344
339 345 # fill repository users
340 346 for p in repo_info.repo_to_perm:
341 347 defaults.update({'u_perm_%s' % p.user.user_id:
342 348 p.permission.permission_name})
343 349
344 350 # fill repository groups
345 351 for p in repo_info.users_group_to_perm:
346 352 defaults.update({'g_perm_%s' % p.users_group.users_group_id:
347 353 p.permission.permission_name})
348 354
349 355 return defaults
350 356
351 357 def update(self, repo, **kwargs):
352 358 try:
353 359 cur_repo = self._get_repo(repo)
354 360 source_repo_name = cur_repo.repo_name
355 361 if 'user' in kwargs:
356 362 cur_repo.user = User.get_by_username(kwargs['user'])
357 363
358 364 if 'repo_group' in kwargs:
359 365 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
360 366 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
361 367
362 368 update_keys = [
363 369 (1, 'repo_enable_downloads'),
364 370 (1, 'repo_description'),
365 371 (1, 'repo_enable_locking'),
366 372 (1, 'repo_landing_rev'),
367 373 (1, 'repo_private'),
368 374 (1, 'repo_enable_statistics'),
369 375 (0, 'clone_uri'),
370 376 (0, 'fork_id')
371 377 ]
372 378 for strip, k in update_keys:
373 379 if k in kwargs:
374 380 val = kwargs[k]
375 381 if strip:
376 382 k = remove_prefix(k, 'repo_')
377 383 if k == 'clone_uri':
378 384 from rhodecode.model.validators import Missing
379 385 _change = kwargs.get('clone_uri_change')
380 386 if _change in [Missing, 'OLD']:
381 387 # we don't change the value, so use original one
382 388 val = cur_repo.clone_uri
383 389
384 390 setattr(cur_repo, k, val)
385 391
386 392 new_name = cur_repo.get_new_name(kwargs['repo_name'])
387 393 cur_repo.repo_name = new_name
388 394
389 395 # if private flag is set, reset default permission to NONE
390 396 if kwargs.get('repo_private'):
391 397 EMPTY_PERM = 'repository.none'
392 398 RepoModel().grant_user_permission(
393 399 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
394 400 )
395 401
396 402 # handle extra fields
397 403 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
398 404 kwargs):
399 405 k = RepositoryField.un_prefix_key(field)
400 406 ex_field = RepositoryField.get_by_key_name(
401 407 key=k, repo=cur_repo)
402 408 if ex_field:
403 409 ex_field.field_value = kwargs[field]
404 410 self.sa.add(ex_field)
405 411 self.sa.add(cur_repo)
406 412
407 413 if source_repo_name != new_name:
408 414 # rename repository
409 415 self._rename_filesystem_repo(
410 416 old=source_repo_name, new=new_name)
411 417
412 418 return cur_repo
413 419 except Exception:
414 420 log.error(traceback.format_exc())
415 421 raise
416 422
417 423 def _create_repo(self, repo_name, repo_type, description, owner,
418 424 private=False, clone_uri=None, repo_group=None,
419 425 landing_rev='rev:tip', fork_of=None,
420 426 copy_fork_permissions=False, enable_statistics=False,
421 427 enable_locking=False, enable_downloads=False,
422 428 copy_group_permissions=False,
423 429 state=Repository.STATE_PENDING):
424 430 """
425 431 Create repository inside database with PENDING state, this should be
426 432 only executed by create() repo. With exception of importing existing
427 433 repos
428 434 """
429 435 from rhodecode.model.scm import ScmModel
430 436
431 437 owner = self._get_user(owner)
432 438 fork_of = self._get_repo(fork_of)
433 439 repo_group = self._get_repo_group(safe_int(repo_group))
434 440
435 441 try:
436 442 repo_name = safe_unicode(repo_name)
437 443 description = safe_unicode(description)
438 444 # repo name is just a name of repository
439 445 # while repo_name_full is a full qualified name that is combined
440 446 # with name and path of group
441 447 repo_name_full = repo_name
442 448 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
443 449
444 450 new_repo = Repository()
445 451 new_repo.repo_state = state
446 452 new_repo.enable_statistics = False
447 453 new_repo.repo_name = repo_name_full
448 454 new_repo.repo_type = repo_type
449 455 new_repo.user = owner
450 456 new_repo.group = repo_group
451 457 new_repo.description = description or repo_name
452 458 new_repo.private = private
453 459 new_repo.clone_uri = clone_uri
454 460 new_repo.landing_rev = landing_rev
455 461
456 462 new_repo.enable_statistics = enable_statistics
457 463 new_repo.enable_locking = enable_locking
458 464 new_repo.enable_downloads = enable_downloads
459 465
460 466 if repo_group:
461 467 new_repo.enable_locking = repo_group.enable_locking
462 468
463 469 if fork_of:
464 470 parent_repo = fork_of
465 471 new_repo.fork = parent_repo
466 472
467 473 self.sa.add(new_repo)
468 474
469 475 EMPTY_PERM = 'repository.none'
470 476 if fork_of and copy_fork_permissions:
471 477 repo = fork_of
472 478 user_perms = UserRepoToPerm.query() \
473 479 .filter(UserRepoToPerm.repository == repo).all()
474 480 group_perms = UserGroupRepoToPerm.query() \
475 481 .filter(UserGroupRepoToPerm.repository == repo).all()
476 482
477 483 for perm in user_perms:
478 484 UserRepoToPerm.create(
479 485 perm.user, new_repo, perm.permission)
480 486
481 487 for perm in group_perms:
482 488 UserGroupRepoToPerm.create(
483 489 perm.users_group, new_repo, perm.permission)
484 490 # in case we copy permissions and also set this repo to private
485 491 # override the default user permission to make it a private
486 492 # repo
487 493 if private:
488 494 RepoModel(self.sa).grant_user_permission(
489 495 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
490 496
491 497 elif repo_group and copy_group_permissions:
492 498 user_perms = UserRepoGroupToPerm.query() \
493 499 .filter(UserRepoGroupToPerm.group == repo_group).all()
494 500
495 501 group_perms = UserGroupRepoGroupToPerm.query() \
496 502 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
497 503
498 504 for perm in user_perms:
499 505 perm_name = perm.permission.permission_name.replace(
500 506 'group.', 'repository.')
501 507 perm_obj = Permission.get_by_key(perm_name)
502 508 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
503 509
504 510 for perm in group_perms:
505 511 perm_name = perm.permission.permission_name.replace(
506 512 'group.', 'repository.')
507 513 perm_obj = Permission.get_by_key(perm_name)
508 514 UserGroupRepoToPerm.create(
509 515 perm.users_group, new_repo, perm_obj)
510 516
511 517 if private:
512 518 RepoModel(self.sa).grant_user_permission(
513 519 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
514 520
515 521 else:
516 522 perm_obj = self._create_default_perms(new_repo, private)
517 523 self.sa.add(perm_obj)
518 524
519 525 # now automatically start following this repository as owner
520 526 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
521 527 owner.user_id)
522 528 # we need to flush here, in order to check if database won't
523 529 # throw any exceptions, create filesystem dirs at the very end
524 530 self.sa.flush()
525 531
526 532 return new_repo
527 533 except Exception:
528 534 log.error(traceback.format_exc())
529 535 raise
530 536
531 537 def create(self, form_data, cur_user):
532 538 """
533 539 Create repository using celery tasks
534 540
535 541 :param form_data:
536 542 :param cur_user:
537 543 """
538 544 from rhodecode.lib.celerylib import tasks, run_task
539 545 return run_task(tasks.create_repo, form_data, cur_user)
540 546
541 547 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
542 548 perm_deletions=None, check_perms=True,
543 549 cur_user=None):
544 550 if not perm_additions:
545 551 perm_additions = []
546 552 if not perm_updates:
547 553 perm_updates = []
548 554 if not perm_deletions:
549 555 perm_deletions = []
550 556
551 557 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
552 558
553 559 # update permissions
554 560 for member_id, perm, member_type in perm_updates:
555 561 member_id = int(member_id)
556 562 if member_type == 'user':
557 563 # this updates also current one if found
558 564 self.grant_user_permission(
559 565 repo=repo, user=member_id, perm=perm)
560 566 else: # set for user group
561 567 # check if we have permissions to alter this usergroup
562 568 member_name = UserGroup.get(member_id).users_group_name
563 569 if not check_perms or HasUserGroupPermissionAny(
564 570 *req_perms)(member_name, user=cur_user):
565 571 self.grant_user_group_permission(
566 572 repo=repo, group_name=member_id, perm=perm)
567 573
568 574 # set new permissions
569 575 for member_id, perm, member_type in perm_additions:
570 576 member_id = int(member_id)
571 577 if member_type == 'user':
572 578 self.grant_user_permission(
573 579 repo=repo, user=member_id, perm=perm)
574 580 else: # set for user group
575 581 # check if we have permissions to alter this usergroup
576 582 member_name = UserGroup.get(member_id).users_group_name
577 583 if not check_perms or HasUserGroupPermissionAny(
578 584 *req_perms)(member_name, user=cur_user):
579 585 self.grant_user_group_permission(
580 586 repo=repo, group_name=member_id, perm=perm)
581 587
582 588 # delete permissions
583 589 for member_id, perm, member_type in perm_deletions:
584 590 member_id = int(member_id)
585 591 if member_type == 'user':
586 592 self.revoke_user_permission(repo=repo, user=member_id)
587 593 else: # set for user group
588 594 # check if we have permissions to alter this usergroup
589 595 member_name = UserGroup.get(member_id).users_group_name
590 596 if not check_perms or HasUserGroupPermissionAny(
591 597 *req_perms)(member_name, user=cur_user):
592 598 self.revoke_user_group_permission(
593 599 repo=repo, group_name=member_id)
594 600
595 601 def create_fork(self, form_data, cur_user):
596 602 """
597 603 Simple wrapper into executing celery task for fork creation
598 604
599 605 :param form_data:
600 606 :param cur_user:
601 607 """
602 608 from rhodecode.lib.celerylib import tasks, run_task
603 609 return run_task(tasks.create_repo_fork, form_data, cur_user)
604 610
605 611 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
606 612 """
607 613 Delete given repository, forks parameter defines what do do with
608 614 attached forks. Throws AttachedForksError if deleted repo has attached
609 615 forks
610 616
611 617 :param repo:
612 618 :param forks: str 'delete' or 'detach'
613 619 :param fs_remove: remove(archive) repo from filesystem
614 620 """
615 621 if not cur_user:
616 622 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
617 623 repo = self._get_repo(repo)
618 624 if repo:
619 625 if forks == 'detach':
620 626 for r in repo.forks:
621 627 r.fork = None
622 628 self.sa.add(r)
623 629 elif forks == 'delete':
624 630 for r in repo.forks:
625 631 self.delete(r, forks='delete')
626 632 elif [f for f in repo.forks]:
627 633 raise AttachedForksError()
628 634
629 635 old_repo_dict = repo.get_dict()
630 636 try:
631 637 self.sa.delete(repo)
632 638 if fs_remove:
633 639 self._delete_filesystem_repo(repo)
634 640 else:
635 641 log.debug('skipping removal from filesystem')
636 642 old_repo_dict.update({
637 643 'deleted_by': cur_user,
638 644 'deleted_on': time.time(),
639 645 })
640 646 log_delete_repository(**old_repo_dict)
641 647 except Exception:
642 648 log.error(traceback.format_exc())
643 649 raise
644 650
645 651 def grant_user_permission(self, repo, user, perm):
646 652 """
647 653 Grant permission for user on given repository, or update existing one
648 654 if found
649 655
650 656 :param repo: Instance of Repository, repository_id, or repository name
651 657 :param user: Instance of User, user_id or username
652 658 :param perm: Instance of Permission, or permission_name
653 659 """
654 660 user = self._get_user(user)
655 661 repo = self._get_repo(repo)
656 662 permission = self._get_perm(perm)
657 663
658 664 # check if we have that permission already
659 665 obj = self.sa.query(UserRepoToPerm) \
660 666 .filter(UserRepoToPerm.user == user) \
661 667 .filter(UserRepoToPerm.repository == repo) \
662 668 .scalar()
663 669 if obj is None:
664 670 # create new !
665 671 obj = UserRepoToPerm()
666 672 obj.repository = repo
667 673 obj.user = user
668 674 obj.permission = permission
669 675 self.sa.add(obj)
670 676 log.debug('Granted perm %s to %s on %s', perm, user, repo)
671 677 action_logger_generic(
672 678 'granted permission: {} to user: {} on repo: {}'.format(
673 679 perm, user, repo), namespace='security.repo')
674 680 return obj
675 681
676 682 def revoke_user_permission(self, repo, user):
677 683 """
678 684 Revoke permission for user on given repository
679 685
680 686 :param repo: Instance of Repository, repository_id, or repository name
681 687 :param user: Instance of User, user_id or username
682 688 """
683 689
684 690 user = self._get_user(user)
685 691 repo = self._get_repo(repo)
686 692
687 693 obj = self.sa.query(UserRepoToPerm) \
688 694 .filter(UserRepoToPerm.repository == repo) \
689 695 .filter(UserRepoToPerm.user == user) \
690 696 .scalar()
691 697 if obj:
692 698 self.sa.delete(obj)
693 699 log.debug('Revoked perm on %s on %s', repo, user)
694 700 action_logger_generic(
695 701 'revoked permission from user: {} on repo: {}'.format(
696 702 user, repo), namespace='security.repo')
697 703
698 704 def grant_user_group_permission(self, repo, group_name, perm):
699 705 """
700 706 Grant permission for user group on given repository, or update
701 707 existing one if found
702 708
703 709 :param repo: Instance of Repository, repository_id, or repository name
704 710 :param group_name: Instance of UserGroup, users_group_id,
705 711 or user group name
706 712 :param perm: Instance of Permission, or permission_name
707 713 """
708 714 repo = self._get_repo(repo)
709 715 group_name = self._get_user_group(group_name)
710 716 permission = self._get_perm(perm)
711 717
712 718 # check if we have that permission already
713 719 obj = self.sa.query(UserGroupRepoToPerm) \
714 720 .filter(UserGroupRepoToPerm.users_group == group_name) \
715 721 .filter(UserGroupRepoToPerm.repository == repo) \
716 722 .scalar()
717 723
718 724 if obj is None:
719 725 # create new
720 726 obj = UserGroupRepoToPerm()
721 727
722 728 obj.repository = repo
723 729 obj.users_group = group_name
724 730 obj.permission = permission
725 731 self.sa.add(obj)
726 732 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
727 733 action_logger_generic(
728 734 'granted permission: {} to usergroup: {} on repo: {}'.format(
729 735 perm, group_name, repo), namespace='security.repo')
730 736
731 737 return obj
732 738
733 739 def revoke_user_group_permission(self, repo, group_name):
734 740 """
735 741 Revoke permission for user group on given repository
736 742
737 743 :param repo: Instance of Repository, repository_id, or repository name
738 744 :param group_name: Instance of UserGroup, users_group_id,
739 745 or user group name
740 746 """
741 747 repo = self._get_repo(repo)
742 748 group_name = self._get_user_group(group_name)
743 749
744 750 obj = self.sa.query(UserGroupRepoToPerm) \
745 751 .filter(UserGroupRepoToPerm.repository == repo) \
746 752 .filter(UserGroupRepoToPerm.users_group == group_name) \
747 753 .scalar()
748 754 if obj:
749 755 self.sa.delete(obj)
750 756 log.debug('Revoked perm to %s on %s', repo, group_name)
751 757 action_logger_generic(
752 758 'revoked permission from usergroup: {} on repo: {}'.format(
753 759 group_name, repo), namespace='security.repo')
754 760
755 761 def delete_stats(self, repo_name):
756 762 """
757 763 removes stats for given repo
758 764
759 765 :param repo_name:
760 766 """
761 767 repo = self._get_repo(repo_name)
762 768 try:
763 769 obj = self.sa.query(Statistics) \
764 770 .filter(Statistics.repository == repo).scalar()
765 771 if obj:
766 772 self.sa.delete(obj)
767 773 except Exception:
768 774 log.error(traceback.format_exc())
769 775 raise
770 776
771 777 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
772 778 field_type='str', field_desc=''):
773 779
774 780 repo = self._get_repo(repo_name)
775 781
776 782 new_field = RepositoryField()
777 783 new_field.repository = repo
778 784 new_field.field_key = field_key
779 785 new_field.field_type = field_type # python type
780 786 new_field.field_value = field_value
781 787 new_field.field_desc = field_desc
782 788 new_field.field_label = field_label
783 789 self.sa.add(new_field)
784 790 return new_field
785 791
786 792 def delete_repo_field(self, repo_name, field_key):
787 793 repo = self._get_repo(repo_name)
788 794 field = RepositoryField.get_by_key_name(field_key, repo)
789 795 if field:
790 796 self.sa.delete(field)
791 797
792 798 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
793 799 clone_uri=None, repo_store_location=None,
794 800 use_global_config=False):
795 801 """
796 802 makes repository on filesystem. It's group aware means it'll create
797 803 a repository within a group, and alter the paths accordingly of
798 804 group location
799 805
800 806 :param repo_name:
801 807 :param alias:
802 808 :param parent:
803 809 :param clone_uri:
804 810 :param repo_store_location:
805 811 """
806 812 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
807 813 from rhodecode.model.scm import ScmModel
808 814
809 815 if Repository.NAME_SEP in repo_name:
810 816 raise ValueError(
811 817 'repo_name must not contain groups got `%s`' % repo_name)
812 818
813 819 if isinstance(repo_group, RepoGroup):
814 820 new_parent_path = os.sep.join(repo_group.full_path_splitted)
815 821 else:
816 822 new_parent_path = repo_group or ''
817 823
818 824 if repo_store_location:
819 825 _paths = [repo_store_location]
820 826 else:
821 827 _paths = [self.repos_path, new_parent_path, repo_name]
822 828 # we need to make it str for mercurial
823 829 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
824 830
825 831 # check if this path is not a repository
826 832 if is_valid_repo(repo_path, self.repos_path):
827 833 raise Exception('This path %s is a valid repository' % repo_path)
828 834
829 835 # check if this path is a group
830 836 if is_valid_repo_group(repo_path, self.repos_path):
831 837 raise Exception('This path %s is a valid group' % repo_path)
832 838
833 839 log.info('creating repo %s in %s from url: `%s`',
834 840 repo_name, safe_unicode(repo_path),
835 841 obfuscate_url_pw(clone_uri))
836 842
837 843 backend = get_backend(repo_type)
838 844
839 845 config_repo = None if use_global_config else repo_name
840 846 if config_repo and new_parent_path:
841 847 config_repo = Repository.NAME_SEP.join(
842 848 (new_parent_path, config_repo))
843 849 config = make_db_config(clear_session=False, repo=config_repo)
844 850 config.set('extensions', 'largefiles', '')
845 851
846 852 # patch and reset hooks section of UI config to not run any
847 853 # hooks on creating remote repo
848 854 config.clear_section('hooks')
849 855
850 856 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
851 857 if repo_type == 'git':
852 858 repo = backend(
853 859 repo_path, config=config, create=True, src_url=clone_uri,
854 860 bare=True)
855 861 else:
856 862 repo = backend(
857 863 repo_path, config=config, create=True, src_url=clone_uri)
858 864
859 865 ScmModel().install_hooks(repo, repo_type=repo_type)
860 866
861 867 log.debug('Created repo %s with %s backend',
862 868 safe_unicode(repo_name), safe_unicode(repo_type))
863 869 return repo
864 870
865 871 def _rename_filesystem_repo(self, old, new):
866 872 """
867 873 renames repository on filesystem
868 874
869 875 :param old: old name
870 876 :param new: new name
871 877 """
872 878 log.info('renaming repo from %s to %s', old, new)
873 879
874 880 old_path = os.path.join(self.repos_path, old)
875 881 new_path = os.path.join(self.repos_path, new)
876 882 if os.path.isdir(new_path):
877 883 raise Exception(
878 884 'Was trying to rename to already existing dir %s' % new_path
879 885 )
880 886 shutil.move(old_path, new_path)
881 887
882 888 def _delete_filesystem_repo(self, repo):
883 889 """
884 890 removes repo from filesystem, the removal is acctually made by
885 891 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
886 892 repository is no longer valid for rhodecode, can be undeleted later on
887 893 by reverting the renames on this repository
888 894
889 895 :param repo: repo object
890 896 """
891 897 rm_path = os.path.join(self.repos_path, repo.repo_name)
892 898 repo_group = repo.group
893 899 log.info("Removing repository %s", rm_path)
894 900 # disable hg/git internal that it doesn't get detected as repo
895 901 alias = repo.repo_type
896 902
897 903 config = make_db_config(clear_session=False)
898 904 config.set('extensions', 'largefiles', '')
899 905 bare = getattr(repo.scm_instance(config=config), 'bare', False)
900 906
901 907 # skip this for bare git repos
902 908 if not bare:
903 909 # disable VCS repo
904 910 vcs_path = os.path.join(rm_path, '.%s' % alias)
905 911 if os.path.exists(vcs_path):
906 912 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
907 913
908 914 _now = datetime.now()
909 915 _ms = str(_now.microsecond).rjust(6, '0')
910 916 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
911 917 repo.just_name)
912 918 if repo_group:
913 919 # if repository is in group, prefix the removal path with the group
914 920 args = repo_group.full_path_splitted + [_d]
915 921 _d = os.path.join(*args)
916 922
917 923 if os.path.isdir(rm_path):
918 924 shutil.move(rm_path, os.path.join(self.repos_path, _d))
@@ -1,42 +1,48 b''
1 1 // # Copyright (C) 2010-2016 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 * autocomplete formatter that uses gravatar
21 21 * */
22 22 var autocompleteFormatResult = function(data, value, org_formatter) {
23 var value_display = data.value_display;
23 var activeUser = data.active || true;
24 var valueDisplay = data.value_display;
25
26 if (!activeUser) {
27 valueDisplay = '<strong>(disabled)</strong> ' + valueDisplay;
28 }
29
24 30 var escapeRegExChars = function (value) {
25 31 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
26 32 };
27 33 var pattern = '(' + escapeRegExChars(value) + ')';
28 value_display = value_display.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
34 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
29 35 var tmpl = '<div class="ac-container-wrap"><img class="gravatar" src="{0}"/>{1}</div>';
30 36 if (data.icon_link === "") {
31 37 tmpl = '<div class="ac-container-wrap">{0}</div>';
32 return tmpl.format(value_display);
38 return tmpl.format(valueDisplay);
33 39 }
34 return tmpl.format(data.icon_link, value_display);
40 return tmpl.format(data.icon_link, valueDisplay);
35 41 };
36 42
37 43 /**
38 44 * autocomplete filter that uses display value to filter
39 45 */
40 46 var autocompleteFilterResult = function (suggestion, originalQuery, queryLowerCase) {
41 47 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
42 48 };
General Comments 0
You need to be logged in to leave comments. Login now