##// END OF EJS Templates
utils/conf...
marcink -
r2109:8ecfed1d beta
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,58 b''
1 # -*- coding: utf-8 -*-
2 """
3 package.rhodecode.config.conf
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 Various config settings for RhodeCode
7
8 :created_on: Mar 7, 2012
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: <name>, see LICENSE_FILE for more details.
12 """
13 from rhodecode import EXTENSIONS
14
15 from rhodecode.lib.utils2 import __get_lem
16
17
18 # language map is also used by whoosh indexer, which for those specified
19 # extensions will index it's content
20 LANGUAGES_EXTENSIONS_MAP = __get_lem()
21
22 #==============================================================================
23 # WHOOSH INDEX EXTENSIONS
24 #==============================================================================
25 # EXTENSIONS WE WANT TO INDEX CONTENT OFF USING WHOOSH
26 INDEX_EXTENSIONS = LANGUAGES_EXTENSIONS_MAP.keys()
27
28 # list of readme files to search in file tree and display in summary
29 # attached weights defines the search order lower is first
30 ALL_READMES = [
31 ('readme', 0), ('README', 0), ('Readme', 0),
32 ('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1),
33 ('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2),
34 ('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2),
35 ('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2),
36 ]
37
38 # extension together with weights to search lower is first
39 RST_EXTS = [
40 ('', 0), ('.rst', 1), ('.rest', 1),
41 ('.RST', 2), ('.REST', 2),
42 ('.txt', 3), ('.TXT', 3)
43 ]
44
45 MARKDOWN_EXTS = [
46 ('.md', 1), ('.MD', 1),
47 ('.mkdn', 2), ('.MKDN', 2),
48 ('.mdown', 3), ('.MDOWN', 3),
49 ('.markdown', 4), ('.MARKDOWN', 4)
50 ]
51
52 PLAIN_EXTS = [('.text', 2), ('.TEXT', 2)]
53
54 ALL_EXTS = MARKDOWN_EXTS + RST_EXTS + PLAIN_EXTS
55
56 DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
57
58 DATE_FORMAT = "%Y-%m-%d"
@@ -0,0 +1,405 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.lib.utils
4 ~~~~~~~~~~~~~~~~~~~
5
6 Some simple helper functions
7
8 :created_on: Jan 5, 2011
9 :author: marcink
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
26 import re
27 from rhodecode.lib.vcs.utils.lazy import LazyProperty
28
29
30 def __get_lem():
31 """
32 Get language extension map based on what's inside pygments lexers
33 """
34 from pygments import lexers
35 from string import lower
36 from collections import defaultdict
37
38 d = defaultdict(lambda: [])
39
40 def __clean(s):
41 s = s.lstrip('*')
42 s = s.lstrip('.')
43
44 if s.find('[') != -1:
45 exts = []
46 start, stop = s.find('['), s.find(']')
47
48 for suffix in s[start + 1:stop]:
49 exts.append(s[:s.find('[')] + suffix)
50 return map(lower, exts)
51 else:
52 return map(lower, [s])
53
54 for lx, t in sorted(lexers.LEXERS.items()):
55 m = map(__clean, t[-2])
56 if m:
57 m = reduce(lambda x, y: x + y, m)
58 for ext in m:
59 desc = lx.replace('Lexer', '')
60 d[ext].append(desc)
61
62 return dict(d)
63
64 def str2bool(_str):
65 """
66 returs True/False value from given string, it tries to translate the
67 string into boolean
68
69 :param _str: string value to translate into boolean
70 :rtype: boolean
71 :returns: boolean from given string
72 """
73 if _str is None:
74 return False
75 if _str in (True, False):
76 return _str
77 _str = str(_str).strip().lower()
78 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
79
80
81 def convert_line_endings(line, mode):
82 """
83 Converts a given line "line end" accordingly to given mode
84
85 Available modes are::
86 0 - Unix
87 1 - Mac
88 2 - DOS
89
90 :param line: given line to convert
91 :param mode: mode to convert to
92 :rtype: str
93 :return: converted line according to mode
94 """
95 from string import replace
96
97 if mode == 0:
98 line = replace(line, '\r\n', '\n')
99 line = replace(line, '\r', '\n')
100 elif mode == 1:
101 line = replace(line, '\r\n', '\r')
102 line = replace(line, '\n', '\r')
103 elif mode == 2:
104 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
105 return line
106
107
108 def detect_mode(line, default):
109 """
110 Detects line break for given line, if line break couldn't be found
111 given default value is returned
112
113 :param line: str line
114 :param default: default
115 :rtype: int
116 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
117 """
118 if line.endswith('\r\n'):
119 return 2
120 elif line.endswith('\n'):
121 return 0
122 elif line.endswith('\r'):
123 return 1
124 else:
125 return default
126
127
128 def generate_api_key(username, salt=None):
129 """
130 Generates unique API key for given username, if salt is not given
131 it'll be generated from some random string
132
133 :param username: username as string
134 :param salt: salt to hash generate KEY
135 :rtype: str
136 :returns: sha1 hash from username+salt
137 """
138 from tempfile import _RandomNameSequence
139 import hashlib
140
141 if salt is None:
142 salt = _RandomNameSequence().next()
143
144 return hashlib.sha1(username + salt).hexdigest()
145
146
147 def safe_unicode(str_, from_encoding=None):
148 """
149 safe unicode function. Does few trick to turn str_ into unicode
150
151 In case of UnicodeDecode error we try to return it with encoding detected
152 by chardet library if it fails fallback to unicode with errors replaced
153
154 :param str_: string to decode
155 :rtype: unicode
156 :returns: unicode object
157 """
158 if isinstance(str_, unicode):
159 return str_
160
161 if not from_encoding:
162 import rhodecode
163 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
164 from_encoding = DEFAULT_ENCODING
165
166 try:
167 return unicode(str_)
168 except UnicodeDecodeError:
169 pass
170
171 try:
172 return unicode(str_, from_encoding)
173 except UnicodeDecodeError:
174 pass
175
176 try:
177 import chardet
178 encoding = chardet.detect(str_)['encoding']
179 if encoding is None:
180 raise Exception()
181 return str_.decode(encoding)
182 except (ImportError, UnicodeDecodeError, Exception):
183 return unicode(str_, from_encoding, 'replace')
184
185
186 def safe_str(unicode_, to_encoding=None):
187 """
188 safe str function. Does few trick to turn unicode_ into string
189
190 In case of UnicodeEncodeError we try to return it with encoding detected
191 by chardet library if it fails fallback to string with errors replaced
192
193 :param unicode_: unicode to encode
194 :rtype: str
195 :returns: str object
196 """
197
198 # if it's not basestr cast to str
199 if not isinstance(unicode_, basestring):
200 return str(unicode_)
201
202 if isinstance(unicode_, str):
203 return unicode_
204
205 if not to_encoding:
206 import rhodecode
207 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
208 to_encoding = DEFAULT_ENCODING
209
210 try:
211 return unicode_.encode(to_encoding)
212 except UnicodeEncodeError:
213 pass
214
215 try:
216 import chardet
217 encoding = chardet.detect(unicode_)['encoding']
218 print encoding
219 if encoding is None:
220 raise UnicodeEncodeError()
221
222 return unicode_.encode(encoding)
223 except (ImportError, UnicodeEncodeError):
224 return unicode_.encode(to_encoding, 'replace')
225
226 return safe_str
227
228
229 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
230 """
231 Custom engine_from_config functions that makes sure we use NullPool for
232 file based sqlite databases. This prevents errors on sqlite. This only
233 applies to sqlalchemy versions < 0.7.0
234
235 """
236 import sqlalchemy
237 from sqlalchemy import engine_from_config as efc
238 import logging
239
240 if int(sqlalchemy.__version__.split('.')[1]) < 7:
241
242 # This solution should work for sqlalchemy < 0.7.0, and should use
243 # proxy=TimerProxy() for execution time profiling
244
245 from sqlalchemy.pool import NullPool
246 url = configuration[prefix + 'url']
247
248 if url.startswith('sqlite'):
249 kwargs.update({'poolclass': NullPool})
250 return efc(configuration, prefix, **kwargs)
251 else:
252 import time
253 from sqlalchemy import event
254 from sqlalchemy.engine import Engine
255
256 log = logging.getLogger('sqlalchemy.engine')
257 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
258 engine = efc(configuration, prefix, **kwargs)
259
260 def color_sql(sql):
261 COLOR_SEQ = "\033[1;%dm"
262 COLOR_SQL = YELLOW
263 normal = '\x1b[0m'
264 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
265
266 if configuration['debug']:
267 #attach events only for debug configuration
268
269 def before_cursor_execute(conn, cursor, statement,
270 parameters, context, executemany):
271 context._query_start_time = time.time()
272 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
273
274
275 def after_cursor_execute(conn, cursor, statement,
276 parameters, context, executemany):
277 total = time.time() - context._query_start_time
278 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
279
280 event.listen(engine, "before_cursor_execute",
281 before_cursor_execute)
282 event.listen(engine, "after_cursor_execute",
283 after_cursor_execute)
284
285 return engine
286
287
288 def age(curdate):
289 """
290 turns a datetime into an age string.
291
292 :param curdate: datetime object
293 :rtype: unicode
294 :returns: unicode words describing age
295 """
296
297 from datetime import datetime
298 from webhelpers.date import time_ago_in_words
299
300 _ = lambda s: s
301
302 if not curdate:
303 return ''
304
305 agescales = [(_(u"year"), 3600 * 24 * 365),
306 (_(u"month"), 3600 * 24 * 30),
307 (_(u"day"), 3600 * 24),
308 (_(u"hour"), 3600),
309 (_(u"minute"), 60),
310 (_(u"second"), 1), ]
311
312 age = datetime.now() - curdate
313 age_seconds = (age.days * agescales[2][1]) + age.seconds
314 pos = 1
315 for scale in agescales:
316 if scale[1] <= age_seconds:
317 if pos == 6:
318 pos = 5
319 return '%s %s' % (time_ago_in_words(curdate,
320 agescales[pos][0]), _('ago'))
321 pos += 1
322
323 return _(u'just now')
324
325
326 def uri_filter(uri):
327 """
328 Removes user:password from given url string
329
330 :param uri:
331 :rtype: unicode
332 :returns: filtered list of strings
333 """
334 if not uri:
335 return ''
336
337 proto = ''
338
339 for pat in ('https://', 'http://'):
340 if uri.startswith(pat):
341 uri = uri[len(pat):]
342 proto = pat
343 break
344
345 # remove passwords and username
346 uri = uri[uri.find('@') + 1:]
347
348 # get the port
349 cred_pos = uri.find(':')
350 if cred_pos == -1:
351 host, port = uri, None
352 else:
353 host, port = uri[:cred_pos], uri[cred_pos + 1:]
354
355 return filter(None, [proto, host, port])
356
357
358 def credentials_filter(uri):
359 """
360 Returns a url with removed credentials
361
362 :param uri:
363 """
364
365 uri = uri_filter(uri)
366 #check if we have port
367 if len(uri) > 2 and uri[2]:
368 uri[2] = ':' + uri[2]
369
370 return ''.join(uri)
371
372
373 def get_changeset_safe(repo, rev):
374 """
375 Safe version of get_changeset if this changeset doesn't exists for a
376 repo it returns a Dummy one instead
377
378 :param repo:
379 :param rev:
380 """
381 from rhodecode.lib.vcs.backends.base import BaseRepository
382 from rhodecode.lib.vcs.exceptions import RepositoryError
383 if not isinstance(repo, BaseRepository):
384 raise Exception('You must pass an Repository '
385 'object as first argument got %s', type(repo))
386
387 try:
388 cs = repo.get_changeset(rev)
389 except RepositoryError:
390 from rhodecode.lib.utils import EmptyChangeset
391 cs = EmptyChangeset(requested_revision=rev)
392 return cs
393
394
395 def extract_mentioned_users(s):
396 """
397 Returns unique usernames from given string s that have @mention
398
399 :param s: string to get mentions
400 """
401 usrs = {}
402 for username in re.findall(r'(?:^@|\s@)(\w+)', s):
403 usrs[username] = username
404
405 return sorted(usrs.keys())
@@ -1,94 +1,96 b''
1 1 """Pylons environment configuration"""
2 2
3 3 import os
4 4 import logging
5 import rhodecode
5 6
6 7 from mako.lookup import TemplateLookup
7 8 from pylons.configuration import PylonsConfig
8 9 from pylons.error import handle_mako_error
9 10
10 import rhodecode
11 # don't remove this import it does magic for celery
12 from rhodecode.lib import celerypylons
13
11 14 import rhodecode.lib.app_globals as app_globals
12 import rhodecode.lib.helpers
13 15
14 16 from rhodecode.config.routing import make_map
15 # don't remove this import it does magic for celery
16 from rhodecode.lib import celerypylons, str2bool
17 from rhodecode.lib import engine_from_config
17
18 from rhodecode.lib import helpers
18 19 from rhodecode.lib.auth import set_available_permissions
19 from rhodecode.lib.utils import repo2db_mapper, make_ui, set_rhodecode_config
20 from rhodecode.lib.utils import repo2db_mapper, make_ui, set_rhodecode_config,\
21 load_rcextensions
22 from rhodecode.lib.utils2 import engine_from_config, str2bool
20 23 from rhodecode.model import init_model
21 24 from rhodecode.model.scm import ScmModel
22 from rhodecode.lib.vcs.utils.fakemod import create_module
23 25
24 26 log = logging.getLogger(__name__)
25 27
26 28
27 29 def load_environment(global_conf, app_conf, initial=False):
28 """Configure the Pylons environment via the ``pylons.config``
30 """
31 Configure the Pylons environment via the ``pylons.config``
29 32 object
30 33 """
31 34 config = PylonsConfig()
32 35
33 36 # Pylons paths
34 37 root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
35 paths = dict(root=root,
36 controllers=os.path.join(root, 'controllers'),
37 static_files=os.path.join(root, 'public'),
38 templates=[os.path.join(root, 'templates')])
38 paths = dict(
39 root=root,
40 controllers=os.path.join(root, 'controllers'),
41 static_files=os.path.join(root, 'public'),
42 templates=[os.path.join(root, 'templates')]
43 )
39 44
40 45 # Initialize config with the basic options
41 46 config.init_app(global_conf, app_conf, package='rhodecode', paths=paths)
42 47
43 48 # store some globals into rhodecode
44 49 rhodecode.CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
45 50
46 51 config['routes.map'] = make_map(config)
47 52 config['pylons.app_globals'] = app_globals.Globals(config)
48 config['pylons.h'] = rhodecode.lib.helpers
53 config['pylons.h'] = helpers
49 54 rhodecode.CONFIG = config
50 55
51 path = os.path.join(config['here'], 'rcextensions', '__init__.py')
52 if os.path.isfile(path):
53 rcext = create_module('rc', path)
54 rhodecode.EXTENSIONS = rcext
55 log.debug('Found rcextensions now loading %s...' % rcext)
56 load_rcextensions(root_path=config['here'])
57
56 58 # Setup cache object as early as possible
57 59 import pylons
58 60 pylons.cache._push_object(config['pylons.app_globals'].cache)
59 61
60 62 # Create the Mako TemplateLookup, with the default auto-escaping
61 63 config['pylons.app_globals'].mako_lookup = TemplateLookup(
62 64 directories=paths['templates'],
63 65 error_handler=handle_mako_error,
64 66 module_directory=os.path.join(app_conf['cache_dir'], 'templates'),
65 67 input_encoding='utf-8', default_filters=['escape'],
66 68 imports=['from webhelpers.html import escape'])
67 69
68 70 # sets the c attribute access when don't existing attribute are accessed
69 71 config['pylons.strict_tmpl_context'] = True
70 72 test = os.path.split(config['__file__'])[-1] == 'test.ini'
71 73 if test:
72 74 from rhodecode.lib.utils import create_test_env, create_test_index
73 75 from rhodecode.tests import TESTS_TMP_PATH
74 76 create_test_env(TESTS_TMP_PATH, config)
75 77 create_test_index(TESTS_TMP_PATH, config, True)
76 78
77 79 # MULTIPLE DB configs
78 80 # Setup the SQLAlchemy database engine
79 81 sa_engine_db1 = engine_from_config(config, 'sqlalchemy.db1.')
80 82
81 83 init_model(sa_engine_db1)
82 84
83 85 repos_path = make_ui('db').configitems('paths')[0][1]
84 86 repo2db_mapper(ScmModel().repo_scan(repos_path))
85 87 set_available_permissions(config)
86 88 config['base_path'] = repos_path
87 89 set_rhodecode_config(config)
88 90 # CONFIGURATION OPTIONS HERE (note: all config options will override
89 91 # any Pylons config options)
90 92
91 93 # store config reference into our module to skip import magic of
92 94 # pylons
93 95 rhodecode.CONFIG.update(config)
94 96 return config
@@ -1,81 +1,79 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.config.rcextensions.make_rcextensions
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Whoosh indexing module for RhodeCode
7 7
8 8 :created_on: Mar 6, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import os
26 26 import sys
27 27 import pkg_resources
28 28 import traceback
29 29 import logging
30 30 from os.path import dirname as dn, join as jn
31 31
32 32 #to get the rhodecode import
33 33 sys.path.append(dn(dn(dn(os.path.realpath(__file__)))))
34 34
35 35 from rhodecode.lib.utils import BasePasterCommand, Command, ask_ok
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class MakeRcExt(BasePasterCommand):
41 41
42 42 max_args = 1
43 43 min_args = 1
44 44
45 45 usage = "CONFIG_FILE"
46 46 summary = "Creates additional extensions for rhodecode"
47 47 group_name = "RhodeCode"
48 48 takes_config_file = -1
49 49 parser = Command.standard_parser(verbose=True)
50 50
51 51 def command(self):
52 52 logging.config.fileConfig(self.path_to_ini_file)
53 53 from pylons import config
54 54
55 55 def _make_file(ext_file):
56 56 bdir = os.path.split(ext_file)[0]
57 57 if not os.path.isdir(bdir):
58 58 os.makedirs(bdir)
59 59 with open(ext_file, 'wb') as f:
60 60 f.write(tmpl)
61 61 log.info('Writen new extensions file to %s' % ext_file)
62 62
63 63 here = config['here']
64 64 tmpl = pkg_resources.resource_string(
65 65 'rhodecode', jn('config', 'rcextensions', '__init__.py')
66 66 )
67 67 ext_file = jn(here, 'rcextensions', '__init__.py')
68 68 if os.path.exists(ext_file):
69 69 msg = ('Extension file already exists, do you want '
70 70 'to overwrite it ? [y/n]')
71 71 if ask_ok(msg):
72 72 _make_file(ext_file)
73 73 else:
74 74 log.info('nothing done...')
75 75 else:
76 76 _make_file(ext_file)
77 77
78 78 def update_parser(self):
79 79 pass
80
81
@@ -1,227 +1,228 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.users_groups
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Users Groups crud controller for pylons
7 7
8 8 :created_on: Jan 25, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29
30 30 from formencode import htmlfill
31 31 from pylons import request, session, tmpl_context as c, url, config
32 32 from pylons.controllers.util import abort, redirect
33 33 from pylons.i18n.translation import _
34 34
35 from rhodecode.lib import helpers as h
35 36 from rhodecode.lib.exceptions import UsersGroupsAssignedException
36 from rhodecode.lib import helpers as h, safe_unicode
37 from rhodecode.lib.utils2 import safe_unicode
37 38 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
38 39 from rhodecode.lib.base import BaseController, render
39 40
40 41 from rhodecode.model.users_group import UsersGroupModel
41 42
42 43 from rhodecode.model.db import User, UsersGroup, Permission, UsersGroupToPerm
43 44 from rhodecode.model.forms import UsersGroupForm
44 45 from rhodecode.model.meta import Session
45 46
46 47 log = logging.getLogger(__name__)
47 48
48 49
49 50 class UsersGroupsController(BaseController):
50 51 """REST Controller styled on the Atom Publishing Protocol"""
51 52 # To properly map this controller, ensure your config/routing.py
52 53 # file has a resource setup:
53 54 # map.resource('users_group', 'users_groups')
54 55
55 56 @LoginRequired()
56 57 @HasPermissionAllDecorator('hg.admin')
57 58 def __before__(self):
58 59 c.admin_user = session.get('admin_user')
59 60 c.admin_username = session.get('admin_username')
60 61 super(UsersGroupsController, self).__before__()
61 62 c.available_permissions = config['available_permissions']
62 63
63 64 def index(self, format='html'):
64 65 """GET /users_groups: All items in the collection"""
65 66 # url('users_groups')
66 67 c.users_groups_list = self.sa.query(UsersGroup).all()
67 68 return render('admin/users_groups/users_groups.html')
68 69
69 70 def create(self):
70 71 """POST /users_groups: Create a new item"""
71 72 # url('users_groups')
72 73
73 74 users_group_form = UsersGroupForm()()
74 75 try:
75 76 form_result = users_group_form.to_python(dict(request.POST))
76 77 UsersGroupModel().create(name=form_result['users_group_name'],
77 78 active=form_result['users_group_active'])
78 79 h.flash(_('created users group %s') \
79 80 % form_result['users_group_name'], category='success')
80 81 #action_logger(self.rhodecode_user, 'new_user', '', '', self.sa)
81 82 Session.commit()
82 83 except formencode.Invalid, errors:
83 84 return htmlfill.render(
84 85 render('admin/users_groups/users_group_add.html'),
85 86 defaults=errors.value,
86 87 errors=errors.error_dict or {},
87 88 prefix_error=False,
88 89 encoding="UTF-8")
89 90 except Exception:
90 91 log.error(traceback.format_exc())
91 92 h.flash(_('error occurred during creation of users group %s') \
92 93 % request.POST.get('users_group_name'), category='error')
93 94
94 95 return redirect(url('users_groups'))
95 96
96 97 def new(self, format='html'):
97 98 """GET /users_groups/new: Form to create a new item"""
98 99 # url('new_users_group')
99 100 return render('admin/users_groups/users_group_add.html')
100 101
101 102 def update(self, id):
102 103 """PUT /users_groups/id: Update an existing item"""
103 104 # Forms posted to this method should contain a hidden field:
104 105 # <input type="hidden" name="_method" value="PUT" />
105 106 # Or using helpers:
106 107 # h.form(url('users_group', id=ID),
107 108 # method='put')
108 109 # url('users_group', id=ID)
109 110
110 111 c.users_group = UsersGroup.get(id)
111 112 c.group_members_obj = [x.user for x in c.users_group.members]
112 113 c.group_members = [(x.user_id, x.username) for x in
113 114 c.group_members_obj]
114 115
115 116 c.available_members = [(x.user_id, x.username) for x in
116 117 self.sa.query(User).all()]
117 118
118 119 available_members = [safe_unicode(x[0]) for x in c.available_members]
119 120
120 121 users_group_form = UsersGroupForm(edit=True,
121 122 old_data=c.users_group.get_dict(),
122 123 available_members=available_members)()
123 124
124 125 try:
125 126 form_result = users_group_form.to_python(request.POST)
126 127 UsersGroupModel().update(c.users_group, form_result)
127 128 h.flash(_('updated users group %s') \
128 129 % form_result['users_group_name'],
129 130 category='success')
130 131 #action_logger(self.rhodecode_user, 'new_user', '', '', self.sa)
131 132 Session.commit()
132 133 except formencode.Invalid, errors:
133 134 e = errors.error_dict or {}
134 135
135 136 perm = Permission.get_by_key('hg.create.repository')
136 137 e.update({'create_repo_perm':
137 138 UsersGroupModel().has_perm(id, perm)})
138 139
139 140 return htmlfill.render(
140 141 render('admin/users_groups/users_group_edit.html'),
141 142 defaults=errors.value,
142 143 errors=e,
143 144 prefix_error=False,
144 145 encoding="UTF-8")
145 146 except Exception:
146 147 log.error(traceback.format_exc())
147 148 h.flash(_('error occurred during update of users group %s') \
148 149 % request.POST.get('users_group_name'), category='error')
149 150
150 151 return redirect(url('users_groups'))
151 152
152 153 def delete(self, id):
153 154 """DELETE /users_groups/id: Delete an existing item"""
154 155 # Forms posted to this method should contain a hidden field:
155 156 # <input type="hidden" name="_method" value="DELETE" />
156 157 # Or using helpers:
157 158 # h.form(url('users_group', id=ID),
158 159 # method='delete')
159 160 # url('users_group', id=ID)
160 161
161 162 try:
162 163 UsersGroupModel().delete(id)
163 164 Session.commit()
164 165 h.flash(_('successfully deleted users group'), category='success')
165 166 except UsersGroupsAssignedException, e:
166 167 h.flash(e, category='error')
167 168 except Exception:
168 169 log.error(traceback.format_exc())
169 170 h.flash(_('An error occurred during deletion of users group'),
170 171 category='error')
171 172 return redirect(url('users_groups'))
172 173
173 174 def show(self, id, format='html'):
174 175 """GET /users_groups/id: Show a specific item"""
175 176 # url('users_group', id=ID)
176 177
177 178 def edit(self, id, format='html'):
178 179 """GET /users_groups/id/edit: Form to edit an existing item"""
179 180 # url('edit_users_group', id=ID)
180 181
181 182 c.users_group = self.sa.query(UsersGroup).get(id)
182 183 if not c.users_group:
183 184 return redirect(url('users_groups'))
184 185
185 186 c.users_group.permissions = {}
186 187 c.group_members_obj = [x.user for x in c.users_group.members]
187 188 c.group_members = [(x.user_id, x.username) for x in
188 189 c.group_members_obj]
189 190 c.available_members = [(x.user_id, x.username) for x in
190 191 self.sa.query(User).all()]
191 192 defaults = c.users_group.get_dict()
192 193 perm = Permission.get_by_key('hg.create.repository')
193 194 defaults.update({'create_repo_perm':
194 195 UsersGroupModel().has_perm(c.users_group, perm)})
195 196 return htmlfill.render(
196 197 render('admin/users_groups/users_group_edit.html'),
197 198 defaults=defaults,
198 199 encoding="UTF-8",
199 200 force_defaults=False
200 201 )
201 202
202 203 def update_perm(self, id):
203 204 """PUT /users_perm/id: Update an existing item"""
204 205 # url('users_group_perm', id=ID, method='put')
205 206
206 207 grant_perm = request.POST.get('create_repo_perm', False)
207 208
208 209 if grant_perm:
209 210 perm = Permission.get_by_key('hg.create.none')
210 211 UsersGroupModel().revoke_perm(id, perm)
211 212
212 213 perm = Permission.get_by_key('hg.create.repository')
213 214 UsersGroupModel().grant_perm(id, perm)
214 215 h.flash(_("Granted 'repository create' permission to user"),
215 216 category='success')
216 217
217 218 Session.commit()
218 219 else:
219 220 perm = Permission.get_by_key('hg.create.repository')
220 221 UsersGroupModel().revoke_perm(id, perm)
221 222
222 223 perm = Permission.get_by_key('hg.create.none')
223 224 UsersGroupModel().grant_perm(id, perm)
224 225 h.flash(_("Revoked 'repository create' permission to user"),
225 226 category='success')
226 227 Session.commit()
227 228 return redirect(url('edit_users_group', id=id))
@@ -1,75 +1,75 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.branches
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 branches controller for rhodecode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27
28 28 from pylons import tmpl_context as c
29 29 import binascii
30 30
31 31 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
32 32 from rhodecode.lib.base import BaseRepoController, render
33 33 from rhodecode.lib.compat import OrderedDict
34 from rhodecode.lib import safe_unicode
34 from rhodecode.lib.utils2 import safe_unicode
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class BranchesController(BaseRepoController):
39 39
40 40 @LoginRequired()
41 41 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
42 42 'repository.admin')
43 43 def __before__(self):
44 44 super(BranchesController, self).__before__()
45 45
46 46 def index(self):
47 47
48 48 def _branchtags(localrepo):
49 49 bt_closed = {}
50 50 for bn, heads in localrepo.branchmap().iteritems():
51 51 tip = heads[-1]
52 52 if 'close' in localrepo.changelog.read(tip)[5]:
53 53 bt_closed[bn] = tip
54 54 return bt_closed
55 55
56 56 cs_g = c.rhodecode_repo.get_changeset
57 57
58 58 c.repo_closed_branches = {}
59 59 if c.rhodecode_db_repo.repo_type == 'hg':
60 60 bt_closed = _branchtags(c.rhodecode_repo._repo)
61 61 _closed_branches = [(safe_unicode(n), cs_g(binascii.hexlify(h)),)
62 62 for n, h in bt_closed.items()]
63 63
64 64 c.repo_closed_branches = OrderedDict(sorted(_closed_branches,
65 65 key=lambda ctx: ctx[0],
66 66 reverse=False))
67 67
68 68 _branches = [(safe_unicode(n), cs_g(h))
69 69 for n, h in c.rhodecode_repo.branches.items()]
70 70 c.repo_branches = OrderedDict(sorted(_branches,
71 71 key=lambda ctx: ctx[0],
72 72 reverse=False))
73 73
74 74
75 75 return render('branches/branches.html')
@@ -1,127 +1,127 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.feed
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Feed controller for rhodecode
7 7
8 8 :created_on: Apr 23, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27
28 28 from pylons import url, response, tmpl_context as c
29 29 from pylons.i18n.translation import _
30 30
31 from rhodecode.lib import safe_unicode
31 from rhodecode.lib.utils2 import safe_unicode
32 32 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 33 from rhodecode.lib.base import BaseRepoController
34 34
35 35 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class FeedController(BaseRepoController):
41 41
42 42 @LoginRequired(api_access=True)
43 43 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
44 44 'repository.admin')
45 45 def __before__(self):
46 46 super(FeedController, self).__before__()
47 47 #common values for feeds
48 48 self.description = _('Changes on %s repository')
49 49 self.title = self.title = _('%s %s feed') % (c.rhodecode_name, '%s')
50 50 self.language = 'en-us'
51 51 self.ttl = "5"
52 52 self.feed_nr = 10
53 53
54 54 def _get_title(self, cs):
55 55 return "R%s:%s - %s" % (
56 56 cs.revision, cs.short_id, cs.message
57 57 )
58 58
59 59 def __changes(self, cs):
60 60 changes = []
61 61
62 62 a = [safe_unicode(n.path) for n in cs.added]
63 63 if a:
64 64 changes.append('\nA ' + '\nA '.join(a))
65 65
66 66 m = [safe_unicode(n.path) for n in cs.changed]
67 67 if m:
68 68 changes.append('\nM ' + '\nM '.join(m))
69 69
70 70 d = [safe_unicode(n.path) for n in cs.removed]
71 71 if d:
72 72 changes.append('\nD ' + '\nD '.join(d))
73 73
74 74 changes.append('</pre>')
75 75
76 76 return ''.join(changes)
77 77
78 78 def atom(self, repo_name):
79 79 """Produce an atom-1.0 feed via feedgenerator module"""
80 80 feed = Atom1Feed(
81 81 title=self.title % repo_name,
82 82 link=url('summary_home', repo_name=repo_name,
83 83 qualified=True),
84 84 description=self.description % repo_name,
85 85 language=self.language,
86 86 ttl=self.ttl
87 87 )
88 88
89 89 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
90 90 desc_msg = []
91 91 desc_msg.append('%s - %s<br/><pre>' % (cs.author, cs.date))
92 92 desc_msg.append(self.__changes(cs))
93 93
94 94 feed.add_item(title=self._get_title(cs),
95 95 link=url('changeset_home', repo_name=repo_name,
96 96 revision=cs.raw_id, qualified=True),
97 97 author_name=cs.author,
98 98 description=''.join(desc_msg))
99 99
100 100 response.content_type = feed.mime_type
101 101 return feed.writeString('utf-8')
102 102
103 103 def rss(self, repo_name):
104 104 """Produce an rss2 feed via feedgenerator module"""
105 105 feed = Rss201rev2Feed(
106 106 title=self.title % repo_name,
107 107 link=url('summary_home', repo_name=repo_name,
108 108 qualified=True),
109 109 description=self.description % repo_name,
110 110 language=self.language,
111 111 ttl=self.ttl
112 112 )
113 113
114 114 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
115 115 desc_msg = []
116 116 desc_msg.append('%s - %s<br/><pre>' % (cs.author, cs.date))
117 117 desc_msg.append(self.__changes(cs))
118 118
119 119 feed.add_item(title=self._get_title(cs),
120 120 link=url('changeset_home', repo_name=repo_name,
121 121 revision=cs.raw_id, qualified=True),
122 122 author_name=cs.author,
123 123 description=''.join(desc_msg),
124 124 )
125 125
126 126 response.content_type = feed.mime_type
127 127 return feed.writeString('utf-8')
@@ -1,494 +1,496 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.files
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Files controller for RhodeCode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import traceback
29 29
30 30 from pylons import request, response, tmpl_context as c, url
31 31 from pylons.i18n.translation import _
32 32 from pylons.controllers.util import redirect
33 33 from pylons.decorators import jsonify
34 34
35 from rhodecode.lib.vcs.conf import settings
36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
37 EmptyRepositoryError, ImproperArchiveTypeError, VCSError, \
38 NodeAlreadyExistsError
39 from rhodecode.lib.vcs.nodes import FileNode
35 from rhodecode.lib import diffs
36 from rhodecode.lib import helpers as h
40 37
41 38 from rhodecode.lib.compat import OrderedDict
42 from rhodecode.lib import convert_line_endings, detect_mode, safe_str
39 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str
43 40 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
44 41 from rhodecode.lib.base import BaseRepoController, render
45 42 from rhodecode.lib.utils import EmptyChangeset
46 from rhodecode.lib import diffs
47 import rhodecode.lib.helpers as h
43 from rhodecode.lib.vcs.conf import settings
44 from rhodecode.lib.vcs.exceptions import RepositoryError, \
45 ChangesetDoesNotExistError, EmptyRepositoryError, \
46 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
47 from rhodecode.lib.vcs.nodes import FileNode
48
48 49 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.scm import ScmModel
51
49 52 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
50 53 _context_url, get_line_ctx, get_ignore_ws
51 from rhodecode.lib.diffs import wrapped_diff
52 from rhodecode.model.scm import ScmModel
54
53 55
54 56 log = logging.getLogger(__name__)
55 57
56 58
57 59 class FilesController(BaseRepoController):
58 60
59 61 @LoginRequired()
60 62 def __before__(self):
61 63 super(FilesController, self).__before__()
62 64 c.cut_off_limit = self.cut_off_limit
63 65
64 66 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
65 67 """
66 68 Safe way to get changeset if error occur it redirects to tip with
67 69 proper message
68 70
69 71 :param rev: revision to fetch
70 72 :param repo_name: repo name to redirect after
71 73 """
72 74
73 75 try:
74 76 return c.rhodecode_repo.get_changeset(rev)
75 77 except EmptyRepositoryError, e:
76 78 if not redirect_after:
77 79 return None
78 80 url_ = url('files_add_home',
79 81 repo_name=c.repo_name,
80 82 revision=0, f_path='')
81 83 add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
82 84 h.flash(h.literal(_('There are no files yet %s' % add_new)),
83 85 category='warning')
84 86 redirect(h.url('summary_home', repo_name=repo_name))
85 87
86 88 except RepositoryError, e:
87 89 h.flash(str(e), category='warning')
88 90 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
89 91
90 92 def __get_filenode_or_redirect(self, repo_name, cs, path):
91 93 """
92 94 Returns file_node, if error occurs or given path is directory,
93 95 it'll redirect to top level path
94 96
95 97 :param repo_name: repo_name
96 98 :param cs: given changeset
97 99 :param path: path to lookup
98 100 """
99 101
100 102 try:
101 103 file_node = cs.get_node(path)
102 104 if file_node.is_dir():
103 105 raise RepositoryError('given path is a directory')
104 106 except RepositoryError, e:
105 107 h.flash(str(e), category='warning')
106 108 redirect(h.url('files_home', repo_name=repo_name,
107 109 revision=cs.raw_id))
108 110
109 111 return file_node
110 112
111 113 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
112 114 'repository.admin')
113 115 def index(self, repo_name, revision, f_path):
114 116 # redirect to given revision from form if given
115 117 post_revision = request.POST.get('at_rev', None)
116 118 if post_revision:
117 119 cs = self.__get_cs_or_redirect(post_revision, repo_name)
118 120 redirect(url('files_home', repo_name=c.repo_name,
119 121 revision=cs.raw_id, f_path=f_path))
120 122
121 123 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
122 124 c.branch = request.GET.get('branch', None)
123 125 c.f_path = f_path
124 126
125 127 cur_rev = c.changeset.revision
126 128
127 129 # prev link
128 130 try:
129 131 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
130 132 c.url_prev = url('files_home', repo_name=c.repo_name,
131 133 revision=prev_rev.raw_id, f_path=f_path)
132 134 if c.branch:
133 135 c.url_prev += '?branch=%s' % c.branch
134 136 except (ChangesetDoesNotExistError, VCSError):
135 137 c.url_prev = '#'
136 138
137 139 # next link
138 140 try:
139 141 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
140 142 c.url_next = url('files_home', repo_name=c.repo_name,
141 143 revision=next_rev.raw_id, f_path=f_path)
142 144 if c.branch:
143 145 c.url_next += '?branch=%s' % c.branch
144 146 except (ChangesetDoesNotExistError, VCSError):
145 147 c.url_next = '#'
146 148
147 149 # files or dirs
148 150 try:
149 151 c.file = c.changeset.get_node(f_path)
150 152
151 153 if c.file.is_file():
152 154 c.file_history = self._get_node_history(c.changeset, f_path)
153 155 else:
154 156 c.file_history = []
155 157 except RepositoryError, e:
156 158 h.flash(str(e), category='warning')
157 159 redirect(h.url('files_home', repo_name=repo_name,
158 160 revision=revision))
159 161
160 162 return render('files/files.html')
161 163
162 164 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
163 165 'repository.admin')
164 166 def rawfile(self, repo_name, revision, f_path):
165 167 cs = self.__get_cs_or_redirect(revision, repo_name)
166 168 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
167 169
168 170 response.content_disposition = 'attachment; filename=%s' % \
169 171 safe_str(f_path.split(os.sep)[-1])
170 172
171 173 response.content_type = file_node.mimetype
172 174 return file_node.content
173 175
174 176 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
175 177 'repository.admin')
176 178 def raw(self, repo_name, revision, f_path):
177 179 cs = self.__get_cs_or_redirect(revision, repo_name)
178 180 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
179 181
180 182 raw_mimetype_mapping = {
181 183 # map original mimetype to a mimetype used for "show as raw"
182 184 # you can also provide a content-disposition to override the
183 185 # default "attachment" disposition.
184 186 # orig_type: (new_type, new_dispo)
185 187
186 188 # show images inline:
187 189 'image/x-icon': ('image/x-icon', 'inline'),
188 190 'image/png': ('image/png', 'inline'),
189 191 'image/gif': ('image/gif', 'inline'),
190 192 'image/jpeg': ('image/jpeg', 'inline'),
191 193 'image/svg+xml': ('image/svg+xml', 'inline'),
192 194 }
193 195
194 196 mimetype = file_node.mimetype
195 197 try:
196 198 mimetype, dispo = raw_mimetype_mapping[mimetype]
197 199 except KeyError:
198 200 # we don't know anything special about this, handle it safely
199 201 if file_node.is_binary:
200 202 # do same as download raw for binary files
201 203 mimetype, dispo = 'application/octet-stream', 'attachment'
202 204 else:
203 205 # do not just use the original mimetype, but force text/plain,
204 206 # otherwise it would serve text/html and that might be unsafe.
205 207 # Note: underlying vcs library fakes text/plain mimetype if the
206 208 # mimetype can not be determined and it thinks it is not
207 209 # binary.This might lead to erroneous text display in some
208 210 # cases, but helps in other cases, like with text files
209 211 # without extension.
210 212 mimetype, dispo = 'text/plain', 'inline'
211 213
212 214 if dispo == 'attachment':
213 215 dispo = 'attachment; filename=%s' % \
214 216 safe_str(f_path.split(os.sep)[-1])
215 217
216 218 response.content_disposition = dispo
217 219 response.content_type = mimetype
218 220 return file_node.content
219 221
220 222 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
221 223 'repository.admin')
222 224 def annotate(self, repo_name, revision, f_path):
223 225 c.cs = self.__get_cs_or_redirect(revision, repo_name)
224 226 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
225 227
226 228 c.file_history = self._get_node_history(c.cs, f_path)
227 229 c.f_path = f_path
228 230 return render('files/files_annotate.html')
229 231
230 232 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
231 233 def edit(self, repo_name, revision, f_path):
232 234 r_post = request.POST
233 235
234 236 c.cs = self.__get_cs_or_redirect(revision, repo_name)
235 237 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
236 238
237 239 if c.file.is_binary:
238 240 return redirect(url('files_home', repo_name=c.repo_name,
239 241 revision=c.cs.raw_id, f_path=f_path))
240 242
241 243 c.f_path = f_path
242 244
243 245 if r_post:
244 246
245 247 old_content = c.file.content
246 248 sl = old_content.splitlines(1)
247 249 first_line = sl[0] if sl else ''
248 250 # modes: 0 - Unix, 1 - Mac, 2 - DOS
249 251 mode = detect_mode(first_line, 0)
250 252 content = convert_line_endings(r_post.get('content'), mode)
251 253
252 254 message = r_post.get('message') or (_('Edited %s via RhodeCode')
253 255 % (f_path))
254 256 author = self.rhodecode_user.full_contact
255 257
256 258 if content == old_content:
257 259 h.flash(_('No changes'),
258 260 category='warning')
259 261 return redirect(url('changeset_home', repo_name=c.repo_name,
260 262 revision='tip'))
261 263
262 264 try:
263 265 self.scm_model.commit_change(repo=c.rhodecode_repo,
264 266 repo_name=repo_name, cs=c.cs,
265 267 user=self.rhodecode_user,
266 268 author=author, message=message,
267 269 content=content, f_path=f_path)
268 270 h.flash(_('Successfully committed to %s' % f_path),
269 271 category='success')
270 272
271 273 except Exception:
272 274 log.error(traceback.format_exc())
273 275 h.flash(_('Error occurred during commit'), category='error')
274 276 return redirect(url('changeset_home',
275 277 repo_name=c.repo_name, revision='tip'))
276 278
277 279 return render('files/files_edit.html')
278 280
279 281 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
280 282 def add(self, repo_name, revision, f_path):
281 283 r_post = request.POST
282 284 c.cs = self.__get_cs_or_redirect(revision, repo_name,
283 285 redirect_after=False)
284 286 if c.cs is None:
285 287 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
286 288
287 289 c.f_path = f_path
288 290
289 291 if r_post:
290 292 unix_mode = 0
291 293 content = convert_line_endings(r_post.get('content'), unix_mode)
292 294
293 295 message = r_post.get('message') or (_('Added %s via RhodeCode')
294 296 % (f_path))
295 297 location = r_post.get('location')
296 298 filename = r_post.get('filename')
297 299 file_obj = r_post.get('upload_file', None)
298 300
299 301 if file_obj is not None and hasattr(file_obj, 'filename'):
300 302 filename = file_obj.filename
301 303 content = file_obj.file
302 304
303 305 node_path = os.path.join(location, filename)
304 306 author = self.rhodecode_user.full_contact
305 307
306 308 if not content:
307 309 h.flash(_('No content'), category='warning')
308 310 return redirect(url('changeset_home', repo_name=c.repo_name,
309 311 revision='tip'))
310 312 if not filename:
311 313 h.flash(_('No filename'), category='warning')
312 314 return redirect(url('changeset_home', repo_name=c.repo_name,
313 315 revision='tip'))
314 316
315 317 try:
316 318 self.scm_model.create_node(repo=c.rhodecode_repo,
317 319 repo_name=repo_name, cs=c.cs,
318 320 user=self.rhodecode_user,
319 321 author=author, message=message,
320 322 content=content, f_path=node_path)
321 323 h.flash(_('Successfully committed to %s' % node_path),
322 324 category='success')
323 325 except NodeAlreadyExistsError, e:
324 326 h.flash(_(e), category='error')
325 327 except Exception:
326 328 log.error(traceback.format_exc())
327 329 h.flash(_('Error occurred during commit'), category='error')
328 330 return redirect(url('changeset_home',
329 331 repo_name=c.repo_name, revision='tip'))
330 332
331 333 return render('files/files_add.html')
332 334
333 335 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
334 336 'repository.admin')
335 337 def archivefile(self, repo_name, fname):
336 338
337 339 fileformat = None
338 340 revision = None
339 341 ext = None
340 342 subrepos = request.GET.get('subrepos') == 'true'
341 343
342 344 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
343 345 archive_spec = fname.split(ext_data[1])
344 346 if len(archive_spec) == 2 and archive_spec[1] == '':
345 347 fileformat = a_type or ext_data[1]
346 348 revision = archive_spec[0]
347 349 ext = ext_data[1]
348 350
349 351 try:
350 352 dbrepo = RepoModel().get_by_repo_name(repo_name)
351 353 if dbrepo.enable_downloads is False:
352 354 return _('downloads disabled')
353 355
354 356 if c.rhodecode_repo.alias == 'hg':
355 357 # patch and reset hooks section of UI config to not run any
356 358 # hooks on fetching archives with subrepos
357 359 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
358 360 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
359 361
360 362 cs = c.rhodecode_repo.get_changeset(revision)
361 363 content_type = settings.ARCHIVE_SPECS[fileformat][0]
362 364 except ChangesetDoesNotExistError:
363 365 return _('Unknown revision %s') % revision
364 366 except EmptyRepositoryError:
365 367 return _('Empty repository')
366 368 except (ImproperArchiveTypeError, KeyError):
367 369 return _('Unknown archive type')
368 370
369 371 response.content_type = content_type
370 372 response.content_disposition = 'attachment; filename=%s-%s%s' \
371 373 % (repo_name, revision, ext)
372 374
373 375 import tempfile
374 376 archive = tempfile.mkstemp()[1]
375 377 t = open(archive, 'wb')
376 378 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
377 379
378 380 def get_chunked_archive(archive):
379 381 stream = open(archive, 'rb')
380 382 while True:
381 383 data = stream.read(4096)
382 384 if not data:
383 385 os.remove(archive)
384 386 break
385 387 yield data
386 388
387 389 return get_chunked_archive(archive)
388 390
389 391 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
390 392 'repository.admin')
391 393 def diff(self, repo_name, f_path):
392 394 ignore_whitespace = request.GET.get('ignorews') == '1'
393 395 line_context = request.GET.get('context', 3)
394 396 diff1 = request.GET.get('diff1', '')
395 397 diff2 = request.GET.get('diff2', '')
396 398 c.action = request.GET.get('diff')
397 399 c.no_changes = diff1 == diff2
398 400 c.f_path = f_path
399 401 c.big_diff = False
400 402 c.anchor_url = anchor_url
401 403 c.ignorews_url = _ignorews_url
402 404 c.context_url = _context_url
403 405 c.changes = OrderedDict()
404 406 c.changes[diff2] = []
405 407 try:
406 408 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
407 409 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
408 410 node1 = c.changeset_1.get_node(f_path)
409 411 else:
410 412 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
411 413 node1 = FileNode('.', '', changeset=c.changeset_1)
412 414
413 415 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
414 416 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
415 417 node2 = c.changeset_2.get_node(f_path)
416 418 else:
417 419 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
418 420 node2 = FileNode('.', '', changeset=c.changeset_2)
419 421 except RepositoryError:
420 422 return redirect(url('files_home', repo_name=c.repo_name,
421 423 f_path=f_path))
422 424
423 425 if c.action == 'download':
424 426 _diff = diffs.get_gitdiff(node1, node2,
425 427 ignore_whitespace=ignore_whitespace,
426 428 context=line_context)
427 429 diff = diffs.DiffProcessor(_diff, format='gitdiff')
428 430
429 431 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
430 432 response.content_type = 'text/plain'
431 433 response.content_disposition = (
432 434 'attachment; filename=%s' % diff_name
433 435 )
434 436 return diff.raw_diff()
435 437
436 438 elif c.action == 'raw':
437 439 _diff = diffs.get_gitdiff(node1, node2,
438 440 ignore_whitespace=ignore_whitespace,
439 441 context=line_context)
440 442 diff = diffs.DiffProcessor(_diff, format='gitdiff')
441 443 response.content_type = 'text/plain'
442 444 return diff.raw_diff()
443 445
444 446 else:
445 447 fid = h.FID(diff2, node2.path)
446 448 line_context_lcl = get_line_ctx(fid, request.GET)
447 449 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
448 450
449 451 lim = request.GET.get('fulldiff') or self.cut_off_limit
450 _, cs1, cs2, diff, st = wrapped_diff(filenode_old=node1,
452 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
451 453 filenode_new=node2,
452 454 cut_off_limit=lim,
453 455 ignore_whitespace=ign_whitespace_lcl,
454 456 line_context=line_context_lcl,
455 457 enable_comments=False)
456 458
457 459 c.changes = [('', node2, diff, cs1, cs2, st,)]
458 460
459 461 return render('files/file_diff.html')
460 462
461 463 def _get_node_history(self, cs, f_path):
462 464 changesets = cs.get_file_history(f_path)
463 465 hist_l = []
464 466
465 467 changesets_group = ([], _("Changesets"))
466 468 branches_group = ([], _("Branches"))
467 469 tags_group = ([], _("Tags"))
468 470 _hg = cs.repository.alias == 'hg'
469 471 for chs in changesets:
470 472 _branch = '(%s)' % chs.branch if _hg else ''
471 473 n_desc = 'r%s:%s %s' % (chs.revision, chs.short_id, _branch)
472 474 changesets_group[0].append((chs.raw_id, n_desc,))
473 475
474 476 hist_l.append(changesets_group)
475 477
476 478 for name, chs in c.rhodecode_repo.branches.items():
477 479 branches_group[0].append((chs, name),)
478 480 hist_l.append(branches_group)
479 481
480 482 for name, chs in c.rhodecode_repo.tags.items():
481 483 tags_group[0].append((chs, name),)
482 484 hist_l.append(tags_group)
483 485
484 486 return hist_l
485 487
486 488 @jsonify
487 489 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
488 490 'repository.admin')
489 491 def nodelist(self, repo_name, revision, f_path):
490 492 if request.environ.get('HTTP_X_PARTIAL_XHR'):
491 493 cs = self.__get_cs_or_redirect(revision, repo_name)
492 494 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
493 495 flat=False)
494 496 return _d + _f
@@ -1,237 +1,237 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.summary
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Summary controller for Rhodecode
7 7
8 8 :created_on: Apr 18, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import traceback
27 27 import calendar
28 28 import logging
29 29 import urllib
30 30 from time import mktime
31 31 from datetime import timedelta, date
32 32 from urlparse import urlparse
33 33 from rhodecode.lib.compat import product
34 34
35 35 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
36 36 NodeDoesNotExistError
37 37
38 38 from pylons import tmpl_context as c, request, url, config
39 39 from pylons.i18n.translation import _
40 40
41 41 from beaker.cache import cache_region, region_invalidate
42 42
43 from rhodecode.config.conf import ALL_READMES, ALL_EXTS, LANGUAGES_EXTENSIONS_MAP
43 44 from rhodecode.model.db import Statistics, CacheInvalidation
44 from rhodecode.lib import ALL_READMES, ALL_EXTS, safe_unicode
45 from rhodecode.lib.utils2 import safe_unicode
45 46 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
46 47 from rhodecode.lib.base import BaseRepoController, render
47 48 from rhodecode.lib.utils import EmptyChangeset
48 49 from rhodecode.lib.markup_renderer import MarkupRenderer
49 50 from rhodecode.lib.celerylib import run_task
50 from rhodecode.lib.celerylib.tasks import get_commits_stats, \
51 LANGUAGES_EXTENSIONS_MAP
51 from rhodecode.lib.celerylib.tasks import get_commits_stats
52 52 from rhodecode.lib.helpers import RepoPage
53 53 from rhodecode.lib.compat import json, OrderedDict
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57 README_FILES = [''.join([x[0][0], x[1][0]]) for x in
58 58 sorted(list(product(ALL_READMES, ALL_EXTS)),
59 59 key=lambda y:y[0][1] + y[1][1])]
60 60
61 61
62 62 class SummaryController(BaseRepoController):
63 63
64 64 @LoginRequired()
65 65 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
66 66 'repository.admin')
67 67 def __before__(self):
68 68 super(SummaryController, self).__before__()
69 69
70 70 def index(self, repo_name):
71 71 c.dbrepo = dbrepo = c.rhodecode_db_repo
72 72 c.following = self.scm_model.is_following_repo(repo_name,
73 73 self.rhodecode_user.user_id)
74 74
75 75 def url_generator(**kw):
76 76 return url('shortlog_home', repo_name=repo_name, size=10, **kw)
77 77
78 78 c.repo_changesets = RepoPage(c.rhodecode_repo, page=1,
79 79 items_per_page=10, url=url_generator)
80 80
81 81 if self.rhodecode_user.username == 'default':
82 82 # for default(anonymous) user we don't need to pass credentials
83 83 username = ''
84 84 password = ''
85 85 else:
86 86 username = str(self.rhodecode_user.username)
87 87 password = '@'
88 88
89 89 parsed_url = urlparse(url.current(qualified=True))
90 90
91 91 default_clone_uri = '{scheme}://{user}{pass}{netloc}{path}'
92 92
93 93 uri_tmpl = config.get('clone_uri', default_clone_uri)
94 94 uri_tmpl = uri_tmpl.replace('{', '%(').replace('}', ')s')
95 95 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
96 96 uri_dict = {
97 97 'user': username,
98 98 'pass': password,
99 99 'scheme': parsed_url.scheme,
100 100 'netloc': parsed_url.netloc,
101 101 'path': decoded_path
102 102 }
103 103
104 104 uri = uri_tmpl % uri_dict
105 105 # generate another clone url by id
106 106 uri_dict.update(
107 107 {'path': decoded_path.replace(repo_name, '_%s' % c.dbrepo.repo_id)}
108 108 )
109 109 uri_id = uri_tmpl % uri_dict
110 110
111 111 c.clone_repo_url = uri
112 112 c.clone_repo_url_id = uri_id
113 113 c.repo_tags = OrderedDict()
114 114 for name, hash_ in c.rhodecode_repo.tags.items()[:10]:
115 115 try:
116 116 c.repo_tags[name] = c.rhodecode_repo.get_changeset(hash_)
117 117 except ChangesetError:
118 118 c.repo_tags[name] = EmptyChangeset(hash_)
119 119
120 120 c.repo_branches = OrderedDict()
121 121 for name, hash_ in c.rhodecode_repo.branches.items()[:10]:
122 122 try:
123 123 c.repo_branches[name] = c.rhodecode_repo.get_changeset(hash_)
124 124 except ChangesetError:
125 125 c.repo_branches[name] = EmptyChangeset(hash_)
126 126
127 127 td = date.today() + timedelta(days=1)
128 128 td_1m = td - timedelta(days=calendar.mdays[td.month])
129 129 td_1y = td - timedelta(days=365)
130 130
131 131 ts_min_m = mktime(td_1m.timetuple())
132 132 ts_min_y = mktime(td_1y.timetuple())
133 133 ts_max_y = mktime(td.timetuple())
134 134
135 135 if dbrepo.enable_statistics:
136 136 c.show_stats = True
137 137 c.no_data_msg = _('No data loaded yet')
138 138 run_task(get_commits_stats, c.dbrepo.repo_name, ts_min_y, ts_max_y)
139 139 else:
140 140 c.show_stats = False
141 141 c.no_data_msg = _('Statistics are disabled for this repository')
142 142 c.ts_min = ts_min_m
143 143 c.ts_max = ts_max_y
144 144
145 145 stats = self.sa.query(Statistics)\
146 146 .filter(Statistics.repository == dbrepo)\
147 147 .scalar()
148 148
149 149 c.stats_percentage = 0
150 150
151 151 if stats and stats.languages:
152 152 c.no_data = False is dbrepo.enable_statistics
153 153 lang_stats_d = json.loads(stats.languages)
154 154 c.commit_data = stats.commit_activity
155 155 c.overview_data = stats.commit_activity_combined
156 156
157 157 lang_stats = ((x, {"count": y,
158 158 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
159 159 for x, y in lang_stats_d.items())
160 160
161 161 c.trending_languages = json.dumps(
162 162 sorted(lang_stats, reverse=True, key=lambda k: k[1])[:10]
163 163 )
164 164 last_rev = stats.stat_on_revision + 1
165 165 c.repo_last_rev = c.rhodecode_repo.count()\
166 166 if c.rhodecode_repo.revisions else 0
167 167 if last_rev == 0 or c.repo_last_rev == 0:
168 168 pass
169 169 else:
170 170 c.stats_percentage = '%.2f' % ((float((last_rev)) /
171 171 c.repo_last_rev) * 100)
172 172 else:
173 173 c.commit_data = json.dumps({})
174 174 c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 10]])
175 175 c.trending_languages = json.dumps({})
176 176 c.no_data = True
177 177
178 178 c.enable_downloads = dbrepo.enable_downloads
179 179 if c.enable_downloads:
180 180 c.download_options = self._get_download_links(c.rhodecode_repo)
181 181
182 182 c.readme_data, c.readme_file = self.__get_readme_data(c.rhodecode_db_repo)
183 183 return render('summary/summary.html')
184 184
185 185 def __get_readme_data(self, repo):
186 186
187 187 @cache_region('long_term')
188 188 def _get_readme_from_cache(key):
189 189 readme_data = None
190 190 readme_file = None
191 191 log.debug('Fetching readme file')
192 192 try:
193 193 cs = repo.get_changeset('tip')
194 194 renderer = MarkupRenderer()
195 195 for f in README_FILES:
196 196 try:
197 197 readme = cs.get_node(f)
198 198 readme_file = f
199 199 readme_data = renderer.render(readme.content, f)
200 200 log.debug('Found readme %s' % readme_file)
201 201 break
202 202 except NodeDoesNotExistError:
203 203 continue
204 204 except ChangesetError:
205 205 pass
206 206 except EmptyRepositoryError:
207 207 pass
208 208 except Exception:
209 209 log.error(traceback.format_exc())
210 210
211 211 return readme_data, readme_file
212 212
213 213 key = repo.repo_name + '_README'
214 214 inv = CacheInvalidation.invalidate(key)
215 215 if inv is not None:
216 216 region_invalidate(_get_readme_from_cache, None, key)
217 217 CacheInvalidation.set_valid(inv.cache_key)
218 218 return _get_readme_from_cache(key)
219 219
220 220 def _get_download_links(self, repo):
221 221
222 222 download_l = []
223 223
224 224 branches_group = ([], _("Branches"))
225 225 tags_group = ([], _("Tags"))
226 226
227 227 for name, chs in c.rhodecode_repo.branches.items():
228 228 #chs = chs.split(':')[-1]
229 229 branches_group[0].append((chs, name),)
230 230 download_l.append(branches_group)
231 231
232 232 for name, chs in c.rhodecode_repo.tags.items():
233 233 #chs = chs.split(':')[-1]
234 234 tags_group[0].append((chs, name),)
235 235 download_l.append(tags_group)
236 236
237 237 return download_l
@@ -1,477 +1,24 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.lib.__init__
4 ~~~~~~~~~~~~~~~~~~~~~~~
5
6 Some simple helper functions
7
8 :created_on: Jan 5, 2011
9 :author: marcink
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
26 1 import os
27 import re
28 from rhodecode import EXTENSIONS
29 from rhodecode.lib.vcs.utils.lazy import LazyProperty
30
31
32 def __get_lem():
33 from pygments import lexers
34 from string import lower
35 from collections import defaultdict
36
37 d = defaultdict(lambda: [])
38
39 def __clean(s):
40 s = s.lstrip('*')
41 s = s.lstrip('.')
42
43 if s.find('[') != -1:
44 exts = []
45 start, stop = s.find('['), s.find(']')
46
47 for suffix in s[start + 1:stop]:
48 exts.append(s[:s.find('[')] + suffix)
49 return map(lower, exts)
50 else:
51 return map(lower, [s])
52
53 for lx, t in sorted(lexers.LEXERS.items()):
54 m = map(__clean, t[-2])
55 if m:
56 m = reduce(lambda x, y: x + y, m)
57 for ext in m:
58 desc = lx.replace('Lexer', '')
59 d[ext].append(desc)
60
61 return dict(d)
62
63 # language map is also used by whoosh indexer, which for those specified
64 # extensions will index it's content
65 LANGUAGES_EXTENSIONS_MAP = __get_lem()
66
67 # Additional mappings that are not present in the pygments lexers
68 LANGUAGES_EXTENSIONS_MAP.update(getattr(EXTENSIONS, 'EXTRA_MAPPINGS', {}))
69
70 #==============================================================================
71 # WHOOSH INDEX EXTENSIONS
72 #==============================================================================
73 # EXTENSIONS WE WANT TO INDEX CONTENT OFF USING WHOOSH
74 INDEX_EXTENSIONS = LANGUAGES_EXTENSIONS_MAP.keys()
75
76 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
77
78 if getattr(EXTENSIONS, 'INDEX_EXTENSIONS', []) != []:
79 INDEX_EXTENSIONS = getattr(EXTENSIONS, 'INDEX_EXTENSIONS', [])
80
81 #ADDITIONAL MAPPINGS
82 INDEX_EXTENSIONS.extend(getattr(EXTENSIONS, 'EXTRA_INDEX_EXTENSIONS', []))
83
84 # list of readme files to search in file tree and display in summary
85 # attached weights defines the search order lower is first
86 ALL_READMES = [
87 ('readme', 0), ('README', 0), ('Readme', 0),
88 ('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1),
89 ('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2),
90 ('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2),
91 ('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2),
92 ]
93
94 # extension together with weights to search lower is first
95 RST_EXTS = [
96 ('', 0), ('.rst', 1), ('.rest', 1),
97 ('.RST', 2), ('.REST', 2),
98 ('.txt', 3), ('.TXT', 3)
99 ]
100
101 MARKDOWN_EXTS = [
102 ('.md', 1), ('.MD', 1),
103 ('.mkdn', 2), ('.MKDN', 2),
104 ('.mdown', 3), ('.MDOWN', 3),
105 ('.markdown', 4), ('.MARKDOWN', 4)
106 ]
107
108 PLAIN_EXTS = [('.text', 2), ('.TEXT', 2)]
109
110 ALL_EXTS = MARKDOWN_EXTS + RST_EXTS + PLAIN_EXTS
111
112
113 def str2bool(_str):
114 """
115 returs True/False value from given string, it tries to translate the
116 string into boolean
117
118 :param _str: string value to translate into boolean
119 :rtype: boolean
120 :returns: boolean from given string
121 """
122 if _str is None:
123 return False
124 if _str in (True, False):
125 return _str
126 _str = str(_str).strip().lower()
127 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
128
129
130 def convert_line_endings(line, mode):
131 """
132 Converts a given line "line end" accordingly to given mode
133
134 Available modes are::
135 0 - Unix
136 1 - Mac
137 2 - DOS
138
139 :param line: given line to convert
140 :param mode: mode to convert to
141 :rtype: str
142 :return: converted line according to mode
143 """
144 from string import replace
145
146 if mode == 0:
147 line = replace(line, '\r\n', '\n')
148 line = replace(line, '\r', '\n')
149 elif mode == 1:
150 line = replace(line, '\r\n', '\r')
151 line = replace(line, '\n', '\r')
152 elif mode == 2:
153 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
154 return line
155
156
157 def detect_mode(line, default):
158 """
159 Detects line break for given line, if line break couldn't be found
160 given default value is returned
161
162 :param line: str line
163 :param default: default
164 :rtype: int
165 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
166 """
167 if line.endswith('\r\n'):
168 return 2
169 elif line.endswith('\n'):
170 return 0
171 elif line.endswith('\r'):
172 return 1
173 else:
174 return default
175
176
177 def generate_api_key(username, salt=None):
178 """
179 Generates unique API key for given username, if salt is not given
180 it'll be generated from some random string
181
182 :param username: username as string
183 :param salt: salt to hash generate KEY
184 :rtype: str
185 :returns: sha1 hash from username+salt
186 """
187 from tempfile import _RandomNameSequence
188 import hashlib
189
190 if salt is None:
191 salt = _RandomNameSequence().next()
192
193 return hashlib.sha1(username + salt).hexdigest()
194
195
196 def safe_unicode(str_, from_encoding=None):
197 """
198 safe unicode function. Does few trick to turn str_ into unicode
199
200 In case of UnicodeDecode error we try to return it with encoding detected
201 by chardet library if it fails fallback to unicode with errors replaced
202
203 :param str_: string to decode
204 :rtype: unicode
205 :returns: unicode object
206 """
207 if isinstance(str_, unicode):
208 return str_
209
210 if not from_encoding:
211 import rhodecode
212 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
213 from_encoding = DEFAULT_ENCODING
214
215 try:
216 return unicode(str_)
217 except UnicodeDecodeError:
218 pass
219
220 try:
221 return unicode(str_, from_encoding)
222 except UnicodeDecodeError:
223 pass
224
225 try:
226 import chardet
227 encoding = chardet.detect(str_)['encoding']
228 if encoding is None:
229 raise Exception()
230 return str_.decode(encoding)
231 except (ImportError, UnicodeDecodeError, Exception):
232 return unicode(str_, from_encoding, 'replace')
233
234
235 def safe_str(unicode_, to_encoding=None):
236 """
237 safe str function. Does few trick to turn unicode_ into string
238
239 In case of UnicodeEncodeError we try to return it with encoding detected
240 by chardet library if it fails fallback to string with errors replaced
241
242 :param unicode_: unicode to encode
243 :rtype: str
244 :returns: str object
245 """
246
247 # if it's not basestr cast to str
248 if not isinstance(unicode_, basestring):
249 return str(unicode_)
250
251 if isinstance(unicode_, str):
252 return unicode_
253
254 if not to_encoding:
255 import rhodecode
256 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
257 to_encoding = DEFAULT_ENCODING
258
259 try:
260 return unicode_.encode(to_encoding)
261 except UnicodeEncodeError:
262 pass
263
264 try:
265 import chardet
266 encoding = chardet.detect(unicode_)['encoding']
267 print encoding
268 if encoding is None:
269 raise UnicodeEncodeError()
270
271 return unicode_.encode(encoding)
272 except (ImportError, UnicodeEncodeError):
273 return unicode_.encode(to_encoding, 'replace')
274
275 return safe_str
276
277
278 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
279 """
280 Custom engine_from_config functions that makes sure we use NullPool for
281 file based sqlite databases. This prevents errors on sqlite. This only
282 applies to sqlalchemy versions < 0.7.0
283
284 """
285 import sqlalchemy
286 from sqlalchemy import engine_from_config as efc
287 import logging
288
289 if int(sqlalchemy.__version__.split('.')[1]) < 7:
290
291 # This solution should work for sqlalchemy < 0.7.0, and should use
292 # proxy=TimerProxy() for execution time profiling
293
294 from sqlalchemy.pool import NullPool
295 url = configuration[prefix + 'url']
296
297 if url.startswith('sqlite'):
298 kwargs.update({'poolclass': NullPool})
299 return efc(configuration, prefix, **kwargs)
300 else:
301 import time
302 from sqlalchemy import event
303 from sqlalchemy.engine import Engine
304
305 log = logging.getLogger('sqlalchemy.engine')
306 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
307 engine = efc(configuration, prefix, **kwargs)
308
309 def color_sql(sql):
310 COLOR_SEQ = "\033[1;%dm"
311 COLOR_SQL = YELLOW
312 normal = '\x1b[0m'
313 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
314
315 if configuration['debug']:
316 #attach events only for debug configuration
317
318 def before_cursor_execute(conn, cursor, statement,
319 parameters, context, executemany):
320 context._query_start_time = time.time()
321 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
322
323
324 def after_cursor_execute(conn, cursor, statement,
325 parameters, context, executemany):
326 total = time.time() - context._query_start_time
327 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
328
329 event.listen(engine, "before_cursor_execute",
330 before_cursor_execute)
331 event.listen(engine, "after_cursor_execute",
332 after_cursor_execute)
333
334 return engine
335
336
337 def age(curdate):
338 """
339 turns a datetime into an age string.
340
341 :param curdate: datetime object
342 :rtype: unicode
343 :returns: unicode words describing age
344 """
345
346 from datetime import datetime
347 from webhelpers.date import time_ago_in_words
348
349 _ = lambda s: s
350
351 if not curdate:
352 return ''
353
354 agescales = [(_(u"year"), 3600 * 24 * 365),
355 (_(u"month"), 3600 * 24 * 30),
356 (_(u"day"), 3600 * 24),
357 (_(u"hour"), 3600),
358 (_(u"minute"), 60),
359 (_(u"second"), 1), ]
360
361 age = datetime.now() - curdate
362 age_seconds = (age.days * agescales[2][1]) + age.seconds
363 pos = 1
364 for scale in agescales:
365 if scale[1] <= age_seconds:
366 if pos == 6:
367 pos = 5
368 return '%s %s' % (time_ago_in_words(curdate,
369 agescales[pos][0]), _('ago'))
370 pos += 1
371
372 return _(u'just now')
373
374
375 def uri_filter(uri):
376 """
377 Removes user:password from given url string
378
379 :param uri:
380 :rtype: unicode
381 :returns: filtered list of strings
382 """
383 if not uri:
384 return ''
385
386 proto = ''
387
388 for pat in ('https://', 'http://'):
389 if uri.startswith(pat):
390 uri = uri[len(pat):]
391 proto = pat
392 break
393
394 # remove passwords and username
395 uri = uri[uri.find('@') + 1:]
396
397 # get the port
398 cred_pos = uri.find(':')
399 if cred_pos == -1:
400 host, port = uri, None
401 else:
402 host, port = uri[:cred_pos], uri[cred_pos + 1:]
403
404 return filter(None, [proto, host, port])
405
406
407 def credentials_filter(uri):
408 """
409 Returns a url with removed credentials
410
411 :param uri:
412 """
413
414 uri = uri_filter(uri)
415 #check if we have port
416 if len(uri) > 2 and uri[2]:
417 uri[2] = ':' + uri[2]
418
419 return ''.join(uri)
420
421
422 def get_changeset_safe(repo, rev):
423 """
424 Safe version of get_changeset if this changeset doesn't exists for a
425 repo it returns a Dummy one instead
426
427 :param repo:
428 :param rev:
429 """
430 from rhodecode.lib.vcs.backends.base import BaseRepository
431 from rhodecode.lib.vcs.exceptions import RepositoryError
432 if not isinstance(repo, BaseRepository):
433 raise Exception('You must pass an Repository '
434 'object as first argument got %s', type(repo))
435
436 try:
437 cs = repo.get_changeset(rev)
438 except RepositoryError:
439 from rhodecode.lib.utils import EmptyChangeset
440 cs = EmptyChangeset(requested_revision=rev)
441 return cs
442 2
443 3
444 4 def get_current_revision(quiet=False):
445 5 """
446 6 Returns tuple of (number, id) from repository containing this package
447 7 or None if repository could not be found.
448 8
449 9 :param quiet: prints error for fetching revision if True
450 10 """
451 11
452 12 try:
453 13 from rhodecode.lib.vcs import get_repo
454 14 from rhodecode.lib.vcs.utils.helpers import get_scm
455 15 repopath = os.path.join(os.path.dirname(__file__), '..', '..')
456 16 scm = get_scm(repopath)[0]
457 17 repo = get_repo(path=repopath, alias=scm)
458 18 tip = repo.get_changeset()
459 19 return (tip.revision, tip.short_id)
460 20 except Exception, err:
461 21 if not quiet:
462 22 print ("Cannot retrieve rhodecode's revision. Original error "
463 23 "was: %s" % err)
464 24 return None
465
466
467 def extract_mentioned_users(s):
468 """
469 Returns unique usernames from given string s that have @mention
470
471 :param s: string to get mentions
472 """
473 usrs = {}
474 for username in re.findall(r'(?:^@|\s@)(\w+)', s):
475 usrs[username] = username
476
477 return sorted(usrs.keys())
@@ -1,814 +1,814 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.auth
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 authentication and permission libraries
7 7
8 8 :created_on: Apr 4, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import random
27 27 import logging
28 28 import traceback
29 29 import hashlib
30 30
31 31 from tempfile import _RandomNameSequence
32 32 from decorator import decorator
33 33
34 34 from pylons import config, url, request
35 35 from pylons.controllers.util import abort, redirect
36 36 from pylons.i18n.translation import _
37 37
38 38 from rhodecode import __platform__, PLATFORM_WIN, PLATFORM_OTHERS
39 39 from rhodecode.model.meta import Session
40 40
41 41 if __platform__ in PLATFORM_WIN:
42 42 from hashlib import sha256
43 43 if __platform__ in PLATFORM_OTHERS:
44 44 import bcrypt
45 45
46 from rhodecode.lib import str2bool, safe_unicode
46 from rhodecode.lib.utils2 import str2bool, safe_unicode
47 47 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
48 48 from rhodecode.lib.utils import get_repo_slug, get_repos_group_slug
49 49 from rhodecode.lib.auth_ldap import AuthLdap
50 50
51 51 from rhodecode.model import meta
52 52 from rhodecode.model.user import UserModel
53 53 from rhodecode.model.db import Permission, RhodeCodeSetting, User
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class PasswordGenerator(object):
59 59 """
60 60 This is a simple class for generating password from different sets of
61 61 characters
62 62 usage::
63 63
64 64 passwd_gen = PasswordGenerator()
65 65 #print 8-letter password containing only big and small letters
66 66 of alphabet
67 67 print passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
68 68 """
69 69 ALPHABETS_NUM = r'''1234567890'''
70 70 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
71 71 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
72 72 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
73 73 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
74 74 + ALPHABETS_NUM + ALPHABETS_SPECIAL
75 75 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
76 76 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
77 77 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
78 78 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
79 79
80 80 def __init__(self, passwd=''):
81 81 self.passwd = passwd
82 82
83 83 def gen_password(self, length, type_=None):
84 84 if type_ is None:
85 85 type_ = self.ALPHABETS_FULL
86 86 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
87 87 return self.passwd
88 88
89 89
90 90 class RhodeCodeCrypto(object):
91 91
92 92 @classmethod
93 93 def hash_string(cls, str_):
94 94 """
95 95 Cryptographic function used for password hashing based on pybcrypt
96 96 or pycrypto in windows
97 97
98 98 :param password: password to hash
99 99 """
100 100 if __platform__ in PLATFORM_WIN:
101 101 return sha256(str_).hexdigest()
102 102 elif __platform__ in PLATFORM_OTHERS:
103 103 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
104 104 else:
105 105 raise Exception('Unknown or unsupported platform %s' \
106 106 % __platform__)
107 107
108 108 @classmethod
109 109 def hash_check(cls, password, hashed):
110 110 """
111 111 Checks matching password with it's hashed value, runs different
112 112 implementation based on platform it runs on
113 113
114 114 :param password: password
115 115 :param hashed: password in hashed form
116 116 """
117 117
118 118 if __platform__ in PLATFORM_WIN:
119 119 return sha256(password).hexdigest() == hashed
120 120 elif __platform__ in PLATFORM_OTHERS:
121 121 return bcrypt.hashpw(password, hashed) == hashed
122 122 else:
123 123 raise Exception('Unknown or unsupported platform %s' \
124 124 % __platform__)
125 125
126 126
127 127 def get_crypt_password(password):
128 128 return RhodeCodeCrypto.hash_string(password)
129 129
130 130
131 131 def check_password(password, hashed):
132 132 return RhodeCodeCrypto.hash_check(password, hashed)
133 133
134 134
135 135 def generate_api_key(str_, salt=None):
136 136 """
137 137 Generates API KEY from given string
138 138
139 139 :param str_:
140 140 :param salt:
141 141 """
142 142
143 143 if salt is None:
144 144 salt = _RandomNameSequence().next()
145 145
146 146 return hashlib.sha1(str_ + salt).hexdigest()
147 147
148 148
149 149 def authfunc(environ, username, password):
150 150 """
151 151 Dummy authentication wrapper function used in Mercurial and Git for
152 152 access control.
153 153
154 154 :param environ: needed only for using in Basic auth
155 155 """
156 156 return authenticate(username, password)
157 157
158 158
159 159 def authenticate(username, password):
160 160 """
161 161 Authentication function used for access control,
162 162 firstly checks for db authentication then if ldap is enabled for ldap
163 163 authentication, also creates ldap user if not in database
164 164
165 165 :param username: username
166 166 :param password: password
167 167 """
168 168
169 169 user_model = UserModel()
170 170 user = User.get_by_username(username)
171 171
172 172 log.debug('Authenticating user using RhodeCode account')
173 173 if user is not None and not user.ldap_dn:
174 174 if user.active:
175 175 if user.username == 'default' and user.active:
176 176 log.info('user %s authenticated correctly as anonymous user' %
177 177 username)
178 178 return True
179 179
180 180 elif user.username == username and check_password(password,
181 181 user.password):
182 182 log.info('user %s authenticated correctly' % username)
183 183 return True
184 184 else:
185 185 log.warning('user %s tried auth but is disabled' % username)
186 186
187 187 else:
188 188 log.debug('Regular authentication failed')
189 189 user_obj = User.get_by_username(username, case_insensitive=True)
190 190
191 191 if user_obj is not None and not user_obj.ldap_dn:
192 192 log.debug('this user already exists as non ldap')
193 193 return False
194 194
195 195 ldap_settings = RhodeCodeSetting.get_ldap_settings()
196 196 #======================================================================
197 197 # FALLBACK TO LDAP AUTH IF ENABLE
198 198 #======================================================================
199 199 if str2bool(ldap_settings.get('ldap_active')):
200 200 log.debug("Authenticating user using ldap")
201 201 kwargs = {
202 202 'server': ldap_settings.get('ldap_host', ''),
203 203 'base_dn': ldap_settings.get('ldap_base_dn', ''),
204 204 'port': ldap_settings.get('ldap_port'),
205 205 'bind_dn': ldap_settings.get('ldap_dn_user'),
206 206 'bind_pass': ldap_settings.get('ldap_dn_pass'),
207 207 'tls_kind': ldap_settings.get('ldap_tls_kind'),
208 208 'tls_reqcert': ldap_settings.get('ldap_tls_reqcert'),
209 209 'ldap_filter': ldap_settings.get('ldap_filter'),
210 210 'search_scope': ldap_settings.get('ldap_search_scope'),
211 211 'attr_login': ldap_settings.get('ldap_attr_login'),
212 212 'ldap_version': 3,
213 213 }
214 214 log.debug('Checking for ldap authentication')
215 215 try:
216 216 aldap = AuthLdap(**kwargs)
217 217 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username,
218 218 password)
219 219 log.debug('Got ldap DN response %s' % user_dn)
220 220
221 221 get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\
222 222 .get(k), [''])[0]
223 223
224 224 user_attrs = {
225 225 'name': safe_unicode(get_ldap_attr('ldap_attr_firstname')),
226 226 'lastname': safe_unicode(get_ldap_attr('ldap_attr_lastname')),
227 227 'email': get_ldap_attr('ldap_attr_email'),
228 228 }
229 229
230 230 # don't store LDAP password since we don't need it. Override
231 231 # with some random generated password
232 232 _password = PasswordGenerator().gen_password(length=8)
233 233 # create this user on the fly if it doesn't exist in rhodecode
234 234 # database
235 235 if user_model.create_ldap(username, _password, user_dn,
236 236 user_attrs):
237 237 log.info('created new ldap user %s' % username)
238 238
239 239 Session.commit()
240 240 return True
241 241 except (LdapUsernameError, LdapPasswordError,):
242 242 pass
243 243 except (Exception,):
244 244 log.error(traceback.format_exc())
245 245 pass
246 246 return False
247 247
248 248
249 249 def login_container_auth(username):
250 250 user = User.get_by_username(username)
251 251 if user is None:
252 252 user_attrs = {
253 253 'name': username,
254 254 'lastname': None,
255 255 'email': None,
256 256 }
257 257 user = UserModel().create_for_container_auth(username, user_attrs)
258 258 if not user:
259 259 return None
260 260 log.info('User %s was created by container authentication' % username)
261 261
262 262 if not user.active:
263 263 return None
264 264
265 265 user.update_lastlogin()
266 266 Session.commit()
267 267
268 268 log.debug('User %s is now logged in by container authentication',
269 269 user.username)
270 270 return user
271 271
272 272
273 273 def get_container_username(environ, config):
274 274 username = None
275 275
276 276 if str2bool(config.get('container_auth_enabled', False)):
277 277 from paste.httpheaders import REMOTE_USER
278 278 username = REMOTE_USER(environ)
279 279
280 280 if not username and str2bool(config.get('proxypass_auth_enabled', False)):
281 281 username = environ.get('HTTP_X_FORWARDED_USER')
282 282
283 283 if username:
284 284 # Removing realm and domain from username
285 285 username = username.partition('@')[0]
286 286 username = username.rpartition('\\')[2]
287 287 log.debug('Received username %s from container' % username)
288 288
289 289 return username
290 290
291 291
292 292 class CookieStoreWrapper(object):
293 293
294 294 def __init__(self, cookie_store):
295 295 self.cookie_store = cookie_store
296 296
297 297 def __repr__(self):
298 298 return 'CookieStore<%s>' % (self.cookie_store)
299 299
300 300 def get(self, key, other=None):
301 301 if isinstance(self.cookie_store, dict):
302 302 return self.cookie_store.get(key, other)
303 303 elif isinstance(self.cookie_store, AuthUser):
304 304 return self.cookie_store.__dict__.get(key, other)
305 305
306 306
307 307 class AuthUser(object):
308 308 """
309 309 A simple object that handles all attributes of user in RhodeCode
310 310
311 311 It does lookup based on API key,given user, or user present in session
312 312 Then it fills all required information for such user. It also checks if
313 313 anonymous access is enabled and if so, it returns default user as logged
314 314 in
315 315 """
316 316
317 317 def __init__(self, user_id=None, api_key=None, username=None):
318 318
319 319 self.user_id = user_id
320 320 self.api_key = None
321 321 self.username = username
322 322
323 323 self.name = ''
324 324 self.lastname = ''
325 325 self.email = ''
326 326 self.is_authenticated = False
327 327 self.admin = False
328 328 self.permissions = {}
329 329 self._api_key = api_key
330 330 self.propagate_data()
331 331 self._instance = None
332 332
333 333 def propagate_data(self):
334 334 user_model = UserModel()
335 335 self.anonymous_user = User.get_by_username('default', cache=True)
336 336 is_user_loaded = False
337 337
338 338 # try go get user by api key
339 339 if self._api_key and self._api_key != self.anonymous_user.api_key:
340 340 log.debug('Auth User lookup by API KEY %s' % self._api_key)
341 341 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
342 342 # lookup by userid
343 343 elif (self.user_id is not None and
344 344 self.user_id != self.anonymous_user.user_id):
345 345 log.debug('Auth User lookup by USER ID %s' % self.user_id)
346 346 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
347 347 # lookup by username
348 348 elif self.username and \
349 349 str2bool(config.get('container_auth_enabled', False)):
350 350
351 351 log.debug('Auth User lookup by USER NAME %s' % self.username)
352 352 dbuser = login_container_auth(self.username)
353 353 if dbuser is not None:
354 354 for k, v in dbuser.get_dict().items():
355 355 setattr(self, k, v)
356 356 self.set_authenticated()
357 357 is_user_loaded = True
358 358 else:
359 359 log.debug('No data in %s that could been used to log in' % self)
360 360
361 361 if not is_user_loaded:
362 362 # if we cannot authenticate user try anonymous
363 363 if self.anonymous_user.active is True:
364 364 user_model.fill_data(self, user_id=self.anonymous_user.user_id)
365 365 # then we set this user is logged in
366 366 self.is_authenticated = True
367 367 else:
368 368 self.user_id = None
369 369 self.username = None
370 370 self.is_authenticated = False
371 371
372 372 if not self.username:
373 373 self.username = 'None'
374 374
375 375 log.debug('Auth User is now %s' % self)
376 376 user_model.fill_perms(self)
377 377
378 378 @property
379 379 def is_admin(self):
380 380 return self.admin
381 381
382 382 def __repr__(self):
383 383 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
384 384 self.is_authenticated)
385 385
386 386 def set_authenticated(self, authenticated=True):
387 387 if self.user_id != self.anonymous_user.user_id:
388 388 self.is_authenticated = authenticated
389 389
390 390 def get_cookie_store(self):
391 391 return {'username': self.username,
392 392 'user_id': self.user_id,
393 393 'is_authenticated': self.is_authenticated}
394 394
395 395 @classmethod
396 396 def from_cookie_store(cls, cookie_store):
397 397 """
398 398 Creates AuthUser from a cookie store
399 399
400 400 :param cls:
401 401 :param cookie_store:
402 402 """
403 403 user_id = cookie_store.get('user_id')
404 404 username = cookie_store.get('username')
405 405 api_key = cookie_store.get('api_key')
406 406 return AuthUser(user_id, api_key, username)
407 407
408 408
409 409 def set_available_permissions(config):
410 410 """
411 411 This function will propagate pylons globals with all available defined
412 412 permission given in db. We don't want to check each time from db for new
413 413 permissions since adding a new permission also requires application restart
414 414 ie. to decorate new views with the newly created permission
415 415
416 416 :param config: current pylons config instance
417 417
418 418 """
419 419 log.info('getting information about all available permissions')
420 420 try:
421 421 sa = meta.Session
422 422 all_perms = sa.query(Permission).all()
423 423 except Exception:
424 424 pass
425 425 finally:
426 426 meta.Session.remove()
427 427
428 428 config['available_permissions'] = [x.permission_name for x in all_perms]
429 429
430 430
431 431 #==============================================================================
432 432 # CHECK DECORATORS
433 433 #==============================================================================
434 434 class LoginRequired(object):
435 435 """
436 436 Must be logged in to execute this function else
437 437 redirect to login page
438 438
439 439 :param api_access: if enabled this checks only for valid auth token
440 440 and grants access based on valid token
441 441 """
442 442
443 443 def __init__(self, api_access=False):
444 444 self.api_access = api_access
445 445
446 446 def __call__(self, func):
447 447 return decorator(self.__wrapper, func)
448 448
449 449 def __wrapper(self, func, *fargs, **fkwargs):
450 450 cls = fargs[0]
451 451 user = cls.rhodecode_user
452 452
453 453 api_access_ok = False
454 454 if self.api_access:
455 455 log.debug('Checking API KEY access for %s' % cls)
456 456 if user.api_key == request.GET.get('api_key'):
457 457 api_access_ok = True
458 458 else:
459 459 log.debug("API KEY token not valid")
460 460 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
461 461 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
462 462 if user.is_authenticated or api_access_ok:
463 463 log.info('user %s is authenticated and granted access to %s' % (
464 464 user.username, loc)
465 465 )
466 466 return func(*fargs, **fkwargs)
467 467 else:
468 468 log.warn('user %s NOT authenticated on func: %s' % (
469 469 user, loc)
470 470 )
471 471 p = url.current()
472 472
473 473 log.debug('redirecting to login page with %s' % p)
474 474 return redirect(url('login_home', came_from=p))
475 475
476 476
477 477 class NotAnonymous(object):
478 478 """
479 479 Must be logged in to execute this function else
480 480 redirect to login page"""
481 481
482 482 def __call__(self, func):
483 483 return decorator(self.__wrapper, func)
484 484
485 485 def __wrapper(self, func, *fargs, **fkwargs):
486 486 cls = fargs[0]
487 487 self.user = cls.rhodecode_user
488 488
489 489 log.debug('Checking if user is not anonymous @%s' % cls)
490 490
491 491 anonymous = self.user.username == 'default'
492 492
493 493 if anonymous:
494 494 p = url.current()
495 495
496 496 import rhodecode.lib.helpers as h
497 497 h.flash(_('You need to be a registered user to '
498 498 'perform this action'),
499 499 category='warning')
500 500 return redirect(url('login_home', came_from=p))
501 501 else:
502 502 return func(*fargs, **fkwargs)
503 503
504 504
505 505 class PermsDecorator(object):
506 506 """Base class for controller decorators"""
507 507
508 508 def __init__(self, *required_perms):
509 509 available_perms = config['available_permissions']
510 510 for perm in required_perms:
511 511 if perm not in available_perms:
512 512 raise Exception("'%s' permission is not defined" % perm)
513 513 self.required_perms = set(required_perms)
514 514 self.user_perms = None
515 515
516 516 def __call__(self, func):
517 517 return decorator(self.__wrapper, func)
518 518
519 519 def __wrapper(self, func, *fargs, **fkwargs):
520 520 cls = fargs[0]
521 521 self.user = cls.rhodecode_user
522 522 self.user_perms = self.user.permissions
523 523 log.debug('checking %s permissions %s for %s %s',
524 524 self.__class__.__name__, self.required_perms, cls,
525 525 self.user)
526 526
527 527 if self.check_permissions():
528 528 log.debug('Permission granted for %s %s' % (cls, self.user))
529 529 return func(*fargs, **fkwargs)
530 530
531 531 else:
532 532 log.debug('Permission denied for %s %s' % (cls, self.user))
533 533 anonymous = self.user.username == 'default'
534 534
535 535 if anonymous:
536 536 p = url.current()
537 537
538 538 import rhodecode.lib.helpers as h
539 539 h.flash(_('You need to be a signed in to '
540 540 'view this page'),
541 541 category='warning')
542 542 return redirect(url('login_home', came_from=p))
543 543
544 544 else:
545 545 # redirect with forbidden ret code
546 546 return abort(403)
547 547
548 548 def check_permissions(self):
549 549 """Dummy function for overriding"""
550 550 raise Exception('You have to write this function in child class')
551 551
552 552
553 553 class HasPermissionAllDecorator(PermsDecorator):
554 554 """
555 555 Checks for access permission for all given predicates. All of them
556 556 have to be meet in order to fulfill the request
557 557 """
558 558
559 559 def check_permissions(self):
560 560 if self.required_perms.issubset(self.user_perms.get('global')):
561 561 return True
562 562 return False
563 563
564 564
565 565 class HasPermissionAnyDecorator(PermsDecorator):
566 566 """
567 567 Checks for access permission for any of given predicates. In order to
568 568 fulfill the request any of predicates must be meet
569 569 """
570 570
571 571 def check_permissions(self):
572 572 if self.required_perms.intersection(self.user_perms.get('global')):
573 573 return True
574 574 return False
575 575
576 576
577 577 class HasRepoPermissionAllDecorator(PermsDecorator):
578 578 """
579 579 Checks for access permission for all given predicates for specific
580 580 repository. All of them have to be meet in order to fulfill the request
581 581 """
582 582
583 583 def check_permissions(self):
584 584 repo_name = get_repo_slug(request)
585 585 try:
586 586 user_perms = set([self.user_perms['repositories'][repo_name]])
587 587 except KeyError:
588 588 return False
589 589 if self.required_perms.issubset(user_perms):
590 590 return True
591 591 return False
592 592
593 593
594 594 class HasRepoPermissionAnyDecorator(PermsDecorator):
595 595 """
596 596 Checks for access permission for any of given predicates for specific
597 597 repository. In order to fulfill the request any of predicates must be meet
598 598 """
599 599
600 600 def check_permissions(self):
601 601 repo_name = get_repo_slug(request)
602 602
603 603 try:
604 604 user_perms = set([self.user_perms['repositories'][repo_name]])
605 605 except KeyError:
606 606 return False
607 607 if self.required_perms.intersection(user_perms):
608 608 return True
609 609 return False
610 610
611 611
612 612 class HasReposGroupPermissionAllDecorator(PermsDecorator):
613 613 """
614 614 Checks for access permission for all given predicates for specific
615 615 repository. All of them have to be meet in order to fulfill the request
616 616 """
617 617
618 618 def check_permissions(self):
619 619 group_name = get_repos_group_slug(request)
620 620 try:
621 621 user_perms = set([self.user_perms['repositories_groups'][group_name]])
622 622 except KeyError:
623 623 return False
624 624 if self.required_perms.issubset(user_perms):
625 625 return True
626 626 return False
627 627
628 628
629 629 class HasReposGroupPermissionAnyDecorator(PermsDecorator):
630 630 """
631 631 Checks for access permission for any of given predicates for specific
632 632 repository. In order to fulfill the request any of predicates must be meet
633 633 """
634 634
635 635 def check_permissions(self):
636 636 group_name = get_repos_group_slug(request)
637 637
638 638 try:
639 639 user_perms = set([self.user_perms['repositories_groups'][group_name]])
640 640 except KeyError:
641 641 return False
642 642 if self.required_perms.intersection(user_perms):
643 643 return True
644 644 return False
645 645
646 646
647 647 #==============================================================================
648 648 # CHECK FUNCTIONS
649 649 #==============================================================================
650 650 class PermsFunction(object):
651 651 """Base function for other check functions"""
652 652
653 653 def __init__(self, *perms):
654 654 available_perms = config['available_permissions']
655 655
656 656 for perm in perms:
657 657 if perm not in available_perms:
658 658 raise Exception("'%s' permission is not defined" % perm)
659 659 self.required_perms = set(perms)
660 660 self.user_perms = None
661 661 self.granted_for = ''
662 662 self.repo_name = None
663 663
664 664 def __call__(self, check_Location=''):
665 665 user = request.user
666 666 log.debug('checking %s %s %s', self.__class__.__name__,
667 667 self.required_perms, user)
668 668 if not user:
669 669 log.debug('Empty request user')
670 670 return False
671 671 self.user_perms = user.permissions
672 672 self.granted_for = user
673 673
674 674 if self.check_permissions():
675 675 log.debug('Permission granted %s @ %s', self.granted_for,
676 676 check_Location or 'unspecified location')
677 677 return True
678 678
679 679 else:
680 680 log.debug('Permission denied for %s @ %s', self.granted_for,
681 681 check_Location or 'unspecified location')
682 682 return False
683 683
684 684 def check_permissions(self):
685 685 """Dummy function for overriding"""
686 686 raise Exception('You have to write this function in child class')
687 687
688 688
689 689 class HasPermissionAll(PermsFunction):
690 690 def check_permissions(self):
691 691 if self.required_perms.issubset(self.user_perms.get('global')):
692 692 return True
693 693 return False
694 694
695 695
696 696 class HasPermissionAny(PermsFunction):
697 697 def check_permissions(self):
698 698 if self.required_perms.intersection(self.user_perms.get('global')):
699 699 return True
700 700 return False
701 701
702 702
703 703 class HasRepoPermissionAll(PermsFunction):
704 704
705 705 def __call__(self, repo_name=None, check_Location=''):
706 706 self.repo_name = repo_name
707 707 return super(HasRepoPermissionAll, self).__call__(check_Location)
708 708
709 709 def check_permissions(self):
710 710 if not self.repo_name:
711 711 self.repo_name = get_repo_slug(request)
712 712
713 713 try:
714 714 self.user_perms = set(
715 715 [self.user_perms['repositories'][self.repo_name]]
716 716 )
717 717 except KeyError:
718 718 return False
719 719 self.granted_for = self.repo_name
720 720 if self.required_perms.issubset(self.user_perms):
721 721 return True
722 722 return False
723 723
724 724
725 725 class HasRepoPermissionAny(PermsFunction):
726 726
727 727 def __call__(self, repo_name=None, check_Location=''):
728 728 self.repo_name = repo_name
729 729 return super(HasRepoPermissionAny, self).__call__(check_Location)
730 730
731 731 def check_permissions(self):
732 732 if not self.repo_name:
733 733 self.repo_name = get_repo_slug(request)
734 734
735 735 try:
736 736 self.user_perms = set(
737 737 [self.user_perms['repositories'][self.repo_name]]
738 738 )
739 739 except KeyError:
740 740 return False
741 741 self.granted_for = self.repo_name
742 742 if self.required_perms.intersection(self.user_perms):
743 743 return True
744 744 return False
745 745
746 746
747 747 class HasReposGroupPermissionAny(PermsFunction):
748 748 def __call__(self, group_name=None, check_Location=''):
749 749 self.group_name = group_name
750 750 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
751 751
752 752 def check_permissions(self):
753 753 try:
754 754 self.user_perms = set(
755 755 [self.user_perms['repositories_groups'][self.group_name]]
756 756 )
757 757 except KeyError:
758 758 return False
759 759 self.granted_for = self.repo_name
760 760 if self.required_perms.intersection(self.user_perms):
761 761 return True
762 762 return False
763 763
764 764
765 765 class HasReposGroupPermissionAll(PermsFunction):
766 766 def __call__(self, group_name=None, check_Location=''):
767 767 self.group_name = group_name
768 768 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
769 769
770 770 def check_permissions(self):
771 771 try:
772 772 self.user_perms = set(
773 773 [self.user_perms['repositories_groups'][self.group_name]]
774 774 )
775 775 except KeyError:
776 776 return False
777 777 self.granted_for = self.repo_name
778 778 if self.required_perms.issubset(self.user_perms):
779 779 return True
780 780 return False
781 781
782 782
783 783 #==============================================================================
784 784 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
785 785 #==============================================================================
786 786 class HasPermissionAnyMiddleware(object):
787 787 def __init__(self, *perms):
788 788 self.required_perms = set(perms)
789 789
790 790 def __call__(self, user, repo_name):
791 791 # repo_name MUST be unicode, since we handle keys in permission
792 792 # dict by unicode
793 793 repo_name = safe_unicode(repo_name)
794 794 usr = AuthUser(user.user_id)
795 795 try:
796 796 self.user_perms = set([usr.permissions['repositories'][repo_name]])
797 797 except Exception:
798 log.error('Exception while accessing permissions %s' %
798 log.error('Exception while accessing permissions %s' %
799 799 traceback.format_exc())
800 800 self.user_perms = set()
801 801 self.granted_for = ''
802 802 self.username = user.username
803 803 self.repo_name = repo_name
804 804 return self.check_permissions()
805 805
806 806 def check_permissions(self):
807 807 log.debug('checking mercurial protocol '
808 808 'permissions %s for user:%s repository:%s', self.user_perms,
809 809 self.username, self.repo_name)
810 810 if self.required_perms.intersection(self.user_perms):
811 811 log.debug('permission granted')
812 812 return True
813 813 log.debug('permission denied')
814 814 return False
@@ -1,183 +1,183 b''
1 1 """The base Controller API
2 2
3 3 Provides the BaseController class for subclassing.
4 4 """
5 5 import logging
6 6 import time
7 7 import traceback
8 8
9 9 from paste.auth.basic import AuthBasicAuthenticator
10 10
11 11 from pylons import config, tmpl_context as c, request, session, url
12 12 from pylons.controllers import WSGIController
13 13 from pylons.controllers.util import redirect
14 14 from pylons.templating import render_mako as render
15 15
16 16 from rhodecode import __version__, BACKENDS
17 17
18 from rhodecode.lib import str2bool, safe_unicode
18 from rhodecode.lib.utils2 import str2bool, safe_unicode
19 19 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
20 20 HasPermissionAnyMiddleware, CookieStoreWrapper
21 21 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
22 22 from rhodecode.model import meta
23 23
24 24 from rhodecode.model.db import Repository
25 25 from rhodecode.model.notification import NotificationModel
26 26 from rhodecode.model.scm import ScmModel
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 class BaseVCSController(object):
32 32
33 33 def __init__(self, application, config):
34 34 self.application = application
35 35 self.config = config
36 36 # base path of repo locations
37 37 self.basepath = self.config['base_path']
38 38 #authenticate this mercurial request using authfunc
39 39 self.authenticate = AuthBasicAuthenticator('', authfunc)
40 40 self.ipaddr = '0.0.0.0'
41 41
42 42 def _handle_request(self, environ, start_response):
43 43 raise NotImplementedError()
44 44
45 45 def _get_by_id(self, repo_name):
46 46 """
47 47 Get's a special pattern _<ID> from clone url and tries to replace it
48 48 with a repository_name for support of _<ID> non changable urls
49 49
50 50 :param repo_name:
51 51 """
52 52 try:
53 53 data = repo_name.split('/')
54 54 if len(data) >= 2:
55 55 by_id = data[1].split('_')
56 56 if len(by_id) == 2 and by_id[1].isdigit():
57 57 _repo_name = Repository.get(by_id[1]).repo_name
58 58 data[1] = _repo_name
59 59 except:
60 60 log.debug('Failed to extract repo_name from id %s' % (
61 61 traceback.format_exc()
62 62 )
63 63 )
64 64
65 65 return '/'.join(data)
66 66
67 67 def _invalidate_cache(self, repo_name):
68 68 """
69 69 Set's cache for this repository for invalidation on next access
70 70
71 71 :param repo_name: full repo name, also a cache key
72 72 """
73 73 invalidate_cache('get_repo_cached_%s' % repo_name)
74 74
75 75 def _check_permission(self, action, user, repo_name):
76 76 """
77 77 Checks permissions using action (push/pull) user and repository
78 78 name
79 79
80 80 :param action: push or pull action
81 81 :param user: user instance
82 82 :param repo_name: repository name
83 83 """
84 84 if action == 'push':
85 85 if not HasPermissionAnyMiddleware('repository.write',
86 86 'repository.admin')(user,
87 87 repo_name):
88 88 return False
89 89
90 90 else:
91 91 #any other action need at least read permission
92 92 if not HasPermissionAnyMiddleware('repository.read',
93 93 'repository.write',
94 94 'repository.admin')(user,
95 95 repo_name):
96 96 return False
97 97
98 98 return True
99 99
100 100 def __call__(self, environ, start_response):
101 101 start = time.time()
102 102 try:
103 103 return self._handle_request(environ, start_response)
104 104 finally:
105 105 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
106 106 log.debug('Request time: %.3fs' % (time.time() - start))
107 107 meta.Session.remove()
108 108
109 109
110 110 class BaseController(WSGIController):
111 111
112 112 def __before__(self):
113 113 c.rhodecode_version = __version__
114 114 c.rhodecode_instanceid = config.get('instance_id')
115 115 c.rhodecode_name = config.get('rhodecode_title')
116 116 c.use_gravatar = str2bool(config.get('use_gravatar'))
117 117 c.ga_code = config.get('rhodecode_ga_code')
118 118 c.repo_name = get_repo_slug(request)
119 119 c.backends = BACKENDS.keys()
120 120 c.unread_notifications = NotificationModel()\
121 121 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
122 122 self.cut_off_limit = int(config.get('cut_off_limit'))
123 123
124 124 self.sa = meta.Session
125 125 self.scm_model = ScmModel(self.sa)
126 126
127 127 def __call__(self, environ, start_response):
128 128 """Invoke the Controller"""
129 129 # WSGIController.__call__ dispatches to the Controller method
130 130 # the request is routed to. This routing information is
131 131 # available in environ['pylons.routes_dict']
132 132 start = time.time()
133 133 try:
134 134 # make sure that we update permissions each time we call controller
135 135 api_key = request.GET.get('api_key')
136 136 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
137 137 user_id = cookie_store.get('user_id', None)
138 138 username = get_container_username(environ, config)
139 139 auth_user = AuthUser(user_id, api_key, username)
140 140 request.user = auth_user
141 141 self.rhodecode_user = c.rhodecode_user = auth_user
142 142 if not self.rhodecode_user.is_authenticated and \
143 143 self.rhodecode_user.user_id is not None:
144 144 self.rhodecode_user.set_authenticated(
145 145 cookie_store.get('is_authenticated')
146 146 )
147 147 log.info('User: %s accessed %s' % (
148 148 auth_user, safe_unicode(environ.get('PATH_INFO')))
149 149 )
150 150 return WSGIController.__call__(self, environ, start_response)
151 151 finally:
152 152 log.info('Request to %s time: %.3fs' % (
153 153 safe_unicode(environ.get('PATH_INFO')), time.time() - start)
154 154 )
155 155 meta.Session.remove()
156 156
157 157
158 158 class BaseRepoController(BaseController):
159 159 """
160 160 Base class for controllers responsible for loading all needed data for
161 161 repository loaded items are
162 162
163 163 c.rhodecode_repo: instance of scm repository
164 164 c.rhodecode_db_repo: instance of db
165 165 c.repository_followers: number of followers
166 166 c.repository_forks: number of forks
167 167 """
168 168
169 169 def __before__(self):
170 170 super(BaseRepoController, self).__before__()
171 171 if c.repo_name:
172 172
173 173 c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
174 174 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
175 175
176 176 if c.rhodecode_repo is None:
177 177 log.error('%s this repository is present in database but it '
178 178 'cannot be created as an scm instance', c.repo_name)
179 179
180 180 redirect(url('home'))
181 181
182 182 c.repository_followers = self.scm_model.get_followers(c.repo_name)
183 183 c.repository_forks = self.scm_model.get_forks(c.repo_name)
@@ -1,301 +1,301 b''
1 1 """caching_query.py
2 2
3 3 Represent persistence structures which allow the usage of
4 4 Beaker caching with SQLAlchemy.
5 5
6 6 The three new concepts introduced here are:
7 7
8 8 * CachingQuery - a Query subclass that caches and
9 9 retrieves results in/from Beaker.
10 10 * FromCache - a query option that establishes caching
11 11 parameters on a Query
12 12 * RelationshipCache - a variant of FromCache which is specific
13 13 to a query invoked during a lazy load.
14 14 * _params_from_query - extracts value parameters from
15 15 a Query.
16 16
17 17 The rest of what's here are standard SQLAlchemy and
18 18 Beaker constructs.
19 19
20 20 """
21 21 import beaker
22 22 from beaker.exceptions import BeakerException
23 23
24 24 from sqlalchemy.orm.interfaces import MapperOption
25 25 from sqlalchemy.orm.query import Query
26 26 from sqlalchemy.sql import visitors
27 from rhodecode.lib import safe_str
27 from rhodecode.lib.utils2 import safe_str
28 28
29 29
30 30 class CachingQuery(Query):
31 31 """A Query subclass which optionally loads full results from a Beaker
32 32 cache region.
33 33
34 34 The CachingQuery stores additional state that allows it to consult
35 35 a Beaker cache before accessing the database:
36 36
37 37 * A "region", which is a cache region argument passed to a
38 38 Beaker CacheManager, specifies a particular cache configuration
39 39 (including backend implementation, expiration times, etc.)
40 40 * A "namespace", which is a qualifying name that identifies a
41 41 group of keys within the cache. A query that filters on a name
42 42 might use the name "by_name", a query that filters on a date range
43 43 to a joined table might use the name "related_date_range".
44 44
45 45 When the above state is present, a Beaker cache is retrieved.
46 46
47 47 The "namespace" name is first concatenated with
48 48 a string composed of the individual entities and columns the Query
49 49 requests, i.e. such as ``Query(User.id, User.name)``.
50 50
51 51 The Beaker cache is then loaded from the cache manager based
52 52 on the region and composed namespace. The key within the cache
53 53 itself is then constructed against the bind parameters specified
54 54 by this query, which are usually literals defined in the
55 55 WHERE clause.
56 56
57 57 The FromCache and RelationshipCache mapper options below represent
58 58 the "public" method of configuring this state upon the CachingQuery.
59 59
60 60 """
61 61
62 62 def __init__(self, manager, *args, **kw):
63 63 self.cache_manager = manager
64 64 Query.__init__(self, *args, **kw)
65 65
66 66 def __iter__(self):
67 67 """override __iter__ to pull results from Beaker
68 68 if particular attributes have been configured.
69 69
70 70 Note that this approach does *not* detach the loaded objects from
71 71 the current session. If the cache backend is an in-process cache
72 72 (like "memory") and lives beyond the scope of the current session's
73 73 transaction, those objects may be expired. The method here can be
74 74 modified to first expunge() each loaded item from the current
75 75 session before returning the list of items, so that the items
76 76 in the cache are not the same ones in the current Session.
77 77
78 78 """
79 79 if hasattr(self, '_cache_parameters'):
80 80 return self.get_value(createfunc=lambda:
81 81 list(Query.__iter__(self)))
82 82 else:
83 83 return Query.__iter__(self)
84 84
85 85 def invalidate(self):
86 86 """Invalidate the value represented by this Query."""
87 87
88 88 cache, cache_key = _get_cache_parameters(self)
89 89 cache.remove(cache_key)
90 90
91 91 def get_value(self, merge=True, createfunc=None):
92 92 """Return the value from the cache for this query.
93 93
94 94 Raise KeyError if no value present and no
95 95 createfunc specified.
96 96
97 97 """
98 98 cache, cache_key = _get_cache_parameters(self)
99 99 ret = cache.get_value(cache_key, createfunc=createfunc)
100 100 if merge:
101 101 ret = self.merge_result(ret, load=False)
102 102 return ret
103 103
104 104 def set_value(self, value):
105 105 """Set the value in the cache for this query."""
106 106
107 107 cache, cache_key = _get_cache_parameters(self)
108 108 cache.put(cache_key, value)
109 109
110 110
111 111 def query_callable(manager, query_cls=CachingQuery):
112 112 def query(*arg, **kw):
113 113 return query_cls(manager, *arg, **kw)
114 114 return query
115 115
116 116
117 117 def get_cache_region(name, region):
118 118 if region not in beaker.cache.cache_regions:
119 119 raise BeakerException('Cache region `%s` not configured '
120 120 'Check if proper cache settings are in the .ini files' % region)
121 121 kw = beaker.cache.cache_regions[region]
122 122 return beaker.cache.Cache._get_cache(name, kw)
123 123
124 124
125 125 def _get_cache_parameters(query):
126 126 """For a query with cache_region and cache_namespace configured,
127 127 return the correspoinding Cache instance and cache key, based
128 128 on this query's current criterion and parameter values.
129 129
130 130 """
131 131 if not hasattr(query, '_cache_parameters'):
132 132 raise ValueError("This Query does not have caching "
133 133 "parameters configured.")
134 134
135 135 region, namespace, cache_key = query._cache_parameters
136 136
137 137 namespace = _namespace_from_query(namespace, query)
138 138
139 139 if cache_key is None:
140 140 # cache key - the value arguments from this query's parameters.
141 141 args = [safe_str(x) for x in _params_from_query(query)]
142 142 args.extend(filter(lambda k: k not in ['None', None, u'None'],
143 143 [str(query._limit), str(query._offset)]))
144 144
145 145 cache_key = " ".join(args)
146 146
147 147 if cache_key is None:
148 148 raise Exception('Cache key cannot be None')
149 149
150 150 # get cache
151 151 #cache = query.cache_manager.get_cache_region(namespace, region)
152 152 cache = get_cache_region(namespace, region)
153 153 # optional - hash the cache_key too for consistent length
154 154 # import uuid
155 155 # cache_key= str(uuid.uuid5(uuid.NAMESPACE_DNS, cache_key))
156 156
157 157 return cache, cache_key
158 158
159 159
160 160 def _namespace_from_query(namespace, query):
161 161 # cache namespace - the token handed in by the
162 162 # option + class we're querying against
163 163 namespace = " ".join([namespace] + [str(x) for x in query._entities])
164 164
165 165 # memcached wants this
166 166 namespace = namespace.replace(' ', '_')
167 167
168 168 return namespace
169 169
170 170
171 171 def _set_cache_parameters(query, region, namespace, cache_key):
172 172
173 173 if hasattr(query, '_cache_parameters'):
174 174 region, namespace, cache_key = query._cache_parameters
175 175 raise ValueError("This query is already configured "
176 176 "for region %r namespace %r" %
177 177 (region, namespace)
178 178 )
179 179 query._cache_parameters = region, namespace, cache_key
180 180
181 181
182 182 class FromCache(MapperOption):
183 183 """Specifies that a Query should load results from a cache."""
184 184
185 185 propagate_to_loaders = False
186 186
187 187 def __init__(self, region, namespace, cache_key=None):
188 188 """Construct a new FromCache.
189 189
190 190 :param region: the cache region. Should be a
191 191 region configured in the Beaker CacheManager.
192 192
193 193 :param namespace: the cache namespace. Should
194 194 be a name uniquely describing the target Query's
195 195 lexical structure.
196 196
197 197 :param cache_key: optional. A string cache key
198 198 that will serve as the key to the query. Use this
199 199 if your query has a huge amount of parameters (such
200 200 as when using in_()) which correspond more simply to
201 201 some other identifier.
202 202
203 203 """
204 204 self.region = region
205 205 self.namespace = namespace
206 206 self.cache_key = cache_key
207 207
208 208 def process_query(self, query):
209 209 """Process a Query during normal loading operation."""
210 210
211 211 _set_cache_parameters(query, self.region, self.namespace,
212 212 self.cache_key)
213 213
214 214
215 215 class RelationshipCache(MapperOption):
216 216 """Specifies that a Query as called within a "lazy load"
217 217 should load results from a cache."""
218 218
219 219 propagate_to_loaders = True
220 220
221 221 def __init__(self, region, namespace, attribute):
222 222 """Construct a new RelationshipCache.
223 223
224 224 :param region: the cache region. Should be a
225 225 region configured in the Beaker CacheManager.
226 226
227 227 :param namespace: the cache namespace. Should
228 228 be a name uniquely describing the target Query's
229 229 lexical structure.
230 230
231 231 :param attribute: A Class.attribute which
232 232 indicates a particular class relationship() whose
233 233 lazy loader should be pulled from the cache.
234 234
235 235 """
236 236 self.region = region
237 237 self.namespace = namespace
238 238 self._relationship_options = {
239 239 (attribute.property.parent.class_, attribute.property.key): self
240 240 }
241 241
242 242 def process_query_conditionally(self, query):
243 243 """Process a Query that is used within a lazy loader.
244 244
245 245 (the process_query_conditionally() method is a SQLAlchemy
246 246 hook invoked only within lazyload.)
247 247
248 248 """
249 249 if query._current_path:
250 250 mapper, key = query._current_path[-2:]
251 251
252 252 for cls in mapper.class_.__mro__:
253 253 if (cls, key) in self._relationship_options:
254 254 relationship_option = \
255 255 self._relationship_options[(cls, key)]
256 256 _set_cache_parameters(
257 257 query,
258 258 relationship_option.region,
259 259 relationship_option.namespace,
260 260 None)
261 261
262 262 def and_(self, option):
263 263 """Chain another RelationshipCache option to this one.
264 264
265 265 While many RelationshipCache objects can be specified on a single
266 266 Query separately, chaining them together allows for a more efficient
267 267 lookup during load.
268 268
269 269 """
270 270 self._relationship_options.update(option._relationship_options)
271 271 return self
272 272
273 273
274 274 def _params_from_query(query):
275 275 """Pull the bind parameter values from a query.
276 276
277 277 This takes into account any scalar attribute bindparam set up.
278 278
279 279 E.g. params_from_query(query.filter(Cls.foo==5).filter(Cls.bar==7)))
280 280 would return [5, 7].
281 281
282 282 """
283 283 v = []
284 284 def visit_bindparam(bind):
285 285
286 286 if bind.key in query._params:
287 287 value = query._params[bind.key]
288 288 elif bind.callable:
289 289 # lazyloader may dig a callable in here, intended
290 290 # to late-evaluate params after autoflush is called.
291 291 # convert to a scalar value.
292 292 value = bind.callable()
293 293 else:
294 294 value = bind.value
295 295
296 296 v.append(value)
297 297 if query._criterion is not None:
298 298 visitors.traverse(query._criterion, {}, {'bindparam':visit_bindparam})
299 299 for f in query._from_obj:
300 300 visitors.traverse(f, {}, {'bindparam':visit_bindparam})
301 301 return v
@@ -1,128 +1,128 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.celerylib.__init__
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 celery libs for RhodeCode
7 7
8 8 :created_on: Nov 27, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import sys
28 28 import socket
29 29 import traceback
30 30 import logging
31 31 from os.path import dirname as dn, join as jn
32 32 from pylons import config
33 33
34 34 from hashlib import md5
35 35 from decorator import decorator
36 36
37 37 from rhodecode.lib.vcs.utils.lazy import LazyProperty
38 38 from rhodecode import CELERY_ON
39 from rhodecode.lib import str2bool, safe_str
39 from rhodecode.lib.utils2 import str2bool, safe_str
40 40 from rhodecode.lib.pidlock import DaemonLock, LockHeld
41 41 from rhodecode.model import init_model
42 42 from rhodecode.model import meta
43 43 from rhodecode.model.db import Statistics, Repository, User
44 44
45 45 from sqlalchemy import engine_from_config
46 46
47 47 from celery.messaging import establish_connection
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 class ResultWrapper(object):
53 53 def __init__(self, task):
54 54 self.task = task
55 55
56 56 @LazyProperty
57 57 def result(self):
58 58 return self.task
59 59
60 60
61 61 def run_task(task, *args, **kwargs):
62 62 if CELERY_ON:
63 63 try:
64 64 t = task.apply_async(args=args, kwargs=kwargs)
65 65 log.info('running task %s:%s' % (t.task_id, task))
66 66 return t
67 67
68 68 except socket.error, e:
69 69 if isinstance(e, IOError) and e.errno == 111:
70 70 log.debug('Unable to connect to celeryd. Sync execution')
71 71 else:
72 72 log.error(traceback.format_exc())
73 73 except KeyError, e:
74 74 log.debug('Unable to connect to celeryd. Sync execution')
75 75 except Exception, e:
76 76 log.error(traceback.format_exc())
77 77
78 78 log.debug('executing task %s in sync mode' % task)
79 79 return ResultWrapper(task(*args, **kwargs))
80 80
81 81
82 82 def __get_lockkey(func, *fargs, **fkwargs):
83 83 params = list(fargs)
84 84 params.extend(['%s-%s' % ar for ar in fkwargs.items()])
85 85
86 86 func_name = str(func.__name__) if hasattr(func, '__name__') else str(func)
87 87
88 88 lockkey = 'task_%s.lock' % \
89 89 md5(func_name + '-' + '-'.join(map(safe_str, params))).hexdigest()
90 90 return lockkey
91 91
92 92
93 93 def locked_task(func):
94 94 def __wrapper(func, *fargs, **fkwargs):
95 95 lockkey = __get_lockkey(func, *fargs, **fkwargs)
96 96 lockkey_path = config['here']
97 97
98 98 log.info('running task with lockkey %s' % lockkey)
99 99 try:
100 100 l = DaemonLock(file_=jn(lockkey_path, lockkey))
101 101 ret = func(*fargs, **fkwargs)
102 102 l.release()
103 103 return ret
104 104 except LockHeld:
105 105 log.info('LockHeld')
106 106 return 'Task with key %s already running' % lockkey
107 107
108 108 return decorator(__wrapper, func)
109 109
110 110
111 111 def get_session():
112 112 if CELERY_ON:
113 113 engine = engine_from_config(config, 'sqlalchemy.db1.')
114 114 init_model(engine)
115 115 sa = meta.Session
116 116 return sa
117 117
118 118
119 119 def dbsession(func):
120 120 def __wrapper(func, *fargs, **fkwargs):
121 121 try:
122 122 ret = func(*fargs, **fkwargs)
123 123 return ret
124 124 finally:
125 125 if CELERY_ON:
126 126 meta.Session.remove()
127 127
128 128 return decorator(__wrapper, func)
@@ -1,414 +1,416 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.celerylib.tasks
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 RhodeCode task modules, containing all task that suppose to be run
7 7 by celery daemon
8 8
9 9 :created_on: Oct 6, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 from celery.decorators import task
27 27
28 28 import os
29 29 import traceback
30 30 import logging
31 31 from os.path import join as jn
32 32
33 33 from time import mktime
34 34 from operator import itemgetter
35 35 from string import lower
36 36
37 37 from pylons import config, url
38 38 from pylons.i18n.translation import _
39 39
40 40 from rhodecode.lib.vcs import get_backend
41 41
42 42 from rhodecode import CELERY_ON
43 from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, safe_str
43 from rhodecode.lib.utils2 import safe_str
44 44 from rhodecode.lib.celerylib import run_task, locked_task, dbsession, \
45 45 str2bool, __get_lockkey, LockHeld, DaemonLock, get_session
46 46 from rhodecode.lib.helpers import person
47 47 from rhodecode.lib.rcmail.smtp_mailer import SmtpMailer
48 48 from rhodecode.lib.utils import add_cache, action_logger
49 49 from rhodecode.lib.compat import json, OrderedDict
50 50
51 51 from rhodecode.model.db import Statistics, Repository, User
52 52
53 53
54 54 add_cache(config)
55 55
56 56 __all__ = ['whoosh_index', 'get_commits_stats',
57 57 'reset_user_password', 'send_email']
58 58
59 59
60 60 def get_logger(cls):
61 61 if CELERY_ON:
62 62 try:
63 63 log = cls.get_logger()
64 64 except:
65 65 log = logging.getLogger(__name__)
66 66 else:
67 67 log = logging.getLogger(__name__)
68 68
69 69 return log
70 70
71 71
72 72 @task(ignore_result=True)
73 73 @locked_task
74 74 @dbsession
75 75 def whoosh_index(repo_location, full_index):
76 76 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
77 77 log = whoosh_index.get_logger(whoosh_index)
78 78 DBS = get_session()
79 79
80 80 index_location = config['index_dir']
81 81 WhooshIndexingDaemon(index_location=index_location,
82 82 repo_location=repo_location, sa=DBS)\
83 83 .run(full_index=full_index)
84 84
85 85
86 86 @task(ignore_result=True)
87 87 @dbsession
88 88 def get_commits_stats(repo_name, ts_min_y, ts_max_y):
89 89 log = get_logger(get_commits_stats)
90 90 DBS = get_session()
91 91 lockkey = __get_lockkey('get_commits_stats', repo_name, ts_min_y,
92 92 ts_max_y)
93 93 lockkey_path = config['here']
94 94
95 95 log.info('running task with lockkey %s' % lockkey)
96 96
97 97 try:
98 98 lock = l = DaemonLock(file_=jn(lockkey_path, lockkey))
99 99
100 100 # for js data compatibility cleans the key for person from '
101 101 akc = lambda k: person(k).replace('"', "")
102 102
103 103 co_day_auth_aggr = {}
104 104 commits_by_day_aggregate = {}
105 105 repo = Repository.get_by_repo_name(repo_name)
106 106 if repo is None:
107 107 return True
108 108
109 109 repo = repo.scm_instance
110 110 repo_size = repo.count()
111 111 # return if repo have no revisions
112 112 if repo_size < 1:
113 113 lock.release()
114 114 return True
115 115
116 116 skip_date_limit = True
117 117 parse_limit = int(config['app_conf'].get('commit_parse_limit'))
118 118 last_rev = None
119 119 last_cs = None
120 120 timegetter = itemgetter('time')
121 121
122 122 dbrepo = DBS.query(Repository)\
123 123 .filter(Repository.repo_name == repo_name).scalar()
124 124 cur_stats = DBS.query(Statistics)\
125 125 .filter(Statistics.repository == dbrepo).scalar()
126 126
127 127 if cur_stats is not None:
128 128 last_rev = cur_stats.stat_on_revision
129 129
130 130 if last_rev == repo.get_changeset().revision and repo_size > 1:
131 131 # pass silently without any work if we're not on first revision or
132 132 # current state of parsing revision(from db marker) is the
133 133 # last revision
134 134 lock.release()
135 135 return True
136 136
137 137 if cur_stats:
138 138 commits_by_day_aggregate = OrderedDict(json.loads(
139 139 cur_stats.commit_activity_combined))
140 140 co_day_auth_aggr = json.loads(cur_stats.commit_activity)
141 141
142 142 log.debug('starting parsing %s' % parse_limit)
143 143 lmktime = mktime
144 144
145 145 last_rev = last_rev + 1 if last_rev >= 0 else 0
146 146 log.debug('Getting revisions from %s to %s' % (
147 147 last_rev, last_rev + parse_limit)
148 148 )
149 149 for cs in repo[last_rev:last_rev + parse_limit]:
150 log.debug('parsing %s' % cs)
150 151 last_cs = cs # remember last parsed changeset
151 152 k = lmktime([cs.date.timetuple()[0], cs.date.timetuple()[1],
152 153 cs.date.timetuple()[2], 0, 0, 0, 0, 0, 0])
153 154
154 155 if akc(cs.author) in co_day_auth_aggr:
155 156 try:
156 157 l = [timegetter(x) for x in
157 158 co_day_auth_aggr[akc(cs.author)]['data']]
158 159 time_pos = l.index(k)
159 160 except ValueError:
160 161 time_pos = False
161 162
162 163 if time_pos >= 0 and time_pos is not False:
163 164
164 165 datadict = \
165 166 co_day_auth_aggr[akc(cs.author)]['data'][time_pos]
166 167
167 168 datadict["commits"] += 1
168 169 datadict["added"] += len(cs.added)
169 170 datadict["changed"] += len(cs.changed)
170 171 datadict["removed"] += len(cs.removed)
171 172
172 173 else:
173 174 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
174 175
175 176 datadict = {"time": k,
176 177 "commits": 1,
177 178 "added": len(cs.added),
178 179 "changed": len(cs.changed),
179 180 "removed": len(cs.removed),
180 181 }
181 182 co_day_auth_aggr[akc(cs.author)]['data']\
182 183 .append(datadict)
183 184
184 185 else:
185 186 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
186 187 co_day_auth_aggr[akc(cs.author)] = {
187 188 "label": akc(cs.author),
188 189 "data": [{"time":k,
189 190 "commits":1,
190 191 "added":len(cs.added),
191 192 "changed":len(cs.changed),
192 193 "removed":len(cs.removed),
193 194 }],
194 195 "schema": ["commits"],
195 196 }
196 197
197 198 #gather all data by day
198 199 if k in commits_by_day_aggregate:
199 200 commits_by_day_aggregate[k] += 1
200 201 else:
201 202 commits_by_day_aggregate[k] = 1
202 203
203 204 overview_data = sorted(commits_by_day_aggregate.items(),
204 205 key=itemgetter(0))
205 206
206 207 if not co_day_auth_aggr:
207 208 co_day_auth_aggr[akc(repo.contact)] = {
208 209 "label": akc(repo.contact),
209 210 "data": [0, 1],
210 211 "schema": ["commits"],
211 212 }
212 213
213 214 stats = cur_stats if cur_stats else Statistics()
214 215 stats.commit_activity = json.dumps(co_day_auth_aggr)
215 216 stats.commit_activity_combined = json.dumps(overview_data)
216 217
217 218 log.debug('last revison %s' % last_rev)
218 219 leftovers = len(repo.revisions[last_rev:])
219 220 log.debug('revisions to parse %s' % leftovers)
220 221
221 222 if last_rev == 0 or leftovers < parse_limit:
222 223 log.debug('getting code trending stats')
223 224 stats.languages = json.dumps(__get_codes_stats(repo_name))
224 225
225 226 try:
226 227 stats.repository = dbrepo
227 228 stats.stat_on_revision = last_cs.revision if last_cs else 0
228 229 DBS.add(stats)
229 230 DBS.commit()
230 231 except:
231 232 log.error(traceback.format_exc())
232 233 DBS.rollback()
233 234 lock.release()
234 235 return False
235 236
236 #final release
237 # final release
237 238 lock.release()
238 239
239 #execute another task if celery is enabled
240 # execute another task if celery is enabled
240 241 if len(repo.revisions) > 1 and CELERY_ON:
241 242 run_task(get_commits_stats, repo_name, ts_min_y, ts_max_y)
242 243 return True
243 244 except LockHeld:
244 245 log.info('LockHeld')
245 246 return 'Task with key %s already running' % lockkey
246 247
247 248 @task(ignore_result=True)
248 249 @dbsession
249 250 def send_password_link(user_email):
250 251 from rhodecode.model.notification import EmailNotificationModel
251 252
252 253 log = get_logger(send_password_link)
253 254 DBS = get_session()
254 255
255 256 try:
256 257 user = User.get_by_email(user_email)
257 258 if user:
258 259 log.debug('password reset user found %s' % user)
259 260 link = url('reset_password_confirmation', key=user.api_key,
260 261 qualified=True)
261 262 reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET
262 263 body = EmailNotificationModel().get_email_tmpl(reg_type,
263 264 **{'user':user.short_contact,
264 265 'reset_url':link})
265 266 log.debug('sending email')
266 267 run_task(send_email, user_email,
267 268 _("password reset link"), body)
268 269 log.info('send new password mail to %s' % user_email)
269 270 else:
270 271 log.debug("password reset email %s not found" % user_email)
271 272 except:
272 273 log.error(traceback.format_exc())
273 274 return False
274 275
275 276 return True
276 277
277 278 @task(ignore_result=True)
278 279 @dbsession
279 280 def reset_user_password(user_email):
280 281 from rhodecode.lib import auth
281 282
282 283 log = get_logger(reset_user_password)
283 284 DBS = get_session()
284 285
285 286 try:
286 287 try:
287 288 user = User.get_by_email(user_email)
288 289 new_passwd = auth.PasswordGenerator().gen_password(8,
289 290 auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
290 291 if user:
291 292 user.password = auth.get_crypt_password(new_passwd)
292 293 user.api_key = auth.generate_api_key(user.username)
293 294 DBS.add(user)
294 295 DBS.commit()
295 296 log.info('change password for %s' % user_email)
296 297 if new_passwd is None:
297 298 raise Exception('unable to generate new password')
298 299 except:
299 300 log.error(traceback.format_exc())
300 301 DBS.rollback()
301 302
302 303 run_task(send_email, user_email,
303 304 'Your new password',
304 305 'Your new RhodeCode password:%s' % (new_passwd))
305 306 log.info('send new password mail to %s' % user_email)
306 307
307 308 except:
308 309 log.error('Failed to update user password')
309 310 log.error(traceback.format_exc())
310 311
311 312 return True
312 313
313 314
314 315 @task(ignore_result=True)
315 316 @dbsession
316 317 def send_email(recipients, subject, body, html_body=''):
317 318 """
318 319 Sends an email with defined parameters from the .ini files.
319 320
320 321 :param recipients: list of recipients, it this is empty the defined email
321 322 address from field 'email_to' is used instead
322 323 :param subject: subject of the mail
323 324 :param body: body of the mail
324 325 :param html_body: html version of body
325 326 """
326 327 log = get_logger(send_email)
327 328 DBS = get_session()
328 329
329 330 email_config = config
330 331 subject = "%s %s" % (email_config.get('email_prefix'), subject)
331 332 if not recipients:
332 333 # if recipients are not defined we send to email_config + all admins
333 334 admins = [u.email for u in User.query()
334 335 .filter(User.admin == True).all()]
335 336 recipients = [email_config.get('email_to')] + admins
336 337
337 338 mail_from = email_config.get('app_email_from', 'RhodeCode')
338 339 user = email_config.get('smtp_username')
339 340 passwd = email_config.get('smtp_password')
340 341 mail_server = email_config.get('smtp_server')
341 342 mail_port = email_config.get('smtp_port')
342 343 tls = str2bool(email_config.get('smtp_use_tls'))
343 344 ssl = str2bool(email_config.get('smtp_use_ssl'))
344 345 debug = str2bool(config.get('debug'))
345 346 smtp_auth = email_config.get('smtp_auth')
346 347
347 348 try:
348 349 m = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth,
349 350 mail_port, ssl, tls, debug=debug)
350 351 m.send(recipients, subject, body, html_body)
351 352 except:
352 353 log.error('Mail sending failed')
353 354 log.error(traceback.format_exc())
354 355 return False
355 356 return True
356 357
357 358
358 359 @task(ignore_result=True)
359 360 @dbsession
360 361 def create_repo_fork(form_data, cur_user):
361 362 """
362 363 Creates a fork of repository using interval VCS methods
363 364
364 365 :param form_data:
365 366 :param cur_user:
366 367 """
367 368 from rhodecode.model.repo import RepoModel
368 369
369 370 log = get_logger(create_repo_fork)
370 371 DBS = get_session()
371 372
372 373 base_path = Repository.base_path()
373 374
374 375 RepoModel(DBS).create(form_data, cur_user, just_db=True, fork=True)
375 376
376 377 alias = form_data['repo_type']
377 378 org_repo_name = form_data['org_path']
378 379 fork_name = form_data['repo_name_full']
379 380 update_after_clone = form_data['update_after_clone']
380 381 source_repo_path = os.path.join(base_path, org_repo_name)
381 382 destination_fork_path = os.path.join(base_path, fork_name)
382 383
383 384 log.info('creating fork of %s as %s', source_repo_path,
384 385 destination_fork_path)
385 386 backend = get_backend(alias)
386 387 backend(safe_str(destination_fork_path), create=True,
387 388 src_url=safe_str(source_repo_path),
388 389 update_after_clone=update_after_clone)
389 390 action_logger(cur_user, 'user_forked_repo:%s' % fork_name,
390 391 org_repo_name, '', DBS)
391 392
392 393 action_logger(cur_user, 'user_created_fork:%s' % fork_name,
393 394 fork_name, '', DBS)
394 395 # finally commit at latest possible stage
395 396 DBS.commit()
396 397
397 398 def __get_codes_stats(repo_name):
399 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
398 400 repo = Repository.get_by_repo_name(repo_name).scm_instance
399 401
400 402 tip = repo.get_changeset()
401 403 code_stats = {}
402 404
403 405 def aggregate(cs):
404 406 for f in cs[2]:
405 407 ext = lower(f.extension)
406 408 if ext in LANGUAGES_EXTENSIONS_MAP.keys() and not f.is_binary:
407 409 if ext in code_stats:
408 410 code_stats[ext] += 1
409 411 else:
410 412 code_stats[ext] = 1
411 413
412 414 map(aggregate, tip.walk('/'))
413 415
414 416 return code_stats or {}
@@ -1,95 +1,98 b''
1 1 import rhodecode
2 from rhodecode.lib.utils import BasePasterCommand, Command
2 from rhodecode.lib.utils import BasePasterCommand, Command, load_rcextensions
3 3 from celery.app import app_or_default
4 4 from celery.bin import camqadm, celerybeat, celeryd, celeryev
5 5
6 from rhodecode.lib import str2bool
6 from rhodecode.lib.utils2 import str2bool
7 7
8 8 __all__ = ['CeleryDaemonCommand', 'CeleryBeatCommand',
9 9 'CAMQPAdminCommand', 'CeleryEventCommand']
10 10
11 11
12 12 class CeleryCommand(BasePasterCommand):
13 13 """Abstract class implements run methods needed for celery
14 14
15 15 Starts the celery worker that uses a paste.deploy configuration
16 16 file.
17 17 """
18 18
19 19 def update_parser(self):
20 20 """
21 21 Abstract method. Allows for the class's parser to be updated
22 22 before the superclass's `run` method is called. Necessary to
23 23 allow options/arguments to be passed through to the underlying
24 24 celery command.
25 25 """
26 26
27 27 cmd = self.celery_command(app_or_default())
28 28 for x in cmd.get_options():
29 29 self.parser.add_option(x)
30 30
31 31 def command(self):
32 32 from pylons import config
33 33 try:
34 34 CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
35 35 except KeyError:
36 36 CELERY_ON = False
37 37
38 38 if CELERY_ON == False:
39 39 raise Exception('Please enable celery_on in .ini config '
40 40 'file before running celeryd')
41 41 rhodecode.CELERY_ON = CELERY_ON
42 load_rcextensions(config['here'])
42 43 cmd = self.celery_command(app_or_default())
43 44 return cmd.run(**vars(self.options))
44 45
46
45 47 class CeleryDaemonCommand(CeleryCommand):
46 48 """Start the celery worker
47 49
48 50 Starts the celery worker that uses a paste.deploy configuration
49 51 file.
50 52 """
51 53 usage = 'CONFIG_FILE [celeryd options...]'
52 54 summary = __doc__.splitlines()[0]
53 55 description = "".join(__doc__.splitlines()[2:])
54 56
55 57 parser = Command.standard_parser(quiet=True)
56 58 celery_command = celeryd.WorkerCommand
57 59
58 60
59 61 class CeleryBeatCommand(CeleryCommand):
60 62 """Start the celery beat server
61 63
62 64 Starts the celery beat server using a paste.deploy configuration
63 65 file.
64 66 """
65 67 usage = 'CONFIG_FILE [celerybeat options...]'
66 68 summary = __doc__.splitlines()[0]
67 69 description = "".join(__doc__.splitlines()[2:])
68 70
69 71 parser = Command.standard_parser(quiet=True)
70 72 celery_command = celerybeat.BeatCommand
71 73
72 74
73 75 class CAMQPAdminCommand(CeleryCommand):
74 76 """CAMQP Admin
75 77
76 78 CAMQP celery admin tool.
77 79 """
78 80 usage = 'CONFIG_FILE [camqadm options...]'
79 81 summary = __doc__.splitlines()[0]
80 82 description = "".join(__doc__.splitlines()[2:])
81 83
82 84 parser = Command.standard_parser(quiet=True)
83 85 celery_command = camqadm.AMQPAdminCommand
84 86
87
85 88 class CeleryEventCommand(CeleryCommand):
86 89 """Celery event command.
87 90
88 91 Capture celery events.
89 92 """
90 93 usage = 'CONFIG_FILE [celeryev options...]'
91 94 summary = __doc__.splitlines()[0]
92 95 description = "".join(__doc__.splitlines()[2:])
93 96
94 97 parser = Command.standard_parser(quiet=True)
95 98 celery_command = celeryev.EvCommand
@@ -1,87 +1,84 b''
1 1 """
2 2 Provide exception classes for :mod:`migrate`
3 3 """
4 4
5 5
6 6 class Error(Exception):
7 7 """Error base class."""
8 8
9 9
10 10 class ApiError(Error):
11 11 """Base class for API errors."""
12 12
13 13
14 14 class KnownError(ApiError):
15 15 """A known error condition."""
16 16
17 17
18 18 class UsageError(ApiError):
19 19 """A known error condition where help should be displayed."""
20 20
21 21
22 22 class ControlledSchemaError(Error):
23 23 """Base class for controlled schema errors."""
24 24
25 25
26 26 class InvalidVersionError(ControlledSchemaError):
27 27 """Invalid version number."""
28 28
29 29
30 30 class DatabaseNotControlledError(ControlledSchemaError):
31 31 """Database should be under version control, but it's not."""
32 32
33 33
34 34 class DatabaseAlreadyControlledError(ControlledSchemaError):
35 35 """Database shouldn't be under version control, but it is"""
36 36
37 37
38 38 class WrongRepositoryError(ControlledSchemaError):
39 39 """This database is under version control by another repository."""
40 40
41 41
42 42 class NoSuchTableError(ControlledSchemaError):
43 43 """The table does not exist."""
44 44
45 45
46 46 class PathError(Error):
47 47 """Base class for path errors."""
48 48
49 49
50 50 class PathNotFoundError(PathError):
51 51 """A path with no file was required; found a file."""
52 52
53 53
54 54 class PathFoundError(PathError):
55 55 """A path with a file was required; found no file."""
56 56
57 57
58 58 class RepositoryError(Error):
59 59 """Base class for repository errors."""
60 60
61 61
62 62 class InvalidRepositoryError(RepositoryError):
63 63 """Invalid repository error."""
64 64
65 65
66 66 class ScriptError(Error):
67 67 """Base class for script errors."""
68 68
69 69
70 70 class InvalidScriptError(ScriptError):
71 71 """Invalid script error."""
72 72
73 73
74 class InvalidVersionError(Error):
75 """Invalid version error."""
76
77 74 # migrate.changeset
78 75
79 76 class NotSupportedError(Error):
80 77 """Not supported error"""
81 78
82 79
83 80 class InvalidConstraintError(Error):
84 81 """Invalid constraint error"""
85 82
86 83 class MigrateDeprecationWarning(DeprecationWarning):
87 84 """Warning for deprecated features in Migrate"""
@@ -1,1098 +1,1097 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 from datetime import date
31 31
32 32 from sqlalchemy import *
33 33 from sqlalchemy.ext.hybrid import hybrid_property
34 34 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
35 35 from beaker.cache import cache_region, region_invalidate
36 36
37 37 from rhodecode.lib.vcs import get_backend
38 38 from rhodecode.lib.vcs.utils.helpers import get_scm
39 39 from rhodecode.lib.vcs.exceptions import VCSError
40 40 from rhodecode.lib.vcs.utils.lazy import LazyProperty
41 41
42 from rhodecode.lib import str2bool, safe_str, get_changeset_safe, \
42 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
43 43 generate_api_key, safe_unicode
44 44 from rhodecode.lib.exceptions import UsersGroupsAssignedException
45 45 from rhodecode.lib.compat import json
46 46
47 47 from rhodecode.model.meta import Base, Session
48 48 from rhodecode.lib.caching_query import FromCache
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53 #==============================================================================
54 54 # BASE CLASSES
55 55 #==============================================================================
56 56
57 57 class ModelSerializer(json.JSONEncoder):
58 58 """
59 59 Simple Serializer for JSON,
60 60
61 61 usage::
62 62
63 63 to make object customized for serialization implement a __json__
64 64 method that will return a dict for serialization into json
65 65
66 66 example::
67 67
68 68 class Task(object):
69 69
70 70 def __init__(self, name, value):
71 71 self.name = name
72 72 self.value = value
73 73
74 74 def __json__(self):
75 75 return dict(name=self.name,
76 76 value=self.value)
77 77
78 78 """
79 79
80 80 def default(self, obj):
81 81
82 82 if hasattr(obj, '__json__'):
83 83 return obj.__json__()
84 84 else:
85 85 return json.JSONEncoder.default(self, obj)
86 86
87 87 class BaseModel(object):
88 88 """Base Model for all classess
89 89
90 90 """
91 91
92 92 @classmethod
93 93 def _get_keys(cls):
94 94 """return column names for this model """
95 95 return class_mapper(cls).c.keys()
96 96
97 97 def get_dict(self):
98 98 """return dict with keys and values corresponding
99 99 to this model data """
100 100
101 101 d = {}
102 102 for k in self._get_keys():
103 103 d[k] = getattr(self, k)
104 104 return d
105 105
106 106 def get_appstruct(self):
107 107 """return list with keys and values tupples corresponding
108 108 to this model data """
109 109
110 110 l = []
111 111 for k in self._get_keys():
112 112 l.append((k, getattr(self, k),))
113 113 return l
114 114
115 115 def populate_obj(self, populate_dict):
116 116 """populate model with data from given populate_dict"""
117 117
118 118 for k in self._get_keys():
119 119 if k in populate_dict:
120 120 setattr(self, k, populate_dict[k])
121 121
122 122 @classmethod
123 123 def query(cls):
124 124 return Session.query(cls)
125 125
126 126 @classmethod
127 127 def get(cls, id_):
128 128 if id_:
129 129 return cls.query().get(id_)
130 130
131 131 @classmethod
132 132 def getAll(cls):
133 133 return cls.query().all()
134 134
135 135 @classmethod
136 136 def delete(cls, id_):
137 137 obj = cls.query().get(id_)
138 138 Session.delete(obj)
139 139 Session.commit()
140 140
141 141
142 142 class RhodeCodeSetting(Base, BaseModel):
143 143 __tablename__ = 'rhodecode_settings'
144 144 __table_args__ = (UniqueConstraint('app_settings_name'), {'extend_existing':True})
145 145 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
146 146 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
147 147 _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
148 148
149 149 def __init__(self, k='', v=''):
150 150 self.app_settings_name = k
151 151 self.app_settings_value = v
152 152
153 153
154 154 @validates('_app_settings_value')
155 155 def validate_settings_value(self, key, val):
156 156 assert type(val) == unicode
157 157 return val
158 158
159 159 @hybrid_property
160 160 def app_settings_value(self):
161 161 v = self._app_settings_value
162 162 if v == 'ldap_active':
163 163 v = str2bool(v)
164 164 return v
165 165
166 166 @app_settings_value.setter
167 167 def app_settings_value(self, val):
168 168 """
169 169 Setter that will always make sure we use unicode in app_settings_value
170 170
171 171 :param val:
172 172 """
173 173 self._app_settings_value = safe_unicode(val)
174 174
175 175 def __repr__(self):
176 176 return "<%s('%s:%s')>" % (self.__class__.__name__,
177 177 self.app_settings_name, self.app_settings_value)
178 178
179 179
180 180 @classmethod
181 181 def get_by_name(cls, ldap_key):
182 182 return cls.query()\
183 183 .filter(cls.app_settings_name == ldap_key).scalar()
184 184
185 185 @classmethod
186 186 def get_app_settings(cls, cache=False):
187 187
188 188 ret = cls.query()
189 189
190 190 if cache:
191 191 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
192 192
193 193 if not ret:
194 194 raise Exception('Could not get application settings !')
195 195 settings = {}
196 196 for each in ret:
197 197 settings['rhodecode_' + each.app_settings_name] = \
198 198 each.app_settings_value
199 199
200 200 return settings
201 201
202 202 @classmethod
203 203 def get_ldap_settings(cls, cache=False):
204 204 ret = cls.query()\
205 205 .filter(cls.app_settings_name.startswith('ldap_')).all()
206 206 fd = {}
207 207 for row in ret:
208 208 fd.update({row.app_settings_name:row.app_settings_value})
209 209
210 210 return fd
211 211
212 212
213 213 class RhodeCodeUi(Base, BaseModel):
214 214 __tablename__ = 'rhodecode_ui'
215 215 __table_args__ = (UniqueConstraint('ui_key'), {'extend_existing':True})
216 216
217 217 HOOK_UPDATE = 'changegroup.update'
218 218 HOOK_REPO_SIZE = 'changegroup.repo_size'
219 219 HOOK_PUSH = 'pretxnchangegroup.push_logger'
220 220 HOOK_PULL = 'preoutgoing.pull_logger'
221 221
222 222 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
223 223 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
224 224 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
225 225 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
226 226 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
227 227
228 228
229 229 @classmethod
230 230 def get_by_key(cls, key):
231 231 return cls.query().filter(cls.ui_key == key)
232 232
233 233
234 234 @classmethod
235 235 def get_builtin_hooks(cls):
236 236 q = cls.query()
237 237 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
238 238 cls.HOOK_REPO_SIZE,
239 239 cls.HOOK_PUSH, cls.HOOK_PULL]))
240 240 return q.all()
241 241
242 242 @classmethod
243 243 def get_custom_hooks(cls):
244 244 q = cls.query()
245 245 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
246 246 cls.HOOK_REPO_SIZE,
247 247 cls.HOOK_PUSH, cls.HOOK_PULL]))
248 248 q = q.filter(cls.ui_section == 'hooks')
249 249 return q.all()
250 250
251 251 @classmethod
252 252 def create_or_update_hook(cls, key, val):
253 253 new_ui = cls.get_by_key(key).scalar() or cls()
254 254 new_ui.ui_section = 'hooks'
255 255 new_ui.ui_active = True
256 256 new_ui.ui_key = key
257 257 new_ui.ui_value = val
258 258
259 259 Session.add(new_ui)
260 260 Session.commit()
261 261
262 262
263 263 class User(Base, BaseModel):
264 264 __tablename__ = 'users'
265 265 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'extend_existing':True})
266 266 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
267 267 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
268 268 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
269 269 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
270 270 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
271 271 name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
272 272 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
273 273 email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
274 274 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
275 275 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
276 276 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
277 277
278 278 user_log = relationship('UserLog', cascade='all')
279 279 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
280 280
281 281 repositories = relationship('Repository')
282 282 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
283 283 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
284 284
285 285 group_member = relationship('UsersGroupMember', cascade='all')
286 286
287 287 @property
288 288 def full_contact(self):
289 289 return '%s %s <%s>' % (self.name, self.lastname, self.email)
290 290
291 291 @property
292 292 def short_contact(self):
293 293 return '%s %s' % (self.name, self.lastname)
294 294
295 295 @property
296 296 def is_admin(self):
297 297 return self.admin
298 298
299 299 def __repr__(self):
300 300 try:
301 301 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
302 302 self.user_id, self.username)
303 303 except:
304 304 return self.__class__.__name__
305 305
306 306 @classmethod
307 307 def get_by_username(cls, username, case_insensitive=False):
308 308 if case_insensitive:
309 309 return Session.query(cls).filter(cls.username.ilike(username)).scalar()
310 310 else:
311 311 return Session.query(cls).filter(cls.username == username).scalar()
312 312
313 313 @classmethod
314 314 def get_by_api_key(cls, api_key):
315 315 return cls.query().filter(cls.api_key == api_key).one()
316 316
317 317 def update_lastlogin(self):
318 318 """Update user lastlogin"""
319 319
320 320 self.last_login = datetime.datetime.now()
321 321 Session.add(self)
322 322 Session.commit()
323 323 log.debug('updated user %s lastlogin' % self.username)
324 324
325 325 @classmethod
326 326 def create(cls, form_data):
327 327 from rhodecode.lib.auth import get_crypt_password
328 328
329 329 try:
330 330 new_user = cls()
331 331 for k, v in form_data.items():
332 332 if k == 'password':
333 333 v = get_crypt_password(v)
334 334 setattr(new_user, k, v)
335 335
336 336 new_user.api_key = generate_api_key(form_data['username'])
337 337 Session.add(new_user)
338 338 Session.commit()
339 339 return new_user
340 340 except:
341 341 log.error(traceback.format_exc())
342 342 Session.rollback()
343 343 raise
344 344
345 345 class UserLog(Base, BaseModel):
346 346 __tablename__ = 'user_logs'
347 347 __table_args__ = {'extend_existing':True}
348 348 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
349 349 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
350 350 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
351 351 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
352 352 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
353 353 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
354 354 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
355 355
356 356 @property
357 357 def action_as_day(self):
358 358 return date(*self.action_date.timetuple()[:3])
359 359
360 360 user = relationship('User')
361 361 repository = relationship('Repository')
362 362
363 363
364 364 class UsersGroup(Base, BaseModel):
365 365 __tablename__ = 'users_groups'
366 366 __table_args__ = {'extend_existing':True}
367 367
368 368 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
369 369 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
370 370 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
371 371
372 372 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
373 373
374 374 def __repr__(self):
375 375 return '<userGroup(%s)>' % (self.users_group_name)
376 376
377 377 @classmethod
378 378 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
379 379 if case_insensitive:
380 380 gr = cls.query()\
381 381 .filter(cls.users_group_name.ilike(group_name))
382 382 else:
383 383 gr = cls.query()\
384 384 .filter(cls.users_group_name == group_name)
385 385 if cache:
386 386 gr = gr.options(FromCache("sql_cache_short",
387 387 "get_user_%s" % group_name))
388 388 return gr.scalar()
389 389
390 390
391 391 @classmethod
392 392 def get(cls, users_group_id, cache=False):
393 393 users_group = cls.query()
394 394 if cache:
395 395 users_group = users_group.options(FromCache("sql_cache_short",
396 396 "get_users_group_%s" % users_group_id))
397 397 return users_group.get(users_group_id)
398 398
399 399 @classmethod
400 400 def create(cls, form_data):
401 401 try:
402 402 new_users_group = cls()
403 403 for k, v in form_data.items():
404 404 setattr(new_users_group, k, v)
405 405
406 406 Session.add(new_users_group)
407 407 Session.commit()
408 408 return new_users_group
409 409 except:
410 410 log.error(traceback.format_exc())
411 411 Session.rollback()
412 412 raise
413 413
414 414 @classmethod
415 415 def update(cls, users_group_id, form_data):
416 416
417 417 try:
418 418 users_group = cls.get(users_group_id, cache=False)
419 419
420 420 for k, v in form_data.items():
421 421 if k == 'users_group_members':
422 422 users_group.members = []
423 423 Session.flush()
424 424 members_list = []
425 425 if v:
426 426 v = [v] if isinstance(v, basestring) else v
427 427 for u_id in set(v):
428 428 member = UsersGroupMember(users_group_id, u_id)
429 429 members_list.append(member)
430 430 setattr(users_group, 'members', members_list)
431 431 setattr(users_group, k, v)
432 432
433 433 Session.add(users_group)
434 434 Session.commit()
435 435 except:
436 436 log.error(traceback.format_exc())
437 437 Session.rollback()
438 438 raise
439 439
440 440 @classmethod
441 441 def delete(cls, users_group_id):
442 442 try:
443 443
444 444 # check if this group is not assigned to repo
445 445 assigned_groups = UsersGroupRepoToPerm.query()\
446 446 .filter(UsersGroupRepoToPerm.users_group_id ==
447 447 users_group_id).all()
448 448
449 449 if assigned_groups:
450 450 raise UsersGroupsAssignedException('RepoGroup assigned to %s' %
451 451 assigned_groups)
452 452
453 453 users_group = cls.get(users_group_id, cache=False)
454 454 Session.delete(users_group)
455 455 Session.commit()
456 456 except:
457 457 log.error(traceback.format_exc())
458 458 Session.rollback()
459 459 raise
460 460
461 461 class UsersGroupMember(Base, BaseModel):
462 462 __tablename__ = 'users_groups_members'
463 463 __table_args__ = {'extend_existing':True}
464 464
465 465 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
466 466 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
467 467 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
468 468
469 469 user = relationship('User', lazy='joined')
470 470 users_group = relationship('UsersGroup')
471 471
472 472 def __init__(self, gr_id='', u_id=''):
473 473 self.users_group_id = gr_id
474 474 self.user_id = u_id
475 475
476 476 @staticmethod
477 477 def add_user_to_group(group, user):
478 478 ugm = UsersGroupMember()
479 479 ugm.users_group = group
480 480 ugm.user = user
481 481 Session.add(ugm)
482 482 Session.commit()
483 483 return ugm
484 484
485 485 class Repository(Base, BaseModel):
486 486 __tablename__ = 'repositories'
487 487 __table_args__ = (UniqueConstraint('repo_name'), {'extend_existing':True},)
488 488
489 489 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
490 490 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
491 491 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
492 492 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
493 493 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
494 494 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
495 495 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
496 496 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
497 497 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
498 498 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
499 499
500 500 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
501 501 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
502 502
503 503
504 504 user = relationship('User')
505 505 fork = relationship('Repository', remote_side=repo_id)
506 506 group = relationship('RepoGroup')
507 507 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
508 508 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
509 509 stats = relationship('Statistics', cascade='all', uselist=False)
510 510
511 511 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
512 512
513 513 logs = relationship('UserLog', cascade='all')
514 514
515 515 def __repr__(self):
516 516 return "<%s('%s:%s')>" % (self.__class__.__name__,
517 517 self.repo_id, self.repo_name)
518 518
519 519 @classmethod
520 520 def url_sep(cls):
521 521 return '/'
522 522
523 523 @classmethod
524 524 def get_by_repo_name(cls, repo_name):
525 525 q = Session.query(cls).filter(cls.repo_name == repo_name)
526 526 q = q.options(joinedload(Repository.fork))\
527 527 .options(joinedload(Repository.user))\
528 528 .options(joinedload(Repository.group))
529 529 return q.one()
530 530
531 531 @classmethod
532 532 def get_repo_forks(cls, repo_id):
533 533 return cls.query().filter(Repository.fork_id == repo_id)
534 534
535 535 @classmethod
536 536 def base_path(cls):
537 537 """
538 538 Returns base path when all repos are stored
539 539
540 540 :param cls:
541 541 """
542 542 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
543 543 cls.url_sep())
544 544 q.options(FromCache("sql_cache_short", "repository_repo_path"))
545 545 return q.one().ui_value
546 546
547 547 @property
548 548 def just_name(self):
549 549 return self.repo_name.split(Repository.url_sep())[-1]
550 550
551 551 @property
552 552 def groups_with_parents(self):
553 553 groups = []
554 554 if self.group is None:
555 555 return groups
556 556
557 557 cur_gr = self.group
558 558 groups.insert(0, cur_gr)
559 559 while 1:
560 560 gr = getattr(cur_gr, 'parent_group', None)
561 561 cur_gr = cur_gr.parent_group
562 562 if gr is None:
563 563 break
564 564 groups.insert(0, gr)
565 565
566 566 return groups
567 567
568 568 @property
569 569 def groups_and_repo(self):
570 570 return self.groups_with_parents, self.just_name
571 571
572 572 @LazyProperty
573 573 def repo_path(self):
574 574 """
575 575 Returns base full path for that repository means where it actually
576 576 exists on a filesystem
577 577 """
578 578 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
579 579 Repository.url_sep())
580 580 q.options(FromCache("sql_cache_short", "repository_repo_path"))
581 581 return q.one().ui_value
582 582
583 583 @property
584 584 def repo_full_path(self):
585 585 p = [self.repo_path]
586 586 # we need to split the name by / since this is how we store the
587 587 # names in the database, but that eventually needs to be converted
588 588 # into a valid system path
589 589 p += self.repo_name.split(Repository.url_sep())
590 590 return os.path.join(*p)
591 591
592 592 def get_new_name(self, repo_name):
593 593 """
594 594 returns new full repository name based on assigned group and new new
595 595
596 596 :param group_name:
597 597 """
598 598 path_prefix = self.group.full_path_splitted if self.group else []
599 599 return Repository.url_sep().join(path_prefix + [repo_name])
600 600
601 601 @property
602 602 def _ui(self):
603 603 """
604 604 Creates an db based ui object for this repository
605 605 """
606 606 from mercurial import ui
607 607 from mercurial import config
608 608 baseui = ui.ui()
609 609
610 610 #clean the baseui object
611 611 baseui._ocfg = config.config()
612 612 baseui._ucfg = config.config()
613 613 baseui._tcfg = config.config()
614 614
615 615
616 616 ret = RhodeCodeUi.query()\
617 617 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
618 618
619 619 hg_ui = ret
620 620 for ui_ in hg_ui:
621 621 if ui_.ui_active:
622 622 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
623 623 ui_.ui_key, ui_.ui_value)
624 624 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
625 625
626 626 return baseui
627 627
628 628 @classmethod
629 629 def is_valid(cls, repo_name):
630 630 """
631 631 returns True if given repo name is a valid filesystem repository
632 632
633 633 :param cls:
634 634 :param repo_name:
635 635 """
636 636 from rhodecode.lib.utils import is_valid_repo
637 637
638 638 return is_valid_repo(repo_name, cls.base_path())
639 639
640 640
641 641 #==========================================================================
642 642 # SCM PROPERTIES
643 643 #==========================================================================
644 644
645 645 def get_changeset(self, rev):
646 646 return get_changeset_safe(self.scm_instance, rev)
647 647
648 648 @property
649 649 def tip(self):
650 650 return self.get_changeset('tip')
651 651
652 652 @property
653 653 def author(self):
654 654 return self.tip.author
655 655
656 656 @property
657 657 def last_change(self):
658 658 return self.scm_instance.last_change
659 659
660 660 #==========================================================================
661 661 # SCM CACHE INSTANCE
662 662 #==========================================================================
663 663
664 664 @property
665 665 def invalidate(self):
666 666 return CacheInvalidation.invalidate(self.repo_name)
667 667
668 668 def set_invalidate(self):
669 669 """
670 670 set a cache for invalidation for this instance
671 671 """
672 672 CacheInvalidation.set_invalidate(self.repo_name)
673 673
674 674 @LazyProperty
675 675 def scm_instance(self):
676 676 return self.__get_instance()
677 677
678 678 @property
679 679 def scm_instance_cached(self):
680 680 @cache_region('long_term')
681 681 def _c(repo_name):
682 682 return self.__get_instance()
683 683 rn = self.repo_name
684 684
685 685 inv = self.invalidate
686 686 if inv is not None:
687 687 region_invalidate(_c, None, rn)
688 688 # update our cache
689 689 CacheInvalidation.set_valid(inv.cache_key)
690 690 return _c(rn)
691 691
692 692 def __get_instance(self):
693 693
694 694 repo_full_path = self.repo_full_path
695 695
696 696 try:
697 697 alias = get_scm(repo_full_path)[0]
698 698 log.debug('Creating instance of %s repository' % alias)
699 699 backend = get_backend(alias)
700 700 except VCSError:
701 701 log.error(traceback.format_exc())
702 702 log.error('Perhaps this repository is in db and not in '
703 703 'filesystem run rescan repositories with '
704 704 '"destroy old data " option from admin panel')
705 705 return
706 706
707 707 if alias == 'hg':
708 708
709 709 repo = backend(safe_str(repo_full_path), create=False,
710 710 baseui=self._ui)
711 711 # skip hidden web repository
712 712 if repo._get_hidden():
713 713 return
714 714 else:
715 715 repo = backend(repo_full_path, create=False)
716 716
717 717 return repo
718 718
719 719
720 class RepoGroup(Base, BaseModel):
720 class Group(Base, BaseModel):
721 721 __tablename__ = 'groups'
722 722 __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'),
723 723 CheckConstraint('group_id != group_parent_id'), {'extend_existing':True},)
724 724 __mapper_args__ = {'order_by':'group_name'}
725 725
726 726 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
727 727 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
728 728 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
729 729 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
730 730
731 parent_group = relationship('RepoGroup', remote_side=group_id)
732
731 parent_group = relationship('Group', remote_side=group_id)
733 732
734 733 def __init__(self, group_name='', parent_group=None):
735 734 self.group_name = group_name
736 735 self.parent_group = parent_group
737 736
738 737 def __repr__(self):
739 738 return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
740 739 self.group_name)
741 740
742 741 @classmethod
743 742 def groups_choices(cls):
744 743 from webhelpers.html import literal as _literal
745 744 repo_groups = [('', '')]
746 745 sep = ' &raquo; '
747 746 _name = lambda k: _literal(sep.join(k))
748 747
749 748 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
750 749 for x in cls.query().all()])
751 750
752 751 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
753 752 return repo_groups
754 753
755 754 @classmethod
756 755 def url_sep(cls):
757 756 return '/'
758 757
759 758 @classmethod
760 759 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
761 760 if case_insensitive:
762 761 gr = cls.query()\
763 762 .filter(cls.group_name.ilike(group_name))
764 763 else:
765 764 gr = cls.query()\
766 765 .filter(cls.group_name == group_name)
767 766 if cache:
768 767 gr = gr.options(FromCache("sql_cache_short",
769 768 "get_group_%s" % group_name))
770 769 return gr.scalar()
771 770
772 771 @property
773 772 def parents(self):
774 773 parents_recursion_limit = 5
775 774 groups = []
776 775 if self.parent_group is None:
777 776 return groups
778 777 cur_gr = self.parent_group
779 778 groups.insert(0, cur_gr)
780 779 cnt = 0
781 780 while 1:
782 781 cnt += 1
783 782 gr = getattr(cur_gr, 'parent_group', None)
784 783 cur_gr = cur_gr.parent_group
785 784 if gr is None:
786 785 break
787 786 if cnt == parents_recursion_limit:
788 787 # this will prevent accidental infinit loops
789 788 log.error('group nested more than %s' %
790 789 parents_recursion_limit)
791 790 break
792 791
793 792 groups.insert(0, gr)
794 793 return groups
795 794
796 795 @property
797 796 def children(self):
798 797 return Group.query().filter(Group.parent_group == self)
799 798
800 799 @property
801 800 def name(self):
802 801 return self.group_name.split(Group.url_sep())[-1]
803 802
804 803 @property
805 804 def full_path(self):
806 805 return self.group_name
807 806
808 807 @property
809 808 def full_path_splitted(self):
810 809 return self.group_name.split(Group.url_sep())
811 810
812 811 @property
813 812 def repositories(self):
814 813 return Repository.query().filter(Repository.group == self)
815 814
816 815 @property
817 816 def repositories_recursive_count(self):
818 817 cnt = self.repositories.count()
819 818
820 819 def children_count(group):
821 820 cnt = 0
822 821 for child in group.children:
823 822 cnt += child.repositories.count()
824 823 cnt += children_count(child)
825 824 return cnt
826 825
827 826 return cnt + children_count(self)
828 827
829 828
830 829 def get_new_name(self, group_name):
831 830 """
832 831 returns new full group name based on parent and new name
833 832
834 833 :param group_name:
835 834 """
836 835 path_prefix = (self.parent_group.full_path_splitted if
837 836 self.parent_group else [])
838 837 return Group.url_sep().join(path_prefix + [group_name])
839 838
840 839
841 840 class Permission(Base, BaseModel):
842 841 __tablename__ = 'permissions'
843 842 __table_args__ = {'extend_existing':True}
844 843 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
845 844 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
846 845 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
847 846
848 847 def __repr__(self):
849 848 return "<%s('%s:%s')>" % (self.__class__.__name__,
850 849 self.permission_id, self.permission_name)
851 850
852 851 @classmethod
853 852 def get_by_key(cls, key):
854 853 return cls.query().filter(cls.permission_name == key).scalar()
855 854
856 855 class UserRepoToPerm(Base, BaseModel):
857 856 __tablename__ = 'repo_to_perm'
858 857 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'extend_existing':True})
859 858 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
860 859 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
861 860 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
862 861 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
863 862
864 863 user = relationship('User')
865 864 permission = relationship('Permission')
866 865 repository = relationship('Repository')
867 866
868 867 class UserToPerm(Base, BaseModel):
869 868 __tablename__ = 'user_to_perm'
870 869 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'extend_existing':True})
871 870 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
872 871 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
873 872 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
874 873
875 874 user = relationship('User')
876 875 permission = relationship('Permission')
877 876
878 877 @classmethod
879 878 def has_perm(cls, user_id, perm):
880 879 if not isinstance(perm, Permission):
881 880 raise Exception('perm needs to be an instance of Permission class')
882 881
883 882 return cls.query().filter(cls.user_id == user_id)\
884 883 .filter(cls.permission == perm).scalar() is not None
885 884
886 885 @classmethod
887 886 def grant_perm(cls, user_id, perm):
888 887 if not isinstance(perm, Permission):
889 888 raise Exception('perm needs to be an instance of Permission class')
890 889
891 890 new = cls()
892 891 new.user_id = user_id
893 892 new.permission = perm
894 893 try:
895 894 Session.add(new)
896 895 Session.commit()
897 896 except:
898 897 Session.rollback()
899 898
900 899
901 900 @classmethod
902 901 def revoke_perm(cls, user_id, perm):
903 902 if not isinstance(perm, Permission):
904 903 raise Exception('perm needs to be an instance of Permission class')
905 904
906 905 try:
907 906 cls.query().filter(cls.user_id == user_id)\
908 907 .filter(cls.permission == perm).delete()
909 908 Session.commit()
910 909 except:
911 910 Session.rollback()
912 911
913 912 class UsersGroupRepoToPerm(Base, BaseModel):
914 913 __tablename__ = 'users_group_repo_to_perm'
915 914 __table_args__ = (UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), {'extend_existing':True})
916 915 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
917 916 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
918 917 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
919 918 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
920 919
921 920 users_group = relationship('UsersGroup')
922 921 permission = relationship('Permission')
923 922 repository = relationship('Repository')
924 923
925 924 def __repr__(self):
926 925 return '<userGroup:%s => %s >' % (self.users_group, self.repository)
927 926
928 927 class UsersGroupToPerm(Base, BaseModel):
929 928 __tablename__ = 'users_group_to_perm'
930 929 __table_args__ = {'extend_existing':True}
931 930 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
932 931 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
933 932 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
934 933
935 934 users_group = relationship('UsersGroup')
936 935 permission = relationship('Permission')
937 936
938 937
939 938 @classmethod
940 939 def has_perm(cls, users_group_id, perm):
941 940 if not isinstance(perm, Permission):
942 941 raise Exception('perm needs to be an instance of Permission class')
943 942
944 943 return cls.query().filter(cls.users_group_id ==
945 944 users_group_id)\
946 945 .filter(cls.permission == perm)\
947 946 .scalar() is not None
948 947
949 948 @classmethod
950 949 def grant_perm(cls, users_group_id, perm):
951 950 if not isinstance(perm, Permission):
952 951 raise Exception('perm needs to be an instance of Permission class')
953 952
954 953 new = cls()
955 954 new.users_group_id = users_group_id
956 955 new.permission = perm
957 956 try:
958 957 Session.add(new)
959 958 Session.commit()
960 959 except:
961 960 Session.rollback()
962 961
963 962
964 963 @classmethod
965 964 def revoke_perm(cls, users_group_id, perm):
966 965 if not isinstance(perm, Permission):
967 966 raise Exception('perm needs to be an instance of Permission class')
968 967
969 968 try:
970 969 cls.query().filter(cls.users_group_id == users_group_id)\
971 970 .filter(cls.permission == perm).delete()
972 971 Session.commit()
973 972 except:
974 973 Session.rollback()
975 974
976 975
977 976 class UserRepoGroupToPerm(Base, BaseModel):
978 977 __tablename__ = 'group_to_perm'
979 978 __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing':True})
980 979
981 980 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
982 981 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
983 982 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
984 983 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
985 984
986 985 user = relationship('User')
987 986 permission = relationship('Permission')
988 987 group = relationship('RepoGroup')
989 988
990 989 class Statistics(Base, BaseModel):
991 990 __tablename__ = 'statistics'
992 991 __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing':True})
993 992 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
994 993 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
995 994 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
996 995 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
997 996 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
998 997 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
999 998
1000 999 repository = relationship('Repository', single_parent=True)
1001 1000
1002 1001 class UserFollowing(Base, BaseModel):
1003 1002 __tablename__ = 'user_followings'
1004 1003 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
1005 1004 UniqueConstraint('user_id', 'follows_user_id')
1006 1005 , {'extend_existing':True})
1007 1006
1008 1007 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1009 1008 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1010 1009 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1011 1010 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1012 1011 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1013 1012
1014 1013 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1015 1014
1016 1015 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1017 1016 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1018 1017
1019 1018
1020 1019 @classmethod
1021 1020 def get_repo_followers(cls, repo_id):
1022 1021 return cls.query().filter(cls.follows_repo_id == repo_id)
1023 1022
1024 1023 class CacheInvalidation(Base, BaseModel):
1025 1024 __tablename__ = 'cache_invalidation'
1026 1025 __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing':True})
1027 1026 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1028 1027 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1029 1028 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1030 1029 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1031 1030
1032 1031
1033 1032 def __init__(self, cache_key, cache_args=''):
1034 1033 self.cache_key = cache_key
1035 1034 self.cache_args = cache_args
1036 1035 self.cache_active = False
1037 1036
1038 1037 def __repr__(self):
1039 1038 return "<%s('%s:%s')>" % (self.__class__.__name__,
1040 1039 self.cache_id, self.cache_key)
1041 1040
1042 1041 @classmethod
1043 1042 def invalidate(cls, key):
1044 1043 """
1045 1044 Returns Invalidation object if this given key should be invalidated
1046 1045 None otherwise. `cache_active = False` means that this cache
1047 1046 state is not valid and needs to be invalidated
1048 1047
1049 1048 :param key:
1050 1049 """
1051 1050 return cls.query()\
1052 1051 .filter(CacheInvalidation.cache_key == key)\
1053 1052 .filter(CacheInvalidation.cache_active == False)\
1054 1053 .scalar()
1055 1054
1056 1055 @classmethod
1057 1056 def set_invalidate(cls, key):
1058 1057 """
1059 1058 Mark this Cache key for invalidation
1060 1059
1061 1060 :param key:
1062 1061 """
1063 1062
1064 1063 log.debug('marking %s for invalidation' % key)
1065 1064 inv_obj = Session.query(cls)\
1066 1065 .filter(cls.cache_key == key).scalar()
1067 1066 if inv_obj:
1068 1067 inv_obj.cache_active = False
1069 1068 else:
1070 1069 log.debug('cache key not found in invalidation db -> creating one')
1071 1070 inv_obj = CacheInvalidation(key)
1072 1071
1073 1072 try:
1074 1073 Session.add(inv_obj)
1075 1074 Session.commit()
1076 1075 except Exception:
1077 1076 log.error(traceback.format_exc())
1078 1077 Session.rollback()
1079 1078
1080 1079 @classmethod
1081 1080 def set_valid(cls, key):
1082 1081 """
1083 1082 Mark this cache key as active and currently cached
1084 1083
1085 1084 :param key:
1086 1085 """
1087 1086 inv_obj = Session.query(CacheInvalidation)\
1088 1087 .filter(CacheInvalidation.cache_key == key).scalar()
1089 1088 inv_obj.cache_active = True
1090 1089 Session.add(inv_obj)
1091 1090 Session.commit()
1092 1091
1093 1092 class DbMigrateVersion(Base, BaseModel):
1094 1093 __tablename__ = 'db_migrate_version'
1095 1094 __table_args__ = {'extend_existing':True}
1096 1095 repository_id = Column('repository_id', String(250), primary_key=True)
1097 1096 repository_path = Column('repository_path', Text)
1098 1097 version = Column('version', Integer)
@@ -1,914 +1,915 b''
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 import StringIO
9 9 import urllib
10 10 import math
11 11 import logging
12 12
13 13 from datetime import datetime
14 14 from pygments.formatters.html import HtmlFormatter
15 15 from pygments import highlight as code_highlight
16 16 from pylons import url, request, config
17 17 from pylons.i18n.translation import _, ungettext
18 18 from hashlib import md5
19 19
20 20 from webhelpers.html import literal, HTML, escape
21 21 from webhelpers.html.tools import *
22 22 from webhelpers.html.builder import make_tag
23 23 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
24 24 end_form, file, form, hidden, image, javascript_link, link_to, \
25 25 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
26 26 submit, text, password, textarea, title, ul, xml_declaration, radio
27 27 from webhelpers.html.tools import auto_link, button_to, highlight, \
28 28 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
29 29 from webhelpers.number import format_byte_size, format_bit_size
30 30 from webhelpers.pylonslib import Flash as _Flash
31 31 from webhelpers.pylonslib.secure_form import secure_form
32 32 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
33 33 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
34 34 replace_whitespace, urlify, truncate, wrap_paragraphs
35 35 from webhelpers.date import time_ago_in_words
36 36 from webhelpers.paginate import Page
37 37 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
38 38 convert_boolean_attrs, NotGiven, _make_safe_id_component
39 39
40 40 from rhodecode.lib.annotate import annotate_highlight
41 41 from rhodecode.lib.utils import repo_name_slug
42 from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
42 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
43 get_changeset_safe
43 44 from rhodecode.lib.markup_renderer import MarkupRenderer
44 45
45 46 log = logging.getLogger(__name__)
46 47
47 48
48 49 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
49 50 """
50 51 Reset button
51 52 """
52 53 _set_input_attrs(attrs, type, name, value)
53 54 _set_id_attr(attrs, id, name)
54 55 convert_boolean_attrs(attrs, ["disabled"])
55 56 return HTML.input(**attrs)
56 57
57 58 reset = _reset
58 59 safeid = _make_safe_id_component
59 60
60 61
61 62 def FID(raw_id, path):
62 63 """
63 64 Creates a uniqe ID for filenode based on it's hash of path and revision
64 65 it's safe to use in urls
65 66
66 67 :param raw_id:
67 68 :param path:
68 69 """
69 70
70 71 return 'C-%s-%s' % (short_id(raw_id), md5(path).hexdigest()[:12])
71 72
72 73
73 74 def get_token():
74 75 """Return the current authentication token, creating one if one doesn't
75 76 already exist.
76 77 """
77 78 token_key = "_authentication_token"
78 79 from pylons import session
79 80 if not token_key in session:
80 81 try:
81 82 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
82 83 except AttributeError: # Python < 2.4
83 84 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
84 85 session[token_key] = token
85 86 if hasattr(session, 'save'):
86 87 session.save()
87 88 return session[token_key]
88 89
89 90 class _GetError(object):
90 91 """Get error from form_errors, and represent it as span wrapped error
91 92 message
92 93
93 94 :param field_name: field to fetch errors for
94 95 :param form_errors: form errors dict
95 96 """
96 97
97 98 def __call__(self, field_name, form_errors):
98 99 tmpl = """<span class="error_msg">%s</span>"""
99 100 if form_errors and form_errors.has_key(field_name):
100 101 return literal(tmpl % form_errors.get(field_name))
101 102
102 103 get_error = _GetError()
103 104
104 105 class _ToolTip(object):
105 106
106 107 def __call__(self, tooltip_title, trim_at=50):
107 108 """Special function just to wrap our text into nice formatted
108 109 autowrapped text
109 110
110 111 :param tooltip_title:
111 112 """
112 113 return escape(tooltip_title)
113 114 tooltip = _ToolTip()
114 115
115 116 class _FilesBreadCrumbs(object):
116 117
117 118 def __call__(self, repo_name, rev, paths):
118 119 if isinstance(paths, str):
119 120 paths = safe_unicode(paths)
120 121 url_l = [link_to(repo_name, url('files_home',
121 122 repo_name=repo_name,
122 123 revision=rev, f_path=''))]
123 124 paths_l = paths.split('/')
124 125 for cnt, p in enumerate(paths_l):
125 126 if p != '':
126 127 url_l.append(link_to(p,
127 128 url('files_home',
128 129 repo_name=repo_name,
129 130 revision=rev,
130 131 f_path='/'.join(paths_l[:cnt + 1])
131 132 )
132 133 )
133 134 )
134 135
135 136 return literal('/'.join(url_l))
136 137
137 138 files_breadcrumbs = _FilesBreadCrumbs()
138 139
139 140 class CodeHtmlFormatter(HtmlFormatter):
140 141 """My code Html Formatter for source codes
141 142 """
142 143
143 144 def wrap(self, source, outfile):
144 145 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
145 146
146 147 def _wrap_code(self, source):
147 148 for cnt, it in enumerate(source):
148 149 i, t = it
149 150 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
150 151 yield i, t
151 152
152 153 def _wrap_tablelinenos(self, inner):
153 154 dummyoutfile = StringIO.StringIO()
154 155 lncount = 0
155 156 for t, line in inner:
156 157 if t:
157 158 lncount += 1
158 159 dummyoutfile.write(line)
159 160
160 161 fl = self.linenostart
161 162 mw = len(str(lncount + fl - 1))
162 163 sp = self.linenospecial
163 164 st = self.linenostep
164 165 la = self.lineanchors
165 166 aln = self.anchorlinenos
166 167 nocls = self.noclasses
167 168 if sp:
168 169 lines = []
169 170
170 171 for i in range(fl, fl + lncount):
171 172 if i % st == 0:
172 173 if i % sp == 0:
173 174 if aln:
174 175 lines.append('<a href="#%s%d" class="special">%*d</a>' %
175 176 (la, i, mw, i))
176 177 else:
177 178 lines.append('<span class="special">%*d</span>' % (mw, i))
178 179 else:
179 180 if aln:
180 181 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
181 182 else:
182 183 lines.append('%*d' % (mw, i))
183 184 else:
184 185 lines.append('')
185 186 ls = '\n'.join(lines)
186 187 else:
187 188 lines = []
188 189 for i in range(fl, fl + lncount):
189 190 if i % st == 0:
190 191 if aln:
191 192 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
192 193 else:
193 194 lines.append('%*d' % (mw, i))
194 195 else:
195 196 lines.append('')
196 197 ls = '\n'.join(lines)
197 198
198 199 # in case you wonder about the seemingly redundant <div> here: since the
199 200 # content in the other cell also is wrapped in a div, some browsers in
200 201 # some configurations seem to mess up the formatting...
201 202 if nocls:
202 203 yield 0, ('<table class="%stable">' % self.cssclass +
203 204 '<tr><td><div class="linenodiv" '
204 205 'style="background-color: #f0f0f0; padding-right: 10px">'
205 206 '<pre style="line-height: 125%">' +
206 207 ls + '</pre></div></td><td id="hlcode" class="code">')
207 208 else:
208 209 yield 0, ('<table class="%stable">' % self.cssclass +
209 210 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
210 211 ls + '</pre></div></td><td id="hlcode" class="code">')
211 212 yield 0, dummyoutfile.getvalue()
212 213 yield 0, '</td></tr></table>'
213 214
214 215
215 216 def pygmentize(filenode, **kwargs):
216 217 """pygmentize function using pygments
217 218
218 219 :param filenode:
219 220 """
220 221
221 222 return literal(code_highlight(filenode.content,
222 223 filenode.lexer, CodeHtmlFormatter(**kwargs)))
223 224
224 225
225 226 def pygmentize_annotation(repo_name, filenode, **kwargs):
226 227 """
227 228 pygmentize function for annotation
228 229
229 230 :param filenode:
230 231 """
231 232
232 233 color_dict = {}
233 234
234 235 def gen_color(n=10000):
235 236 """generator for getting n of evenly distributed colors using
236 237 hsv color and golden ratio. It always return same order of colors
237 238
238 239 :returns: RGB tuple
239 240 """
240 241
241 242 def hsv_to_rgb(h, s, v):
242 243 if s == 0.0:
243 244 return v, v, v
244 245 i = int(h * 6.0) # XXX assume int() truncates!
245 246 f = (h * 6.0) - i
246 247 p = v * (1.0 - s)
247 248 q = v * (1.0 - s * f)
248 249 t = v * (1.0 - s * (1.0 - f))
249 250 i = i % 6
250 251 if i == 0:
251 252 return v, t, p
252 253 if i == 1:
253 254 return q, v, p
254 255 if i == 2:
255 256 return p, v, t
256 257 if i == 3:
257 258 return p, q, v
258 259 if i == 4:
259 260 return t, p, v
260 261 if i == 5:
261 262 return v, p, q
262 263
263 264 golden_ratio = 0.618033988749895
264 265 h = 0.22717784590367374
265 266
266 267 for _ in xrange(n):
267 268 h += golden_ratio
268 269 h %= 1
269 270 HSV_tuple = [h, 0.95, 0.95]
270 271 RGB_tuple = hsv_to_rgb(*HSV_tuple)
271 272 yield map(lambda x: str(int(x * 256)), RGB_tuple)
272 273
273 274 cgenerator = gen_color()
274 275
275 276 def get_color_string(cs):
276 277 if cs in color_dict:
277 278 col = color_dict[cs]
278 279 else:
279 280 col = color_dict[cs] = cgenerator.next()
280 281 return "color: rgb(%s)! important;" % (', '.join(col))
281 282
282 283 def url_func(repo_name):
283 284
284 285 def _url_func(changeset):
285 286 author = changeset.author
286 287 date = changeset.date
287 288 message = tooltip(changeset.message)
288 289
289 290 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
290 291 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
291 292 "</b> %s<br/></div>")
292 293
293 294 tooltip_html = tooltip_html % (author, date, message)
294 295 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
295 296 short_id(changeset.raw_id))
296 297 uri = link_to(
297 298 lnk_format,
298 299 url('changeset_home', repo_name=repo_name,
299 300 revision=changeset.raw_id),
300 301 style=get_color_string(changeset.raw_id),
301 302 class_='tooltip',
302 303 title=tooltip_html
303 304 )
304 305
305 306 uri += '\n'
306 307 return uri
307 308 return _url_func
308 309
309 310 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
310 311
311 312
312 313 def is_following_repo(repo_name, user_id):
313 314 from rhodecode.model.scm import ScmModel
314 315 return ScmModel().is_following_repo(repo_name, user_id)
315 316
316 317 flash = _Flash()
317 318
318 319 #==============================================================================
319 320 # SCM FILTERS available via h.
320 321 #==============================================================================
321 322 from rhodecode.lib.vcs.utils import author_name, author_email
322 from rhodecode.lib import credentials_filter, age as _age
323 from rhodecode.lib.utils2 import credentials_filter, age as _age
323 324 from rhodecode.model.db import User
324 325
325 326 age = lambda x: _age(x)
326 327 capitalize = lambda x: x.capitalize()
327 328 email = author_email
328 329 short_id = lambda x: x[:12]
329 330 hide_credentials = lambda x: ''.join(credentials_filter(x))
330 331
331 332
332 333 def is_git(repository):
333 334 if hasattr(repository, 'alias'):
334 335 _type = repository.alias
335 336 elif hasattr(repository, 'repo_type'):
336 337 _type = repository.repo_type
337 338 else:
338 339 _type = repository
339 340 return _type == 'git'
340 341
341 342
342 343 def is_hg(repository):
343 344 if hasattr(repository, 'alias'):
344 345 _type = repository.alias
345 346 elif hasattr(repository, 'repo_type'):
346 347 _type = repository.repo_type
347 348 else:
348 349 _type = repository
349 350 return _type == 'hg'
350 351
351 352
352 353 def email_or_none(author):
353 354 _email = email(author)
354 355 if _email != '':
355 356 return _email
356 357
357 358 # See if it contains a username we can get an email from
358 359 user = User.get_by_username(author_name(author), case_insensitive=True,
359 360 cache=True)
360 361 if user is not None:
361 362 return user.email
362 363
363 364 # No valid email, not a valid user in the system, none!
364 365 return None
365 366
366 367
367 368 def person(author):
368 369 # attr to return from fetched user
369 370 person_getter = lambda usr: usr.username
370 371
371 372 # Valid email in the attribute passed, see if they're in the system
372 373 _email = email(author)
373 374 if _email != '':
374 375 user = User.get_by_email(_email, case_insensitive=True, cache=True)
375 376 if user is not None:
376 377 return person_getter(user)
377 378 return _email
378 379
379 380 # Maybe it's a username?
380 381 _author = author_name(author)
381 382 user = User.get_by_username(_author, case_insensitive=True,
382 383 cache=True)
383 384 if user is not None:
384 385 return person_getter(user)
385 386
386 387 # Still nothing? Just pass back the author name then
387 388 return _author
388 389
389 390
390 391 def bool2icon(value):
391 392 """Returns True/False values represented as small html image of true/false
392 393 icons
393 394
394 395 :param value: bool value
395 396 """
396 397
397 398 if value is True:
398 399 return HTML.tag('img', src=url("/images/icons/accept.png"),
399 400 alt=_('True'))
400 401
401 402 if value is False:
402 403 return HTML.tag('img', src=url("/images/icons/cancel.png"),
403 404 alt=_('False'))
404 405
405 406 return value
406 407
407 408
408 409 def action_parser(user_log, feed=False):
409 410 """
410 411 This helper will action_map the specified string action into translated
411 412 fancy names with icons and links
412 413
413 414 :param user_log: user log instance
414 415 :param feed: use output for feeds (no html and fancy icons)
415 416 """
416 417
417 418 action = user_log.action
418 419 action_params = ' '
419 420
420 421 x = action.split(':')
421 422
422 423 if len(x) > 1:
423 424 action, action_params = x
424 425
425 426 def get_cs_links():
426 427 revs_limit = 3 # display this amount always
427 428 revs_top_limit = 50 # show upto this amount of changesets hidden
428 429 revs_ids = action_params.split(',')
429 430 deleted = user_log.repository is None
430 431 if deleted:
431 432 return ','.join(revs_ids)
432 433
433 434 repo_name = user_log.repository.repo_name
434 435
435 436 repo = user_log.repository.scm_instance
436 437
437 438 message = lambda rev: rev.message
438 439 lnk = lambda rev, repo_name: (
439 440 link_to('r%s:%s' % (rev.revision, rev.short_id),
440 441 url('changeset_home', repo_name=repo_name,
441 442 revision=rev.raw_id),
442 443 title=tooltip(message(rev)), class_='tooltip')
443 444 )
444 445 # get only max revs_top_limit of changeset for performance/ui reasons
445 446 revs = [
446 447 x for x in repo.get_changesets(revs_ids[0],
447 448 revs_ids[:revs_top_limit][-1])
448 449 ]
449 450
450 451 cs_links = []
451 452 cs_links.append(" " + ', '.join(
452 453 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
453 454 )
454 455 )
455 456
456 457 compare_view = (
457 458 ' <div class="compare_view tooltip" title="%s">'
458 459 '<a href="%s">%s</a> </div>' % (
459 460 _('Show all combined changesets %s->%s') % (
460 461 revs_ids[0], revs_ids[-1]
461 462 ),
462 463 url('changeset_home', repo_name=repo_name,
463 464 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
464 465 ),
465 466 _('compare view')
466 467 )
467 468 )
468 469
469 470 # if we have exactly one more than normally displayed
470 471 # just display it, takes less space than displaying
471 472 # "and 1 more revisions"
472 473 if len(revs_ids) == revs_limit + 1:
473 474 rev = revs[revs_limit]
474 475 cs_links.append(", " + lnk(rev, repo_name))
475 476
476 477 # hidden-by-default ones
477 478 if len(revs_ids) > revs_limit + 1:
478 479 uniq_id = revs_ids[0]
479 480 html_tmpl = (
480 481 '<span> %s <a class="show_more" id="_%s" '
481 482 'href="#more">%s</a> %s</span>'
482 483 )
483 484 if not feed:
484 485 cs_links.append(html_tmpl % (
485 486 _('and'),
486 487 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
487 488 _('revisions')
488 489 )
489 490 )
490 491
491 492 if not feed:
492 493 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
493 494 else:
494 495 html_tmpl = '<span id="%s"> %s </span>'
495 496
496 497 morelinks = ', '.join(
497 498 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
498 499 )
499 500
500 501 if len(revs_ids) > revs_top_limit:
501 502 morelinks += ', ...'
502 503
503 504 cs_links.append(html_tmpl % (uniq_id, morelinks))
504 505 if len(revs) > 1:
505 506 cs_links.append(compare_view)
506 507 return ''.join(cs_links)
507 508
508 509 def get_fork_name():
509 510 repo_name = action_params
510 511 return _('fork name ') + str(link_to(action_params, url('summary_home',
511 512 repo_name=repo_name,)))
512 513
513 514 action_map = {'user_deleted_repo': (_('[deleted] repository'), None),
514 515 'user_created_repo': (_('[created] repository'), None),
515 516 'user_created_fork': (_('[created] repository as fork'), None),
516 517 'user_forked_repo': (_('[forked] repository'), get_fork_name),
517 518 'user_updated_repo': (_('[updated] repository'), None),
518 519 'admin_deleted_repo': (_('[delete] repository'), None),
519 520 'admin_created_repo': (_('[created] repository'), None),
520 521 'admin_forked_repo': (_('[forked] repository'), None),
521 522 'admin_updated_repo': (_('[updated] repository'), None),
522 523 'push': (_('[pushed] into'), get_cs_links),
523 524 'push_local': (_('[committed via RhodeCode] into'), get_cs_links),
524 525 'push_remote': (_('[pulled from remote] into'), get_cs_links),
525 526 'pull': (_('[pulled] from'), None),
526 527 'started_following_repo': (_('[started following] repository'), None),
527 528 'stopped_following_repo': (_('[stopped following] repository'), None),
528 529 }
529 530
530 531 action_str = action_map.get(action, action)
531 532 if feed:
532 533 action = action_str[0].replace('[', '').replace(']', '')
533 534 else:
534 535 action = action_str[0]\
535 536 .replace('[', '<span class="journal_highlight">')\
536 537 .replace(']', '</span>')
537 538
538 539 action_params_func = lambda: ""
539 540
540 541 if callable(action_str[1]):
541 542 action_params_func = action_str[1]
542 543
543 544 return [literal(action), action_params_func]
544 545
545 546
546 547 def action_parser_icon(user_log):
547 548 action = user_log.action
548 549 action_params = None
549 550 x = action.split(':')
550 551
551 552 if len(x) > 1:
552 553 action, action_params = x
553 554
554 555 tmpl = """<img src="%s%s" alt="%s"/>"""
555 556 map = {'user_deleted_repo':'database_delete.png',
556 557 'user_created_repo':'database_add.png',
557 558 'user_created_fork':'arrow_divide.png',
558 559 'user_forked_repo':'arrow_divide.png',
559 560 'user_updated_repo':'database_edit.png',
560 561 'admin_deleted_repo':'database_delete.png',
561 562 'admin_created_repo':'database_add.png',
562 563 'admin_forked_repo':'arrow_divide.png',
563 564 'admin_updated_repo':'database_edit.png',
564 565 'push':'script_add.png',
565 566 'push_local':'script_edit.png',
566 567 'push_remote':'connect.png',
567 568 'pull':'down_16.png',
568 569 'started_following_repo':'heart_add.png',
569 570 'stopped_following_repo':'heart_delete.png',
570 571 }
571 572 return literal(tmpl % ((url('/images/icons/')),
572 573 map.get(action, action), action))
573 574
574 575
575 576 #==============================================================================
576 577 # PERMS
577 578 #==============================================================================
578 579 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
579 580 HasRepoPermissionAny, HasRepoPermissionAll
580 581
581 582
582 583 #==============================================================================
583 584 # GRAVATAR URL
584 585 #==============================================================================
585 586
586 587 def gravatar_url(email_address, size=30):
587 588 if (not str2bool(config['app_conf'].get('use_gravatar')) or
588 589 not email_address or email_address == 'anonymous@rhodecode.org'):
589 590 f = lambda a, l: min(l, key=lambda x: abs(x - a))
590 591 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
591 592
592 593 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
593 594 default = 'identicon'
594 595 baseurl_nossl = "http://www.gravatar.com/avatar/"
595 596 baseurl_ssl = "https://secure.gravatar.com/avatar/"
596 597 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
597 598
598 599 if isinstance(email_address, unicode):
599 600 #hashlib crashes on unicode items
600 601 email_address = safe_str(email_address)
601 602 # construct the url
602 603 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
603 604 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
604 605
605 606 return gravatar_url
606 607
607 608
608 609 #==============================================================================
609 610 # REPO PAGER, PAGER FOR REPOSITORY
610 611 #==============================================================================
611 612 class RepoPage(Page):
612 613
613 614 def __init__(self, collection, page=1, items_per_page=20,
614 615 item_count=None, url=None, **kwargs):
615 616
616 617 """Create a "RepoPage" instance. special pager for paging
617 618 repository
618 619 """
619 620 self._url_generator = url
620 621
621 622 # Safe the kwargs class-wide so they can be used in the pager() method
622 623 self.kwargs = kwargs
623 624
624 625 # Save a reference to the collection
625 626 self.original_collection = collection
626 627
627 628 self.collection = collection
628 629
629 630 # The self.page is the number of the current page.
630 631 # The first page has the number 1!
631 632 try:
632 633 self.page = int(page) # make it int() if we get it as a string
633 634 except (ValueError, TypeError):
634 635 self.page = 1
635 636
636 637 self.items_per_page = items_per_page
637 638
638 639 # Unless the user tells us how many items the collections has
639 640 # we calculate that ourselves.
640 641 if item_count is not None:
641 642 self.item_count = item_count
642 643 else:
643 644 self.item_count = len(self.collection)
644 645
645 646 # Compute the number of the first and last available page
646 647 if self.item_count > 0:
647 648 self.first_page = 1
648 649 self.page_count = int(math.ceil(float(self.item_count) /
649 650 self.items_per_page))
650 651 self.last_page = self.first_page + self.page_count - 1
651 652
652 653 # Make sure that the requested page number is the range of
653 654 # valid pages
654 655 if self.page > self.last_page:
655 656 self.page = self.last_page
656 657 elif self.page < self.first_page:
657 658 self.page = self.first_page
658 659
659 660 # Note: the number of items on this page can be less than
660 661 # items_per_page if the last page is not full
661 662 self.first_item = max(0, (self.item_count) - (self.page *
662 663 items_per_page))
663 664 self.last_item = ((self.item_count - 1) - items_per_page *
664 665 (self.page - 1))
665 666
666 667 self.items = list(self.collection[self.first_item:self.last_item + 1])
667 668
668 669 # Links to previous and next page
669 670 if self.page > self.first_page:
670 671 self.previous_page = self.page - 1
671 672 else:
672 673 self.previous_page = None
673 674
674 675 if self.page < self.last_page:
675 676 self.next_page = self.page + 1
676 677 else:
677 678 self.next_page = None
678 679
679 680 # No items available
680 681 else:
681 682 self.first_page = None
682 683 self.page_count = 0
683 684 self.last_page = None
684 685 self.first_item = None
685 686 self.last_item = None
686 687 self.previous_page = None
687 688 self.next_page = None
688 689 self.items = []
689 690
690 691 # This is a subclass of the 'list' type. Initialise the list now.
691 692 list.__init__(self, reversed(self.items))
692 693
693 694
694 695 def changed_tooltip(nodes):
695 696 """
696 697 Generates a html string for changed nodes in changeset page.
697 698 It limits the output to 30 entries
698 699
699 700 :param nodes: LazyNodesGenerator
700 701 """
701 702 if nodes:
702 703 pref = ': <br/> '
703 704 suf = ''
704 705 if len(nodes) > 30:
705 706 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
706 707 return literal(pref + '<br/> '.join([safe_unicode(x.path)
707 708 for x in nodes[:30]]) + suf)
708 709 else:
709 710 return ': ' + _('No Files')
710 711
711 712
712 713 def repo_link(groups_and_repos):
713 714 """
714 715 Makes a breadcrumbs link to repo within a group
715 716 joins &raquo; on each group to create a fancy link
716 717
717 718 ex::
718 719 group >> subgroup >> repo
719 720
720 721 :param groups_and_repos:
721 722 """
722 723 groups, repo_name = groups_and_repos
723 724
724 725 if not groups:
725 726 return repo_name
726 727 else:
727 728 def make_link(group):
728 729 return link_to(group.name, url('repos_group_home',
729 730 group_name=group.group_name))
730 731 return literal(' &raquo; '.join(map(make_link, groups)) + \
731 732 " &raquo; " + repo_name)
732 733
733 734
734 735 def fancy_file_stats(stats):
735 736 """
736 737 Displays a fancy two colored bar for number of added/deleted
737 738 lines of code on file
738 739
739 740 :param stats: two element list of added/deleted lines of code
740 741 """
741 742
742 743 a, d, t = stats[0], stats[1], stats[0] + stats[1]
743 744 width = 100
744 745 unit = float(width) / (t or 1)
745 746
746 747 # needs > 9% of width to be visible or 0 to be hidden
747 748 a_p = max(9, unit * a) if a > 0 else 0
748 749 d_p = max(9, unit * d) if d > 0 else 0
749 750 p_sum = a_p + d_p
750 751
751 752 if p_sum > width:
752 753 #adjust the percentage to be == 100% since we adjusted to 9
753 754 if a_p > d_p:
754 755 a_p = a_p - (p_sum - width)
755 756 else:
756 757 d_p = d_p - (p_sum - width)
757 758
758 759 a_v = a if a > 0 else ''
759 760 d_v = d if d > 0 else ''
760 761
761 762 def cgen(l_type):
762 763 mapping = {'tr': 'top-right-rounded-corner',
763 764 'tl': 'top-left-rounded-corner',
764 765 'br': 'bottom-right-rounded-corner',
765 766 'bl': 'bottom-left-rounded-corner'}
766 767 map_getter = lambda x: mapping[x]
767 768
768 769 if l_type == 'a' and d_v:
769 770 #case when added and deleted are present
770 771 return ' '.join(map(map_getter, ['tl', 'bl']))
771 772
772 773 if l_type == 'a' and not d_v:
773 774 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
774 775
775 776 if l_type == 'd' and a_v:
776 777 return ' '.join(map(map_getter, ['tr', 'br']))
777 778
778 779 if l_type == 'd' and not a_v:
779 780 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
780 781
781 782 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
782 783 cgen('a'), a_p, a_v
783 784 )
784 785 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
785 786 cgen('d'), d_p, d_v
786 787 )
787 788 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
788 789
789 790
790 791 def urlify_text(text_):
791 792 import re
792 793
793 794 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
794 795 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
795 796
796 797 def url_func(match_obj):
797 798 url_full = match_obj.groups()[0]
798 799 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
799 800
800 801 return literal(url_pat.sub(url_func, text_))
801 802
802 803
803 804 def urlify_changesets(text_, repository):
804 805 import re
805 806 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
806 807
807 808 def url_func(match_obj):
808 809 rev = match_obj.groups()[0]
809 810 pref = ''
810 811 if match_obj.group().startswith(' '):
811 812 pref = ' '
812 813 tmpl = (
813 814 '%(pref)s<a class="%(cls)s" href="%(url)s">'
814 815 '%(rev)s'
815 816 '</a>'
816 817 )
817 818 return tmpl % {
818 819 'pref': pref,
819 820 'cls': 'revision-link',
820 821 'url': url('changeset_home', repo_name=repository, revision=rev),
821 822 'rev': rev,
822 823 }
823 824
824 825 newtext = URL_PAT.sub(url_func, text_)
825 826
826 827 return newtext
827 828
828 829
829 830 def urlify_commit(text_, repository=None, link_=None):
830 831 """
831 832 Parses given text message and makes proper links.
832 833 issues are linked to given issue-server, and rest is a changeset link
833 834 if link_ is given, in other case it's a plain text
834 835
835 836 :param text_:
836 837 :param repository:
837 838 :param link_: changeset link
838 839 """
839 840 import re
840 841 import traceback
841 842
842 843 # urlify changesets
843 844 text_ = urlify_changesets(text_, repository)
844 845
845 846 def linkify_others(t, l):
846 847 urls = re.compile(r'(\<a.*?\<\/a\>)',)
847 848 links = []
848 849 for e in urls.split(t):
849 850 if not urls.match(e):
850 851 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
851 852 else:
852 853 links.append(e)
853 854
854 855 return ''.join(links)
855 856 try:
856 857 conf = config['app_conf']
857 858
858 859 URL_PAT = re.compile(r'%s' % conf.get('issue_pat'))
859 860
860 861 if URL_PAT:
861 862 ISSUE_SERVER_LNK = conf.get('issue_server_link')
862 863 ISSUE_PREFIX = conf.get('issue_prefix')
863 864
864 865 def url_func(match_obj):
865 866 pref = ''
866 867 if match_obj.group().startswith(' '):
867 868 pref = ' '
868 869
869 870 issue_id = ''.join(match_obj.groups())
870 871 tmpl = (
871 872 '%(pref)s<a class="%(cls)s" href="%(url)s">'
872 873 '%(issue-prefix)s%(id-repr)s'
873 874 '</a>'
874 875 )
875 876 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
876 877 if repository:
877 878 url = url.replace('{repo}', repository)
878 879
879 880 return tmpl % {
880 881 'pref': pref,
881 882 'cls': 'issue-tracker-link',
882 883 'url': url,
883 884 'id-repr': issue_id,
884 885 'issue-prefix': ISSUE_PREFIX,
885 886 'serv': ISSUE_SERVER_LNK,
886 887 }
887 888
888 889 newtext = URL_PAT.sub(url_func, text_)
889 890
890 891 if link_:
891 892 # wrap not links into final link => link_
892 893 newtext = linkify_others(newtext, link_)
893 894
894 895 return literal(newtext)
895 896 except:
896 897 log.error(traceback.format_exc())
897 898 pass
898 899
899 900 return text_
900 901
901 902
902 903 def rst(source):
903 904 return literal('<div class="rst-block">%s</div>' %
904 905 MarkupRenderer.rst(source))
905 906
906 907
907 908 def rst_w_mentions(source):
908 909 """
909 910 Wrapped rst renderer with @mention highlighting
910 911
911 912 :param source:
912 913 """
913 914 return literal('<div class="rst-block">%s</div>' %
914 915 MarkupRenderer.rst_with_mentions(source))
@@ -1,174 +1,174 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.hooks
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Hooks runned by rhodecode
7 7
8 8 :created_on: Aug 6, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import os
26 26 import sys
27 27
28 28 from mercurial.scmutil import revrange
29 29 from mercurial.node import nullrev
30 30 from rhodecode import EXTENSIONS
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib.utils import action_logger
33 33 from inspect import isfunction
34 34
35 35
36 36 def repo_size(ui, repo, hooktype=None, **kwargs):
37 37 """
38 38 Presents size of repository after push
39 39
40 40 :param ui:
41 41 :param repo:
42 42 :param hooktype:
43 43 """
44 44
45 45 size_hg, size_root = 0, 0
46 46 for path, dirs, files in os.walk(repo.root):
47 47 if path.find('.hg') != -1:
48 48 for f in files:
49 49 try:
50 50 size_hg += os.path.getsize(os.path.join(path, f))
51 51 except OSError:
52 52 pass
53 53 else:
54 54 for f in files:
55 55 try:
56 56 size_root += os.path.getsize(os.path.join(path, f))
57 57 except OSError:
58 58 pass
59 59
60 60 size_hg_f = h.format_byte_size(size_hg)
61 61 size_root_f = h.format_byte_size(size_root)
62 62 size_total_f = h.format_byte_size(size_root + size_hg)
63 63
64 64 last_cs = repo[len(repo) - 1]
65 65
66 66 msg = ('Repository size .hg:%s repo:%s total:%s\n'
67 67 'Last revision is now r%s:%s\n') % (
68 68 size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12]
69 69 )
70 70
71 71 sys.stdout.write(msg)
72 72
73 73
74 74 def log_pull_action(ui, repo, **kwargs):
75 75 """
76 76 Logs user last pull action
77 77
78 78 :param ui:
79 79 :param repo:
80 80 """
81 81
82 82 extras = dict(repo.ui.configitems('rhodecode_extras'))
83 83 username = extras['username']
84 84 repository = extras['repository']
85 85 action = 'pull'
86 86
87 87 action_logger(username, action, repository, extras['ip'], commit=True)
88 88 # extension hook call
89 89 callback = getattr(EXTENSIONS, 'PULL_HOOK', None)
90 90
91 91 if isfunction(callback):
92 92 kw = {}
93 93 kw.update(extras)
94 94 callback(**kw)
95 95 return 0
96 96
97 97
98 98 def log_push_action(ui, repo, **kwargs):
99 99 """
100 100 Maps user last push action to new changeset id, from mercurial
101 101
102 102 :param ui:
103 103 :param repo:
104 104 """
105 105
106 106 extras = dict(repo.ui.configitems('rhodecode_extras'))
107 107 username = extras['username']
108 108 repository = extras['repository']
109 109 action = extras['action'] + ':%s'
110 110 node = kwargs['node']
111 111
112 112 def get_revs(repo, rev_opt):
113 113 if rev_opt:
114 114 revs = revrange(repo, rev_opt)
115 115
116 116 if len(revs) == 0:
117 117 return (nullrev, nullrev)
118 118 return (max(revs), min(revs))
119 119 else:
120 120 return (len(repo) - 1, 0)
121 121
122 122 stop, start = get_revs(repo, [node + ':'])
123 123
124 124 revs = (str(repo[r]) for r in xrange(start, stop + 1))
125 125
126 126 action = action % ','.join(revs)
127 127
128 128 action_logger(username, action, repository, extras['ip'], commit=True)
129 129
130 130 # extension hook call
131 131 callback = getattr(EXTENSIONS, 'PUSH_HOOK', None)
132 132 if isfunction(callback):
133 133 kw = {'pushed_revs': revs}
134 134 kw.update(extras)
135 135 callback(**kw)
136 136 return 0
137 137
138 138
139 139 def log_create_repository(repository_dict, created_by, **kwargs):
140 140 """
141 141 Post create repository Hook. This is a dummy function for admins to re-use
142 if needed. It's taken from rhodecode-extensions module and executed
142 if needed. It's taken from rhodecode-extensions module and executed
143 143 if present
144 144
145 145 :param repository: dict dump of repository object
146 146 :param created_by: username who created repository
147 147 :param created_date: date of creation
148 148
149 149 available keys of repository_dict:
150 150
151 151 'repo_type',
152 152 'description',
153 153 'private',
154 154 'created_on',
155 155 'enable_downloads',
156 156 'repo_id',
157 157 'user_id',
158 158 'enable_statistics',
159 159 'clone_uri',
160 160 'fork_id',
161 161 'group_id',
162 162 'repo_name'
163 163
164 164 """
165 165
166 166 callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
167 167 if isfunction(callback):
168 168 kw = {}
169 169 kw.update(repository_dict)
170 170 kw.update({'created_by': created_by})
171 171 kw.update(kwargs)
172 172 return callback(**kw)
173 173
174 174 return 0
@@ -1,228 +1,227 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.indexers.__init__
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Whoosh indexing module for RhodeCode
7 7
8 8 :created_on: Aug 17, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import os
26 26 import sys
27 27 import traceback
28 28 import logging
29 29 from os.path import dirname as dn, join as jn
30 30
31 31 #to get the rhodecode import
32 32 sys.path.append(dn(dn(dn(os.path.realpath(__file__)))))
33 33
34 34 from string import strip
35 35 from shutil import rmtree
36 36
37 37 from whoosh.analysis import RegexTokenizer, LowercaseFilter, StopFilter
38 38 from whoosh.fields import TEXT, ID, STORED, Schema, FieldType
39 39 from whoosh.index import create_in, open_dir
40 40 from whoosh.formats import Characters
41 41 from whoosh.highlight import highlight, HtmlFormatter, ContextFragmenter
42 42
43 43 from webhelpers.html.builder import escape
44 44 from sqlalchemy import engine_from_config
45 45
46 46 from rhodecode.model import init_model
47 47 from rhodecode.model.scm import ScmModel
48 48 from rhodecode.model.repo import RepoModel
49 49 from rhodecode.config.environment import load_environment
50 from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, INDEX_EXTENSIONS, \
51 LazyProperty
52 from rhodecode.lib.utils import BasePasterCommand, Command, add_cache
50 from rhodecode.lib.utils2 import LazyProperty
51 from rhodecode.lib.utils import BasePasterCommand, Command, add_cache,\
52 load_rcextensions
53 53
54 54 # CUSTOM ANALYZER wordsplit + lowercase filter
55 55 ANALYZER = RegexTokenizer(expression=r"\w+") | LowercaseFilter()
56 56
57 57
58 58 #INDEX SCHEMA DEFINITION
59 59 SCHEMA = Schema(
60 60 owner=TEXT(),
61 61 repository=TEXT(stored=True),
62 62 path=TEXT(stored=True),
63 63 content=FieldType(format=Characters(), analyzer=ANALYZER,
64 64 scorable=True, stored=True),
65 65 modtime=STORED(),
66 66 extension=TEXT(stored=True)
67 67 )
68 68
69 69 IDX_NAME = 'HG_INDEX'
70 70 FORMATTER = HtmlFormatter('span', between='\n<span class="break">...</span>\n')
71 71 FRAGMENTER = ContextFragmenter(200)
72 72
73 73
74 74 class MakeIndex(BasePasterCommand):
75 75
76 76 max_args = 1
77 77 min_args = 1
78 78
79 79 usage = "CONFIG_FILE"
80 80 summary = "Creates index for full text search given configuration file"
81 81 group_name = "RhodeCode"
82 82 takes_config_file = -1
83 83 parser = Command.standard_parser(verbose=True)
84 84
85 85 def command(self):
86 86 logging.config.fileConfig(self.path_to_ini_file)
87 87 from pylons import config
88 88 add_cache(config)
89 89 engine = engine_from_config(config, 'sqlalchemy.db1.')
90 90 init_model(engine)
91
92 91 index_location = config['index_dir']
93 92 repo_location = self.options.repo_location \
94 93 if self.options.repo_location else RepoModel().repos_path
95 94 repo_list = map(strip, self.options.repo_list.split(',')) \
96 95 if self.options.repo_list else None
97
96 load_rcextensions(config['here'])
98 97 #======================================================================
99 98 # WHOOSH DAEMON
100 99 #======================================================================
101 100 from rhodecode.lib.pidlock import LockHeld, DaemonLock
102 101 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
103 102 try:
104 103 l = DaemonLock(file_=jn(dn(dn(index_location)), 'make_index.lock'))
105 104 WhooshIndexingDaemon(index_location=index_location,
106 105 repo_location=repo_location,
107 repo_list=repo_list)\
106 repo_list=repo_list,)\
108 107 .run(full_index=self.options.full_index)
109 108 l.release()
110 109 except LockHeld:
111 110 sys.exit(1)
112 111
113 112 def update_parser(self):
114 113 self.parser.add_option('--repo-location',
115 114 action='store',
116 115 dest='repo_location',
117 116 help="Specifies repositories location to index OPTIONAL",
118 117 )
119 118 self.parser.add_option('--index-only',
120 119 action='store',
121 120 dest='repo_list',
122 121 help="Specifies a comma separated list of repositores "
123 122 "to build index on OPTIONAL",
124 123 )
125 124 self.parser.add_option('-f',
126 125 action='store_true',
127 126 dest='full_index',
128 127 help="Specifies that index should be made full i.e"
129 128 " destroy old and build from scratch",
130 129 default=False)
131 130
132 131
133 132 class ResultWrapper(object):
134 133 def __init__(self, search_type, searcher, matcher, highlight_items):
135 134 self.search_type = search_type
136 135 self.searcher = searcher
137 136 self.matcher = matcher
138 137 self.highlight_items = highlight_items
139 138 self.fragment_size = 200
140 139
141 140 @LazyProperty
142 141 def doc_ids(self):
143 142 docs_id = []
144 143 while self.matcher.is_active():
145 144 docnum = self.matcher.id()
146 145 chunks = [offsets for offsets in self.get_chunks()]
147 146 docs_id.append([docnum, chunks])
148 147 self.matcher.next()
149 148 return docs_id
150 149
151 150 def __str__(self):
152 151 return '<%s at %s>' % (self.__class__.__name__, len(self.doc_ids))
153 152
154 153 def __repr__(self):
155 154 return self.__str__()
156 155
157 156 def __len__(self):
158 157 return len(self.doc_ids)
159 158
160 159 def __iter__(self):
161 160 """
162 161 Allows Iteration over results,and lazy generate content
163 162
164 163 *Requires* implementation of ``__getitem__`` method.
165 164 """
166 165 for docid in self.doc_ids:
167 166 yield self.get_full_content(docid)
168 167
169 168 def __getitem__(self, key):
170 169 """
171 170 Slicing of resultWrapper
172 171 """
173 172 i, j = key.start, key.stop
174 173
175 174 slices = []
176 175 for docid in self.doc_ids[i:j]:
177 176 slices.append(self.get_full_content(docid))
178 177 return slices
179 178
180 179 def get_full_content(self, docid):
181 180 res = self.searcher.stored_fields(docid[0])
182 181 f_path = res['path'][res['path'].find(res['repository']) \
183 182 + len(res['repository']):].lstrip('/')
184 183
185 184 content_short = self.get_short_content(res, docid[1])
186 185 res.update({'content_short': content_short,
187 186 'content_short_hl': self.highlight(content_short),
188 187 'f_path': f_path})
189 188
190 189 return res
191 190
192 191 def get_short_content(self, res, chunks):
193 192
194 193 return ''.join([res['content'][chunk[0]:chunk[1]] for chunk in chunks])
195 194
196 195 def get_chunks(self):
197 196 """
198 197 Smart function that implements chunking the content
199 198 but not overlap chunks so it doesn't highlight the same
200 199 close occurrences twice.
201 200
202 201 :param matcher:
203 202 :param size:
204 203 """
205 204 memory = [(0, 0)]
206 205 for span in self.matcher.spans():
207 206 start = span.startchar or 0
208 207 end = span.endchar or 0
209 208 start_offseted = max(0, start - self.fragment_size)
210 209 end_offseted = end + self.fragment_size
211 210
212 211 if start_offseted < memory[-1][1]:
213 212 start_offseted = memory[-1][1]
214 213 memory.append((start_offseted, end_offseted,))
215 214 yield (start_offseted, end_offseted,)
216 215
217 216 def highlight(self, content, top=5):
218 217 if self.search_type != 'content':
219 218 return ''
220 219 hl = highlight(
221 220 text=escape(content),
222 221 terms=self.highlight_items,
223 222 analyzer=ANALYZER,
224 223 fragmenter=FRAGMENTER,
225 224 formatter=FORMATTER,
226 225 top=top
227 226 )
228 227 return hl
@@ -1,224 +1,237 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.indexers.daemon
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 A daemon will read from task table and run tasks
7 7
8 8 :created_on: Jan 26, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import sys
28 28 import logging
29 29 import traceback
30 30
31 31 from shutil import rmtree
32 32 from time import mktime
33 33
34 34 from os.path import dirname as dn
35 35 from os.path import join as jn
36 36
37 37 #to get the rhodecode import
38 38 project_path = dn(dn(dn(dn(os.path.realpath(__file__)))))
39 39 sys.path.append(project_path)
40 40
41
41 from rhodecode.config.conf import INDEX_EXTENSIONS
42 42 from rhodecode.model.scm import ScmModel
43 from rhodecode.lib import safe_unicode
44 from rhodecode.lib.indexers import INDEX_EXTENSIONS, SCHEMA, IDX_NAME
43 from rhodecode.lib.utils2 import safe_unicode
44 from rhodecode.lib.indexers import SCHEMA, IDX_NAME
45 45
46 46 from rhodecode.lib.vcs.exceptions import ChangesetError, RepositoryError, \
47 47 NodeDoesNotExistError
48 48
49 49 from whoosh.index import create_in, open_dir
50 50
51 51 log = logging.getLogger('whoosh_indexer')
52 52
53 53
54 54 class WhooshIndexingDaemon(object):
55 55 """
56 56 Daemon for atomic jobs
57 57 """
58 58
59 59 def __init__(self, indexname=IDX_NAME, index_location=None,
60 60 repo_location=None, sa=None, repo_list=None):
61 61 self.indexname = indexname
62 62
63 63 self.index_location = index_location
64 64 if not index_location:
65 65 raise Exception('You have to provide index location')
66 66
67 67 self.repo_location = repo_location
68 68 if not repo_location:
69 69 raise Exception('You have to provide repositories location')
70 70
71 71 self.repo_paths = ScmModel(sa).repo_scan(self.repo_location)
72 72
73 73 if repo_list:
74 74 filtered_repo_paths = {}
75 75 for repo_name, repo in self.repo_paths.items():
76 76 if repo_name in repo_list:
77 77 filtered_repo_paths[repo_name] = repo
78 78
79 79 self.repo_paths = filtered_repo_paths
80 80
81 81 self.initial = False
82 82 if not os.path.isdir(self.index_location):
83 83 os.makedirs(self.index_location)
84 84 log.info('Cannot run incremental index since it does not'
85 85 ' yet exist running full build')
86 86 self.initial = True
87 87
88 88 def get_paths(self, repo):
89 89 """
90 90 recursive walk in root dir and return a set of all path in that dir
91 91 based on repository walk function
92 92 """
93 93 index_paths_ = set()
94 94 try:
95 95 tip = repo.get_changeset('tip')
96 96 for topnode, dirs, files in tip.walk('/'):
97 97 for f in files:
98 98 index_paths_.add(jn(repo.path, f.path))
99 99
100 100 except RepositoryError, e:
101 101 log.debug(traceback.format_exc())
102 102 pass
103 103 return index_paths_
104 104
105 105 def get_node(self, repo, path):
106 106 n_path = path[len(repo.path) + 1:]
107 107 node = repo.get_changeset().get_node(n_path)
108 108 return node
109 109
110 110 def get_node_mtime(self, node):
111 111 return mktime(node.last_changeset.date.timetuple())
112 112
113 113 def add_doc(self, writer, path, repo, repo_name):
114 114 """
115 115 Adding doc to writer this function itself fetches data from
116 116 the instance of vcs backend
117 117 """
118 118
119 119 node = self.get_node(repo, path)
120
120 indexed = indexed_w_content = 0
121 121 # we just index the content of chosen files, and skip binary files
122 122 if node.extension in INDEX_EXTENSIONS and not node.is_binary:
123
124 123 u_content = node.content
125 124 if not isinstance(u_content, unicode):
126 125 log.warning(' >> %s Could not get this content as unicode '
127 126 'replacing with empty content' % path)
128 127 u_content = u''
129 128 else:
130 129 log.debug(' >> %s [WITH CONTENT]' % path)
130 indexed_w_content += 1
131 131
132 132 else:
133 133 log.debug(' >> %s' % path)
134 134 # just index file name without it's content
135 135 u_content = u''
136 indexed += 1
136 137
137 138 writer.add_document(
138 139 owner=unicode(repo.contact),
139 140 repository=safe_unicode(repo_name),
140 141 path=safe_unicode(path),
141 142 content=u_content,
142 143 modtime=self.get_node_mtime(node),
143 144 extension=node.extension
144 145 )
146 return indexed, indexed_w_content
145 147
146 148 def build_index(self):
147 149 if os.path.exists(self.index_location):
148 150 log.debug('removing previous index')
149 151 rmtree(self.index_location)
150 152
151 153 if not os.path.exists(self.index_location):
152 154 os.mkdir(self.index_location)
153 155
154 156 idx = create_in(self.index_location, SCHEMA, indexname=IDX_NAME)
155 157 writer = idx.writer()
156
158 log.debug('BUILDIN INDEX FOR EXTENSIONS %s' % INDEX_EXTENSIONS)
157 159 for repo_name, repo in self.repo_paths.items():
158 160 log.debug('building index @ %s' % repo.path)
159
161 i_cnt = iwc_cnt = 0
160 162 for idx_path in self.get_paths(repo):
161 self.add_doc(writer, idx_path, repo, repo_name)
163 i, iwc = self.add_doc(writer, idx_path, repo, repo_name)
164 i_cnt += i
165 iwc_cnt += iwc
166 log.debug('added %s files %s with content for repo %s' % (
167 i_cnt + iwc_cnt, iwc_cnt, repo.path)
168 )
162 169
163 170 log.debug('>> COMMITING CHANGES <<')
164 171 writer.commit(merge=True)
165 172 log.debug('>>> FINISHED BUILDING INDEX <<<')
166 173
167 174 def update_index(self):
168 log.debug('STARTING INCREMENTAL INDEXING UPDATE')
175 log.debug('STARTING INCREMENTAL INDEXING UPDATE FOR EXTENSIONS %s' %
176 INDEX_EXTENSIONS)
169 177
170 178 idx = open_dir(self.index_location, indexname=self.indexname)
171 179 # The set of all paths in the index
172 180 indexed_paths = set()
173 181 # The set of all paths we need to re-index
174 182 to_index = set()
175 183
176 184 reader = idx.reader()
177 185 writer = idx.writer()
178 186
179 187 # Loop over the stored fields in the index
180 188 for fields in reader.all_stored_fields():
181 189 indexed_path = fields['path']
182 190 indexed_paths.add(indexed_path)
183 191
184 192 repo = self.repo_paths[fields['repository']]
185 193
186 194 try:
187 195 node = self.get_node(repo, indexed_path)
188 196 except (ChangesetError, NodeDoesNotExistError):
189 197 # This file was deleted since it was indexed
190 198 log.debug('removing from index %s' % indexed_path)
191 199 writer.delete_by_term('path', indexed_path)
192 200
193 201 else:
194 202 # Check if this file was changed since it was indexed
195 203 indexed_time = fields['modtime']
196 204 mtime = self.get_node_mtime(node)
197 205 if mtime > indexed_time:
198 206 # The file has changed, delete it and add it to the list of
199 207 # files to reindex
200 208 log.debug('adding to reindex list %s' % indexed_path)
201 209 writer.delete_by_term('path', indexed_path)
202 210 to_index.add(indexed_path)
203 211
204 212 # Loop over the files in the filesystem
205 213 # Assume we have a function that gathers the filenames of the
206 214 # documents to be indexed
215 ri_cnt = riwc_cnt = 0
207 216 for repo_name, repo in self.repo_paths.items():
208 217 for path in self.get_paths(repo):
209 218 if path in to_index or path not in indexed_paths:
210 219 # This is either a file that's changed, or a new file
211 220 # that wasn't indexed before. So index it!
212 self.add_doc(writer, path, repo, repo_name)
221 i, iwc = self.add_doc(writer, path, repo, repo_name)
213 222 log.debug('re indexing %s' % path)
214
223 ri_cnt += i
224 riwc_cnt += iwc
225 log.debug('added %s files %s with content for repo %s' % (
226 ri_cnt + riwc_cnt, riwc_cnt, repo.path)
227 )
215 228 log.debug('>> COMMITING CHANGES <<')
216 229 writer.commit(merge=True)
217 230 log.debug('>>> FINISHED REBUILDING INDEX <<<')
218 231
219 232 def run(self, full_index=False):
220 233 """Run daemon"""
221 234 if full_index or self.initial:
222 235 self.build_index()
223 236 else:
224 237 self.update_index()
@@ -1,137 +1,137 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.markup_renderer
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6
7 7 Renderer for markup languages with ability to parse using rst or markdown
8 8
9 9 :created_on: Oct 27, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import re
28 28 import logging
29 29
30 from rhodecode.lib import safe_unicode
30 from rhodecode.lib.utils2 import safe_unicode
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 class MarkupRenderer(object):
36 36 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
37 37
38 38 MARKDOWN_PAT = re.compile(r'md|mkdn?|mdown|markdown', re.IGNORECASE)
39 39 RST_PAT = re.compile(r're?st', re.IGNORECASE)
40 40 PLAIN_PAT = re.compile(r'readme', re.IGNORECASE)
41 41
42 42 def __detect_renderer(self, source, filename=None):
43 43 """
44 44 runs detection of what renderer should be used for generating html
45 45 from a markup language
46 46
47 47 filename can be also explicitly a renderer name
48 48
49 49 :param source:
50 50 :param filename:
51 51 """
52 52
53 53 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
54 54 detected_renderer = 'markdown'
55 55 elif MarkupRenderer.RST_PAT.findall(filename):
56 56 detected_renderer = 'rst'
57 57 elif MarkupRenderer.PLAIN_PAT.findall(filename):
58 58 detected_renderer = 'rst'
59 59 else:
60 60 detected_renderer = 'plain'
61 61
62 62 return getattr(MarkupRenderer, detected_renderer)
63 63
64 64 def render(self, source, filename=None):
65 65 """
66 66 Renders a given filename using detected renderer
67 67 it detects renderers based on file extension or mimetype.
68 68 At last it will just do a simple html replacing new lines with <br/>
69 69
70 70 :param file_name:
71 71 :param source:
72 72 """
73 73
74 74 renderer = self.__detect_renderer(source, filename)
75 75 readme_data = renderer(source)
76 76 return readme_data
77 77
78 78 @classmethod
79 79 def plain(cls, source):
80 80 source = safe_unicode(source)
81 81
82 82 def urlify_text(text):
83 83 url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'
84 84 '|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
85 85
86 86 def url_func(match_obj):
87 87 url_full = match_obj.groups()[0]
88 88 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
89 89
90 90 return url_pat.sub(url_func, text)
91 91
92 92 source = urlify_text(source)
93 93 return '<br />' + source.replace("\n", '<br />')
94 94
95 95 @classmethod
96 96 def markdown(cls, source):
97 97 source = safe_unicode(source)
98 98 try:
99 99 import markdown as __markdown
100 100 return __markdown.markdown(source, ['codehilite'])
101 101 except ImportError:
102 102 log.warning('Install markdown to use this function')
103 103 return cls.plain(source)
104 104
105 105 @classmethod
106 106 def rst(cls, source):
107 107 source = safe_unicode(source)
108 108 try:
109 109 from docutils.core import publish_parts
110 110 from docutils.parsers.rst import directives
111 111 docutils_settings = dict([(alias, None) for alias in
112 112 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
113 113
114 114 docutils_settings.update({'input_encoding': 'unicode',
115 115 'report_level': 4})
116 116
117 117 for k, v in docutils_settings.iteritems():
118 118 directives.register_directive(k, v)
119 119
120 120 parts = publish_parts(source=source,
121 121 writer_name="html4css1",
122 122 settings_overrides=docutils_settings)
123 123
124 124 return parts['html_title'] + parts["fragment"]
125 125 except ImportError:
126 126 log.warning('Install docutils to use this function')
127 127 return cls.plain(source)
128 128
129 129 @classmethod
130 130 def rst_with_mentions(cls, source):
131 131 mention_pat = re.compile(r'(?:^@|\s@)(\w+)')
132 132
133 133 def wrapp(match_obj):
134 134 uname = match_obj.groups()[0]
135 135 return ' **@%(uname)s** ' % {'uname':uname}
136 136 mention_hl = mention_pat.sub(wrapp, source).strip()
137 137 return cls.rst(mention_hl)
@@ -1,62 +1,62 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.https_fixup
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 middleware to handle https correctly
7 7
8 8 :created_on: May 23, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 from rhodecode.lib import str2bool
26 from rhodecode.lib.utils2 import str2bool
27 27
28 28
29 29 class HttpsFixup(object):
30 30
31 31 def __init__(self, app, config):
32 32 self.application = app
33 33 self.config = config
34 34
35 35 def __call__(self, environ, start_response):
36 36 self.__fixup(environ)
37 37 return self.application(environ, start_response)
38 38
39 39 def __fixup(self, environ):
40 40 """
41 41 Function to fixup the environ as needed. In order to use this
42 42 middleware you should set this header inside your
43 43 proxy ie. nginx, apache etc.
44 44 """
45 45
46 46 if str2bool(self.config.get('force_https')):
47 47 proto = 'https'
48 48 else:
49 49 if 'HTTP_X_URL_SCHEME' in environ:
50 50 proto = environ.get('HTTP_X_URL_SCHEME')
51 51 elif 'HTTP_X_FORWARDED_SCHEME' in environ:
52 52 proto = environ.get('HTTP_X_FORWARDED_SCHEME')
53 53 elif 'HTTP_X_FORWARDED_PROTO' in environ:
54 54 proto = environ.get('HTTP_X_FORWARDED_PROTO')
55 55 else:
56 56 proto = 'http'
57 57 if proto == 'https':
58 58 environ['wsgi.url_scheme'] = proto
59 59 else:
60 60 environ['wsgi.url_scheme'] = 'http'
61 61
62 62 return None
@@ -1,251 +1,251 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplegit
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 7 It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import re
29 29 import logging
30 30 import traceback
31 31
32 32 from dulwich import server as dulserver
33 33
34 34
35 35 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
36 36
37 37 def handle(self):
38 38 write = lambda x: self.proto.write_sideband(1, x)
39 39
40 40 graph_walker = dulserver.ProtocolGraphWalker(self,
41 41 self.repo.object_store,
42 42 self.repo.get_peeled)
43 43 objects_iter = self.repo.fetch_objects(
44 44 graph_walker.determine_wants, graph_walker, self.progress,
45 45 get_tagged=self.get_tagged)
46 46
47 47 # Do they want any objects?
48 48 if objects_iter is None or len(objects_iter) == 0:
49 49 return
50 50
51 51 self.progress("counting objects: %d, done.\n" % len(objects_iter))
52 52 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
53 53 objects_iter, len(objects_iter))
54 54 messages = []
55 55 messages.append('thank you for using rhodecode')
56 56
57 57 for msg in messages:
58 58 self.progress(msg + "\n")
59 59 # we are done
60 60 self.proto.write("0000")
61 61
62 62 dulserver.DEFAULT_HANDLERS = {
63 63 'git-upload-pack': SimpleGitUploadPackHandler,
64 64 'git-receive-pack': dulserver.ReceivePackHandler,
65 65 }
66 66
67 67 from dulwich.repo import Repo
68 68 from dulwich.web import HTTPGitApplication
69 69
70 70 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
71 71
72 from rhodecode.lib import safe_str
72 from rhodecode.lib.utils2 import safe_str
73 73 from rhodecode.lib.base import BaseVCSController
74 74 from rhodecode.lib.auth import get_container_username
75 75 from rhodecode.lib.utils import is_valid_repo
76 76 from rhodecode.model.db import User
77 77
78 78 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
79 79
80 80 log = logging.getLogger(__name__)
81 81
82 82
83 83 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
84 84
85 85
86 86 def is_git(environ):
87 87 path_info = environ['PATH_INFO']
88 88 isgit_path = GIT_PROTO_PAT.match(path_info)
89 89 log.debug('pathinfo: %s detected as GIT %s' % (
90 90 path_info, isgit_path != None)
91 91 )
92 92 return isgit_path
93 93
94 94
95 95 class SimpleGit(BaseVCSController):
96 96
97 97 def _handle_request(self, environ, start_response):
98 98
99 99 if not is_git(environ):
100 100 return self.application(environ, start_response)
101 101
102 102 proxy_key = 'HTTP_X_REAL_IP'
103 103 def_key = 'REMOTE_ADDR'
104 104 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
105 105 username = None
106 106 # skip passing error to error controller
107 107 environ['pylons.status_code_redirect'] = True
108 108
109 109 #======================================================================
110 110 # EXTRACT REPOSITORY NAME FROM ENV
111 111 #======================================================================
112 112 try:
113 113 repo_name = self.__get_repository(environ)
114 114 log.debug('Extracted repo name is %s' % repo_name)
115 115 except:
116 116 return HTTPInternalServerError()(environ, start_response)
117 117
118 118 #======================================================================
119 119 # GET ACTION PULL or PUSH
120 120 #======================================================================
121 121 action = self.__get_action(environ)
122 122
123 123 #======================================================================
124 124 # CHECK ANONYMOUS PERMISSION
125 125 #======================================================================
126 126 if action in ['pull', 'push']:
127 127 anonymous_user = self.__get_user('default')
128 128 username = anonymous_user.username
129 129 anonymous_perm = self._check_permission(action, anonymous_user,
130 130 repo_name)
131 131
132 132 if anonymous_perm is not True or anonymous_user.active is False:
133 133 if anonymous_perm is not True:
134 134 log.debug('Not enough credentials to access this '
135 135 'repository as anonymous user')
136 136 if anonymous_user.active is False:
137 137 log.debug('Anonymous access is disabled, running '
138 138 'authentication')
139 139 #==============================================================
140 140 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
141 141 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
142 142 #==============================================================
143 143
144 144 # Attempting to retrieve username from the container
145 145 username = get_container_username(environ, self.config)
146 146
147 147 # If not authenticated by the container, running basic auth
148 148 if not username:
149 149 self.authenticate.realm = \
150 150 safe_str(self.config['rhodecode_realm'])
151 151 result = self.authenticate(environ)
152 152 if isinstance(result, str):
153 153 AUTH_TYPE.update(environ, 'basic')
154 154 REMOTE_USER.update(environ, result)
155 155 username = result
156 156 else:
157 157 return result.wsgi_application(environ, start_response)
158 158
159 159 #==============================================================
160 160 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
161 161 #==============================================================
162 162 if action in ['pull', 'push']:
163 163 try:
164 164 user = self.__get_user(username)
165 165 if user is None or not user.active:
166 166 return HTTPForbidden()(environ, start_response)
167 167 username = user.username
168 168 except:
169 169 log.error(traceback.format_exc())
170 170 return HTTPInternalServerError()(environ,
171 171 start_response)
172 172
173 173 #check permissions for this repository
174 174 perm = self._check_permission(action, user, repo_name)
175 175 if perm is not True:
176 176 return HTTPForbidden()(environ, start_response)
177 177
178 178 #===================================================================
179 179 # GIT REQUEST HANDLING
180 180 #===================================================================
181 181 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
182 182 log.debug('Repository path is %s' % repo_path)
183 183
184 184 # quick check if that dir exists...
185 185 if is_valid_repo(repo_name, self.basepath) is False:
186 186 return HTTPNotFound()(environ, start_response)
187 187
188 188 try:
189 189 #invalidate cache on push
190 190 if action == 'push':
191 191 self._invalidate_cache(repo_name)
192 192 log.info('%s action on GIT repo "%s"' % (action, repo_name))
193 193 app = self.__make_app(repo_name, repo_path)
194 194 return app(environ, start_response)
195 195 except Exception:
196 196 log.error(traceback.format_exc())
197 197 return HTTPInternalServerError()(environ, start_response)
198 198
199 199 def __make_app(self, repo_name, repo_path):
200 200 """
201 201 Make an wsgi application using dulserver
202 202
203 203 :param repo_name: name of the repository
204 204 :param repo_path: full path to the repository
205 205 """
206 206 _d = {'/' + repo_name: Repo(repo_path)}
207 207 backend = dulserver.DictBackend(_d)
208 208 gitserve = HTTPGitApplication(backend)
209 209
210 210 return gitserve
211 211
212 212 def __get_repository(self, environ):
213 213 """
214 214 Get's repository name out of PATH_INFO header
215 215
216 216 :param environ: environ where PATH_INFO is stored
217 217 """
218 218 try:
219 219 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
220 220 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
221 221 except:
222 222 log.error(traceback.format_exc())
223 223 raise
224 224
225 225 return repo_name
226 226
227 227 def __get_user(self, username):
228 228 return User.get_by_username(username)
229 229
230 230 def __get_action(self, environ):
231 231 """
232 232 Maps git request commands into a pull or push command.
233 233
234 234 :param environ:
235 235 """
236 236 service = environ['QUERY_STRING'].split('=')
237 237
238 238 if len(service) > 1:
239 239 service_cmd = service[1]
240 240 mapping = {
241 241 'git-receive-pack': 'push',
242 242 'git-upload-pack': 'pull',
243 243 }
244 244 op = mapping[service_cmd]
245 245 self._git_stored_op = op
246 246 return op
247 247 else:
248 248 # try to fallback to stored variable as we don't know if the last
249 249 # operation is pull/push
250 250 op = getattr(self, '_git_stored_op', 'pull')
251 251 return op
@@ -1,258 +1,258 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplehg
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleHG middleware for handling mercurial protocol request
7 7 (push/clone etc.). It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30 import urllib
31 31
32 32 from mercurial.error import RepoError
33 33 from mercurial.hgweb import hgweb_mod
34 34
35 35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36 36
37 from rhodecode.lib import safe_str
37 from rhodecode.lib.utils2 import safe_str
38 38 from rhodecode.lib.base import BaseVCSController
39 39 from rhodecode.lib.auth import get_container_username
40 40 from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections
41 41 from rhodecode.model.db import User
42 42
43 43 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 def is_mercurial(environ):
49 49 """
50 50 Returns True if request's target is mercurial server - header
51 51 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
52 52 """
53 53 http_accept = environ.get('HTTP_ACCEPT')
54 54 path_info = environ['PATH_INFO']
55 55 if http_accept and http_accept.startswith('application/mercurial'):
56 56 ishg_path = True
57 57 else:
58 58 ishg_path = False
59 59
60 60 log.debug('pathinfo: %s detected as HG %s' % (
61 61 path_info, ishg_path)
62 62 )
63 63 return ishg_path
64 64
65 65
66 66 class SimpleHg(BaseVCSController):
67 67
68 68 def _handle_request(self, environ, start_response):
69 69 if not is_mercurial(environ):
70 70 return self.application(environ, start_response)
71 71
72 72 proxy_key = 'HTTP_X_REAL_IP'
73 73 def_key = 'REMOTE_ADDR'
74 74 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
75 75
76 76 # skip passing error to error controller
77 77 environ['pylons.status_code_redirect'] = True
78 78
79 79 #======================================================================
80 80 # EXTRACT REPOSITORY NAME FROM ENV
81 81 #======================================================================
82 82 try:
83 83 repo_name = environ['REPO_NAME'] = self.__get_repository(environ)
84 84 log.debug('Extracted repo name is %s' % repo_name)
85 85 except:
86 86 return HTTPInternalServerError()(environ, start_response)
87 87
88 88 #======================================================================
89 89 # GET ACTION PULL or PUSH
90 90 #======================================================================
91 91 action = self.__get_action(environ)
92 92
93 93 #======================================================================
94 94 # CHECK ANONYMOUS PERMISSION
95 95 #======================================================================
96 96 if action in ['pull', 'push']:
97 97 anonymous_user = self.__get_user('default')
98 98 username = anonymous_user.username
99 99 anonymous_perm = self._check_permission(action, anonymous_user,
100 100 repo_name)
101 101
102 102 if anonymous_perm is not True or anonymous_user.active is False:
103 103 if anonymous_perm is not True:
104 104 log.debug('Not enough credentials to access this '
105 105 'repository as anonymous user')
106 106 if anonymous_user.active is False:
107 107 log.debug('Anonymous access is disabled, running '
108 108 'authentication')
109 109 #==============================================================
110 110 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
111 111 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
112 112 #==============================================================
113 113
114 114 # Attempting to retrieve username from the container
115 115 username = get_container_username(environ, self.config)
116 116
117 117 # If not authenticated by the container, running basic auth
118 118 if not username:
119 119 self.authenticate.realm = \
120 120 safe_str(self.config['rhodecode_realm'])
121 121 result = self.authenticate(environ)
122 122 if isinstance(result, str):
123 123 AUTH_TYPE.update(environ, 'basic')
124 124 REMOTE_USER.update(environ, result)
125 125 username = result
126 126 else:
127 127 return result.wsgi_application(environ, start_response)
128 128
129 129 #==============================================================
130 130 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
131 131 #==============================================================
132 132 if action in ['pull', 'push']:
133 133 try:
134 134 user = self.__get_user(username)
135 135 if user is None or not user.active:
136 136 return HTTPForbidden()(environ, start_response)
137 137 username = user.username
138 138 except:
139 139 log.error(traceback.format_exc())
140 140 return HTTPInternalServerError()(environ,
141 141 start_response)
142 142
143 143 #check permissions for this repository
144 144 perm = self._check_permission(action, user, repo_name)
145 145 if perm is not True:
146 146 return HTTPForbidden()(environ, start_response)
147 147
148 148 # extras are injected into mercurial UI object and later available
149 149 # in hg hooks executed by rhodecode
150 150 extras = {
151 151 'ip': ipaddr,
152 152 'username': username,
153 153 'action': action,
154 154 'repository': repo_name
155 155 }
156 156
157 157 #======================================================================
158 158 # MERCURIAL REQUEST HANDLING
159 159 #======================================================================
160 160 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
161 161 log.debug('Repository path is %s' % repo_path)
162 162
163 163 baseui = make_ui('db')
164 164 self.__inject_extras(repo_path, baseui, extras)
165 165
166 166 # quick check if that dir exists...
167 167 if is_valid_repo(repo_name, self.basepath) is False:
168 168 return HTTPNotFound()(environ, start_response)
169 169
170 170 try:
171 171 # invalidate cache on push
172 172 if action == 'push':
173 173 self._invalidate_cache(repo_name)
174 174 log.info('%s action on HG repo "%s"' % (action, repo_name))
175 175 app = self.__make_app(repo_path, baseui, extras)
176 176 return app(environ, start_response)
177 177 except RepoError, e:
178 178 if str(e).find('not found') != -1:
179 179 return HTTPNotFound()(environ, start_response)
180 180 except Exception:
181 181 log.error(traceback.format_exc())
182 182 return HTTPInternalServerError()(environ, start_response)
183 183
184 184 def __make_app(self, repo_name, baseui, extras):
185 185 """
186 186 Make an wsgi application using hgweb, and inject generated baseui
187 187 instance, additionally inject some extras into ui object
188 188 """
189 189 return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui)
190 190
191 191 def __get_repository(self, environ):
192 192 """
193 193 Get's repository name out of PATH_INFO header
194 194
195 195 :param environ: environ where PATH_INFO is stored
196 196 """
197 197 try:
198 198 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
199 199 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
200 200 if repo_name.endswith('/'):
201 201 repo_name = repo_name.rstrip('/')
202 202 except:
203 203 log.error(traceback.format_exc())
204 204 raise
205 205
206 206 return repo_name
207 207
208 208 def __get_user(self, username):
209 209 return User.get_by_username(username)
210 210
211 211 def __get_action(self, environ):
212 212 """
213 213 Maps mercurial request commands into a clone,pull or push command.
214 214 This should always return a valid command string
215 215
216 216 :param environ:
217 217 """
218 218 mapping = {'changegroup': 'pull',
219 219 'changegroupsubset': 'pull',
220 220 'stream_out': 'pull',
221 221 'listkeys': 'pull',
222 222 'unbundle': 'push',
223 223 'pushkey': 'push', }
224 224 for qry in environ['QUERY_STRING'].split('&'):
225 225 if qry.startswith('cmd'):
226 226 cmd = qry.split('=')[-1]
227 227 if cmd in mapping:
228 228 return mapping[cmd]
229 229 else:
230 230 return 'pull'
231 231
232 232 def __inject_extras(self, repo_path, baseui, extras={}):
233 233 """
234 234 Injects some extra params into baseui instance
235 235
236 236 also overwrites global settings with those takes from local hgrc file
237 237
238 238 :param baseui: baseui instance
239 239 :param extras: dict with extra params to put into baseui
240 240 """
241 241
242 242 hgrc = os.path.join(repo_path, '.hg', 'hgrc')
243 243
244 244 # make our hgweb quiet so it doesn't print output
245 245 baseui.setconfig('ui', 'quiet', 'true')
246 246
247 247 #inject some additional parameters that will be available in ui
248 248 #for hooks
249 249 for k, v in extras.items():
250 250 baseui.setconfig('rhodecode_extras', k, v)
251 251
252 252 repoui = make_ui('file', hgrc, False)
253 253
254 254 if repoui:
255 255 #overwrite our ui instance with the section from hgrc file
256 256 for section in ui_sections:
257 257 for k, v in repoui.configitems(section):
258 258 baseui.setconfig(section, k, v)
@@ -1,634 +1,662 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Utilities library for RhodeCode
7 7
8 8 :created_on: Apr 18, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import datetime
30 30 import traceback
31 31 import paste
32 32 import beaker
33 33 import tarfile
34 34 import shutil
35 35 from os.path import abspath
36 36 from os.path import dirname as dn, join as jn
37 37
38 38 from paste.script.command import Command, BadCommand
39 39
40 40 from mercurial import ui, config
41 41
42 42 from webhelpers.text import collapse, remove_formatting, strip_tags
43 43
44 44 from rhodecode.lib.vcs import get_backend
45 45 from rhodecode.lib.vcs.backends.base import BaseChangeset
46 46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 47 from rhodecode.lib.vcs.utils.helpers import get_scm
48 48 from rhodecode.lib.vcs.exceptions import VCSError
49 49
50 50 from rhodecode.lib.caching_query import FromCache
51 51
52 52 from rhodecode.model import meta
53 53 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
54 54 UserLog, RepoGroup, RhodeCodeSetting, UserRepoGroupToPerm
55 55 from rhodecode.model.meta import Session
56 56 from rhodecode.model.repos_group import ReposGroupModel
57 from rhodecode.lib import safe_str, safe_unicode
57 from rhodecode.lib.utils2 import safe_str, safe_unicode
58 from rhodecode.lib.vcs.utils.fakemod import create_module
58 59
59 60 log = logging.getLogger(__name__)
60 61
61 62 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
62 63
63 64
64 65 def recursive_replace(str_, replace=' '):
65 """Recursive replace of given sign to just one instance
66 """
67 Recursive replace of given sign to just one instance
66 68
67 69 :param str_: given string
68 70 :param replace: char to find and replace multiple instances
69 71
70 72 Examples::
71 73 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
72 74 'Mighty-Mighty-Bo-sstones'
73 75 """
74 76
75 77 if str_.find(replace * 2) == -1:
76 78 return str_
77 79 else:
78 80 str_ = str_.replace(replace * 2, replace)
79 81 return recursive_replace(str_, replace)
80 82
81 83
82 84 def repo_name_slug(value):
83 """Return slug of name of repository
85 """
86 Return slug of name of repository
84 87 This function is called on each creation/modification
85 88 of repository to prevent bad names in repo
86 89 """
87 90
88 91 slug = remove_formatting(value)
89 92 slug = strip_tags(slug)
90 93
91 94 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """:
92 95 slug = slug.replace(c, '-')
93 96 slug = recursive_replace(slug, '-')
94 97 slug = collapse(slug, '-')
95 98 return slug
96 99
97 100
98 101 def get_repo_slug(request):
99 102 _repo = request.environ['pylons.routes_dict'].get('repo_name')
100 103 if _repo:
101 104 _repo = _repo.rstrip('/')
102 105 return _repo
103 106
104 107
105 108 def get_repos_group_slug(request):
106 109 _group = request.environ['pylons.routes_dict'].get('group_name')
107 110 if _group:
108 111 _group = _group.rstrip('/')
109 112 return _group
110 113
111 114
112 115 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
113 116 """
114 117 Action logger for various actions made by users
115 118
116 119 :param user: user that made this action, can be a unique username string or
117 120 object containing user_id attribute
118 121 :param action: action to log, should be on of predefined unique actions for
119 122 easy translations
120 123 :param repo: string name of repository or object containing repo_id,
121 124 that action was made on
122 125 :param ipaddr: optional ip address from what the action was made
123 126 :param sa: optional sqlalchemy session
124 127
125 128 """
126 129
127 130 if not sa:
128 131 sa = meta.Session
129 132
130 133 try:
131 134 if hasattr(user, 'user_id'):
132 135 user_obj = user
133 136 elif isinstance(user, basestring):
134 137 user_obj = User.get_by_username(user)
135 138 else:
136 139 raise Exception('You have to provide user object or username')
137 140
138 141 if hasattr(repo, 'repo_id'):
139 142 repo_obj = Repository.get(repo.repo_id)
140 143 repo_name = repo_obj.repo_name
141 144 elif isinstance(repo, basestring):
142 145 repo_name = repo.lstrip('/')
143 146 repo_obj = Repository.get_by_repo_name(repo_name)
144 147 else:
145 148 raise Exception('You have to provide repository to action logger')
146 149
147 150 user_log = UserLog()
148 151 user_log.user_id = user_obj.user_id
149 152 user_log.action = action
150 153
151 154 user_log.repository_id = repo_obj.repo_id
152 155 user_log.repository_name = repo_name
153 156
154 157 user_log.action_date = datetime.datetime.now()
155 158 user_log.user_ip = ipaddr
156 159 sa.add(user_log)
157 160
158 161 log.info(
159 162 'Adding user %s, action %s on %s' % (user_obj, action,
160 163 safe_unicode(repo))
161 164 )
162 165 if commit:
163 166 sa.commit()
164 167 except:
165 168 log.error(traceback.format_exc())
166 169 raise
167 170
168 171
169 172 def get_repos(path, recursive=False):
170 173 """
171 174 Scans given path for repos and return (name,(type,path)) tuple
172 175
173 176 :param path: path to scan for repositories
174 177 :param recursive: recursive search and return names with subdirs in front
175 178 """
176 179
177 180 # remove ending slash for better results
178 181 path = path.rstrip(os.sep)
179 182
180 183 def _get_repos(p):
181 184 if not os.access(p, os.W_OK):
182 185 return
183 186 for dirpath in os.listdir(p):
184 187 if os.path.isfile(os.path.join(p, dirpath)):
185 188 continue
186 189 cur_path = os.path.join(p, dirpath)
187 190 try:
188 191 scm_info = get_scm(cur_path)
189 192 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
190 193 except VCSError:
191 194 if not recursive:
192 195 continue
193 196 #check if this dir containts other repos for recursive scan
194 197 rec_path = os.path.join(p, dirpath)
195 198 if os.path.isdir(rec_path):
196 199 for inner_scm in _get_repos(rec_path):
197 200 yield inner_scm
198 201
199 202 return _get_repos(path)
200 203
201 204
202 205 def is_valid_repo(repo_name, base_path):
203 206 """
204 207 Returns True if given path is a valid repository False otherwise
205 208
206 209 :param repo_name:
207 210 :param base_path:
208 211
209 212 :return True: if given path is a valid repository
210 213 """
211 214 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
212 215
213 216 try:
214 217 get_scm(full_path)
215 218 return True
216 219 except VCSError:
217 220 return False
218 221
219 222
220 223 def is_valid_repos_group(repos_group_name, base_path):
221 224 """
222 225 Returns True if given path is a repos group False otherwise
223 226
224 227 :param repo_name:
225 228 :param base_path:
226 229 """
227 230 full_path = os.path.join(safe_str(base_path), safe_str(repos_group_name))
228 231
229 232 # check if it's not a repo
230 233 if is_valid_repo(repos_group_name, base_path):
231 234 return False
232 235
233 236 # check if it's a valid path
234 237 if os.path.isdir(full_path):
235 238 return True
236 239
237 240 return False
238 241
239 242
240 243 def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
241 244 while True:
242 245 ok = raw_input(prompt)
243 246 if ok in ('y', 'ye', 'yes'):
244 247 return True
245 248 if ok in ('n', 'no', 'nop', 'nope'):
246 249 return False
247 250 retries = retries - 1
248 251 if retries < 0:
249 252 raise IOError
250 253 print complaint
251 254
252 255 #propagated from mercurial documentation
253 256 ui_sections = ['alias', 'auth',
254 257 'decode/encode', 'defaults',
255 258 'diff', 'email',
256 259 'extensions', 'format',
257 260 'merge-patterns', 'merge-tools',
258 261 'hooks', 'http_proxy',
259 262 'smtp', 'patch',
260 263 'paths', 'profiling',
261 264 'server', 'trusted',
262 265 'ui', 'web', ]
263 266
264 267
265 268 def make_ui(read_from='file', path=None, checkpaths=True):
266 """A function that will read python rc files or database
269 """
270 A function that will read python rc files or database
267 271 and make an mercurial ui object from read options
268 272
269 273 :param path: path to mercurial config file
270 274 :param checkpaths: check the path
271 275 :param read_from: read from 'file' or 'db'
272 276 """
273 277
274 278 baseui = ui.ui()
275 279
276 280 # clean the baseui object
277 281 baseui._ocfg = config.config()
278 282 baseui._ucfg = config.config()
279 283 baseui._tcfg = config.config()
280 284
281 285 if read_from == 'file':
282 286 if not os.path.isfile(path):
283 287 log.debug('hgrc file is not present at %s skipping...' % path)
284 288 return False
285 289 log.debug('reading hgrc from %s' % path)
286 290 cfg = config.config()
287 291 cfg.read(path)
288 292 for section in ui_sections:
289 293 for k, v in cfg.items(section):
290 294 log.debug('settings ui from file[%s]%s:%s' % (section, k, v))
291 295 baseui.setconfig(section, k, v)
292 296
293 297 elif read_from == 'db':
294 298 sa = meta.Session
295 299 ret = sa.query(RhodeCodeUi)\
296 300 .options(FromCache("sql_cache_short", "get_hg_ui_settings"))\
297 301 .all()
298 302
299 303 hg_ui = ret
300 304 for ui_ in hg_ui:
301 305 if ui_.ui_active:
302 306 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
303 307 ui_.ui_key, ui_.ui_value)
304 308 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
305 309
306 310 meta.Session.remove()
307 311 return baseui
308 312
309 313
310 314 def set_rhodecode_config(config):
311 315 """
312 316 Updates pylons config with new settings from database
313 317
314 318 :param config:
315 319 """
316 320 hgsettings = RhodeCodeSetting.get_app_settings()
317 321
318 322 for k, v in hgsettings.items():
319 323 config[k] = v
320 324
321 325
322 326 def invalidate_cache(cache_key, *args):
323 327 """
324 328 Puts cache invalidation task into db for
325 329 further global cache invalidation
326 330 """
327 331
328 332 from rhodecode.model.scm import ScmModel
329 333
330 334 if cache_key.startswith('get_repo_cached_'):
331 335 name = cache_key.split('get_repo_cached_')[-1]
332 336 ScmModel().mark_for_invalidation(name)
333 337
334 338
335 339 class EmptyChangeset(BaseChangeset):
336 340 """
337 341 An dummy empty changeset. It's possible to pass hash when creating
338 342 an EmptyChangeset
339 343 """
340 344
341 345 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
342 346 alias=None):
343 347 self._empty_cs = cs
344 348 self.revision = -1
345 349 self.message = ''
346 350 self.author = ''
347 351 self.date = ''
348 352 self.repository = repo
349 353 self.requested_revision = requested_revision
350 354 self.alias = alias
351 355
352 356 @LazyProperty
353 357 def raw_id(self):
354 358 """
355 359 Returns raw string identifying this changeset, useful for web
356 360 representation.
357 361 """
358 362
359 363 return self._empty_cs
360 364
361 365 @LazyProperty
362 366 def branch(self):
363 367 return get_backend(self.alias).DEFAULT_BRANCH_NAME
364 368
365 369 @LazyProperty
366 370 def short_id(self):
367 371 return self.raw_id[:12]
368 372
369 373 def get_file_changeset(self, path):
370 374 return self
371 375
372 376 def get_file_content(self, path):
373 377 return u''
374 378
375 379 def get_file_size(self, path):
376 380 return 0
377 381
378 382
379 383 def map_groups(groups):
380 384 """
381 385 Checks for groups existence, and creates groups structures.
382 386 It returns last group in structure
383 387
384 388 :param groups: list of groups structure
385 389 """
386 390 sa = meta.Session
387 391
388 392 parent = None
389 393 group = None
390 394
391 395 # last element is repo in nested groups structure
392 396 groups = groups[:-1]
393 397 rgm = ReposGroupModel(sa)
394 398 for lvl, group_name in enumerate(groups):
395 399 group_name = '/'.join(groups[:lvl] + [group_name])
396 400 group = RepoGroup.get_by_group_name(group_name)
397 401 desc = '%s group' % group_name
398 402
399 403 # # WTF that doesn't work !?
400 404 # if group is None:
401 405 # group = rgm.create(group_name, desc, parent, just_db=True)
402 406 # sa.commit()
403 407
404 408 # skip folders that are now removed repos
405 409 if REMOVED_REPO_PAT.match(group_name):
406 410 break
407 411
408 412 if group is None:
409 413 log.debug('creating group level: %s group_name: %s' % (lvl, group_name))
410 414 group = RepoGroup(group_name, parent)
411 415 group.group_description = desc
412 416 sa.add(group)
413 417 rgm._create_default_perms(group)
414 418 sa.commit()
415 419 parent = group
416 420 return group
417 421
418 422
419 423 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
420 424 """
421 425 maps all repos given in initial_repo_list, non existing repositories
422 426 are created, if remove_obsolete is True it also check for db entries
423 427 that are not in initial_repo_list and removes them.
424 428
425 429 :param initial_repo_list: list of repositories found by scanning methods
426 430 :param remove_obsolete: check for obsolete entries in database
427 431 """
428 432 from rhodecode.model.repo import RepoModel
429 433 sa = meta.Session
430 434 rm = RepoModel()
431 435 user = sa.query(User).filter(User.admin == True).first()
432 436 if user is None:
433 437 raise Exception('Missing administrative account !')
434 438 added = []
435 439
436 440 for name, repo in initial_repo_list.items():
437 441 group = map_groups(name.split(Repository.url_sep()))
438 442 if not rm.get_by_repo_name(name, cache=False):
439 443 log.info('repository %s not found creating default' % name)
440 444 added.append(name)
441 445 form_data = {
442 446 'repo_name': name,
443 447 'repo_name_full': name,
444 448 'repo_type': repo.alias,
445 449 'description': repo.description \
446 450 if repo.description != 'unknown' else '%s repository' % name,
447 451 'private': False,
448 452 'group_id': getattr(group, 'group_id', None)
449 453 }
450 454 rm.create(form_data, user, just_db=True)
451 455 sa.commit()
452 456 removed = []
453 457 if remove_obsolete:
454 458 #remove from database those repositories that are not in the filesystem
455 459 for repo in sa.query(Repository).all():
456 460 if repo.repo_name not in initial_repo_list.keys():
457 461 removed.append(repo.repo_name)
458 462 sa.delete(repo)
459 463 sa.commit()
460 464
461 465 return added, removed
462 466
463 467
464 468 # set cache regions for beaker so celery can utilise it
465 469 def add_cache(settings):
466 470 cache_settings = {'regions': None}
467 471 for key in settings.keys():
468 472 for prefix in ['beaker.cache.', 'cache.']:
469 473 if key.startswith(prefix):
470 474 name = key.split(prefix)[1].strip()
471 475 cache_settings[name] = settings[key].strip()
472 476 if cache_settings['regions']:
473 477 for region in cache_settings['regions'].split(','):
474 478 region = region.strip()
475 479 region_settings = {}
476 480 for key, value in cache_settings.items():
477 481 if key.startswith(region):
478 482 region_settings[key.split('.')[1]] = value
479 483 region_settings['expire'] = int(region_settings.get('expire',
480 484 60))
481 485 region_settings.setdefault('lock_dir',
482 486 cache_settings.get('lock_dir'))
483 487 region_settings.setdefault('data_dir',
484 488 cache_settings.get('data_dir'))
485 489
486 490 if 'type' not in region_settings:
487 491 region_settings['type'] = cache_settings.get('type',
488 492 'memory')
489 493 beaker.cache.cache_regions[region] = region_settings
490 494
491 495
496 def load_rcextensions(root_path):
497 import rhodecode
498 from rhodecode.config import conf
499
500 path = os.path.join(root_path, 'rcextensions', '__init__.py')
501 if os.path.isfile(path):
502 rcext = create_module('rc', path)
503 EXT = rhodecode.EXTENSIONS = rcext
504 log.debug('Found rcextensions now loading %s...' % rcext)
505
506 # Additional mappings that are not present in the pygments lexers
507 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
508
509 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
510
511 if getattr(EXT, 'INDEX_EXTENSIONS', []) != []:
512 log.debug('settings custom INDEX_EXTENSIONS')
513 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
514
515 #ADDITIONAL MAPPINGS
516 log.debug('adding extra into INDEX_EXTENSIONS')
517 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
518
519
492 520 #==============================================================================
493 521 # TEST FUNCTIONS AND CREATORS
494 522 #==============================================================================
495 523 def create_test_index(repo_location, config, full_index):
496 524 """
497 525 Makes default test index
498 526
499 527 :param config: test config
500 528 :param full_index:
501 529 """
502 530
503 531 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
504 532 from rhodecode.lib.pidlock import DaemonLock, LockHeld
505 533
506 534 repo_location = repo_location
507 535
508 536 index_location = os.path.join(config['app_conf']['index_dir'])
509 537 if not os.path.exists(index_location):
510 538 os.makedirs(index_location)
511 539
512 540 try:
513 541 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
514 542 WhooshIndexingDaemon(index_location=index_location,
515 543 repo_location=repo_location)\
516 544 .run(full_index=full_index)
517 545 l.release()
518 546 except LockHeld:
519 547 pass
520 548
521 549
522 550 def create_test_env(repos_test_path, config):
523 551 """
524 552 Makes a fresh database and
525 553 install test repository into tmp dir
526 554 """
527 555 from rhodecode.lib.db_manage import DbManage
528 556 from rhodecode.tests import HG_REPO, TESTS_TMP_PATH
529 557
530 558 # PART ONE create db
531 559 dbconf = config['sqlalchemy.db1.url']
532 560 log.debug('making test db %s' % dbconf)
533 561
534 562 # create test dir if it doesn't exist
535 563 if not os.path.isdir(repos_test_path):
536 564 log.debug('Creating testdir %s' % repos_test_path)
537 565 os.makedirs(repos_test_path)
538 566
539 567 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
540 568 tests=True)
541 569 dbmanage.create_tables(override=True)
542 570 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
543 571 dbmanage.create_default_user()
544 572 dbmanage.admin_prompt()
545 573 dbmanage.create_permissions()
546 574 dbmanage.populate_default_permissions()
547 575 Session.commit()
548 576 # PART TWO make test repo
549 577 log.debug('making test vcs repositories')
550 578
551 579 idx_path = config['app_conf']['index_dir']
552 580 data_path = config['app_conf']['cache_dir']
553 581
554 582 #clean index and data
555 583 if idx_path and os.path.exists(idx_path):
556 584 log.debug('remove %s' % idx_path)
557 585 shutil.rmtree(idx_path)
558 586
559 587 if data_path and os.path.exists(data_path):
560 588 log.debug('remove %s' % data_path)
561 589 shutil.rmtree(data_path)
562 590
563 591 #CREATE DEFAULT HG REPOSITORY
564 592 cur_dir = dn(dn(abspath(__file__)))
565 593 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
566 594 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
567 595 tar.close()
568 596
569 597
570 598 #==============================================================================
571 599 # PASTER COMMANDS
572 600 #==============================================================================
573 601 class BasePasterCommand(Command):
574 602 """
575 603 Abstract Base Class for paster commands.
576 604
577 605 The celery commands are somewhat aggressive about loading
578 606 celery.conf, and since our module sets the `CELERY_LOADER`
579 607 environment variable to our loader, we have to bootstrap a bit and
580 608 make sure we've had a chance to load the pylons config off of the
581 609 command line, otherwise everything fails.
582 610 """
583 611 min_args = 1
584 612 min_args_error = "Please provide a paster config file as an argument."
585 613 takes_config_file = 1
586 614 requires_config_file = True
587 615
588 616 def notify_msg(self, msg, log=False):
589 617 """Make a notification to user, additionally if logger is passed
590 618 it logs this action using given logger
591 619
592 620 :param msg: message that will be printed to user
593 621 :param log: logging instance, to use to additionally log this message
594 622
595 623 """
596 624 if log and isinstance(log, logging):
597 625 log(msg)
598 626
599 627 def run(self, args):
600 628 """
601 629 Overrides Command.run
602 630
603 631 Checks for a config file argument and loads it.
604 632 """
605 633 if len(args) < self.min_args:
606 634 raise BadCommand(
607 635 self.min_args_error % {'min_args': self.min_args,
608 636 'actual_args': len(args)})
609 637
610 638 # Decrement because we're going to lob off the first argument.
611 639 # @@ This is hacky
612 640 self.min_args -= 1
613 641 self.bootstrap_config(args[0])
614 642 self.update_parser()
615 643 return super(BasePasterCommand, self).run(args[1:])
616 644
617 645 def update_parser(self):
618 646 """
619 647 Abstract method. Allows for the class's parser to be updated
620 648 before the superclass's `run` method is called. Necessary to
621 649 allow options/arguments to be passed through to the underlying
622 650 celery command.
623 651 """
624 652 raise NotImplementedError("Abstract Method.")
625 653
626 654 def bootstrap_config(self, conf):
627 655 """
628 656 Loads the pylons configuration.
629 657 """
630 658 from pylons import config as pylonsconfig
631 659
632 660 self.path_to_ini_file = os.path.realpath(conf)
633 661 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
634 662 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
@@ -1,148 +1,148 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.comment
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 comments model for RhodeCode
7 7
8 8 :created_on: Nov 11, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons.i18n.translation import _
30 30 from sqlalchemy.util.compat import defaultdict
31 31
32 from rhodecode.lib import extract_mentioned_users
32 from rhodecode.lib.utils2 import extract_mentioned_users
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.model import BaseModel
35 35 from rhodecode.model.db import ChangesetComment, User, Repository, Notification
36 36 from rhodecode.model.notification import NotificationModel
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class ChangesetCommentsModel(BaseModel):
42 42
43 43 def __get_changeset_comment(self, changeset_comment):
44 44 return self._get_instance(ChangesetComment, changeset_comment)
45 45
46 46 def _extract_mentions(self, s):
47 47 user_objects = []
48 48 for username in extract_mentioned_users(s):
49 49 user_obj = User.get_by_username(username, case_insensitive=True)
50 50 if user_obj:
51 51 user_objects.append(user_obj)
52 52 return user_objects
53 53
54 54 def create(self, text, repo_id, user_id, revision, f_path=None,
55 55 line_no=None):
56 56 """
57 57 Creates new comment for changeset
58 58
59 59 :param text:
60 60 :param repo_id:
61 61 :param user_id:
62 62 :param revision:
63 63 :param f_path:
64 64 :param line_no:
65 65 """
66 66 if text:
67 67 repo = Repository.get(repo_id)
68 68 cs = repo.scm_instance.get_changeset(revision)
69 69 desc = cs.message
70 70 author_email = cs.author_email
71 71 comment = ChangesetComment()
72 72 comment.repo = repo
73 73 comment.user_id = user_id
74 74 comment.revision = revision
75 75 comment.text = text
76 76 comment.f_path = f_path
77 77 comment.line_no = line_no
78 78
79 79 self.sa.add(comment)
80 80 self.sa.flush()
81 81
82 82 # make notification
83 83 line = ''
84 84 if line_no:
85 85 line = _('on line %s') % line_no
86 86 subj = h.link_to('Re commit: %(commit_desc)s %(line)s' % \
87 87 {'commit_desc': desc, 'line': line},
88 88 h.url('changeset_home', repo_name=repo.repo_name,
89 89 revision=revision,
90 90 anchor='comment-%s' % comment.comment_id,
91 91 qualified=True,
92 92 )
93 93 )
94 94 body = text
95 95
96 96 # get the current participants of this changeset
97 97 recipients = ChangesetComment.get_users(revision=revision)
98 98
99 99 # add changeset author if it's in rhodecode system
100 100 recipients += [User.get_by_email(author_email)]
101 101
102 102 NotificationModel().create(
103 103 created_by=user_id, subject=subj, body=body,
104 104 recipients=recipients, type_=Notification.TYPE_CHANGESET_COMMENT
105 105 )
106 106
107 107 mention_recipients = set(self._extract_mentions(body))\
108 108 .difference(recipients)
109 109 if mention_recipients:
110 110 subj = _('[Mention]') + ' ' + subj
111 111 NotificationModel().create(
112 112 created_by=user_id, subject=subj, body=body,
113 113 recipients=mention_recipients,
114 114 type_=Notification.TYPE_CHANGESET_COMMENT
115 115 )
116 116
117 117 return comment
118 118
119 119 def delete(self, comment):
120 120 """
121 121 Deletes given comment
122 122
123 123 :param comment_id:
124 124 """
125 125 comment = self.__get_changeset_comment(comment)
126 126 self.sa.delete(comment)
127 127
128 128 return comment
129 129
130 130 def get_comments(self, repo_id, revision):
131 131 return ChangesetComment.query()\
132 132 .filter(ChangesetComment.repo_id == repo_id)\
133 133 .filter(ChangesetComment.revision == revision)\
134 134 .filter(ChangesetComment.line_no == None)\
135 135 .filter(ChangesetComment.f_path == None).all()
136 136
137 137 def get_inline_comments(self, repo_id, revision):
138 138 comments = self.sa.query(ChangesetComment)\
139 139 .filter(ChangesetComment.repo_id == repo_id)\
140 140 .filter(ChangesetComment.revision == revision)\
141 141 .filter(ChangesetComment.line_no != None)\
142 142 .filter(ChangesetComment.f_path != None).all()
143 143
144 144 paths = defaultdict(lambda: defaultdict(list))
145 145
146 146 for co in comments:
147 147 paths[co.f_path][co.line_no].append(co)
148 148 return paths.items()
@@ -1,1218 +1,1219 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 from collections import defaultdict
31 31
32 32 from sqlalchemy import *
33 33 from sqlalchemy.ext.hybrid import hybrid_property
34 34 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
35 35 from beaker.cache import cache_region, region_invalidate
36 36
37 37 from rhodecode.lib.vcs import get_backend
38 38 from rhodecode.lib.vcs.utils.helpers import get_scm
39 39 from rhodecode.lib.vcs.exceptions import VCSError
40 40 from rhodecode.lib.vcs.utils.lazy import LazyProperty
41 41
42 from rhodecode.lib import str2bool, safe_str, get_changeset_safe, safe_unicode
42 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
43 safe_unicode
43 44 from rhodecode.lib.compat import json
44 45 from rhodecode.lib.caching_query import FromCache
45 46
46 47 from rhodecode.model.meta import Base, Session
47 48 import hashlib
48 49
49 50
50 51 log = logging.getLogger(__name__)
51 52
52 53 #==============================================================================
53 54 # BASE CLASSES
54 55 #==============================================================================
55 56
56 57 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
57 58
58 59
59 60 class ModelSerializer(json.JSONEncoder):
60 61 """
61 62 Simple Serializer for JSON,
62 63
63 64 usage::
64 65
65 66 to make object customized for serialization implement a __json__
66 67 method that will return a dict for serialization into json
67 68
68 69 example::
69 70
70 71 class Task(object):
71 72
72 73 def __init__(self, name, value):
73 74 self.name = name
74 75 self.value = value
75 76
76 77 def __json__(self):
77 78 return dict(name=self.name,
78 79 value=self.value)
79 80
80 81 """
81 82
82 83 def default(self, obj):
83 84
84 85 if hasattr(obj, '__json__'):
85 86 return obj.__json__()
86 87 else:
87 88 return json.JSONEncoder.default(self, obj)
88 89
89 90
90 91 class BaseModel(object):
91 92 """
92 93 Base Model for all classess
93 94 """
94 95
95 96 @classmethod
96 97 def _get_keys(cls):
97 98 """return column names for this model """
98 99 return class_mapper(cls).c.keys()
99 100
100 101 def get_dict(self):
101 102 """
102 103 return dict with keys and values corresponding
103 104 to this model data """
104 105
105 106 d = {}
106 107 for k in self._get_keys():
107 108 d[k] = getattr(self, k)
108 109
109 110 # also use __json__() if present to get additional fields
110 111 for k, val in getattr(self, '__json__', lambda: {})().iteritems():
111 112 d[k] = val
112 113 return d
113 114
114 115 def get_appstruct(self):
115 116 """return list with keys and values tupples corresponding
116 117 to this model data """
117 118
118 119 l = []
119 120 for k in self._get_keys():
120 121 l.append((k, getattr(self, k),))
121 122 return l
122 123
123 124 def populate_obj(self, populate_dict):
124 125 """populate model with data from given populate_dict"""
125 126
126 127 for k in self._get_keys():
127 128 if k in populate_dict:
128 129 setattr(self, k, populate_dict[k])
129 130
130 131 @classmethod
131 132 def query(cls):
132 133 return Session.query(cls)
133 134
134 135 @classmethod
135 136 def get(cls, id_):
136 137 if id_:
137 138 return cls.query().get(id_)
138 139
139 140 @classmethod
140 141 def getAll(cls):
141 142 return cls.query().all()
142 143
143 144 @classmethod
144 145 def delete(cls, id_):
145 146 obj = cls.query().get(id_)
146 147 Session.delete(obj)
147 148
148 149
149 150 class RhodeCodeSetting(Base, BaseModel):
150 151 __tablename__ = 'rhodecode_settings'
151 152 __table_args__ = (
152 153 UniqueConstraint('app_settings_name'),
153 154 {'extend_existing': True}
154 155 )
155 156 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
156 157 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
157 158 _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
158 159
159 160 def __init__(self, k='', v=''):
160 161 self.app_settings_name = k
161 162 self.app_settings_value = v
162 163
163 164 @validates('_app_settings_value')
164 165 def validate_settings_value(self, key, val):
165 166 assert type(val) == unicode
166 167 return val
167 168
168 169 @hybrid_property
169 170 def app_settings_value(self):
170 171 v = self._app_settings_value
171 172 if self.app_settings_name == 'ldap_active':
172 173 v = str2bool(v)
173 174 return v
174 175
175 176 @app_settings_value.setter
176 177 def app_settings_value(self, val):
177 178 """
178 179 Setter that will always make sure we use unicode in app_settings_value
179 180
180 181 :param val:
181 182 """
182 183 self._app_settings_value = safe_unicode(val)
183 184
184 185 def __repr__(self):
185 186 return "<%s('%s:%s')>" % (
186 187 self.__class__.__name__,
187 188 self.app_settings_name, self.app_settings_value
188 189 )
189 190
190 191 @classmethod
191 192 def get_by_name(cls, ldap_key):
192 193 return cls.query()\
193 194 .filter(cls.app_settings_name == ldap_key).scalar()
194 195
195 196 @classmethod
196 197 def get_app_settings(cls, cache=False):
197 198
198 199 ret = cls.query()
199 200
200 201 if cache:
201 202 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
202 203
203 204 if not ret:
204 205 raise Exception('Could not get application settings !')
205 206 settings = {}
206 207 for each in ret:
207 208 settings['rhodecode_' + each.app_settings_name] = \
208 209 each.app_settings_value
209 210
210 211 return settings
211 212
212 213 @classmethod
213 214 def get_ldap_settings(cls, cache=False):
214 215 ret = cls.query()\
215 216 .filter(cls.app_settings_name.startswith('ldap_')).all()
216 217 fd = {}
217 218 for row in ret:
218 219 fd.update({row.app_settings_name:row.app_settings_value})
219 220
220 221 return fd
221 222
222 223
223 224 class RhodeCodeUi(Base, BaseModel):
224 225 __tablename__ = 'rhodecode_ui'
225 226 __table_args__ = (
226 227 UniqueConstraint('ui_key'),
227 228 {'extend_existing': True}
228 229 )
229 230
230 231 HOOK_UPDATE = 'changegroup.update'
231 232 HOOK_REPO_SIZE = 'changegroup.repo_size'
232 233 HOOK_PUSH = 'pretxnchangegroup.push_logger'
233 234 HOOK_PULL = 'preoutgoing.pull_logger'
234 235
235 236 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
236 237 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
237 238 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
238 239 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
239 240 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
240 241
241 242 @classmethod
242 243 def get_by_key(cls, key):
243 244 return cls.query().filter(cls.ui_key == key)
244 245
245 246 @classmethod
246 247 def get_builtin_hooks(cls):
247 248 q = cls.query()
248 249 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
249 250 cls.HOOK_REPO_SIZE,
250 251 cls.HOOK_PUSH, cls.HOOK_PULL]))
251 252 return q.all()
252 253
253 254 @classmethod
254 255 def get_custom_hooks(cls):
255 256 q = cls.query()
256 257 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
257 258 cls.HOOK_REPO_SIZE,
258 259 cls.HOOK_PUSH, cls.HOOK_PULL]))
259 260 q = q.filter(cls.ui_section == 'hooks')
260 261 return q.all()
261 262
262 263 @classmethod
263 264 def create_or_update_hook(cls, key, val):
264 265 new_ui = cls.get_by_key(key).scalar() or cls()
265 266 new_ui.ui_section = 'hooks'
266 267 new_ui.ui_active = True
267 268 new_ui.ui_key = key
268 269 new_ui.ui_value = val
269 270
270 271 Session.add(new_ui)
271 272
272 273
273 274 class User(Base, BaseModel):
274 275 __tablename__ = 'users'
275 276 __table_args__ = (
276 277 UniqueConstraint('username'), UniqueConstraint('email'),
277 278 {'extend_existing': True}
278 279 )
279 280 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
280 281 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
281 282 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
282 283 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
283 284 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
284 285 name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
285 286 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
286 287 _email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
287 288 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
288 289 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
289 290 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
290 291
291 292 user_log = relationship('UserLog', cascade='all')
292 293 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
293 294
294 295 repositories = relationship('Repository')
295 296 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
296 297 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
297 298
298 299 group_member = relationship('UsersGroupMember', cascade='all')
299 300
300 301 notifications = relationship('UserNotification',)
301 302
302 303 @hybrid_property
303 304 def email(self):
304 305 return self._email
305 306
306 307 @email.setter
307 308 def email(self, val):
308 309 self._email = val.lower() if val else None
309 310
310 311 @property
311 312 def full_name(self):
312 313 return '%s %s' % (self.name, self.lastname)
313 314
314 315 @property
315 316 def full_name_or_username(self):
316 317 return ('%s %s' % (self.name, self.lastname)
317 318 if (self.name and self.lastname) else self.username)
318 319
319 320 @property
320 321 def full_contact(self):
321 322 return '%s %s <%s>' % (self.name, self.lastname, self.email)
322 323
323 324 @property
324 325 def short_contact(self):
325 326 return '%s %s' % (self.name, self.lastname)
326 327
327 328 @property
328 329 def is_admin(self):
329 330 return self.admin
330 331
331 332 def __repr__(self):
332 333 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
333 334 self.user_id, self.username)
334 335
335 336 @classmethod
336 337 def get_by_username(cls, username, case_insensitive=False, cache=False):
337 338 if case_insensitive:
338 339 q = cls.query().filter(cls.username.ilike(username))
339 340 else:
340 341 q = cls.query().filter(cls.username == username)
341 342
342 343 if cache:
343 344 q = q.options(FromCache(
344 345 "sql_cache_short",
345 346 "get_user_%s" % _hash_key(username)
346 347 )
347 348 )
348 349 return q.scalar()
349 350
350 351 @classmethod
351 352 def get_by_api_key(cls, api_key, cache=False):
352 353 q = cls.query().filter(cls.api_key == api_key)
353 354
354 355 if cache:
355 356 q = q.options(FromCache("sql_cache_short",
356 357 "get_api_key_%s" % api_key))
357 358 return q.scalar()
358 359
359 360 @classmethod
360 361 def get_by_email(cls, email, case_insensitive=False, cache=False):
361 362 if case_insensitive:
362 363 q = cls.query().filter(cls.email.ilike(email))
363 364 else:
364 365 q = cls.query().filter(cls.email == email)
365 366
366 367 if cache:
367 368 q = q.options(FromCache("sql_cache_short",
368 369 "get_api_key_%s" % email))
369 370 return q.scalar()
370 371
371 372 def update_lastlogin(self):
372 373 """Update user lastlogin"""
373 374 self.last_login = datetime.datetime.now()
374 375 Session.add(self)
375 376 log.debug('updated user %s lastlogin' % self.username)
376 377
377 378 def __json__(self):
378 379 return dict(
379 380 email=self.email,
380 381 full_name=self.full_name,
381 382 full_name_or_username=self.full_name_or_username,
382 383 short_contact=self.short_contact,
383 384 full_contact=self.full_contact
384 385 )
385 386
386 387
387 388 class UserLog(Base, BaseModel):
388 389 __tablename__ = 'user_logs'
389 390 __table_args__ = {'extend_existing': True}
390 391 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
391 392 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
392 393 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
393 394 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
394 395 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
395 396 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
396 397 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
397 398
398 399 @property
399 400 def action_as_day(self):
400 401 return datetime.date(*self.action_date.timetuple()[:3])
401 402
402 403 user = relationship('User')
403 404 repository = relationship('Repository', cascade='')
404 405
405 406
406 407 class UsersGroup(Base, BaseModel):
407 408 __tablename__ = 'users_groups'
408 409 __table_args__ = {'extend_existing': True}
409 410
410 411 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
411 412 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
412 413 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
413 414
414 415 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
415 416 users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
416 417
417 418 def __repr__(self):
418 419 return '<userGroup(%s)>' % (self.users_group_name)
419 420
420 421 @classmethod
421 422 def get_by_group_name(cls, group_name, cache=False,
422 423 case_insensitive=False):
423 424 if case_insensitive:
424 425 q = cls.query().filter(cls.users_group_name.ilike(group_name))
425 426 else:
426 427 q = cls.query().filter(cls.users_group_name == group_name)
427 428 if cache:
428 429 q = q.options(FromCache(
429 430 "sql_cache_short",
430 431 "get_user_%s" % _hash_key(group_name)
431 432 )
432 433 )
433 434 return q.scalar()
434 435
435 436 @classmethod
436 437 def get(cls, users_group_id, cache=False):
437 438 users_group = cls.query()
438 439 if cache:
439 440 users_group = users_group.options(FromCache("sql_cache_short",
440 441 "get_users_group_%s" % users_group_id))
441 442 return users_group.get(users_group_id)
442 443
443 444
444 445 class UsersGroupMember(Base, BaseModel):
445 446 __tablename__ = 'users_groups_members'
446 447 __table_args__ = {'extend_existing': True}
447 448
448 449 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
449 450 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
450 451 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
451 452
452 453 user = relationship('User', lazy='joined')
453 454 users_group = relationship('UsersGroup')
454 455
455 456 def __init__(self, gr_id='', u_id=''):
456 457 self.users_group_id = gr_id
457 458 self.user_id = u_id
458 459
459 460
460 461 class Repository(Base, BaseModel):
461 462 __tablename__ = 'repositories'
462 463 __table_args__ = (
463 464 UniqueConstraint('repo_name'),
464 465 {'extend_existing': True},
465 466 )
466 467
467 468 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
468 469 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
469 470 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
470 471 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
471 472 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
472 473 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
473 474 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
474 475 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
475 476 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
476 477 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
477 478
478 479 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
479 480 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
480 481
481 482 user = relationship('User')
482 483 fork = relationship('Repository', remote_side=repo_id)
483 484 group = relationship('RepoGroup')
484 485 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
485 486 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
486 487 stats = relationship('Statistics', cascade='all', uselist=False)
487 488
488 489 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
489 490
490 491 logs = relationship('UserLog')
491 492
492 493 def __repr__(self):
493 494 return "<%s('%s:%s')>" % (self.__class__.__name__,
494 495 self.repo_id, self.repo_name)
495 496
496 497 @classmethod
497 498 def url_sep(cls):
498 499 return '/'
499 500
500 501 @classmethod
501 502 def get_by_repo_name(cls, repo_name):
502 503 q = Session.query(cls).filter(cls.repo_name == repo_name)
503 504 q = q.options(joinedload(Repository.fork))\
504 505 .options(joinedload(Repository.user))\
505 506 .options(joinedload(Repository.group))
506 507 return q.scalar()
507 508
508 509 @classmethod
509 510 def get_repo_forks(cls, repo_id):
510 511 return cls.query().filter(Repository.fork_id == repo_id)
511 512
512 513 @classmethod
513 514 def base_path(cls):
514 515 """
515 516 Returns base path when all repos are stored
516 517
517 518 :param cls:
518 519 """
519 520 q = Session.query(RhodeCodeUi)\
520 521 .filter(RhodeCodeUi.ui_key == cls.url_sep())
521 522 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
522 523 return q.one().ui_value
523 524
524 525 @property
525 526 def just_name(self):
526 527 return self.repo_name.split(Repository.url_sep())[-1]
527 528
528 529 @property
529 530 def groups_with_parents(self):
530 531 groups = []
531 532 if self.group is None:
532 533 return groups
533 534
534 535 cur_gr = self.group
535 536 groups.insert(0, cur_gr)
536 537 while 1:
537 538 gr = getattr(cur_gr, 'parent_group', None)
538 539 cur_gr = cur_gr.parent_group
539 540 if gr is None:
540 541 break
541 542 groups.insert(0, gr)
542 543
543 544 return groups
544 545
545 546 @property
546 547 def groups_and_repo(self):
547 548 return self.groups_with_parents, self.just_name
548 549
549 550 @LazyProperty
550 551 def repo_path(self):
551 552 """
552 553 Returns base full path for that repository means where it actually
553 554 exists on a filesystem
554 555 """
555 556 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
556 557 Repository.url_sep())
557 558 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
558 559 return q.one().ui_value
559 560
560 561 @property
561 562 def repo_full_path(self):
562 563 p = [self.repo_path]
563 564 # we need to split the name by / since this is how we store the
564 565 # names in the database, but that eventually needs to be converted
565 566 # into a valid system path
566 567 p += self.repo_name.split(Repository.url_sep())
567 568 return os.path.join(*p)
568 569
569 570 def get_new_name(self, repo_name):
570 571 """
571 572 returns new full repository name based on assigned group and new new
572 573
573 574 :param group_name:
574 575 """
575 576 path_prefix = self.group.full_path_splitted if self.group else []
576 577 return Repository.url_sep().join(path_prefix + [repo_name])
577 578
578 579 @property
579 580 def _ui(self):
580 581 """
581 582 Creates an db based ui object for this repository
582 583 """
583 584 from mercurial import ui
584 585 from mercurial import config
585 586 baseui = ui.ui()
586 587
587 588 #clean the baseui object
588 589 baseui._ocfg = config.config()
589 590 baseui._ucfg = config.config()
590 591 baseui._tcfg = config.config()
591 592
592 593 ret = RhodeCodeUi.query()\
593 594 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
594 595
595 596 hg_ui = ret
596 597 for ui_ in hg_ui:
597 598 if ui_.ui_active:
598 599 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
599 600 ui_.ui_key, ui_.ui_value)
600 601 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
601 602
602 603 return baseui
603 604
604 605 @classmethod
605 606 def is_valid(cls, repo_name):
606 607 """
607 608 returns True if given repo name is a valid filesystem repository
608 609
609 610 :param cls:
610 611 :param repo_name:
611 612 """
612 613 from rhodecode.lib.utils import is_valid_repo
613 614
614 615 return is_valid_repo(repo_name, cls.base_path())
615 616
616 617 #==========================================================================
617 618 # SCM PROPERTIES
618 619 #==========================================================================
619 620
620 621 def get_changeset(self, rev):
621 622 return get_changeset_safe(self.scm_instance, rev)
622 623
623 624 @property
624 625 def tip(self):
625 626 return self.get_changeset('tip')
626 627
627 628 @property
628 629 def author(self):
629 630 return self.tip.author
630 631
631 632 @property
632 633 def last_change(self):
633 634 return self.scm_instance.last_change
634 635
635 636 def comments(self, revisions=None):
636 637 """
637 638 Returns comments for this repository grouped by revisions
638 639
639 640 :param revisions: filter query by revisions only
640 641 """
641 642 cmts = ChangesetComment.query()\
642 643 .filter(ChangesetComment.repo == self)
643 644 if revisions:
644 645 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
645 646 grouped = defaultdict(list)
646 647 for cmt in cmts.all():
647 648 grouped[cmt.revision].append(cmt)
648 649 return grouped
649 650
650 651 #==========================================================================
651 652 # SCM CACHE INSTANCE
652 653 #==========================================================================
653 654
654 655 @property
655 656 def invalidate(self):
656 657 return CacheInvalidation.invalidate(self.repo_name)
657 658
658 659 def set_invalidate(self):
659 660 """
660 661 set a cache for invalidation for this instance
661 662 """
662 663 CacheInvalidation.set_invalidate(self.repo_name)
663 664
664 665 @LazyProperty
665 666 def scm_instance(self):
666 667 return self.__get_instance()
667 668
668 669 @property
669 670 def scm_instance_cached(self):
670 671 @cache_region('long_term')
671 672 def _c(repo_name):
672 673 return self.__get_instance()
673 674 rn = self.repo_name
674 675 log.debug('Getting cached instance of repo')
675 676 inv = self.invalidate
676 677 if inv is not None:
677 678 region_invalidate(_c, None, rn)
678 679 # update our cache
679 680 CacheInvalidation.set_valid(inv.cache_key)
680 681 return _c(rn)
681 682
682 683 def __get_instance(self):
683 684 repo_full_path = self.repo_full_path
684 685 try:
685 686 alias = get_scm(repo_full_path)[0]
686 687 log.debug('Creating instance of %s repository' % alias)
687 688 backend = get_backend(alias)
688 689 except VCSError:
689 690 log.error(traceback.format_exc())
690 691 log.error('Perhaps this repository is in db and not in '
691 692 'filesystem run rescan repositories with '
692 693 '"destroy old data " option from admin panel')
693 694 return
694 695
695 696 if alias == 'hg':
696 697
697 698 repo = backend(safe_str(repo_full_path), create=False,
698 699 baseui=self._ui)
699 700 # skip hidden web repository
700 701 if repo._get_hidden():
701 702 return
702 703 else:
703 704 repo = backend(repo_full_path, create=False)
704 705
705 706 return repo
706 707
707 708
708 709 class RepoGroup(Base, BaseModel):
709 710 __tablename__ = 'groups'
710 711 __table_args__ = (
711 712 UniqueConstraint('group_name', 'group_parent_id'),
712 713 CheckConstraint('group_id != group_parent_id'),
713 714 {'extend_existing': True},
714 715 )
715 716 __mapper_args__ = {'order_by': 'group_name'}
716 717
717 718 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
718 719 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
719 720 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
720 721 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
721 722
722 723 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
723 724 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
724 725
725 726 parent_group = relationship('RepoGroup', remote_side=group_id)
726 727
727 728 def __init__(self, group_name='', parent_group=None):
728 729 self.group_name = group_name
729 730 self.parent_group = parent_group
730 731
731 732 def __repr__(self):
732 733 return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
733 734 self.group_name)
734 735
735 736 @classmethod
736 737 def groups_choices(cls):
737 738 from webhelpers.html import literal as _literal
738 739 repo_groups = [('', '')]
739 740 sep = ' &raquo; '
740 741 _name = lambda k: _literal(sep.join(k))
741 742
742 743 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
743 744 for x in cls.query().all()])
744 745
745 746 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
746 747 return repo_groups
747 748
748 749 @classmethod
749 750 def url_sep(cls):
750 751 return '/'
751 752
752 753 @classmethod
753 754 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
754 755 if case_insensitive:
755 756 gr = cls.query()\
756 757 .filter(cls.group_name.ilike(group_name))
757 758 else:
758 759 gr = cls.query()\
759 760 .filter(cls.group_name == group_name)
760 761 if cache:
761 762 gr = gr.options(FromCache(
762 763 "sql_cache_short",
763 764 "get_group_%s" % _hash_key(group_name)
764 765 )
765 766 )
766 767 return gr.scalar()
767 768
768 769 @property
769 770 def parents(self):
770 771 parents_recursion_limit = 5
771 772 groups = []
772 773 if self.parent_group is None:
773 774 return groups
774 775 cur_gr = self.parent_group
775 776 groups.insert(0, cur_gr)
776 777 cnt = 0
777 778 while 1:
778 779 cnt += 1
779 780 gr = getattr(cur_gr, 'parent_group', None)
780 781 cur_gr = cur_gr.parent_group
781 782 if gr is None:
782 783 break
783 784 if cnt == parents_recursion_limit:
784 785 # this will prevent accidental infinit loops
785 786 log.error('group nested more than %s' %
786 787 parents_recursion_limit)
787 788 break
788 789
789 790 groups.insert(0, gr)
790 791 return groups
791 792
792 793 @property
793 794 def children(self):
794 795 return RepoGroup.query().filter(RepoGroup.parent_group == self)
795 796
796 797 @property
797 798 def name(self):
798 799 return self.group_name.split(RepoGroup.url_sep())[-1]
799 800
800 801 @property
801 802 def full_path(self):
802 803 return self.group_name
803 804
804 805 @property
805 806 def full_path_splitted(self):
806 807 return self.group_name.split(RepoGroup.url_sep())
807 808
808 809 @property
809 810 def repositories(self):
810 811 return Repository.query()\
811 812 .filter(Repository.group == self)\
812 813 .order_by(Repository.repo_name)
813 814
814 815 @property
815 816 def repositories_recursive_count(self):
816 817 cnt = self.repositories.count()
817 818
818 819 def children_count(group):
819 820 cnt = 0
820 821 for child in group.children:
821 822 cnt += child.repositories.count()
822 823 cnt += children_count(child)
823 824 return cnt
824 825
825 826 return cnt + children_count(self)
826 827
827 828 def get_new_name(self, group_name):
828 829 """
829 830 returns new full group name based on parent and new name
830 831
831 832 :param group_name:
832 833 """
833 834 path_prefix = (self.parent_group.full_path_splitted if
834 835 self.parent_group else [])
835 836 return RepoGroup.url_sep().join(path_prefix + [group_name])
836 837
837 838
838 839 class Permission(Base, BaseModel):
839 840 __tablename__ = 'permissions'
840 841 __table_args__ = {'extend_existing': True}
841 842 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
842 843 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
843 844 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
844 845
845 846 def __repr__(self):
846 847 return "<%s('%s:%s')>" % (
847 848 self.__class__.__name__, self.permission_id, self.permission_name
848 849 )
849 850
850 851 @classmethod
851 852 def get_by_key(cls, key):
852 853 return cls.query().filter(cls.permission_name == key).scalar()
853 854
854 855 @classmethod
855 856 def get_default_perms(cls, default_user_id):
856 857 q = Session.query(UserRepoToPerm, Repository, cls)\
857 858 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
858 859 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
859 860 .filter(UserRepoToPerm.user_id == default_user_id)
860 861
861 862 return q.all()
862 863
863 864 @classmethod
864 865 def get_default_group_perms(cls, default_user_id):
865 866 q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\
866 867 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
867 868 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
868 869 .filter(UserRepoGroupToPerm.user_id == default_user_id)
869 870
870 871 return q.all()
871 872
872 873
873 874 class UserRepoToPerm(Base, BaseModel):
874 875 __tablename__ = 'repo_to_perm'
875 876 __table_args__ = (
876 877 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
877 878 {'extend_existing': True}
878 879 )
879 880 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
880 881 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
881 882 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
882 883 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
883 884
884 885 user = relationship('User')
885 886 repository = relationship('Repository')
886 887 permission = relationship('Permission')
887 888
888 889 @classmethod
889 890 def create(cls, user, repository, permission):
890 891 n = cls()
891 892 n.user = user
892 893 n.repository = repository
893 894 n.permission = permission
894 895 Session.add(n)
895 896 return n
896 897
897 898 def __repr__(self):
898 899 return '<user:%s => %s >' % (self.user, self.repository)
899 900
900 901
901 902 class UserToPerm(Base, BaseModel):
902 903 __tablename__ = 'user_to_perm'
903 904 __table_args__ = (
904 905 UniqueConstraint('user_id', 'permission_id'),
905 906 {'extend_existing': True}
906 907 )
907 908 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
908 909 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
909 910 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
910 911
911 912 user = relationship('User')
912 913 permission = relationship('Permission', lazy='joined')
913 914
914 915
915 916 class UsersGroupRepoToPerm(Base, BaseModel):
916 917 __tablename__ = 'users_group_repo_to_perm'
917 918 __table_args__ = (
918 919 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
919 920 {'extend_existing': True}
920 921 )
921 922 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
922 923 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
923 924 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
924 925 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
925 926
926 927 users_group = relationship('UsersGroup')
927 928 permission = relationship('Permission')
928 929 repository = relationship('Repository')
929 930
930 931 @classmethod
931 932 def create(cls, users_group, repository, permission):
932 933 n = cls()
933 934 n.users_group = users_group
934 935 n.repository = repository
935 936 n.permission = permission
936 937 Session.add(n)
937 938 return n
938 939
939 940 def __repr__(self):
940 941 return '<userGroup:%s => %s >' % (self.users_group, self.repository)
941 942
942 943
943 944 class UsersGroupToPerm(Base, BaseModel):
944 945 __tablename__ = 'users_group_to_perm'
945 946 __table_args__ = (
946 947 UniqueConstraint('users_group_id', 'permission_id',),
947 948 {'extend_existing': True}
948 949 )
949 950 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
950 951 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
951 952 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
952 953
953 954 users_group = relationship('UsersGroup')
954 955 permission = relationship('Permission')
955 956
956 957
957 958 class UserRepoGroupToPerm(Base, BaseModel):
958 959 __tablename__ = 'user_repo_group_to_perm'
959 960 __table_args__ = (
960 961 UniqueConstraint('user_id', 'group_id', 'permission_id'),
961 962 {'extend_existing': True}
962 963 )
963 964
964 965 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
965 966 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
966 967 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
967 968 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
968 969
969 970 user = relationship('User')
970 971 group = relationship('RepoGroup')
971 972 permission = relationship('Permission')
972 973
973 974
974 975 class UsersGroupRepoGroupToPerm(Base, BaseModel):
975 976 __tablename__ = 'users_group_repo_group_to_perm'
976 977 __table_args__ = (
977 978 UniqueConstraint('users_group_id', 'group_id'),
978 979 {'extend_existing': True}
979 980 )
980 981
981 982 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)
982 983 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
983 984 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
984 985 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
985 986
986 987 users_group = relationship('UsersGroup')
987 988 permission = relationship('Permission')
988 989 group = relationship('RepoGroup')
989 990
990 991
991 992 class Statistics(Base, BaseModel):
992 993 __tablename__ = 'statistics'
993 994 __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing': True})
994 995 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
995 996 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
996 997 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
997 998 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
998 999 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
999 1000 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1000 1001
1001 1002 repository = relationship('Repository', single_parent=True)
1002 1003
1003 1004
1004 1005 class UserFollowing(Base, BaseModel):
1005 1006 __tablename__ = 'user_followings'
1006 1007 __table_args__ = (
1007 1008 UniqueConstraint('user_id', 'follows_repository_id'),
1008 1009 UniqueConstraint('user_id', 'follows_user_id'),
1009 1010 {'extend_existing': True}
1010 1011 )
1011 1012
1012 1013 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1013 1014 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1014 1015 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1015 1016 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1016 1017 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1017 1018
1018 1019 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1019 1020
1020 1021 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1021 1022 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1022 1023
1023 1024 @classmethod
1024 1025 def get_repo_followers(cls, repo_id):
1025 1026 return cls.query().filter(cls.follows_repo_id == repo_id)
1026 1027
1027 1028
1028 1029 class CacheInvalidation(Base, BaseModel):
1029 1030 __tablename__ = 'cache_invalidation'
1030 1031 __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing': True})
1031 1032 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1032 1033 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1033 1034 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1034 1035 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1035 1036
1036 1037 def __init__(self, cache_key, cache_args=''):
1037 1038 self.cache_key = cache_key
1038 1039 self.cache_args = cache_args
1039 1040 self.cache_active = False
1040 1041
1041 1042 def __repr__(self):
1042 1043 return "<%s('%s:%s')>" % (self.__class__.__name__,
1043 1044 self.cache_id, self.cache_key)
1044 1045
1045 1046 @classmethod
1046 1047 def _get_key(cls, key):
1047 1048 """
1048 1049 Wrapper for generating a key
1049 1050
1050 1051 :param key:
1051 1052 """
1052 1053 import rhodecode
1053 1054 prefix = ''
1054 1055 iid = rhodecode.CONFIG.get('instance_id')
1055 1056 if iid:
1056 1057 prefix = iid
1057 1058 return "%s%s" % (prefix, key)
1058 1059
1059 1060 @classmethod
1060 1061 def get_by_key(cls, key):
1061 1062 return cls.query().filter(cls.cache_key == key).scalar()
1062 1063
1063 1064 @classmethod
1064 1065 def invalidate(cls, key):
1065 1066 """
1066 1067 Returns Invalidation object if this given key should be invalidated
1067 1068 None otherwise. `cache_active = False` means that this cache
1068 1069 state is not valid and needs to be invalidated
1069 1070
1070 1071 :param key:
1071 1072 """
1072 1073 return cls.query()\
1073 1074 .filter(CacheInvalidation.cache_key == key)\
1074 1075 .filter(CacheInvalidation.cache_active == False)\
1075 1076 .scalar()
1076 1077
1077 1078 @classmethod
1078 1079 def set_invalidate(cls, key):
1079 1080 """
1080 1081 Mark this Cache key for invalidation
1081 1082
1082 1083 :param key:
1083 1084 """
1084 1085
1085 1086 log.debug('marking %s for invalidation' % key)
1086 1087 inv_obj = Session.query(cls)\
1087 1088 .filter(cls.cache_key == key).scalar()
1088 1089 if inv_obj:
1089 1090 inv_obj.cache_active = False
1090 1091 else:
1091 1092 log.debug('cache key not found in invalidation db -> creating one')
1092 1093 inv_obj = CacheInvalidation(key)
1093 1094
1094 1095 try:
1095 1096 Session.add(inv_obj)
1096 1097 Session.commit()
1097 1098 except Exception:
1098 1099 log.error(traceback.format_exc())
1099 1100 Session.rollback()
1100 1101
1101 1102 @classmethod
1102 1103 def set_valid(cls, key):
1103 1104 """
1104 1105 Mark this cache key as active and currently cached
1105 1106
1106 1107 :param key:
1107 1108 """
1108 1109 inv_obj = cls.get_by_key(key)
1109 1110 inv_obj.cache_active = True
1110 1111 Session.add(inv_obj)
1111 1112 Session.commit()
1112 1113
1113 1114
1114 1115 class ChangesetComment(Base, BaseModel):
1115 1116 __tablename__ = 'changeset_comments'
1116 1117 __table_args__ = ({'extend_existing': True},)
1117 1118 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1118 1119 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1119 1120 revision = Column('revision', String(40), nullable=False)
1120 1121 line_no = Column('line_no', Unicode(10), nullable=True)
1121 1122 f_path = Column('f_path', Unicode(1000), nullable=True)
1122 1123 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1123 1124 text = Column('text', Unicode(25000), nullable=False)
1124 1125 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1125 1126
1126 1127 author = relationship('User', lazy='joined')
1127 1128 repo = relationship('Repository')
1128 1129
1129 1130 @classmethod
1130 1131 def get_users(cls, revision):
1131 1132 """
1132 1133 Returns user associated with this changesetComment. ie those
1133 1134 who actually commented
1134 1135
1135 1136 :param cls:
1136 1137 :param revision:
1137 1138 """
1138 1139 return Session.query(User)\
1139 1140 .filter(cls.revision == revision)\
1140 1141 .join(ChangesetComment.author).all()
1141 1142
1142 1143
1143 1144 class Notification(Base, BaseModel):
1144 1145 __tablename__ = 'notifications'
1145 1146 __table_args__ = ({'extend_existing': True},)
1146 1147
1147 1148 TYPE_CHANGESET_COMMENT = u'cs_comment'
1148 1149 TYPE_MESSAGE = u'message'
1149 1150 TYPE_MENTION = u'mention'
1150 1151 TYPE_REGISTRATION = u'registration'
1151 1152
1152 1153 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1153 1154 subject = Column('subject', Unicode(512), nullable=True)
1154 1155 body = Column('body', Unicode(50000), nullable=True)
1155 1156 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1156 1157 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1157 1158 type_ = Column('type', Unicode(256))
1158 1159
1159 1160 created_by_user = relationship('User')
1160 1161 notifications_to_users = relationship('UserNotification', lazy='joined',
1161 1162 cascade="all, delete, delete-orphan")
1162 1163
1163 1164 @property
1164 1165 def recipients(self):
1165 1166 return [x.user for x in UserNotification.query()\
1166 1167 .filter(UserNotification.notification == self).all()]
1167 1168
1168 1169 @classmethod
1169 1170 def create(cls, created_by, subject, body, recipients, type_=None):
1170 1171 if type_ is None:
1171 1172 type_ = Notification.TYPE_MESSAGE
1172 1173
1173 1174 notification = cls()
1174 1175 notification.created_by_user = created_by
1175 1176 notification.subject = subject
1176 1177 notification.body = body
1177 1178 notification.type_ = type_
1178 1179 notification.created_on = datetime.datetime.now()
1179 1180
1180 1181 for u in recipients:
1181 1182 assoc = UserNotification()
1182 1183 assoc.notification = notification
1183 1184 u.notifications.append(assoc)
1184 1185 Session.add(notification)
1185 1186 return notification
1186 1187
1187 1188 @property
1188 1189 def description(self):
1189 1190 from rhodecode.model.notification import NotificationModel
1190 1191 return NotificationModel().make_description(self)
1191 1192
1192 1193
1193 1194 class UserNotification(Base, BaseModel):
1194 1195 __tablename__ = 'user_to_notification'
1195 1196 __table_args__ = (
1196 1197 UniqueConstraint('user_id', 'notification_id'),
1197 1198 {'extend_existing': True}
1198 1199 )
1199 1200 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1200 1201 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1201 1202 read = Column('read', Boolean, default=False)
1202 1203 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1203 1204
1204 1205 user = relationship('User', lazy="joined")
1205 1206 notification = relationship('Notification', lazy="joined",
1206 1207 order_by=lambda: Notification.created_on.desc(),)
1207 1208
1208 1209 def mark_as_read(self):
1209 1210 self.read = True
1210 1211 Session.add(self)
1211 1212
1212 1213
1213 1214 class DbMigrateVersion(Base, BaseModel):
1214 1215 __tablename__ = 'db_migrate_version'
1215 1216 __table_args__ = {'extend_existing': True}
1216 1217 repository_id = Column('repository_id', String(250), primary_key=True)
1217 1218 repository_path = Column('repository_path', Text)
1218 1219 version = Column('version', Integer)
@@ -1,225 +1,224 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.notification
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Model for notifications
7 7
8 8
9 9 :created_on: Nov 20, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30 import datetime
31 31
32 32 from pylons.i18n.translation import _
33 33
34 34 import rhodecode
35 from rhodecode.config.conf import DATETIME_FORMAT
35 36 from rhodecode.lib import helpers as h
36 37 from rhodecode.model import BaseModel
37 38 from rhodecode.model.db import Notification, User, UserNotification
38 39
39 40 log = logging.getLogger(__name__)
40 41
41 42
42 43 class NotificationModel(BaseModel):
43 44
44 45 def __get_user(self, user):
45 46 return self._get_instance(User, user, callback=User.get_by_username)
46 47
47 48 def __get_notification(self, notification):
48 49 if isinstance(notification, Notification):
49 50 return notification
50 51 elif isinstance(notification, int):
51 52 return Notification.get(notification)
52 53 else:
53 54 if notification:
54 55 raise Exception('notification must be int or Instance'
55 56 ' of Notification got %s' % type(notification))
56 57
57 58 def create(self, created_by, subject, body, recipients=None,
58 59 type_=Notification.TYPE_MESSAGE, with_email=True,
59 60 email_kwargs={}):
60 61 """
61 62
62 63 Creates notification of given type
63 64
64 65 :param created_by: int, str or User instance. User who created this
65 66 notification
66 67 :param subject:
67 68 :param body:
68 69 :param recipients: list of int, str or User objects, when None
69 70 is given send to all admins
70 71 :param type_: type of notification
71 72 :param with_email: send email with this notification
72 73 :param email_kwargs: additional dict to pass as args to email template
73 74 """
74 75 from rhodecode.lib.celerylib import tasks, run_task
75 76
76 77 if recipients and not getattr(recipients, '__iter__', False):
77 78 raise Exception('recipients must be a list of iterable')
78 79
79 80 created_by_obj = self.__get_user(created_by)
80 81
81 82 if recipients:
82 83 recipients_objs = []
83 84 for u in recipients:
84 85 obj = self.__get_user(u)
85 86 if obj:
86 87 recipients_objs.append(obj)
87 88 recipients_objs = set(recipients_objs)
88 89 log.debug('sending notifications %s to %s' % (
89 90 type_, recipients_objs)
90 91 )
91 92 else:
92 93 # empty recipients means to all admins
93 94 recipients_objs = User.query().filter(User.admin == True).all()
94 95 log.debug('sending notifications %s to admins: %s' % (
95 96 type_, recipients_objs)
96 97 )
97 98 notif = Notification.create(
98 99 created_by=created_by_obj, subject=subject,
99 100 body=body, recipients=recipients_objs, type_=type_
100 101 )
101 102
102 103 if with_email is False:
103 104 return notif
104 105
105 106 # send email with notification
106 107 for rec in recipients_objs:
107 108 email_subject = NotificationModel().make_description(notif, False)
108 109 type_ = type_
109 110 email_body = body
110 111 kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
111 112 kwargs.update(email_kwargs)
112 113 email_body_html = EmailNotificationModel()\
113 114 .get_email_tmpl(type_, **kwargs)
114 115 run_task(tasks.send_email, rec.email, email_subject, email_body,
115 116 email_body_html)
116 117
117 118 return notif
118 119
119 120 def delete(self, user, notification):
120 121 # we don't want to remove actual notification just the assignment
121 122 try:
122 123 notification = self.__get_notification(notification)
123 124 user = self.__get_user(user)
124 125 if notification and user:
125 126 obj = UserNotification.query()\
126 127 .filter(UserNotification.user == user)\
127 128 .filter(UserNotification.notification
128 129 == notification)\
129 130 .one()
130 131 self.sa.delete(obj)
131 132 return True
132 133 except Exception:
133 134 log.error(traceback.format_exc())
134 135 raise
135 136
136 137 def get_for_user(self, user):
137 138 user = self.__get_user(user)
138 139 return user.notifications
139 140
140 141 def mark_all_read_for_user(self, user):
141 142 user = self.__get_user(user)
142 143 UserNotification.query()\
143 144 .filter(UserNotification.read==False)\
144 145 .update({'read': True})
145 146
146 147 def get_unread_cnt_for_user(self, user):
147 148 user = self.__get_user(user)
148 149 return UserNotification.query()\
149 150 .filter(UserNotification.read == False)\
150 151 .filter(UserNotification.user == user).count()
151 152
152 153 def get_unread_for_user(self, user):
153 154 user = self.__get_user(user)
154 155 return [x.notification for x in UserNotification.query()\
155 156 .filter(UserNotification.read == False)\
156 157 .filter(UserNotification.user == user).all()]
157 158
158 159 def get_user_notification(self, user, notification):
159 160 user = self.__get_user(user)
160 161 notification = self.__get_notification(notification)
161 162
162 163 return UserNotification.query()\
163 164 .filter(UserNotification.notification == notification)\
164 165 .filter(UserNotification.user == user).scalar()
165 166
166 167 def make_description(self, notification, show_age=True):
167 168 """
168 169 Creates a human readable description based on properties
169 170 of notification object
170 171 """
171 172
172 173 _map = {
173 174 notification.TYPE_CHANGESET_COMMENT: _('commented on commit'),
174 175 notification.TYPE_MESSAGE: _('sent message'),
175 176 notification.TYPE_MENTION: _('mentioned you'),
176 177 notification.TYPE_REGISTRATION: _('registered in RhodeCode')
177 178 }
178 179
179 DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
180
181 180 tmpl = "%(user)s %(action)s %(when)s"
182 181 if show_age:
183 182 when = h.age(notification.created_on)
184 183 else:
185 184 DTF = lambda d: datetime.datetime.strftime(d, DATETIME_FORMAT)
186 185 when = DTF(notification.created_on)
187 186 data = dict(
188 187 user=notification.created_by_user.username,
189 188 action=_map[notification.type_], when=when,
190 189 )
191 190 return tmpl % data
192 191
193 192
194 193 class EmailNotificationModel(BaseModel):
195 194
196 195 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
197 196 TYPE_PASSWORD_RESET = 'passoword_link'
198 197 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
199 198 TYPE_DEFAULT = 'default'
200 199
201 200 def __init__(self):
202 201 self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0]
203 202 self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
204 203
205 204 self.email_types = {
206 205 self.TYPE_CHANGESET_COMMENT: 'email_templates/changeset_comment.html',
207 206 self.TYPE_PASSWORD_RESET: 'email_templates/password_reset.html',
208 207 self.TYPE_REGISTRATION: 'email_templates/registration.html',
209 208 self.TYPE_DEFAULT: 'email_templates/default.html'
210 209 }
211 210
212 211 def get_email_tmpl(self, type_, **kwargs):
213 212 """
214 213 return generated template for email based on given type
215 214
216 215 :param type_:
217 216 """
218 217
219 218 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
220 219 email_template = self._tmpl_lookup.get_template(base)
221 220 # translator inject
222 221 _kwargs = {'_': _}
223 222 _kwargs.update(kwargs)
224 223 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
225 224 return email_template.render(**_kwargs)
@@ -1,495 +1,494 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.repo
4 4 ~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Repository model for rhodecode
7 7
8 8 :created_on: Jun 5, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import os
26 26 import shutil
27 27 import logging
28 28 import traceback
29 29 from datetime import datetime
30 30
31 31 from rhodecode.lib.vcs.backends import get_backend
32 32
33 from rhodecode.lib import LazyProperty
34 from rhodecode.lib import safe_str, safe_unicode
33 from rhodecode.lib.utils2 import LazyProperty, safe_str, safe_unicode
35 34 from rhodecode.lib.caching_query import FromCache
36 35 from rhodecode.lib.hooks import log_create_repository
37 36
38 37 from rhodecode.model import BaseModel
39 38 from rhodecode.model.db import Repository, UserRepoToPerm, User, Permission, \
40 39 Statistics, UsersGroup, UsersGroupRepoToPerm, RhodeCodeUi, RepoGroup
41 40
42 41
43 42 log = logging.getLogger(__name__)
44 43
45 44
46 45 class RepoModel(BaseModel):
47 46
48 47 def __get_user(self, user):
49 48 return self._get_instance(User, user, callback=User.get_by_username)
50 49
51 50 def __get_users_group(self, users_group):
52 51 return self._get_instance(UsersGroup, users_group,
53 52 callback=UsersGroup.get_by_group_name)
54 53
55 54 def __get_repos_group(self, repos_group):
56 55 return self._get_instance(RepoGroup, repos_group,
57 56 callback=RepoGroup.get_by_group_name)
58 57
59 58 def __get_repo(self, repository):
60 59 return self._get_instance(Repository, repository,
61 60 callback=Repository.get_by_repo_name)
62 61
63 62 def __get_perm(self, permission):
64 63 return self._get_instance(Permission, permission,
65 64 callback=Permission.get_by_key)
66 65
67 66 @LazyProperty
68 67 def repos_path(self):
69 68 """
70 69 Get's the repositories root path from database
71 70 """
72 71
73 72 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
74 73 return q.ui_value
75 74
76 75 def get(self, repo_id, cache=False):
77 76 repo = self.sa.query(Repository)\
78 77 .filter(Repository.repo_id == repo_id)
79 78
80 79 if cache:
81 80 repo = repo.options(FromCache("sql_cache_short",
82 81 "get_repo_%s" % repo_id))
83 82 return repo.scalar()
84 83
85 84 def get_repo(self, repository):
86 85 return self.__get_repo(repository)
87 86
88 87 def get_by_repo_name(self, repo_name, cache=False):
89 88 repo = self.sa.query(Repository)\
90 89 .filter(Repository.repo_name == repo_name)
91 90
92 91 if cache:
93 92 repo = repo.options(FromCache("sql_cache_short",
94 93 "get_repo_%s" % repo_name))
95 94 return repo.scalar()
96 95
97 96 def get_users_js(self):
98 97
99 98 users = self.sa.query(User).filter(User.active == True).all()
100 99 u_tmpl = '''{id:%s, fname:"%s", lname:"%s", nname:"%s"},'''
101 100 users_array = '[%s]' % '\n'.join([u_tmpl % (u.user_id, u.name,
102 101 u.lastname, u.username)
103 102 for u in users])
104 103 return users_array
105 104
106 105 def get_users_groups_js(self):
107 106 users_groups = self.sa.query(UsersGroup)\
108 107 .filter(UsersGroup.users_group_active == True).all()
109 108
110 109 g_tmpl = '''{id:%s, grname:"%s",grmembers:"%s"},'''
111 110
112 111 users_groups_array = '[%s]' % '\n'.join([g_tmpl % \
113 112 (gr.users_group_id, gr.users_group_name,
114 113 len(gr.members))
115 114 for gr in users_groups])
116 115 return users_groups_array
117 116
118 117 def _get_defaults(self, repo_name):
119 118 """
120 119 Get's information about repository, and returns a dict for
121 120 usage in forms
122 121
123 122 :param repo_name:
124 123 """
125 124
126 125 repo_info = Repository.get_by_repo_name(repo_name)
127 126
128 127 if repo_info is None:
129 128 return None
130 129
131 130 defaults = repo_info.get_dict()
132 131 group, repo_name = repo_info.groups_and_repo
133 132 defaults['repo_name'] = repo_name
134 133 defaults['repo_group'] = getattr(group[-1] if group else None,
135 134 'group_id', None)
136 135
137 136 # fill owner
138 137 if repo_info.user:
139 138 defaults.update({'user': repo_info.user.username})
140 139 else:
141 140 replacement_user = User.query().filter(User.admin ==
142 141 True).first().username
143 142 defaults.update({'user': replacement_user})
144 143
145 144 # fill repository users
146 145 for p in repo_info.repo_to_perm:
147 146 defaults.update({'u_perm_%s' % p.user.username:
148 147 p.permission.permission_name})
149 148
150 149 # fill repository groups
151 150 for p in repo_info.users_group_to_perm:
152 151 defaults.update({'g_perm_%s' % p.users_group.users_group_name:
153 152 p.permission.permission_name})
154 153
155 154 return defaults
156 155
157 156 def update(self, repo_name, form_data):
158 157 try:
159 158 cur_repo = self.get_by_repo_name(repo_name, cache=False)
160 159
161 160 # update permissions
162 161 for member, perm, member_type in form_data['perms_updates']:
163 162 if member_type == 'user':
164 163 # this updates existing one
165 164 RepoModel().grant_user_permission(
166 165 repo=cur_repo, user=member, perm=perm
167 166 )
168 167 else:
169 168 RepoModel().grant_users_group_permission(
170 169 repo=cur_repo, group_name=member, perm=perm
171 170 )
172 171 # set new permissions
173 172 for member, perm, member_type in form_data['perms_new']:
174 173 if member_type == 'user':
175 174 RepoModel().grant_user_permission(
176 175 repo=cur_repo, user=member, perm=perm
177 176 )
178 177 else:
179 178 RepoModel().grant_users_group_permission(
180 179 repo=cur_repo, group_name=member, perm=perm
181 180 )
182 181
183 182 # update current repo
184 183 for k, v in form_data.items():
185 184 if k == 'user':
186 185 cur_repo.user = User.get_by_username(v)
187 186 elif k == 'repo_name':
188 187 pass
189 188 elif k == 'repo_group':
190 189 cur_repo.group = RepoGroup.get(v)
191 190
192 191 else:
193 192 setattr(cur_repo, k, v)
194 193
195 194 new_name = cur_repo.get_new_name(form_data['repo_name'])
196 195 cur_repo.repo_name = new_name
197 196
198 197 self.sa.add(cur_repo)
199 198
200 199 if repo_name != new_name:
201 200 # rename repository
202 201 self.__rename_repo(old=repo_name, new=new_name)
203 202
204 203 return cur_repo
205 204 except:
206 205 log.error(traceback.format_exc())
207 206 raise
208 207
209 208 def create(self, form_data, cur_user, just_db=False, fork=False):
210 209 from rhodecode.model.scm import ScmModel
211 210
212 211 try:
213 212 if fork:
214 213 fork_parent_id = form_data['fork_parent_id']
215 214
216 215 # repo name is just a name of repository
217 216 # while repo_name_full is a full qualified name that is combined
218 217 # with name and path of group
219 218 repo_name = form_data['repo_name']
220 219 repo_name_full = form_data['repo_name_full']
221 220
222 221 new_repo = Repository()
223 222 new_repo.enable_statistics = False
224 223
225 224 for k, v in form_data.items():
226 225 if k == 'repo_name':
227 226 v = repo_name_full
228 227 if k == 'repo_group':
229 228 k = 'group_id'
230 229 if k == 'description':
231 230 v = v or repo_name
232 231
233 232 setattr(new_repo, k, v)
234 233
235 234 if fork:
236 235 parent_repo = Repository.get(fork_parent_id)
237 236 new_repo.fork = parent_repo
238 237
239 238 new_repo.user_id = cur_user.user_id
240 239 self.sa.add(new_repo)
241 240
242 241 def _create_default_perms():
243 242 # create default permission
244 243 repo_to_perm = UserRepoToPerm()
245 244 default = 'repository.read'
246 245 for p in User.get_by_username('default').user_perms:
247 246 if p.permission.permission_name.startswith('repository.'):
248 247 default = p.permission.permission_name
249 248 break
250 249
251 250 default_perm = 'repository.none' if form_data['private'] else default
252 251
253 252 repo_to_perm.permission_id = self.sa.query(Permission)\
254 253 .filter(Permission.permission_name == default_perm)\
255 254 .one().permission_id
256 255
257 256 repo_to_perm.repository = new_repo
258 257 repo_to_perm.user_id = User.get_by_username('default').user_id
259 258
260 259 self.sa.add(repo_to_perm)
261 260
262 261 if fork:
263 262 if form_data.get('copy_permissions'):
264 263 repo = Repository.get(fork_parent_id)
265 264 user_perms = UserRepoToPerm.query()\
266 265 .filter(UserRepoToPerm.repository == repo).all()
267 266 group_perms = UsersGroupRepoToPerm.query()\
268 267 .filter(UsersGroupRepoToPerm.repository == repo).all()
269 268
270 269 for perm in user_perms:
271 270 UserRepoToPerm.create(perm.user, new_repo,
272 271 perm.permission)
273 272
274 273 for perm in group_perms:
275 274 UsersGroupRepoToPerm.create(perm.users_group, new_repo,
276 275 perm.permission)
277 276 else:
278 277 _create_default_perms()
279 278 else:
280 279 _create_default_perms()
281 280
282 281 if not just_db:
283 282 self.__create_repo(repo_name, form_data['repo_type'],
284 283 form_data['repo_group'],
285 284 form_data['clone_uri'])
286 285
287 286 # now automatically start following this repository as owner
288 287 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
289 288 cur_user.user_id)
290 289 log_create_repository(new_repo.get_dict(),
291 290 created_by=cur_user.username)
292 291 return new_repo
293 292 except:
294 293 log.error(traceback.format_exc())
295 294 raise
296 295
297 296 def create_fork(self, form_data, cur_user):
298 297 """
299 298 Simple wrapper into executing celery task for fork creation
300 299
301 300 :param form_data:
302 301 :param cur_user:
303 302 """
304 303 from rhodecode.lib.celerylib import tasks, run_task
305 304 run_task(tasks.create_repo_fork, form_data, cur_user)
306 305
307 306 def delete(self, repo):
308 307 repo = self.__get_repo(repo)
309 308 try:
310 309 self.sa.delete(repo)
311 310 self.__delete_repo(repo)
312 311 except:
313 312 log.error(traceback.format_exc())
314 313 raise
315 314
316 315 def grant_user_permission(self, repo, user, perm):
317 316 """
318 317 Grant permission for user on given repository, or update existing one
319 318 if found
320 319
321 320 :param repo: Instance of Repository, repository_id, or repository name
322 321 :param user: Instance of User, user_id or username
323 322 :param perm: Instance of Permission, or permission_name
324 323 """
325 324 user = self.__get_user(user)
326 325 repo = self.__get_repo(repo)
327 326 permission = self.__get_perm(perm)
328 327
329 328 # check if we have that permission already
330 329 obj = self.sa.query(UserRepoToPerm)\
331 330 .filter(UserRepoToPerm.user == user)\
332 331 .filter(UserRepoToPerm.repository == repo)\
333 332 .scalar()
334 333 if obj is None:
335 334 # create new !
336 335 obj = UserRepoToPerm()
337 336 obj.repository = repo
338 337 obj.user = user
339 338 obj.permission = permission
340 339 self.sa.add(obj)
341 340
342 341 def revoke_user_permission(self, repo, user):
343 342 """
344 343 Revoke permission for user on given repository
345 344
346 345 :param repo: Instance of Repository, repository_id, or repository name
347 346 :param user: Instance of User, user_id or username
348 347 """
349 348 user = self.__get_user(user)
350 349 repo = self.__get_repo(repo)
351 350
352 351 obj = self.sa.query(UserRepoToPerm)\
353 352 .filter(UserRepoToPerm.repository == repo)\
354 353 .filter(UserRepoToPerm.user == user)\
355 354 .one()
356 355 self.sa.delete(obj)
357 356
358 357 def grant_users_group_permission(self, repo, group_name, perm):
359 358 """
360 359 Grant permission for users group on given repository, or update
361 360 existing one if found
362 361
363 362 :param repo: Instance of Repository, repository_id, or repository name
364 363 :param group_name: Instance of UserGroup, users_group_id,
365 364 or users group name
366 365 :param perm: Instance of Permission, or permission_name
367 366 """
368 367 repo = self.__get_repo(repo)
369 368 group_name = self.__get_users_group(group_name)
370 369 permission = self.__get_perm(perm)
371 370
372 371 # check if we have that permission already
373 372 obj = self.sa.query(UsersGroupRepoToPerm)\
374 373 .filter(UsersGroupRepoToPerm.users_group == group_name)\
375 374 .filter(UsersGroupRepoToPerm.repository == repo)\
376 375 .scalar()
377 376
378 377 if obj is None:
379 378 # create new
380 379 obj = UsersGroupRepoToPerm()
381 380
382 381 obj.repository = repo
383 382 obj.users_group = group_name
384 383 obj.permission = permission
385 384 self.sa.add(obj)
386 385
387 386 def revoke_users_group_permission(self, repo, group_name):
388 387 """
389 388 Revoke permission for users group on given repository
390 389
391 390 :param repo: Instance of Repository, repository_id, or repository name
392 391 :param group_name: Instance of UserGroup, users_group_id,
393 392 or users group name
394 393 """
395 394 repo = self.__get_repo(repo)
396 395 group_name = self.__get_users_group(group_name)
397 396
398 397 obj = self.sa.query(UsersGroupRepoToPerm)\
399 398 .filter(UsersGroupRepoToPerm.repository == repo)\
400 399 .filter(UsersGroupRepoToPerm.users_group == group_name)\
401 400 .one()
402 401 self.sa.delete(obj)
403 402
404 403 def delete_stats(self, repo_name):
405 404 """
406 405 removes stats for given repo
407 406
408 407 :param repo_name:
409 408 """
410 409 try:
411 410 obj = self.sa.query(Statistics)\
412 411 .filter(Statistics.repository ==
413 412 self.get_by_repo_name(repo_name))\
414 413 .one()
415 414 self.sa.delete(obj)
416 415 except:
417 416 log.error(traceback.format_exc())
418 417 raise
419 418
420 419 def __create_repo(self, repo_name, alias, new_parent_id, clone_uri=False):
421 420 """
422 421 makes repository on filesystem. It's group aware means it'll create
423 422 a repository within a group, and alter the paths accordingly of
424 423 group location
425 424
426 425 :param repo_name:
427 426 :param alias:
428 427 :param parent_id:
429 428 :param clone_uri:
430 429 """
431 430 from rhodecode.lib.utils import is_valid_repo, is_valid_repos_group
432 431
433 432 if new_parent_id:
434 433 paths = RepoGroup.get(new_parent_id)\
435 434 .full_path.split(RepoGroup.url_sep())
436 435 new_parent_path = os.sep.join(paths)
437 436 else:
438 437 new_parent_path = ''
439 438
440 439 # we need to make it str for mercurial
441 440 repo_path = os.path.join(*map(lambda x: safe_str(x),
442 441 [self.repos_path, new_parent_path, repo_name]))
443 442
444 443 # check if this path is not a repository
445 444 if is_valid_repo(repo_path, self.repos_path):
446 445 raise Exception('This path %s is a valid repository' % repo_path)
447 446
448 447 # check if this path is a group
449 448 if is_valid_repos_group(repo_path, self.repos_path):
450 449 raise Exception('This path %s is a valid group' % repo_path)
451 450
452 451 log.info('creating repo %s in %s @ %s' % (
453 452 repo_name, safe_unicode(repo_path), clone_uri
454 453 )
455 454 )
456 455 backend = get_backend(alias)
457 456
458 457 backend(repo_path, create=True, src_url=clone_uri)
459 458
460 459 def __rename_repo(self, old, new):
461 460 """
462 461 renames repository on filesystem
463 462
464 463 :param old: old name
465 464 :param new: new name
466 465 """
467 466 log.info('renaming repo from %s to %s' % (old, new))
468 467
469 468 old_path = os.path.join(self.repos_path, old)
470 469 new_path = os.path.join(self.repos_path, new)
471 470 if os.path.isdir(new_path):
472 471 raise Exception(
473 472 'Was trying to rename to already existing dir %s' % new_path
474 473 )
475 474 shutil.move(old_path, new_path)
476 475
477 476 def __delete_repo(self, repo):
478 477 """
479 478 removes repo from filesystem, the removal is acctually made by
480 479 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
481 480 repository is no longer valid for rhodecode, can be undeleted later on
482 481 by reverting the renames on this repository
483 482
484 483 :param repo: repo object
485 484 """
486 485 rm_path = os.path.join(self.repos_path, repo.repo_name)
487 486 log.info("Removing %s" % (rm_path))
488 487 # disable hg/git
489 488 alias = repo.repo_type
490 489 shutil.move(os.path.join(rm_path, '.%s' % alias),
491 490 os.path.join(rm_path, 'rm__.%s' % alias))
492 491 # disable repo
493 492 _d = 'rm__%s__%s' % (datetime.now().strftime('%Y%m%d_%H%M%S_%f'),
494 493 repo.repo_name)
495 494 shutil.move(rm_path, os.path.join(self.repos_path, _d))
@@ -1,310 +1,310 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.user_group
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 users groups model for RhodeCode
7 7
8 8 :created_on: Jan 25, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import traceback
29 29 import shutil
30 30
31 from rhodecode.lib import LazyProperty
31 from rhodecode.lib.utils2 import LazyProperty
32 32
33 33 from rhodecode.model import BaseModel
34 34 from rhodecode.model.db import RepoGroup, RhodeCodeUi, UserRepoGroupToPerm, \
35 35 User, Permission, UsersGroupRepoGroupToPerm, UsersGroup
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class ReposGroupModel(BaseModel):
41 41
42 42 def __get_user(self, user):
43 43 return self._get_instance(User, user, callback=User.get_by_username)
44 44
45 45 def __get_users_group(self, users_group):
46 46 return self._get_instance(UsersGroup, users_group,
47 47 callback=UsersGroup.get_by_group_name)
48 48
49 49 def __get_repos_group(self, repos_group):
50 50 return self._get_instance(RepoGroup, repos_group,
51 51 callback=RepoGroup.get_by_group_name)
52 52
53 53 def __get_perm(self, permission):
54 54 return self._get_instance(Permission, permission,
55 55 callback=Permission.get_by_key)
56 56
57 57 @LazyProperty
58 58 def repos_path(self):
59 59 """
60 60 Get's the repositories root path from database
61 61 """
62 62
63 63 q = RhodeCodeUi.get_by_key('/').one()
64 64 return q.ui_value
65 65
66 66 def _create_default_perms(self, new_group):
67 67 # create default permission
68 68 repo_group_to_perm = UserRepoGroupToPerm()
69 69 default_perm = 'group.read'
70 70 for p in User.get_by_username('default').user_perms:
71 71 if p.permission.permission_name.startswith('group.'):
72 72 default_perm = p.permission.permission_name
73 73 break
74 74
75 75 repo_group_to_perm.permission_id = self.sa.query(Permission)\
76 76 .filter(Permission.permission_name == default_perm)\
77 77 .one().permission_id
78 78
79 79 repo_group_to_perm.group = new_group
80 80 repo_group_to_perm.user_id = User.get_by_username('default').user_id
81 81
82 82 self.sa.add(repo_group_to_perm)
83 83
84 84 def __create_group(self, group_name):
85 85 """
86 86 makes repositories group on filesystem
87 87
88 88 :param repo_name:
89 89 :param parent_id:
90 90 """
91 91
92 92 create_path = os.path.join(self.repos_path, group_name)
93 93 log.debug('creating new group in %s' % create_path)
94 94
95 95 if os.path.isdir(create_path):
96 96 raise Exception('That directory already exists !')
97 97
98 98 os.makedirs(create_path)
99 99
100 100 def __rename_group(self, old, new):
101 101 """
102 102 Renames a group on filesystem
103 103
104 104 :param group_name:
105 105 """
106 106
107 107 if old == new:
108 108 log.debug('skipping group rename')
109 109 return
110 110
111 111 log.debug('renaming repos group from %s to %s' % (old, new))
112 112
113 113 old_path = os.path.join(self.repos_path, old)
114 114 new_path = os.path.join(self.repos_path, new)
115 115
116 116 log.debug('renaming repos paths from %s to %s' % (old_path, new_path))
117 117
118 118 if os.path.isdir(new_path):
119 119 raise Exception('Was trying to rename to already '
120 120 'existing dir %s' % new_path)
121 121 shutil.move(old_path, new_path)
122 122
123 123 def __delete_group(self, group):
124 124 """
125 125 Deletes a group from a filesystem
126 126
127 127 :param group: instance of group from database
128 128 """
129 129 paths = group.full_path.split(RepoGroup.url_sep())
130 130 paths = os.sep.join(paths)
131 131
132 132 rm_path = os.path.join(self.repos_path, paths)
133 133 if os.path.isdir(rm_path):
134 134 # delete only if that path really exists
135 135 os.rmdir(rm_path)
136 136
137 137 def create(self, group_name, group_description, parent, just_db=False):
138 138 try:
139 139 new_repos_group = RepoGroup()
140 140 new_repos_group.group_description = group_description
141 141 new_repos_group.parent_group = self.__get_repos_group(parent)
142 142 new_repos_group.group_name = new_repos_group.get_new_name(group_name)
143 143
144 144 self.sa.add(new_repos_group)
145 145 self._create_default_perms(new_repos_group)
146 146
147 147 if not just_db:
148 148 # we need to flush here, in order to check if database won't
149 149 # throw any exceptions, create filesystem dirs at the very end
150 150 self.sa.flush()
151 151 self.__create_group(new_repos_group.group_name)
152 152
153 153 return new_repos_group
154 154 except:
155 155 log.error(traceback.format_exc())
156 156 raise
157 157
158 158 def update(self, repos_group_id, form_data):
159 159
160 160 try:
161 161 repos_group = RepoGroup.get(repos_group_id)
162 162
163 163 # update permissions
164 164 for member, perm, member_type in form_data['perms_updates']:
165 165 if member_type == 'user':
166 166 # this updates also current one if found
167 167 ReposGroupModel().grant_user_permission(
168 168 repos_group=repos_group, user=member, perm=perm
169 169 )
170 170 else:
171 171 ReposGroupModel().grant_users_group_permission(
172 172 repos_group=repos_group, group_name=member, perm=perm
173 173 )
174 174 # set new permissions
175 175 for member, perm, member_type in form_data['perms_new']:
176 176 if member_type == 'user':
177 177 ReposGroupModel().grant_user_permission(
178 178 repos_group=repos_group, user=member, perm=perm
179 179 )
180 180 else:
181 181 ReposGroupModel().grant_users_group_permission(
182 182 repos_group=repos_group, group_name=member, perm=perm
183 183 )
184 184
185 185 old_path = repos_group.full_path
186 186
187 187 # change properties
188 188 repos_group.group_description = form_data['group_description']
189 189 repos_group.parent_group = RepoGroup.get(form_data['group_parent_id'])
190 190 repos_group.group_parent_id = form_data['group_parent_id']
191 191 repos_group.group_name = repos_group.get_new_name(form_data['group_name'])
192 192 new_path = repos_group.full_path
193 193
194 194 self.sa.add(repos_group)
195 195
196 196 # we need to get all repositories from this new group and
197 197 # rename them accordingly to new group path
198 198 for r in repos_group.repositories:
199 199 r.repo_name = r.get_new_name(r.just_name)
200 200 self.sa.add(r)
201 201
202 202 self.__rename_group(old_path, new_path)
203 203
204 204 return repos_group
205 205 except:
206 206 log.error(traceback.format_exc())
207 207 raise
208 208
209 209 def delete(self, users_group_id):
210 210 try:
211 211 users_group = RepoGroup.get(users_group_id)
212 212 self.sa.delete(users_group)
213 213 self.__delete_group(users_group)
214 214 except:
215 215 log.error(traceback.format_exc())
216 216 raise
217 217
218 218 def grant_user_permission(self, repos_group, user, perm):
219 219 """
220 220 Grant permission for user on given repositories group, or update
221 221 existing one if found
222 222
223 223 :param repos_group: Instance of ReposGroup, repositories_group_id,
224 224 or repositories_group name
225 225 :param user: Instance of User, user_id or username
226 226 :param perm: Instance of Permission, or permission_name
227 227 """
228 228
229 229 repos_group = self.__get_repos_group(repos_group)
230 230 user = self.__get_user(user)
231 231 permission = self.__get_perm(perm)
232 232
233 233 # check if we have that permission already
234 234 obj = self.sa.query(UserRepoGroupToPerm)\
235 235 .filter(UserRepoGroupToPerm.user == user)\
236 236 .filter(UserRepoGroupToPerm.group == repos_group)\
237 237 .scalar()
238 238 if obj is None:
239 239 # create new !
240 240 obj = UserRepoGroupToPerm()
241 241 obj.group = repos_group
242 242 obj.user = user
243 243 obj.permission = permission
244 244 self.sa.add(obj)
245 245
246 246 def revoke_user_permission(self, repos_group, user):
247 247 """
248 248 Revoke permission for user on given repositories group
249 249
250 250 :param repos_group: Instance of ReposGroup, repositories_group_id,
251 251 or repositories_group name
252 252 :param user: Instance of User, user_id or username
253 253 """
254 254
255 255 repos_group = self.__get_repos_group(repos_group)
256 256 user = self.__get_user(user)
257 257
258 258 obj = self.sa.query(UserRepoGroupToPerm)\
259 259 .filter(UserRepoGroupToPerm.user == user)\
260 260 .filter(UserRepoGroupToPerm.group == repos_group)\
261 261 .one()
262 262 self.sa.delete(obj)
263 263
264 264 def grant_users_group_permission(self, repos_group, group_name, perm):
265 265 """
266 266 Grant permission for users group on given repositories group, or update
267 267 existing one if found
268 268
269 269 :param repos_group: Instance of ReposGroup, repositories_group_id,
270 270 or repositories_group name
271 271 :param group_name: Instance of UserGroup, users_group_id,
272 272 or users group name
273 273 :param perm: Instance of Permission, or permission_name
274 274 """
275 275 repos_group = self.__get_repos_group(repos_group)
276 276 group_name = self.__get_users_group(group_name)
277 277 permission = self.__get_perm(perm)
278 278
279 279 # check if we have that permission already
280 280 obj = self.sa.query(UsersGroupRepoGroupToPerm)\
281 281 .filter(UsersGroupRepoGroupToPerm.group == repos_group)\
282 282 .filter(UsersGroupRepoGroupToPerm.users_group == group_name)\
283 283 .scalar()
284 284
285 285 if obj is None:
286 286 # create new
287 287 obj = UsersGroupRepoGroupToPerm()
288 288
289 289 obj.group = repos_group
290 290 obj.users_group = group_name
291 291 obj.permission = permission
292 292 self.sa.add(obj)
293 293
294 294 def revoke_users_group_permission(self, repos_group, group_name):
295 295 """
296 296 Revoke permission for users group on given repositories group
297 297
298 298 :param repos_group: Instance of ReposGroup, repositories_group_id,
299 299 or repositories_group name
300 300 :param group_name: Instance of UserGroup, users_group_id,
301 301 or users group name
302 302 """
303 303 repos_group = self.__get_repos_group(repos_group)
304 304 group_name = self.__get_users_group(group_name)
305 305
306 306 obj = self.sa.query(UsersGroupRepoGroupToPerm)\
307 307 .filter(UsersGroupRepoGroupToPerm.group == repos_group)\
308 308 .filter(UsersGroupRepoGroupToPerm.users_group == group_name)\
309 309 .one()
310 310 self.sa.delete(obj)
@@ -1,459 +1,459 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.scm
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Scm model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import os
26 26 import time
27 27 import traceback
28 28 import logging
29 29 import cStringIO
30 30
31 31 from rhodecode.lib.vcs import get_backend
32 32 from rhodecode.lib.vcs.exceptions import RepositoryError
33 33 from rhodecode.lib.vcs.utils.lazy import LazyProperty
34 34 from rhodecode.lib.vcs.nodes import FileNode
35 35
36 36 from rhodecode import BACKENDS
37 37 from rhodecode.lib import helpers as h
38 from rhodecode.lib import safe_str
38 from rhodecode.lib.utils2 import safe_str
39 39 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny
40 40 from rhodecode.lib.utils import get_repos as get_filesystem_repos, make_ui, \
41 41 action_logger, EmptyChangeset, REMOVED_REPO_PAT
42 42 from rhodecode.model import BaseModel
43 43 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
44 44 UserFollowing, UserLog, User, RepoGroup
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class UserTemp(object):
50 50 def __init__(self, user_id):
51 51 self.user_id = user_id
52 52
53 53 def __repr__(self):
54 54 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
55 55
56 56
57 57 class RepoTemp(object):
58 58 def __init__(self, repo_id):
59 59 self.repo_id = repo_id
60 60
61 61 def __repr__(self):
62 62 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
63 63
64 64
65 65 class CachedRepoList(object):
66 66
67 67 def __init__(self, db_repo_list, repos_path, order_by=None):
68 68 self.db_repo_list = db_repo_list
69 69 self.repos_path = repos_path
70 70 self.order_by = order_by
71 71 self.reversed = (order_by or '').startswith('-')
72 72
73 73 def __len__(self):
74 74 return len(self.db_repo_list)
75 75
76 76 def __repr__(self):
77 77 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
78 78
79 79 def __iter__(self):
80 80 for dbr in self.db_repo_list:
81 81 scmr = dbr.scm_instance_cached
82 82 # check permission at this level
83 83 if not HasRepoPermissionAny(
84 84 'repository.read', 'repository.write', 'repository.admin'
85 85 )(dbr.repo_name, 'get repo check'):
86 86 continue
87 87
88 88 if scmr is None:
89 89 log.error(
90 90 '%s this repository is present in database but it '
91 91 'cannot be created as an scm instance' % dbr.repo_name
92 92 )
93 93 continue
94 94
95 95 last_change = scmr.last_change
96 96 tip = h.get_changeset_safe(scmr, 'tip')
97 97
98 98 tmp_d = {}
99 99 tmp_d['name'] = dbr.repo_name
100 100 tmp_d['name_sort'] = tmp_d['name'].lower()
101 101 tmp_d['description'] = dbr.description
102 102 tmp_d['description_sort'] = tmp_d['description']
103 103 tmp_d['last_change'] = last_change
104 104 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
105 105 tmp_d['tip'] = tip.raw_id
106 106 tmp_d['tip_sort'] = tip.revision
107 107 tmp_d['rev'] = tip.revision
108 108 tmp_d['contact'] = dbr.user.full_contact
109 109 tmp_d['contact_sort'] = tmp_d['contact']
110 110 tmp_d['owner_sort'] = tmp_d['contact']
111 111 tmp_d['repo_archives'] = list(scmr._get_archives())
112 112 tmp_d['last_msg'] = tip.message
113 113 tmp_d['author'] = tip.author
114 114 tmp_d['dbrepo'] = dbr.get_dict()
115 115 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
116 116 yield tmp_d
117 117
118 118
119 119 class GroupList(object):
120 120
121 121 def __init__(self, db_repo_group_list):
122 122 self.db_repo_group_list = db_repo_group_list
123 123
124 124 def __len__(self):
125 125 return len(self.db_repo_group_list)
126 126
127 127 def __repr__(self):
128 128 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
129 129
130 130 def __iter__(self):
131 131 for dbgr in self.db_repo_group_list:
132 132 # check permission at this level
133 133 if not HasReposGroupPermissionAny(
134 134 'group.read', 'group.write', 'group.admin'
135 135 )(dbgr.group_name, 'get group repo check'):
136 136 continue
137 137
138 138 yield dbgr
139 139
140 140
141 141 class ScmModel(BaseModel):
142 142 """
143 143 Generic Scm Model
144 144 """
145 145
146 146 def __get_repo(self, instance):
147 147 cls = Repository
148 148 if isinstance(instance, cls):
149 149 return instance
150 150 elif isinstance(instance, int) or str(instance).isdigit():
151 151 return cls.get(instance)
152 152 elif isinstance(instance, basestring):
153 153 return cls.get_by_repo_name(instance)
154 154 elif instance:
155 155 raise Exception('given object must be int, basestr or Instance'
156 156 ' of %s got %s' % (type(cls), type(instance)))
157 157
158 158 @LazyProperty
159 159 def repos_path(self):
160 160 """
161 161 Get's the repositories root path from database
162 162 """
163 163
164 164 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
165 165
166 166 return q.ui_value
167 167
168 168 def repo_scan(self, repos_path=None):
169 169 """
170 170 Listing of repositories in given path. This path should not be a
171 171 repository itself. Return a dictionary of repository objects
172 172
173 173 :param repos_path: path to directory containing repositories
174 174 """
175 175
176 176 if repos_path is None:
177 177 repos_path = self.repos_path
178 178
179 179 log.info('scanning for repositories in %s' % repos_path)
180 180
181 181 baseui = make_ui('db')
182 182 repos = {}
183 183
184 184 for name, path in get_filesystem_repos(repos_path, recursive=True):
185 185 # skip removed repos
186 186 if REMOVED_REPO_PAT.match(name):
187 187 continue
188 188
189 189 # name need to be decomposed and put back together using the /
190 190 # since this is internal storage separator for rhodecode
191 191 name = Repository.url_sep().join(name.split(os.sep))
192 192
193 193 try:
194 194 if name in repos:
195 195 raise RepositoryError('Duplicate repository name %s '
196 196 'found in %s' % (name, path))
197 197 else:
198 198
199 199 klass = get_backend(path[0])
200 200
201 201 if path[0] == 'hg' and path[0] in BACKENDS.keys():
202 202 repos[name] = klass(safe_str(path[1]), baseui=baseui)
203 203
204 204 if path[0] == 'git' and path[0] in BACKENDS.keys():
205 205 repos[name] = klass(path[1])
206 206 except OSError:
207 207 continue
208 208
209 209 return repos
210 210
211 211 def get_repos(self, all_repos=None, sort_key=None):
212 212 """
213 213 Get all repos from db and for each repo create it's
214 214 backend instance and fill that backed with information from database
215 215
216 216 :param all_repos: list of repository names as strings
217 217 give specific repositories list, good for filtering
218 218 """
219 219 if all_repos is None:
220 220 all_repos = self.sa.query(Repository)\
221 221 .filter(Repository.group_id == None)\
222 222 .order_by(Repository.repo_name).all()
223 223
224 224 repo_iter = CachedRepoList(all_repos, repos_path=self.repos_path,
225 225 order_by=sort_key)
226 226
227 227 return repo_iter
228 228
229 229 def get_repos_groups(self, all_groups=None):
230 230 if all_groups is None:
231 231 all_groups = RepoGroup.query()\
232 232 .filter(RepoGroup.group_parent_id == None).all()
233 233 group_iter = GroupList(all_groups)
234 234
235 235 return group_iter
236 236
237 237 def mark_for_invalidation(self, repo_name):
238 238 """Puts cache invalidation task into db for
239 239 further global cache invalidation
240 240
241 241 :param repo_name: this repo that should invalidation take place
242 242 """
243 243 CacheInvalidation.set_invalidate(repo_name)
244 244 CacheInvalidation.set_invalidate(repo_name + "_README")
245 245
246 246 def toggle_following_repo(self, follow_repo_id, user_id):
247 247
248 248 f = self.sa.query(UserFollowing)\
249 249 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
250 250 .filter(UserFollowing.user_id == user_id).scalar()
251 251
252 252 if f is not None:
253 253 try:
254 254 self.sa.delete(f)
255 255 action_logger(UserTemp(user_id),
256 256 'stopped_following_repo',
257 257 RepoTemp(follow_repo_id))
258 258 return
259 259 except:
260 260 log.error(traceback.format_exc())
261 261 raise
262 262
263 263 try:
264 264 f = UserFollowing()
265 265 f.user_id = user_id
266 266 f.follows_repo_id = follow_repo_id
267 267 self.sa.add(f)
268 268
269 269 action_logger(UserTemp(user_id),
270 270 'started_following_repo',
271 271 RepoTemp(follow_repo_id))
272 272 except:
273 273 log.error(traceback.format_exc())
274 274 raise
275 275
276 276 def toggle_following_user(self, follow_user_id, user_id):
277 277 f = self.sa.query(UserFollowing)\
278 278 .filter(UserFollowing.follows_user_id == follow_user_id)\
279 279 .filter(UserFollowing.user_id == user_id).scalar()
280 280
281 281 if f is not None:
282 282 try:
283 283 self.sa.delete(f)
284 284 return
285 285 except:
286 286 log.error(traceback.format_exc())
287 287 raise
288 288
289 289 try:
290 290 f = UserFollowing()
291 291 f.user_id = user_id
292 292 f.follows_user_id = follow_user_id
293 293 self.sa.add(f)
294 294 except:
295 295 log.error(traceback.format_exc())
296 296 raise
297 297
298 298 def is_following_repo(self, repo_name, user_id, cache=False):
299 299 r = self.sa.query(Repository)\
300 300 .filter(Repository.repo_name == repo_name).scalar()
301 301
302 302 f = self.sa.query(UserFollowing)\
303 303 .filter(UserFollowing.follows_repository == r)\
304 304 .filter(UserFollowing.user_id == user_id).scalar()
305 305
306 306 return f is not None
307 307
308 308 def is_following_user(self, username, user_id, cache=False):
309 309 u = User.get_by_username(username)
310 310
311 311 f = self.sa.query(UserFollowing)\
312 312 .filter(UserFollowing.follows_user == u)\
313 313 .filter(UserFollowing.user_id == user_id).scalar()
314 314
315 315 return f is not None
316 316
317 317 def get_followers(self, repo_id):
318 318 if not isinstance(repo_id, int):
319 319 repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
320 320
321 321 return self.sa.query(UserFollowing)\
322 322 .filter(UserFollowing.follows_repo_id == repo_id).count()
323 323
324 324 def get_forks(self, repo_id):
325 325 if not isinstance(repo_id, int):
326 326 repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
327 327
328 328 return self.sa.query(Repository)\
329 329 .filter(Repository.fork_id == repo_id).count()
330 330
331 331 def mark_as_fork(self, repo, fork, user):
332 332 repo = self.__get_repo(repo)
333 333 fork = self.__get_repo(fork)
334 334 repo.fork = fork
335 335 self.sa.add(repo)
336 336 return repo
337 337
338 338 def pull_changes(self, repo_name, username):
339 339 dbrepo = Repository.get_by_repo_name(repo_name)
340 340 clone_uri = dbrepo.clone_uri
341 341 if not clone_uri:
342 342 raise Exception("This repository doesn't have a clone uri")
343 343
344 344 repo = dbrepo.scm_instance
345 345 try:
346 346 extras = {'ip': '',
347 347 'username': username,
348 348 'action': 'push_remote',
349 349 'repository': repo_name}
350 350
351 351 #inject ui extra param to log this action via push logger
352 352 for k, v in extras.items():
353 353 repo._repo.ui.setconfig('rhodecode_extras', k, v)
354 354
355 355 repo.pull(clone_uri)
356 356 self.mark_for_invalidation(repo_name)
357 357 except:
358 358 log.error(traceback.format_exc())
359 359 raise
360 360
361 361 def commit_change(self, repo, repo_name, cs, user, author, message,
362 362 content, f_path):
363 363
364 364 if repo.alias == 'hg':
365 365 from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC
366 366 elif repo.alias == 'git':
367 367 from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC
368 368
369 369 # decoding here will force that we have proper encoded values
370 370 # in any other case this will throw exceptions and deny commit
371 371 content = safe_str(content)
372 372 message = safe_str(message)
373 373 path = safe_str(f_path)
374 374 author = safe_str(author)
375 375 m = IMC(repo)
376 376 m.change(FileNode(path, content))
377 377 tip = m.commit(message=message,
378 378 author=author,
379 379 parents=[cs], branch=cs.branch)
380 380
381 381 new_cs = tip.short_id
382 382 action = 'push_local:%s' % new_cs
383 383
384 384 action_logger(user, action, repo_name)
385 385
386 386 self.mark_for_invalidation(repo_name)
387 387
388 388 def create_node(self, repo, repo_name, cs, user, author, message, content,
389 389 f_path):
390 390 if repo.alias == 'hg':
391 391 from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC
392 392 elif repo.alias == 'git':
393 393 from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC
394 394 # decoding here will force that we have proper encoded values
395 395 # in any other case this will throw exceptions and deny commit
396 396
397 397 if isinstance(content, (basestring,)):
398 398 content = safe_str(content)
399 399 elif isinstance(content, (file, cStringIO.OutputType,)):
400 400 content = content.read()
401 401 else:
402 402 raise Exception('Content is of unrecognized type %s' % (
403 403 type(content)
404 404 ))
405 405
406 406 message = safe_str(message)
407 407 path = safe_str(f_path)
408 408 author = safe_str(author)
409 409 m = IMC(repo)
410 410
411 411 if isinstance(cs, EmptyChangeset):
412 412 # Emptychangeset means we we're editing empty repository
413 413 parents = None
414 414 else:
415 415 parents = [cs]
416 416
417 417 m.add(FileNode(path, content=content))
418 418 tip = m.commit(message=message,
419 419 author=author,
420 420 parents=parents, branch=cs.branch)
421 421 new_cs = tip.short_id
422 422 action = 'push_local:%s' % new_cs
423 423
424 424 action_logger(user, action, repo_name)
425 425
426 426 self.mark_for_invalidation(repo_name)
427 427
428 428 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
429 429 """
430 430 recursive walk in root dir and return a set of all path in that dir
431 431 based on repository walk function
432 432
433 433 :param repo_name: name of repository
434 434 :param revision: revision for which to list nodes
435 435 :param root_path: root path to list
436 436 :param flat: return as a list, if False returns a dict with decription
437 437
438 438 """
439 439 _files = list()
440 440 _dirs = list()
441 441 try:
442 442 _repo = self.__get_repo(repo_name)
443 443 changeset = _repo.scm_instance.get_changeset(revision)
444 444 root_path = root_path.lstrip('/')
445 445 for topnode, dirs, files in changeset.walk(root_path):
446 446 for f in files:
447 447 _files.append(f.path if flat else {"name": f.path,
448 448 "type": "file"})
449 449 for d in dirs:
450 450 _dirs.append(d.path if flat else {"name": d.path,
451 451 "type": "dir"})
452 452 except RepositoryError:
453 453 log.debug(traceback.format_exc())
454 454 raise
455 455
456 456 return _dirs, _files
457 457
458 458 def get_unread_journal(self):
459 459 return self.sa.query(UserLog).count()
@@ -1,561 +1,561 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.user
4 4 ~~~~~~~~~~~~~~~~~~~~
5 5
6 6 users model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons import url
30 30 from pylons.i18n.translation import _
31 31
32 from rhodecode.lib import safe_unicode
32 from rhodecode.lib.utils2 import safe_unicode, generate_api_key
33 33 from rhodecode.lib.caching_query import FromCache
34 34
35 35 from rhodecode.model import BaseModel
36 36 from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \
37 37 UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember, \
38 38 Notification, RepoGroup, UserRepoGroupToPerm, UsersGroup
39 39 from rhodecode.lib.exceptions import DefaultUserException, \
40 40 UserOwnsReposException
41 41
42 42 from sqlalchemy.exc import DatabaseError
43 from rhodecode.lib import generate_api_key
43
44 44 from sqlalchemy.orm import joinedload
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 PERM_WEIGHTS = {
50 50 'repository.none': 0,
51 51 'repository.read': 1,
52 52 'repository.write': 3,
53 53 'repository.admin': 4,
54 54 'group.none': 0,
55 55 'group.read': 1,
56 56 'group.write': 3,
57 57 'group.admin': 4,
58 58 }
59 59
60 60
61 61 class UserModel(BaseModel):
62 62
63 63 def __get_user(self, user):
64 64 return self._get_instance(User, user, callback=User.get_by_username)
65 65
66 66 def __get_perm(self, permission):
67 67 return self._get_instance(Permission, permission,
68 68 callback=Permission.get_by_key)
69 69
70 70 def get(self, user_id, cache=False):
71 71 user = self.sa.query(User)
72 72 if cache:
73 73 user = user.options(FromCache("sql_cache_short",
74 74 "get_user_%s" % user_id))
75 75 return user.get(user_id)
76 76
77 77 def get_user(self, user):
78 78 return self.__get_user(user)
79 79
80 80 def get_by_username(self, username, cache=False, case_insensitive=False):
81 81
82 82 if case_insensitive:
83 83 user = self.sa.query(User).filter(User.username.ilike(username))
84 84 else:
85 85 user = self.sa.query(User)\
86 86 .filter(User.username == username)
87 87 if cache:
88 88 user = user.options(FromCache("sql_cache_short",
89 89 "get_user_%s" % username))
90 90 return user.scalar()
91 91
92 92 def get_by_api_key(self, api_key, cache=False):
93 93 return User.get_by_api_key(api_key, cache)
94 94
95 95 def create(self, form_data):
96 96 try:
97 97 new_user = User()
98 98 for k, v in form_data.items():
99 99 setattr(new_user, k, v)
100 100
101 101 new_user.api_key = generate_api_key(form_data['username'])
102 102 self.sa.add(new_user)
103 103 return new_user
104 104 except:
105 105 log.error(traceback.format_exc())
106 106 raise
107 107
108 108 def create_or_update(self, username, password, email, name, lastname,
109 109 active=True, admin=False, ldap_dn=None):
110 110 """
111 111 Creates a new instance if not found, or updates current one
112 112
113 113 :param username:
114 114 :param password:
115 115 :param email:
116 116 :param active:
117 117 :param name:
118 118 :param lastname:
119 119 :param active:
120 120 :param admin:
121 121 :param ldap_dn:
122 122 """
123 123
124 124 from rhodecode.lib.auth import get_crypt_password
125 125
126 126 log.debug('Checking for %s account in RhodeCode database' % username)
127 127 user = User.get_by_username(username, case_insensitive=True)
128 128 if user is None:
129 129 log.debug('creating new user %s' % username)
130 130 new_user = User()
131 131 else:
132 132 log.debug('updating user %s' % username)
133 133 new_user = user
134 134
135 135 try:
136 136 new_user.username = username
137 137 new_user.admin = admin
138 138 new_user.password = get_crypt_password(password)
139 139 new_user.api_key = generate_api_key(username)
140 140 new_user.email = email
141 141 new_user.active = active
142 142 new_user.ldap_dn = safe_unicode(ldap_dn) if ldap_dn else None
143 143 new_user.name = name
144 144 new_user.lastname = lastname
145 145 self.sa.add(new_user)
146 146 return new_user
147 147 except (DatabaseError,):
148 148 log.error(traceback.format_exc())
149 149 raise
150 150
151 151 def create_for_container_auth(self, username, attrs):
152 152 """
153 153 Creates the given user if it's not already in the database
154 154
155 155 :param username:
156 156 :param attrs:
157 157 """
158 158 if self.get_by_username(username, case_insensitive=True) is None:
159 159
160 160 # autogenerate email for container account without one
161 161 generate_email = lambda usr: '%s@container_auth.account' % usr
162 162
163 163 try:
164 164 new_user = User()
165 165 new_user.username = username
166 166 new_user.password = None
167 167 new_user.api_key = generate_api_key(username)
168 168 new_user.email = attrs['email']
169 169 new_user.active = attrs.get('active', True)
170 170 new_user.name = attrs['name'] or generate_email(username)
171 171 new_user.lastname = attrs['lastname']
172 172
173 173 self.sa.add(new_user)
174 174 return new_user
175 175 except (DatabaseError,):
176 176 log.error(traceback.format_exc())
177 177 self.sa.rollback()
178 178 raise
179 179 log.debug('User %s already exists. Skipping creation of account'
180 180 ' for container auth.', username)
181 181 return None
182 182
183 183 def create_ldap(self, username, password, user_dn, attrs):
184 184 """
185 185 Checks if user is in database, if not creates this user marked
186 186 as ldap user
187 187
188 188 :param username:
189 189 :param password:
190 190 :param user_dn:
191 191 :param attrs:
192 192 """
193 193 from rhodecode.lib.auth import get_crypt_password
194 194 log.debug('Checking for such ldap account in RhodeCode database')
195 195 if self.get_by_username(username, case_insensitive=True) is None:
196 196
197 197 # autogenerate email for ldap account without one
198 198 generate_email = lambda usr: '%s@ldap.account' % usr
199 199
200 200 try:
201 201 new_user = User()
202 202 username = username.lower()
203 203 # add ldap account always lowercase
204 204 new_user.username = username
205 205 new_user.password = get_crypt_password(password)
206 206 new_user.api_key = generate_api_key(username)
207 207 new_user.email = attrs['email'] or generate_email(username)
208 208 new_user.active = attrs.get('active', True)
209 209 new_user.ldap_dn = safe_unicode(user_dn)
210 210 new_user.name = attrs['name']
211 211 new_user.lastname = attrs['lastname']
212 212
213 213 self.sa.add(new_user)
214 214 return new_user
215 215 except (DatabaseError,):
216 216 log.error(traceback.format_exc())
217 217 self.sa.rollback()
218 218 raise
219 219 log.debug('this %s user exists skipping creation of ldap account',
220 220 username)
221 221 return None
222 222
223 223 def create_registration(self, form_data):
224 224 from rhodecode.model.notification import NotificationModel
225 225
226 226 try:
227 227 new_user = User()
228 228 for k, v in form_data.items():
229 229 if k != 'admin':
230 230 setattr(new_user, k, v)
231 231
232 232 self.sa.add(new_user)
233 233 self.sa.flush()
234 234
235 235 # notification to admins
236 236 subject = _('new user registration')
237 237 body = ('New user registration\n'
238 238 '---------------------\n'
239 239 '- Username: %s\n'
240 240 '- Full Name: %s\n'
241 241 '- Email: %s\n')
242 242 body = body % (new_user.username, new_user.full_name,
243 243 new_user.email)
244 244 edit_url = url('edit_user', id=new_user.user_id, qualified=True)
245 245 kw = {'registered_user_url': edit_url}
246 246 NotificationModel().create(created_by=new_user, subject=subject,
247 247 body=body, recipients=None,
248 248 type_=Notification.TYPE_REGISTRATION,
249 249 email_kwargs=kw)
250 250
251 251 except:
252 252 log.error(traceback.format_exc())
253 253 raise
254 254
255 255 def update(self, user_id, form_data):
256 256 try:
257 257 user = self.get(user_id, cache=False)
258 258 if user.username == 'default':
259 259 raise DefaultUserException(
260 260 _("You can't Edit this user since it's"
261 261 " crucial for entire application"))
262 262
263 263 for k, v in form_data.items():
264 264 if k == 'new_password' and v != '':
265 265 user.password = v
266 266 user.api_key = generate_api_key(user.username)
267 267 else:
268 268 setattr(user, k, v)
269 269
270 270 self.sa.add(user)
271 271 except:
272 272 log.error(traceback.format_exc())
273 273 raise
274 274
275 275 def update_my_account(self, user_id, form_data):
276 276 try:
277 277 user = self.get(user_id, cache=False)
278 278 if user.username == 'default':
279 279 raise DefaultUserException(
280 280 _("You can't Edit this user since it's"
281 281 " crucial for entire application"))
282 282 for k, v in form_data.items():
283 283 if k == 'new_password' and v != '':
284 284 user.password = v
285 285 user.api_key = generate_api_key(user.username)
286 286 else:
287 287 if k not in ['admin', 'active']:
288 288 setattr(user, k, v)
289 289
290 290 self.sa.add(user)
291 291 except:
292 292 log.error(traceback.format_exc())
293 293 raise
294 294
295 295 def delete(self, user):
296 296 user = self.__get_user(user)
297 297
298 298 try:
299 299 if user.username == 'default':
300 300 raise DefaultUserException(
301 301 _("You can't remove this user since it's"
302 302 " crucial for entire application"))
303 303 if user.repositories:
304 304 raise UserOwnsReposException(_('This user still owns %s '
305 305 'repositories and cannot be '
306 306 'removed. Switch owners or '
307 307 'remove those repositories') \
308 308 % user.repositories)
309 309 self.sa.delete(user)
310 310 except:
311 311 log.error(traceback.format_exc())
312 312 raise
313 313
314 314 def reset_password_link(self, data):
315 315 from rhodecode.lib.celerylib import tasks, run_task
316 316 run_task(tasks.send_password_link, data['email'])
317 317
318 318 def reset_password(self, data):
319 319 from rhodecode.lib.celerylib import tasks, run_task
320 320 run_task(tasks.reset_user_password, data['email'])
321 321
322 322 def fill_data(self, auth_user, user_id=None, api_key=None):
323 323 """
324 324 Fetches auth_user by user_id,or api_key if present.
325 325 Fills auth_user attributes with those taken from database.
326 326 Additionally set's is_authenitated if lookup fails
327 327 present in database
328 328
329 329 :param auth_user: instance of user to set attributes
330 330 :param user_id: user id to fetch by
331 331 :param api_key: api key to fetch by
332 332 """
333 333 if user_id is None and api_key is None:
334 334 raise Exception('You need to pass user_id or api_key')
335 335
336 336 try:
337 337 if api_key:
338 338 dbuser = self.get_by_api_key(api_key)
339 339 else:
340 340 dbuser = self.get(user_id)
341 341
342 342 if dbuser is not None and dbuser.active:
343 343 log.debug('filling %s data' % dbuser)
344 344 for k, v in dbuser.get_dict().items():
345 345 setattr(auth_user, k, v)
346 346 else:
347 347 return False
348 348
349 349 except:
350 350 log.error(traceback.format_exc())
351 351 auth_user.is_authenticated = False
352 352 return False
353 353
354 354 return True
355 355
356 356 def fill_perms(self, user):
357 357 """
358 358 Fills user permission attribute with permissions taken from database
359 359 works for permissions given for repositories, and for permissions that
360 360 are granted to groups
361 361
362 362 :param user: user instance to fill his perms
363 363 """
364 364 RK = 'repositories'
365 365 GK = 'repositories_groups'
366 366 GLOBAL = 'global'
367 367 user.permissions[RK] = {}
368 368 user.permissions[GK] = {}
369 369 user.permissions[GLOBAL] = set()
370 370
371 371 #======================================================================
372 372 # fetch default permissions
373 373 #======================================================================
374 374 default_user = User.get_by_username('default', cache=True)
375 375 default_user_id = default_user.user_id
376 376
377 377 default_repo_perms = Permission.get_default_perms(default_user_id)
378 378 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
379 379
380 380 if user.is_admin:
381 381 #==================================================================
382 382 # admin user have all default rights for repositories
383 383 # and groups set to admin
384 384 #==================================================================
385 385 user.permissions[GLOBAL].add('hg.admin')
386 386
387 387 # repositories
388 388 for perm in default_repo_perms:
389 389 r_k = perm.UserRepoToPerm.repository.repo_name
390 390 p = 'repository.admin'
391 391 user.permissions[RK][r_k] = p
392 392
393 393 # repositories groups
394 394 for perm in default_repo_groups_perms:
395 395 rg_k = perm.UserRepoGroupToPerm.group.group_name
396 396 p = 'group.admin'
397 397 user.permissions[GK][rg_k] = p
398 398
399 399 else:
400 400 #==================================================================
401 401 # set default permissions first for repositories and groups
402 402 #==================================================================
403 403 uid = user.user_id
404 404
405 405 # default global permissions
406 406 default_global_perms = self.sa.query(UserToPerm)\
407 407 .filter(UserToPerm.user_id == default_user_id)
408 408
409 409 for perm in default_global_perms:
410 410 user.permissions[GLOBAL].add(perm.permission.permission_name)
411 411
412 412 # default for repositories
413 413 for perm in default_repo_perms:
414 414 r_k = perm.UserRepoToPerm.repository.repo_name
415 415 if perm.Repository.private and not (perm.Repository.user_id == uid):
416 416 # disable defaults for private repos,
417 417 p = 'repository.none'
418 418 elif perm.Repository.user_id == uid:
419 419 # set admin if owner
420 420 p = 'repository.admin'
421 421 else:
422 422 p = perm.Permission.permission_name
423 423
424 424 user.permissions[RK][r_k] = p
425 425
426 426 # default for repositories groups
427 427 for perm in default_repo_groups_perms:
428 428 rg_k = perm.UserRepoGroupToPerm.group.group_name
429 429 p = perm.Permission.permission_name
430 430 user.permissions[GK][rg_k] = p
431 431
432 432 #==================================================================
433 433 # overwrite default with user permissions if any
434 434 #==================================================================
435 435
436 436 # user global
437 437 user_perms = self.sa.query(UserToPerm)\
438 438 .options(joinedload(UserToPerm.permission))\
439 439 .filter(UserToPerm.user_id == uid).all()
440 440
441 441 for perm in user_perms:
442 442 user.permissions[GLOBAL].add(perm.permission.permission_name)
443 443
444 444 # user repositories
445 445 user_repo_perms = \
446 446 self.sa.query(UserRepoToPerm, Permission, Repository)\
447 447 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
448 448 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
449 449 .filter(UserRepoToPerm.user_id == uid)\
450 450 .all()
451 451
452 452 for perm in user_repo_perms:
453 453 # set admin if owner
454 454 r_k = perm.UserRepoToPerm.repository.repo_name
455 455 if perm.Repository.user_id == uid:
456 456 p = 'repository.admin'
457 457 else:
458 458 p = perm.Permission.permission_name
459 459 user.permissions[RK][r_k] = p
460 460
461 461 #==================================================================
462 462 # check if user is part of groups for this repository and fill in
463 463 # (or replace with higher) permissions
464 464 #==================================================================
465 465
466 466 # users group global
467 467 user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\
468 468 .options(joinedload(UsersGroupToPerm.permission))\
469 469 .join((UsersGroupMember, UsersGroupToPerm.users_group_id ==
470 470 UsersGroupMember.users_group_id))\
471 471 .filter(UsersGroupMember.user_id == uid).all()
472 472
473 473 for perm in user_perms_from_users_groups:
474 474 user.permissions[GLOBAL].add(perm.permission.permission_name)
475 475
476 476 # users group repositories
477 477 user_repo_perms_from_users_groups = \
478 478 self.sa.query(UsersGroupRepoToPerm, Permission, Repository,)\
479 479 .join((Repository, UsersGroupRepoToPerm.repository_id == Repository.repo_id))\
480 480 .join((Permission, UsersGroupRepoToPerm.permission_id == Permission.permission_id))\
481 481 .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id == UsersGroupMember.users_group_id))\
482 482 .filter(UsersGroupMember.user_id == uid)\
483 483 .all()
484 484
485 485 for perm in user_repo_perms_from_users_groups:
486 486 r_k = perm.UsersGroupRepoToPerm.repository.repo_name
487 487 p = perm.Permission.permission_name
488 488 cur_perm = user.permissions[RK][r_k]
489 489 # overwrite permission only if it's greater than permission
490 490 # given from other sources
491 491 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
492 492 user.permissions[RK][r_k] = p
493 493
494 494 #==================================================================
495 495 # get access for this user for repos group and override defaults
496 496 #==================================================================
497 497
498 498 # user repositories groups
499 499 user_repo_groups_perms = \
500 500 self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\
501 501 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
502 502 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
503 503 .filter(UserRepoToPerm.user_id == uid)\
504 504 .all()
505 505
506 506 for perm in user_repo_groups_perms:
507 507 rg_k = perm.UserRepoGroupToPerm.group.group_name
508 508 p = perm.Permission.permission_name
509 509 cur_perm = user.permissions[GK][rg_k]
510 510 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
511 511 user.permissions[GK][rg_k] = p
512 512
513 513 return user
514 514
515 515 def has_perm(self, user, perm):
516 516 if not isinstance(perm, Permission):
517 517 raise Exception('perm needs to be an instance of Permission class '
518 518 'got %s instead' % type(perm))
519 519
520 520 user = self.__get_user(user)
521 521
522 522 return UserToPerm.query().filter(UserToPerm.user == user)\
523 523 .filter(UserToPerm.permission == perm).scalar() is not None
524 524
525 525 def grant_perm(self, user, perm):
526 526 """
527 527 Grant user global permissions
528 528
529 529 :param user:
530 530 :param perm:
531 531 """
532 532 user = self.__get_user(user)
533 533 perm = self.__get_perm(perm)
534 534 # if this permission is already granted skip it
535 535 _perm = UserToPerm.query()\
536 536 .filter(UserToPerm.user == user)\
537 537 .filter(UserToPerm.permission == perm)\
538 538 .scalar()
539 539 if _perm:
540 540 return
541 541 new = UserToPerm()
542 542 new.user = user
543 543 new.permission = perm
544 544 self.sa.add(new)
545 545
546 546 def revoke_perm(self, user, perm):
547 547 """
548 548 Revoke users global permissions
549 549
550 550 :param user:
551 551 :param perm:
552 552 """
553 553 user = self.__get_user(user)
554 554 perm = self.__get_perm(perm)
555 555
556 556 obj = UserToPerm.query()\
557 557 .filter(UserToPerm.user == user)\
558 558 .filter(UserToPerm.permission == perm)\
559 559 .scalar()
560 560 if obj:
561 561 self.sa.delete(obj)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now