##// END OF EJS Templates
merged with beta branch
marcink -
r857:3d0661b8 merge default
parent child Browse files
Show More

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

@@ -0,0 +1,10
1 .. _api:
2
3 API Reference
4 =============
5
6 .. toctree::
7 :maxdepth: 3
8
9 models
10 No newline at end of file
@@ -0,0 +1,19
1 .. _models:
2
3 The :mod:`models` Module
4 ========================
5
6 .. automodule:: rhodecode.model
7 :members:
8
9 .. automodule:: rhodecode.model.permission
10 :members:
11
12 .. automodule:: rhodecode.model.repo
13 :members:
14
15 .. automodule:: rhodecode.model.scm
16 :members:
17
18 .. automodule:: rhodecode.model.user
19 :members:
@@ -0,0 +1,9
1 .. _contributing:
2
3 Contributing in RhodeCode
4 =========================
5
6 If You would like to contribute to RhodeCode, please contact me, any help is
7 greatly appreciated.
8
9 Thank You.
@@ -0,0 +1,21
1 .. _enable_git:
2
3 Enabling GIT support (beta)
4 ===========================
5
6
7 Git support in RhodeCode 1.1 was disabled due to some instability issues, but
8 If You would like to test it fell free to re-enable it. To enable GIT just
9 uncomment git line in rhodecode/__init__.py file
10
11 .. code-block:: python
12
13 BACKENDS = {
14 'hg': 'Mercurial repository',
15 #'git': 'Git repository',
16 }
17
18 .. note::
19 Please note that it's not fully stable and it might crash (that's why it
20 was disabled), so be careful about enabling git support. Don't use it in
21 production ! No newline at end of file
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,13
1 .. _screenshots:
2
3 .. figure:: images/screenshot1_main_page.png
4
5 Main page of RhodeCode
6
7 .. figure:: images/screenshot2_summary_page.png
8
9 Summary page
10
11 .. figure:: images/screenshot3_changelog_page.png
12
13 Changelog with DAG graph No newline at end of file
@@ -0,0 +1,32
1 .. _statistics:
2
3
4 Statistics
5 ==========
6
7 RhodeCode statistics system is heavy on resources, so in order to keep a
8 balance between the usability and performance statistics are cached inside db
9 and are gathered incrementally, this is how RhodeCode does this:
10
11 With Celery disabled
12 ++++++++++++++++++++
13
14 - on each first visit on summary page a set of 250 commits are parsed and
15 updates statistics cache
16 - this happens on each single visit of statistics page until all commits are
17 fetched. Statistics are kept cached until some more commits are added to
18 repository, in such case RhodeCode will fetch only the ones added and will
19 update it's cache.
20
21
22 With Celery enabled
23 +++++++++++++++++++
24
25 - on first visit on summary page RhodeCode will create task that will execute
26 on celery workers, that will gather all stats until all commits are parsed,
27 each task will parse 250 commits, and run next task to parse next 250
28 commits, until all are parsed.
29
30 .. note::
31 In any time You can disable statistics on each repository in repository edit
32 form in admin panel, just uncheck the statistics checkbox. No newline at end of file
@@ -0,0 +1,14
1 {% extends "basic/layout.html" %}
2
3 {% block sidebarlogo %}
4 <h3>Support my development effort.</h3>
5 <div style="text-align:center">
6 <form action="https://www.paypal.com/cgi-bin/webscr" method="post">
7 <input type="hidden" name="cmd" value="_s-xclick">
8 <input type="hidden" name="hosted_button_id" value="8U2LLRPLBKWDU">
9 <input style="border:0px !important" type="image" src="https://www.paypal.com/en_US/i/btn/btn_donate_SM.gif"
10 border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">
11 <img alt="" border="0" src="https://www.paypal.com/en_US/i/scr/pixel.gif" width="1" height="1">
12 </form>
13 </div>
14 {% endblock %}}
@@ -0,0 +1,106
1 # -*- coding: utf-8 -*-
2 """
3 package.rhodecode.controllers.admin.ldap_settings
4 ~~~~~~~~~~~~~~
5
6 ldap controller for RhodeCode
7 :created_on: Nov 26, 2010
8 :author: marcink
9 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
10 :license: GPLv3, see COPYING for more details.
11 """
12 # This program is free software; you can redistribute it and/or
13 # modify it under the terms of the GNU General Public License
14 # as published by the Free Software Foundation; version 2
15 # of the License or (at your opinion) any later version of the license.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # MA 02110-1301, USA.
26 import logging
27 import formencode
28 import traceback
29
30 from formencode import htmlfill
31
32 from pylons import request, response, session, tmpl_context as c, url
33 from pylons.controllers.util import abort, redirect
34 from pylons.i18n.translation import _
35
36 from rhodecode.lib.base import BaseController, render
37 from rhodecode.lib import helpers as h
38 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
39 from rhodecode.lib.auth_ldap import LdapImportError
40 from rhodecode.model.settings import SettingsModel
41 from rhodecode.model.forms import LdapSettingsForm
42 from sqlalchemy.exc import DatabaseError
43
44 log = logging.getLogger(__name__)
45
46
47
48 class LdapSettingsController(BaseController):
49
50 @LoginRequired()
51 @HasPermissionAllDecorator('hg.admin')
52 def __before__(self):
53 c.admin_user = session.get('admin_user')
54 c.admin_username = session.get('admin_username')
55 super(LdapSettingsController, self).__before__()
56
57 def index(self):
58 defaults = SettingsModel().get_ldap_settings()
59
60 return htmlfill.render(
61 render('admin/ldap/ldap.html'),
62 defaults=defaults,
63 encoding="UTF-8",
64 force_defaults=True,)
65
66 def ldap_settings(self):
67 """
68 POST ldap create and store ldap settings
69 """
70
71 settings_model = SettingsModel()
72 _form = LdapSettingsForm()()
73
74 try:
75 form_result = _form.to_python(dict(request.POST))
76 try:
77
78 for k, v in form_result.items():
79 if k.startswith('ldap_'):
80 setting = settings_model.get(k)
81 setting.app_settings_value = v
82 self.sa.add(setting)
83
84 self.sa.commit()
85 h.flash(_('Ldap settings updated successfully'),
86 category='success')
87 except (DatabaseError,):
88 raise
89 except LdapImportError:
90 h.flash(_('Unable to activate ldap. The "python-ldap" library '
91 'is missing.'), category='warning')
92
93 except formencode.Invalid, errors:
94
95 return htmlfill.render(
96 render('admin/ldap/ldap.html'),
97 defaults=errors.value,
98 errors=errors.error_dict or {},
99 prefix_error=False,
100 encoding="UTF-8")
101 except Exception:
102 log.error(traceback.format_exc())
103 h.flash(_('error occured during update of ldap settings'),
104 category='error')
105
106 return redirect(url('ldap_home'))
@@ -0,0 +1,93
1 #!/usr/bin/env python
2 # encoding: utf-8
3 # journal controller for pylons
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
9 # of the License or (at your opinion) any later version of the license.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
20 """
21 Created on November 21, 2010
22 journal controller for pylons
23 @author: marcink
24 """
25
26 from pylons import request, response, session, tmpl_context as c, url
27 from pylons.controllers.util import abort, redirect
28 from rhodecode.lib.auth import LoginRequired, NotAnonymous
29 from rhodecode.lib.base import BaseController, render
30 from rhodecode.lib.helpers import get_token
31 from rhodecode.model.db import UserLog, UserFollowing
32 from rhodecode.model.scm import ScmModel
33 from sqlalchemy import or_
34 import logging
35 from paste.httpexceptions import HTTPInternalServerError, HTTPNotFound
36
37 log = logging.getLogger(__name__)
38
39 class JournalController(BaseController):
40
41
42 @LoginRequired()
43 @NotAnonymous()
44 def __before__(self):
45 super(JournalController, self).__before__()
46
47 def index(self):
48 # Return a rendered template
49
50 c.following = self.sa.query(UserFollowing)\
51 .filter(UserFollowing.user_id == c.rhodecode_user.user_id).all()
52
53 repo_ids = [x.follows_repository.repo_id for x in c.following
54 if x.follows_repository is not None]
55 user_ids = [x.follows_user.user_id for x in c.following
56 if x.follows_user is not None]
57
58 c.journal = self.sa.query(UserLog)\
59 .filter(or_(
60 UserLog.repository_id.in_(repo_ids),
61 UserLog.user_id.in_(user_ids),
62 ))\
63 .order_by(UserLog.action_date.desc())\
64 .limit(20)\
65 .all()
66 return render('/journal.html')
67
68 def toggle_following(self):
69
70 if request.POST.get('auth_token') == get_token():
71 scm_model = ScmModel()
72
73 user_id = request.POST.get('follows_user_id')
74 if user_id:
75 try:
76 scm_model.toggle_following_user(user_id,
77 c.rhodecode_user.user_id)
78 return 'ok'
79 except:
80 raise HTTPInternalServerError()
81
82 repo_id = request.POST.get('follows_repo_id')
83 if repo_id:
84 try:
85 scm_model.toggle_following_repo(repo_id,
86 c.rhodecode_user.user_id)
87 return 'ok'
88 except:
89 raise HTTPInternalServerError()
90
91
92
93 raise HTTPInternalServerError()
@@ -0,0 +1,104
1 #!/usr/bin/env python
2 # encoding: utf-8
3 # ldap authentication lib
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
9 # of the License or (at your opinion) any later version of the license.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
20 """
21 Created on Nov 17, 2010
22
23 @author: marcink
24 """
25
26 from rhodecode.lib.exceptions import *
27 import logging
28
29 log = logging.getLogger(__name__)
30
31 try:
32 import ldap
33 except ImportError:
34 pass
35
36 class AuthLdap(object):
37
38 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
39 use_ldaps=False, ldap_version=3):
40 self.ldap_version = ldap_version
41 if use_ldaps:
42 port = port or 689
43 self.LDAP_USE_LDAPS = use_ldaps
44 self.LDAP_SERVER_ADDRESS = server
45 self.LDAP_SERVER_PORT = port
46
47 #USE FOR READ ONLY BIND TO LDAP SERVER
48 self.LDAP_BIND_DN = bind_dn
49 self.LDAP_BIND_PASS = bind_pass
50
51 ldap_server_type = 'ldap'
52 if self.LDAP_USE_LDAPS:ldap_server_type = ldap_server_type + 's'
53 self.LDAP_SERVER = "%s://%s:%s" % (ldap_server_type,
54 self.LDAP_SERVER_ADDRESS,
55 self.LDAP_SERVER_PORT)
56
57 self.BASE_DN = base_dn
58
59 def authenticate_ldap(self, username, password):
60 """Authenticate a user via LDAP and return his/her LDAP properties.
61
62 Raises AuthenticationError if the credentials are rejected, or
63 EnvironmentError if the LDAP server can't be reached.
64
65 :param username: username
66 :param password: password
67 """
68
69 from rhodecode.lib.helpers import chop_at
70
71 uid = chop_at(username, "@%s" % self.LDAP_SERVER_ADDRESS)
72
73 if "," in username:
74 raise LdapUsernameError("invalid character in username: ,")
75 try:
76 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, '/etc/openldap/cacerts')
77 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
78 server = ldap.initialize(self.LDAP_SERVER)
79 if self.ldap_version == 2:
80 server.protocol = ldap.VERSION2
81 else:
82 server.protocol = ldap.VERSION3
83
84 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
85 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
86
87 dn = self.BASE_DN % {'user':uid}
88 log.debug("Authenticating %r at %s", dn, self.LDAP_SERVER)
89 server.simple_bind_s(dn, password)
90
91 properties = server.search_s(dn, ldap.SCOPE_SUBTREE)
92 if not properties:
93 raise ldap.NO_SUCH_OBJECT()
94 except ldap.NO_SUCH_OBJECT, e:
95 log.debug("LDAP says no such user '%s' (%s)", uid, username)
96 raise LdapUsernameError()
97 except ldap.INVALID_CREDENTIALS, e:
98 log.debug("LDAP rejected password for user '%s' (%s)", uid, username)
99 raise LdapPasswordError()
100 except ldap.SERVER_DOWN, e:
101 raise LdapConnectionError("LDAP can't access authentication server")
102
103 return properties[0]
104
@@ -0,0 +1,16
1 """
2 Automatically sets the environment variable `CELERY_LOADER` to
3 `celerypylons.loader:PylonsLoader`. This ensures the loader is
4 specified when accessing the rest of this package, and allows celery
5 to be installed in a webapp just by importing celerypylons::
6
7 import celerypylons
8
9 """
10 import os
11 import warnings
12
13 CELERYPYLONS_LOADER = 'rhodecode.lib.celerypylons.loader.PylonsLoader'
14 if os.environ.get('CELERY_LOADER', CELERYPYLONS_LOADER) != CELERYPYLONS_LOADER:
15 warnings.warn("'CELERY_LOADER' environment variable will be overridden by celery-pylons.")
16 os.environ['CELERY_LOADER'] = CELERYPYLONS_LOADER
@@ -0,0 +1,90
1 from rhodecode.lib.utils import BasePasterCommand, Command
2
3
4 __all__ = ['CeleryDaemonCommand', 'CeleryBeatCommand',
5 'CAMQPAdminCommand', 'CeleryEventCommand']
6
7
8 class CeleryDaemonCommand(BasePasterCommand):
9 """Start the celery worker
10
11 Starts the celery worker that uses a paste.deploy configuration
12 file.
13 """
14 usage = 'CONFIG_FILE [celeryd options...]'
15 summary = __doc__.splitlines()[0]
16 description = "".join(__doc__.splitlines()[2:])
17
18 parser = Command.standard_parser(quiet=True)
19
20 def update_parser(self):
21 from celery.bin import celeryd
22 for x in celeryd.WorkerCommand().get_options():
23 self.parser.add_option(x)
24
25 def command(self):
26 from celery.bin import celeryd
27 return celeryd.WorkerCommand().run(**vars(self.options))
28
29
30 class CeleryBeatCommand(BasePasterCommand):
31 """Start the celery beat server
32
33 Starts the celery beat server using a paste.deploy configuration
34 file.
35 """
36 usage = 'CONFIG_FILE [celerybeat options...]'
37 summary = __doc__.splitlines()[0]
38 description = "".join(__doc__.splitlines()[2:])
39
40 parser = Command.standard_parser(quiet=True)
41
42 def update_parser(self):
43 from celery.bin import celerybeat
44 for x in celerybeat.BeatCommand().get_options():
45 self.parser.add_option(x)
46
47 def command(self):
48 from celery.bin import celerybeat
49 return celerybeat.BeatCommand(**vars(self.options))
50
51 class CAMQPAdminCommand(BasePasterCommand):
52 """CAMQP Admin
53
54 CAMQP celery admin tool.
55 """
56 usage = 'CONFIG_FILE [camqadm options...]'
57 summary = __doc__.splitlines()[0]
58 description = "".join(__doc__.splitlines()[2:])
59
60 parser = Command.standard_parser(quiet=True)
61
62 def update_parser(self):
63 from celery.bin import camqadm
64 for x in camqadm.OPTION_LIST:
65 self.parser.add_option(x)
66
67 def command(self):
68 from celery.bin import camqadm
69 return camqadm.camqadm(*self.args, **vars(self.options))
70
71
72 class CeleryEventCommand(BasePasterCommand):
73 """Celery event commandd.
74
75 Capture celery events.
76 """
77 usage = 'CONFIG_FILE [celeryev options...]'
78 summary = __doc__.splitlines()[0]
79 description = "".join(__doc__.splitlines()[2:])
80
81 parser = Command.standard_parser(quiet=True)
82
83 def update_parser(self):
84 from celery.bin import celeryev
85 for x in celeryev.OPTION_LIST:
86 self.parser.add_option(x)
87
88 def command(self):
89 from celery.bin import celeryev
90 return celeryev.run_celeryev(**vars(self.options))
@@ -0,0 +1,55
1 from celery.loaders.base import BaseLoader
2 from pylons import config
3
4 to_pylons = lambda x: x.replace('_', '.').lower()
5 to_celery = lambda x: x.replace('.', '_').upper()
6
7 LIST_PARAMS = """CELERY_IMPORTS ADMINS ROUTES""".split()
8
9
10 class PylonsSettingsProxy(object):
11 """Pylons Settings Proxy
12
13 Proxies settings from pylons.config
14
15 """
16 def __getattr__(self, key):
17 pylons_key = to_pylons(key)
18 try:
19 value = config[pylons_key]
20 if key in LIST_PARAMS: return value.split()
21 return self.type_converter(value)
22 except KeyError:
23 raise AttributeError(pylons_key)
24
25 def __setattr__(self, key, value):
26 pylons_key = to_pylons(key)
27 config[pylons_key] = value
28
29
30 def type_converter(self, value):
31 #cast to int
32 if value.isdigit():
33 return int(value)
34
35 #cast to bool
36 if value.lower() in ['true', 'false']:
37 return value.lower() == 'true'
38
39 return value
40
41 class PylonsLoader(BaseLoader):
42 """Pylons celery loader
43
44 Maps the celery config onto pylons.config
45
46 """
47 def read_configuration(self):
48 self.configured = True
49 return PylonsSettingsProxy()
50
51 def on_worker_init(self):
52 """
53 Import task modules.
54 """
55 self.import_default_modules()
@@ -0,0 +1,69
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.lib.dbmigrate.__init__
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 Database migration modules
7
8 :created_on: Dec 11, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 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
14 # modify it under the terms of the GNU General Public License
15 # as published by the Free Software Foundation; version 2
16 # of the License or (at your opinion) any later version of the license.
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, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 # MA 02110-1301, USA.
27
28 import logging
29 from sqlalchemy import engine_from_config
30
31
32 from rhodecode.lib.utils import BasePasterCommand, Command, add_cache
33 from rhodecode.lib.db_manage import DbManage
34
35 log = logging.getLogger(__name__)
36
37 class UpgradeDb(BasePasterCommand):
38 """Command used for paster to upgrade our database to newer version
39 """
40
41 max_args = 1
42 min_args = 1
43
44 usage = "CONFIG_FILE"
45 summary = "Upgrades current db to newer version given configuration file"
46 group_name = "RhodeCode"
47
48 parser = Command.standard_parser(verbose=True)
49
50 def command(self):
51 from pylons import config
52
53 add_cache(config)
54
55 db_uri = config['sqlalchemy.db1.url']
56
57 dbmanage = DbManage(log_sql=True, dbconf=db_uri,
58 root=config['here'], tests=False)
59
60 dbmanage.upgrade()
61
62
63
64 def update_parser(self):
65 self.parser.add_option('--sql',
66 action='store_true',
67 dest='just_sql',
68 help="Prints upgrade sql for further investigation",
69 default=False)
@@ -0,0 +1,20
1 [db_settings]
2 # Used to identify which repository this database is versioned under.
3 # You can use the name of your project.
4 repository_id=rhodecode_db_migrations
5
6 # The name of the database table used to track the schema version.
7 # This name shouldn't already be used by your project.
8 # If this is changed once a database is under version control, you'll need to
9 # change the table name in each database too.
10 version_table=db_migrate_version
11
12 # When committing a change script, Migrate will attempt to generate the
13 # sql for all supported databases; normally, if one of them fails - probably
14 # because you don't have that database installed - it is ignored and the
15 # commit continues, perhaps ending successfully.
16 # Databases in this list MUST compile successfully during a commit, or the
17 # entire commit will fail. List the databases your application will actually
18 # be using to ensure your updates to that database work properly.
19 # This must be a list; example: ['postgres','sqlite']
20 required_dbs=['sqlite']
@@ -0,0 +1,9
1 """
2 SQLAlchemy migrate provides two APIs :mod:`migrate.versioning` for
3 database schema version and repository management and
4 :mod:`migrate.changeset` that allows to define database schema changes
5 using Python.
6 """
7
8 from rhodecode.lib.dbmigrate.migrate.versioning import *
9 from rhodecode.lib.dbmigrate.migrate.changeset import *
@@ -0,0 +1,28
1 """
2 This module extends SQLAlchemy and provides additional DDL [#]_
3 support.
4
5 .. [#] SQL Data Definition Language
6 """
7 import re
8 import warnings
9
10 import sqlalchemy
11 from sqlalchemy import __version__ as _sa_version
12
13 warnings.simplefilter('always', DeprecationWarning)
14
15 _sa_version = tuple(int(re.match("\d+", x).group(0)) for x in _sa_version.split("."))
16 SQLA_06 = _sa_version >= (0, 6)
17
18 del re
19 del _sa_version
20
21 from rhodecode.lib.dbmigrate.migrate.changeset.schema import *
22 from rhodecode.lib.dbmigrate.migrate.changeset.constraint import *
23
24 sqlalchemy.schema.Table.__bases__ += (ChangesetTable, )
25 sqlalchemy.schema.Column.__bases__ += (ChangesetColumn, )
26 sqlalchemy.schema.Index.__bases__ += (ChangesetIndex, )
27
28 sqlalchemy.schema.DefaultClause.__bases__ += (ChangesetDefaultClause, )
@@ -0,0 +1,358
1 """
2 Extensions to SQLAlchemy for altering existing tables.
3
4 At the moment, this isn't so much based off of ANSI as much as
5 things that just happen to work with multiple databases.
6 """
7 import StringIO
8
9 import sqlalchemy as sa
10 from sqlalchemy.schema import SchemaVisitor
11 from sqlalchemy.engine.default import DefaultDialect
12 from sqlalchemy.sql import ClauseElement
13 from sqlalchemy.schema import (ForeignKeyConstraint,
14 PrimaryKeyConstraint,
15 CheckConstraint,
16 UniqueConstraint,
17 Index)
18
19 from rhodecode.lib.dbmigrate.migrate import exceptions
20 from rhodecode.lib.dbmigrate.migrate.changeset import constraint, SQLA_06
21
22 if not SQLA_06:
23 from sqlalchemy.sql.compiler import SchemaGenerator, SchemaDropper
24 else:
25 from sqlalchemy.schema import AddConstraint, DropConstraint
26 from sqlalchemy.sql.compiler import DDLCompiler
27 SchemaGenerator = SchemaDropper = DDLCompiler
28
29
30 class AlterTableVisitor(SchemaVisitor):
31 """Common operations for ``ALTER TABLE`` statements."""
32
33 if SQLA_06:
34 # engine.Compiler looks for .statement
35 # when it spawns off a new compiler
36 statement = ClauseElement()
37
38 def append(self, s):
39 """Append content to the SchemaIterator's query buffer."""
40
41 self.buffer.write(s)
42
43 def execute(self):
44 """Execute the contents of the SchemaIterator's buffer."""
45 try:
46 return self.connection.execute(self.buffer.getvalue())
47 finally:
48 self.buffer.truncate(0)
49
50 def __init__(self, dialect, connection, **kw):
51 self.connection = connection
52 self.buffer = StringIO.StringIO()
53 self.preparer = dialect.identifier_preparer
54 self.dialect = dialect
55
56 def traverse_single(self, elem):
57 ret = super(AlterTableVisitor, self).traverse_single(elem)
58 if ret:
59 # adapt to 0.6 which uses a string-returning
60 # object
61 self.append(" %s" % ret)
62
63 def _to_table(self, param):
64 """Returns the table object for the given param object."""
65 if isinstance(param, (sa.Column, sa.Index, sa.schema.Constraint)):
66 ret = param.table
67 else:
68 ret = param
69 return ret
70
71 def start_alter_table(self, param):
72 """Returns the start of an ``ALTER TABLE`` SQL-Statement.
73
74 Use the param object to determine the table name and use it
75 for building the SQL statement.
76
77 :param param: object to determine the table from
78 :type param: :class:`sqlalchemy.Column`, :class:`sqlalchemy.Index`,
79 :class:`sqlalchemy.schema.Constraint`, :class:`sqlalchemy.Table`,
80 or string (table name)
81 """
82 table = self._to_table(param)
83 self.append('\nALTER TABLE %s ' % self.preparer.format_table(table))
84 return table
85
86
87 class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator):
88 """Extends ansisql generator for column creation (alter table add col)"""
89
90 def visit_column(self, column):
91 """Create a column (table already exists).
92
93 :param column: column object
94 :type column: :class:`sqlalchemy.Column` instance
95 """
96 if column.default is not None:
97 self.traverse_single(column.default)
98
99 table = self.start_alter_table(column)
100 self.append("ADD ")
101 self.append(self.get_column_specification(column))
102
103 for cons in column.constraints:
104 self.traverse_single(cons)
105 self.execute()
106
107 # ALTER TABLE STATEMENTS
108
109 # add indexes and unique constraints
110 if column.index_name:
111 Index(column.index_name,column).create()
112 elif column.unique_name:
113 constraint.UniqueConstraint(column,
114 name=column.unique_name).create()
115
116 # SA bounds FK constraints to table, add manually
117 for fk in column.foreign_keys:
118 self.add_foreignkey(fk.constraint)
119
120 # add primary key constraint if needed
121 if column.primary_key_name:
122 cons = constraint.PrimaryKeyConstraint(column,
123 name=column.primary_key_name)
124 cons.create()
125
126 if SQLA_06:
127 def add_foreignkey(self, fk):
128 self.connection.execute(AddConstraint(fk))
129
130 class ANSIColumnDropper(AlterTableVisitor, SchemaDropper):
131 """Extends ANSI SQL dropper for column dropping (``ALTER TABLE
132 DROP COLUMN``).
133 """
134
135 def visit_column(self, column):
136 """Drop a column from its table.
137
138 :param column: the column object
139 :type column: :class:`sqlalchemy.Column`
140 """
141 table = self.start_alter_table(column)
142 self.append('DROP COLUMN %s' % self.preparer.format_column(column))
143 self.execute()
144
145
146 class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator):
147 """Manages changes to existing schema elements.
148
149 Note that columns are schema elements; ``ALTER TABLE ADD COLUMN``
150 is in SchemaGenerator.
151
152 All items may be renamed. Columns can also have many of their properties -
153 type, for example - changed.
154
155 Each function is passed a tuple, containing (object, name); where
156 object is a type of object you'd expect for that function
157 (ie. table for visit_table) and name is the object's new
158 name. NONE means the name is unchanged.
159 """
160
161 def visit_table(self, table):
162 """Rename a table. Other ops aren't supported."""
163 self.start_alter_table(table)
164 self.append("RENAME TO %s" % self.preparer.quote(table.new_name,
165 table.quote))
166 self.execute()
167
168 def visit_index(self, index):
169 """Rename an index"""
170 if hasattr(self, '_validate_identifier'):
171 # SA <= 0.6.3
172 self.append("ALTER INDEX %s RENAME TO %s" % (
173 self.preparer.quote(
174 self._validate_identifier(
175 index.name, True), index.quote),
176 self.preparer.quote(
177 self._validate_identifier(
178 index.new_name, True), index.quote)))
179 else:
180 # SA >= 0.6.5
181 self.append("ALTER INDEX %s RENAME TO %s" % (
182 self.preparer.quote(
183 self._index_identifier(
184 index.name), index.quote),
185 self.preparer.quote(
186 self._index_identifier(
187 index.new_name), index.quote)))
188 self.execute()
189
190 def visit_column(self, delta):
191 """Rename/change a column."""
192 # ALTER COLUMN is implemented as several ALTER statements
193 keys = delta.keys()
194 if 'type' in keys:
195 self._run_subvisit(delta, self._visit_column_type)
196 if 'nullable' in keys:
197 self._run_subvisit(delta, self._visit_column_nullable)
198 if 'server_default' in keys:
199 # Skip 'default': only handle server-side defaults, others
200 # are managed by the app, not the db.
201 self._run_subvisit(delta, self._visit_column_default)
202 if 'name' in keys:
203 self._run_subvisit(delta, self._visit_column_name, start_alter=False)
204
205 def _run_subvisit(self, delta, func, start_alter=True):
206 """Runs visit method based on what needs to be changed on column"""
207 table = self._to_table(delta.table)
208 col_name = delta.current_name
209 if start_alter:
210 self.start_alter_column(table, col_name)
211 ret = func(table, delta.result_column, delta)
212 self.execute()
213
214 def start_alter_column(self, table, col_name):
215 """Starts ALTER COLUMN"""
216 self.start_alter_table(table)
217 self.append("ALTER COLUMN %s " % self.preparer.quote(col_name, table.quote))
218
219 def _visit_column_nullable(self, table, column, delta):
220 nullable = delta['nullable']
221 if nullable:
222 self.append("DROP NOT NULL")
223 else:
224 self.append("SET NOT NULL")
225
226 def _visit_column_default(self, table, column, delta):
227 default_text = self.get_column_default_string(column)
228 if default_text is not None:
229 self.append("SET DEFAULT %s" % default_text)
230 else:
231 self.append("DROP DEFAULT")
232
233 def _visit_column_type(self, table, column, delta):
234 type_ = delta['type']
235 if SQLA_06:
236 type_text = str(type_.compile(dialect=self.dialect))
237 else:
238 type_text = type_.dialect_impl(self.dialect).get_col_spec()
239 self.append("TYPE %s" % type_text)
240
241 def _visit_column_name(self, table, column, delta):
242 self.start_alter_table(table)
243 col_name = self.preparer.quote(delta.current_name, table.quote)
244 new_name = self.preparer.format_column(delta.result_column)
245 self.append('RENAME COLUMN %s TO %s' % (col_name, new_name))
246
247
248 class ANSIConstraintCommon(AlterTableVisitor):
249 """
250 Migrate's constraints require a separate creation function from
251 SA's: Migrate's constraints are created independently of a table;
252 SA's are created at the same time as the table.
253 """
254
255 def get_constraint_name(self, cons):
256 """Gets a name for the given constraint.
257
258 If the name is already set it will be used otherwise the
259 constraint's :meth:`autoname <migrate.changeset.constraint.ConstraintChangeset.autoname>`
260 method is used.
261
262 :param cons: constraint object
263 """
264 if cons.name is not None:
265 ret = cons.name
266 else:
267 ret = cons.name = cons.autoname()
268 return self.preparer.quote(ret, cons.quote)
269
270 def visit_migrate_primary_key_constraint(self, *p, **k):
271 self._visit_constraint(*p, **k)
272
273 def visit_migrate_foreign_key_constraint(self, *p, **k):
274 self._visit_constraint(*p, **k)
275
276 def visit_migrate_check_constraint(self, *p, **k):
277 self._visit_constraint(*p, **k)
278
279 def visit_migrate_unique_constraint(self, *p, **k):
280 self._visit_constraint(*p, **k)
281
282 if SQLA_06:
283 class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator):
284 def _visit_constraint(self, constraint):
285 constraint.name = self.get_constraint_name(constraint)
286 self.append(self.process(AddConstraint(constraint)))
287 self.execute()
288
289 class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper):
290 def _visit_constraint(self, constraint):
291 constraint.name = self.get_constraint_name(constraint)
292 self.append(self.process(DropConstraint(constraint, cascade=constraint.cascade)))
293 self.execute()
294
295 else:
296 class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator):
297
298 def get_constraint_specification(self, cons, **kwargs):
299 """Constaint SQL generators.
300
301 We cannot use SA visitors because they append comma.
302 """
303
304 if isinstance(cons, PrimaryKeyConstraint):
305 if cons.name is not None:
306 self.append("CONSTRAINT %s " % self.preparer.format_constraint(cons))
307 self.append("PRIMARY KEY ")
308 self.append("(%s)" % ', '.join(self.preparer.quote(c.name, c.quote)
309 for c in cons))
310 self.define_constraint_deferrability(cons)
311 elif isinstance(cons, ForeignKeyConstraint):
312 self.define_foreign_key(cons)
313 elif isinstance(cons, CheckConstraint):
314 if cons.name is not None:
315 self.append("CONSTRAINT %s " %
316 self.preparer.format_constraint(cons))
317 self.append("CHECK (%s)" % cons.sqltext)
318 self.define_constraint_deferrability(cons)
319 elif isinstance(cons, UniqueConstraint):
320 if cons.name is not None:
321 self.append("CONSTRAINT %s " %
322 self.preparer.format_constraint(cons))
323 self.append("UNIQUE (%s)" % \
324 (', '.join(self.preparer.quote(c.name, c.quote) for c in cons)))
325 self.define_constraint_deferrability(cons)
326 else:
327 raise exceptions.InvalidConstraintError(cons)
328
329 def _visit_constraint(self, constraint):
330
331 table = self.start_alter_table(constraint)
332 constraint.name = self.get_constraint_name(constraint)
333 self.append("ADD ")
334 self.get_constraint_specification(constraint)
335 self.execute()
336
337
338 class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper):
339
340 def _visit_constraint(self, constraint):
341 self.start_alter_table(constraint)
342 self.append("DROP CONSTRAINT ")
343 constraint.name = self.get_constraint_name(constraint)
344 self.append(self.preparer.format_constraint(constraint))
345 if constraint.cascade:
346 self.cascade_constraint(constraint)
347 self.execute()
348
349 def cascade_constraint(self, constraint):
350 self.append(" CASCADE")
351
352
353 class ANSIDialect(DefaultDialect):
354 columngenerator = ANSIColumnGenerator
355 columndropper = ANSIColumnDropper
356 schemachanger = ANSISchemaChanger
357 constraintgenerator = ANSIConstraintGenerator
358 constraintdropper = ANSIConstraintDropper
@@ -0,0 +1,202
1 """
2 This module defines standalone schema constraint classes.
3 """
4 from sqlalchemy import schema
5
6 from rhodecode.lib.dbmigrate.migrate.exceptions import *
7 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_06
8
9 class ConstraintChangeset(object):
10 """Base class for Constraint classes."""
11
12 def _normalize_columns(self, cols, table_name=False):
13 """Given: column objects or names; return col names and
14 (maybe) a table"""
15 colnames = []
16 table = None
17 for col in cols:
18 if isinstance(col, schema.Column):
19 if col.table is not None and table is None:
20 table = col.table
21 if table_name:
22 col = '.'.join((col.table.name, col.name))
23 else:
24 col = col.name
25 colnames.append(col)
26 return colnames, table
27
28 def __do_imports(self, visitor_name, *a, **kw):
29 engine = kw.pop('engine', self.table.bind)
30 from rhodecode.lib.dbmigrate.migrate.changeset.databases.visitor import (get_engine_visitor,
31 run_single_visitor)
32 visitorcallable = get_engine_visitor(engine, visitor_name)
33 run_single_visitor(engine, visitorcallable, self, *a, **kw)
34
35 def create(self, *a, **kw):
36 """Create the constraint in the database.
37
38 :param engine: the database engine to use. If this is \
39 :keyword:`None` the instance's engine will be used
40 :type engine: :class:`sqlalchemy.engine.base.Engine`
41 :param connection: reuse connection istead of creating new one.
42 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
43 """
44 # TODO: set the parent here instead of in __init__
45 self.__do_imports('constraintgenerator', *a, **kw)
46
47 def drop(self, *a, **kw):
48 """Drop the constraint from the database.
49
50 :param engine: the database engine to use. If this is
51 :keyword:`None` the instance's engine will be used
52 :param cascade: Issue CASCADE drop if database supports it
53 :type engine: :class:`sqlalchemy.engine.base.Engine`
54 :type cascade: bool
55 :param connection: reuse connection istead of creating new one.
56 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
57 :returns: Instance with cleared columns
58 """
59 self.cascade = kw.pop('cascade', False)
60 self.__do_imports('constraintdropper', *a, **kw)
61 # the spirit of Constraint objects is that they
62 # are immutable (just like in a DB. they're only ADDed
63 # or DROPped).
64 #self.columns.clear()
65 return self
66
67
68 class PrimaryKeyConstraint(ConstraintChangeset, schema.PrimaryKeyConstraint):
69 """Construct PrimaryKeyConstraint
70
71 Migrate's additional parameters:
72
73 :param cols: Columns in constraint.
74 :param table: If columns are passed as strings, this kw is required
75 :type table: Table instance
76 :type cols: strings or Column instances
77 """
78
79 __migrate_visit_name__ = 'migrate_primary_key_constraint'
80
81 def __init__(self, *cols, **kwargs):
82 colnames, table = self._normalize_columns(cols)
83 table = kwargs.pop('table', table)
84 super(PrimaryKeyConstraint, self).__init__(*colnames, **kwargs)
85 if table is not None:
86 self._set_parent(table)
87
88
89 def autoname(self):
90 """Mimic the database's automatic constraint names"""
91 return "%s_pkey" % self.table.name
92
93
94 class ForeignKeyConstraint(ConstraintChangeset, schema.ForeignKeyConstraint):
95 """Construct ForeignKeyConstraint
96
97 Migrate's additional parameters:
98
99 :param columns: Columns in constraint
100 :param refcolumns: Columns that this FK reffers to in another table.
101 :param table: If columns are passed as strings, this kw is required
102 :type table: Table instance
103 :type columns: list of strings or Column instances
104 :type refcolumns: list of strings or Column instances
105 """
106
107 __migrate_visit_name__ = 'migrate_foreign_key_constraint'
108
109 def __init__(self, columns, refcolumns, *args, **kwargs):
110 colnames, table = self._normalize_columns(columns)
111 table = kwargs.pop('table', table)
112 refcolnames, reftable = self._normalize_columns(refcolumns,
113 table_name=True)
114 super(ForeignKeyConstraint, self).__init__(colnames, refcolnames, *args,
115 **kwargs)
116 if table is not None:
117 self._set_parent(table)
118
119 @property
120 def referenced(self):
121 return [e.column for e in self.elements]
122
123 @property
124 def reftable(self):
125 return self.referenced[0].table
126
127 def autoname(self):
128 """Mimic the database's automatic constraint names"""
129 if hasattr(self.columns, 'keys'):
130 # SA <= 0.5
131 firstcol = self.columns[self.columns.keys()[0]]
132 ret = "%(table)s_%(firstcolumn)s_fkey" % dict(
133 table=firstcol.table.name,
134 firstcolumn=firstcol.name,)
135 else:
136 # SA >= 0.6
137 ret = "%(table)s_%(firstcolumn)s_fkey" % dict(
138 table=self.table.name,
139 firstcolumn=self.columns[0],)
140 return ret
141
142
143 class CheckConstraint(ConstraintChangeset, schema.CheckConstraint):
144 """Construct CheckConstraint
145
146 Migrate's additional parameters:
147
148 :param sqltext: Plain SQL text to check condition
149 :param columns: If not name is applied, you must supply this kw\
150 to autoname constraint
151 :param table: If columns are passed as strings, this kw is required
152 :type table: Table instance
153 :type columns: list of Columns instances
154 :type sqltext: string
155 """
156
157 __migrate_visit_name__ = 'migrate_check_constraint'
158
159 def __init__(self, sqltext, *args, **kwargs):
160 cols = kwargs.pop('columns', [])
161 if not cols and not kwargs.get('name', False):
162 raise InvalidConstraintError('You must either set "name"'
163 'parameter or "columns" to autogenarate it.')
164 colnames, table = self._normalize_columns(cols)
165 table = kwargs.pop('table', table)
166 schema.CheckConstraint.__init__(self, sqltext, *args, **kwargs)
167 if table is not None:
168 if not SQLA_06:
169 self.table = table
170 self._set_parent(table)
171 self.colnames = colnames
172
173 def autoname(self):
174 return "%(table)s_%(cols)s_check" % \
175 dict(table=self.table.name, cols="_".join(self.colnames))
176
177
178 class UniqueConstraint(ConstraintChangeset, schema.UniqueConstraint):
179 """Construct UniqueConstraint
180
181 Migrate's additional parameters:
182
183 :param cols: Columns in constraint.
184 :param table: If columns are passed as strings, this kw is required
185 :type table: Table instance
186 :type cols: strings or Column instances
187
188 .. versionadded:: 0.6.0
189 """
190
191 __migrate_visit_name__ = 'migrate_unique_constraint'
192
193 def __init__(self, *cols, **kwargs):
194 self.colnames, table = self._normalize_columns(cols)
195 table = kwargs.pop('table', table)
196 super(UniqueConstraint, self).__init__(*self.colnames, **kwargs)
197 if table is not None:
198 self._set_parent(table)
199
200 def autoname(self):
201 """Mimic the database's automatic constraint names"""
202 return "%s_%s_key" % (self.table.name, self.colnames[0])
@@ -0,0 +1,10
1 """
2 This module contains database dialect specific changeset
3 implementations.
4 """
5 __all__ = [
6 'postgres',
7 'sqlite',
8 'mysql',
9 'oracle',
10 ]
@@ -0,0 +1,80
1 """
2 Firebird database specific implementations of changeset classes.
3 """
4 from sqlalchemy.databases import firebird as sa_base
5
6 from rhodecode.lib.dbmigrate.migrate import exceptions
7 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06
8
9
10 if SQLA_06:
11 FBSchemaGenerator = sa_base.FBDDLCompiler
12 else:
13 FBSchemaGenerator = sa_base.FBSchemaGenerator
14
15 class FBColumnGenerator(FBSchemaGenerator, ansisql.ANSIColumnGenerator):
16 """Firebird column generator implementation."""
17
18
19 class FBColumnDropper(ansisql.ANSIColumnDropper):
20 """Firebird column dropper implementation."""
21
22 def visit_column(self, column):
23 """Firebird supports 'DROP col' instead of 'DROP COLUMN col' syntax
24
25 Drop primary key and unique constraints if dropped column is referencing it."""
26 if column.primary_key:
27 if column.table.primary_key.columns.contains_column(column):
28 column.table.primary_key.drop()
29 # TODO: recreate primary key if it references more than this column
30 if column.unique or getattr(column, 'unique_name', None):
31 for cons in column.table.constraints:
32 if cons.contains_column(column):
33 cons.drop()
34 # TODO: recreate unique constraint if it refenrences more than this column
35
36 table = self.start_alter_table(column)
37 self.append('DROP %s' % self.preparer.format_column(column))
38 self.execute()
39
40
41 class FBSchemaChanger(ansisql.ANSISchemaChanger):
42 """Firebird schema changer implementation."""
43
44 def visit_table(self, table):
45 """Rename table not supported"""
46 raise exceptions.NotSupportedError(
47 "Firebird does not support renaming tables.")
48
49 def _visit_column_name(self, table, column, delta):
50 self.start_alter_table(table)
51 col_name = self.preparer.quote(delta.current_name, table.quote)
52 new_name = self.preparer.format_column(delta.result_column)
53 self.append('ALTER COLUMN %s TO %s' % (col_name, new_name))
54
55 def _visit_column_nullable(self, table, column, delta):
56 """Changing NULL is not supported"""
57 # TODO: http://www.firebirdfaq.org/faq103/
58 raise exceptions.NotSupportedError(
59 "Firebird does not support altering NULL bevahior.")
60
61
62 class FBConstraintGenerator(ansisql.ANSIConstraintGenerator):
63 """Firebird constraint generator implementation."""
64
65
66 class FBConstraintDropper(ansisql.ANSIConstraintDropper):
67 """Firebird constaint dropper implementation."""
68
69 def cascade_constraint(self, constraint):
70 """Cascading constraints is not supported"""
71 raise exceptions.NotSupportedError(
72 "Firebird does not support cascading constraints")
73
74
75 class FBDialect(ansisql.ANSIDialect):
76 columngenerator = FBColumnGenerator
77 columndropper = FBColumnDropper
78 schemachanger = FBSchemaChanger
79 constraintgenerator = FBConstraintGenerator
80 constraintdropper = FBConstraintDropper
@@ -0,0 +1,94
1 """
2 MySQL database specific implementations of changeset classes.
3 """
4
5 from sqlalchemy.databases import mysql as sa_base
6 from sqlalchemy import types as sqltypes
7
8 from rhodecode.lib.dbmigrate.migrate import exceptions
9 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06
10
11
12 if not SQLA_06:
13 MySQLSchemaGenerator = sa_base.MySQLSchemaGenerator
14 else:
15 MySQLSchemaGenerator = sa_base.MySQLDDLCompiler
16
17 class MySQLColumnGenerator(MySQLSchemaGenerator, ansisql.ANSIColumnGenerator):
18 pass
19
20
21 class MySQLColumnDropper(ansisql.ANSIColumnDropper):
22 pass
23
24
25 class MySQLSchemaChanger(MySQLSchemaGenerator, ansisql.ANSISchemaChanger):
26
27 def visit_column(self, delta):
28 table = delta.table
29 colspec = self.get_column_specification(delta.result_column)
30 if delta.result_column.autoincrement:
31 primary_keys = [c for c in table.primary_key.columns
32 if (c.autoincrement and
33 isinstance(c.type, sqltypes.Integer) and
34 not c.foreign_keys)]
35
36 if primary_keys:
37 first = primary_keys.pop(0)
38 if first.name == delta.current_name:
39 colspec += " AUTO_INCREMENT"
40 old_col_name = self.preparer.quote(delta.current_name, table.quote)
41
42 self.start_alter_table(table)
43
44 self.append("CHANGE COLUMN %s " % old_col_name)
45 self.append(colspec)
46 self.execute()
47
48 def visit_index(self, param):
49 # If MySQL can do this, I can't find how
50 raise exceptions.NotSupportedError("MySQL cannot rename indexes")
51
52
53 class MySQLConstraintGenerator(ansisql.ANSIConstraintGenerator):
54 pass
55
56 if SQLA_06:
57 class MySQLConstraintDropper(MySQLSchemaGenerator, ansisql.ANSIConstraintDropper):
58 def visit_migrate_check_constraint(self, *p, **k):
59 raise exceptions.NotSupportedError("MySQL does not support CHECK"
60 " constraints, use triggers instead.")
61
62 else:
63 class MySQLConstraintDropper(ansisql.ANSIConstraintDropper):
64
65 def visit_migrate_primary_key_constraint(self, constraint):
66 self.start_alter_table(constraint)
67 self.append("DROP PRIMARY KEY")
68 self.execute()
69
70 def visit_migrate_foreign_key_constraint(self, constraint):
71 self.start_alter_table(constraint)
72 self.append("DROP FOREIGN KEY ")
73 constraint.name = self.get_constraint_name(constraint)
74 self.append(self.preparer.format_constraint(constraint))
75 self.execute()
76
77 def visit_migrate_check_constraint(self, *p, **k):
78 raise exceptions.NotSupportedError("MySQL does not support CHECK"
79 " constraints, use triggers instead.")
80
81 def visit_migrate_unique_constraint(self, constraint, *p, **k):
82 self.start_alter_table(constraint)
83 self.append('DROP INDEX ')
84 constraint.name = self.get_constraint_name(constraint)
85 self.append(self.preparer.format_constraint(constraint))
86 self.execute()
87
88
89 class MySQLDialect(ansisql.ANSIDialect):
90 columngenerator = MySQLColumnGenerator
91 columndropper = MySQLColumnDropper
92 schemachanger = MySQLSchemaChanger
93 constraintgenerator = MySQLConstraintGenerator
94 constraintdropper = MySQLConstraintDropper
@@ -0,0 +1,111
1 """
2 Oracle database specific implementations of changeset classes.
3 """
4 import sqlalchemy as sa
5 from sqlalchemy.databases import oracle as sa_base
6
7 from rhodecode.lib.dbmigrate.migrate import exceptions
8 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06
9
10
11 if not SQLA_06:
12 OracleSchemaGenerator = sa_base.OracleSchemaGenerator
13 else:
14 OracleSchemaGenerator = sa_base.OracleDDLCompiler
15
16
17 class OracleColumnGenerator(OracleSchemaGenerator, ansisql.ANSIColumnGenerator):
18 pass
19
20
21 class OracleColumnDropper(ansisql.ANSIColumnDropper):
22 pass
23
24
25 class OracleSchemaChanger(OracleSchemaGenerator, ansisql.ANSISchemaChanger):
26
27 def get_column_specification(self, column, **kwargs):
28 # Ignore the NOT NULL generated
29 override_nullable = kwargs.pop('override_nullable', None)
30 if override_nullable:
31 orig = column.nullable
32 column.nullable = True
33 ret = super(OracleSchemaChanger, self).get_column_specification(
34 column, **kwargs)
35 if override_nullable:
36 column.nullable = orig
37 return ret
38
39 def visit_column(self, delta):
40 keys = delta.keys()
41
42 if 'name' in keys:
43 self._run_subvisit(delta,
44 self._visit_column_name,
45 start_alter=False)
46
47 if len(set(('type', 'nullable', 'server_default')).intersection(keys)):
48 self._run_subvisit(delta,
49 self._visit_column_change,
50 start_alter=False)
51
52 def _visit_column_change(self, table, column, delta):
53 # Oracle cannot drop a default once created, but it can set it
54 # to null. We'll do that if default=None
55 # http://forums.oracle.com/forums/message.jspa?messageID=1273234#1273234
56 dropdefault_hack = (column.server_default is None \
57 and 'server_default' in delta.keys())
58 # Oracle apparently doesn't like it when we say "not null" if
59 # the column's already not null. Fudge it, so we don't need a
60 # new function
61 notnull_hack = ((not column.nullable) \
62 and ('nullable' not in delta.keys()))
63 # We need to specify NULL if we're removing a NOT NULL
64 # constraint
65 null_hack = (column.nullable and ('nullable' in delta.keys()))
66
67 if dropdefault_hack:
68 column.server_default = sa.PassiveDefault(sa.sql.null())
69 if notnull_hack:
70 column.nullable = True
71 colspec = self.get_column_specification(column,
72 override_nullable=null_hack)
73 if null_hack:
74 colspec += ' NULL'
75 if notnull_hack:
76 column.nullable = False
77 if dropdefault_hack:
78 column.server_default = None
79
80 self.start_alter_table(table)
81 self.append("MODIFY (")
82 self.append(colspec)
83 self.append(")")
84
85
86 class OracleConstraintCommon(object):
87
88 def get_constraint_name(self, cons):
89 # Oracle constraints can't guess their name like other DBs
90 if not cons.name:
91 raise exceptions.NotSupportedError(
92 "Oracle constraint names must be explicitly stated")
93 return cons.name
94
95
96 class OracleConstraintGenerator(OracleConstraintCommon,
97 ansisql.ANSIConstraintGenerator):
98 pass
99
100
101 class OracleConstraintDropper(OracleConstraintCommon,
102 ansisql.ANSIConstraintDropper):
103 pass
104
105
106 class OracleDialect(ansisql.ANSIDialect):
107 columngenerator = OracleColumnGenerator
108 columndropper = OracleColumnDropper
109 schemachanger = OracleSchemaChanger
110 constraintgenerator = OracleConstraintGenerator
111 constraintdropper = OracleConstraintDropper
@@ -0,0 +1,46
1 """
2 `PostgreSQL`_ database specific implementations of changeset classes.
3
4 .. _`PostgreSQL`: http://www.postgresql.org/
5 """
6 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06
7
8 if not SQLA_06:
9 from sqlalchemy.databases import postgres as sa_base
10 PGSchemaGenerator = sa_base.PGSchemaGenerator
11 else:
12 from sqlalchemy.databases import postgresql as sa_base
13 PGSchemaGenerator = sa_base.PGDDLCompiler
14
15
16 class PGColumnGenerator(PGSchemaGenerator, ansisql.ANSIColumnGenerator):
17 """PostgreSQL column generator implementation."""
18 pass
19
20
21 class PGColumnDropper(ansisql.ANSIColumnDropper):
22 """PostgreSQL column dropper implementation."""
23 pass
24
25
26 class PGSchemaChanger(ansisql.ANSISchemaChanger):
27 """PostgreSQL schema changer implementation."""
28 pass
29
30
31 class PGConstraintGenerator(ansisql.ANSIConstraintGenerator):
32 """PostgreSQL constraint generator implementation."""
33 pass
34
35
36 class PGConstraintDropper(ansisql.ANSIConstraintDropper):
37 """PostgreSQL constaint dropper implementation."""
38 pass
39
40
41 class PGDialect(ansisql.ANSIDialect):
42 columngenerator = PGColumnGenerator
43 columndropper = PGColumnDropper
44 schemachanger = PGSchemaChanger
45 constraintgenerator = PGConstraintGenerator
46 constraintdropper = PGConstraintDropper
@@ -0,0 +1,148
1 """
2 `SQLite`_ database specific implementations of changeset classes.
3
4 .. _`SQLite`: http://www.sqlite.org/
5 """
6 from UserDict import DictMixin
7 from copy import copy
8
9 from sqlalchemy.databases import sqlite as sa_base
10
11 from rhodecode.lib.dbmigrate.migrate import exceptions
12 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06
13
14
15 if not SQLA_06:
16 SQLiteSchemaGenerator = sa_base.SQLiteSchemaGenerator
17 else:
18 SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler
19
20 class SQLiteCommon(object):
21
22 def _not_supported(self, op):
23 raise exceptions.NotSupportedError("SQLite does not support "
24 "%s; see http://www.sqlite.org/lang_altertable.html" % op)
25
26
27 class SQLiteHelper(SQLiteCommon):
28
29 def recreate_table(self,table,column=None,delta=None):
30 table_name = self.preparer.format_table(table)
31
32 # we remove all indexes so as not to have
33 # problems during copy and re-create
34 for index in table.indexes:
35 index.drop()
36
37 self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name)
38 self.execute()
39
40 insertion_string = self._modify_table(table, column, delta)
41
42 table.create()
43 self.append(insertion_string % {'table_name': table_name})
44 self.execute()
45 self.append('DROP TABLE migration_tmp')
46 self.execute()
47
48 def visit_column(self, delta):
49 if isinstance(delta, DictMixin):
50 column = delta.result_column
51 table = self._to_table(delta.table)
52 else:
53 column = delta
54 table = self._to_table(column.table)
55 self.recreate_table(table,column,delta)
56
57 class SQLiteColumnGenerator(SQLiteSchemaGenerator,
58 ansisql.ANSIColumnGenerator,
59 # at the end so we get the normal
60 # visit_column by default
61 SQLiteHelper,
62 SQLiteCommon
63 ):
64 """SQLite ColumnGenerator"""
65
66 def _modify_table(self, table, column, delta):
67 columns = ' ,'.join(map(
68 self.preparer.format_column,
69 [c for c in table.columns if c.name!=column.name]))
70 return ('INSERT INTO %%(table_name)s (%(cols)s) '
71 'SELECT %(cols)s from migration_tmp')%{'cols':columns}
72
73 def visit_column(self,column):
74 if column.foreign_keys:
75 SQLiteHelper.visit_column(self,column)
76 else:
77 super(SQLiteColumnGenerator,self).visit_column(column)
78
79 class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper):
80 """SQLite ColumnDropper"""
81
82 def _modify_table(self, table, column, delta):
83 columns = ' ,'.join(map(self.preparer.format_column, table.columns))
84 return 'INSERT INTO %(table_name)s SELECT ' + columns + \
85 ' from migration_tmp'
86
87
88 class SQLiteSchemaChanger(SQLiteHelper, ansisql.ANSISchemaChanger):
89 """SQLite SchemaChanger"""
90
91 def _modify_table(self, table, column, delta):
92 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
93
94 def visit_index(self, index):
95 """Does not support ALTER INDEX"""
96 self._not_supported('ALTER INDEX')
97
98
99 class SQLiteConstraintGenerator(ansisql.ANSIConstraintGenerator, SQLiteHelper, SQLiteCommon):
100
101 def visit_migrate_primary_key_constraint(self, constraint):
102 tmpl = "CREATE UNIQUE INDEX %s ON %s ( %s )"
103 cols = ', '.join(map(self.preparer.format_column, constraint.columns))
104 tname = self.preparer.format_table(constraint.table)
105 name = self.get_constraint_name(constraint)
106 msg = tmpl % (name, tname, cols)
107 self.append(msg)
108 self.execute()
109
110 def _modify_table(self, table, column, delta):
111 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
112
113 def visit_migrate_foreign_key_constraint(self, *p, **k):
114 self.recreate_table(p[0].table)
115
116 def visit_migrate_unique_constraint(self, *p, **k):
117 self.recreate_table(p[0].table)
118
119
120 class SQLiteConstraintDropper(ansisql.ANSIColumnDropper,
121 SQLiteCommon,
122 ansisql.ANSIConstraintCommon):
123
124 def visit_migrate_primary_key_constraint(self, constraint):
125 tmpl = "DROP INDEX %s "
126 name = self.get_constraint_name(constraint)
127 msg = tmpl % (name)
128 self.append(msg)
129 self.execute()
130
131 def visit_migrate_foreign_key_constraint(self, *p, **k):
132 self._not_supported('ALTER TABLE DROP CONSTRAINT')
133
134 def visit_migrate_check_constraint(self, *p, **k):
135 self._not_supported('ALTER TABLE DROP CONSTRAINT')
136
137 def visit_migrate_unique_constraint(self, *p, **k):
138 self._not_supported('ALTER TABLE DROP CONSTRAINT')
139
140
141 # TODO: technically primary key is a NOT NULL + UNIQUE constraint, should add NOT NULL to index
142
143 class SQLiteDialect(ansisql.ANSIDialect):
144 columngenerator = SQLiteColumnGenerator
145 columndropper = SQLiteColumnDropper
146 schemachanger = SQLiteSchemaChanger
147 constraintgenerator = SQLiteConstraintGenerator
148 constraintdropper = SQLiteConstraintDropper
@@ -0,0 +1,78
1 """
2 Module for visitor class mapping.
3 """
4 import sqlalchemy as sa
5
6 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql
7 from rhodecode.lib.dbmigrate.migrate.changeset.databases import (sqlite,
8 postgres,
9 mysql,
10 oracle,
11 firebird)
12
13
14 # Map SA dialects to the corresponding Migrate extensions
15 DIALECTS = {
16 "default": ansisql.ANSIDialect,
17 "sqlite": sqlite.SQLiteDialect,
18 "postgres": postgres.PGDialect,
19 "postgresql": postgres.PGDialect,
20 "mysql": mysql.MySQLDialect,
21 "oracle": oracle.OracleDialect,
22 "firebird": firebird.FBDialect,
23 }
24
25
26 def get_engine_visitor(engine, name):
27 """
28 Get the visitor implementation for the given database engine.
29
30 :param engine: SQLAlchemy Engine
31 :param name: Name of the visitor
32 :type name: string
33 :type engine: Engine
34 :returns: visitor
35 """
36 # TODO: link to supported visitors
37 return get_dialect_visitor(engine.dialect, name)
38
39
40 def get_dialect_visitor(sa_dialect, name):
41 """
42 Get the visitor implementation for the given dialect.
43
44 Finds the visitor implementation based on the dialect class and
45 returns and instance initialized with the given name.
46
47 Binds dialect specific preparer to visitor.
48 """
49
50 # map sa dialect to migrate dialect and return visitor
51 sa_dialect_name = getattr(sa_dialect, 'name', 'default')
52 migrate_dialect_cls = DIALECTS[sa_dialect_name]
53 visitor = getattr(migrate_dialect_cls, name)
54
55 # bind preparer
56 visitor.preparer = sa_dialect.preparer(sa_dialect)
57
58 return visitor
59
60 def run_single_visitor(engine, visitorcallable, element,
61 connection=None, **kwargs):
62 """Taken from :meth:`sqlalchemy.engine.base.Engine._run_single_visitor`
63 with support for migrate visitors.
64 """
65 if connection is None:
66 conn = engine.contextual_connect(close_with_result=False)
67 else:
68 conn = connection
69 visitor = visitorcallable(engine.dialect, conn)
70 try:
71 if hasattr(element, '__migrate_visit_name__'):
72 fn = getattr(visitor, 'visit_' + element.__migrate_visit_name__)
73 else:
74 fn = getattr(visitor, 'visit_' + element.__visit_name__)
75 fn(element, **kwargs)
76 finally:
77 if connection is None:
78 conn.close()
This diff has been collapsed as it changes many lines, (669 lines changed) Show them Hide them
@@ -0,0 +1,669
1 """
2 Schema module providing common schema operations.
3 """
4 import warnings
5
6 from UserDict import DictMixin
7
8 import sqlalchemy
9
10 from sqlalchemy.schema import ForeignKeyConstraint
11 from sqlalchemy.schema import UniqueConstraint
12
13 from rhodecode.lib.dbmigrate.migrate.exceptions import *
14 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_06
15 from rhodecode.lib.dbmigrate.migrate.changeset.databases.visitor import (get_engine_visitor,
16 run_single_visitor)
17
18
19 __all__ = [
20 'create_column',
21 'drop_column',
22 'alter_column',
23 'rename_table',
24 'rename_index',
25 'ChangesetTable',
26 'ChangesetColumn',
27 'ChangesetIndex',
28 'ChangesetDefaultClause',
29 'ColumnDelta',
30 ]
31
32 DEFAULT_ALTER_METADATA = True
33
34
35 def create_column(column, table=None, *p, **kw):
36 """Create a column, given the table.
37
38 API to :meth:`ChangesetColumn.create`.
39 """
40 if table is not None:
41 return table.create_column(column, *p, **kw)
42 return column.create(*p, **kw)
43
44
45 def drop_column(column, table=None, *p, **kw):
46 """Drop a column, given the table.
47
48 API to :meth:`ChangesetColumn.drop`.
49 """
50 if table is not None:
51 return table.drop_column(column, *p, **kw)
52 return column.drop(*p, **kw)
53
54
55 def rename_table(table, name, engine=None, **kw):
56 """Rename a table.
57
58 If Table instance is given, engine is not used.
59
60 API to :meth:`ChangesetTable.rename`.
61
62 :param table: Table to be renamed.
63 :param name: New name for Table.
64 :param engine: Engine instance.
65 :type table: string or Table instance
66 :type name: string
67 :type engine: obj
68 """
69 table = _to_table(table, engine)
70 table.rename(name, **kw)
71
72
73 def rename_index(index, name, table=None, engine=None, **kw):
74 """Rename an index.
75
76 If Index instance is given,
77 table and engine are not used.
78
79 API to :meth:`ChangesetIndex.rename`.
80
81 :param index: Index to be renamed.
82 :param name: New name for index.
83 :param table: Table to which Index is reffered.
84 :param engine: Engine instance.
85 :type index: string or Index instance
86 :type name: string
87 :type table: string or Table instance
88 :type engine: obj
89 """
90 index = _to_index(index, table, engine)
91 index.rename(name, **kw)
92
93
94 def alter_column(*p, **k):
95 """Alter a column.
96
97 This is a helper function that creates a :class:`ColumnDelta` and
98 runs it.
99
100 :argument column:
101 The name of the column to be altered or a
102 :class:`ChangesetColumn` column representing it.
103
104 :param table:
105 A :class:`~sqlalchemy.schema.Table` or table name to
106 for the table where the column will be changed.
107
108 :param engine:
109 The :class:`~sqlalchemy.engine.base.Engine` to use for table
110 reflection and schema alterations.
111
112 :param alter_metadata:
113 If `True`, which is the default, the
114 :class:`~sqlalchemy.schema.Column` will also modified.
115 If `False`, the :class:`~sqlalchemy.schema.Column` will be left
116 as it was.
117
118 :returns: A :class:`ColumnDelta` instance representing the change.
119
120
121 """
122
123 k.setdefault('alter_metadata', DEFAULT_ALTER_METADATA)
124
125 if 'table' not in k and isinstance(p[0], sqlalchemy.Column):
126 k['table'] = p[0].table
127 if 'engine' not in k:
128 k['engine'] = k['table'].bind
129
130 # deprecation
131 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
132 warnings.warn(
133 "Passing a Column object to alter_column is deprecated."
134 " Just pass in keyword parameters instead.",
135 MigrateDeprecationWarning
136 )
137 engine = k['engine']
138 delta = ColumnDelta(*p, **k)
139
140 visitorcallable = get_engine_visitor(engine, 'schemachanger')
141 engine._run_visitor(visitorcallable, delta)
142
143 return delta
144
145
146 def _to_table(table, engine=None):
147 """Return if instance of Table, else construct new with metadata"""
148 if isinstance(table, sqlalchemy.Table):
149 return table
150
151 # Given: table name, maybe an engine
152 meta = sqlalchemy.MetaData()
153 if engine is not None:
154 meta.bind = engine
155 return sqlalchemy.Table(table, meta)
156
157
158 def _to_index(index, table=None, engine=None):
159 """Return if instance of Index, else construct new with metadata"""
160 if isinstance(index, sqlalchemy.Index):
161 return index
162
163 # Given: index name; table name required
164 table = _to_table(table, engine)
165 ret = sqlalchemy.Index(index)
166 ret.table = table
167 return ret
168
169
170 class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem):
171 """Extracts the differences between two columns/column-parameters
172
173 May receive parameters arranged in several different ways:
174
175 * **current_column, new_column, \*p, \*\*kw**
176 Additional parameters can be specified to override column
177 differences.
178
179 * **current_column, \*p, \*\*kw**
180 Additional parameters alter current_column. Table name is extracted
181 from current_column object.
182 Name is changed to current_column.name from current_name,
183 if current_name is specified.
184
185 * **current_col_name, \*p, \*\*kw**
186 Table kw must specified.
187
188 :param table: Table at which current Column should be bound to.\
189 If table name is given, reflection will be used.
190 :type table: string or Table instance
191 :param alter_metadata: If True, it will apply changes to metadata.
192 :type alter_metadata: bool
193 :param metadata: If `alter_metadata` is true, \
194 metadata is used to reflect table names into
195 :type metadata: :class:`MetaData` instance
196 :param engine: When reflecting tables, either engine or metadata must \
197 be specified to acquire engine object.
198 :type engine: :class:`Engine` instance
199 :returns: :class:`ColumnDelta` instance provides interface for altered attributes to \
200 `result_column` through :func:`dict` alike object.
201
202 * :class:`ColumnDelta`.result_column is altered column with new attributes
203
204 * :class:`ColumnDelta`.current_name is current name of column in db
205
206
207 """
208
209 # Column attributes that can be altered
210 diff_keys = ('name', 'type', 'primary_key', 'nullable',
211 'server_onupdate', 'server_default', 'autoincrement')
212 diffs = dict()
213 __visit_name__ = 'column'
214
215 def __init__(self, *p, **kw):
216 self.alter_metadata = kw.pop("alter_metadata", False)
217 self.meta = kw.pop("metadata", None)
218 self.engine = kw.pop("engine", None)
219
220 # Things are initialized differently depending on how many column
221 # parameters are given. Figure out how many and call the appropriate
222 # method.
223 if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column):
224 # At least one column specified
225 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
226 # Two columns specified
227 diffs = self.compare_2_columns(*p, **kw)
228 else:
229 # Exactly one column specified
230 diffs = self.compare_1_column(*p, **kw)
231 else:
232 # Zero columns specified
233 if not len(p) or not isinstance(p[0], basestring):
234 raise ValueError("First argument must be column name")
235 diffs = self.compare_parameters(*p, **kw)
236
237 self.apply_diffs(diffs)
238
239 def __repr__(self):
240 return '<ColumnDelta altermetadata=%r, %s>' % (self.alter_metadata,
241 super(ColumnDelta, self).__repr__())
242
243 def __getitem__(self, key):
244 if key not in self.keys():
245 raise KeyError("No such diff key, available: %s" % self.diffs)
246 return getattr(self.result_column, key)
247
248 def __setitem__(self, key, value):
249 if key not in self.keys():
250 raise KeyError("No such diff key, available: %s" % self.diffs)
251 setattr(self.result_column, key, value)
252
253 def __delitem__(self, key):
254 raise NotImplementedError
255
256 def keys(self):
257 return self.diffs.keys()
258
259 def compare_parameters(self, current_name, *p, **k):
260 """Compares Column objects with reflection"""
261 self.table = k.pop('table')
262 self.result_column = self._table.c.get(current_name)
263 if len(p):
264 k = self._extract_parameters(p, k, self.result_column)
265 return k
266
267 def compare_1_column(self, col, *p, **k):
268 """Compares one Column object"""
269 self.table = k.pop('table', None)
270 if self.table is None:
271 self.table = col.table
272 self.result_column = col
273 if len(p):
274 k = self._extract_parameters(p, k, self.result_column)
275 return k
276
277 def compare_2_columns(self, old_col, new_col, *p, **k):
278 """Compares two Column objects"""
279 self.process_column(new_col)
280 self.table = k.pop('table', None)
281 # we cannot use bool() on table in SA06
282 if self.table is None:
283 self.table = old_col.table
284 if self.table is None:
285 new_col.table
286 self.result_column = old_col
287
288 # set differences
289 # leave out some stuff for later comp
290 for key in (set(self.diff_keys) - set(('type',))):
291 val = getattr(new_col, key, None)
292 if getattr(self.result_column, key, None) != val:
293 k.setdefault(key, val)
294
295 # inspect types
296 if not self.are_column_types_eq(self.result_column.type, new_col.type):
297 k.setdefault('type', new_col.type)
298
299 if len(p):
300 k = self._extract_parameters(p, k, self.result_column)
301 return k
302
303 def apply_diffs(self, diffs):
304 """Populate dict and column object with new values"""
305 self.diffs = diffs
306 for key in self.diff_keys:
307 if key in diffs:
308 setattr(self.result_column, key, diffs[key])
309
310 self.process_column(self.result_column)
311
312 # create an instance of class type if not yet
313 if 'type' in diffs and callable(self.result_column.type):
314 self.result_column.type = self.result_column.type()
315
316 # add column to the table
317 if self.table is not None and self.alter_metadata:
318 self.result_column.add_to_table(self.table)
319
320 def are_column_types_eq(self, old_type, new_type):
321 """Compares two types to be equal"""
322 ret = old_type.__class__ == new_type.__class__
323
324 # String length is a special case
325 if ret and isinstance(new_type, sqlalchemy.types.String):
326 ret = (getattr(old_type, 'length', None) == \
327 getattr(new_type, 'length', None))
328 return ret
329
330 def _extract_parameters(self, p, k, column):
331 """Extracts data from p and modifies diffs"""
332 p = list(p)
333 while len(p):
334 if isinstance(p[0], basestring):
335 k.setdefault('name', p.pop(0))
336 elif isinstance(p[0], sqlalchemy.types.AbstractType):
337 k.setdefault('type', p.pop(0))
338 elif callable(p[0]):
339 p[0] = p[0]()
340 else:
341 break
342
343 if len(p):
344 new_col = column.copy_fixed()
345 new_col._init_items(*p)
346 k = self.compare_2_columns(column, new_col, **k)
347 return k
348
349 def process_column(self, column):
350 """Processes default values for column"""
351 # XXX: this is a snippet from SA processing of positional parameters
352 if not SQLA_06 and column.args:
353 toinit = list(column.args)
354 else:
355 toinit = list()
356
357 if column.server_default is not None:
358 if isinstance(column.server_default, sqlalchemy.FetchedValue):
359 toinit.append(column.server_default)
360 else:
361 toinit.append(sqlalchemy.DefaultClause(column.server_default))
362 if column.server_onupdate is not None:
363 if isinstance(column.server_onupdate, FetchedValue):
364 toinit.append(column.server_default)
365 else:
366 toinit.append(sqlalchemy.DefaultClause(column.server_onupdate,
367 for_update=True))
368 if toinit:
369 column._init_items(*toinit)
370
371 if not SQLA_06:
372 column.args = []
373
374 def _get_table(self):
375 return getattr(self, '_table', None)
376
377 def _set_table(self, table):
378 if isinstance(table, basestring):
379 if self.alter_metadata:
380 if not self.meta:
381 raise ValueError("metadata must be specified for table"
382 " reflection when using alter_metadata")
383 meta = self.meta
384 if self.engine:
385 meta.bind = self.engine
386 else:
387 if not self.engine and not self.meta:
388 raise ValueError("engine or metadata must be specified"
389 " to reflect tables")
390 if not self.engine:
391 self.engine = self.meta.bind
392 meta = sqlalchemy.MetaData(bind=self.engine)
393 self._table = sqlalchemy.Table(table, meta, autoload=True)
394 elif isinstance(table, sqlalchemy.Table):
395 self._table = table
396 if not self.alter_metadata:
397 self._table.meta = sqlalchemy.MetaData(bind=self._table.bind)
398
399 def _get_result_column(self):
400 return getattr(self, '_result_column', None)
401
402 def _set_result_column(self, column):
403 """Set Column to Table based on alter_metadata evaluation."""
404 self.process_column(column)
405 if not hasattr(self, 'current_name'):
406 self.current_name = column.name
407 if self.alter_metadata:
408 self._result_column = column
409 else:
410 self._result_column = column.copy_fixed()
411
412 table = property(_get_table, _set_table)
413 result_column = property(_get_result_column, _set_result_column)
414
415
416 class ChangesetTable(object):
417 """Changeset extensions to SQLAlchemy tables."""
418
419 def create_column(self, column, *p, **kw):
420 """Creates a column.
421
422 The column parameter may be a column definition or the name of
423 a column in this table.
424
425 API to :meth:`ChangesetColumn.create`
426
427 :param column: Column to be created
428 :type column: Column instance or string
429 """
430 if not isinstance(column, sqlalchemy.Column):
431 # It's a column name
432 column = getattr(self.c, str(column))
433 column.create(table=self, *p, **kw)
434
435 def drop_column(self, column, *p, **kw):
436 """Drop a column, given its name or definition.
437
438 API to :meth:`ChangesetColumn.drop`
439
440 :param column: Column to be droped
441 :type column: Column instance or string
442 """
443 if not isinstance(column, sqlalchemy.Column):
444 # It's a column name
445 try:
446 column = getattr(self.c, str(column))
447 except AttributeError:
448 # That column isn't part of the table. We don't need
449 # its entire definition to drop the column, just its
450 # name, so create a dummy column with the same name.
451 column = sqlalchemy.Column(str(column), sqlalchemy.Integer())
452 column.drop(table=self, *p, **kw)
453
454 def rename(self, name, connection=None, **kwargs):
455 """Rename this table.
456
457 :param name: New name of the table.
458 :type name: string
459 :param alter_metadata: If True, table will be removed from metadata
460 :type alter_metadata: bool
461 :param connection: reuse connection istead of creating new one.
462 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
463 """
464 self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA)
465 engine = self.bind
466 self.new_name = name
467 visitorcallable = get_engine_visitor(engine, 'schemachanger')
468 run_single_visitor(engine, visitorcallable, self, connection, **kwargs)
469
470 # Fix metadata registration
471 if self.alter_metadata:
472 self.name = name
473 self.deregister()
474 self._set_parent(self.metadata)
475
476 def _meta_key(self):
477 return sqlalchemy.schema._get_table_key(self.name, self.schema)
478
479 def deregister(self):
480 """Remove this table from its metadata"""
481 key = self._meta_key()
482 meta = self.metadata
483 if key in meta.tables:
484 del meta.tables[key]
485
486
487 class ChangesetColumn(object):
488 """Changeset extensions to SQLAlchemy columns."""
489
490 def alter(self, *p, **k):
491 """Makes a call to :func:`alter_column` for the column this
492 method is called on.
493 """
494 if 'table' not in k:
495 k['table'] = self.table
496 if 'engine' not in k:
497 k['engine'] = k['table'].bind
498 return alter_column(self, *p, **k)
499
500 def create(self, table=None, index_name=None, unique_name=None,
501 primary_key_name=None, populate_default=True, connection=None, **kwargs):
502 """Create this column in the database.
503
504 Assumes the given table exists. ``ALTER TABLE ADD COLUMN``,
505 for most databases.
506
507 :param table: Table instance to create on.
508 :param index_name: Creates :class:`ChangesetIndex` on this column.
509 :param unique_name: Creates :class:\
510 `~migrate.changeset.constraint.UniqueConstraint` on this column.
511 :param primary_key_name: Creates :class:\
512 `~migrate.changeset.constraint.PrimaryKeyConstraint` on this column.
513 :param alter_metadata: If True, column will be added to table object.
514 :param populate_default: If True, created column will be \
515 populated with defaults
516 :param connection: reuse connection istead of creating new one.
517 :type table: Table instance
518 :type index_name: string
519 :type unique_name: string
520 :type primary_key_name: string
521 :type alter_metadata: bool
522 :type populate_default: bool
523 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
524
525 :returns: self
526 """
527 self.populate_default = populate_default
528 self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA)
529 self.index_name = index_name
530 self.unique_name = unique_name
531 self.primary_key_name = primary_key_name
532 for cons in ('index_name', 'unique_name', 'primary_key_name'):
533 self._check_sanity_constraints(cons)
534
535 if self.alter_metadata:
536 self.add_to_table(table)
537 engine = self.table.bind
538 visitorcallable = get_engine_visitor(engine, 'columngenerator')
539 engine._run_visitor(visitorcallable, self, connection, **kwargs)
540
541 # TODO: reuse existing connection
542 if self.populate_default and self.default is not None:
543 stmt = table.update().values({self: engine._execute_default(self.default)})
544 engine.execute(stmt)
545
546 return self
547
548 def drop(self, table=None, connection=None, **kwargs):
549 """Drop this column from the database, leaving its table intact.
550
551 ``ALTER TABLE DROP COLUMN``, for most databases.
552
553 :param alter_metadata: If True, column will be removed from table object.
554 :type alter_metadata: bool
555 :param connection: reuse connection istead of creating new one.
556 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
557 """
558 self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA)
559 if table is not None:
560 self.table = table
561 engine = self.table.bind
562 if self.alter_metadata:
563 self.remove_from_table(self.table, unset_table=False)
564 visitorcallable = get_engine_visitor(engine, 'columndropper')
565 engine._run_visitor(visitorcallable, self, connection, **kwargs)
566 if self.alter_metadata:
567 self.table = None
568 return self
569
570 def add_to_table(self, table):
571 if table is not None and self.table is None:
572 self._set_parent(table)
573
574 def _col_name_in_constraint(self, cons, name):
575 return False
576
577 def remove_from_table(self, table, unset_table=True):
578 # TODO: remove primary keys, constraints, etc
579 if unset_table:
580 self.table = None
581
582 to_drop = set()
583 for index in table.indexes:
584 columns = []
585 for col in index.columns:
586 if col.name != self.name:
587 columns.append(col)
588 if columns:
589 index.columns = columns
590 else:
591 to_drop.add(index)
592 table.indexes = table.indexes - to_drop
593
594 to_drop = set()
595 for cons in table.constraints:
596 # TODO: deal with other types of constraint
597 if isinstance(cons, (ForeignKeyConstraint,
598 UniqueConstraint)):
599 for col_name in cons.columns:
600 if not isinstance(col_name, basestring):
601 col_name = col_name.name
602 if self.name == col_name:
603 to_drop.add(cons)
604 table.constraints = table.constraints - to_drop
605
606 if table.c.contains_column(self):
607 table.c.remove(self)
608
609 # TODO: this is fixed in 0.6
610 def copy_fixed(self, **kw):
611 """Create a copy of this ``Column``, with all attributes."""
612 return sqlalchemy.Column(self.name, self.type, self.default,
613 key=self.key,
614 primary_key=self.primary_key,
615 nullable=self.nullable,
616 quote=self.quote,
617 index=self.index,
618 unique=self.unique,
619 onupdate=self.onupdate,
620 autoincrement=self.autoincrement,
621 server_default=self.server_default,
622 server_onupdate=self.server_onupdate,
623 *[c.copy(**kw) for c in self.constraints])
624
625 def _check_sanity_constraints(self, name):
626 """Check if constraints names are correct"""
627 obj = getattr(self, name)
628 if (getattr(self, name[:-5]) and not obj):
629 raise InvalidConstraintError("Column.create() accepts index_name,"
630 " primary_key_name and unique_name to generate constraints")
631 if not isinstance(obj, basestring) and obj is not None:
632 raise InvalidConstraintError(
633 "%s argument for column must be constraint name" % name)
634
635
636 class ChangesetIndex(object):
637 """Changeset extensions to SQLAlchemy Indexes."""
638
639 __visit_name__ = 'index'
640
641 def rename(self, name, connection=None, **kwargs):
642 """Change the name of an index.
643
644 :param name: New name of the Index.
645 :type name: string
646 :param alter_metadata: If True, Index object will be altered.
647 :type alter_metadata: bool
648 :param connection: reuse connection istead of creating new one.
649 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
650 """
651 self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA)
652 engine = self.table.bind
653 self.new_name = name
654 visitorcallable = get_engine_visitor(engine, 'schemachanger')
655 engine._run_visitor(visitorcallable, self, connection, **kwargs)
656 if self.alter_metadata:
657 self.name = name
658
659
660 class ChangesetDefaultClause(object):
661 """Implements comparison between :class:`DefaultClause` instances"""
662
663 def __eq__(self, other):
664 if isinstance(other, self.__class__):
665 if self.arg == other.arg:
666 return True
667
668 def __ne__(self, other):
669 return not self.__eq__(other)
@@ -0,0 +1,87
1 """
2 Provide exception classes for :mod:`migrate`
3 """
4
5
6 class Error(Exception):
7 """Error base class."""
8
9
10 class ApiError(Error):
11 """Base class for API errors."""
12
13
14 class KnownError(ApiError):
15 """A known error condition."""
16
17
18 class UsageError(ApiError):
19 """A known error condition where help should be displayed."""
20
21
22 class ControlledSchemaError(Error):
23 """Base class for controlled schema errors."""
24
25
26 class InvalidVersionError(ControlledSchemaError):
27 """Invalid version number."""
28
29
30 class DatabaseNotControlledError(ControlledSchemaError):
31 """Database should be under version control, but it's not."""
32
33
34 class DatabaseAlreadyControlledError(ControlledSchemaError):
35 """Database shouldn't be under version control, but it is"""
36
37
38 class WrongRepositoryError(ControlledSchemaError):
39 """This database is under version control by another repository."""
40
41
42 class NoSuchTableError(ControlledSchemaError):
43 """The table does not exist."""
44
45
46 class PathError(Error):
47 """Base class for path errors."""
48
49
50 class PathNotFoundError(PathError):
51 """A path with no file was required; found a file."""
52
53
54 class PathFoundError(PathError):
55 """A path with a file was required; found no file."""
56
57
58 class RepositoryError(Error):
59 """Base class for repository errors."""
60
61
62 class InvalidRepositoryError(RepositoryError):
63 """Invalid repository error."""
64
65
66 class ScriptError(Error):
67 """Base class for script errors."""
68
69
70 class InvalidScriptError(ScriptError):
71 """Invalid script error."""
72
73
74 class InvalidVersionError(Error):
75 """Invalid version error."""
76
77 # migrate.changeset
78
79 class NotSupportedError(Error):
80 """Not supported error"""
81
82
83 class InvalidConstraintError(Error):
84 """Invalid constraint error"""
85
86 class MigrateDeprecationWarning(DeprecationWarning):
87 """Warning for deprecated features in Migrate"""
@@ -0,0 +1,5
1 """
2 This package provides functionality to create and manage
3 repositories of database schema changesets and to apply these
4 changesets to databases.
5 """
@@ -0,0 +1,383
1 """
2 This module provides an external API to the versioning system.
3
4 .. versionchanged:: 0.6.0
5 :func:`migrate.versioning.api.test` and schema diff functions
6 changed order of positional arguments so all accept `url` and `repository`
7 as first arguments.
8
9 .. versionchanged:: 0.5.4
10 ``--preview_sql`` displays source file when using SQL scripts.
11 If Python script is used, it runs the action with mocked engine and
12 returns captured SQL statements.
13
14 .. versionchanged:: 0.5.4
15 Deprecated ``--echo`` parameter in favour of new
16 :func:`migrate.versioning.util.construct_engine` behavior.
17 """
18
19 # Dear migrate developers,
20 #
21 # please do not comment this module using sphinx syntax because its
22 # docstrings are presented as user help and most users cannot
23 # interpret sphinx annotated ReStructuredText.
24 #
25 # Thanks,
26 # Jan Dittberner
27
28 import sys
29 import inspect
30 import logging
31
32 from rhodecode.lib.dbmigrate.migrate import exceptions
33 from rhodecode.lib.dbmigrate.migrate.versioning import repository, schema, version, \
34 script as script_ # command name conflict
35 from rhodecode.lib.dbmigrate.migrate.versioning.util import catch_known_errors, with_engine
36
37
38 log = logging.getLogger(__name__)
39 command_desc = {
40 'help': 'displays help on a given command',
41 'create': 'create an empty repository at the specified path',
42 'script': 'create an empty change Python script',
43 'script_sql': 'create empty change SQL scripts for given database',
44 'version': 'display the latest version available in a repository',
45 'db_version': 'show the current version of the repository under version control',
46 'source': 'display the Python code for a particular version in this repository',
47 'version_control': 'mark a database as under this repository\'s version control',
48 'upgrade': 'upgrade a database to a later version',
49 'downgrade': 'downgrade a database to an earlier version',
50 'drop_version_control': 'removes version control from a database',
51 'manage': 'creates a Python script that runs Migrate with a set of default values',
52 'test': 'performs the upgrade and downgrade command on the given database',
53 'compare_model_to_db': 'compare MetaData against the current database state',
54 'create_model': 'dump the current database as a Python model to stdout',
55 'make_update_script_for_model': 'create a script changing the old MetaData to the new (current) MetaData',
56 'update_db_from_model': 'modify the database to match the structure of the current MetaData',
57 }
58 __all__ = command_desc.keys()
59
60 Repository = repository.Repository
61 ControlledSchema = schema.ControlledSchema
62 VerNum = version.VerNum
63 PythonScript = script_.PythonScript
64 SqlScript = script_.SqlScript
65
66
67 # deprecated
68 def help(cmd=None, **opts):
69 """%prog help COMMAND
70
71 Displays help on a given command.
72 """
73 if cmd is None:
74 raise exceptions.UsageError(None)
75 try:
76 func = globals()[cmd]
77 except:
78 raise exceptions.UsageError(
79 "'%s' isn't a valid command. Try 'help COMMAND'" % cmd)
80 ret = func.__doc__
81 if sys.argv[0]:
82 ret = ret.replace('%prog', sys.argv[0])
83 return ret
84
85 @catch_known_errors
86 def create(repository, name, **opts):
87 """%prog create REPOSITORY_PATH NAME [--table=TABLE]
88
89 Create an empty repository at the specified path.
90
91 You can specify the version_table to be used; by default, it is
92 'migrate_version'. This table is created in all version-controlled
93 databases.
94 """
95 repo_path = Repository.create(repository, name, **opts)
96
97
98 @catch_known_errors
99 def script(description, repository, **opts):
100 """%prog script DESCRIPTION REPOSITORY_PATH
101
102 Create an empty change script using the next unused version number
103 appended with the given description.
104
105 For instance, manage.py script "Add initial tables" creates:
106 repository/versions/001_Add_initial_tables.py
107 """
108 repo = Repository(repository)
109 repo.create_script(description, **opts)
110
111
112 @catch_known_errors
113 def script_sql(database, repository, **opts):
114 """%prog script_sql DATABASE REPOSITORY_PATH
115
116 Create empty change SQL scripts for given DATABASE, where DATABASE
117 is either specific ('postgres', 'mysql', 'oracle', 'sqlite', etc.)
118 or generic ('default').
119
120 For instance, manage.py script_sql postgres creates:
121 repository/versions/001_postgres_upgrade.sql and
122 repository/versions/001_postgres_postgres.sql
123 """
124 repo = Repository(repository)
125 repo.create_script_sql(database, **opts)
126
127
128 def version(repository, **opts):
129 """%prog version REPOSITORY_PATH
130
131 Display the latest version available in a repository.
132 """
133 repo = Repository(repository)
134 return repo.latest
135
136
137 @with_engine
138 def db_version(url, repository, **opts):
139 """%prog db_version URL REPOSITORY_PATH
140
141 Show the current version of the repository with the given
142 connection string, under version control of the specified
143 repository.
144
145 The url should be any valid SQLAlchemy connection string.
146 """
147 engine = opts.pop('engine')
148 schema = ControlledSchema(engine, repository)
149 return schema.version
150
151
152 def source(version, dest=None, repository=None, **opts):
153 """%prog source VERSION [DESTINATION] --repository=REPOSITORY_PATH
154
155 Display the Python code for a particular version in this
156 repository. Save it to the file at DESTINATION or, if omitted,
157 send to stdout.
158 """
159 if repository is None:
160 raise exceptions.UsageError("A repository must be specified")
161 repo = Repository(repository)
162 ret = repo.version(version).script().source()
163 if dest is not None:
164 dest = open(dest, 'w')
165 dest.write(ret)
166 dest.close()
167 ret = None
168 return ret
169
170
171 def upgrade(url, repository, version=None, **opts):
172 """%prog upgrade URL REPOSITORY_PATH [VERSION] [--preview_py|--preview_sql]
173
174 Upgrade a database to a later version.
175
176 This runs the upgrade() function defined in your change scripts.
177
178 By default, the database is updated to the latest available
179 version. You may specify a version instead, if you wish.
180
181 You may preview the Python or SQL code to be executed, rather than
182 actually executing it, using the appropriate 'preview' option.
183 """
184 err = "Cannot upgrade a database of version %s to version %s. "\
185 "Try 'downgrade' instead."
186 return _migrate(url, repository, version, upgrade=True, err=err, **opts)
187
188
189 def downgrade(url, repository, version, **opts):
190 """%prog downgrade URL REPOSITORY_PATH VERSION [--preview_py|--preview_sql]
191
192 Downgrade a database to an earlier version.
193
194 This is the reverse of upgrade; this runs the downgrade() function
195 defined in your change scripts.
196
197 You may preview the Python or SQL code to be executed, rather than
198 actually executing it, using the appropriate 'preview' option.
199 """
200 err = "Cannot downgrade a database of version %s to version %s. "\
201 "Try 'upgrade' instead."
202 return _migrate(url, repository, version, upgrade=False, err=err, **opts)
203
204 @with_engine
205 def test(url, repository, **opts):
206 """%prog test URL REPOSITORY_PATH [VERSION]
207
208 Performs the upgrade and downgrade option on the given
209 database. This is not a real test and may leave the database in a
210 bad state. You should therefore better run the test on a copy of
211 your database.
212 """
213 engine = opts.pop('engine')
214 repos = Repository(repository)
215 script = repos.version(None).script()
216
217 # Upgrade
218 log.info("Upgrading...")
219 script.run(engine, 1)
220 log.info("done")
221
222 log.info("Downgrading...")
223 script.run(engine, -1)
224 log.info("done")
225 log.info("Success")
226
227
228 @with_engine
229 def version_control(url, repository, version=None, **opts):
230 """%prog version_control URL REPOSITORY_PATH [VERSION]
231
232 Mark a database as under this repository's version control.
233
234 Once a database is under version control, schema changes should
235 only be done via change scripts in this repository.
236
237 This creates the table version_table in the database.
238
239 The url should be any valid SQLAlchemy connection string.
240
241 By default, the database begins at version 0 and is assumed to be
242 empty. If the database is not empty, you may specify a version at
243 which to begin instead. No attempt is made to verify this
244 version's correctness - the database schema is expected to be
245 identical to what it would be if the database were created from
246 scratch.
247 """
248 engine = opts.pop('engine')
249 ControlledSchema.create(engine, repository, version)
250
251
252 @with_engine
253 def drop_version_control(url, repository, **opts):
254 """%prog drop_version_control URL REPOSITORY_PATH
255
256 Removes version control from a database.
257 """
258 engine = opts.pop('engine')
259 schema = ControlledSchema(engine, repository)
260 schema.drop()
261
262
263 def manage(file, **opts):
264 """%prog manage FILENAME [VARIABLES...]
265
266 Creates a script that runs Migrate with a set of default values.
267
268 For example::
269
270 %prog manage manage.py --repository=/path/to/repository \
271 --url=sqlite:///project.db
272
273 would create the script manage.py. The following two commands
274 would then have exactly the same results::
275
276 python manage.py version
277 %prog version --repository=/path/to/repository
278 """
279 Repository.create_manage_file(file, **opts)
280
281
282 @with_engine
283 def compare_model_to_db(url, repository, model, **opts):
284 """%prog compare_model_to_db URL REPOSITORY_PATH MODEL
285
286 Compare the current model (assumed to be a module level variable
287 of type sqlalchemy.MetaData) against the current database.
288
289 NOTE: This is EXPERIMENTAL.
290 """ # TODO: get rid of EXPERIMENTAL label
291 engine = opts.pop('engine')
292 return ControlledSchema.compare_model_to_db(engine, model, repository)
293
294
295 @with_engine
296 def create_model(url, repository, **opts):
297 """%prog create_model URL REPOSITORY_PATH [DECLERATIVE=True]
298
299 Dump the current database as a Python model to stdout.
300
301 NOTE: This is EXPERIMENTAL.
302 """ # TODO: get rid of EXPERIMENTAL label
303 engine = opts.pop('engine')
304 declarative = opts.get('declarative', False)
305 return ControlledSchema.create_model(engine, repository, declarative)
306
307
308 @catch_known_errors
309 @with_engine
310 def make_update_script_for_model(url, repository, oldmodel, model, **opts):
311 """%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH
312
313 Create a script changing the old Python model to the new (current)
314 Python model, sending to stdout.
315
316 NOTE: This is EXPERIMENTAL.
317 """ # TODO: get rid of EXPERIMENTAL label
318 engine = opts.pop('engine')
319 return PythonScript.make_update_script_for_model(
320 engine, oldmodel, model, repository, **opts)
321
322
323 @with_engine
324 def update_db_from_model(url, repository, model, **opts):
325 """%prog update_db_from_model URL REPOSITORY_PATH MODEL
326
327 Modify the database to match the structure of the current Python
328 model. This also sets the db_version number to the latest in the
329 repository.
330
331 NOTE: This is EXPERIMENTAL.
332 """ # TODO: get rid of EXPERIMENTAL label
333 engine = opts.pop('engine')
334 schema = ControlledSchema(engine, repository)
335 schema.update_db_from_model(model)
336
337 @with_engine
338 def _migrate(url, repository, version, upgrade, err, **opts):
339 engine = opts.pop('engine')
340 url = str(engine.url)
341 schema = ControlledSchema(engine, repository)
342 version = _migrate_version(schema, version, upgrade, err)
343
344 changeset = schema.changeset(version)
345 for ver, change in changeset:
346 nextver = ver + changeset.step
347 log.info('%s -> %s... ', ver, nextver)
348
349 if opts.get('preview_sql'):
350 if isinstance(change, PythonScript):
351 log.info(change.preview_sql(url, changeset.step, **opts))
352 elif isinstance(change, SqlScript):
353 log.info(change.source())
354
355 elif opts.get('preview_py'):
356 if not isinstance(change, PythonScript):
357 raise exceptions.UsageError("Python source can be only displayed"
358 " for python migration files")
359 source_ver = max(ver, nextver)
360 module = schema.repository.version(source_ver).script().module
361 funcname = upgrade and "upgrade" or "downgrade"
362 func = getattr(module, funcname)
363 log.info(inspect.getsource(func))
364 else:
365 schema.runchange(ver, change, changeset.step)
366 log.info('done')
367
368
369 def _migrate_version(schema, version, upgrade, err):
370 if version is None:
371 return version
372 # Version is specified: ensure we're upgrading in the right direction
373 # (current version < target version for upgrading; reverse for down)
374 version = VerNum(version)
375 cur = schema.version
376 if upgrade is not None:
377 if upgrade:
378 direction = cur <= version
379 else:
380 direction = cur >= version
381 if not direction:
382 raise exceptions.KnownError(err % (cur, version))
383 return version
@@ -0,0 +1,27
1 """
2 Configuration parser module.
3 """
4
5 from ConfigParser import ConfigParser
6
7 from rhodecode.lib.dbmigrate.migrate.versioning.config import *
8 from rhodecode.lib.dbmigrate.migrate.versioning import pathed
9
10
11 class Parser(ConfigParser):
12 """A project configuration file."""
13
14 def to_dict(self, sections=None):
15 """It's easier to access config values like dictionaries"""
16 return self._sections
17
18
19 class Config(pathed.Pathed, Parser):
20 """Configuration class."""
21
22 def __init__(self, path, *p, **k):
23 """Confirm the config file exists; read it."""
24 self.require_found(path)
25 pathed.Pathed.__init__(self, path)
26 Parser.__init__(self, *p, **k)
27 self.read(path)
@@ -0,0 +1,14
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 from sqlalchemy.util import OrderedDict
5
6
7 __all__ = ['databases', 'operations']
8
9 databases = ('sqlite', 'postgres', 'mysql', 'oracle', 'mssql', 'firebird')
10
11 # Map operation names to function names
12 operations = OrderedDict()
13 operations['upgrade'] = 'upgrade'
14 operations['downgrade'] = 'downgrade'
@@ -0,0 +1,253
1 """
2 Code to generate a Python model from a database or differences
3 between a model and database.
4
5 Some of this is borrowed heavily from the AutoCode project at:
6 http://code.google.com/p/sqlautocode/
7 """
8
9 import sys
10 import logging
11
12 import sqlalchemy
13
14 from rhodecode.lib.dbmigrate import migrate
15 from rhodecode.lib.dbmigrate.migrate import changeset
16
17 log = logging.getLogger(__name__)
18 HEADER = """
19 ## File autogenerated by genmodel.py
20
21 from sqlalchemy import *
22 meta = MetaData()
23 """
24
25 DECLARATIVE_HEADER = """
26 ## File autogenerated by genmodel.py
27
28 from sqlalchemy import *
29 from sqlalchemy.ext import declarative
30
31 Base = declarative.declarative_base()
32 """
33
34
35 class ModelGenerator(object):
36
37 def __init__(self, diff, engine, declarative=False):
38 self.diff = diff
39 self.engine = engine
40 self.declarative = declarative
41
42 def column_repr(self, col):
43 kwarg = []
44 if col.key != col.name:
45 kwarg.append('key')
46 if col.primary_key:
47 col.primary_key = True # otherwise it dumps it as 1
48 kwarg.append('primary_key')
49 if not col.nullable:
50 kwarg.append('nullable')
51 if col.onupdate:
52 kwarg.append('onupdate')
53 if col.default:
54 if col.primary_key:
55 # I found that PostgreSQL automatically creates a
56 # default value for the sequence, but let's not show
57 # that.
58 pass
59 else:
60 kwarg.append('default')
61 ks = ', '.join('%s=%r' % (k, getattr(col, k)) for k in kwarg)
62
63 # crs: not sure if this is good idea, but it gets rid of extra
64 # u''
65 name = col.name.encode('utf8')
66
67 type_ = col.type
68 for cls in col.type.__class__.__mro__:
69 if cls.__module__ == 'sqlalchemy.types' and \
70 not cls.__name__.isupper():
71 if cls is not type_.__class__:
72 type_ = cls()
73 break
74
75 data = {
76 'name': name,
77 'type': type_,
78 'constraints': ', '.join([repr(cn) for cn in col.constraints]),
79 'args': ks and ks or ''}
80
81 if data['constraints']:
82 if data['args']:
83 data['args'] = ',' + data['args']
84
85 if data['constraints'] or data['args']:
86 data['maybeComma'] = ','
87 else:
88 data['maybeComma'] = ''
89
90 commonStuff = """ %(maybeComma)s %(constraints)s %(args)s)""" % data
91 commonStuff = commonStuff.strip()
92 data['commonStuff'] = commonStuff
93 if self.declarative:
94 return """%(name)s = Column(%(type)r%(commonStuff)s""" % data
95 else:
96 return """Column(%(name)r, %(type)r%(commonStuff)s""" % data
97
98 def getTableDefn(self, table):
99 out = []
100 tableName = table.name
101 if self.declarative:
102 out.append("class %(table)s(Base):" % {'table': tableName})
103 out.append(" __tablename__ = '%(table)s'" % {'table': tableName})
104 for col in table.columns:
105 out.append(" %s" % self.column_repr(col))
106 else:
107 out.append("%(table)s = Table('%(table)s', meta," % \
108 {'table': tableName})
109 for col in table.columns:
110 out.append(" %s," % self.column_repr(col))
111 out.append(")")
112 return out
113
114 def _get_tables(self, missingA=False, missingB=False, modified=False):
115 to_process = []
116 for bool_, names, metadata in (
117 (missingA, self.diff.tables_missing_from_A, self.diff.metadataB),
118 (missingB, self.diff.tables_missing_from_B, self.diff.metadataA),
119 (modified, self.diff.tables_different, self.diff.metadataA),
120 ):
121 if bool_:
122 for name in names:
123 yield metadata.tables.get(name)
124
125 def toPython(self):
126 """Assume database is current and model is empty."""
127 out = []
128 if self.declarative:
129 out.append(DECLARATIVE_HEADER)
130 else:
131 out.append(HEADER)
132 out.append("")
133 for table in self._get_tables(missingA=True):
134 out.extend(self.getTableDefn(table))
135 out.append("")
136 return '\n'.join(out)
137
138 def toUpgradeDowngradePython(self, indent=' '):
139 ''' Assume model is most current and database is out-of-date. '''
140 decls = ['from rhodecode.lib.dbmigrate.migrate.changeset import schema',
141 'meta = MetaData()']
142 for table in self._get_tables(
143 missingA=True, missingB=True, modified=True
144 ):
145 decls.extend(self.getTableDefn(table))
146
147 upgradeCommands, downgradeCommands = [], []
148 for tableName in self.diff.tables_missing_from_A:
149 upgradeCommands.append("%(table)s.drop()" % {'table': tableName})
150 downgradeCommands.append("%(table)s.create()" % \
151 {'table': tableName})
152 for tableName in self.diff.tables_missing_from_B:
153 upgradeCommands.append("%(table)s.create()" % {'table': tableName})
154 downgradeCommands.append("%(table)s.drop()" % {'table': tableName})
155
156 for tableName in self.diff.tables_different:
157 dbTable = self.diff.metadataB.tables[tableName]
158 missingInDatabase, missingInModel, diffDecl = \
159 self.diff.colDiffs[tableName]
160 for col in missingInDatabase:
161 upgradeCommands.append('%s.columns[%r].create()' % (
162 modelTable, col.name))
163 downgradeCommands.append('%s.columns[%r].drop()' % (
164 modelTable, col.name))
165 for col in missingInModel:
166 upgradeCommands.append('%s.columns[%r].drop()' % (
167 modelTable, col.name))
168 downgradeCommands.append('%s.columns[%r].create()' % (
169 modelTable, col.name))
170 for modelCol, databaseCol, modelDecl, databaseDecl in diffDecl:
171 upgradeCommands.append(
172 'assert False, "Can\'t alter columns: %s:%s=>%s"',
173 modelTable, modelCol.name, databaseCol.name)
174 downgradeCommands.append(
175 'assert False, "Can\'t alter columns: %s:%s=>%s"',
176 modelTable, modelCol.name, databaseCol.name)
177 pre_command = ' meta.bind = migrate_engine'
178
179 return (
180 '\n'.join(decls),
181 '\n'.join([pre_command] + ['%s%s' % (indent, line) for line in upgradeCommands]),
182 '\n'.join([pre_command] + ['%s%s' % (indent, line) for line in downgradeCommands]))
183
184 def _db_can_handle_this_change(self, td):
185 if (td.columns_missing_from_B
186 and not td.columns_missing_from_A
187 and not td.columns_different):
188 # Even sqlite can handle this.
189 return True
190 else:
191 return not self.engine.url.drivername.startswith('sqlite')
192
193 def applyModel(self):
194 """Apply model to current database."""
195
196 meta = sqlalchemy.MetaData(self.engine)
197
198 for table in self._get_tables(missingA=True):
199 table = table.tometadata(meta)
200 table.drop()
201 for table in self._get_tables(missingB=True):
202 table = table.tometadata(meta)
203 table.create()
204 for modelTable in self._get_tables(modified=True):
205 tableName = modelTable.name
206 modelTable = modelTable.tometadata(meta)
207 dbTable = self.diff.metadataB.tables[tableName]
208
209 td = self.diff.tables_different[tableName]
210
211 if self._db_can_handle_this_change(td):
212
213 for col in td.columns_missing_from_B:
214 modelTable.columns[col].create()
215 for col in td.columns_missing_from_A:
216 dbTable.columns[col].drop()
217 # XXX handle column changes here.
218 else:
219 # Sqlite doesn't support drop column, so you have to
220 # do more: create temp table, copy data to it, drop
221 # old table, create new table, copy data back.
222 #
223 # I wonder if this is guaranteed to be unique?
224 tempName = '_temp_%s' % modelTable.name
225
226 def getCopyStatement():
227 preparer = self.engine.dialect.preparer
228 commonCols = []
229 for modelCol in modelTable.columns:
230 if modelCol.name in dbTable.columns:
231 commonCols.append(modelCol.name)
232 commonColsStr = ', '.join(commonCols)
233 return 'INSERT INTO %s (%s) SELECT %s FROM %s' % \
234 (tableName, commonColsStr, commonColsStr, tempName)
235
236 # Move the data in one transaction, so that we don't
237 # leave the database in a nasty state.
238 connection = self.engine.connect()
239 trans = connection.begin()
240 try:
241 connection.execute(
242 'CREATE TEMPORARY TABLE %s as SELECT * from %s' % \
243 (tempName, modelTable.name))
244 # make sure the drop takes place inside our
245 # transaction with the bind parameter
246 modelTable.drop(bind=connection)
247 modelTable.create(bind=connection)
248 connection.execute(getCopyStatement())
249 connection.execute('DROP TABLE %s' % tempName)
250 trans.commit()
251 except:
252 trans.rollback()
253 raise
@@ -0,0 +1,100
1 """
2 Script to migrate repository from sqlalchemy <= 0.4.4 to the new
3 repository schema. This shouldn't use any other migrate modules, so
4 that it can work in any version.
5 """
6
7 import os
8 import sys
9 import logging
10
11 log = logging.getLogger(__name__)
12
13
14 def usage():
15 """Gives usage information."""
16 print """Usage: %(prog)s repository-to-migrate
17
18 Upgrade your repository to the new flat format.
19
20 NOTE: You should probably make a backup before running this.
21 """ % {'prog': sys.argv[0]}
22
23 sys.exit(1)
24
25
26 def delete_file(filepath):
27 """Deletes a file and prints a message."""
28 log.info('Deleting file: %s' % filepath)
29 os.remove(filepath)
30
31
32 def move_file(src, tgt):
33 """Moves a file and prints a message."""
34 log.info('Moving file %s to %s' % (src, tgt))
35 if os.path.exists(tgt):
36 raise Exception(
37 'Cannot move file %s because target %s already exists' % \
38 (src, tgt))
39 os.rename(src, tgt)
40
41
42 def delete_directory(dirpath):
43 """Delete a directory and print a message."""
44 log.info('Deleting directory: %s' % dirpath)
45 os.rmdir(dirpath)
46
47
48 def migrate_repository(repos):
49 """Does the actual migration to the new repository format."""
50 log.info('Migrating repository at: %s to new format' % repos)
51 versions = '%s/versions' % repos
52 dirs = os.listdir(versions)
53 # Only use int's in list.
54 numdirs = [int(dirname) for dirname in dirs if dirname.isdigit()]
55 numdirs.sort() # Sort list.
56 for dirname in numdirs:
57 origdir = '%s/%s' % (versions, dirname)
58 log.info('Working on directory: %s' % origdir)
59 files = os.listdir(origdir)
60 files.sort()
61 for filename in files:
62 # Delete compiled Python files.
63 if filename.endswith('.pyc') or filename.endswith('.pyo'):
64 delete_file('%s/%s' % (origdir, filename))
65
66 # Delete empty __init__.py files.
67 origfile = '%s/__init__.py' % origdir
68 if os.path.exists(origfile) and len(open(origfile).read()) == 0:
69 delete_file(origfile)
70
71 # Move sql upgrade scripts.
72 if filename.endswith('.sql'):
73 version, dbms, operation = filename.split('.', 3)[0:3]
74 origfile = '%s/%s' % (origdir, filename)
75 # For instance: 2.postgres.upgrade.sql ->
76 # 002_postgres_upgrade.sql
77 tgtfile = '%s/%03d_%s_%s.sql' % (
78 versions, int(version), dbms, operation)
79 move_file(origfile, tgtfile)
80
81 # Move Python upgrade script.
82 pyfile = '%s.py' % dirname
83 pyfilepath = '%s/%s' % (origdir, pyfile)
84 if os.path.exists(pyfilepath):
85 tgtfile = '%s/%03d.py' % (versions, int(dirname))
86 move_file(pyfilepath, tgtfile)
87
88 # Try to remove directory. Will fail if it's not empty.
89 delete_directory(origdir)
90
91
92 def main():
93 """Main function to be called when using this script."""
94 if len(sys.argv) != 2:
95 usage()
96 migrate_repository(sys.argv[1])
97
98
99 if __name__ == '__main__':
100 main()
@@ -0,0 +1,75
1 """
2 A path/directory class.
3 """
4
5 import os
6 import shutil
7 import logging
8
9 from rhodecode.lib.dbmigrate.migrate import exceptions
10 from rhodecode.lib.dbmigrate.migrate.versioning.config import *
11 from rhodecode.lib.dbmigrate.migrate.versioning.util import KeyedInstance
12
13
14 log = logging.getLogger(__name__)
15
16 class Pathed(KeyedInstance):
17 """
18 A class associated with a path/directory tree.
19
20 Only one instance of this class may exist for a particular file;
21 __new__ will return an existing instance if possible
22 """
23 parent = None
24
25 @classmethod
26 def _key(cls, path):
27 return str(path)
28
29 def __init__(self, path):
30 self.path = path
31 if self.__class__.parent is not None:
32 self._init_parent(path)
33
34 def _init_parent(self, path):
35 """Try to initialize this object's parent, if it has one"""
36 parent_path = self.__class__._parent_path(path)
37 self.parent = self.__class__.parent(parent_path)
38 log.debug("Getting parent %r:%r" % (self.__class__.parent, parent_path))
39 self.parent._init_child(path, self)
40
41 def _init_child(self, child, path):
42 """Run when a child of this object is initialized.
43
44 Parameters: the child object; the path to this object (its
45 parent)
46 """
47
48 @classmethod
49 def _parent_path(cls, path):
50 """
51 Fetch the path of this object's parent from this object's path.
52 """
53 # os.path.dirname(), but strip directories like files (like
54 # unix basename)
55 #
56 # Treat directories like files...
57 if path[-1] == '/':
58 path = path[:-1]
59 ret = os.path.dirname(path)
60 return ret
61
62 @classmethod
63 def require_notfound(cls, path):
64 """Ensures a given path does not already exist"""
65 if os.path.exists(path):
66 raise exceptions.PathFoundError(path)
67
68 @classmethod
69 def require_found(cls, path):
70 """Ensures a given path already exists"""
71 if not os.path.exists(path):
72 raise exceptions.PathNotFoundError(path)
73
74 def __str__(self):
75 return self.path
@@ -0,0 +1,231
1 """
2 SQLAlchemy migrate repository management.
3 """
4 import os
5 import shutil
6 import string
7 import logging
8
9 from pkg_resources import resource_filename
10 from tempita import Template as TempitaTemplate
11
12 from rhodecode.lib.dbmigrate.migrate import exceptions
13 from rhodecode.lib.dbmigrate.migrate.versioning import version, pathed, cfgparse
14 from rhodecode.lib.dbmigrate.migrate.versioning.template import Template
15 from rhodecode.lib.dbmigrate.migrate.versioning.config import *
16
17
18 log = logging.getLogger(__name__)
19
20 class Changeset(dict):
21 """A collection of changes to be applied to a database.
22
23 Changesets are bound to a repository and manage a set of
24 scripts from that repository.
25
26 Behaves like a dict, for the most part. Keys are ordered based on step value.
27 """
28
29 def __init__(self, start, *changes, **k):
30 """
31 Give a start version; step must be explicitly stated.
32 """
33 self.step = k.pop('step', 1)
34 self.start = version.VerNum(start)
35 self.end = self.start
36 for change in changes:
37 self.add(change)
38
39 def __iter__(self):
40 return iter(self.items())
41
42 def keys(self):
43 """
44 In a series of upgrades x -> y, keys are version x. Sorted.
45 """
46 ret = super(Changeset, self).keys()
47 # Reverse order if downgrading
48 ret.sort(reverse=(self.step < 1))
49 return ret
50
51 def values(self):
52 return [self[k] for k in self.keys()]
53
54 def items(self):
55 return zip(self.keys(), self.values())
56
57 def add(self, change):
58 """Add new change to changeset"""
59 key = self.end
60 self.end += self.step
61 self[key] = change
62
63 def run(self, *p, **k):
64 """Run the changeset scripts"""
65 for version, script in self:
66 script.run(*p, **k)
67
68
69 class Repository(pathed.Pathed):
70 """A project's change script repository"""
71
72 _config = 'migrate.cfg'
73 _versions = 'versions'
74
75 def __init__(self, path):
76 log.debug('Loading repository %s...' % path)
77 self.verify(path)
78 super(Repository, self).__init__(path)
79 self.config = cfgparse.Config(os.path.join(self.path, self._config))
80 self.versions = version.Collection(os.path.join(self.path,
81 self._versions))
82 log.debug('Repository %s loaded successfully' % path)
83 log.debug('Config: %r' % self.config.to_dict())
84
85 @classmethod
86 def verify(cls, path):
87 """
88 Ensure the target path is a valid repository.
89
90 :raises: :exc:`InvalidRepositoryError <migrate.exceptions.InvalidRepositoryError>`
91 """
92 # Ensure the existence of required files
93 try:
94 cls.require_found(path)
95 cls.require_found(os.path.join(path, cls._config))
96 cls.require_found(os.path.join(path, cls._versions))
97 except exceptions.PathNotFoundError, e:
98 raise exceptions.InvalidRepositoryError(path)
99
100 @classmethod
101 def prepare_config(cls, tmpl_dir, name, options=None):
102 """
103 Prepare a project configuration file for a new project.
104
105 :param tmpl_dir: Path to Repository template
106 :param config_file: Name of the config file in Repository template
107 :param name: Repository name
108 :type tmpl_dir: string
109 :type config_file: string
110 :type name: string
111 :returns: Populated config file
112 """
113 if options is None:
114 options = {}
115 options.setdefault('version_table', 'migrate_version')
116 options.setdefault('repository_id', name)
117 options.setdefault('required_dbs', [])
118
119 tmpl = open(os.path.join(tmpl_dir, cls._config)).read()
120 ret = TempitaTemplate(tmpl).substitute(options)
121
122 # cleanup
123 del options['__template_name__']
124
125 return ret
126
127 @classmethod
128 def create(cls, path, name, **opts):
129 """Create a repository at a specified path"""
130 cls.require_notfound(path)
131 theme = opts.pop('templates_theme', None)
132 t_path = opts.pop('templates_path', None)
133
134 # Create repository
135 tmpl_dir = Template(t_path).get_repository(theme=theme)
136 shutil.copytree(tmpl_dir, path)
137
138 # Edit config defaults
139 config_text = cls.prepare_config(tmpl_dir, name, options=opts)
140 fd = open(os.path.join(path, cls._config), 'w')
141 fd.write(config_text)
142 fd.close()
143
144 opts['repository_name'] = name
145
146 # Create a management script
147 manager = os.path.join(path, 'manage.py')
148 Repository.create_manage_file(manager, templates_theme=theme,
149 templates_path=t_path, **opts)
150
151 return cls(path)
152
153 def create_script(self, description, **k):
154 """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`"""
155 self.versions.create_new_python_version(description, **k)
156
157 def create_script_sql(self, database, **k):
158 """API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`"""
159 self.versions.create_new_sql_version(database, **k)
160
161 @property
162 def latest(self):
163 """API to :attr:`migrate.versioning.version.Collection.latest`"""
164 return self.versions.latest
165
166 @property
167 def version_table(self):
168 """Returns version_table name specified in config"""
169 return self.config.get('db_settings', 'version_table')
170
171 @property
172 def id(self):
173 """Returns repository id specified in config"""
174 return self.config.get('db_settings', 'repository_id')
175
176 def version(self, *p, **k):
177 """API to :attr:`migrate.versioning.version.Collection.version`"""
178 return self.versions.version(*p, **k)
179
180 @classmethod
181 def clear(cls):
182 # TODO: deletes repo
183 super(Repository, cls).clear()
184 version.Collection.clear()
185
186 def changeset(self, database, start, end=None):
187 """Create a changeset to migrate this database from ver. start to end/latest.
188
189 :param database: name of database to generate changeset
190 :param start: version to start at
191 :param end: version to end at (latest if None given)
192 :type database: string
193 :type start: int
194 :type end: int
195 :returns: :class:`Changeset instance <migration.versioning.repository.Changeset>`
196 """
197 start = version.VerNum(start)
198
199 if end is None:
200 end = self.latest
201 else:
202 end = version.VerNum(end)
203
204 if start <= end:
205 step = 1
206 range_mod = 1
207 op = 'upgrade'
208 else:
209 step = -1
210 range_mod = 0
211 op = 'downgrade'
212
213 versions = range(start + range_mod, end + range_mod, step)
214 changes = [self.version(v).script(database, op) for v in versions]
215 ret = Changeset(start, step=step, *changes)
216 return ret
217
218 @classmethod
219 def create_manage_file(cls, file_, **opts):
220 """Create a project management script (manage.py)
221
222 :param file_: Destination file to be written
223 :param opts: Options that are passed to :func:`migrate.versioning.shell.main`
224 """
225 mng_file = Template(opts.pop('templates_path', None))\
226 .get_manage(theme=opts.pop('templates_theme', None))
227
228 tmpl = open(mng_file).read()
229 fd = open(file_, 'w')
230 fd.write(TempitaTemplate(tmpl).substitute(opts))
231 fd.close()
@@ -0,0 +1,213
1 """
2 Database schema version management.
3 """
4 import sys
5 import logging
6
7 from sqlalchemy import (Table, Column, MetaData, String, Text, Integer,
8 create_engine)
9 from sqlalchemy.sql import and_
10 from sqlalchemy import exceptions as sa_exceptions
11 from sqlalchemy.sql import bindparam
12
13 from rhodecode.lib.dbmigrate.migrate import exceptions
14 from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff
15 from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository
16 from rhodecode.lib.dbmigrate.migrate.versioning.util import load_model
17 from rhodecode.lib.dbmigrate.migrate.versioning.version import VerNum
18
19
20 log = logging.getLogger(__name__)
21
22 class ControlledSchema(object):
23 """A database under version control"""
24
25 def __init__(self, engine, repository):
26 if isinstance(repository, basestring):
27 repository = Repository(repository)
28 self.engine = engine
29 self.repository = repository
30 self.meta = MetaData(engine)
31 self.load()
32
33 def __eq__(self, other):
34 """Compare two schemas by repositories and versions"""
35 return (self.repository is other.repository \
36 and self.version == other.version)
37
38 def load(self):
39 """Load controlled schema version info from DB"""
40 tname = self.repository.version_table
41 try:
42 if not hasattr(self, 'table') or self.table is None:
43 self.table = Table(tname, self.meta, autoload=True)
44
45 result = self.engine.execute(self.table.select(
46 self.table.c.repository_id == str(self.repository.id)))
47
48 data = list(result)[0]
49 except:
50 cls, exc, tb = sys.exc_info()
51 raise exceptions.DatabaseNotControlledError, exc.__str__(), tb
52
53 self.version = data['version']
54 return data
55
56 def drop(self):
57 """
58 Remove version control from a database.
59 """
60 try:
61 self.table.drop()
62 except (sa_exceptions.SQLError):
63 raise exceptions.DatabaseNotControlledError(str(self.table))
64
65 def changeset(self, version=None):
66 """API to Changeset creation.
67
68 Uses self.version for start version and engine.name
69 to get database name.
70 """
71 database = self.engine.name
72 start_ver = self.version
73 changeset = self.repository.changeset(database, start_ver, version)
74 return changeset
75
76 def runchange(self, ver, change, step):
77 startver = ver
78 endver = ver + step
79 # Current database version must be correct! Don't run if corrupt!
80 if self.version != startver:
81 raise exceptions.InvalidVersionError("%s is not %s" % \
82 (self.version, startver))
83 # Run the change
84 change.run(self.engine, step)
85
86 # Update/refresh database version
87 self.update_repository_table(startver, endver)
88 self.load()
89
90 def update_repository_table(self, startver, endver):
91 """Update version_table with new information"""
92 update = self.table.update(and_(self.table.c.version == int(startver),
93 self.table.c.repository_id == str(self.repository.id)))
94 self.engine.execute(update, version=int(endver))
95
96 def upgrade(self, version=None):
97 """
98 Upgrade (or downgrade) to a specified version, or latest version.
99 """
100 changeset = self.changeset(version)
101 for ver, change in changeset:
102 self.runchange(ver, change, changeset.step)
103
104 def update_db_from_model(self, model):
105 """
106 Modify the database to match the structure of the current Python model.
107 """
108 model = load_model(model)
109
110 diff = schemadiff.getDiffOfModelAgainstDatabase(
111 model, self.engine, excludeTables=[self.repository.version_table]
112 )
113 genmodel.ModelGenerator(diff,self.engine).applyModel()
114
115 self.update_repository_table(self.version, int(self.repository.latest))
116
117 self.load()
118
119 @classmethod
120 def create(cls, engine, repository, version=None):
121 """
122 Declare a database to be under a repository's version control.
123
124 :raises: :exc:`DatabaseAlreadyControlledError`
125 :returns: :class:`ControlledSchema`
126 """
127 # Confirm that the version # is valid: positive, integer,
128 # exists in repos
129 if isinstance(repository, basestring):
130 repository = Repository(repository)
131 version = cls._validate_version(repository, version)
132 table = cls._create_table_version(engine, repository, version)
133 # TODO: history table
134 # Load repository information and return
135 return cls(engine, repository)
136
137 @classmethod
138 def _validate_version(cls, repository, version):
139 """
140 Ensures this is a valid version number for this repository.
141
142 :raises: :exc:`InvalidVersionError` if invalid
143 :return: valid version number
144 """
145 if version is None:
146 version = 0
147 try:
148 version = VerNum(version) # raises valueerror
149 if version < 0 or version > repository.latest:
150 raise ValueError()
151 except ValueError:
152 raise exceptions.InvalidVersionError(version)
153 return version
154
155 @classmethod
156 def _create_table_version(cls, engine, repository, version):
157 """
158 Creates the versioning table in a database.
159
160 :raises: :exc:`DatabaseAlreadyControlledError`
161 """
162 # Create tables
163 tname = repository.version_table
164 meta = MetaData(engine)
165
166 table = Table(
167 tname, meta,
168 Column('repository_id', String(250), primary_key=True),
169 Column('repository_path', Text),
170 Column('version', Integer), )
171
172 # there can be multiple repositories/schemas in the same db
173 if not table.exists():
174 table.create()
175
176 # test for existing repository_id
177 s = table.select(table.c.repository_id == bindparam("repository_id"))
178 result = engine.execute(s, repository_id=repository.id)
179 if result.fetchone():
180 raise exceptions.DatabaseAlreadyControlledError
181
182 # Insert data
183 engine.execute(table.insert().values(
184 repository_id=repository.id,
185 repository_path=repository.path,
186 version=int(version)))
187 return table
188
189 @classmethod
190 def compare_model_to_db(cls, engine, model, repository):
191 """
192 Compare the current model against the current database.
193 """
194 if isinstance(repository, basestring):
195 repository = Repository(repository)
196 model = load_model(model)
197
198 diff = schemadiff.getDiffOfModelAgainstDatabase(
199 model, engine, excludeTables=[repository.version_table])
200 return diff
201
202 @classmethod
203 def create_model(cls, engine, repository, declarative=False):
204 """
205 Dump the current database as a Python model.
206 """
207 if isinstance(repository, basestring):
208 repository = Repository(repository)
209
210 diff = schemadiff.getDiffOfModelAgainstDatabase(
211 MetaData(), engine, excludeTables=[repository.version_table]
212 )
213 return genmodel.ModelGenerator(diff, engine, declarative).toPython()
@@ -0,0 +1,285
1 """
2 Schema differencing support.
3 """
4
5 import logging
6 import sqlalchemy
7
8 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_06
9 from sqlalchemy.types import Float
10
11 log = logging.getLogger(__name__)
12
13 def getDiffOfModelAgainstDatabase(metadata, engine, excludeTables=None):
14 """
15 Return differences of model against database.
16
17 :return: object which will evaluate to :keyword:`True` if there \
18 are differences else :keyword:`False`.
19 """
20 return SchemaDiff(metadata,
21 sqlalchemy.MetaData(engine, reflect=True),
22 labelA='model',
23 labelB='database',
24 excludeTables=excludeTables)
25
26
27 def getDiffOfModelAgainstModel(metadataA, metadataB, excludeTables=None):
28 """
29 Return differences of model against another model.
30
31 :return: object which will evaluate to :keyword:`True` if there \
32 are differences else :keyword:`False`.
33 """
34 return SchemaDiff(metadataA, metadataB, excludeTables)
35
36
37 class ColDiff(object):
38 """
39 Container for differences in one :class:`~sqlalchemy.schema.Column`
40 between two :class:`~sqlalchemy.schema.Table` instances, ``A``
41 and ``B``.
42
43 .. attribute:: col_A
44
45 The :class:`~sqlalchemy.schema.Column` object for A.
46
47 .. attribute:: col_B
48
49 The :class:`~sqlalchemy.schema.Column` object for B.
50
51 .. attribute:: type_A
52
53 The most generic type of the :class:`~sqlalchemy.schema.Column`
54 object in A.
55
56 .. attribute:: type_B
57
58 The most generic type of the :class:`~sqlalchemy.schema.Column`
59 object in A.
60
61 """
62
63 diff = False
64
65 def __init__(self,col_A,col_B):
66 self.col_A = col_A
67 self.col_B = col_B
68
69 self.type_A = col_A.type
70 self.type_B = col_B.type
71
72 self.affinity_A = self.type_A._type_affinity
73 self.affinity_B = self.type_B._type_affinity
74
75 if self.affinity_A is not self.affinity_B:
76 self.diff = True
77 return
78
79 if isinstance(self.type_A,Float) or isinstance(self.type_B,Float):
80 if not (isinstance(self.type_A,Float) and isinstance(self.type_B,Float)):
81 self.diff=True
82 return
83
84 for attr in ('precision','scale','length'):
85 A = getattr(self.type_A,attr,None)
86 B = getattr(self.type_B,attr,None)
87 if not (A is None or B is None) and A!=B:
88 self.diff=True
89 return
90
91 def __nonzero__(self):
92 return self.diff
93
94 class TableDiff(object):
95 """
96 Container for differences in one :class:`~sqlalchemy.schema.Table`
97 between two :class:`~sqlalchemy.schema.MetaData` instances, ``A``
98 and ``B``.
99
100 .. attribute:: columns_missing_from_A
101
102 A sequence of column names that were found in B but weren't in
103 A.
104
105 .. attribute:: columns_missing_from_B
106
107 A sequence of column names that were found in A but weren't in
108 B.
109
110 .. attribute:: columns_different
111
112 A dictionary containing information about columns that were
113 found to be different.
114 It maps column names to a :class:`ColDiff` objects describing the
115 differences found.
116 """
117 __slots__ = (
118 'columns_missing_from_A',
119 'columns_missing_from_B',
120 'columns_different',
121 )
122
123 def __nonzero__(self):
124 return bool(
125 self.columns_missing_from_A or
126 self.columns_missing_from_B or
127 self.columns_different
128 )
129
130 class SchemaDiff(object):
131 """
132 Compute the difference between two :class:`~sqlalchemy.schema.MetaData`
133 objects.
134
135 The string representation of a :class:`SchemaDiff` will summarise
136 the changes found between the two
137 :class:`~sqlalchemy.schema.MetaData` objects.
138
139 The length of a :class:`SchemaDiff` will give the number of
140 changes found, enabling it to be used much like a boolean in
141 expressions.
142
143 :param metadataA:
144 First :class:`~sqlalchemy.schema.MetaData` to compare.
145
146 :param metadataB:
147 Second :class:`~sqlalchemy.schema.MetaData` to compare.
148
149 :param labelA:
150 The label to use in messages about the first
151 :class:`~sqlalchemy.schema.MetaData`.
152
153 :param labelB:
154 The label to use in messages about the second
155 :class:`~sqlalchemy.schema.MetaData`.
156
157 :param excludeTables:
158 A sequence of table names to exclude.
159
160 .. attribute:: tables_missing_from_A
161
162 A sequence of table names that were found in B but weren't in
163 A.
164
165 .. attribute:: tables_missing_from_B
166
167 A sequence of table names that were found in A but weren't in
168 B.
169
170 .. attribute:: tables_different
171
172 A dictionary containing information about tables that were found
173 to be different.
174 It maps table names to a :class:`TableDiff` objects describing the
175 differences found.
176 """
177
178 def __init__(self,
179 metadataA, metadataB,
180 labelA='metadataA',
181 labelB='metadataB',
182 excludeTables=None):
183
184 self.metadataA, self.metadataB = metadataA, metadataB
185 self.labelA, self.labelB = labelA, labelB
186 self.label_width = max(len(labelA),len(labelB))
187 excludeTables = set(excludeTables or [])
188
189 A_table_names = set(metadataA.tables.keys())
190 B_table_names = set(metadataB.tables.keys())
191
192 self.tables_missing_from_A = sorted(
193 B_table_names - A_table_names - excludeTables
194 )
195 self.tables_missing_from_B = sorted(
196 A_table_names - B_table_names - excludeTables
197 )
198
199 self.tables_different = {}
200 for table_name in A_table_names.intersection(B_table_names):
201
202 td = TableDiff()
203
204 A_table = metadataA.tables[table_name]
205 B_table = metadataB.tables[table_name]
206
207 A_column_names = set(A_table.columns.keys())
208 B_column_names = set(B_table.columns.keys())
209
210 td.columns_missing_from_A = sorted(
211 B_column_names - A_column_names
212 )
213
214 td.columns_missing_from_B = sorted(
215 A_column_names - B_column_names
216 )
217
218 td.columns_different = {}
219
220 for col_name in A_column_names.intersection(B_column_names):
221
222 cd = ColDiff(
223 A_table.columns.get(col_name),
224 B_table.columns.get(col_name)
225 )
226
227 if cd:
228 td.columns_different[col_name]=cd
229
230 # XXX - index and constraint differences should
231 # be checked for here
232
233 if td:
234 self.tables_different[table_name]=td
235
236 def __str__(self):
237 ''' Summarize differences. '''
238 out = []
239 column_template =' %%%is: %%r' % self.label_width
240
241 for names,label in (
242 (self.tables_missing_from_A,self.labelA),
243 (self.tables_missing_from_B,self.labelB),
244 ):
245 if names:
246 out.append(
247 ' tables missing from %s: %s' % (
248 label,', '.join(sorted(names))
249 )
250 )
251
252 for name,td in sorted(self.tables_different.items()):
253 out.append(
254 ' table with differences: %s' % name
255 )
256 for names,label in (
257 (td.columns_missing_from_A,self.labelA),
258 (td.columns_missing_from_B,self.labelB),
259 ):
260 if names:
261 out.append(
262 ' %s missing these columns: %s' % (
263 label,', '.join(sorted(names))
264 )
265 )
266 for name,cd in td.columns_different.items():
267 out.append(' column with differences: %s' % name)
268 out.append(column_template % (self.labelA,cd.col_A))
269 out.append(column_template % (self.labelB,cd.col_B))
270
271 if out:
272 out.insert(0, 'Schema diffs:')
273 return '\n'.join(out)
274 else:
275 return 'No schema diffs'
276
277 def __len__(self):
278 """
279 Used in bool evaluation, return of 0 means no diffs.
280 """
281 return (
282 len(self.tables_missing_from_A) +
283 len(self.tables_missing_from_B) +
284 len(self.tables_different)
285 )
@@ -0,0 +1,6
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from rhodecode.lib.dbmigrate.migrate.versioning.script.base import BaseScript
5 from rhodecode.lib.dbmigrate.migrate.versioning.script.py import PythonScript
6 from rhodecode.lib.dbmigrate.migrate.versioning.script.sql import SqlScript
@@ -0,0 +1,57
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 import logging
4
5 from rhodecode.lib.dbmigrate.migrate import exceptions
6 from rhodecode.lib.dbmigrate.migrate.versioning.config import operations
7 from rhodecode.lib.dbmigrate.migrate.versioning import pathed
8
9
10 log = logging.getLogger(__name__)
11
12 class BaseScript(pathed.Pathed):
13 """Base class for other types of scripts.
14 All scripts have the following properties:
15
16 source (script.source())
17 The source code of the script
18 version (script.version())
19 The version number of the script
20 operations (script.operations())
21 The operations defined by the script: upgrade(), downgrade() or both.
22 Returns a tuple of operations.
23 Can also check for an operation with ex. script.operation(Script.ops.up)
24 """ # TODO: sphinxfy this and implement it correctly
25
26 def __init__(self, path):
27 log.debug('Loading script %s...' % path)
28 self.verify(path)
29 super(BaseScript, self).__init__(path)
30 log.debug('Script %s loaded successfully' % path)
31
32 @classmethod
33 def verify(cls, path):
34 """Ensure this is a valid script
35 This version simply ensures the script file's existence
36
37 :raises: :exc:`InvalidScriptError <migrate.exceptions.InvalidScriptError>`
38 """
39 try:
40 cls.require_found(path)
41 except:
42 raise exceptions.InvalidScriptError(path)
43
44 def source(self):
45 """:returns: source code of the script.
46 :rtype: string
47 """
48 fd = open(self.path)
49 ret = fd.read()
50 fd.close()
51 return ret
52
53 def run(self, engine):
54 """Core of each BaseScript subclass.
55 This method executes the script.
56 """
57 raise NotImplementedError()
@@ -0,0 +1,159
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import shutil
5 import warnings
6 import logging
7 from StringIO import StringIO
8
9 from rhodecode.lib.dbmigrate import migrate
10 from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff
11 from rhodecode.lib.dbmigrate.migrate.versioning.config import operations
12 from rhodecode.lib.dbmigrate.migrate.versioning.template import Template
13 from rhodecode.lib.dbmigrate.migrate.versioning.script import base
14 from rhodecode.lib.dbmigrate.migrate.versioning.util import import_path, load_model, with_engine
15 from rhodecode.lib.dbmigrate.migrate.exceptions import MigrateDeprecationWarning, InvalidScriptError, ScriptError
16
17 log = logging.getLogger(__name__)
18 __all__ = ['PythonScript']
19
20
21 class PythonScript(base.BaseScript):
22 """Base for Python scripts"""
23
24 @classmethod
25 def create(cls, path, **opts):
26 """Create an empty migration script at specified path
27
28 :returns: :class:`PythonScript instance <migrate.versioning.script.py.PythonScript>`"""
29 cls.require_notfound(path)
30
31 src = Template(opts.pop('templates_path', None)).get_script(theme=opts.pop('templates_theme', None))
32 shutil.copy(src, path)
33
34 return cls(path)
35
36 @classmethod
37 def make_update_script_for_model(cls, engine, oldmodel,
38 model, repository, **opts):
39 """Create a migration script based on difference between two SA models.
40
41 :param repository: path to migrate repository
42 :param oldmodel: dotted.module.name:SAClass or SAClass object
43 :param model: dotted.module.name:SAClass or SAClass object
44 :param engine: SQLAlchemy engine
45 :type repository: string or :class:`Repository instance <migrate.versioning.repository.Repository>`
46 :type oldmodel: string or Class
47 :type model: string or Class
48 :type engine: Engine instance
49 :returns: Upgrade / Downgrade script
50 :rtype: string
51 """
52
53 if isinstance(repository, basestring):
54 # oh dear, an import cycle!
55 from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository
56 repository = Repository(repository)
57
58 oldmodel = load_model(oldmodel)
59 model = load_model(model)
60
61 # Compute differences.
62 diff = schemadiff.getDiffOfModelAgainstModel(
63 oldmodel,
64 model,
65 excludeTables=[repository.version_table])
66 # TODO: diff can be False (there is no difference?)
67 decls, upgradeCommands, downgradeCommands = \
68 genmodel.ModelGenerator(diff, engine).toUpgradeDowngradePython()
69
70 # Store differences into file.
71 src = Template(opts.pop('templates_path', None)).get_script(opts.pop('templates_theme', None))
72 f = open(src)
73 contents = f.read()
74 f.close()
75
76 # generate source
77 search = 'def upgrade(migrate_engine):'
78 contents = contents.replace(search, '\n\n'.join((decls, search)), 1)
79 if upgradeCommands:
80 contents = contents.replace(' pass', upgradeCommands, 1)
81 if downgradeCommands:
82 contents = contents.replace(' pass', downgradeCommands, 1)
83 return contents
84
85 @classmethod
86 def verify_module(cls, path):
87 """Ensure path is a valid script
88
89 :param path: Script location
90 :type path: string
91 :raises: :exc:`InvalidScriptError <migrate.exceptions.InvalidScriptError>`
92 :returns: Python module
93 """
94 # Try to import and get the upgrade() func
95 module = import_path(path)
96 try:
97 assert callable(module.upgrade)
98 except Exception, e:
99 raise InvalidScriptError(path + ': %s' % str(e))
100 return module
101
102 def preview_sql(self, url, step, **args):
103 """Mocks SQLAlchemy Engine to store all executed calls in a string
104 and runs :meth:`PythonScript.run <migrate.versioning.script.py.PythonScript.run>`
105
106 :returns: SQL file
107 """
108 buf = StringIO()
109 args['engine_arg_strategy'] = 'mock'
110 args['engine_arg_executor'] = lambda s, p = '': buf.write(str(s) + p)
111
112 @with_engine
113 def go(url, step, **kw):
114 engine = kw.pop('engine')
115 self.run(engine, step)
116 return buf.getvalue()
117
118 return go(url, step, **args)
119
120 def run(self, engine, step):
121 """Core method of Script file.
122 Exectues :func:`update` or :func:`downgrade` functions
123
124 :param engine: SQLAlchemy Engine
125 :param step: Operation to run
126 :type engine: string
127 :type step: int
128 """
129 if step > 0:
130 op = 'upgrade'
131 elif step < 0:
132 op = 'downgrade'
133 else:
134 raise ScriptError("%d is not a valid step" % step)
135
136 funcname = base.operations[op]
137 script_func = self._func(funcname)
138
139 try:
140 script_func(engine)
141 except TypeError:
142 warnings.warn("upgrade/downgrade functions must accept engine"
143 " parameter (since version > 0.5.4)", MigrateDeprecationWarning)
144 raise
145
146 @property
147 def module(self):
148 """Calls :meth:`migrate.versioning.script.py.verify_module`
149 and returns it.
150 """
151 if not hasattr(self, '_module'):
152 self._module = self.verify_module(self.path)
153 return self._module
154
155 def _func(self, funcname):
156 if not hasattr(self.module, funcname):
157 msg = "Function '%s' is not defined in this script"
158 raise ScriptError(msg % funcname)
159 return getattr(self.module, funcname)
@@ -0,0 +1,48
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 import logging
4 import shutil
5
6 from rhodecode.lib.dbmigrate.migrate.versioning.script import base
7 from rhodecode.lib.dbmigrate.migrate.versioning.template import Template
8
9
10 log = logging.getLogger(__name__)
11
12 class SqlScript(base.BaseScript):
13 """A file containing plain SQL statements."""
14
15 @classmethod
16 def create(cls, path, **opts):
17 """Create an empty migration script at specified path
18
19 :returns: :class:`SqlScript instance <migrate.versioning.script.sql.SqlScript>`"""
20 cls.require_notfound(path)
21 src = Template(opts.pop('templates_path', None)).get_sql_script(theme=opts.pop('templates_theme', None))
22 shutil.copy(src, path)
23 return cls(path)
24
25 # TODO: why is step parameter even here?
26 def run(self, engine, step=None, executemany=True):
27 """Runs SQL script through raw dbapi execute call"""
28 text = self.source()
29 # Don't rely on SA's autocommit here
30 # (SA uses .startswith to check if a commit is needed. What if script
31 # starts with a comment?)
32 conn = engine.connect()
33 try:
34 trans = conn.begin()
35 try:
36 # HACK: SQLite doesn't allow multiple statements through
37 # its execute() method, but it provides executescript() instead
38 dbapi = conn.engine.raw_connection()
39 if executemany and getattr(dbapi, 'executescript', None):
40 dbapi.executescript(text)
41 else:
42 conn.execute(text)
43 trans.commit()
44 except:
45 trans.rollback()
46 raise
47 finally:
48 conn.close()
@@ -0,0 +1,215
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 """The migrate command-line tool."""
5
6 import sys
7 import inspect
8 import logging
9 from optparse import OptionParser, BadOptionError
10
11 from rhodecode.lib.dbmigrate.migrate import exceptions
12 from rhodecode.lib.dbmigrate.migrate.versioning import api
13 from rhodecode.lib.dbmigrate.migrate.versioning.config import *
14 from rhodecode.lib.dbmigrate.migrate.versioning.util import asbool
15
16
17 alias = dict(
18 s=api.script,
19 vc=api.version_control,
20 dbv=api.db_version,
21 v=api.version,
22 )
23
24 def alias_setup():
25 global alias
26 for key, val in alias.iteritems():
27 setattr(api, key, val)
28 alias_setup()
29
30
31 class PassiveOptionParser(OptionParser):
32
33 def _process_args(self, largs, rargs, values):
34 """little hack to support all --some_option=value parameters"""
35
36 while rargs:
37 arg = rargs[0]
38 if arg == "--":
39 del rargs[0]
40 return
41 elif arg[0:2] == "--":
42 # if parser does not know about the option
43 # pass it along (make it anonymous)
44 try:
45 opt = arg.split('=', 1)[0]
46 self._match_long_opt(opt)
47 except BadOptionError:
48 largs.append(arg)
49 del rargs[0]
50 else:
51 self._process_long_opt(rargs, values)
52 elif arg[:1] == "-" and len(arg) > 1:
53 self._process_short_opts(rargs, values)
54 elif self.allow_interspersed_args:
55 largs.append(arg)
56 del rargs[0]
57
58 def main(argv=None, **kwargs):
59 """Shell interface to :mod:`migrate.versioning.api`.
60
61 kwargs are default options that can be overriden with passing
62 --some_option as command line option
63
64 :param disable_logging: Let migrate configure logging
65 :type disable_logging: bool
66 """
67 if argv is not None:
68 argv = argv
69 else:
70 argv = list(sys.argv[1:])
71 commands = list(api.__all__)
72 commands.sort()
73
74 usage = """%%prog COMMAND ...
75
76 Available commands:
77 %s
78
79 Enter "%%prog help COMMAND" for information on a particular command.
80 """ % '\n\t'.join(["%s - %s" % (command.ljust(28),
81 api.command_desc.get(command)) for command in commands])
82
83 parser = PassiveOptionParser(usage=usage)
84 parser.add_option("-d", "--debug",
85 action="store_true",
86 dest="debug",
87 default=False,
88 help="Shortcut to turn on DEBUG mode for logging")
89 parser.add_option("-q", "--disable_logging",
90 action="store_true",
91 dest="disable_logging",
92 default=False,
93 help="Use this option to disable logging configuration")
94 help_commands = ['help', '-h', '--help']
95 HELP = False
96
97 try:
98 command = argv.pop(0)
99 if command in help_commands:
100 HELP = True
101 command = argv.pop(0)
102 except IndexError:
103 parser.print_help()
104 return
105
106 command_func = getattr(api, command, None)
107 if command_func is None or command.startswith('_'):
108 parser.error("Invalid command %s" % command)
109
110 parser.set_usage(inspect.getdoc(command_func))
111 f_args, f_varargs, f_kwargs, f_defaults = inspect.getargspec(command_func)
112 for arg in f_args:
113 parser.add_option(
114 "--%s" % arg,
115 dest=arg,
116 action='store',
117 type="string")
118
119 # display help of the current command
120 if HELP:
121 parser.print_help()
122 return
123
124 options, args = parser.parse_args(argv)
125
126 # override kwargs with anonymous parameters
127 override_kwargs = dict()
128 for arg in list(args):
129 if arg.startswith('--'):
130 args.remove(arg)
131 if '=' in arg:
132 opt, value = arg[2:].split('=', 1)
133 else:
134 opt = arg[2:]
135 value = True
136 override_kwargs[opt] = value
137
138 # override kwargs with options if user is overwriting
139 for key, value in options.__dict__.iteritems():
140 if value is not None:
141 override_kwargs[key] = value
142
143 # arguments that function accepts without passed kwargs
144 f_required = list(f_args)
145 candidates = dict(kwargs)
146 candidates.update(override_kwargs)
147 for key, value in candidates.iteritems():
148 if key in f_args:
149 f_required.remove(key)
150
151 # map function arguments to parsed arguments
152 for arg in args:
153 try:
154 kw = f_required.pop(0)
155 except IndexError:
156 parser.error("Too many arguments for command %s: %s" % (command,
157 arg))
158 kwargs[kw] = arg
159
160 # apply overrides
161 kwargs.update(override_kwargs)
162
163 # configure options
164 for key, value in options.__dict__.iteritems():
165 kwargs.setdefault(key, value)
166
167 # configure logging
168 if not asbool(kwargs.pop('disable_logging', False)):
169 # filter to log =< INFO into stdout and rest to stderr
170 class SingleLevelFilter(logging.Filter):
171 def __init__(self, min=None, max=None):
172 self.min = min or 0
173 self.max = max or 100
174
175 def filter(self, record):
176 return self.min <= record.levelno <= self.max
177
178 logger = logging.getLogger()
179 h1 = logging.StreamHandler(sys.stdout)
180 f1 = SingleLevelFilter(max=logging.INFO)
181 h1.addFilter(f1)
182 h2 = logging.StreamHandler(sys.stderr)
183 f2 = SingleLevelFilter(min=logging.WARN)
184 h2.addFilter(f2)
185 logger.addHandler(h1)
186 logger.addHandler(h2)
187
188 if options.debug:
189 logger.setLevel(logging.DEBUG)
190 else:
191 logger.setLevel(logging.INFO)
192
193 log = logging.getLogger(__name__)
194
195 # check if all args are given
196 try:
197 num_defaults = len(f_defaults)
198 except TypeError:
199 num_defaults = 0
200 f_args_default = f_args[len(f_args) - num_defaults:]
201 required = list(set(f_required) - set(f_args_default))
202 if required:
203 parser.error("Not enough arguments for command %s: %s not specified" \
204 % (command, ', '.join(required)))
205
206 # handle command
207 try:
208 ret = command_func(**kwargs)
209 if ret is not None:
210 log.info(ret)
211 except (exceptions.UsageError, exceptions.KnownError), e:
212 parser.error(e.args[0])
213
214 if __name__ == "__main__":
215 main()
@@ -0,0 +1,94
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import os
5 import shutil
6 import sys
7
8 from pkg_resources import resource_filename
9
10 from rhodecode.lib.dbmigrate.migrate.versioning.config import *
11 from rhodecode.lib.dbmigrate.migrate.versioning import pathed
12
13
14 class Collection(pathed.Pathed):
15 """A collection of templates of a specific type"""
16 _mask = None
17
18 def get_path(self, file):
19 return os.path.join(self.path, str(file))
20
21
22 class RepositoryCollection(Collection):
23 _mask = '%s'
24
25 class ScriptCollection(Collection):
26 _mask = '%s.py_tmpl'
27
28 class ManageCollection(Collection):
29 _mask = '%s.py_tmpl'
30
31 class SQLScriptCollection(Collection):
32 _mask = '%s.py_tmpl'
33
34 class Template(pathed.Pathed):
35 """Finds the paths/packages of various Migrate templates.
36
37 :param path: Templates are loaded from rhodecode.lib.dbmigrate.migrate package
38 if `path` is not provided.
39 """
40 pkg = 'rhodecode.lib.dbmigrate.migrate.versioning.templates'
41 _manage = 'manage.py_tmpl'
42
43 def __new__(cls, path=None):
44 if path is None:
45 path = cls._find_path(cls.pkg)
46 return super(Template, cls).__new__(cls, path)
47
48 def __init__(self, path=None):
49 if path is None:
50 path = Template._find_path(self.pkg)
51 super(Template, self).__init__(path)
52 self.repository = RepositoryCollection(os.path.join(path, 'repository'))
53 self.script = ScriptCollection(os.path.join(path, 'script'))
54 self.manage = ManageCollection(os.path.join(path, 'manage'))
55 self.sql_script = SQLScriptCollection(os.path.join(path, 'sql_script'))
56
57 @classmethod
58 def _find_path(cls, pkg):
59 """Returns absolute path to dotted python package."""
60 tmp_pkg = pkg.rsplit('.', 1)
61
62 if len(tmp_pkg) != 1:
63 return resource_filename(tmp_pkg[0], tmp_pkg[1])
64 else:
65 return resource_filename(tmp_pkg[0], '')
66
67 def _get_item(self, collection, theme=None):
68 """Locates and returns collection.
69
70 :param collection: name of collection to locate
71 :param type_: type of subfolder in collection (defaults to "_default")
72 :returns: (package, source)
73 :rtype: str, str
74 """
75 item = getattr(self, collection)
76 theme_mask = getattr(item, '_mask')
77 theme = theme_mask % (theme or 'default')
78 return item.get_path(theme)
79
80 def get_repository(self, *a, **kw):
81 """Calls self._get_item('repository', *a, **kw)"""
82 return self._get_item('repository', *a, **kw)
83
84 def get_script(self, *a, **kw):
85 """Calls self._get_item('script', *a, **kw)"""
86 return self._get_item('script', *a, **kw)
87
88 def get_sql_script(self, *a, **kw):
89 """Calls self._get_item('sql_script', *a, **kw)"""
90 return self._get_item('sql_script', *a, **kw)
91
92 def get_manage(self, *a, **kw):
93 """Calls self._get_item('manage', *a, **kw)"""
94 return self._get_item('manage', *a, **kw)
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,5
1 #!/usr/bin/env python
2 from migrate.versioning.shell import main
3
4 if __name__ == '__main__':
5 main(%(defaults)s)
@@ -0,0 +1,10
1 #!/usr/bin/env python
2 from migrate.versioning.shell import main
3
4 {{py:
5 _vars = locals().copy()
6 del _vars['__template_name__']
7 _vars.pop('repository_name', None)
8 defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()])
9 }}
10 main({{ defaults }})
@@ -0,0 +1,29
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 import sys
4
5 from sqlalchemy import engine_from_config
6 from paste.deploy.loadwsgi import ConfigLoader
7
8 from migrate.versioning.shell import main
9 from {{ locals().pop('repository_name') }}.model import migrations
10
11
12 if '-c' in sys.argv:
13 pos = sys.argv.index('-c')
14 conf_path = sys.argv[pos + 1]
15 del sys.argv[pos:pos + 2]
16 else:
17 conf_path = 'development.ini'
18
19 {{py:
20 _vars = locals().copy()
21 del _vars['__template_name__']
22 defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()])
23 }}
24
25 conf_dict = ConfigLoader(conf_path).parser._sections['app:main']
26
27 # migrate supports passing url as an existing Engine instance (since 0.6.0)
28 # usage: migrate -c path/to/config.ini COMMANDS
29 main(url=engine_from_config(conf_dict), repository=migrations.__path__[0],{{ defaults }})
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,4
1 This is a database migration repository.
2
3 More information at
4 http://code.google.com/p/sqlalchemy-migrate/
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,20
1 [db_settings]
2 # Used to identify which repository this database is versioned under.
3 # You can use the name of your project.
4 repository_id={{ locals().pop('repository_id') }}
5
6 # The name of the database table used to track the schema version.
7 # This name shouldn't already be used by your project.
8 # If this is changed once a database is under version control, you'll need to
9 # change the table name in each database too.
10 version_table={{ locals().pop('version_table') }}
11
12 # When committing a change script, Migrate will attempt to generate the
13 # sql for all supported databases; normally, if one of them fails - probably
14 # because you don't have that database installed - it is ignored and the
15 # commit continues, perhaps ending successfully.
16 # Databases in this list MUST compile successfully during a commit, or the
17 # entire commit will fail. List the databases your application will actually
18 # be using to ensure your updates to that database work properly.
19 # This must be a list; example: ['postgres','sqlite']
20 required_dbs={{ locals().pop('required_dbs') }}
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,4
1 This is a database migration repository.
2
3 More information at
4 http://code.google.com/p/sqlalchemy-migrate/
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,20
1 [db_settings]
2 # Used to identify which repository this database is versioned under.
3 # You can use the name of your project.
4 repository_id={{ locals().pop('repository_id') }}
5
6 # The name of the database table used to track the schema version.
7 # This name shouldn't already be used by your project.
8 # If this is changed once a database is under version control, you'll need to
9 # change the table name in each database too.
10 version_table={{ locals().pop('version_table') }}
11
12 # When committing a change script, Migrate will attempt to generate the
13 # sql for all supported databases; normally, if one of them fails - probably
14 # because you don't have that database installed - it is ignored and the
15 # commit continues, perhaps ending successfully.
16 # Databases in this list MUST compile successfully during a commit, or the
17 # entire commit will fail. List the databases your application will actually
18 # be using to ensure your updates to that database work properly.
19 # This must be a list; example: ['postgres','sqlite']
20 required_dbs={{ locals().pop('required_dbs') }}
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,11
1 from sqlalchemy import *
2 from migrate import *
3
4 def upgrade(migrate_engine):
5 # Upgrade operations go here. Don't create your own engine; bind migrate_engine
6 # to your metadata
7 pass
8
9 def downgrade(migrate_engine):
10 # Operations to reverse the above upgrade go here.
11 pass
@@ -0,0 +1,11
1 from sqlalchemy import *
2 from migrate import *
3
4 def upgrade(migrate_engine):
5 # Upgrade operations go here. Don't create your own engine; bind migrate_engine
6 # to your metadata
7 pass
8
9 def downgrade(migrate_engine):
10 # Operations to reverse the above upgrade go here.
11 pass
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,179
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """.. currentmodule:: migrate.versioning.util"""
4
5 import warnings
6 import logging
7 from decorator import decorator
8 from pkg_resources import EntryPoint
9
10 from sqlalchemy import create_engine
11 from sqlalchemy.engine import Engine
12 from sqlalchemy.pool import StaticPool
13
14 from rhodecode.lib.dbmigrate.migrate import exceptions
15 from rhodecode.lib.dbmigrate.migrate.versioning.util.keyedinstance import KeyedInstance
16 from rhodecode.lib.dbmigrate.migrate.versioning.util.importpath import import_path
17
18
19 log = logging.getLogger(__name__)
20
21 def load_model(dotted_name):
22 """Import module and use module-level variable".
23
24 :param dotted_name: path to model in form of string: ``some.python.module:Class``
25
26 .. versionchanged:: 0.5.4
27
28 """
29 if isinstance(dotted_name, basestring):
30 if ':' not in dotted_name:
31 # backwards compatibility
32 warnings.warn('model should be in form of module.model:User '
33 'and not module.model.User', exceptions.MigrateDeprecationWarning)
34 dotted_name = ':'.join(dotted_name.rsplit('.', 1))
35 return EntryPoint.parse('x=%s' % dotted_name).load(False)
36 else:
37 # Assume it's already loaded.
38 return dotted_name
39
40 def asbool(obj):
41 """Do everything to use object as bool"""
42 if isinstance(obj, basestring):
43 obj = obj.strip().lower()
44 if obj in ['true', 'yes', 'on', 'y', 't', '1']:
45 return True
46 elif obj in ['false', 'no', 'off', 'n', 'f', '0']:
47 return False
48 else:
49 raise ValueError("String is not true/false: %r" % obj)
50 if obj in (True, False):
51 return bool(obj)
52 else:
53 raise ValueError("String is not true/false: %r" % obj)
54
55 def guess_obj_type(obj):
56 """Do everything to guess object type from string
57
58 Tries to convert to `int`, `bool` and finally returns if not succeded.
59
60 .. versionadded: 0.5.4
61 """
62
63 result = None
64
65 try:
66 result = int(obj)
67 except:
68 pass
69
70 if result is None:
71 try:
72 result = asbool(obj)
73 except:
74 pass
75
76 if result is not None:
77 return result
78 else:
79 return obj
80
81 @decorator
82 def catch_known_errors(f, *a, **kw):
83 """Decorator that catches known api errors
84
85 .. versionadded: 0.5.4
86 """
87
88 try:
89 return f(*a, **kw)
90 except exceptions.PathFoundError, e:
91 raise exceptions.KnownError("The path %s already exists" % e.args[0])
92
93 def construct_engine(engine, **opts):
94 """.. versionadded:: 0.5.4
95
96 Constructs and returns SQLAlchemy engine.
97
98 Currently, there are 2 ways to pass create_engine options to :mod:`migrate.versioning.api` functions:
99
100 :param engine: connection string or a existing engine
101 :param engine_dict: python dictionary of options to pass to `create_engine`
102 :param engine_arg_*: keyword parameters to pass to `create_engine` (evaluated with :func:`migrate.versioning.util.guess_obj_type`)
103 :type engine_dict: dict
104 :type engine: string or Engine instance
105 :type engine_arg_*: string
106 :returns: SQLAlchemy Engine
107
108 .. note::
109
110 keyword parameters override ``engine_dict`` values.
111
112 """
113 if isinstance(engine, Engine):
114 return engine
115 elif not isinstance(engine, basestring):
116 raise ValueError("you need to pass either an existing engine or a database uri")
117
118 # get options for create_engine
119 if opts.get('engine_dict') and isinstance(opts['engine_dict'], dict):
120 kwargs = opts['engine_dict']
121 else:
122 kwargs = dict()
123
124 # DEPRECATED: handle echo the old way
125 echo = asbool(opts.get('echo', False))
126 if echo:
127 warnings.warn('echo=True parameter is deprecated, pass '
128 'engine_arg_echo=True or engine_dict={"echo": True}',
129 exceptions.MigrateDeprecationWarning)
130 kwargs['echo'] = echo
131
132 # parse keyword arguments
133 for key, value in opts.iteritems():
134 if key.startswith('engine_arg_'):
135 kwargs[key[11:]] = guess_obj_type(value)
136
137 log.debug('Constructing engine')
138 # TODO: return create_engine(engine, poolclass=StaticPool, **kwargs)
139 # seems like 0.5.x branch does not work with engine.dispose and staticpool
140 return create_engine(engine, **kwargs)
141
142 @decorator
143 def with_engine(f, *a, **kw):
144 """Decorator for :mod:`migrate.versioning.api` functions
145 to safely close resources after function usage.
146
147 Passes engine parameters to :func:`construct_engine` and
148 resulting parameter is available as kw['engine'].
149
150 Engine is disposed after wrapped function is executed.
151
152 .. versionadded: 0.6.0
153 """
154 url = a[0]
155 engine = construct_engine(url, **kw)
156
157 try:
158 kw['engine'] = engine
159 return f(*a, **kw)
160 finally:
161 if isinstance(engine, Engine):
162 log.debug('Disposing SQLAlchemy engine %s', engine)
163 engine.dispose()
164
165
166 class Memoize:
167 """Memoize(fn) - an instance which acts like fn but memoizes its arguments
168 Will only work on functions with non-mutable arguments
169
170 ActiveState Code 52201
171 """
172 def __init__(self, fn):
173 self.fn = fn
174 self.memo = {}
175
176 def __call__(self, *args):
177 if not self.memo.has_key(args):
178 self.memo[args] = self.fn(*args)
179 return self.memo[args]
@@ -0,0 +1,16
1 import os
2 import sys
3
4 def import_path(fullpath):
5 """ Import a file with full path specification. Allows one to
6 import from anywhere, something __import__ does not do.
7 """
8 # http://zephyrfalcon.org/weblog/arch_d7_2002_08_31.html
9 path, filename = os.path.split(fullpath)
10 filename, ext = os.path.splitext(filename)
11 sys.path.append(path)
12 module = __import__(filename)
13 reload(module) # Might be out of date during tests
14 del sys.path[-1]
15 return module
16
@@ -0,0 +1,36
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 class KeyedInstance(object):
5 """A class whose instances have a unique identifier of some sort
6 No two instances with the same unique ID should exist - if we try to create
7 a second instance, the first should be returned.
8 """
9
10 _instances = dict()
11
12 def __new__(cls, *p, **k):
13 instances = cls._instances
14 clskey = str(cls)
15 if clskey not in instances:
16 instances[clskey] = dict()
17 instances = instances[clskey]
18
19 key = cls._key(*p, **k)
20 if key not in instances:
21 instances[key] = super(KeyedInstance, cls).__new__(cls)
22 return instances[key]
23
24 @classmethod
25 def _key(cls, *p, **k):
26 """Given a unique identifier, return a dictionary key
27 This should be overridden by child classes, to specify which parameters
28 should determine an object's uniqueness
29 """
30 raise NotImplementedError()
31
32 @classmethod
33 def clear(cls):
34 # Allow cls.clear() as well as uniqueInstance.clear(cls)
35 if str(cls) in cls._instances:
36 del cls._instances[str(cls)]
@@ -0,0 +1,215
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import os
5 import re
6 import shutil
7 import logging
8
9 from rhodecode.lib.dbmigrate.migrate import exceptions
10 from rhodecode.lib.dbmigrate.migrate.versioning import pathed, script
11
12
13 log = logging.getLogger(__name__)
14
15 class VerNum(object):
16 """A version number that behaves like a string and int at the same time"""
17
18 _instances = dict()
19
20 def __new__(cls, value):
21 val = str(value)
22 if val not in cls._instances:
23 cls._instances[val] = super(VerNum, cls).__new__(cls)
24 ret = cls._instances[val]
25 return ret
26
27 def __init__(self,value):
28 self.value = str(int(value))
29 if self < 0:
30 raise ValueError("Version number cannot be negative")
31
32 def __add__(self, value):
33 ret = int(self) + int(value)
34 return VerNum(ret)
35
36 def __sub__(self, value):
37 return self + (int(value) * -1)
38
39 def __cmp__(self, value):
40 return int(self) - int(value)
41
42 def __repr__(self):
43 return "<VerNum(%s)>" % self.value
44
45 def __str__(self):
46 return str(self.value)
47
48 def __int__(self):
49 return int(self.value)
50
51
52 class Collection(pathed.Pathed):
53 """A collection of versioning scripts in a repository"""
54
55 FILENAME_WITH_VERSION = re.compile(r'^(\d{3,}).*')
56
57 def __init__(self, path):
58 """Collect current version scripts in repository
59 and store them in self.versions
60 """
61 super(Collection, self).__init__(path)
62
63 # Create temporary list of files, allowing skipped version numbers.
64 files = os.listdir(path)
65 if '1' in files:
66 # deprecation
67 raise Exception('It looks like you have a repository in the old '
68 'format (with directories for each version). '
69 'Please convert repository before proceeding.')
70
71 tempVersions = dict()
72 for filename in files:
73 match = self.FILENAME_WITH_VERSION.match(filename)
74 if match:
75 num = int(match.group(1))
76 tempVersions.setdefault(num, []).append(filename)
77 else:
78 pass # Must be a helper file or something, let's ignore it.
79
80 # Create the versions member where the keys
81 # are VerNum's and the values are Version's.
82 self.versions = dict()
83 for num, files in tempVersions.items():
84 self.versions[VerNum(num)] = Version(num, path, files)
85
86 @property
87 def latest(self):
88 """:returns: Latest version in Collection"""
89 return max([VerNum(0)] + self.versions.keys())
90
91 def create_new_python_version(self, description, **k):
92 """Create Python files for new version"""
93 ver = self.latest + 1
94 extra = str_to_filename(description)
95
96 if extra:
97 if extra == '_':
98 extra = ''
99 elif not extra.startswith('_'):
100 extra = '_%s' % extra
101
102 filename = '%03d%s.py' % (ver, extra)
103 filepath = self._version_path(filename)
104
105 script.PythonScript.create(filepath, **k)
106 self.versions[ver] = Version(ver, self.path, [filename])
107
108 def create_new_sql_version(self, database, **k):
109 """Create SQL files for new version"""
110 ver = self.latest + 1
111 self.versions[ver] = Version(ver, self.path, [])
112
113 # Create new files.
114 for op in ('upgrade', 'downgrade'):
115 filename = '%03d_%s_%s.sql' % (ver, database, op)
116 filepath = self._version_path(filename)
117 script.SqlScript.create(filepath, **k)
118 self.versions[ver].add_script(filepath)
119
120 def version(self, vernum=None):
121 """Returns latest Version if vernum is not given.
122 Otherwise, returns wanted version"""
123 if vernum is None:
124 vernum = self.latest
125 return self.versions[VerNum(vernum)]
126
127 @classmethod
128 def clear(cls):
129 super(Collection, cls).clear()
130
131 def _version_path(self, ver):
132 """Returns path of file in versions repository"""
133 return os.path.join(self.path, str(ver))
134
135
136 class Version(object):
137 """A single version in a collection
138 :param vernum: Version Number
139 :param path: Path to script files
140 :param filelist: List of scripts
141 :type vernum: int, VerNum
142 :type path: string
143 :type filelist: list
144 """
145
146 def __init__(self, vernum, path, filelist):
147 self.version = VerNum(vernum)
148
149 # Collect scripts in this folder
150 self.sql = dict()
151 self.python = None
152
153 for script in filelist:
154 self.add_script(os.path.join(path, script))
155
156 def script(self, database=None, operation=None):
157 """Returns SQL or Python Script"""
158 for db in (database, 'default'):
159 # Try to return a .sql script first
160 try:
161 return self.sql[db][operation]
162 except KeyError:
163 continue # No .sql script exists
164
165 # TODO: maybe add force Python parameter?
166 ret = self.python
167
168 assert ret is not None, \
169 "There is no script for %d version" % self.version
170 return ret
171
172 def add_script(self, path):
173 """Add script to Collection/Version"""
174 if path.endswith(Extensions.py):
175 self._add_script_py(path)
176 elif path.endswith(Extensions.sql):
177 self._add_script_sql(path)
178
179 SQL_FILENAME = re.compile(r'^(\d+)_([^_]+)_([^_]+).sql')
180
181 def _add_script_sql(self, path):
182 basename = os.path.basename(path)
183 match = self.SQL_FILENAME.match(basename)
184
185 if match:
186 version, dbms, op = match.group(1), match.group(2), match.group(3)
187 else:
188 raise exceptions.ScriptError(
189 "Invalid SQL script name %s " % basename + \
190 "(needs to be ###_database_operation.sql)")
191
192 # File the script into a dictionary
193 self.sql.setdefault(dbms, {})[op] = script.SqlScript(path)
194
195 def _add_script_py(self, path):
196 if self.python is not None:
197 raise exceptions.ScriptError('You can only have one Python script '
198 'per version, but you have: %s and %s' % (self.python, path))
199 self.python = script.PythonScript(path)
200
201
202 class Extensions:
203 """A namespace for file extensions"""
204 py = 'py'
205 sql = 'sql'
206
207 def str_to_filename(s):
208 """Replaces spaces, (double and single) quotes
209 and double underscores to underscores
210 """
211
212 s = s.replace(' ', '_').replace('"', '_').replace("'", '_').replace(".", "_")
213 while '__' in s:
214 s = s.replace('__', '_')
215 return s
@@ -0,0 +1,237
1 #==============================================================================
2 # DB INITIAL MODEL
3 #==============================================================================
4 import logging
5 import datetime
6
7 from sqlalchemy import *
8 from sqlalchemy.exc import DatabaseError
9 from sqlalchemy.orm import relation, backref, class_mapper
10 from sqlalchemy.orm.session import Session
11 from rhodecode.model.meta import Base
12
13 from rhodecode.lib.dbmigrate.migrate import *
14
15 log = logging.getLogger(__name__)
16
17 class BaseModel(object):
18
19 @classmethod
20 def _get_keys(cls):
21 """return column names for this model """
22 return class_mapper(cls).c.keys()
23
24 def get_dict(self):
25 """return dict with keys and values corresponding
26 to this model data """
27
28 d = {}
29 for k in self._get_keys():
30 d[k] = getattr(self, k)
31 return d
32
33 def get_appstruct(self):
34 """return list with keys and values tupples corresponding
35 to this model data """
36
37 l = []
38 for k in self._get_keys():
39 l.append((k, getattr(self, k),))
40 return l
41
42 def populate_obj(self, populate_dict):
43 """populate model with data from given populate_dict"""
44
45 for k in self._get_keys():
46 if k in populate_dict:
47 setattr(self, k, populate_dict[k])
48
49 class RhodeCodeSettings(Base, BaseModel):
50 __tablename__ = 'rhodecode_settings'
51 __table_args__ = (UniqueConstraint('app_settings_name'), {'useexisting':True})
52 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
53 app_settings_name = Column("app_settings_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
54 app_settings_value = Column("app_settings_value", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
55
56 def __init__(self, k, v):
57 self.app_settings_name = k
58 self.app_settings_value = v
59
60 def __repr__(self):
61 return "<RhodeCodeSetting('%s:%s')>" % (self.app_settings_name,
62 self.app_settings_value)
63
64 class RhodeCodeUi(Base, BaseModel):
65 __tablename__ = 'rhodecode_ui'
66 __table_args__ = {'useexisting':True}
67 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
68 ui_section = Column("ui_section", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
69 ui_key = Column("ui_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
70 ui_value = Column("ui_value", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
71 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
72
73
74 class User(Base, BaseModel):
75 __tablename__ = 'users'
76 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'useexisting':True})
77 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
78 username = Column("username", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
79 password = Column("password", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
80 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
81 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
82 name = Column("name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
83 lastname = Column("lastname", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
84 email = Column("email", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
85 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
86 is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False)
87
88 user_log = relation('UserLog', cascade='all')
89 user_perms = relation('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
90
91 repositories = relation('Repository')
92 user_followers = relation('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
93
94 @property
95 def full_contact(self):
96 return '%s %s <%s>' % (self.name, self.lastname, self.email)
97
98 def __repr__(self):
99 return "<User('id:%s:%s')>" % (self.user_id, self.username)
100
101 def update_lastlogin(self):
102 """Update user lastlogin"""
103
104 try:
105 session = Session.object_session(self)
106 self.last_login = datetime.datetime.now()
107 session.add(self)
108 session.commit()
109 log.debug('updated user %s lastlogin', self.username)
110 except (DatabaseError,):
111 session.rollback()
112
113
114 class UserLog(Base, BaseModel):
115 __tablename__ = 'user_logs'
116 __table_args__ = {'useexisting':True}
117 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
118 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
119 repository_id = Column("repository_id", Integer(length=None, convert_unicode=False, assert_unicode=None), ForeignKey(u'repositories.repo_id'), nullable=False, unique=None, default=None)
120 repository_name = Column("repository_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
121 user_ip = Column("user_ip", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
122 action = Column("action", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
123 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
124
125 user = relation('User')
126 repository = relation('Repository')
127
128 class Repository(Base, BaseModel):
129 __tablename__ = 'repositories'
130 __table_args__ = (UniqueConstraint('repo_name'), {'useexisting':True},)
131 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
132 repo_name = Column("repo_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
133 repo_type = Column("repo_type", String(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
134 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=False, default=None)
135 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
136 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
137 description = Column("description", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
138 fork_id = Column("fork_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=False, default=None)
139
140 user = relation('User')
141 fork = relation('Repository', remote_side=repo_id)
142 repo_to_perm = relation('RepoToPerm', cascade='all')
143 stats = relation('Statistics', cascade='all', uselist=False)
144
145 repo_followers = relation('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
146
147
148 def __repr__(self):
149 return "<Repository('%s:%s')>" % (self.repo_id, self.repo_name)
150
151 class Permission(Base, BaseModel):
152 __tablename__ = 'permissions'
153 __table_args__ = {'useexisting':True}
154 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
155 permission_name = Column("permission_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
156 permission_longname = Column("permission_longname", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
157
158 def __repr__(self):
159 return "<Permission('%s:%s')>" % (self.permission_id, self.permission_name)
160
161 class RepoToPerm(Base, BaseModel):
162 __tablename__ = 'repo_to_perm'
163 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'useexisting':True})
164 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
165 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
166 permission_id = Column("permission_id", Integer(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None)
167 repository_id = Column("repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=None, default=None)
168
169 user = relation('User')
170 permission = relation('Permission')
171 repository = relation('Repository')
172
173 class UserToPerm(Base, BaseModel):
174 __tablename__ = 'user_to_perm'
175 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'useexisting':True})
176 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
177 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
178 permission_id = Column("permission_id", Integer(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None)
179
180 user = relation('User')
181 permission = relation('Permission')
182
183 class Statistics(Base, BaseModel):
184 __tablename__ = 'statistics'
185 __table_args__ = (UniqueConstraint('repository_id'), {'useexisting':True})
186 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
187 repository_id = Column("repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=True, default=None)
188 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
189 commit_activity = Column("commit_activity", LargeBinary(), nullable=False)#JSON data
190 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
191 languages = Column("languages", LargeBinary(), nullable=False)#JSON data
192
193 repository = relation('Repository', single_parent=True)
194
195 class UserFollowing(Base, BaseModel):
196 __tablename__ = 'user_followings'
197 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
198 UniqueConstraint('user_id', 'follows_user_id')
199 , {'useexisting':True})
200
201 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
202 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
203 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=None, default=None)
204 follows_user_id = Column("follows_user_id", Integer(), ForeignKey(u'users.user_id'), nullable=True, unique=None, default=None)
205
206 user = relation('User', primaryjoin='User.user_id==UserFollowing.user_id')
207
208 follows_user = relation('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
209 follows_repository = relation('Repository')
210
211
212 class CacheInvalidation(Base, BaseModel):
213 __tablename__ = 'cache_invalidation'
214 __table_args__ = (UniqueConstraint('cache_key'), {'useexisting':True})
215 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
216 cache_key = Column("cache_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
217 cache_args = Column("cache_args", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
218 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
219
220
221 def __init__(self, cache_key, cache_args=''):
222 self.cache_key = cache_key
223 self.cache_args = cache_args
224 self.cache_active = False
225
226 def __repr__(self):
227 return "<CacheInvalidation('%s:%s')>" % (self.cache_id, self.cache_key)
228
229
230 def upgrade(migrate_engine):
231 # Upgrade operations go here. Don't create your own engine; bind migrate_engine
232 # to your metadata
233 Base.metadata.create_all(bind=migrate_engine, checkfirst=False)
234
235 def downgrade(migrate_engine):
236 # Operations to reverse the above upgrade go here.
237 Base.metadata.drop_all(bind=migrate_engine, checkfirst=False)
@@ -0,0 +1,127
1 import logging
2 import datetime
3
4 from sqlalchemy import *
5 from sqlalchemy.exc import DatabaseError
6 from sqlalchemy.orm import relation, backref, class_mapper
7 from sqlalchemy.orm.session import Session
8 from rhodecode.model.meta import Base
9 from rhodecode.model.db import BaseModel
10
11 from rhodecode.lib.dbmigrate.migrate import *
12
13 log = logging.getLogger(__name__)
14
15 def upgrade(migrate_engine):
16 """ Upgrade operations go here.
17 Don't create your own engine; bind migrate_engine to your metadata
18 """
19
20 #==========================================================================
21 # Upgrade of `users` table
22 #==========================================================================
23 tblname = 'users'
24 tbl = Table(tblname, MetaData(bind=migrate_engine), autoload=True,
25 autoload_with=migrate_engine)
26
27 #ADD is_ldap column
28 is_ldap = Column("is_ldap", Boolean(), nullable=True,
29 unique=None, default=False)
30 is_ldap.create(tbl, populate_default=True)
31 is_ldap.alter(nullable=False)
32
33 #==========================================================================
34 # Upgrade of `user_logs` table
35 #==========================================================================
36
37 tblname = 'users'
38 tbl = Table(tblname, MetaData(bind=migrate_engine), autoload=True,
39 autoload_with=migrate_engine)
40
41 #ADD revision column
42 revision = Column('revision', TEXT(length=None, convert_unicode=False,
43 assert_unicode=None),
44 nullable=True, unique=None, default=None)
45 revision.create(tbl)
46
47
48
49 #==========================================================================
50 # Upgrade of `repositories` table
51 #==========================================================================
52 tblname = 'repositories'
53 tbl = Table(tblname, MetaData(bind=migrate_engine), autoload=True,
54 autoload_with=migrate_engine)
55
56 #ADD repo_type column#
57 repo_type = Column("repo_type", String(length=None, convert_unicode=False,
58 assert_unicode=None),
59 nullable=True, unique=False, default='hg')
60
61 repo_type.create(tbl, populate_default=True)
62 #repo_type.alter(nullable=False)
63
64 #ADD statistics column#
65 enable_statistics = Column("statistics", Boolean(), nullable=True,
66 unique=None, default=True)
67 enable_statistics.create(tbl)
68
69 #==========================================================================
70 # Add table `user_followings`
71 #==========================================================================
72 tblname = 'user_followings'
73
74 class UserFollowing(Base, BaseModel):
75 __tablename__ = 'user_followings'
76 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
77 UniqueConstraint('user_id', 'follows_user_id')
78 , {'useexisting':True})
79
80 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
81 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
82 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=None, default=None)
83 follows_user_id = Column("follows_user_id", Integer(), ForeignKey(u'users.user_id'), nullable=True, unique=None, default=None)
84
85 user = relation('User', primaryjoin='User.user_id==UserFollowing.user_id')
86
87 follows_user = relation('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
88 follows_repository = relation('Repository')
89
90 Base.metadata.tables[tblname].create(migrate_engine)
91
92 #==========================================================================
93 # Add table `cache_invalidation`
94 #==========================================================================
95 tblname = 'cache_invalidation'
96
97 class CacheInvalidation(Base, BaseModel):
98 __tablename__ = 'cache_invalidation'
99 __table_args__ = (UniqueConstraint('cache_key'), {'useexisting':True})
100 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
101 cache_key = Column("cache_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
102 cache_args = Column("cache_args", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
103 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
104
105
106 def __init__(self, cache_key, cache_args=''):
107 self.cache_key = cache_key
108 self.cache_args = cache_args
109 self.cache_active = False
110
111 def __repr__(self):
112 return "<CacheInvalidation('%s:%s')>" % (self.cache_id, self.cache_key)
113
114 Base.metadata.tables[tblname].create(migrate_engine)
115
116 return
117
118
119
120
121
122
123 def downgrade(migrate_engine):
124 meta = MetaData()
125 meta.bind = migrate_engine
126
127
@@ -0,0 +1,26
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.lib.dbmigrate.versions.__init__
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 Package containing new versions of database models
7
8 :created_on: Dec 11, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 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
14 # modify it under the terms of the GNU General Public License
15 # as published by the Free Software Foundation; version 2
16 # of the License or (at your opinion) any later version of the license.
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, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 # MA 02110-1301, USA.
@@ -0,0 +1,32
1 #!/usr/bin/env python
2 # encoding: utf-8
3 # Custom Exceptions modules
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
9 # of the License or (at your opinion) any later version of the license.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
20 """
21 Created on Nov 17, 2010
22 Custom Exceptions modules
23 @author: marcink
24 """
25
26 class LdapUsernameError(Exception):pass
27 class LdapPasswordError(Exception):pass
28 class LdapConnectionError(Exception):pass
29 class LdapImportError(Exception):pass
30
31 class DefaultUserException(Exception):pass
32 class UserOwnsReposException(Exception):pass
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
@@ -1,5 +1,11
1 syntax: glob
2 *.pyc
3 *.swp
1
4
2 syntax: regexp
5 syntax: regexp
6 ^build
7 ^docs/build/
8 ^docs/_build/
3 ^data$
9 ^data$
4 ^\.settings$
10 ^\.settings$
5 ^\.project$
11 ^\.project$
@@ -7,4 +13,4 syntax: regexp
7 ^rhodecode\.db$
13 ^rhodecode\.db$
8 ^test\.db$
14 ^test\.db$
9 ^repositories\.config$
15 ^repositories\.config$
10 ^RhodeCode\.egg-info$ No newline at end of file
16 ^RhodeCode\.egg-info$
@@ -1,4 +1,5
1 include rhodecode/config/deployment.ini_tmpl
1 include rhodecode/config/deployment.ini_tmpl
2 include rhodecode/lib/dbmigrate/migrate.cfg
2
3
3 include README.rst
4 include README.rst
4 recursive-include rhodecode/i18n/ *
5 recursive-include rhodecode/i18n/ *
@@ -7,7 +8,7 recursive-include rhodecode/i18n/ *
7 recursive-include rhodecode/public/css *
8 recursive-include rhodecode/public/css *
8 recursive-include rhodecode/public/images *
9 recursive-include rhodecode/public/images *
9 #js
10 #js
10 include rhodecode/public/js/yui2.js
11 include rhodecode/public/js/yui2a.js
11 include rhodecode/public/js/excanvas.min.js
12 include rhodecode/public/js/excanvas.min.js
12 include rhodecode/public/js/yui.flot.js
13 include rhodecode/public/js/yui.flot.js
13 include rhodecode/public/js/graph.js
14 include rhodecode/public/js/graph.js
@@ -1,23 +1,25
1
1
2 RhodeCode (RhodiumCode)
2 =================================================
3 =======================
3 Welcome to RhodeCode (RhodiumCode) documentation!
4 =================================================
4
5
5 ``RhodeCode`` (formerly hg-app) is Pylons based repository management and
6 ``RhodeCode`` (formerly hg-app) is Pylons framework based Mercurial repository
6 serving for mercurial_. It's similar to github or bitbucket, but it's suppose to run
7 browser/management with build in push/pull server and full text search.
7 as standalone app, it's open source and focuses more on restricted access to repositories
8 It works on http/https, has build in permission/authentication(+ldap) features
8 There's no default free access to RhodeCode You have to create an account in order
9 It's similar to github or bitbucket, but it's suppose to run as standalone
9 to use the application. It's powered by vcs_ library that we created to handle
10 hosted application, it's open source and focuses more on restricted access to
10 many various version control systems.
11 repositories. It's powered by vcs_ library that me and Lukasz Balcerzak created
12 to handle many various version control systems.
11
13
12 RhodeCode uses `Semantic Versioning <http://semver.org/>`_
14 RhodeCode uses `Semantic Versioning <http://semver.org/>`_
13
15
14
15 RhodeCode demo
16 RhodeCode demo
16 --------------
17 --------------
17
18
18 http://hg.python-works.com
19 http://hg.python-works.com
19
20
20 The default access is
21 The default access is anonymous but You can login to administrative account
22 using those credentials
21
23
22 - username: demo
24 - username: demo
23 - password: demo
25 - password: demo
@@ -25,14 +27,14 The default access is
25 Source code
27 Source code
26 -----------
28 -----------
27
29
28 Source code is along with issue tracker is available at
30 The most up to date sources can be obtained from my own RhodeCode instance
31 https://rhodecode.org
32
33 Rarely updated source code and issue tracker is available at bitbcuket
29 http://bitbucket.org/marcinkuzminski/rhodecode
34 http://bitbucket.org/marcinkuzminski/rhodecode
30
35
31 Also a source codes can be obtained from demo rhodecode instance
36 Installation
32 http://hg.python-works.com/rhodecode/summary
37 ------------
33
34 Instalation
35 -----------
36
38
37 Please visit http://packages.python.org/RhodeCode/installation.html
39 Please visit http://packages.python.org/RhodeCode/installation.html
38
40
@@ -40,41 +42,49 Instalation
40 Features
42 Features
41 --------
43 --------
42
44
43 - Has it's own middleware to handle mercurial_ protocol request. Each request
45 - Has it's own middleware to handle mercurial_ protocol request.
44 can be logged and authenticated. Runs on threads unlikely to hgweb You can
46 Each request can be logged and authenticated. Runs on threads unlikely to
45 make multiple pulls/pushes simultaneous. Supports http/https
47 hgweb. You can make multiple pulls/pushes simultaneous. Supports http/https
46 - Full permissions and authentication per project private/read/write/admin.
48 and ldap
47 One account for web interface and mercurial_ push/pull/clone.
49 - Full permissions (private/read/write/admin) and authentication per project.
50 One account for web interface and mercurial_ push/pull/clone operations.
48 - Mako templates let's you customize look and feel of application.
51 - Mako templates let's you customize look and feel of application.
49 - Beautiful diffs, annotations and source codes all colored by pygments.
52 - Beautiful diffs, annotations and source codes all colored by pygments.
50 - Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics
53 - Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics
51 - Admin interface with user/permission management. User activity journal logs
54 - Admin interface with user/permission management. Admin activity journal, logs
52 pulls, pushes, forks,registrations. Possible to disable built in hooks
55 pulls, pushes, forks, registrations and other actions made by all users.
53 - Server side forks, it's possible to fork a project and hack it free without
56 - Server side forks, it's possible to fork a project and hack it free without
54 breaking the main.
57 breaking the main repository.
55 - Full text search on source codes, search on file names. All powered by whoosh
58 - Full text search powered by Whoosh on source codes, and file names.
56 and build in indexing daemons
59 Build in indexing daemons, with optional incremental index build
57 (no external search servers required all in one application)
60 (no external search servers required all in one application)
58 - Rss / atom feeds, gravatar support, download sources as zip/tarballs
61 - Setup project descriptions and info inside built in db for easy, non
62 file-system operations
63 - Inteligent cache with invalidation after push or project change, provides high
64 performance and always up to date data.
65 - Rss / atom feeds, gravatar support, download sources as zip/tar/gz
59 - Async tasks for speed and performance using celery_ (works without them too)
66 - Async tasks for speed and performance using celery_ (works without them too)
60 - Backup scripts can do backup of whole app and send it over scp to desired
67 - Backup scripts can do backup of whole app and send it over scp to desired
61 location
68 location
62 - Setup project descriptions and info inside built in db for easy, non
69 - Based on pylons / sqlalchemy / sqlite / whoosh / vcs
63 file-system operations
70
64 - Added cache with invalidation on push/repo management for high performance and
71
65 always up to date data.
72 .. include:: ./docs/screenshots.rst
66 - Based on pylons 1.0 / sqlalchemy 0.6 / sqlite
67
73
68
74
69 Incoming
75 Incoming / Plans
70 --------
76 ----------------
71
77
78 - project grouping
79 - User groups/teams
72 - code review (probably based on hg-review)
80 - code review (probably based on hg-review)
73 - full git_ support, with push/pull server
81 - full git_ support, with push/pull server (currently in beta tests)
82 - redmine integration
83 - public accessible activity feeds
74 - commit based build in wiki system
84 - commit based build in wiki system
75 - clone points and cloning from remote repositories into rhodecode
85 - clone points and cloning from remote repositories into rhodecode
76 (git_ and mercurial_)
86 (git_ and mercurial_)
77 - some cache optimizations
87 - more statistics and graph (global annotation + some more statistics)
78 - other cools stuff that i can figure out (or You can help me figure out)
88 - other cools stuff that i can figure out (or You can help me figure out)
79
89
80 License
90 License
@@ -83,8 +93,18 License
83 ``rhodecode`` is released under GPL_ license.
93 ``rhodecode`` is released under GPL_ license.
84
94
85
95
86 Documentation
96 Mailing group Q&A
87 -------------
97 -----------------
98
99 join the `Google group <http://groups.google.com/group/rhodecode>`_
100
101 open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
102
103 join #rhodecode on FreeNode (irc.freenode.net)
104 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
105
106 Online documentation
107 --------------------
88
108
89 Online documentation for current version is available at
109 Online documentation for current version is available at
90 http://packages.python.org/RhodeCode/.
110 http://packages.python.org/RhodeCode/.
@@ -92,13 +112,3 Documentation
92
112
93 make html
113 make html
94
114
95 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
96 .. _python: http://www.python.org/
97 .. _django: http://www.djangoproject.com/
98 .. _mercurial: http://mercurial.selenic.com/
99 .. _subversion: http://subversion.tigris.org/
100 .. _git: http://git-scm.com/
101 .. _celery: http://celeryproject.org/
102 .. _Sphinx: http://sphinx.pocoo.org/
103 .. _GPL: http://www.gnu.org/licenses/gpl.html
104 .. _vcs: http://pypi.python.org/pypi/vcs No newline at end of file
@@ -1,6 +1,6
1 ################################################################################
1 ################################################################################
2 ################################################################################
2 ################################################################################
3 # rhodecode - Pylons environment configuration #
3 # RhodeCode - Pylons environment configuration #
4 # #
4 # #
5 # The %(here)s variable will be replaced with the parent directory of this file#
5 # The %(here)s variable will be replaced with the parent directory of this file#
6 ################################################################################
6 ################################################################################
@@ -10,7 +10,7 debug = true
10 ################################################################################
10 ################################################################################
11 ## Uncomment and replace with the address which should receive ##
11 ## Uncomment and replace with the address which should receive ##
12 ## any error reports after application crash ##
12 ## any error reports after application crash ##
13 ## Additionally those settings will be used by rhodecode mailing system ##
13 ## Additionally those settings will be used by RhodeCode mailing system ##
14 ################################################################################
14 ################################################################################
15 #email_to = admin@localhost
15 #email_to = admin@localhost
16 #error_email_from = paste_error@localhost
16 #error_email_from = paste_error@localhost
@@ -21,20 +21,21 debug = true
21 #smtp_username =
21 #smtp_username =
22 #smtp_password =
22 #smtp_password =
23 #smtp_port =
23 #smtp_port =
24 #smtp_use_tls =
24 #smtp_use_tls = false
25 #smtp_use_ssl = true
25
26
26 [server:main]
27 [server:main]
27 ##nr of threads to spawn
28 ##nr of threads to spawn
28 threadpool_workers = 5
29 threadpool_workers = 5
29
30
30 ##max request before
31 ##max request before thread respawn
31 threadpool_max_requests = 6
32 threadpool_max_requests = 6
32
33
33 ##option to use threads of process
34 ##option to use threads of process
34 use_threadpool = false
35 use_threadpool = false
35
36
36 use = egg:Paste#http
37 use = egg:Paste#http
37 host = 127.0.0.1
38 host = 0.0.0.0
38 port = 5000
39 port = 5000
39
40
40 [app:main]
41 [app:main]
@@ -43,6 +44,35 full_stack = true
43 static_files = true
44 static_files = true
44 lang=en
45 lang=en
45 cache_dir = %(here)s/data
46 cache_dir = %(here)s/data
47 index_dir = %(here)s/data/index
48 cut_off_limit = 256000
49
50 ####################################
51 ### CELERY CONFIG ####
52 ####################################
53 use_celery = false
54 broker.host = localhost
55 broker.vhost = rabbitmqhost
56 broker.port = 5672
57 broker.user = rabbitmq
58 broker.password = qweqwe
59
60 celery.imports = rhodecode.lib.celerylib.tasks
61
62 celery.result.backend = amqp
63 celery.result.dburi = amqp://
64 celery.result.serialier = json
65
66 #celery.send.task.error.emails = true
67 #celery.amqp.task.result.expires = 18000
68
69 celeryd.concurrency = 2
70 #celeryd.log.file = celeryd.log
71 celeryd.log.level = debug
72 celeryd.max.tasks.per.child = 3
73
74 #tasks will never be sent to the queue, but executed locally instead.
75 celery.always.eager = false
46
76
47 ####################################
77 ####################################
48 ### BEAKER CACHE ####
78 ### BEAKER CACHE ####
@@ -60,9 +90,8 beaker.cache.short_term.expire=60
60 beaker.cache.long_term.type=memory
90 beaker.cache.long_term.type=memory
61 beaker.cache.long_term.expire=36000
91 beaker.cache.long_term.expire=36000
62
92
63
64 beaker.cache.sql_cache_short.type=memory
93 beaker.cache.sql_cache_short.type=memory
65 beaker.cache.sql_cache_short.expire=5
94 beaker.cache.sql_cache_short.expire=10
66
95
67 beaker.cache.sql_cache_med.type=memory
96 beaker.cache.sql_cache_med.type=memory
68 beaker.cache.sql_cache_med.expire=360
97 beaker.cache.sql_cache_med.expire=360
@@ -74,7 +103,7 beaker.cache.sql_cache_long.expire=3600
74 ### BEAKER SESSION ####
103 ### BEAKER SESSION ####
75 ####################################
104 ####################################
76 ## Type of storage used for the session, current types are
105 ## Type of storage used for the session, current types are
77 ## "dbm", "file", "memcached", "database", and "memory".
106 ## dbm, file, memcached, database, and memory.
78 ## The storage uses the Container API
107 ## The storage uses the Container API
79 ##that is also used by the cache system.
108 ##that is also used by the cache system.
80 beaker.session.type = file
109 beaker.session.type = file
@@ -116,7 +145,7 sqlalchemy.convert_unicode = true
116 ### LOGGING CONFIGURATION ####
145 ### LOGGING CONFIGURATION ####
117 ################################
146 ################################
118 [loggers]
147 [loggers]
119 keys = root, routes, rhodecode, sqlalchemy
148 keys = root, routes, rhodecode, sqlalchemy,beaker,templates
120
149
121 [handlers]
150 [handlers]
122 keys = console
151 keys = console
@@ -136,6 +165,19 level = DEBUG
136 handlers = console
165 handlers = console
137 qualname = routes.middleware
166 qualname = routes.middleware
138 # "level = DEBUG" logs the route matched and routing variables.
167 # "level = DEBUG" logs the route matched and routing variables.
168 propagate = 0
169
170 [logger_beaker]
171 level = ERROR
172 handlers = console
173 qualname = beaker.container
174 propagate = 0
175
176 [logger_templates]
177 level = INFO
178 handlers = console
179 qualname = pylons.templating
180 propagate = 0
139
181
140 [logger_rhodecode]
182 [logger_rhodecode]
141 level = DEBUG
183 level = DEBUG
@@ -3,22 +3,88
3 Changelog
3 Changelog
4 =========
4 =========
5
5
6 1.0.2 (**2010-11-XX**)
6 1.1.0 (**2010-12-18**)
7 ----------------------
7 ----------------------
8
8
9 :status: in-progress
10 :branch: beta
11
12 news
13 ++++
14
15 - rewrite of internals for vcs >=0.1.10
16 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
17 with older clients
18 - anonymous access, authentication via ldap
19 - performance upgrade for cached repos list - each repository has it's own
20 cache that's invalidated when needed.
21 - performance upgrades on repositories with large amount of commits (20K+)
22 - main page quick filter for filtering repositories
23 - user dashboards with ability to follow chosen repositories actions
24 - sends email to admin on new user registration
25 - added cache/statistics reset options into repository settings
26 - more detailed action logger (based on hooks) with pushed changesets lists
27 and options to disable those hooks from admin panel
28 - introduced new enhanced changelog for merges that shows more accurate results
29 - new improved and faster code stats (based on pygments lexers mapping tables,
30 showing up to 10 trending sources for each repository. Additionally stats
31 can be disabled in repository settings.
32 - gui optimizations, fixed application width to 1024px
33 - added cut off (for large files/changesets) limit into config files
34 - whoosh, celeryd, upgrade moved to paster command
35 - other than sqlite database backends can be used
36
37 fixes
38 +++++
39
40 - fixes #61 forked repo was showing only after cache expired
41 - fixes #76 no confirmation on user deletes
42 - fixes #66 Name field misspelled
43 - fixes #72 block user removal when he owns repositories
44 - fixes #69 added password confirmation fields
45 - fixes #87 RhodeCode crashes occasionally on updating repository owner
46 - fixes #82 broken annotations on files with more than 1 blank line at the end
47 - a lot of fixes and tweaks for file browser
48 - fixed detached session issues
49 - fixed when user had no repos he would see all repos listed in my account
50 - fixed ui() instance bug when global hgrc settings was loaded for server
51 instance and all hgrc options were merged with our db ui() object
52 - numerous small bugfixes
53
54 (special thanks for TkSoh for detailed feedback)
55
56
57 1.0.2 (**2010-11-12**)
58 ----------------------
59
60 news
61 ++++
62
63 - tested under python2.7
64 - bumped sqlalchemy and celery versions
65
66 fixes
67 +++++
68
9 - fixed #59 missing graph.js
69 - fixed #59 missing graph.js
10 - fixed repo_size crash when repository had broken symlinks
70 - fixed repo_size crash when repository had broken symlinks
11 - fixed python2.5 crashes.
71 - fixed python2.5 crashes.
12 - tested under python2.7
72
13 - bumped sqlalcehmy and celery versions
14
73
15 1.0.1 (**2010-11-10**)
74 1.0.1 (**2010-11-10**)
16 ----------------------
75 ----------------------
17
76
77 news
78 ++++
79
80 - small css updated
81
82 fixes
83 +++++
84
18 - fixed #53 python2.5 incompatible enumerate calls
85 - fixed #53 python2.5 incompatible enumerate calls
19 - fixed #52 disable mercurial extension for web
86 - fixed #52 disable mercurial extension for web
20 - fixed #51 deleting repositories don't delete it's dependent objects
87 - fixed #51 deleting repositories don't delete it's dependent objects
21 - small css updated
22
88
23
89
24 1.0.0 (**2010-11-02**)
90 1.0.0 (**2010-11-02**)
@@ -52,3 +118,4 1.0.0rc2 (**2010-10-11**)
52 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
118 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
53 occure. After vcs is fixed it'll be put back again.
119 occure. After vcs is fixed it'll be put back again.
54 - templating/css rewrites, optimized css.
120 - templating/css rewrites, optimized css.
121
@@ -16,7 +16,7 import sys, os
16 # If extensions (or modules to document with autodoc) are in another directory,
16 # If extensions (or modules to document with autodoc) are in another directory,
17 # add these directories to sys.path here. If the directory is relative to the
17 # add these directories to sys.path here. If the directory is relative to the
18 # documentation root, use os.path.abspath to make it absolute, like shown here.
18 # documentation root, use os.path.abspath to make it absolute, like shown here.
19 #sys.path.insert(0, os.path.abspath('.'))
19 sys.path.insert(0, os.path.abspath('..'))
20
20
21 # -- General configuration -----------------------------------------------------
21 # -- General configuration -----------------------------------------------------
22
22
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -1,107 +1,43
1 .. _index:
1 .. _index:
2
2
3 Welcome to RhodeCode (RhodiumCode) documentation!
3 .. include:: ./../README.rst
4 =================================================
5
6 ``RhodeCode`` (formerly hg-app) is Pylons based repository management and
7 serving for mercurial_. It's similar to github or bitbucket, but it's suppose to run
8 as standalone app, it's open source and focuses more on restricted access to repositories
9 There's no default free access to RhodeCode You have to create an account in order
10 to use the application. It's powered by vcs_ library that we created to handle
11 many various version control systems.
12
13 RhodeCode uses `Semantic Versioning <http://semver.org/>`_
14
15
16 RhodeCode demo
17 --------------
18
19 http://hg.python-works.com
20
21 The default access is
22
23 - username: demo
24 - password: demo
25
26 Source code
27 -----------
28
29 Source code is along with issue tracker is available at
30 http://bitbucket.org/marcinkuzminski/rhodecode
31
32 Also a source codes can be obtained from demo rhodecode instance
33 http://hg.python-works.com/rhodecode/summary
34
35 Features
36 --------
37
38 - Has it's own middleware to handle mercurial_ protocol request. Each request
39 can be logged and authenticated. Runs on threads unlikely to hgweb You can
40 make multiple pulls/pushes simultaneous. Supports http/https
41 - Full permissions and authentication per project private/read/write/admin.
42 One account for web interface and mercurial_ push/pull/clone.
43 - Mako templates let's you customize look and feel of application.
44 - Beautiful diffs, annotations and source codes all colored by pygments.
45 - Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics
46 - Admin interface with user/permission management. User activity journal logs
47 pulls, pushes, forks,registrations. Possible to disable built in hooks
48 - Server side forks, it's possible to fork a project and hack it free without
49 breaking the main.
50 - Full text search on source codes, search on file names. All powered by whoosh
51 and build in indexing daemons
52 (no external search servers required all in one application)
53 - Rss / atom feeds, gravatar support, download sources as zip/tarballs
54 - Async tasks for speed and performance using celery_ (works without them too)
55 - Backup scripts can do backup of whole app and send it over scp to desired
56 location
57 - Setup project descriptions and info inside built in db for easy, non
58 file-system operations
59 - Added cache with invalidation on push/repo management for high performance and
60 always up to date data.
61 - Based on pylons 1.0 / sqlalchemy 0.6 / sqlite
62
63
64 .. figure:: images/screenshot1_main_page.png
65 :align: left
66
67 Main page of RhodeCode
68
69 .. figure:: images/screenshot2_summary_page.png
70 :align: left
71
72 Summary page
73
74
75 Incoming
76 --------
77
78 - code review (probably based on hg-review)
79 - full git_ support, with push/pull server
80 - commit based build in wiki system
81 - clone points and cloning from remote repositories into rhodecode
82 (git_ and mercurial_)
83 - more statistics and graph (global annotation + some more statistics)
84 - user customized activity dashboards
85 - some cache optimizations
86 - other cools stuff that i can figure out (or You can help me figure out)
87
88 License
89 -------
90
91 ``rhodecode`` is released under GPL_ license.
92
93
4
94 Documentation
5 Documentation
95 -------------
6 -------------
96
7
8 **Installation:**
9
97 .. toctree::
10 .. toctree::
98 :maxdepth: 1
11 :maxdepth: 1
99
12
100 installation
13 installation
14 setup
101 upgrade
15 upgrade
102 setup
16
17 **Usage**
18
19 .. toctree::
20 :maxdepth: 1
21
22 enable_git
23 statistics
24
25 **Develop**
26
27 .. toctree::
28 :maxdepth: 1
29
30 contributing
103 changelog
31 changelog
104
32
33 **API**
34
35 .. toctree::
36 :maxdepth: 2
37
38 api/index
39
40
105 Other topics
41 Other topics
106 ------------
42 ------------
107
43
@@ -5,31 +5,20 Installation
5
5
6 ``RhodeCode`` is written entirely in Python, but in order to use it's full
6 ``RhodeCode`` is written entirely in Python, but in order to use it's full
7 potential there are some third-party requirements. When RhodeCode is used
7 potential there are some third-party requirements. When RhodeCode is used
8 together with celery_ You have to install some kind of message broker,
8 together with celery You have to install some kind of message broker,
9 recommended one is rabbitmq_ to make the async tasks work.
9 recommended one is rabbitmq_ to make the async tasks work.
10
10
11 Of course RhodeCode works in sync mode also, then You don't have to install
11 Of course RhodeCode works in sync mode also, then You don't have to install
12 any third party apps. Celery_ will give You large speed improvement when using
12 any third party apps. Celery_ will give You large speed improvement when using
13 many big repositories. If You plan to use it for 5 or 10 small repositories, it
13 many big repositories. If You plan to use it for 7 or 10 small repositories, it
14 will work just fine without celery running.
14 will work just fine without celery running.
15
15
16 After You decide to Run it with celery make sure You run celeryd and
16 After You decide to Run it with celery make sure You run celeryd using paster
17 message broker together with the application.
17 and message broker together with the application.
18
19 Requirements for Celery
20 -----------------------
21
22 **Message Broker**
23
24 - preferred is `RabbitMq <http://www.rabbitmq.com/>`_
25 - possible other is `Redis <http://code.google.com/p/redis/>`_
26
27 For installation instructions You can visit:
28 http://ask.github.com/celery/getting-started/index.html
29 It's very nice tutorial how to start celery_ with rabbitmq_
30
18
31 Install from Cheese Shop
19 Install from Cheese Shop
32 ------------------------
20 ------------------------
21 Rhodecode requires python 2.x greater than version 2.5
33
22
34 Easiest way to install ``rhodecode`` is to run::
23 Easiest way to install ``rhodecode`` is to run::
35
24
@@ -62,7 +51,7 Step by step installation example
62
51
63 ::
52 ::
64
53
65 source /var/www/rhodecode-venv/bin/activate
54 source activate /var/www/rhodecode-venv/bin/activate
66
55
67 - Make a folder for rhodecode somewhere on the filesystem for example
56 - Make a folder for rhodecode somewhere on the filesystem for example
68
57
@@ -80,8 +69,28 Step by step installation example
80 - this will install rhodecode together with pylons
69 - this will install rhodecode together with pylons
81 and all other required python libraries
70 and all other required python libraries
82
71
72 Requirements for Celery (optional)
73 ----------------------------------
74
75 .. note::
76 Installing message broker and using celery is optional, RhodeCode will
77 work without them perfectly fine.
78
79
80 **Message Broker**
81
82 - preferred is `RabbitMq <http://www.rabbitmq.com/>`_
83 - possible other is `Redis <http://code.google.com/p/redis/>`_
84
85 For installation instructions You can visit:
86 http://ask.github.com/celery/getting-started/index.html
87 It's very nice tutorial how to start celery_ with rabbitmq_
88
83
89
84 You can now proceed to :ref:`setup`
90 You can now proceed to :ref:`setup`
91 -----------------------------------
92
93
85
94
86 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
95 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
87 .. _python: http://www.python.org/
96 .. _python: http://www.python.org/
@@ -7,13 +7,20 Setup
7 Setting up the application
7 Setting up the application
8 --------------------------
8 --------------------------
9
9
10 First You'll ned to create RhodeCode config file. Run the following command
11 to do this
12
10 ::
13 ::
11
14
12 paster make-config RhodeCode production.ini
15 paster make-config RhodeCode production.ini
13
16
14 - This will create `production.ini` config inside the directory
17 - This will create `production.ini` config inside the directory
15 this config contain various settings for rhodecode, e.g port, email settings
18 this config contains various settings for RhodeCode, e.g proxy port,
16 static files, cache and logging.
19 email settings, usage of static files, cache, celery settings and logging.
20
21
22
23 Next we need to create the database.
17
24
18 ::
25 ::
19
26
@@ -24,55 +31,136 Setting up the application
24 existing ones. RhodeCode will simply add all new found repositories to
31 existing ones. RhodeCode will simply add all new found repositories to
25 it's database. Also make sure You specify correct path to repositories.
32 it's database. Also make sure You specify correct path to repositories.
26 - Remember that the given path for mercurial_ repositories must be write
33 - Remember that the given path for mercurial_ repositories must be write
27 accessible for the application. It's very important since RhodeCode web interface
34 accessible for the application. It's very important since RhodeCode web
28 will work even without such an access but, when trying to do a push it'll
35 interface will work even without such an access but, when trying to do a
29 eventually fail with permission denied errors.
36 push it'll eventually fail with permission denied errors.
30 - Run
37
38 You are ready to use rhodecode, to run it simply execute
31
39
32 ::
40 ::
33
41
34 paster serve production.ini
42 paster serve production.ini
35
43
36 - This command runs the rhodecode server the app should be available at the
44 - This command runs the RhodeCode server the app should be available at the
37 127.0.0.1:5000. This ip and port is configurable via the production.ini
45 127.0.0.1:5000. This ip and port is configurable via the production.ini
38 file created in previous step
46 file created in previous step
39 - Use admin account you created to login.
47 - Use admin account you created to login.
40 - Default permissions on each repository is read, and owner is admin. So
48 - Default permissions on each repository is read, and owner is admin. So
41 remember to update these if needed.
49 remember to update these if needed. In the admin panel You can toggle ldap,
50 anonymous, permissions settings. As well as edit more advanced options on
51 users and repositories
52
53
54 Setting up Whoosh full text search
55 ----------------------------------
56
57 Index for whoosh can be build starting from version 1.1 using paster command
58 passing repo locations to index, as well as Your config file that stores
59 whoosh index files locations. There is possible to pass `-f` to the options
60 to enable full index rebuild. Without that indexing will run always in in
61 incremental mode.
62
63 ::
64
65 paster make-index production.ini --repo-location=<location for repos>
66
67 for full index rebuild You can use
68
69 ::
42
70
43 Note
71 paster make-index production.ini -f --repo-location=<location for repos>
44 ----
72
73 - For full text search You can either put crontab entry for
74
75 This command can be run even from crontab in order to do periodical
76 index builds and keep Your index always up to date. An example entry might
77 look like this
78
79 ::
80
81 /path/to/python/bin/paster /path/to/rhodecode/production.ini --repo-location=<location for repos>
82
83 When using incremental(default) mode whoosh will check last modification date
84 of each file and add it to reindex if newer file is available. Also indexing
85 daemon checks for removed files and removes them from index.
86
87 Sometime You might want to rebuild index from scratch. You can do that using
88 the `-f` flag passed to paster command or, in admin panel You can check
89 `build from scratch` flag.
90
91
92 Setting up LDAP support
93 -----------------------
45
94
46 RhodeCode when running without the celery it's running all it's task in sync
95 RhodeCode starting from version 1.1 supports ldap authentication. In order
47 mode, for first few times when visiting summary page You can notice few
96 to use ldap, You have to install python-ldap package. This package is available
48 slow downs, this is due the statistics building it's cache. After all changesets
97 via pypi, so You can install it by running
49 are parsed it'll take the stats from cache and run much faster. Each summary
98
50 page display parse at most 250 changesets in order to not stress the cpu, so
99 ::
51 the full stats are going to be loaded after total_number_of_changesets/250
100
52 summary page visits.
101 easy_install python-ldap
102
103 ::
104
105 pip install python-ldap
106
107 .. note::
108 python-ldap requires some certain libs on Your system, so before installing
109 it check that You have at least `openldap`, and `sasl` libraries.
110
111 ldap settings are located in admin->ldap section,
112
113 Here's a typical ldap setup::
114
115 Enable ldap = checked #controls if ldap access is enabled
116 Host = host.domain.org #actual ldap server to connect
117 Port = 389 or 689 for ldaps #ldap server ports
118 Enable LDAPS = unchecked #enable disable ldaps
119 Account = <account> #access for ldap server(if required)
120 Password = <password> #password for ldap server(if required)
121 Base DN = uid=%(user)s,CN=users,DC=host,DC=domain,DC=org
122
123
124 `Account` and `Password` are optional, and used for two-phase ldap
125 authentication so those are credentials to access Your ldap, if it doesn't
126 support anonymous search/user lookups.
127
128 Base DN must have %(user)s template inside, it's a placer where Your uid used
129 to login would go, it allows admins to specify not standard schema for uid
130 variable
131
132 If all data are entered correctly, and `python-ldap` is properly installed
133 Users should be granted to access RhodeCode wit ldap accounts. When
134 logging at the first time an special ldap account is created inside RhodeCode,
135 so You can control over permissions even on ldap users. If such user exists
136 already in RhodeCode database ldap user with the same username would be not
137 able to access RhodeCode.
138
139 If You have problems with ldap access and believe You entered correct
140 information check out the RhodeCode logs,any error messages sent from
141 ldap will be saved there.
53
142
54
143
55
144
56 Setting up Whoosh
145 Setting Up Celery
57 -----------------
146 -----------------
58
147
59 - For full text search You can either put crontab entry for
148 Since version 1.1 celery is configured by the rhodecode ini configuration files
60
149 simply set use_celery=true in the ini file then add / change the configuration
61 ::
150 variables inside the ini file.
62
63 python /var/www/rhodecode/<rhodecode_installation_path>/lib/indexers/daemon.py incremental <put_here_path_to_repos>
64
151
65 When using incremental mode whoosh will check last modification date of each file
152 Remember that the ini files uses format with '.' not with '_' like celery
66 and add it to reindex if newer file is available. Also indexing daemon checks
153 so for example setting `BROKER_HOST` in celery means setting `broker.host` in
67 for removed files and removes them from index. Sometime You might want to rebuild
154 the config file.
68 index from scratch, in admin panel You can check `build from scratch` flag
155
69 and in standalone daemon You can pass `full` instead on incremental to build
156 In order to make start using celery run::
70 remove previous index and build new one.
157 paster celeryd <configfile.ini>
158
71
159
72 Nginx virtual host example
160 Nginx virtual host example
73 --------------------------
161 --------------------------
74
162
75 Sample config for nginx::
163 Sample config for nginx using proxy::
76
164
77 server {
165 server {
78 listen 80;
166 listen 80;
@@ -122,6 +210,16 in production.ini file::
122
210
123 To not have the statics served by the application. And improve speed.
211 To not have the statics served by the application. And improve speed.
124
212
213 Apache reverse proxy
214 --------------------
215 Tutorial can be found here
216 http://wiki.pylonshq.com/display/pylonscookbook/Apache+as+a+reverse+proxy+for+Pylons
217
218
219 Apache's example FCGI config
220 ----------------------------
221
222 TODO !
125
223
126 Other configuration files
224 Other configuration files
127 -------------------------
225 -------------------------
@@ -132,6 +230,29 http://hg.python-works.com/rhodecode/fil
132 and also an celeryconfig file can be use from here:
230 and also an celeryconfig file can be use from here:
133 http://hg.python-works.com/rhodecode/files/tip/celeryconfig.py
231 http://hg.python-works.com/rhodecode/files/tip/celeryconfig.py
134
232
233 Troubleshooting
234 ---------------
235
236 - missing static files ?
237
238 - make sure either to set the `static_files = true` in the .ini file or
239 double check the root path for Your http setup. It should point to
240 for example:
241 /home/my-virtual-python/lib/python2.6/site-packages/rhodecode/public
242
243 - can't install celery/rabbitmq
244
245 - don't worry RhodeCode works without them too. No extra setup required
246
247 - long lasting push timeouts ?
248
249 - make sure You set a longer timeouts in Your proxy/fcgi settings, timeouts
250 are caused by https server and not RhodeCode
251
252 - large pushes timeouts ?
253
254 - make sure You set a proper max_body_size for the http server
255
135
256
136
257
137 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
258 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
@@ -22,7 +22,26 Then make sure You run from the installa
22 paster make-config RhodeCode production.ini
22 paster make-config RhodeCode production.ini
23
23
24 This will display any changes made from new version of RhodeCode To your
24 This will display any changes made from new version of RhodeCode To your
25 current config. And tries to do an automerge.
25 current config. And tries to do an automerge. It's always better to do a backup
26 of config file and recheck the content after merge.
27
28 It's also good to rebuild the whoosh index since after upgrading the whoosh
29 version there could be introduced incompatible index changes.
30
31
32 The last step is to upgrade the database. To do this simply run
33
34 ::
35
36 paster upgrade-db production.ini
37
38 This will upgrade schema, as well as update some default on the database,
39 always recheck the settings of the application, if there are no new options
40 that need to be set.
41
42 .. note::
43 Always perform a database backup before doing upgrade.
44
26
45
27
46
28 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
47 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
@@ -22,6 +22,7 debug = true
22 #smtp_password =
22 #smtp_password =
23 #smtp_port =
23 #smtp_port =
24 #smtp_use_tls = false
24 #smtp_use_tls = false
25 #smtp_use_ssl = true
25
26
26 [server:main]
27 [server:main]
27 ##nr of threads to spawn
28 ##nr of threads to spawn
@@ -43,6 +44,35 full_stack = true
43 static_files = false
44 static_files = false
44 lang=en
45 lang=en
45 cache_dir = %(here)s/data
46 cache_dir = %(here)s/data
47 index_dir = %(here)s/data/index
48 cut_off_limit = 256000
49
50 ####################################
51 ### CELERY CONFIG ####
52 ####################################
53 use_celery = false
54 broker.host = localhost
55 broker.vhost = rabbitmqhost
56 broker.port = 5672
57 broker.user = rabbitmq
58 broker.password = qweqwe
59
60 celery.imports = rhodecode.lib.celerylib.tasks
61
62 celery.result.backend = amqp
63 celery.result.dburi = amqp://
64 celery.result.serialier = json
65
66 #celery.send.task.error.emails = true
67 #celery.amqp.task.result.expires = 18000
68
69 celeryd.concurrency = 2
70 #celeryd.log.file = celeryd.log
71 celeryd.log.level = debug
72 celeryd.max.tasks.per.child = 3
73
74 #tasks will never be sent to the queue, but executed locally instead.
75 celery.always.eager = false
46
76
47 ####################################
77 ####################################
48 ### BEAKER CACHE ####
78 ### BEAKER CACHE ####
@@ -136,6 +166,7 level = INFO
136 handlers = console
166 handlers = console
137 qualname = routes.middleware
167 qualname = routes.middleware
138 # "level = DEBUG" logs the route matched and routing variables.
168 # "level = DEBUG" logs the route matched and routing variables.
169 propagate = 0
139
170
140 [logger_rhodecode]
171 [logger_rhodecode]
141 level = DEBUG
172 level = DEBUG
@@ -1,8 +1,16
1 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 # encoding: utf-8
2 """
3 # RhodeCode, a web based repository management based on pylons
3 rhodecode.__init__
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 ~~~~~~~~~~~~~~~~~~
5 #
5
6 RhodeCode, a web based repository management based on pylons
7 versioning implementation: http://semver.org/
8
9 :created_on: Apr 9, 2010
10 :author: marcink
11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
13 """
6 # This program is free software; you can redistribute it and/or
14 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
15 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
16 # as published by the Free Software Foundation; version 2
@@ -17,19 +25,28
17 # along with this program; if not, write to the Free Software
25 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
27 # MA 02110-1301, USA.
20 """
28
21 Created on April 9, 2010
29
22 RhodeCode, a web based repository management based on pylons
30 VERSION = (1, 1, 0)
23 versioning implementation: http://semver.org/
31 __version__ = '.'.join((str(each) for each in VERSION[:4]))
24 @author: marcink
32 __dbversion__ = 2 #defines current db version for migrations
25 """
26
33
27 VERSION = (1, 0, 2,)
34 try:
35 from rhodecode.lib.utils import get_current_revision
36 _rev = get_current_revision()
37 except ImportError:
38 #this is needed when doing some setup.py operations
39 _rev = False
28
40
29 __version__ = '.'.join((str(each) for each in VERSION[:4]))
41 if len(VERSION) > 3 and _rev:
42 __version__ += ' [rev:%s]' % _rev[0]
30
43
31 def get_version():
44 def get_version():
32 """
45 """Returns shorter version (digit parts only) as string."""
33 Returns shorter version (digit parts only) as string.
46
34 """
35 return '.'.join((str(each) for each in VERSION[:3]))
47 return '.'.join((str(each) for each in VERSION[:3]))
48
49 BACKENDS = {
50 'hg': 'Mercurial repository',
51 #'git': 'Git repository',
52 }
@@ -1,6 +1,6
1 ################################################################################
1 ################################################################################
2 ################################################################################
2 ################################################################################
3 # rhodecode - Pylons environment configuration #
3 # RhodeCode - Pylons environment configuration #
4 # #
4 # #
5 # The %(here)s variable will be replaced with the parent directory of this file#
5 # The %(here)s variable will be replaced with the parent directory of this file#
6 ################################################################################
6 ################################################################################
@@ -10,7 +10,7 debug = true
10 ################################################################################
10 ################################################################################
11 ## Uncomment and replace with the address which should receive ##
11 ## Uncomment and replace with the address which should receive ##
12 ## any error reports after application crash ##
12 ## any error reports after application crash ##
13 ## Additionally those settings will be used by rhodecode mailing system ##
13 ## Additionally those settings will be used by RhodeCode mailing system ##
14 ################################################################################
14 ################################################################################
15 #email_to = admin@localhost
15 #email_to = admin@localhost
16 #error_email_from = paste_error@localhost
16 #error_email_from = paste_error@localhost
@@ -22,13 +22,14 debug = true
22 #smtp_password =
22 #smtp_password =
23 #smtp_port =
23 #smtp_port =
24 #smtp_use_tls = false
24 #smtp_use_tls = false
25 #smtp_use_ssl = true
25
26
26 [server:main]
27 [server:main]
27 ##nr of threads to spawn
28 ##nr of threads to spawn
28 threadpool_workers = 5
29 threadpool_workers = 5
29
30
30 ##max request before thread respawn
31 ##max request before thread respawn
31 threadpool_max_requests = 2
32 threadpool_max_requests = 10
32
33
33 ##option to use threads of process
34 ##option to use threads of process
34 use_threadpool = true
35 use_threadpool = true
@@ -43,7 +44,36 full_stack = true
43 static_files = true
44 static_files = true
44 lang=en
45 lang=en
45 cache_dir = %(here)s/data
46 cache_dir = %(here)s/data
47 index_dir = %(here)s/data/index
46 app_instance_uuid = ${app_instance_uuid}
48 app_instance_uuid = ${app_instance_uuid}
49 cut_off_limit = 256000
50
51 ####################################
52 ### CELERY CONFIG ####
53 ####################################
54 use_celery = false
55 broker.host = localhost
56 broker.vhost = rabbitmqhost
57 broker.port = 5672
58 broker.user = rabbitmq
59 broker.password = qweqwe
60
61 celery.imports = rhodecode.lib.celerylib.tasks
62
63 celery.result.backend = amqp
64 celery.result.dburi = amqp://
65 celery.result.serialier = json
66
67 #celery.send.task.error.emails = true
68 #celery.amqp.task.result.expires = 18000
69
70 celeryd.concurrency = 2
71 #celeryd.log.file = celeryd.log
72 celeryd.log.level = debug
73 celeryd.max.tasks.per.child = 3
74
75 #tasks will never be sent to the queue, but executed locally instead.
76 celery.always.eager = false
47
77
48 ####################################
78 ####################################
49 ### BEAKER CACHE ####
79 ### BEAKER CACHE ####
@@ -62,7 +92,7 beaker.cache.long_term.type=memory
62 beaker.cache.long_term.expire=36000
92 beaker.cache.long_term.expire=36000
63
93
64 beaker.cache.sql_cache_short.type=memory
94 beaker.cache.sql_cache_short.type=memory
65 beaker.cache.sql_cache_short.expire=5
95 beaker.cache.sql_cache_short.expire=10
66
96
67 beaker.cache.sql_cache_med.type=memory
97 beaker.cache.sql_cache_med.type=memory
68 beaker.cache.sql_cache_med.expire=360
98 beaker.cache.sql_cache_med.expire=360
@@ -136,6 +166,7 level = INFO
136 handlers = console
166 handlers = console
137 qualname = routes.middleware
167 qualname = routes.middleware
138 # "level = DEBUG" logs the route matched and routing variables.
168 # "level = DEBUG" logs the route matched and routing variables.
169 propagate = 0
139
170
140 [logger_rhodecode]
171 [logger_rhodecode]
141 level = DEBUG
172 level = DEBUG
@@ -6,7 +6,7 from rhodecode.config.routing import mak
6 from rhodecode.lib.auth import set_available_permissions, set_base_path
6 from rhodecode.lib.auth import set_available_permissions, set_base_path
7 from rhodecode.lib.utils import repo2db_mapper, make_ui, set_rhodecode_config
7 from rhodecode.lib.utils import repo2db_mapper, make_ui, set_rhodecode_config
8 from rhodecode.model import init_model
8 from rhodecode.model import init_model
9 from rhodecode.model.hg_model import _get_repos_cached_initial
9 from rhodecode.model.scm import ScmModel
10 from sqlalchemy import engine_from_config
10 from sqlalchemy import engine_from_config
11 import logging
11 import logging
12 import os
12 import os
@@ -52,8 +52,9 def load_environment(global_conf, app_co
52 test = os.path.split(config['__file__'])[-1] == 'test.ini'
52 test = os.path.split(config['__file__'])[-1] == 'test.ini'
53 if test:
53 if test:
54 from rhodecode.lib.utils import create_test_env, create_test_index
54 from rhodecode.lib.utils import create_test_env, create_test_index
55 create_test_env('/tmp', config)
55 from rhodecode.tests import TESTS_TMP_PATH
56 create_test_index('/tmp/*', True)
56 create_test_env(TESTS_TMP_PATH, config)
57 create_test_index(TESTS_TMP_PATH, True)
57
58
58 #MULTIPLE DB configs
59 #MULTIPLE DB configs
59 # Setup the SQLAlchemy database engine
60 # Setup the SQLAlchemy database engine
@@ -69,7 +70,8 def load_environment(global_conf, app_co
69 #init baseui
70 #init baseui
70 config['pylons.app_globals'].baseui = make_ui('db')
71 config['pylons.app_globals'].baseui = make_ui('db')
71
72
72 repo2db_mapper(_get_repos_cached_initial(config['pylons.app_globals'], initial))
73 g = config['pylons.app_globals']
74 repo2db_mapper(ScmModel().repo_scan(g.paths[0][1], g.baseui))
73 set_available_permissions(config)
75 set_available_permissions(config)
74 set_base_path(config)
76 set_base_path(config)
75 set_rhodecode_config(config)
77 set_rhodecode_config(config)
@@ -8,8 +8,10 from pylons.middleware import ErrorHandl
8 from pylons.wsgiapp import PylonsApp
8 from pylons.wsgiapp import PylonsApp
9 from routes.middleware import RoutesMiddleware
9 from routes.middleware import RoutesMiddleware
10 from rhodecode.lib.middleware.simplehg import SimpleHg
10 from rhodecode.lib.middleware.simplehg import SimpleHg
11 from rhodecode.lib.middleware.simplegit import SimpleGit
11 from rhodecode.lib.middleware.https_fixup import HttpsFixup
12 from rhodecode.lib.middleware.https_fixup import HttpsFixup
12 from rhodecode.config.environment import load_environment
13 from rhodecode.config.environment import load_environment
14 from paste.gzipper import make_gzip_middleware
13
15
14 def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
16 def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
15 """Create a Pylons WSGI application and return it
17 """Create a Pylons WSGI application and return it
@@ -43,6 +45,7 def make_app(global_conf, full_stack=Tru
43 # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
45 # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
44
46
45 app = SimpleHg(app, config)
47 app = SimpleHg(app, config)
48 app = SimpleGit(app, config)
46
49
47 if asbool(full_stack):
50 if asbool(full_stack):
48 # Handle Python exceptions
51 # Handle Python exceptions
@@ -65,6 +68,7 def make_app(global_conf, full_stack=Tru
65 # Serve static files
68 # Serve static files
66 static_app = StaticURLParser(config['pylons.paths']['static_files'])
69 static_app = StaticURLParser(config['pylons.paths']['static_files'])
67 app = Cascade([static_app, app])
70 app = Cascade([static_app, app])
71 app = make_gzip_middleware(app, global_conf, compress_level=1)
68
72
69 app.config = config
73 app.config = config
70
74
@@ -35,7 +35,7 def make_map(config):
35 #==========================================================================
35 #==========================================================================
36
36
37 #MAIN PAGE
37 #MAIN PAGE
38 map.connect('hg_home', '/', controller='hg', action='index')
38 map.connect('home', '/', controller='home', action='index')
39 map.connect('bugtracker', "http://bitbucket.org/marcinkuzminski/rhodecode/issues", _static=True)
39 map.connect('bugtracker', "http://bitbucket.org/marcinkuzminski/rhodecode/issues", _static=True)
40 map.connect('gpl_license', "http://www.gnu.org/licenses/gpl.html", _static=True)
40 map.connect('gpl_license', "http://www.gnu.org/licenses/gpl.html", _static=True)
41 #ADMIN REPOSITORY REST ROUTES
41 #ADMIN REPOSITORY REST ROUTES
@@ -73,13 +73,27 def make_map(config):
73 m.connect('delete_repo_user', "/repos_delete_user/{repo_name:.*}",
73 m.connect('delete_repo_user', "/repos_delete_user/{repo_name:.*}",
74 action="delete_perm_user", conditions=dict(method=["DELETE"],
74 action="delete_perm_user", conditions=dict(method=["DELETE"],
75 function=check_repo))
75 function=check_repo))
76
76 #settings actions
77 m.connect('repo_stats', "/repos_stats/{repo_name:.*}",
78 action="repo_stats", conditions=dict(method=["DELETE"],
79 function=check_repo))
80 m.connect('repo_cache', "/repos_cache/{repo_name:.*}",
81 action="repo_cache", conditions=dict(method=["DELETE"],
82 function=check_repo))
77 #ADMIN USER REST ROUTES
83 #ADMIN USER REST ROUTES
78 map.resource('user', 'users', controller='admin/users', path_prefix='/_admin')
84 map.resource('user', 'users', controller='admin/users', path_prefix='/_admin')
79
85
80 #ADMIN PERMISSIONS REST ROUTES
86 #ADMIN PERMISSIONS REST ROUTES
81 map.resource('permission', 'permissions', controller='admin/permissions', path_prefix='/_admin')
87 map.resource('permission', 'permissions', controller='admin/permissions', path_prefix='/_admin')
82
88
89
90 ##ADMIN LDAP SETTINGS
91 map.connect('ldap_settings', '/_admin/ldap', controller='admin/ldap_settings',
92 action='ldap_settings', conditions=dict(method=["POST"]))
93 map.connect('ldap_home', '/_admin/ldap', controller='admin/ldap_settings',)
94
95
96
83 #ADMIN SETTINGS REST ROUTES
97 #ADMIN SETTINGS REST ROUTES
84 with map.submapper(path_prefix='/_admin', controller='admin/settings') as m:
98 with map.submapper(path_prefix='/_admin', controller='admin/settings') as m:
85 m.connect("admin_settings", "/settings",
99 m.connect("admin_settings", "/settings",
@@ -116,6 +130,14 def make_map(config):
116 m.connect('admin_home', '', action='index')#main page
130 m.connect('admin_home', '', action='index')#main page
117 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
131 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
118 action='add_repo')
132 action='add_repo')
133
134
135 #USER JOURNAL
136 map.connect('journal', '/_admin/journal', controller='journal',)
137 map.connect('toggle_following', '/_admin/toggle_following', controller='journal',
138 action='toggle_following', conditions=dict(method=["POST"]))
139
140
119 #SEARCH
141 #SEARCH
120 map.connect('search', '/_admin/search', controller='search',)
142 map.connect('search', '/_admin/search', controller='search',)
121 map.connect('search_repo', '/_admin/search/{search_repo:.*}', controller='search')
143 map.connect('search_repo', '/_admin/search/{search_repo:.*}', controller='search')
@@ -1,8 +1,15
1 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 # encoding: utf-8
2 """
3 # admin controller for pylons
3 rhodecode.controllers.admin.admin
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Controller for Admin panel of Rhodecode
7
8 :created_on: Apr 7, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
6 # This program is free software; you can redistribute it and/or
13 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
14 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
15 # as published by the Free Software Foundation; version 2
@@ -17,15 +24,10
17 # along with this program; if not, write to the Free Software
24 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
26 # MA 02110-1301, USA.
20 """
27
21 Created on April 7, 2010
22 admin controller for pylons
23 @author: marcink
24 """
25 import logging
28 import logging
26 from pylons import request, response, session, tmpl_context as c
29 from pylons import request, tmpl_context as c
27 from rhodecode.lib.base import BaseController, render
30 from rhodecode.lib.base import BaseController, render
28 from rhodecode.model import meta
29 from rhodecode.model.db import UserLog
31 from rhodecode.model.db import UserLog
30 from webhelpers.paginate import Page
32 from webhelpers.paginate import Page
31 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
33 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
@@ -1,8 +1,15
1 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 # encoding: utf-8
2 """
3 # permissions controller for pylons
3 rhodecode.controllers.admin.permissions
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 #
5
6 permissions controller for Rhodecode
7
8 :created_on: Apr 27, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
6 # This program is free software; you can redistribute it and/or
13 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
14 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
15 # as published by the Free Software Foundation; version 2
@@ -17,11 +24,6
17 # along with this program; if not, write to the Free Software
24 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
26 # MA 02110-1301, USA.
20 """
21 Created on April 27, 2010
22 permissions controller for pylons
23 @author: marcink
24 """
25
27
26 from formencode import htmlfill
28 from formencode import htmlfill
27 from pylons import request, session, tmpl_context as c, url
29 from pylons import request, session, tmpl_context as c, url
@@ -29,11 +31,12 from pylons.controllers.util import abor
29 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
30 from rhodecode.lib import helpers as h
32 from rhodecode.lib import helpers as h
31 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
33 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
34 from rhodecode.lib.auth_ldap import LdapImportError
32 from rhodecode.lib.base import BaseController, render
35 from rhodecode.lib.base import BaseController, render
33 from rhodecode.model.db import User, UserLog
36 from rhodecode.model.forms import LdapSettingsForm, DefaultPermissionsForm
34 from rhodecode.model.forms import UserForm, DefaultPermissionsForm
37 from rhodecode.model.permission import PermissionModel
35 from rhodecode.model.permission_model import PermissionModel
38 from rhodecode.model.settings import SettingsModel
36 from rhodecode.model.user_model import UserModel
39 from rhodecode.model.user import UserModel
37 import formencode
40 import formencode
38 import logging
41 import logging
39 import traceback
42 import traceback
@@ -58,7 +61,8 class PermissionsController(BaseControll
58 ('repository.write', _('Write'),),
61 ('repository.write', _('Write'),),
59 ('repository.admin', _('Admin'),)]
62 ('repository.admin', _('Admin'),)]
60 self.register_choices = [
63 self.register_choices = [
61 ('hg.register.none', 'disabled'),
64 ('hg.register.none',
65 _('disabled')),
62 ('hg.register.manual_activate',
66 ('hg.register.manual_activate',
63 _('allowed with manual account activation')),
67 _('allowed with manual account activation')),
64 ('hg.register.auto_activate',
68 ('hg.register.auto_activate',
@@ -99,17 +103,18 class PermissionsController(BaseControll
99 form_result = _form.to_python(dict(request.POST))
103 form_result = _form.to_python(dict(request.POST))
100 form_result.update({'perm_user_name':id})
104 form_result.update({'perm_user_name':id})
101 permission_model.update(form_result)
105 permission_model.update(form_result)
102 h.flash(_('Default permissions updated succesfully'),
106 h.flash(_('Default permissions updated successfully'),
103 category='success')
107 category='success')
104
108
105 except formencode.Invalid, errors:
109 except formencode.Invalid, errors:
106 c.perms_choices = self.perms_choices
110 c.perms_choices = self.perms_choices
107 c.register_choices = self.register_choices
111 c.register_choices = self.register_choices
108 c.create_choices = self.create_choices
112 c.create_choices = self.create_choices
113 defaults = errors.value
109
114
110 return htmlfill.render(
115 return htmlfill.render(
111 render('admin/permissions/permissions.html'),
116 render('admin/permissions/permissions.html'),
112 defaults=errors.value,
117 defaults=defaults,
113 errors=errors.error_dict or {},
118 errors=errors.error_dict or {},
114 prefix_error=False,
119 prefix_error=False,
115 encoding="UTF-8")
120 encoding="UTF-8")
@@ -143,8 +148,11 class PermissionsController(BaseControll
143 c.create_choices = self.create_choices
148 c.create_choices = self.create_choices
144
149
145 if id == 'default':
150 if id == 'default':
146 defaults = {'_method':'put'}
151 default_user = UserModel().get_by_username('default')
147 for p in UserModel().get_default().user_perms:
152 defaults = {'_method':'put',
153 'anonymous':default_user.active}
154
155 for p in default_user.user_perms:
148 if p.permission.permission_name.startswith('repository.'):
156 if p.permission.permission_name.startswith('repository.'):
149 defaults['default_perm'] = p.permission.permission_name
157 defaults['default_perm'] = p.permission.permission_name
150
158
@@ -1,8 +1,15
1 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 # encoding: utf-8
2 """
3 # repos controller for pylons
3 rhodecode.controllers.admin.repos
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 #
5
6 Admin controller for RhodeCode
7
8 :created_on: Apr 7, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
6 # This program is free software; you can redistribute it and/or
13 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
14 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
15 # as published by the Free Software Foundation; version 2
@@ -17,17 +24,18
17 # along with this program; if not, write to the Free Software
24 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
26 # MA 02110-1301, USA.
20 """
27
21 Created on April 7, 2010
28 import logging
22 admin controller for pylons
29 import traceback
23 @author: marcink
30 import formencode
24 """
31 from operator import itemgetter
25 from formencode import htmlfill
32 from formencode import htmlfill
26 from operator import itemgetter
33
27 from paste.httpexceptions import HTTPInternalServerError
34 from paste.httpexceptions import HTTPInternalServerError
28 from pylons import request, response, session, tmpl_context as c, url
35 from pylons import request, response, session, tmpl_context as c, url
29 from pylons.controllers.util import abort, redirect
36 from pylons.controllers.util import abort, redirect
30 from pylons.i18n.translation import _
37 from pylons.i18n.translation import _
38
31 from rhodecode.lib import helpers as h
39 from rhodecode.lib import helpers as h
32 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
40 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
33 HasPermissionAnyDecorator
41 HasPermissionAnyDecorator
@@ -35,11 +43,9 from rhodecode.lib.base import BaseContr
35 from rhodecode.lib.utils import invalidate_cache, action_logger
43 from rhodecode.lib.utils import invalidate_cache, action_logger
36 from rhodecode.model.db import User
44 from rhodecode.model.db import User
37 from rhodecode.model.forms import RepoForm
45 from rhodecode.model.forms import RepoForm
38 from rhodecode.model.hg_model import HgModel
46 from rhodecode.model.scm import ScmModel
39 from rhodecode.model.repo_model import RepoModel
47 from rhodecode.model.repo import RepoModel
40 import formencode
48
41 import logging
42 import traceback
43
49
44 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
45
51
@@ -60,7 +66,7 class ReposController(BaseController):
60 def index(self, format='html'):
66 def index(self, format='html'):
61 """GET /repos: All items in the collection"""
67 """GET /repos: All items in the collection"""
62 # url('repos')
68 # url('repos')
63 cached_repo_list = HgModel().get_repos()
69 cached_repo_list = ScmModel().get_repos()
64 c.repos_list = sorted(cached_repo_list, key=itemgetter('name_sort'))
70 c.repos_list = sorted(cached_repo_list, key=itemgetter('name_sort'))
65 return render('admin/repos/repos.html')
71 return render('admin/repos/repos.html')
66
72
@@ -74,7 +80,6 class ReposController(BaseController):
74 try:
80 try:
75 form_result = _form.to_python(dict(request.POST))
81 form_result = _form.to_python(dict(request.POST))
76 repo_model.create(form_result, c.rhodecode_user)
82 repo_model.create(form_result, c.rhodecode_user)
77 invalidate_cache('cached_repo_list')
78 h.flash(_('created repository %s') % form_result['repo_name'],
83 h.flash(_('created repository %s') % form_result['repo_name'],
79 category='success')
84 category='success')
80
85
@@ -106,7 +111,7 class ReposController(BaseController):
106 % form_result.get('repo_name')
111 % form_result.get('repo_name')
107 h.flash(msg, category='error')
112 h.flash(msg, category='error')
108 if request.POST.get('user_created'):
113 if request.POST.get('user_created'):
109 return redirect(url('hg_home'))
114 return redirect(url('home'))
110 return redirect(url('repos'))
115 return redirect(url('repos'))
111
116
112 @HasPermissionAllDecorator('hg.admin')
117 @HasPermissionAllDecorator('hg.admin')
@@ -133,12 +138,29 class ReposController(BaseController):
133 try:
138 try:
134 form_result = _form.to_python(dict(request.POST))
139 form_result = _form.to_python(dict(request.POST))
135 repo_model.update(repo_name, form_result)
140 repo_model.update(repo_name, form_result)
136 invalidate_cache('cached_repo_list')
141 invalidate_cache('get_repo_cached_%s' % repo_name)
137 h.flash(_('Repository %s updated succesfully' % repo_name),
142 h.flash(_('Repository %s updated successfully' % repo_name),
138 category='success')
143 category='success')
139 changed_name = form_result['repo_name']
144 changed_name = form_result['repo_name']
145 action_logger(self.rhodecode_user, 'admin_updated_repo',
146 changed_name, '', self.sa)
147
140 except formencode.Invalid, errors:
148 except formencode.Invalid, errors:
141 c.repo_info = repo_model.get(repo_name)
149 c.repo_info = repo_model.get_by_repo_name(repo_name)
150 if c.repo_info.stats:
151 last_rev = c.repo_info.stats.stat_on_revision
152 else:
153 last_rev = 0
154 c.stats_revision = last_rev
155 r = ScmModel().get(repo_name)
156 c.repo_last_rev = r.revisions[-1] if r.revisions else 0
157
158 if last_rev == 0:
159 c.stats_percentage = 0
160 else:
161 c.stats_percentage = '%.2f' % ((float((last_rev)) /
162 c.repo_last_rev) * 100)
163
142 c.users_array = repo_model.get_users_js()
164 c.users_array = repo_model.get_users_js()
143 errors.value.update({'user':c.repo_info.user.username})
165 errors.value.update({'user':c.repo_info.user.username})
144 return htmlfill.render(
166 return htmlfill.render(
@@ -150,7 +172,7 class ReposController(BaseController):
150
172
151 except Exception:
173 except Exception:
152 log.error(traceback.format_exc())
174 log.error(traceback.format_exc())
153 h.flash(_('error occured during update of repository %s') \
175 h.flash(_('error occurred during update of repository %s') \
154 % repo_name, category='error')
176 % repo_name, category='error')
155
177
156 return redirect(url('edit_repo', repo_name=changed_name))
178 return redirect(url('edit_repo', repo_name=changed_name))
@@ -166,7 +188,7 class ReposController(BaseController):
166 # url('repo', repo_name=ID)
188 # url('repo', repo_name=ID)
167
189
168 repo_model = RepoModel()
190 repo_model = RepoModel()
169 repo = repo_model.get(repo_name)
191 repo = repo_model.get_by_repo_name(repo_name)
170 if not repo:
192 if not repo:
171 h.flash(_('%s repository is not mapped to db perhaps'
193 h.flash(_('%s repository is not mapped to db perhaps'
172 ' it was moved or renamed from the filesystem'
194 ' it was moved or renamed from the filesystem'
@@ -179,7 +201,7 class ReposController(BaseController):
179 action_logger(self.rhodecode_user, 'admin_deleted_repo',
201 action_logger(self.rhodecode_user, 'admin_deleted_repo',
180 repo_name, '', self.sa)
202 repo_name, '', self.sa)
181 repo_model.delete(repo)
203 repo_model.delete(repo)
182 invalidate_cache('cached_repo_list')
204 invalidate_cache('get_repo_cached_%s' % repo_name)
183 h.flash(_('deleted repository %s') % repo_name, category='success')
205 h.flash(_('deleted repository %s') % repo_name, category='success')
184
206
185 except Exception, e:
207 except Exception, e:
@@ -205,6 +227,35 class ReposController(BaseController):
205 raise HTTPInternalServerError()
227 raise HTTPInternalServerError()
206
228
207 @HasPermissionAllDecorator('hg.admin')
229 @HasPermissionAllDecorator('hg.admin')
230 def repo_stats(self, repo_name):
231 """
232 DELETE an existing repository statistics
233 :param repo_name:
234 """
235
236 try:
237 repo_model = RepoModel()
238 repo_model.delete_stats(repo_name)
239 except Exception, e:
240 h.flash(_('An error occured during deletion of repository stats'),
241 category='error')
242 return redirect(url('edit_repo', repo_name=repo_name))
243
244 @HasPermissionAllDecorator('hg.admin')
245 def repo_cache(self, repo_name):
246 """
247 INVALIDATE exisitings repository cache
248 :param repo_name:
249 """
250
251 try:
252 ScmModel().mark_for_invalidation(repo_name)
253 except Exception, e:
254 h.flash(_('An error occurred during cache invalidation'),
255 category='error')
256 return redirect(url('edit_repo', repo_name=repo_name))
257
258 @HasPermissionAllDecorator('hg.admin')
208 def show(self, repo_name, format='html'):
259 def show(self, repo_name, format='html'):
209 """GET /repos/repo_name: Show a specific item"""
260 """GET /repos/repo_name: Show a specific item"""
210 # url('repo', repo_name=ID)
261 # url('repo', repo_name=ID)
@@ -214,8 +265,10 class ReposController(BaseController):
214 """GET /repos/repo_name/edit: Form to edit an existing item"""
265 """GET /repos/repo_name/edit: Form to edit an existing item"""
215 # url('edit_repo', repo_name=ID)
266 # url('edit_repo', repo_name=ID)
216 repo_model = RepoModel()
267 repo_model = RepoModel()
217 c.repo_info = repo = repo_model.get(repo_name)
268 r = ScmModel().get(repo_name)
218 if not repo:
269 c.repo_info = repo_model.get_by_repo_name(repo_name)
270
271 if c.repo_info is None:
219 h.flash(_('%s repository is not mapped to db perhaps'
272 h.flash(_('%s repository is not mapped to db perhaps'
220 ' it was created or renamed from the filesystem'
273 ' it was created or renamed from the filesystem'
221 ' please run the application again'
274 ' please run the application again'
@@ -223,7 +276,22 class ReposController(BaseController):
223 category='error')
276 category='error')
224
277
225 return redirect(url('repos'))
278 return redirect(url('repos'))
226 defaults = c.repo_info.__dict__
279
280 if c.repo_info.stats:
281 last_rev = c.repo_info.stats.stat_on_revision
282 else:
283 last_rev = 0
284 c.stats_revision = last_rev
285
286 c.repo_last_rev = r.revisions[-1] if r.revisions else 0
287
288 if last_rev == 0:
289 c.stats_percentage = 0
290 else:
291 c.stats_percentage = '%.2f' % ((float((last_rev)) /
292 c.repo_last_rev) * 100)
293
294 defaults = c.repo_info.get_dict()
227 if c.repo_info.user:
295 if c.repo_info.user:
228 defaults.update({'user':c.repo_info.user.username})
296 defaults.update({'user':c.repo_info.user.username})
229 else:
297 else:
@@ -1,8 +1,14
1 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 # encoding: utf-8
2 """
3 # settings controller for pylons
3 package.rhodecode.controllers.admin.settings
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 ~~~~~~~~~~~~~~
5 #
5 settings controller for rhodecode admin
6
7 :created_on: Jul 14, 2010
8 :author: marcink
9 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
10 :license: GPLv3, see COPYING for more details.
11 """
6 # This program is free software; you can redistribute it and/or
12 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
13 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
14 # as published by the Free Software Foundation; version 2
@@ -17,11 +23,7
17 # along with this program; if not, write to the Free Software
23 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
24 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
25 # MA 02110-1301, USA.
20 """
26
21 Created on July 14, 2010
22 settings controller for pylons
23 @author: marcink
24 """
25 from formencode import htmlfill
27 from formencode import htmlfill
26 from pylons import request, session, tmpl_context as c, url, app_globals as g, \
28 from pylons import request, session, tmpl_context as c, url, app_globals as g, \
27 config
29 config
@@ -29,16 +31,18 from pylons.controllers.util import abor
29 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
30 from rhodecode.lib import helpers as h
32 from rhodecode.lib import helpers as h
31 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
33 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
32 HasPermissionAnyDecorator
34 HasPermissionAnyDecorator, NotAnonymous
33 from rhodecode.lib.base import BaseController, render
35 from rhodecode.lib.base import BaseController, render
36 from rhodecode.lib.celerylib import tasks, run_task
34 from rhodecode.lib.utils import repo2db_mapper, invalidate_cache, \
37 from rhodecode.lib.utils import repo2db_mapper, invalidate_cache, \
35 set_rhodecode_config, get_hg_settings, get_hg_ui_settings, make_ui
38 set_rhodecode_config
36 from rhodecode.model.db import User, UserLog, RhodeCodeSettings, RhodeCodeUi
39 from rhodecode.model.db import RhodeCodeUi, Repository
37 from rhodecode.model.forms import UserForm, ApplicationSettingsForm, \
40 from rhodecode.model.forms import UserForm, ApplicationSettingsForm, \
38 ApplicationUiSettingsForm
41 ApplicationUiSettingsForm
39 from rhodecode.model.hg_model import HgModel
42 from rhodecode.model.scm import ScmModel
40 from rhodecode.model.user_model import UserModel
43 from rhodecode.model.settings import SettingsModel
41 from rhodecode.lib.celerylib import tasks, run_task
44 from rhodecode.model.user import UserModel
45 from sqlalchemy import func
42 import formencode
46 import formencode
43 import logging
47 import logging
44 import traceback
48 import traceback
@@ -66,8 +70,8 class SettingsController(BaseController)
66 """GET /admin/settings: All items in the collection"""
70 """GET /admin/settings: All items in the collection"""
67 # url('admin_settings')
71 # url('admin_settings')
68
72
69 defaults = get_hg_settings()
73 defaults = SettingsModel().get_app_settings()
70 defaults.update(get_hg_ui_settings())
74 defaults.update(self.get_hg_ui_settings())
71 return htmlfill.render(
75 return htmlfill.render(
72 render('admin/settings/settings.html'),
76 render('admin/settings/settings.html'),
73 defaults=defaults,
77 defaults=defaults,
@@ -98,13 +102,16 class SettingsController(BaseController)
98 rm_obsolete = request.POST.get('destroy', False)
102 rm_obsolete = request.POST.get('destroy', False)
99 log.debug('Rescanning directories with destroy=%s', rm_obsolete)
103 log.debug('Rescanning directories with destroy=%s', rm_obsolete)
100
104
101 initial = HgModel.repo_scan(g.paths[0][0], g.paths[0][1], g.baseui)
105 initial = ScmModel().repo_scan(g.paths[0][1], g.baseui)
106 for repo_name in initial.keys():
107 invalidate_cache('get_repo_cached_%s' % repo_name)
108
102 repo2db_mapper(initial, rm_obsolete)
109 repo2db_mapper(initial, rm_obsolete)
103 invalidate_cache('cached_repo_list')
110
104 h.flash(_('Repositories successfully rescanned'), category='success')
111 h.flash(_('Repositories successfully rescanned'), category='success')
105
112
106 if setting_id == 'whoosh':
113 if setting_id == 'whoosh':
107 repo_location = get_hg_ui_settings()['paths_root_path']
114 repo_location = self.get_hg_ui_settings()['paths_root_path']
108 full_index = request.POST.get('full_index', False)
115 full_index = request.POST.get('full_index', False)
109 task = run_task(tasks.whoosh_index, repo_location, full_index)
116 task = run_task(tasks.whoosh_index, repo_location, full_index)
110
117
@@ -114,14 +121,12 class SettingsController(BaseController)
114 application_form = ApplicationSettingsForm()()
121 application_form = ApplicationSettingsForm()()
115 try:
122 try:
116 form_result = application_form.to_python(dict(request.POST))
123 form_result = application_form.to_python(dict(request.POST))
117
124 settings_model = SettingsModel()
118 try:
125 try:
119 hgsettings1 = self.sa.query(RhodeCodeSettings)\
126 hgsettings1 = settings_model.get('title')
120 .filter(RhodeCodeSettings.app_settings_name == 'title').one()
121 hgsettings1.app_settings_value = form_result['rhodecode_title']
127 hgsettings1.app_settings_value = form_result['rhodecode_title']
122
128
123 hgsettings2 = self.sa.query(RhodeCodeSettings)\
129 hgsettings2 = settings_model.get('realm')
124 .filter(RhodeCodeSettings.app_settings_name == 'realm').one()
125 hgsettings2.app_settings_value = form_result['rhodecode_realm']
130 hgsettings2.app_settings_value = form_result['rhodecode_realm']
126
131
127
132
@@ -173,13 +178,21 class SettingsController(BaseController)
173 .filter(RhodeCodeUi.ui_key == 'changegroup.repo_size').one()
178 .filter(RhodeCodeUi.ui_key == 'changegroup.repo_size').one()
174 hgsettings4.ui_active = bool(form_result['hooks_changegroup_repo_size'])
179 hgsettings4.ui_active = bool(form_result['hooks_changegroup_repo_size'])
175
180
181 hgsettings5 = self.sa.query(RhodeCodeUi)\
182 .filter(RhodeCodeUi.ui_key == 'pretxnchangegroup.push_logger').one()
183 hgsettings5.ui_active = bool(form_result['hooks_pretxnchangegroup_push_logger'])
176
184
185 hgsettings6 = self.sa.query(RhodeCodeUi)\
186 .filter(RhodeCodeUi.ui_key == 'preoutgoing.pull_logger').one()
187 hgsettings6.ui_active = bool(form_result['hooks_preoutgoing_pull_logger'])
177
188
178
189
179 self.sa.add(hgsettings1)
190 self.sa.add(hgsettings1)
180 self.sa.add(hgsettings2)
191 self.sa.add(hgsettings2)
181 self.sa.add(hgsettings3)
192 self.sa.add(hgsettings3)
182 self.sa.add(hgsettings4)
193 self.sa.add(hgsettings4)
194 self.sa.add(hgsettings5)
195 self.sa.add(hgsettings6)
183 self.sa.commit()
196 self.sa.commit()
184
197
185 h.flash(_('Updated mercurial settings'),
198 h.flash(_('Updated mercurial settings'),
@@ -225,24 +238,27 class SettingsController(BaseController)
225 """GET /admin/settings/setting_id/edit: Form to edit an existing item"""
238 """GET /admin/settings/setting_id/edit: Form to edit an existing item"""
226 # url('admin_edit_setting', setting_id=ID)
239 # url('admin_edit_setting', setting_id=ID)
227
240
228
241 @NotAnonymous()
229 def my_account(self):
242 def my_account(self):
230 """
243 """
231 GET /_admin/my_account Displays info about my account
244 GET /_admin/my_account Displays info about my account
232 """
245 """
233 # url('admin_settings_my_account')
246 # url('admin_settings_my_account')
234 c.user = self.sa.query(User).get(c.rhodecode_user.user_id)
247
235 c.user_repos = []
248 c.user = UserModel().get(c.rhodecode_user.user_id, cache=False)
236 for repo in c.cached_repo_list.values():
249 all_repos = self.sa.query(Repository)\
237 if repo.dbrepo.user.username == c.user.username:
250 .filter(Repository.user_id == c.user.user_id)\
238 c.user_repos.append(repo)
251 .order_by(func.lower(Repository.repo_name))\
252 .all()
253
254 c.user_repos = ScmModel().get_repos(all_repos)
239
255
240 if c.user.username == 'default':
256 if c.user.username == 'default':
241 h.flash(_("You can't edit this user since it's"
257 h.flash(_("You can't edit this user since it's"
242 " crucial for entire application"), category='warning')
258 " crucial for entire application"), category='warning')
243 return redirect(url('users'))
259 return redirect(url('users'))
244
260
245 defaults = c.user.__dict__
261 defaults = c.user.get_dict()
246 return htmlfill.render(
262 return htmlfill.render(
247 render('admin/users/user_edit_my_account.html'),
263 render('admin/users/user_edit_my_account.html'),
248 defaults=defaults,
264 defaults=defaults,
@@ -266,15 +282,18 class SettingsController(BaseController)
266 try:
282 try:
267 form_result = _form.to_python(dict(request.POST))
283 form_result = _form.to_python(dict(request.POST))
268 user_model.update_my_account(uid, form_result)
284 user_model.update_my_account(uid, form_result)
269 h.flash(_('Your account was updated succesfully'),
285 h.flash(_('Your account was updated successfully'),
270 category='success')
286 category='success')
271
287
272 except formencode.Invalid, errors:
288 except formencode.Invalid, errors:
273 c.user = self.sa.query(User).get(c.rhodecode_user.user_id)
289 c.user = user_model.get(c.rhodecode_user.user_id, cache=False)
274 c.user_repos = []
290 c.user = UserModel().get(c.rhodecode_user.user_id, cache=False)
275 for repo in c.cached_repo_list.values():
291 all_repos = self.sa.query(Repository)\
276 if repo.dbrepo.user.username == c.user.username:
292 .filter(Repository.user_id == c.user.user_id)\
277 c.user_repos.append(repo)
293 .order_by(func.lower(Repository.repo_name))\
294 .all()
295 c.user_repos = ScmModel().get_repos(all_repos)
296
278 return htmlfill.render(
297 return htmlfill.render(
279 render('admin/users/user_edit_my_account.html'),
298 render('admin/users/user_edit_my_account.html'),
280 defaults=errors.value,
299 defaults=errors.value,
@@ -283,11 +302,12 class SettingsController(BaseController)
283 encoding="UTF-8")
302 encoding="UTF-8")
284 except Exception:
303 except Exception:
285 log.error(traceback.format_exc())
304 log.error(traceback.format_exc())
286 h.flash(_('error occured during update of user %s') \
305 h.flash(_('error occurred during update of user %s') \
287 % form_result.get('username'), category='error')
306 % form_result.get('username'), category='error')
288
307
289 return redirect(url('my_account'))
308 return redirect(url('my_account'))
290
309
310 @NotAnonymous()
291 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
311 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
292 def create_repository(self):
312 def create_repository(self):
293 """GET /_admin/create_repository: Form to create a new item"""
313 """GET /_admin/create_repository: Form to create a new item"""
@@ -296,3 +316,24 class SettingsController(BaseController)
296
316
297 return render('admin/repos/repo_add_create_repository.html')
317 return render('admin/repos/repo_add_create_repository.html')
298
318
319 def get_hg_ui_settings(self):
320 ret = self.sa.query(RhodeCodeUi).all()
321
322 if not ret:
323 raise Exception('Could not get application ui settings !')
324 settings = {}
325 for each in ret:
326 k = each.ui_key
327 v = each.ui_value
328 if k == '/':
329 k = 'root_path'
330
331 if k.find('.') != -1:
332 k = k.replace('.', '_')
333
334 if each.ui_section == 'hooks':
335 v = each.ui_active
336
337 settings[each.ui_section + '_' + k] = v
338
339 return settings
@@ -1,8 +1,15
1 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 # encoding: utf-8
2 """
3 # users controller for pylons
3 rhodecode.controllers.admin.users
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 #
5
6 Users crud controller for pylons
7
8 :created_on: Apr 4, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
6 # This program is free software; you can redistribute it and/or
13 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
14 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
15 # as published by the Free Software Foundation; version 2
@@ -17,26 +24,24
17 # along with this program; if not, write to the Free Software
24 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
26 # MA 02110-1301, USA.
20 from rhodecode.lib.utils import action_logger
27
21 """
28 import logging
22 Created on April 4, 2010
29 import traceback
23 users controller for pylons
30 import formencode
24 @author: marcink
25 """
26
31
27 from formencode import htmlfill
32 from formencode import htmlfill
28 from pylons import request, session, tmpl_context as c, url
33 from pylons import request, session, tmpl_context as c, url
29 from pylons.controllers.util import abort, redirect
34 from pylons.controllers.util import abort, redirect
30 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36
37 from rhodecode.lib.exceptions import *
31 from rhodecode.lib import helpers as h
38 from rhodecode.lib import helpers as h
32 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
39 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
33 from rhodecode.lib.base import BaseController, render
40 from rhodecode.lib.base import BaseController, render
34 from rhodecode.model.db import User, UserLog
41
42 from rhodecode.model.db import User
35 from rhodecode.model.forms import UserForm
43 from rhodecode.model.forms import UserForm
36 from rhodecode.model.user_model import UserModel, DefaultUserException
44 from rhodecode.model.user import UserModel
37 import formencode
38 import logging
39 import traceback
40
45
41 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
42
47
@@ -100,7 +105,7 class UsersController(BaseController):
100 # method='put')
105 # method='put')
101 # url('user', id=ID)
106 # url('user', id=ID)
102 user_model = UserModel()
107 user_model = UserModel()
103 c.user = user_model.get_user(id)
108 c.user = user_model.get(id)
104
109
105 _form = UserForm(edit=True, old_data={'user_id':id,
110 _form = UserForm(edit=True, old_data={'user_id':id,
106 'email':c.user.email})()
111 'email':c.user.email})()
@@ -119,7 +124,7 class UsersController(BaseController):
119 encoding="UTF-8")
124 encoding="UTF-8")
120 except Exception:
125 except Exception:
121 log.error(traceback.format_exc())
126 log.error(traceback.format_exc())
122 h.flash(_('error occured during update of user %s') \
127 h.flash(_('error occurred during update of user %s') \
123 % form_result.get('username'), category='error')
128 % form_result.get('username'), category='error')
124
129
125 return redirect(url('users'))
130 return redirect(url('users'))
@@ -136,7 +141,7 class UsersController(BaseController):
136 try:
141 try:
137 user_model.delete(id)
142 user_model.delete(id)
138 h.flash(_('sucessfully deleted user'), category='success')
143 h.flash(_('sucessfully deleted user'), category='success')
139 except DefaultUserException, e:
144 except (UserOwnsReposException, DefaultUserException), e:
140 h.flash(str(e), category='warning')
145 h.flash(str(e), category='warning')
141 except Exception:
146 except Exception:
142 h.flash(_('An error occured during deletion of user'),
147 h.flash(_('An error occured during deletion of user'),
@@ -155,11 +160,10 class UsersController(BaseController):
155 if not c.user:
160 if not c.user:
156 return redirect(url('users'))
161 return redirect(url('users'))
157 if c.user.username == 'default':
162 if c.user.username == 'default':
158 h.flash(_("You can't edit this user since it's"
163 h.flash(_("You can't edit this user"), category='warning')
159 " crucial for entire application"), category='warning')
160 return redirect(url('users'))
164 return redirect(url('users'))
161
165
162 defaults = c.user.__dict__
166 defaults = c.user.get_dict()
163 return htmlfill.render(
167 return htmlfill.render(
164 render('admin/users/user_edit.html'),
168 render('admin/users/user_edit.html'),
165 defaults=defaults,
169 defaults=defaults,
@@ -26,7 +26,7 from pylons import tmpl_context as c
26 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
26 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27 from rhodecode.lib.base import BaseController, render
27 from rhodecode.lib.base import BaseController, render
28 from rhodecode.lib.utils import OrderedDict
28 from rhodecode.lib.utils import OrderedDict
29 from rhodecode.model.hg_model import HgModel
29 from rhodecode.model.scm import ScmModel
30 import logging
30 import logging
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
@@ -38,7 +38,7 class BranchesController(BaseController)
38 super(BranchesController, self).__before__()
38 super(BranchesController, self).__before__()
39
39
40 def index(self):
40 def index(self):
41 hg_model = HgModel()
41 hg_model = ScmModel()
42 c.repo_info = hg_model.get_repo(c.repo_name)
42 c.repo_info = hg_model.get_repo(c.repo_name)
43 c.repo_branches = OrderedDict()
43 c.repo_branches = OrderedDict()
44 for name, hash_ in c.repo_info.branches.items():
44 for name, hash_ in c.repo_info.branches.items():
@@ -32,7 +32,7 from mercurial.graphmod import colored,
32 from pylons import request, session, tmpl_context as c
32 from pylons import request, session, tmpl_context as c
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.base import BaseController, render
34 from rhodecode.lib.base import BaseController, render
35 from rhodecode.model.hg_model import HgModel
35 from rhodecode.model.scm import ScmModel
36 from webhelpers.paginate import Page
36 from webhelpers.paginate import Page
37 import logging
37 import logging
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
@@ -60,7 +60,7 class ChangelogController(BaseController
60 else:
60 else:
61 c.size = int(session.get('changelog_size', default))
61 c.size = int(session.get('changelog_size', default))
62
62
63 changesets = HgModel().get_repo(c.repo_name)
63 changesets = ScmModel().get_repo(c.repo_name)
64
64
65 p = int(request.params.get('page', 1))
65 p = int(request.params.get('page', 1))
66 c.total_cs = len(changesets)
66 c.total_cs = len(changesets)
@@ -74,10 +74,14 class ChangelogController(BaseController
74
74
75 def _graph(self, repo, size, p):
75 def _graph(self, repo, size, p):
76 revcount = size
76 revcount = size
77 if not repo.revisions:return json.dumps([]), 0
77 if not repo.revisions or repo.alias == 'git':
78 c.jsdata = json.dumps([])
79 return
78
80
79 max_rev = repo.revisions[-1]
81 max_rev = repo.revisions[-1]
82
80 offset = 1 if p == 1 else ((p - 1) * revcount + 1)
83 offset = 1 if p == 1 else ((p - 1) * revcount + 1)
84
81 rev_start = repo.revisions[(-1 * offset)]
85 rev_start = repo.revisions[(-1 * offset)]
82
86
83 revcount = min(max_rev, revcount)
87 revcount = min(max_rev, revcount)
@@ -1,7 +1,15
1 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 # encoding: utf-8
2 """
3 # changeset controller for pylons
3 rhodecode.controllers.changeset
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 changeset controller for pylons
7
8 :created_on: Apr 25, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
5 # This program is free software; you can redistribute it and/or
13 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
14 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; version 2
15 # as published by the Free Software Foundation; version 2
@@ -16,23 +24,22
16 # along with this program; if not, write to the Free Software
24 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18 # MA 02110-1301, USA.
26 # MA 02110-1301, USA.
19 from rhodecode.lib.utils import EmptyChangeset
27 import logging
20 """
28 import traceback
21 Created on April 25, 2010
29
22 changeset controller for pylons
23 @author: marcink
24 """
25 from pylons import tmpl_context as c, url, request, response
30 from pylons import tmpl_context as c, url, request, response
26 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
27 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33
34 import rhodecode.lib.helpers as h
28 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
29 from rhodecode.lib.base import BaseController, render
36 from rhodecode.lib.base import BaseController, render
30 from rhodecode.model.hg_model import HgModel
37 from rhodecode.lib.utils import EmptyChangeset
38 from rhodecode.model.scm import ScmModel
39
31 from vcs.exceptions import RepositoryError, ChangesetError
40 from vcs.exceptions import RepositoryError, ChangesetError
32 from vcs.nodes import FileNode
41 from vcs.nodes import FileNode
33 from vcs.utils import diffs as differ
42 from vcs.utils import diffs as differ
34 import logging
35 import traceback
36
43
37 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
38
45
@@ -45,8 +52,7 class ChangesetController(BaseController
45 super(ChangesetController, self).__before__()
52 super(ChangesetController, self).__before__()
46
53
47 def index(self, revision):
54 def index(self, revision):
48 hg_model = HgModel()
55 hg_model = ScmModel()
49 cut_off_limit = 1024 * 250
50
56
51 def wrap_to_table(str):
57 def wrap_to_table(str):
52
58
@@ -59,9 +65,10 class ChangesetController(BaseController
59
65
60 try:
66 try:
61 c.changeset = hg_model.get_repo(c.repo_name).get_changeset(revision)
67 c.changeset = hg_model.get_repo(c.repo_name).get_changeset(revision)
62 except RepositoryError:
68 except RepositoryError, e:
63 log.error(traceback.format_exc())
69 log.error(traceback.format_exc())
64 return redirect(url('hg_home'))
70 h.flash(str(e), category='warning')
71 return redirect(url('home'))
65 else:
72 else:
66 try:
73 try:
67 c.changeset_old = c.changeset.parents[0]
74 c.changeset_old = c.changeset.parents[0]
@@ -80,7 +87,7 class ChangesetController(BaseController
80 diff = wrap_to_table(_('binary file'))
87 diff = wrap_to_table(_('binary file'))
81 else:
88 else:
82 c.sum_added += node.size
89 c.sum_added += node.size
83 if c.sum_added < cut_off_limit:
90 if c.sum_added < self.cut_off_limit:
84 f_udiff = differ.get_udiff(filenode_old, node)
91 f_udiff = differ.get_udiff(filenode_old, node)
85 diff = differ.DiffProcessor(f_udiff).as_html()
92 diff = differ.DiffProcessor(f_udiff).as_html()
86
93
@@ -89,7 +96,7 class ChangesetController(BaseController
89 ' off, see raw changeset instead'))
96 ' off, see raw changeset instead'))
90
97
91 cs1 = None
98 cs1 = None
92 cs2 = node.last_changeset.short_id
99 cs2 = node.last_changeset.raw_id
93 c.changes.append(('added', node, diff, cs1, cs2))
100 c.changes.append(('added', node, diff, cs1, cs2))
94
101
95 #===================================================================
102 #===================================================================
@@ -106,7 +113,7 class ChangesetController(BaseController
106 diff = wrap_to_table(_('binary file'))
113 diff = wrap_to_table(_('binary file'))
107 else:
114 else:
108
115
109 if c.sum_removed < cut_off_limit:
116 if c.sum_removed < self.cut_off_limit:
110 f_udiff = differ.get_udiff(filenode_old, node)
117 f_udiff = differ.get_udiff(filenode_old, node)
111 diff = differ.DiffProcessor(f_udiff).as_html()
118 diff = differ.DiffProcessor(f_udiff).as_html()
112 if diff:
119 if diff:
@@ -116,8 +123,8 class ChangesetController(BaseController
116 ' off, see raw changeset instead'))
123 ' off, see raw changeset instead'))
117
124
118
125
119 cs1 = filenode_old.last_changeset.short_id
126 cs1 = filenode_old.last_changeset.raw_id
120 cs2 = node.last_changeset.short_id
127 cs2 = node.last_changeset.raw_id
121 c.changes.append(('changed', node, diff, cs1, cs2))
128 c.changes.append(('changed', node, diff, cs1, cs2))
122
129
123 #===================================================================
130 #===================================================================
@@ -130,13 +137,15 class ChangesetController(BaseController
130
137
131 def raw_changeset(self, revision):
138 def raw_changeset(self, revision):
132
139
133 hg_model = HgModel()
140 hg_model = ScmModel()
134 method = request.GET.get('diff', 'show')
141 method = request.GET.get('diff', 'show')
135 try:
142 try:
136 c.changeset = hg_model.get_repo(c.repo_name).get_changeset(revision)
143 r = hg_model.get_repo(c.repo_name)
144 c.scm_type = r.alias
145 c.changeset = r.get_changeset(revision)
137 except RepositoryError:
146 except RepositoryError:
138 log.error(traceback.format_exc())
147 log.error(traceback.format_exc())
139 return redirect(url('hg_home'))
148 return redirect(url('home'))
140 else:
149 else:
141 try:
150 try:
142 c.changeset_old = c.changeset.parents[0]
151 c.changeset_old = c.changeset.parents[0]
@@ -147,13 +156,13 class ChangesetController(BaseController
147 for node in c.changeset.added:
156 for node in c.changeset.added:
148 filenode_old = FileNode(node.path, '')
157 filenode_old = FileNode(node.path, '')
149 if filenode_old.is_binary or node.is_binary:
158 if filenode_old.is_binary or node.is_binary:
150 diff = _('binary file')
159 diff = _('binary file') + '\n'
151 else:
160 else:
152 f_udiff = differ.get_udiff(filenode_old, node)
161 f_udiff = differ.get_udiff(filenode_old, node)
153 diff = differ.DiffProcessor(f_udiff).raw_diff()
162 diff = differ.DiffProcessor(f_udiff).raw_diff()
154
163
155 cs1 = None
164 cs1 = None
156 cs2 = node.last_changeset.short_id
165 cs2 = node.last_changeset.raw_id
157 c.changes.append(('added', node, diff, cs1, cs2))
166 c.changes.append(('added', node, diff, cs1, cs2))
158
167
159 for node in c.changeset.changed:
168 for node in c.changeset.changed:
@@ -164,13 +173,15 class ChangesetController(BaseController
164 f_udiff = differ.get_udiff(filenode_old, node)
173 f_udiff = differ.get_udiff(filenode_old, node)
165 diff = differ.DiffProcessor(f_udiff).raw_diff()
174 diff = differ.DiffProcessor(f_udiff).raw_diff()
166
175
167 cs1 = filenode_old.last_changeset.short_id
176 cs1 = filenode_old.last_changeset.raw_id
168 cs2 = node.last_changeset.short_id
177 cs2 = node.last_changeset.raw_id
169 c.changes.append(('changed', node, diff, cs1, cs2))
178 c.changes.append(('changed', node, diff, cs1, cs2))
170
179
171 response.content_type = 'text/plain'
180 response.content_type = 'text/plain'
181
172 if method == 'download':
182 if method == 'download':
173 response.content_disposition = 'attachment; filename=%s.patch' % revision
183 response.content_disposition = 'attachment; filename=%s.patch' % revision
184
174 parent = True if len(c.changeset.parents) > 0 else False
185 parent = True if len(c.changeset.parents) > 0 else False
175 c.parent_tmpl = 'Parent %s' % c.changeset.parents[0].raw_id if parent else ''
186 c.parent_tmpl = 'Parent %s' % c.changeset.parents[0].raw_id if parent else ''
176
187
@@ -1,27 +1,52
1 import logging
1 # -*- coding: utf-8 -*-
2 """
3 package.rhodecode.controllers.error
4 ~~~~~~~~~~~~~~
5
6 RhodeCode error controller
7
8 :created_on: Dec 8, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 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
14 # modify it under the terms of the GNU General Public License
15 # as published by the Free Software Foundation; version 2
16 # of the License or (at your opinion) any later version of the license.
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, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 # MA 02110-1301, USA.
27 import os
2 import cgi
28 import cgi
3 import os
29 import logging
4 import paste.fileapp
30 import paste.fileapp
5 from pylons import tmpl_context as c, app_globals as g, request, config
31
6 from pylons.controllers.util import forward
32 from pylons import tmpl_context as c, request
7 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34 from pylons.middleware import media_path
35
8 from rhodecode.lib.base import BaseController, render
36 from rhodecode.lib.base import BaseController, render
9 from pylons.middleware import media_path
37
10 from rhodecode.lib.utils import check_repo
11 import rhodecode.lib.helpers as h
12 from rhodecode import __version__
13 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
14
39
15 class ErrorController(BaseController):
40 class ErrorController(BaseController):
16 """
41 """Generates error documents as and when they are required.
17 Generates error documents as and when they are required.
18
42
19 The ErrorDocuments middleware forwards to ErrorController when error
43 The ErrorDocuments middleware forwards to ErrorController when error
20 related status codes are returned from the application.
44 related status codes are returned from the application.
21
45
22 This behaviour can be altered by changing the parameters to the
46 This behavior can be altered by changing the parameters to the
23 ErrorDocuments middleware in your config/middleware.py file.
47 ErrorDocuments middleware in your config/middleware.py file.
24 """
48 """
49
25 def __before__(self):
50 def __before__(self):
26 pass#disable all base actions since we don't need them here
51 pass#disable all base actions since we don't need them here
27
52
@@ -74,7 +99,7 class ErrorController(BaseController):
74 if code == 400:
99 if code == 400:
75 return _('The request could not be understood by the server due to malformed syntax.')
100 return _('The request could not be understood by the server due to malformed syntax.')
76 if code == 401:
101 if code == 401:
77 return _('Unathorized access to resource')
102 return _('Unauthorized access to resource')
78 if code == 403:
103 if code == 403:
79 return _("You don't have permission to view this page")
104 return _("You don't have permission to view this page")
80 if code == 404:
105 if code == 404:
@@ -24,7 +24,7 feed controller for pylons
24 """
24 """
25 from pylons import tmpl_context as c, url, response
25 from pylons import tmpl_context as c, url, response
26 from rhodecode.lib.base import BaseController, render
26 from rhodecode.lib.base import BaseController, render
27 from rhodecode.model.hg_model import HgModel
27 from rhodecode.model.scm import ScmModel
28 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
28 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
29 import logging
29 import logging
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
@@ -49,12 +49,12 class FeedController(BaseController):
49 language=self.language,
49 language=self.language,
50 ttl=self.ttl)
50 ttl=self.ttl)
51
51
52 changesets = HgModel().get_repo(repo_name)
52 changesets = ScmModel().get_repo(repo_name)
53
53
54 for cs in changesets[:self.feed_nr]:
54 for cs in changesets[:self.feed_nr]:
55 feed.add_item(title=cs.message,
55 feed.add_item(title=cs.message,
56 link=url('changeset_home', repo_name=repo_name,
56 link=url('changeset_home', repo_name=repo_name,
57 revision=cs.short_id, qualified=True),
57 revision=cs.raw_id, qualified=True),
58 description=str(cs.date))
58 description=str(cs.date))
59
59
60 response.content_type = feed.mime_type
60 response.content_type = feed.mime_type
@@ -69,11 +69,11 class FeedController(BaseController):
69 language=self.language,
69 language=self.language,
70 ttl=self.ttl)
70 ttl=self.ttl)
71
71
72 changesets = HgModel().get_repo(repo_name)
72 changesets = ScmModel().get_repo(repo_name)
73 for cs in changesets[:self.feed_nr]:
73 for cs in changesets[:self.feed_nr]:
74 feed.add_item(title=cs.message,
74 feed.add_item(title=cs.message,
75 link=url('changeset_home', repo_name=repo_name,
75 link=url('changeset_home', repo_name=repo_name,
76 revision=cs.short_id, qualified=True),
76 revision=cs.raw_id, qualified=True),
77 description=str(cs.date))
77 description=str(cs.date))
78
78
79 response.content_type = feed.mime_type
79 response.content_type = feed.mime_type
@@ -1,8 +1,15
1 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 # encoding: utf-8
2 """
3 # files controller for pylons
3 rhodecode.controllers.files
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Files controller for RhodeCode
7
8 :created_on: Apr 21, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
6 # This program is free software; you can redistribute it and/or
13 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
14 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
15 # as published by the Free Software Foundation; version 2
@@ -17,25 +24,24
17 # along with this program; if not, write to the Free Software
24 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
26 # MA 02110-1301, USA.
20 """
27 import tempfile
21 Created on April 21, 2010
28 import logging
22 files controller for pylons
29 import rhodecode.lib.helpers as h
23 @author: marcink
30
24 """
25 from mercurial import archival
31 from mercurial import archival
32
26 from pylons import request, response, session, tmpl_context as c, url
33 from pylons import request, response, session, tmpl_context as c, url
27 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
28 from pylons.controllers.util import redirect
35 from pylons.controllers.util import redirect
36
29 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
30 from rhodecode.lib.base import BaseController, render
38 from rhodecode.lib.base import BaseController, render
31 from rhodecode.lib.utils import EmptyChangeset
39 from rhodecode.lib.utils import EmptyChangeset
32 from rhodecode.model.hg_model import HgModel
40 from rhodecode.model.scm import ScmModel
41
33 from vcs.exceptions import RepositoryError, ChangesetError
42 from vcs.exceptions import RepositoryError, ChangesetError
34 from vcs.nodes import FileNode
43 from vcs.nodes import FileNode
35 from vcs.utils import diffs as differ
44 from vcs.utils import diffs as differ
36 import logging
37 import rhodecode.lib.helpers as h
38 import tempfile
39
45
40 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
41
47
@@ -46,11 +52,11 class FilesController(BaseController):
46 'repository.admin')
52 'repository.admin')
47 def __before__(self):
53 def __before__(self):
48 super(FilesController, self).__before__()
54 super(FilesController, self).__before__()
49 c.file_size_limit = 250 * 1024 #limit of file size to display
55 c.cut_off_limit = self.cut_off_limit
50
56
51 def index(self, repo_name, revision, f_path):
57 def index(self, repo_name, revision, f_path):
52 hg_model = HgModel()
58 hg_model = ScmModel()
53 c.repo = repo = hg_model.get_repo(c.repo_name)
59 c.repo = hg_model.get_repo(c.repo_name)
54 revision = request.POST.get('at_rev', None) or revision
60 revision = request.POST.get('at_rev', None) or revision
55
61
56 def get_next_rev(cur):
62 def get_next_rev(cur):
@@ -68,29 +74,33 class FilesController(BaseController):
68
74
69
75
70 try:
76 try:
71 cur_rev = repo.get_changeset(revision).revision
77 c.changeset = c.repo.get_changeset(revision)
72 prev_rev = repo.get_changeset(get_prev_rev(cur_rev)).short_id
78 cur_rev = c.changeset.revision
73 next_rev = repo.get_changeset(get_next_rev(cur_rev)).short_id
79 prev_rev = c.repo.get_changeset(get_prev_rev(cur_rev)).raw_id
80 next_rev = c.repo.get_changeset(get_next_rev(cur_rev)).raw_id
74
81
75 c.url_prev = url('files_home', repo_name=c.repo_name,
82 c.url_prev = url('files_home', repo_name=c.repo_name,
76 revision=prev_rev, f_path=f_path)
83 revision=prev_rev, f_path=f_path)
77 c.url_next = url('files_home', repo_name=c.repo_name,
84 c.url_next = url('files_home', repo_name=c.repo_name,
78 revision=next_rev, f_path=f_path)
85 revision=next_rev, f_path=f_path)
79
86
80 c.changeset = repo.get_changeset(revision)
87 try:
81
82 c.cur_rev = c.changeset.short_id
83 c.rev_nr = c.changeset.revision
84 c.files_list = c.changeset.get_node(f_path)
88 c.files_list = c.changeset.get_node(f_path)
85 c.file_history = self._get_history(repo, c.files_list, f_path)
89 c.file_history = self._get_history(c.repo, c.files_list, f_path)
90 except RepositoryError, e:
91 h.flash(str(e), category='warning')
92 redirect(h.url('files_home', repo_name=repo_name, revision=revision))
86
93
87 except (RepositoryError, ChangesetError):
94 except RepositoryError, e:
88 c.files_list = None
95 h.flash(str(e), category='warning')
96 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
97
98
89
99
90 return render('files/files.html')
100 return render('files/files.html')
91
101
92 def rawfile(self, repo_name, revision, f_path):
102 def rawfile(self, repo_name, revision, f_path):
93 hg_model = HgModel()
103 hg_model = ScmModel()
94 c.repo = hg_model.get_repo(c.repo_name)
104 c.repo = hg_model.get_repo(c.repo_name)
95 file_node = c.repo.get_changeset(revision).get_node(f_path)
105 file_node = c.repo.get_changeset(revision).get_node(f_path)
96 response.content_type = file_node.mimetype
106 response.content_type = file_node.mimetype
@@ -99,7 +109,7 class FilesController(BaseController):
99 return file_node.content
109 return file_node.content
100
110
101 def raw(self, repo_name, revision, f_path):
111 def raw(self, repo_name, revision, f_path):
102 hg_model = HgModel()
112 hg_model = ScmModel()
103 c.repo = hg_model.get_repo(c.repo_name)
113 c.repo = hg_model.get_repo(c.repo_name)
104 file_node = c.repo.get_changeset(revision).get_node(f_path)
114 file_node = c.repo.get_changeset(revision).get_node(f_path)
105 response.content_type = 'text/plain'
115 response.content_type = 'text/plain'
@@ -107,13 +117,18 class FilesController(BaseController):
107 return file_node.content
117 return file_node.content
108
118
109 def annotate(self, repo_name, revision, f_path):
119 def annotate(self, repo_name, revision, f_path):
110 hg_model = HgModel()
120 hg_model = ScmModel()
111 c.repo = hg_model.get_repo(c.repo_name)
121 c.repo = hg_model.get_repo(c.repo_name)
112 cs = c.repo.get_changeset(revision)
122
113 c.file = cs.get_node(f_path)
123 try:
114 c.file_msg = cs.get_file_message(f_path)
124 c.cs = c.repo.get_changeset(revision)
115 c.cur_rev = cs.short_id
125 c.file = c.cs.get_node(f_path)
116 c.rev_nr = cs.revision
126 except RepositoryError, e:
127 h.flash(str(e), category='warning')
128 redirect(h.url('files_home', repo_name=repo_name, revision=revision))
129
130 c.file_history = self._get_history(c.repo, c.file, f_path)
131
117 c.f_path = f_path
132 c.f_path = f_path
118
133
119 return render('files/files_annotate.html')
134 return render('files/files_annotate.html')
@@ -137,7 +152,7 class FilesController(BaseController):
137 yield data
152 yield data
138
153
139 archive = tempfile.TemporaryFile()
154 archive = tempfile.TemporaryFile()
140 repo = HgModel().get_repo(repo_name).repo
155 repo = ScmModel().get_repo(repo_name).repo
141 fname = '%s-%s%s' % (repo_name, revision, fileformat)
156 fname = '%s-%s%s' % (repo_name, revision, fileformat)
142 archival.archive(repo, archive, revision, archive_specs[fileformat][1],
157 archival.archive(repo, archive, revision, archive_specs[fileformat][1],
143 prefix='%s-%s' % (repo_name, revision))
158 prefix='%s-%s' % (repo_name, revision))
@@ -147,7 +162,7 class FilesController(BaseController):
147 return read_in_chunks(archive)
162 return read_in_chunks(archive)
148
163
149 def diff(self, repo_name, f_path):
164 def diff(self, repo_name, f_path):
150 hg_model = HgModel()
165 hg_model = ScmModel()
151 diff1 = request.GET.get('diff1')
166 diff1 = request.GET.get('diff1')
152 diff2 = request.GET.get('diff2')
167 diff2 = request.GET.get('diff2')
153 c.action = request.GET.get('diff')
168 c.action = request.GET.get('diff')
@@ -173,9 +188,6 class FilesController(BaseController):
173 return redirect(url('files_home',
188 return redirect(url('files_home',
174 repo_name=c.repo_name, f_path=f_path))
189 repo_name=c.repo_name, f_path=f_path))
175
190
176 c.diff1 = 'r%s:%s' % (c.changeset_1.revision, c.changeset_1.short_id)
177 c.diff2 = 'r%s:%s' % (c.changeset_2.revision, c.changeset_2.short_id)
178
179 f_udiff = differ.get_udiff(node1, node2)
191 f_udiff = differ.get_udiff(node1, node2)
180 diff = differ.DiffProcessor(f_udiff)
192 diff = differ.DiffProcessor(f_udiff)
181
193
@@ -187,15 +199,17 class FilesController(BaseController):
187 return diff.raw_diff()
199 return diff.raw_diff()
188
200
189 elif c.action == 'raw':
201 elif c.action == 'raw':
190 c.cur_diff = '<pre class="raw">%s</pre>' % h.escape(diff.raw_diff())
202 response.content_type = 'text/plain'
203 return diff.raw_diff()
204
191 elif c.action == 'diff':
205 elif c.action == 'diff':
192 if node1.size > c.file_size_limit or node2.size > c.file_size_limit:
206 if node1.size > self.cut_off_limit or node2.size > self.cut_off_limit:
193 c.cur_diff = _('Diff is to big to display')
207 c.cur_diff = _('Diff is to big to display')
194 else:
208 else:
195 c.cur_diff = diff.as_html()
209 c.cur_diff = diff.as_html()
196 else:
210 else:
197 #default option
211 #default option
198 if node1.size > c.file_size_limit or node2.size > c.file_size_limit:
212 if node1.size > self.cut_off_limit or node2.size > self.cut_off_limit:
199 c.cur_diff = _('Diff is to big to display')
213 c.cur_diff = _('Diff is to big to display')
200 else:
214 else:
201 c.cur_diff = diff.as_html()
215 c.cur_diff = diff.as_html()
@@ -209,7 +223,31 class FilesController(BaseController):
209 return []
223 return []
210 changesets = node.history
224 changesets = node.history
211 hist_l = []
225 hist_l = []
226
227 changesets_group = ([], _("Changesets"))
228 branches_group = ([], _("Branches"))
229 tags_group = ([], _("Tags"))
230
212 for chs in changesets:
231 for chs in changesets:
213 n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
232 n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
214 hist_l.append((chs.short_id, n_desc,))
233 changesets_group[0].append((chs.raw_id, n_desc,))
234
235 hist_l.append(changesets_group)
236
237 for name, chs in c.repository_branches.items():
238 #chs = chs.split(':')[-1]
239 branches_group[0].append((chs, name),)
240 hist_l.append(branches_group)
241
242 for name, chs in c.repository_tags.items():
243 #chs = chs.split(':')[-1]
244 tags_group[0].append((chs, name),)
245 hist_l.append(tags_group)
246
215 return hist_l
247 return hist_l
248
249 # [
250 # ([("u1", "User1"), ("u2", "User2")], "Users"),
251 # ([("g1", "Group1"), ("g2", "Group2")], "Groups")
252 # ]
253
@@ -26,15 +26,15 from operator import itemgetter
26 from pylons import tmpl_context as c, request
26 from pylons import tmpl_context as c, request
27 from rhodecode.lib.auth import LoginRequired
27 from rhodecode.lib.auth import LoginRequired
28 from rhodecode.lib.base import BaseController, render
28 from rhodecode.lib.base import BaseController, render
29 from rhodecode.model.hg_model import HgModel
29 from rhodecode.model.scm import ScmModel
30 import logging
30 import logging
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33 class HgController(BaseController):
33 class HomeController(BaseController):
34
34
35 @LoginRequired()
35 @LoginRequired()
36 def __before__(self):
36 def __before__(self):
37 super(HgController, self).__before__()
37 super(HomeController, self).__before__()
38
38
39 def index(self):
39 def index(self):
40 sortables = ['name', 'description', 'last_change', 'tip', 'contact']
40 sortables = ['name', 'description', 'last_change', 'tip', 'contact']
@@ -47,7 +47,7 class HgController(BaseController):
47 else:
47 else:
48 c.sort_by = current_sort
48 c.sort_by = current_sort
49 c.sort_slug = current_sort_slug
49 c.sort_slug = current_sort_slug
50 cached_repo_list = HgModel().get_repos()
50 cached_repo_list = ScmModel().get_repos()
51
51
52 sort_key = current_sort_slug + '_sort'
52 sort_key = current_sort_slug + '_sort'
53 if c.sort_by.startswith('-'):
53 if c.sort_by.startswith('-'):
@@ -31,7 +31,7 from rhodecode.lib.base import BaseContr
31 import rhodecode.lib.helpers as h
31 import rhodecode.lib.helpers as h
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
33 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
34 from rhodecode.model.user_model import UserModel
34 from rhodecode.model.user import UserModel
35 import formencode
35 import formencode
36 import logging
36 import logging
37
37
@@ -46,8 +46,10 class LoginController(BaseController):
46 #redirect if already logged in
46 #redirect if already logged in
47 c.came_from = request.GET.get('came_from', None)
47 c.came_from = request.GET.get('came_from', None)
48
48
49 if c.rhodecode_user.is_authenticated:
49 if c.rhodecode_user.is_authenticated \
50 return redirect(url('hg_home'))
50 and c.rhodecode_user.username != 'default':
51
52 return redirect(url('home'))
51
53
52 if request.POST:
54 if request.POST:
53 #import Login Form validator class
55 #import Login Form validator class
@@ -55,7 +57,7 class LoginController(BaseController):
55 try:
57 try:
56 c.form_result = login_form.to_python(dict(request.POST))
58 c.form_result = login_form.to_python(dict(request.POST))
57 username = c.form_result['username']
59 username = c.form_result['username']
58 user = UserModel().get_user_by_name(username)
60 user = UserModel().get_by_username(username, case_insensitive=True)
59 auth_user = AuthUser()
61 auth_user = AuthUser()
60 auth_user.username = user.username
62 auth_user.username = user.username
61 auth_user.is_authenticated = True
63 auth_user.is_authenticated = True
@@ -72,7 +74,7 class LoginController(BaseController):
72 if c.came_from:
74 if c.came_from:
73 return redirect(c.came_from)
75 return redirect(c.came_from)
74 else:
76 else:
75 return redirect(url('hg_home'))
77 return redirect(url('home'))
76
78
77 except formencode.Invalid, errors:
79 except formencode.Invalid, errors:
78 return htmlfill.render(
80 return htmlfill.render(
@@ -89,7 +91,7 class LoginController(BaseController):
89 def register(self):
91 def register(self):
90 user_model = UserModel()
92 user_model = UserModel()
91 c.auto_active = False
93 c.auto_active = False
92 for perm in user_model.get_default().user_perms:
94 for perm in user_model.get_by_username('default', cache=False).user_perms:
93 if perm.permission.permission_name == 'hg.register.auto_activate':
95 if perm.permission.permission_name == 'hg.register.auto_activate':
94 c.auto_active = True
96 c.auto_active = True
95 break
97 break
@@ -141,4 +143,4 class LoginController(BaseController):
141 session['rhodecode_user'] = AuthUser()
143 session['rhodecode_user'] = AuthUser()
142 session.save()
144 session.save()
143 log.info('Logging out and setting user as Empty')
145 log.info('Logging out and setting user as Empty')
144 redirect(url('hg_home'))
146 redirect(url('home'))
@@ -22,11 +22,11 Created on Aug 7, 2010
22 search controller for pylons
22 search controller for pylons
23 @author: marcink
23 @author: marcink
24 """
24 """
25 from pylons import request, response, session, tmpl_context as c, url
25 from pylons import request, response, config, session, tmpl_context as c, url
26 from pylons.controllers.util import abort, redirect
26 from pylons.controllers.util import abort, redirect
27 from rhodecode.lib.auth import LoginRequired
27 from rhodecode.lib.auth import LoginRequired
28 from rhodecode.lib.base import BaseController, render
28 from rhodecode.lib.base import BaseController, render
29 from rhodecode.lib.indexers import IDX_LOCATION, SCHEMA, IDX_NAME, ResultWrapper
29 from rhodecode.lib.indexers import SCHEMA, IDX_NAME, ResultWrapper
30 from webhelpers.paginate import Page
30 from webhelpers.paginate import Page
31 from webhelpers.util import update_params
31 from webhelpers.util import update_params
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
@@ -64,7 +64,8 class SearchController(BaseController):
64 p = int(request.params.get('page', 1))
64 p = int(request.params.get('page', 1))
65 highlight_items = set()
65 highlight_items = set()
66 try:
66 try:
67 idx = open_dir(IDX_LOCATION, indexname=IDX_NAME)
67 idx = open_dir(config['app_conf']['index_dir']
68 , indexname=IDX_NAME)
68 searcher = idx.searcher()
69 searcher = idx.searcher()
69
70
70 qp = QueryParser(search_type, schema=SCHEMA)
71 qp = QueryParser(search_type, schema=SCHEMA)
@@ -30,7 +30,7 from rhodecode.lib.auth import LoginRequ
30 from rhodecode.lib.base import BaseController, render
30 from rhodecode.lib.base import BaseController, render
31 from rhodecode.lib.utils import invalidate_cache, action_logger
31 from rhodecode.lib.utils import invalidate_cache, action_logger
32 from rhodecode.model.forms import RepoSettingsForm, RepoForkForm
32 from rhodecode.model.forms import RepoSettingsForm, RepoForkForm
33 from rhodecode.model.repo_model import RepoModel
33 from rhodecode.model.repo import RepoModel
34 import formencode
34 import formencode
35 import logging
35 import logging
36 import rhodecode.lib.helpers as h
36 import rhodecode.lib.helpers as h
@@ -47,7 +47,7 class SettingsController(BaseController)
47
47
48 def index(self, repo_name):
48 def index(self, repo_name):
49 repo_model = RepoModel()
49 repo_model = RepoModel()
50 c.repo_info = repo = repo_model.get(repo_name)
50 c.repo_info = repo = repo_model.get_by_repo_name(repo_name)
51 if not repo:
51 if not repo:
52 h.flash(_('%s repository is not mapped to db perhaps'
52 h.flash(_('%s repository is not mapped to db perhaps'
53 ' it was created or renamed from the filesystem'
53 ' it was created or renamed from the filesystem'
@@ -55,8 +55,8 class SettingsController(BaseController)
55 ' in order to rescan repositories') % repo_name,
55 ' in order to rescan repositories') % repo_name,
56 category='error')
56 category='error')
57
57
58 return redirect(url('hg_home'))
58 return redirect(url('home'))
59 defaults = c.repo_info.__dict__
59 defaults = c.repo_info.get_dict()
60 defaults.update({'user':c.repo_info.user.username})
60 defaults.update({'user':c.repo_info.user.username})
61 c.users_array = repo_model.get_users_js()
61 c.users_array = repo_model.get_users_js()
62
62
@@ -78,12 +78,14 class SettingsController(BaseController)
78 try:
78 try:
79 form_result = _form.to_python(dict(request.POST))
79 form_result = _form.to_python(dict(request.POST))
80 repo_model.update(repo_name, form_result)
80 repo_model.update(repo_name, form_result)
81 invalidate_cache('cached_repo_list')
81 invalidate_cache('get_repo_cached_%s' % repo_name)
82 h.flash(_('Repository %s updated successfully' % repo_name),
82 h.flash(_('Repository %s updated successfully' % repo_name),
83 category='success')
83 category='success')
84 changed_name = form_result['repo_name']
84 changed_name = form_result['repo_name']
85 action_logger(self.rhodecode_user, 'user_updated_repo',
86 changed_name, '', self.sa)
85 except formencode.Invalid, errors:
87 except formencode.Invalid, errors:
86 c.repo_info = repo_model.get(repo_name)
88 c.repo_info = repo_model.get_by_repo_name(repo_name)
87 c.users_array = repo_model.get_users_js()
89 c.users_array = repo_model.get_users_js()
88 errors.value.update({'user':c.repo_info.user.username})
90 errors.value.update({'user':c.repo_info.user.username})
89 return htmlfill.render(
91 return htmlfill.render(
@@ -94,7 +96,7 class SettingsController(BaseController)
94 encoding="UTF-8")
96 encoding="UTF-8")
95 except Exception:
97 except Exception:
96 log.error(traceback.format_exc())
98 log.error(traceback.format_exc())
97 h.flash(_('error occured during update of repository %s') \
99 h.flash(_('error occurred during update of repository %s') \
98 % repo_name, category='error')
100 % repo_name, category='error')
99
101
100 return redirect(url('repo_settings_home', repo_name=changed_name))
102 return redirect(url('repo_settings_home', repo_name=changed_name))
@@ -111,7 +113,7 class SettingsController(BaseController)
111 # url('repo_settings_delete', repo_name=ID)
113 # url('repo_settings_delete', repo_name=ID)
112
114
113 repo_model = RepoModel()
115 repo_model = RepoModel()
114 repo = repo_model.get(repo_name)
116 repo = repo_model.get_by_repo_name(repo_name)
115 if not repo:
117 if not repo:
116 h.flash(_('%s repository is not mapped to db perhaps'
118 h.flash(_('%s repository is not mapped to db perhaps'
117 ' it was moved or renamed from the filesystem'
119 ' it was moved or renamed from the filesystem'
@@ -119,22 +121,22 class SettingsController(BaseController)
119 ' in order to rescan repositories') % repo_name,
121 ' in order to rescan repositories') % repo_name,
120 category='error')
122 category='error')
121
123
122 return redirect(url('hg_home'))
124 return redirect(url('home'))
123 try:
125 try:
124 action_logger(self.rhodecode_user, 'user_deleted_repo',
126 action_logger(self.rhodecode_user, 'user_deleted_repo',
125 repo_name, '', self.sa)
127 repo_name, '', self.sa)
126 repo_model.delete(repo)
128 repo_model.delete(repo)
127 invalidate_cache('cached_repo_list')
129 invalidate_cache('get_repo_cached_%s' % repo_name)
128 h.flash(_('deleted repository %s') % repo_name, category='success')
130 h.flash(_('deleted repository %s') % repo_name, category='success')
129 except Exception:
131 except Exception:
130 h.flash(_('An error occurred during deletion of %s') % repo_name,
132 h.flash(_('An error occurred during deletion of %s') % repo_name,
131 category='error')
133 category='error')
132
134
133 return redirect(url('hg_home'))
135 return redirect(url('home'))
134
136
135 def fork(self, repo_name):
137 def fork(self, repo_name):
136 repo_model = RepoModel()
138 repo_model = RepoModel()
137 c.repo_info = repo = repo_model.get(repo_name)
139 c.repo_info = repo = repo_model.get_by_repo_name(repo_name)
138 if not repo:
140 if not repo:
139 h.flash(_('%s repository is not mapped to db perhaps'
141 h.flash(_('%s repository is not mapped to db perhaps'
140 ' it was created or renamed from the filesystem'
142 ' it was created or renamed from the filesystem'
@@ -142,7 +144,7 class SettingsController(BaseController)
142 ' in order to rescan repositories') % repo_name,
144 ' in order to rescan repositories') % repo_name,
143 category='error')
145 category='error')
144
146
145 return redirect(url('hg_home'))
147 return redirect(url('home'))
146
148
147 return render('settings/repo_fork.html')
149 return render('settings/repo_fork.html')
148
150
@@ -150,17 +152,18 class SettingsController(BaseController)
150
152
151 def fork_create(self, repo_name):
153 def fork_create(self, repo_name):
152 repo_model = RepoModel()
154 repo_model = RepoModel()
153 c.repo_info = repo_model.get(repo_name)
155 c.repo_info = repo_model.get_by_repo_name(repo_name)
154 _form = RepoForkForm()()
156 _form = RepoForkForm(old_data={'repo_type':c.repo_info.repo_type})()
155 form_result = {}
157 form_result = {}
156 try:
158 try:
157 form_result = _form.to_python(dict(request.POST))
159 form_result = _form.to_python(dict(request.POST))
158 form_result.update({'repo_name':repo_name})
160 form_result.update({'repo_name':repo_name})
159 repo_model.create_fork(form_result, c.rhodecode_user)
161 repo_model.create_fork(form_result, c.rhodecode_user)
160 h.flash(_('fork %s repository as %s task added') \
162 h.flash(_('forked %s repository as %s') \
161 % (repo_name, form_result['fork_name']),
163 % (repo_name, form_result['fork_name']),
162 category='success')
164 category='success')
163 action_logger(self.rhodecode_user, 'user_forked_repo',
165 action_logger(self.rhodecode_user,
166 'user_forked_repo:%s' % form_result['fork_name'],
164 repo_name, '', self.sa)
167 repo_name, '', self.sa)
165 except formencode.Invalid, errors:
168 except formencode.Invalid, errors:
166 c.new_repo = errors.value['fork_name']
169 c.new_repo = errors.value['fork_name']
@@ -172,4 +175,4 class SettingsController(BaseController)
172 errors=errors.error_dict or {},
175 errors=errors.error_dict or {},
173 prefix_error=False,
176 prefix_error=False,
174 encoding="UTF-8")
177 encoding="UTF-8")
175 return redirect(url('hg_home'))
178 return redirect(url('home'))
@@ -25,7 +25,7 shortlog controller for pylons
25 from pylons import tmpl_context as c, request
25 from pylons import tmpl_context as c, request
26 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
26 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27 from rhodecode.lib.base import BaseController, render
27 from rhodecode.lib.base import BaseController, render
28 from rhodecode.model.hg_model import HgModel
28 from rhodecode.model.scm import ScmModel
29 from webhelpers.paginate import Page
29 from webhelpers.paginate import Page
30 import logging
30 import logging
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
@@ -40,7 +40,7 class ShortlogController(BaseController)
40
40
41 def index(self):
41 def index(self):
42 p = int(request.params.get('page', 1))
42 p = int(request.params.get('page', 1))
43 repo = HgModel().get_repo(c.repo_name)
43 repo = ScmModel().get_repo(c.repo_name)
44 c.repo_changesets = Page(repo, page=p, items_per_page=20)
44 c.repo_changesets = Page(repo, page=p, items_per_page=20)
45 c.shortlog_data = render('shortlog/shortlog_data.html')
45 c.shortlog_data = render('shortlog/shortlog_data.html')
46 if request.params.get('partial'):
46 if request.params.get('partial'):
@@ -1,8 +1,15
1 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 # encoding: utf-8
2 """
3 # summary controller for pylons
3 package.rhodecode.controllers.summary
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 ~~~~~~~~~~~~~~
5 #
5
6 Summary controller for Rhodecode
7
8 :created_on: Apr 18, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
6 # This program is free software; you can redistribute it and/or
13 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
14 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
15 # as published by the Free Software Foundation; version 2
@@ -17,24 +24,29
17 # along with this program; if not, write to the Free Software
24 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
26 # MA 02110-1301, USA.
20 """
27
21 Created on April 18, 2010
28 import calendar
22 summary controller for pylons
29 import logging
23 @author: marcink
30 from time import mktime
24 """
31 from datetime import datetime, timedelta
32
33 from vcs.exceptions import ChangesetError
34
25 from pylons import tmpl_context as c, request, url
35 from pylons import tmpl_context as c, request, url
36 from pylons.i18n.translation import _
37
38 from rhodecode.model.scm import ScmModel
39 from rhodecode.model.db import Statistics
40
26 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27 from rhodecode.lib.base import BaseController, render
42 from rhodecode.lib.base import BaseController, render
28 from rhodecode.lib.utils import OrderedDict
43 from rhodecode.lib.utils import OrderedDict, EmptyChangeset
29 from rhodecode.model.hg_model import HgModel
44
30 from rhodecode.model.db import Statistics
31 from webhelpers.paginate import Page
32 from rhodecode.lib.celerylib import run_task
45 from rhodecode.lib.celerylib import run_task
33 from rhodecode.lib.celerylib.tasks import get_commits_stats
46 from rhodecode.lib.celerylib.tasks import get_commits_stats
34 from datetime import datetime, timedelta
47
35 from time import mktime
48 from webhelpers.paginate import Page
36 import calendar
49
37 import logging
38 try:
50 try:
39 import json
51 import json
40 except ImportError:
52 except ImportError:
@@ -51,25 +63,44 class SummaryController(BaseController):
51 super(SummaryController, self).__before__()
63 super(SummaryController, self).__before__()
52
64
53 def index(self):
65 def index(self):
54 hg_model = HgModel()
66 scm_model = ScmModel()
55 c.repo_info = hg_model.get_repo(c.repo_name)
67 c.repo_info = scm_model.get_repo(c.repo_name)
56 c.repo_changesets = Page(list(c.repo_info[:10]), page=1, items_per_page=20)
68 c.following = scm_model.is_following_repo(c.repo_name,
69 c.rhodecode_user.user_id)
70 def url_generator(**kw):
71 return url('shortlog_home', repo_name=c.repo_name, **kw)
72
73 c.repo_changesets = Page(c.repo_info, page=1, items_per_page=10,
74 url=url_generator)
75
57 e = request.environ
76 e = request.environ
58
77
59 uri = u'%(protocol)s://%(user)s@%(host)s%(prefix)s/%(repo_name)s' % {
78 if self.rhodecode_user.username == 'default':
79 password = ':default'
80 else:
81 password = ''
82
83 uri = u'%(protocol)s://%(user)s%(password)s@%(host)s%(prefix)s/%(repo_name)s' % {
60 'protocol': e.get('wsgi.url_scheme'),
84 'protocol': e.get('wsgi.url_scheme'),
61 'user':str(c.rhodecode_user.username),
85 'user':str(c.rhodecode_user.username),
86 'password':password,
62 'host':e.get('HTTP_HOST'),
87 'host':e.get('HTTP_HOST'),
63 'prefix':e.get('SCRIPT_NAME'),
88 'prefix':e.get('SCRIPT_NAME'),
64 'repo_name':c.repo_name, }
89 'repo_name':c.repo_name, }
65 c.clone_repo_url = uri
90 c.clone_repo_url = uri
66 c.repo_tags = OrderedDict()
91 c.repo_tags = OrderedDict()
67 for name, hash in c.repo_info.tags.items()[:10]:
92 for name, hash in c.repo_info.tags.items()[:10]:
93 try:
68 c.repo_tags[name] = c.repo_info.get_changeset(hash)
94 c.repo_tags[name] = c.repo_info.get_changeset(hash)
95 except ChangesetError:
96 c.repo_tags[name] = EmptyChangeset(hash)
69
97
70 c.repo_branches = OrderedDict()
98 c.repo_branches = OrderedDict()
71 for name, hash in c.repo_info.branches.items()[:10]:
99 for name, hash in c.repo_info.branches.items()[:10]:
100 try:
72 c.repo_branches[name] = c.repo_info.get_changeset(hash)
101 c.repo_branches[name] = c.repo_info.get_changeset(hash)
102 except ChangesetError:
103 c.repo_branches[name] = EmptyChangeset(hash)
73
104
74 td = datetime.today() + timedelta(days=1)
105 td = datetime.today() + timedelta(days=1)
75 y, m, d = td.year, td.month, td.day
106 y, m, d = td.year, td.month, td.day
@@ -80,8 +111,11 class SummaryController(BaseController):
80 d, 0, 0, 0, 0, 0, 0,))
111 d, 0, 0, 0, 0, 0, 0,))
81
112
82 ts_max_y = mktime((y, m, d, 0, 0, 0, 0, 0, 0,))
113 ts_max_y = mktime((y, m, d, 0, 0, 0, 0, 0, 0,))
83
114 if c.repo_info.dbrepo.enable_statistics:
115 c.no_data_msg = _('No data loaded yet')
84 run_task(get_commits_stats, c.repo_info.name, ts_min_y, ts_max_y)
116 run_task(get_commits_stats, c.repo_info.name, ts_min_y, ts_max_y)
117 else:
118 c.no_data_msg = _('Statistics update are disabled for this repository')
85 c.ts_min = ts_min_m
119 c.ts_min = ts_min_m
86 c.ts_max = ts_max_y
120 c.ts_max = ts_max_y
87
121
@@ -91,18 +125,20 class SummaryController(BaseController):
91
125
92
126
93 if stats and stats.languages:
127 if stats and stats.languages:
128 c.no_data = False is c.repo_info.dbrepo.enable_statistics
94 lang_stats = json.loads(stats.languages)
129 lang_stats = json.loads(stats.languages)
95 c.commit_data = stats.commit_activity
130 c.commit_data = stats.commit_activity
96 c.overview_data = stats.commit_activity_combined
131 c.overview_data = stats.commit_activity_combined
97 c.trending_languages = json.dumps(OrderedDict(
132 c.trending_languages = json.dumps(OrderedDict(
98 sorted(lang_stats.items(), reverse=True,
133 sorted(lang_stats.items(), reverse=True,
99 key=lambda k: k[1])[:2]
134 key=lambda k: k[1])[:10]
100 )
135 )
101 )
136 )
102 else:
137 else:
103 c.commit_data = json.dumps({})
138 c.commit_data = json.dumps({})
104 c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 0] ])
139 c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 10] ])
105 c.trending_languages = json.dumps({})
140 c.trending_languages = json.dumps({})
141 c.no_data = True
106
142
107 return render('summary/summary.html')
143 return render('summary/summary.html')
108
144
@@ -26,7 +26,7 from pylons import tmpl_context as c
26 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
26 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27 from rhodecode.lib.base import BaseController, render
27 from rhodecode.lib.base import BaseController, render
28 from rhodecode.lib.utils import OrderedDict
28 from rhodecode.lib.utils import OrderedDict
29 from rhodecode.model.hg_model import HgModel
29 from rhodecode.model.scm import ScmModel
30 import logging
30 import logging
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
@@ -38,7 +38,7 class TagsController(BaseController):
38 super(TagsController, self).__before__()
38 super(TagsController, self).__before__()
39
39
40 def index(self):
40 def index(self):
41 hg_model = HgModel()
41 hg_model = ScmModel()
42 c.repo_info = hg_model.get_repo(c.repo_name)
42 c.repo_info = hg_model.get_repo(c.repo_name)
43 c.repo_tags = OrderedDict()
43 c.repo_tags = OrderedDict()
44 for name, hash_ in c.repo_info.tags.items():
44 for name, hash_ in c.repo_info.tags.items():
@@ -28,4 +28,4 class Globals(object):
28 @LazyProperty
28 @LazyProperty
29 def base_path(self):
29 def base_path(self):
30 if self.baseui:
30 if self.baseui:
31 return self.paths[0][1].replace('*', '')
31 return self.paths[0][1]
@@ -22,20 +22,21 Created on April 4, 2010
22
22
23 @author: marcink
23 @author: marcink
24 """
24 """
25 from beaker.cache import cache_region
26 from pylons import config, session, url, request
25 from pylons import config, session, url, request
27 from pylons.controllers.util import abort, redirect
26 from pylons.controllers.util import abort, redirect
27 from rhodecode.lib.exceptions import *
28 from rhodecode.lib.utils import get_repo_slug
28 from rhodecode.lib.utils import get_repo_slug
29 from rhodecode.lib.auth_ldap import AuthLdap
29 from rhodecode.model import meta
30 from rhodecode.model import meta
31 from rhodecode.model.user import UserModel
30 from rhodecode.model.caching_query import FromCache
32 from rhodecode.model.caching_query import FromCache
31 from rhodecode.model.db import User, RepoToPerm, Repository, Permission, \
33 from rhodecode.model.db import User, RepoToPerm, Repository, Permission, \
32 UserToPerm
34 UserToPerm
33 from sqlalchemy.exc import OperationalError
34 from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
35 import bcrypt
35 import bcrypt
36 from decorator import decorator
36 from decorator import decorator
37 import logging
37 import logging
38 import random
38 import random
39 import traceback
39
40
40 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
41
42
@@ -74,30 +75,83 def get_crypt_password(password):
74 def check_password(password, hashed):
75 def check_password(password, hashed):
75 return bcrypt.hashpw(password, hashed) == hashed
76 return bcrypt.hashpw(password, hashed) == hashed
76
77
77 @cache_region('super_short_term', 'cached_user')
78 def authfunc(environ, username, password):
78 def get_user_cached(username):
79 """
79 sa = meta.Session
80 Dummy authentication function used in Mercurial/Git/ and access control,
80 try:
81
81 user = sa.query(User).filter(User.username == username).one()
82 :param environ: needed only for using in Basic auth
82 finally:
83 """
83 meta.Session.remove()
84 return authenticate(username, password)
84 return user
85
86
87 def authenticate(username, password):
88 """
89 Authentication function used for access control,
90 firstly checks for db authentication then if ldap is enabled for ldap
91 authentication, also creates ldap user if not in database
85
92
86 def authfunc(environ, username, password):
93 :param username: username
87 try:
94 :param password: password
88 user = get_user_cached(username)
95 """
89 except (NoResultFound, MultipleResultsFound, OperationalError), e:
96 user_model = UserModel()
90 log.error(e)
97 user = user_model.get_by_username(username, cache=False)
91 user = None
92
98
93 if user:
99 log.debug('Authenticating user using RhodeCode account')
100 if user is not None and user.is_ldap is False:
94 if user.active:
101 if user.active:
95 if user.username == username and check_password(password, user.password):
102
103 if user.username == 'default' and user.active:
104 log.info('user %s authenticated correctly as anonymous user',
105 username)
106 return True
107
108 elif user.username == username and check_password(password, user.password):
96 log.info('user %s authenticated correctly', username)
109 log.info('user %s authenticated correctly', username)
97 return True
110 return True
98 else:
111 else:
99 log.error('user %s is disabled', username)
112 log.warning('user %s is disabled', username)
113
114 else:
115 log.debug('Regular authentication failed')
116 user_obj = user_model.get_by_username(username, cache=False,
117 case_insensitive=True)
118
119 if user_obj is not None and user_obj.is_ldap is False:
120 log.debug('this user already exists as non ldap')
121 return False
122
123 from rhodecode.model.settings import SettingsModel
124 ldap_settings = SettingsModel().get_ldap_settings()
100
125
126 #======================================================================
127 # FALLBACK TO LDAP AUTH IN ENABLE
128 #======================================================================
129 if ldap_settings.get('ldap_active', False):
130 log.debug("Authenticating user using ldap")
131 kwargs = {
132 'server':ldap_settings.get('ldap_host', ''),
133 'base_dn':ldap_settings.get('ldap_base_dn', ''),
134 'port':ldap_settings.get('ldap_port'),
135 'bind_dn':ldap_settings.get('ldap_dn_user'),
136 'bind_pass':ldap_settings.get('ldap_dn_pass'),
137 'use_ldaps':ldap_settings.get('ldap_ldaps'),
138 'ldap_version':3,
139 }
140 log.debug('Checking for ldap authentication')
141 try:
142 aldap = AuthLdap(**kwargs)
143 res = aldap.authenticate_ldap(username, password)
144 log.debug('Got ldap response %s', res)
145
146 if user_model.create_ldap(username, password):
147 log.info('created new ldap user')
148
149 return True
150 except (LdapUsernameError, LdapPasswordError,):
151 pass
152 except (Exception,):
153 log.error(traceback.format_exc())
154 pass
101 return False
155 return False
102
156
103 class AuthUser(object):
157 class AuthUser(object):
@@ -114,6 +168,8 class AuthUser(object):
114 self.is_admin = False
168 self.is_admin = False
115 self.permissions = {}
169 self.permissions = {}
116
170
171 def __repr__(self):
172 return "<AuthUser('id:%s:%s')>" % (self.user_id, self.username)
117
173
118 def set_available_permissions(config):
174 def set_available_permissions(config):
119 """
175 """
@@ -125,8 +181,10 def set_available_permissions(config):
125 """
181 """
126 log.info('getting information about all available permissions')
182 log.info('getting information about all available permissions')
127 try:
183 try:
128 sa = meta.Session
184 sa = meta.Session()
129 all_perms = sa.query(Permission).all()
185 all_perms = sa.query(Permission).all()
186 except:
187 pass
130 finally:
188 finally:
131 meta.Session.remove()
189 meta.Session.remove()
132
190
@@ -135,26 +193,6 def set_available_permissions(config):
135 def set_base_path(config):
193 def set_base_path(config):
136 config['base_path'] = config['pylons.app_globals'].base_path
194 config['base_path'] = config['pylons.app_globals'].base_path
137
195
138 def fill_data(user):
139 """
140 Fills user data with those from database and log out user if not present
141 in database
142 :param user:
143 """
144 sa = meta.Session
145 dbuser = sa.query(User).options(FromCache('sql_cache_short',
146 'getuser_%s' % user.user_id))\
147 .get(user.user_id)
148 if dbuser:
149 user.username = dbuser.username
150 user.is_admin = dbuser.admin
151 user.name = dbuser.name
152 user.lastname = dbuser.lastname
153 user.email = dbuser.email
154 else:
155 user.is_authenticated = False
156 meta.Session.remove()
157 return user
158
196
159 def fill_perms(user):
197 def fill_perms(user):
160 """
198 """
@@ -162,16 +200,14 def fill_perms(user):
162 :param user:
200 :param user:
163 """
201 """
164
202
165 sa = meta.Session
203 sa = meta.Session()
166 user.permissions['repositories'] = {}
204 user.permissions['repositories'] = {}
167 user.permissions['global'] = set()
205 user.permissions['global'] = set()
168
206
169 #===========================================================================
207 #===========================================================================
170 # fetch default permissions
208 # fetch default permissions
171 #===========================================================================
209 #===========================================================================
172 default_user = sa.query(User)\
210 default_user = UserModel().get_by_username('default', cache=True)
173 .options(FromCache('sql_cache_short','getuser_%s' % 'default'))\
174 .filter(User.username == 'default').scalar()
175
211
176 default_perms = sa.query(RepoToPerm, Repository, Permission)\
212 default_perms = sa.query(RepoToPerm, Repository, Permission)\
177 .join((Repository, RepoToPerm.repository_id == Repository.repo_id))\
213 .join((Repository, RepoToPerm.repository_id == Repository.repo_id))\
@@ -195,8 +231,8 def fill_perms(user):
195
231
196 #default global
232 #default global
197 default_global_perms = sa.query(UserToPerm)\
233 default_global_perms = sa.query(UserToPerm)\
198 .filter(UserToPerm.user == sa.query(User).filter(User.username ==
234 .filter(UserToPerm.user == sa.query(User)\
199 'default').one())
235 .filter(User.username == 'default').one())
200
236
201 for perm in default_global_perms:
237 for perm in default_global_perms:
202 user.permissions['global'].add(perm.permission.permission_name)
238 user.permissions['global'].add(perm.permission.permission_name)
@@ -237,8 +273,21 def get_user(session):
237 :param session:
273 :param session:
238 """
274 """
239 user = session.get('rhodecode_user', AuthUser())
275 user = session.get('rhodecode_user', AuthUser())
276 #if the user is not logged in we check for anonymous access
277 #if user is logged and it's a default user check if we still have anonymous
278 #access enabled
279 if user.user_id is None or user.username == 'default':
280 anonymous_user = UserModel().get_by_username('default', cache=True)
281 if anonymous_user.active is True:
282 #then we set this user is logged in
283 user.is_authenticated = True
284 user.user_id = anonymous_user.user_id
285 else:
286 user.is_authenticated = False
287
240 if user.is_authenticated:
288 if user.is_authenticated:
241 user = fill_data(user)
289 user = UserModel().fill_data(user)
290
242 user = fill_perms(user)
291 user = fill_perms(user)
243 session['rhodecode_user'] = user
292 session['rhodecode_user'] = user
244 session.save()
293 session.save()
@@ -248,7 +297,8 def get_user(session):
248 # CHECK DECORATORS
297 # CHECK DECORATORS
249 #===============================================================================
298 #===============================================================================
250 class LoginRequired(object):
299 class LoginRequired(object):
251 """Must be logged in to execute this function else redirect to login page"""
300 """Must be logged in to execute this function else
301 redirect to login page"""
252
302
253 def __call__(self, func):
303 def __call__(self, func):
254 return decorator(self.__wrapper, func)
304 return decorator(self.__wrapper, func)
@@ -273,6 +323,31 class LoginRequired(object):
273 log.debug('redirecting to login page with %s', p)
323 log.debug('redirecting to login page with %s', p)
274 return redirect(url('login_home', came_from=p))
324 return redirect(url('login_home', came_from=p))
275
325
326 class NotAnonymous(object):
327 """Must be logged in to execute this function else
328 redirect to login page"""
329
330 def __call__(self, func):
331 return decorator(self.__wrapper, func)
332
333 def __wrapper(self, func, *fargs, **fkwargs):
334 user = session.get('rhodecode_user', AuthUser())
335 log.debug('Checking if user is not anonymous')
336
337 anonymous = user.username == 'default'
338
339 if anonymous:
340 p = ''
341 if request.environ.get('SCRIPT_NAME') != '/':
342 p += request.environ.get('SCRIPT_NAME')
343
344 p += request.environ.get('PATH_INFO')
345 if request.environ.get('QUERY_STRING'):
346 p += '?' + request.environ.get('QUERY_STRING')
347 return redirect(url('login_home', came_from=p))
348 else:
349 return func(*fargs, **fkwargs)
350
276 class PermsDecorator(object):
351 class PermsDecorator(object):
277 """Base class for decorators"""
352 """Base class for decorators"""
278
353
@@ -292,18 +367,19 class PermsDecorator(object):
292 # _wrapper.__name__ = func.__name__
367 # _wrapper.__name__ = func.__name__
293 # _wrapper.__dict__.update(func.__dict__)
368 # _wrapper.__dict__.update(func.__dict__)
294 # _wrapper.__doc__ = func.__doc__
369 # _wrapper.__doc__ = func.__doc__
295
370 self.user = session.get('rhodecode_user', AuthUser())
296 self.user_perms = session.get('rhodecode_user', AuthUser()).permissions
371 self.user_perms = self.user.permissions
297 log.debug('checking %s permissions %s for %s',
372 log.debug('checking %s permissions %s for %s %s',
298 self.__class__.__name__, self.required_perms, func.__name__)
373 self.__class__.__name__, self.required_perms, func.__name__,
374 self.user)
299
375
300 if self.check_permissions():
376 if self.check_permissions():
301 log.debug('Permission granted for %s', func.__name__)
377 log.debug('Permission granted for %s %s', func.__name__, self.user)
302
378
303 return func(*fargs, **fkwargs)
379 return func(*fargs, **fkwargs)
304
380
305 else:
381 else:
306 log.warning('Permission denied for %s', func.__name__)
382 log.warning('Permission denied for %s %s', func.__name__, self.user)
307 #redirect with forbidden ret code
383 #redirect with forbidden ret code
308 return abort(403)
384 return abort(403)
309
385
@@ -389,16 +465,17 class PermsFunction(object):
389 return False
465 return False
390 self.user_perms = user.permissions
466 self.user_perms = user.permissions
391 self.granted_for = user.username
467 self.granted_for = user.username
392 log.debug('checking %s %s', self.__class__.__name__, self.required_perms)
468 log.debug('checking %s %s %s', self.__class__.__name__,
469 self.required_perms, user)
393
470
394 if self.check_permissions():
471 if self.check_permissions():
395 log.debug('Permission granted for %s @%s', self.granted_for,
472 log.debug('Permission granted for %s @ %s %s', self.granted_for,
396 check_Location)
473 check_Location, user)
397 return True
474 return True
398
475
399 else:
476 else:
400 log.warning('Permission denied for %s @%s', self.granted_for,
477 log.warning('Permission denied for %s @ %s %s', self.granted_for,
401 check_Location)
478 check_Location, user)
402 return False
479 return False
403
480
404 def check_permissions(self):
481 def check_permissions(self):
@@ -9,8 +9,8 from rhodecode import __version__
9 from rhodecode.lib import auth
9 from rhodecode.lib import auth
10 from rhodecode.lib.utils import get_repo_slug
10 from rhodecode.lib.utils import get_repo_slug
11 from rhodecode.model import meta
11 from rhodecode.model import meta
12 from rhodecode.model.hg_model import _get_repos_cached, \
12 from rhodecode.model.scm import ScmModel
13 _get_repos_switcher_cached
13 from rhodecode import BACKENDS
14
14
15 class BaseController(WSGIController):
15 class BaseController(WSGIController):
16
16
@@ -18,20 +18,26 class BaseController(WSGIController):
18 c.rhodecode_version = __version__
18 c.rhodecode_version = __version__
19 c.rhodecode_name = config['rhodecode_title']
19 c.rhodecode_name = config['rhodecode_title']
20 c.repo_name = get_repo_slug(request)
20 c.repo_name = get_repo_slug(request)
21 c.cached_repo_list = _get_repos_cached()
21 c.cached_repo_list = ScmModel().get_repos()
22 c.repo_switcher_list = _get_repos_switcher_cached(c.cached_repo_list)
22 c.backends = BACKENDS.keys()
23 self.cut_off_limit = int(config['cut_off_limit'])
24 self.sa = meta.Session()
25 scm_model = ScmModel(self.sa)
26 #c.unread_journal = scm_model.get_unread_journal()
23
27
24 if c.repo_name:
28 if c.repo_name:
25 cached_repo = c.cached_repo_list.get(c.repo_name)
29 cached_repo = scm_model.get(c.repo_name)
26
27 if cached_repo:
30 if cached_repo:
28 c.repository_tags = cached_repo.tags
31 c.repository_tags = cached_repo.tags
29 c.repository_branches = cached_repo.branches
32 c.repository_branches = cached_repo.branches
33 c.repository_followers = scm_model.get_followers(cached_repo.dbrepo.repo_id)
34 c.repository_forks = scm_model.get_forks(cached_repo.dbrepo.repo_id)
30 else:
35 else:
31 c.repository_tags = {}
36 c.repository_tags = {}
32 c.repository_branches = {}
37 c.repository_branches = {}
38 c.repository_followers = 0
39 c.repository_forks = 0
33
40
34 self.sa = meta.Session
35
41
36 def __call__(self, environ, start_response):
42 def __call__(self, environ, start_response):
37 """Invoke the Controller"""
43 """Invoke the Controller"""
@@ -1,14 +1,54
1 from rhodecode.lib.pidlock import DaemonLock, LockHeld
1 # -*- coding: utf-8 -*-
2 from vcs.utils.lazy import LazyProperty
2 """
3 from decorator import decorator
3 package.rhodecode.lib.celerylib.__init__
4 import logging
4 ~~~~~~~~~~~~~~
5
6 celery libs for RhodeCode
7
8 :created_on: Nov 27, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 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
14 # modify it under the terms of the GNU General Public License
15 # as published by the Free Software Foundation; version 2
16 # of the License or (at your opinion) any later version of the license.
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, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 # MA 02110-1301, USA.
27
5 import os
28 import os
6 import sys
29 import sys
30 import socket
7 import traceback
31 import traceback
32 import logging
33
8 from hashlib import md5
34 from hashlib import md5
9 import socket
35 from decorator import decorator
36 from vcs.utils.lazy import LazyProperty
37
38 from rhodecode.lib.pidlock import DaemonLock, LockHeld
39
40 from pylons import config
41
10 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
11
43
44 def str2bool(v):
45 return v.lower() in ["yes", "true", "t", "1"] if v else None
46
47 try:
48 CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
49 except KeyError:
50 CELERY_ON = False
51
12 class ResultWrapper(object):
52 class ResultWrapper(object):
13 def __init__(self, task):
53 def __init__(self, task):
14 self.task = task
54 self.task = task
@@ -18,27 +58,22 class ResultWrapper(object):
18 return self.task
58 return self.task
19
59
20 def run_task(task, *args, **kwargs):
60 def run_task(task, *args, **kwargs):
61 if CELERY_ON:
21 try:
62 try:
22 t = task.delay(*args, **kwargs)
63 t = task.delay(*args, **kwargs)
23 log.info('running task %s', t.task_id)
64 log.info('running task %s:%s', t.task_id, task)
24 return t
65 return t
25 except socket.error, e:
66 except socket.error, e:
26
67 if e.errno == 111:
27 try:
28 conn_failed = e.errno == 111
29 except AttributeError:
30 conn_failed = False
31
32 if conn_failed:
33 log.debug('Unable to connect to celeryd. Sync execution')
68 log.debug('Unable to connect to celeryd. Sync execution')
34 else:
69 else:
35 log.debug('Unable to connect to celeryd. Sync execution')
70 log.error(traceback.format_exc())
36
37 except KeyError, e:
71 except KeyError, e:
38 log.debug('Unable to connect to celeryd. Sync execution')
72 log.debug('Unable to connect to celeryd. Sync execution')
39 except Exception, e:
73 except Exception, e:
40 log.error(traceback.format_exc())
74 log.error(traceback.format_exc())
41
75
76 log.debug('executing task %s in sync mode', task)
42 return ResultWrapper(task(*args, **kwargs))
77 return ResultWrapper(task(*args, **kwargs))
43
78
44
79
@@ -1,14 +1,26
1 from celery.decorators import task
1 from celery.decorators import task
2
2
3 import os
4 import traceback
5 from time import mktime
3 from operator import itemgetter
6 from operator import itemgetter
7
8 from pylons import config
4 from pylons.i18n.translation import _
9 from pylons.i18n.translation import _
5 from rhodecode.lib.celerylib import run_task, locked_task
10
11 from rhodecode.lib.celerylib import run_task, locked_task, str2bool
6 from rhodecode.lib.helpers import person
12 from rhodecode.lib.helpers import person
7 from rhodecode.lib.smtp_mailer import SmtpMailer
13 from rhodecode.lib.smtp_mailer import SmtpMailer
8 from rhodecode.lib.utils import OrderedDict
14 from rhodecode.lib.utils import OrderedDict, add_cache
9 from time import mktime
15 from rhodecode.model import init_model
10 from vcs.backends.hg import MercurialRepository
16 from rhodecode.model import meta
11 import traceback
17 from rhodecode.model.db import RhodeCodeUi
18
19 from vcs.backends import get_repo
20
21 from sqlalchemy import engine_from_config
22
23 add_cache(config)
12
24
13 try:
25 try:
14 import json
26 import json
@@ -16,90 +28,50 except ImportError:
16 #python 2.5 compatibility
28 #python 2.5 compatibility
17 import simplejson as json
29 import simplejson as json
18
30
19 try:
20 from celeryconfig import PYLONS_CONFIG as config
21 celery_on = True
22 except ImportError:
23 #if celeryconfig is not present let's just load our pylons
24 #config instead
25 from pylons import config
26 celery_on = False
27
28
29 __all__ = ['whoosh_index', 'get_commits_stats',
31 __all__ = ['whoosh_index', 'get_commits_stats',
30 'reset_user_password', 'send_email']
32 'reset_user_password', 'send_email']
31
33
34 CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
35
32 def get_session():
36 def get_session():
33 if celery_on:
37 if CELERY_ON:
34 from sqlalchemy import engine_from_config
38 engine = engine_from_config(config, 'sqlalchemy.db1.')
35 from sqlalchemy.orm import sessionmaker, scoped_session
39 init_model(engine)
36 engine = engine_from_config(dict(config.items('app:main')), 'sqlalchemy.db1.')
40 sa = meta.Session()
37 sa = scoped_session(sessionmaker(bind=engine))
38 else:
39 #If we don't use celery reuse our current application Session
40 from rhodecode.model.meta import Session
41 sa = Session
42
43 return sa
41 return sa
44
42
45 def get_hg_settings():
43 def get_repos_path():
46 from rhodecode.model.db import RhodeCodeSettings
47 sa = get_session()
48 ret = sa.query(RhodeCodeSettings).all()
49
50 if not ret:
51 raise Exception('Could not get application settings !')
52 settings = {}
53 for each in ret:
54 settings['rhodecode_' + each.app_settings_name] = each.app_settings_value
55
56 return settings
57
58 def get_hg_ui_settings():
59 from rhodecode.model.db import RhodeCodeUi
60 sa = get_session()
44 sa = get_session()
61 ret = sa.query(RhodeCodeUi).all()
45 q = sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
62
46 return q.ui_value
63 if not ret:
64 raise Exception('Could not get application ui settings !')
65 settings = {}
66 for each in ret:
67 k = each.ui_key
68 v = each.ui_value
69 if k == '/':
70 k = 'root_path'
71
72 if k.find('.') != -1:
73 k = k.replace('.', '_')
74
75 if each.ui_section == 'hooks':
76 v = each.ui_active
77
78 settings[each.ui_section + '_' + k] = v
79
80 return settings
81
47
82 @task
48 @task
83 @locked_task
49 @locked_task
84 def whoosh_index(repo_location, full_index):
50 def whoosh_index(repo_location, full_index):
85 log = whoosh_index.get_logger()
51 log = whoosh_index.get_logger()
86 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
52 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
87 WhooshIndexingDaemon(repo_location=repo_location).run(full_index=full_index)
53 index_location = config['index_dir']
54 WhooshIndexingDaemon(index_location=index_location,
55 repo_location=repo_location, sa=get_session())\
56 .run(full_index=full_index)
88
57
89 @task
58 @task
90 @locked_task
59 @locked_task
91 def get_commits_stats(repo_name, ts_min_y, ts_max_y):
60 def get_commits_stats(repo_name, ts_min_y, ts_max_y):
92 from rhodecode.model.db import Statistics, Repository
61 from rhodecode.model.db import Statistics, Repository
93 log = get_commits_stats.get_logger()
62 log = get_commits_stats.get_logger()
94 author_key_cleaner = lambda k: person(k).replace('"', "") #for js data compatibilty
63
64 #for js data compatibilty
65 author_key_cleaner = lambda k: person(k).replace('"', "")
95
66
96 commits_by_day_author_aggregate = {}
67 commits_by_day_author_aggregate = {}
97 commits_by_day_aggregate = {}
68 commits_by_day_aggregate = {}
98 repos_path = get_hg_ui_settings()['paths_root_path'].replace('*', '')
69 repos_path = get_repos_path()
99 repo = MercurialRepository(repos_path + repo_name)
70 p = os.path.join(repos_path, repo_name)
71 repo = get_repo(p)
100
72
101 skip_date_limit = True
73 skip_date_limit = True
102 parse_limit = 350 #limit for single task changeset parsing optimal for
74 parse_limit = 250 #limit for single task changeset parsing optimal for
103 last_rev = 0
75 last_rev = 0
104 last_cs = None
76 last_cs = None
105 timegetter = itemgetter('time')
77 timegetter = itemgetter('time')
@@ -116,8 +88,8 def get_commits_stats(repo_name, ts_min_
116 return True
88 return True
117
89
118 if last_rev == repo.revisions[-1] and len(repo.revisions) > 1:
90 if last_rev == repo.revisions[-1] and len(repo.revisions) > 1:
119 #pass silently without any work if we're not on first revision or current
91 #pass silently without any work if we're not on first revision or
120 #state of parsing revision(from db marker) is the last revision
92 #current state of parsing revision(from db marker) is the last revision
121 return True
93 return True
122
94
123 if cur_stats:
95 if cur_stats:
@@ -127,13 +99,14 def get_commits_stats(repo_name, ts_min_
127 commits_by_day_author_aggregate = json.loads(cur_stats.commit_activity)
99 commits_by_day_author_aggregate = json.loads(cur_stats.commit_activity)
128
100
129 log.debug('starting parsing %s', parse_limit)
101 log.debug('starting parsing %s', parse_limit)
130 for cnt, rev in enumerate(repo.revisions[last_rev:]):
102 lmktime = mktime
103
104 last_rev = last_rev + 1 if last_rev > 0 else last_rev
105 for rev in repo.revisions[last_rev:last_rev + parse_limit]:
131 last_cs = cs = repo.get_changeset(rev)
106 last_cs = cs = repo.get_changeset(rev)
132 k = '%s-%s-%s' % (cs.date.timetuple()[0], cs.date.timetuple()[1],
107 k = lmktime([cs.date.timetuple()[0], cs.date.timetuple()[1],
133 cs.date.timetuple()[2])
108 cs.date.timetuple()[2], 0, 0, 0, 0, 0, 0])
134 timetupple = [int(x) for x in k.split('-')]
109
135 timetupple.extend([0 for _ in xrange(6)])
136 k = mktime(timetupple)
137 if commits_by_day_author_aggregate.has_key(author_key_cleaner(cs.author)):
110 if commits_by_day_author_aggregate.has_key(author_key_cleaner(cs.author)):
138 try:
111 try:
139 l = [timegetter(x) for x in commits_by_day_author_aggregate\
112 l = [timegetter(x) for x in commits_by_day_author_aggregate\
@@ -183,15 +156,7 def get_commits_stats(repo_name, ts_min_
183 else:
156 else:
184 commits_by_day_aggregate[k] = 1
157 commits_by_day_aggregate[k] = 1
185
158
186 if cnt >= parse_limit:
159 overview_data = sorted(commits_by_day_aggregate.items(), key=itemgetter(0))
187 #don't fetch to much data since we can freeze application
188 break
189
190 overview_data = []
191 for k, v in commits_by_day_aggregate.items():
192 overview_data.append([k, v])
193 overview_data = sorted(overview_data, key=itemgetter(0))
194
195 if not commits_by_day_author_aggregate:
160 if not commits_by_day_author_aggregate:
196 commits_by_day_author_aggregate[author_key_cleaner(repo.contact)] = {
161 commits_by_day_author_aggregate[author_key_cleaner(repo.contact)] = {
197 "label":author_key_cleaner(repo.contact),
162 "label":author_key_cleaner(repo.contact),
@@ -208,6 +173,7 def get_commits_stats(repo_name, ts_min_
208 log.debug('revisions to parse %s', leftovers)
173 log.debug('revisions to parse %s', leftovers)
209
174
210 if last_rev == 0 or leftovers < parse_limit:
175 if last_rev == 0 or leftovers < parse_limit:
176 log.debug('getting code trending stats')
211 stats.languages = json.dumps(__get_codes_stats(repo_name))
177 stats.languages = json.dumps(__get_codes_stats(repo_name))
212
178
213 stats.repository = dbrepo
179 stats.repository = dbrepo
@@ -258,19 +224,33 def reset_user_password(user_email):
258 except:
224 except:
259 log.error('Failed to update user password')
225 log.error('Failed to update user password')
260 log.error(traceback.format_exc())
226 log.error(traceback.format_exc())
227
261 return True
228 return True
262
229
263 @task
230 @task
264 def send_email(recipients, subject, body):
231 def send_email(recipients, subject, body):
232 """
233 Sends an email with defined parameters from the .ini files.
234
235
236 :param recipients: list of recipients, it this is empty the defined email
237 address from field 'email_to' is used instead
238 :param subject: subject of the mail
239 :param body: body of the mail
240 """
265 log = send_email.get_logger()
241 log = send_email.get_logger()
266 email_config = dict(config.items('DEFAULT'))
242 email_config = config
243
244 if not recipients:
245 recipients = [email_config.get('email_to')]
246
267 mail_from = email_config.get('app_email_from')
247 mail_from = email_config.get('app_email_from')
268 user = email_config.get('smtp_username')
248 user = email_config.get('smtp_username')
269 passwd = email_config.get('smtp_password')
249 passwd = email_config.get('smtp_password')
270 mail_server = email_config.get('smtp_server')
250 mail_server = email_config.get('smtp_server')
271 mail_port = email_config.get('smtp_port')
251 mail_port = email_config.get('smtp_port')
272 tls = email_config.get('smtp_use_tls')
252 tls = str2bool(email_config.get('smtp_use_tls'))
273 ssl = False
253 ssl = str2bool(email_config.get('smtp_use_ssl'))
274
254
275 try:
255 try:
276 m = SmtpMailer(mail_from, user, passwd, mail_server,
256 m = SmtpMailer(mail_from, user, passwd, mail_server,
@@ -284,45 +264,96 def send_email(recipients, subject, body
284
264
285 @task
265 @task
286 def create_repo_fork(form_data, cur_user):
266 def create_repo_fork(form_data, cur_user):
287 import os
267 from rhodecode.model.repo import RepoModel
288 from rhodecode.model.repo_model import RepoModel
268 from vcs import get_backend
289 sa = get_session()
269 log = create_repo_fork.get_logger()
290 rm = RepoModel(sa)
270 repo_model = RepoModel(get_session())
291
271 repo_model.create(form_data, cur_user, just_db=True, fork=True)
292 rm.create(form_data, cur_user, just_db=True, fork=True)
272 repo_name = form_data['repo_name']
273 repos_path = get_repos_path()
274 repo_path = os.path.join(repos_path, repo_name)
275 repo_fork_path = os.path.join(repos_path, form_data['fork_name'])
276 alias = form_data['repo_type']
293
277
294 repos_path = get_hg_ui_settings()['paths_root_path'].replace('*', '')
278 log.info('creating repo fork %s as %s', repo_name, repo_path)
295 repo_path = os.path.join(repos_path, form_data['repo_name'])
279 backend = get_backend(alias)
296 repo_fork_path = os.path.join(repos_path, form_data['fork_name'])
280 backend(str(repo_fork_path), create=True, src_url=str(repo_path))
297
298 MercurialRepository(str(repo_fork_path), True, clone_url=str(repo_path))
299
300
281
301 def __get_codes_stats(repo_name):
282 def __get_codes_stats(repo_name):
302 LANGUAGES_EXTENSIONS = ['action', 'adp', 'ashx', 'asmx', 'aspx', 'asx', 'axd', 'c',
283 LANGUAGES_EXTENSIONS_MAP = {'scm': 'Scheme', 'asmx': 'VbNetAspx', 'Rout':
303 'cfg', 'cfm', 'cpp', 'cs', 'diff', 'do', 'el', 'erl',
284 'RConsole', 'rest': 'Rst', 'abap': 'ABAP', 'go': 'Go', 'phtml': 'HtmlPhp',
304 'h', 'java', 'js', 'jsp', 'jspx', 'lisp',
285 'ns2': 'Newspeak', 'xml': 'EvoqueXml', 'sh-session': 'BashSession', 'ads':
305 'lua', 'm', 'mako', 'ml', 'pas', 'patch', 'php', 'php3',
286 'Ada', 'clj': 'Clojure', 'll': 'Llvm', 'ebuild': 'Bash', 'adb': 'Ada',
306 'php4', 'phtml', 'pm', 'py', 'rb', 'rst', 's', 'sh',
287 'ada': 'Ada', 'c++-objdump': 'CppObjdump', 'aspx':
307 'tpl', 'txt', 'vim', 'wss', 'xhtml', 'xml', 'xsl', 'xslt',
288 'VbNetAspx', 'ksh': 'Bash', 'coffee': 'CoffeeScript', 'vert': 'GLShader',
308 'yaws']
289 'Makefile.*': 'Makefile', 'di': 'D', 'dpatch': 'DarcsPatch', 'rake':
309 repos_path = get_hg_ui_settings()['paths_root_path'].replace('*', '')
290 'Ruby', 'moo': 'MOOCode', 'erl-sh': 'ErlangShell', 'geo': 'GLShader',
310 repo = MercurialRepository(repos_path + repo_name)
291 'pov': 'Povray', 'bas': 'VbNet', 'bat': 'Batch', 'd': 'D', 'lisp':
292 'CommonLisp', 'h': 'C', 'rbx': 'Ruby', 'tcl': 'Tcl', 'c++': 'Cpp', 'md':
293 'MiniD', '.vimrc': 'Vim', 'xsd': 'Xml', 'ml': 'Ocaml', 'el': 'CommonLisp',
294 'befunge': 'Befunge', 'xsl': 'Xslt', 'pyx': 'Cython', 'cfm':
295 'ColdfusionHtml', 'evoque': 'Evoque', 'cfg': 'Ini', 'htm': 'Html',
296 'Makefile': 'Makefile', 'cfc': 'ColdfusionHtml', 'tex': 'Tex', 'cs':
297 'CSharp', 'mxml': 'Mxml', 'patch': 'Diff', 'apache.conf': 'ApacheConf',
298 'scala': 'Scala', 'applescript': 'AppleScript', 'GNUmakefile': 'Makefile',
299 'c-objdump': 'CObjdump', 'lua': 'Lua', 'apache2.conf': 'ApacheConf', 'rb':
300 'Ruby', 'gemspec': 'Ruby', 'rl': 'RagelObjectiveC', 'vala': 'Vala', 'tmpl':
301 'Cheetah', 'bf': 'Brainfuck', 'plt': 'Gnuplot', 'G': 'AntlrRuby', 'xslt':
302 'Xslt', 'flxh': 'Felix', 'asax': 'VbNetAspx', 'Rakefile': 'Ruby', 'S': 'S',
303 'wsdl': 'Xml', 'js': 'Javascript', 'autodelegate': 'Myghty', 'properties':
304 'Ini', 'bash': 'Bash', 'c': 'C', 'g': 'AntlrRuby', 'r3': 'Rebol', 's':
305 'Gas', 'ashx': 'VbNetAspx', 'cxx': 'Cpp', 'boo': 'Boo', 'prolog': 'Prolog',
306 'sqlite3-console': 'SqliteConsole', 'cl': 'CommonLisp', 'cc': 'Cpp', 'pot':
307 'Gettext', 'vim': 'Vim', 'pxi': 'Cython', 'yaml': 'Yaml', 'SConstruct':
308 'Python', 'diff': 'Diff', 'txt': 'Text', 'cw': 'Redcode', 'pxd': 'Cython',
309 'plot': 'Gnuplot', 'java': 'Java', 'hrl': 'Erlang', 'py': 'Python',
310 'makefile': 'Makefile', 'squid.conf': 'SquidConf', 'asm': 'Nasm', 'toc':
311 'Tex', 'kid': 'Genshi', 'rhtml': 'Rhtml', 'po': 'Gettext', 'pl': 'Prolog',
312 'pm': 'Perl', 'hx': 'Haxe', 'ascx': 'VbNetAspx', 'ooc': 'Ooc', 'asy':
313 'Asymptote', 'hs': 'Haskell', 'SConscript': 'Python', 'pytb':
314 'PythonTraceback', 'myt': 'Myghty', 'hh': 'Cpp', 'R': 'S', 'aux': 'Tex',
315 'rst': 'Rst', 'cpp-objdump': 'CppObjdump', 'lgt': 'Logtalk', 'rss': 'Xml',
316 'flx': 'Felix', 'b': 'Brainfuck', 'f': 'Fortran', 'rbw': 'Ruby',
317 '.htaccess': 'ApacheConf', 'cxx-objdump': 'CppObjdump', 'j': 'ObjectiveJ',
318 'mll': 'Ocaml', 'yml': 'Yaml', 'mu': 'MuPAD', 'r': 'Rebol', 'ASM': 'Nasm',
319 'erl': 'Erlang', 'mly': 'Ocaml', 'mo': 'Modelica', 'def': 'Modula2', 'ini':
320 'Ini', 'control': 'DebianControl', 'vb': 'VbNet', 'vapi': 'Vala', 'pro':
321 'Prolog', 'spt': 'Cheetah', 'mli': 'Ocaml', 'as': 'ActionScript3', 'cmd':
322 'Batch', 'cpp': 'Cpp', 'io': 'Io', 'tac': 'Python', 'haml': 'Haml', 'rkt':
323 'Racket', 'st':'Smalltalk', 'inc': 'Povray', 'pas': 'Delphi', 'cmake':
324 'CMake', 'csh':'Tcsh', 'hpp': 'Cpp', 'feature': 'Gherkin', 'html': 'Html',
325 'php':'Php', 'php3':'Php', 'php4':'Php', 'php5':'Php', 'xhtml': 'Html',
326 'hxx': 'Cpp', 'eclass': 'Bash', 'css': 'Css',
327 'frag': 'GLShader', 'd-objdump': 'DObjdump', 'weechatlog': 'IrcLogs',
328 'tcsh': 'Tcsh', 'objdump': 'Objdump', 'pyw': 'Python', 'h++': 'Cpp',
329 'py3tb': 'Python3Traceback', 'jsp': 'Jsp', 'sql': 'Sql', 'mak': 'Makefile',
330 'php': 'Php', 'mao': 'Mako', 'man': 'Groff', 'dylan': 'Dylan', 'sass':
331 'Sass', 'cfml': 'ColdfusionHtml', 'darcspatch': 'DarcsPatch', 'tpl':
332 'Smarty', 'm': 'ObjectiveC', 'f90': 'Fortran', 'mod': 'Modula2', 'sh':
333 'Bash', 'lhs': 'LiterateHaskell', 'sources.list': 'SourcesList', 'axd':
334 'VbNetAspx', 'sc': 'Python'}
335
336 repos_path = get_repos_path()
337 p = os.path.join(repos_path, repo_name)
338 repo = get_repo(p)
311 tip = repo.get_changeset()
339 tip = repo.get_changeset()
340 code_stats = {}
312
341
313 code_stats = {}
342 def aggregate(cs):
314 for topnode, dirs, files in tip.walk('/'):
343 for f in cs[2]:
315 for f in files:
344 ext = f.extension
316 k = f.mimetype
345 key = LANGUAGES_EXTENSIONS_MAP.get(ext, ext)
317 if f.extension in LANGUAGES_EXTENSIONS:
346 key = key or ext
318 if code_stats.has_key(k):
347 if ext in LANGUAGES_EXTENSIONS_MAP.keys() and not f.is_binary:
319 code_stats[k] += 1
348 if code_stats.has_key(key):
349 code_stats[key] += 1
320 else:
350 else:
321 code_stats[k] = 1
351 code_stats[key] = 1
352
353 map(aggregate, tip.walk('/'))
322
354
323 return code_stats or {}
355 return code_stats or {}
324
356
325
357
326
358
327
359
328
@@ -1,8 +1,16
1 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 # encoding: utf-8
2 """
3 # database management for RhodeCode
3 rhodecode.lib.db_manage
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 ~~~~~~~~~~~~~~~~~~~~~~~
5 #
5
6 Database creation, and setup module for RhodeCode. Used for creation
7 of database as well as for migration operations
8
9 :created_on: Apr 10, 2010
10 :author: marcink
11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
13 """
6 # This program is free software; you can redistribute it and/or
14 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
15 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
16 # as published by the Free Software Foundation; version 2
@@ -18,51 +26,50
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
27 # MA 02110-1301, USA.
20
28
21 """
22 Created on April 10, 2010
23 database management and creation for RhodeCode
24 @author: marcink
25 """
26
27 from os.path import dirname as dn, join as jn
28 import os
29 import os
29 import sys
30 import sys
30 import uuid
31 import uuid
32 import logging
33 from os.path import dirname as dn, join as jn
34
35 from rhodecode import __dbversion__
36 from rhodecode.model import meta
31
37
32 from rhodecode.lib.auth import get_crypt_password
38 from rhodecode.lib.auth import get_crypt_password
33 from rhodecode.lib.utils import ask_ok
39 from rhodecode.lib.utils import ask_ok
34 from rhodecode.model import init_model
40 from rhodecode.model import init_model
35 from rhodecode.model.db import User, Permission, RhodeCodeUi, RhodeCodeSettings, \
41 from rhodecode.model.db import User, Permission, RhodeCodeUi, RhodeCodeSettings, \
36 UserToPerm
42 UserToPerm, DbMigrateVersion
37 from rhodecode.model import meta
43
38 from sqlalchemy.engine import create_engine
44 from sqlalchemy.engine import create_engine
39 import logging
40
45
41 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
42
47
43 class DbManage(object):
48 class DbManage(object):
44 def __init__(self, log_sql, dbname, root, tests=False):
49 def __init__(self, log_sql, dbconf, root, tests=False):
45 self.dbname = dbname
50 self.dbname = dbconf.split('/')[-1]
46 self.tests = tests
51 self.tests = tests
47 self.root = root
52 self.root = root
48 dburi = 'sqlite:////%s' % jn(self.root, self.dbname)
53 self.dburi = dbconf
49 engine = create_engine(dburi, echo=log_sql)
54 engine = create_engine(self.dburi, echo=log_sql)
50 init_model(engine)
55 init_model(engine)
51 self.sa = meta.Session
56 self.sa = meta.Session()
52 self.db_exists = False
57 self.db_exists = False
53
58
54 def check_for_db(self, override):
59 def check_for_db(self, override):
55 db_path = jn(self.root, self.dbname)
60 db_path = jn(self.root, self.dbname)
61 if self.dburi.startswith('sqlite'):
56 log.info('checking for existing db in %s', db_path)
62 log.info('checking for existing db in %s', db_path)
57 if os.path.isfile(db_path):
63 if os.path.isfile(db_path):
64
58 self.db_exists = True
65 self.db_exists = True
59 if not override:
66 if not override:
60 raise Exception('database already exists')
67 raise Exception('database already exists')
61
68
62 def create_tables(self, override=False):
69 def create_tables(self, override=False):
70 """Create a auth database
63 """
71 """
64 Create a auth database
72
65 """
66 self.check_for_db(override)
73 self.check_for_db(override)
67 if self.db_exists:
74 if self.db_exists:
68 log.info("database exist and it's going to be destroyed")
75 log.info("database exist and it's going to be destroyed")
@@ -78,6 +85,135 class DbManage(object):
78 meta.Base.metadata.create_all(checkfirst=checkfirst)
85 meta.Base.metadata.create_all(checkfirst=checkfirst)
79 log.info('Created tables for %s', self.dbname)
86 log.info('Created tables for %s', self.dbname)
80
87
88
89
90 def set_db_version(self):
91 try:
92 ver = DbMigrateVersion()
93 ver.version = __dbversion__
94 ver.repository_id = 'rhodecode_db_migrations'
95 ver.repository_path = 'versions'
96 self.sa.add(ver)
97 self.sa.commit()
98 except:
99 self.sa.rollback()
100 raise
101 log.info('db version set to: %s', __dbversion__)
102
103
104 def upgrade(self):
105 """Upgrades given database schema to given revision following
106 all needed steps,
107
108 :param revision: revision to upgrade to
109 """
110
111 from rhodecode.lib.dbmigrate.migrate.versioning import api
112 from rhodecode.lib.dbmigrate.migrate.exceptions import \
113 DatabaseNotControlledError
114
115 upgrade = ask_ok('You are about to perform database upgrade, make '
116 'sure You backed up your database before. '
117 'Continue ? [y/n]')
118 if not upgrade:
119 sys.exit('Nothing done')
120
121 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
122 'rhodecode/lib/dbmigrate')
123 db_uri = self.dburi
124
125 try:
126 curr_version = api.db_version(db_uri, repository_path)
127 msg = ('Found current database under version'
128 ' control with version %s' % curr_version)
129
130 except (RuntimeError, DatabaseNotControlledError), e:
131 curr_version = 1
132 msg = ('Current database is not under version control. Setting'
133 ' as version %s' % curr_version)
134 api.version_control(db_uri, repository_path, curr_version)
135
136 print (msg)
137
138 if curr_version == __dbversion__:
139 sys.exit('This database is already at the newest version')
140
141 #======================================================================
142 # UPGRADE STEPS
143 #======================================================================
144 class UpgradeSteps(object):
145
146 def __init__(self, klass):
147 self.klass = klass
148
149 def step_0(self):
150 #step 0 is the schema upgrade, and than follow proper upgrades
151 print ('attempting to do database upgrade to version %s' \
152 % __dbversion__)
153 api.upgrade(db_uri, repository_path, __dbversion__)
154 print ('Schema upgrade completed')
155
156 def step_1(self):
157 pass
158
159 def step_2(self):
160 print ('Patching repo paths for newer version of RhodeCode')
161 self.klass.fix_repo_paths()
162
163 print ('Patching default user of RhodeCode')
164 self.klass.fix_default_user()
165
166 log.info('Changing ui settings')
167 self.klass.create_ui_settings()
168
169
170 upgrade_steps = [0] + range(curr_version + 1, __dbversion__ + 1)
171
172 #CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
173 for step in upgrade_steps:
174 print ('performing upgrade step %s' % step)
175 callable = getattr(UpgradeSteps(self), 'step_%s' % step)()
176
177
178
179 def fix_repo_paths(self):
180 """Fixes a old rhodecode version path into new one without a '*'
181 """
182
183 paths = self.sa.query(RhodeCodeUi)\
184 .filter(RhodeCodeUi.ui_key == '/')\
185 .scalar()
186
187 paths.ui_value = paths.ui_value.replace('*', '')
188
189 try:
190 self.sa.add(paths)
191 self.sa.commit()
192 except:
193 self.sa.rollback()
194 raise
195
196 def fix_default_user(self):
197 """Fixes a old default user with some 'nicer' default values,
198 used mostly for anonymous access
199 """
200 def_user = self.sa.query(User)\
201 .filter(User.username == 'default')\
202 .one()
203
204 def_user.name = 'Anonymous'
205 def_user.lastname = 'User'
206 def_user.email = 'anonymous@rhodecode.org'
207
208 try:
209 self.sa.add(def_user)
210 self.sa.commit()
211 except:
212 self.sa.rollback()
213 raise
214
215
216
81 def admin_prompt(self, second=False):
217 def admin_prompt(self, second=False):
82 if not self.tests:
218 if not self.tests:
83 import getpass
219 import getpass
@@ -113,7 +249,72 class DbManage(object):
113 self.create_user('test_regular', 'test12', 'test_regular@mail.com', False)
249 self.create_user('test_regular', 'test12', 'test_regular@mail.com', False)
114 self.create_user('test_regular2', 'test12', 'test_regular2@mail.com', False)
250 self.create_user('test_regular2', 'test12', 'test_regular2@mail.com', False)
115
251
252 def create_ui_settings(self):
253 """Creates ui settings, fills out hooks
254 and disables dotencode
116
255
256 """
257 #HOOKS
258 hooks1_key = 'changegroup.update'
259 hooks1_ = self.sa.query(RhodeCodeUi)\
260 .filter(RhodeCodeUi.ui_key == hooks1_key).scalar()
261
262 hooks1 = RhodeCodeUi() if hooks1_ is None else hooks1_
263 hooks1.ui_section = 'hooks'
264 hooks1.ui_key = hooks1_key
265 hooks1.ui_value = 'hg update >&2'
266 hooks1.ui_active = False
267
268 hooks2_key = 'changegroup.repo_size'
269 hooks2_ = self.sa.query(RhodeCodeUi)\
270 .filter(RhodeCodeUi.ui_key == hooks2_key).scalar()
271
272 hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_
273 hooks2.ui_section = 'hooks'
274 hooks2.ui_key = hooks2_key
275 hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size'
276
277 hooks3 = RhodeCodeUi()
278 hooks3.ui_section = 'hooks'
279 hooks3.ui_key = 'pretxnchangegroup.push_logger'
280 hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action'
281
282 hooks4 = RhodeCodeUi()
283 hooks4.ui_section = 'hooks'
284 hooks4.ui_key = 'preoutgoing.pull_logger'
285 hooks4.ui_value = 'python:rhodecode.lib.hooks.log_pull_action'
286
287 #For mercurial 1.7 set backward comapatibility with format
288 dotencode_disable = RhodeCodeUi()
289 dotencode_disable.ui_section = 'format'
290 dotencode_disable.ui_key = 'dotencode'
291 dotencode_disable.ui_value = 'false'
292
293 try:
294 self.sa.add(hooks1)
295 self.sa.add(hooks2)
296 self.sa.add(hooks3)
297 self.sa.add(hooks4)
298 self.sa.add(dotencode_disable)
299 self.sa.commit()
300 except:
301 self.sa.rollback()
302 raise
303
304
305 def create_ldap_options(self):
306 """Creates ldap settings"""
307
308 try:
309 for k in ['ldap_active', 'ldap_host', 'ldap_port', 'ldap_ldaps',
310 'ldap_dn_user', 'ldap_dn_pass', 'ldap_base_dn']:
311
312 setting = RhodeCodeSettings(k, '')
313 self.sa.add(setting)
314 self.sa.commit()
315 except:
316 self.sa.rollback()
317 raise
117
318
118 def config_prompt(self, test_repo_path=''):
319 def config_prompt(self, test_repo_path=''):
119 log.info('Setting up repositories config')
320 log.info('Setting up repositories config')
@@ -128,17 +329,9 class DbManage(object):
128 log.error('You entered wrong path: %s', path)
329 log.error('You entered wrong path: %s', path)
129 sys.exit()
330 sys.exit()
130
331
131 hooks1 = RhodeCodeUi()
332 self.create_ui_settings()
132 hooks1.ui_section = 'hooks'
133 hooks1.ui_key = 'changegroup.update'
134 hooks1.ui_value = 'hg update >&2'
135 hooks1.ui_active = False
136
333
137 hooks2 = RhodeCodeUi()
334 #HG UI OPTIONS
138 hooks2.ui_section = 'hooks'
139 hooks2.ui_key = 'changegroup.repo_size'
140 hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size'
141
142 web1 = RhodeCodeUi()
335 web1 = RhodeCodeUi()
143 web1.ui_section = 'web'
336 web1.ui_section = 'web'
144 web1.ui_key = 'push_ssl'
337 web1.ui_key = 'push_ssl'
@@ -162,21 +355,14 class DbManage(object):
162 paths = RhodeCodeUi()
355 paths = RhodeCodeUi()
163 paths.ui_section = 'paths'
356 paths.ui_section = 'paths'
164 paths.ui_key = '/'
357 paths.ui_key = '/'
165 paths.ui_value = os.path.join(path, '*')
358 paths.ui_value = path
166
359
167
360
168 hgsettings1 = RhodeCodeSettings()
361 hgsettings1 = RhodeCodeSettings('realm', 'RhodeCode authentication')
362 hgsettings2 = RhodeCodeSettings('title', 'RhodeCode')
169
363
170 hgsettings1.app_settings_name = 'realm'
171 hgsettings1.app_settings_value = 'RhodeCode authentication'
172
173 hgsettings2 = RhodeCodeSettings()
174 hgsettings2.app_settings_name = 'title'
175 hgsettings2.app_settings_value = 'RhodeCode'
176
364
177 try:
365 try:
178 self.sa.add(hooks1)
179 self.sa.add(hooks2)
180 self.sa.add(web1)
366 self.sa.add(web1)
181 self.sa.add(web2)
367 self.sa.add(web2)
182 self.sa.add(web3)
368 self.sa.add(web3)
@@ -184,10 +370,14 class DbManage(object):
184 self.sa.add(paths)
370 self.sa.add(paths)
185 self.sa.add(hgsettings1)
371 self.sa.add(hgsettings1)
186 self.sa.add(hgsettings2)
372 self.sa.add(hgsettings2)
373
187 self.sa.commit()
374 self.sa.commit()
188 except:
375 except:
189 self.sa.rollback()
376 self.sa.rollback()
190 raise
377 raise
378
379 self.create_ldap_options()
380
191 log.info('created ui config')
381 log.info('created ui config')
192
382
193 def create_user(self, username, password, email='', admin=False):
383 def create_user(self, username, password, email='', admin=False):
@@ -214,9 +404,9 class DbManage(object):
214 def_user = User()
404 def_user = User()
215 def_user.username = 'default'
405 def_user.username = 'default'
216 def_user.password = get_crypt_password(str(uuid.uuid1())[:8])
406 def_user.password = get_crypt_password(str(uuid.uuid1())[:8])
217 def_user.name = 'default'
407 def_user.name = 'Anonymous'
218 def_user.lastname = 'default'
408 def_user.lastname = 'User'
219 def_user.email = 'default@default.com'
409 def_user.email = 'anonymous@rhodecode.org'
220 def_user.admin = False
410 def_user.admin = False
221 def_user.active = False
411 def_user.active = False
222 try:
412 try:
@@ -3,6 +3,8
3 Consists of functions to typically be used within templates, but also
3 Consists of functions to typically be used within templates, but also
4 available to Controllers. This module is available to both as 'h'.
4 available to Controllers. This module is available to both as 'h'.
5 """
5 """
6 import random
7 import hashlib
6 from pygments.formatters import HtmlFormatter
8 from pygments.formatters import HtmlFormatter
7 from pygments import highlight as code_highlight
9 from pygments import highlight as code_highlight
8 from pylons import url, app_globals as g
10 from pylons import url, app_globals as g
@@ -23,6 +25,36 from webhelpers.pylonslib.secure_form im
23 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
25 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
24 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
26 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
25 replace_whitespace, urlify, truncate, wrap_paragraphs
27 replace_whitespace, urlify, truncate, wrap_paragraphs
28 from webhelpers.date import time_ago_in_words
29
30 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
31 convert_boolean_attrs, NotGiven
32
33 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
34 _set_input_attrs(attrs, type, name, value)
35 _set_id_attr(attrs, id, name)
36 convert_boolean_attrs(attrs, ["disabled"])
37 return HTML.input(**attrs)
38
39 reset = _reset
40
41
42 def get_token():
43 """Return the current authentication token, creating one if one doesn't
44 already exist.
45 """
46 token_key = "_authentication_token"
47 from pylons import session
48 if not token_key in session:
49 try:
50 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
51 except AttributeError: # Python < 2.4
52 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
53 session[token_key] = token
54 if hasattr(session, 'save'):
55 session.save()
56 return session[token_key]
57
26
58
27 #Custom helpers here :)
59 #Custom helpers here :)
28 class _Link(object):
60 class _Link(object):
@@ -93,7 +125,7 class _ToolTip(object):
93 var tts = YAHOO.util.Dom.getElementsByClassName('tooltip');
125 var tts = YAHOO.util.Dom.getElementsByClassName('tooltip');
94
126
95 for (var i = 0; i < tts.length; i++) {
127 for (var i = 0; i < tts.length; i++) {
96 //if element doesn not have and id autgenerate one for tooltip
128 //if element doesn't not have and id autgenerate one for tooltip
97
129
98 if (!tts[i].id){
130 if (!tts[i].id){
99 tts[i].id='tt'+i*100;
131 tts[i].id='tt'+i*100;
@@ -111,7 +143,7 class _ToolTip(object):
111 showdelay:20,
143 showdelay:20,
112 });
144 });
113
145
114 //Mouse Over event disabled for new repositories since they dont
146 //Mouse Over event disabled for new repositories since they don't
115 //have last commit message
147 //have last commit message
116 myToolTips.contextMouseOverEvent.subscribe(
148 myToolTips.contextMouseOverEvent.subscribe(
117 function(type, args) {
149 function(type, args) {
@@ -270,13 +302,13 def pygmentize_annotation(filenode, **kw
270 tooltip_html = tooltip_html % (changeset.author,
302 tooltip_html = tooltip_html % (changeset.author,
271 changeset.date,
303 changeset.date,
272 tooltip(changeset.message))
304 tooltip(changeset.message))
273 lnk_format = 'r%-5s:%s' % (changeset.revision,
305 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
274 changeset.short_id)
306 short_id(changeset.raw_id))
275 uri = link_to(
307 uri = link_to(
276 lnk_format,
308 lnk_format,
277 url('changeset_home', repo_name=changeset.repository.name,
309 url('changeset_home', repo_name=changeset.repository.name,
278 revision=changeset.short_id),
310 revision=changeset.raw_id),
279 style=get_color_string(changeset.short_id),
311 style=get_color_string(changeset.raw_id),
280 class_='tooltip',
312 class_='tooltip',
281 tooltip_title=tooltip_html
313 tooltip_title=tooltip_html
282 )
314 )
@@ -317,37 +349,168 def get_changeset_safe(repo, rev):
317 flash = _Flash()
349 flash = _Flash()
318
350
319
351
320 #===============================================================================
352 #==============================================================================
321 # MERCURIAL FILTERS available via h.
353 # MERCURIAL FILTERS available via h.
322 #===============================================================================
354 #==============================================================================
323 from mercurial import util
355 from mercurial import util
324 from mercurial.templatefilters import age as _age, person as _person
356 from mercurial.templatefilters import person as _person
357
358
359
360 def _age(curdate):
361 """turns a datetime into an age string."""
362
363 if not curdate:
364 return ''
365
366 from datetime import timedelta, datetime
367
368 agescales = [("year", 3600 * 24 * 365),
369 ("month", 3600 * 24 * 30),
370 ("day", 3600 * 24),
371 ("hour", 3600),
372 ("minute", 60),
373 ("second", 1), ]
374
375 age = datetime.now() - curdate
376 age_seconds = (age.days * agescales[2][1]) + age.seconds
377 pos = 1
378 for scale in agescales:
379 if scale[1] <= age_seconds:
380 if pos == 6:pos = 5
381 return time_ago_in_words(curdate, agescales[pos][0]) + ' ' + _('ago')
382 pos += 1
383
384 return _('just now')
325
385
326 age = lambda x:_age(x)
386 age = lambda x:_age(x)
327 capitalize = lambda x: x.capitalize()
387 capitalize = lambda x: x.capitalize()
328 date = lambda x: util.datestr(x)
329 email = util.email
388 email = util.email
330 email_or_none = lambda x: util.email(x) if util.email(x) != x else None
389 email_or_none = lambda x: util.email(x) if util.email(x) != x else None
331 person = lambda x: _person(x)
390 person = lambda x: _person(x)
332 hgdate = lambda x: "%d %d" % x
391 short_id = lambda x: x[:12]
333 isodate = lambda x: util.datestr(x, '%Y-%m-%d %H:%M %1%2')
392
334 isodatesec = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2')
393
335 localdate = lambda x: (x[0], util.makedate()[1])
394 def bool2icon(value):
336 rfc822date = lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S %1%2")
395 """
337 rfc822date_notz = lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S")
396 Returns True/False values represented as small html image of true/false
338 rfc3339date = lambda x: util.datestr(x, "%Y-%m-%dT%H:%M:%S%1:%2")
397 icons
339 time_ago = lambda x: util.datestr(_age(x), "%a, %d %b %Y %H:%M:%S %1%2")
398 :param value: bool value
399 """
400
401 if value is True:
402 return HTML.tag('img', src="/images/icons/accept.png", alt=_('True'))
403
404 if value is False:
405 return HTML.tag('img', src="/images/icons/cancel.png", alt=_('False'))
406
407 return value
340
408
341
409
342 #===============================================================================
410 def action_parser(user_log):
411 """
412 This helper will map the specified string action into translated
413 fancy names with icons and links
414
415 @param action:
416 """
417 action = user_log.action
418 action_params = ' '
419
420 x = action.split(':')
421
422 if len(x) > 1:
423 action, action_params = x
424
425 def get_cs_links():
426 if action == 'push':
427 revs_limit = 5
428 revs = action_params.split(',')
429 cs_links = " " + ', '.join ([link(rev,
430 url('changeset_home',
431 repo_name=user_log.repository.repo_name,
432 revision=rev)) for rev in revs[:revs_limit] ])
433 if len(revs) > revs_limit:
434 uniq_id = revs[0]
435 html_tmpl = ('<span> %s '
436 '<a class="show_more" id="_%s" href="#">%s</a> '
437 '%s</span>')
438 cs_links += html_tmpl % (_('and'), uniq_id, _('%s more') \
439 % (len(revs) - revs_limit),
440 _('revisions'))
441
442 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
443 cs_links += html_tmpl % (uniq_id, ', '.join([link(rev,
444 url('changeset_home',
445 repo_name=user_log.repository.repo_name,
446 revision=rev)) for rev in revs[:revs_limit] ]))
447
448 return cs_links
449 return ''
450
451 def get_fork_name():
452 if action == 'user_forked_repo':
453 from rhodecode.model.scm import ScmModel
454 repo_name = action_params
455 repo = ScmModel().get(repo_name)
456 if repo is None:
457 return repo_name
458 return link_to(action_params, url('summary_home',
459 repo_name=repo.name,),
460 title=repo.dbrepo.description)
461 return ''
462 map = {'user_deleted_repo':_('User [deleted] repository'),
463 'user_created_repo':_('User [created] repository'),
464 'user_forked_repo':_('User [forked] repository as: %s') % get_fork_name(),
465 'user_updated_repo':_('User [updated] repository'),
466 'admin_deleted_repo':_('Admin [delete] repository'),
467 'admin_created_repo':_('Admin [created] repository'),
468 'admin_forked_repo':_('Admin [forked] repository'),
469 'admin_updated_repo':_('Admin [updated] repository'),
470 'push':_('[Pushed] %s') % get_cs_links(),
471 'pull':_('[Pulled]'),
472 'started_following_repo':_('User [started following] repository'),
473 'stopped_following_repo':_('User [stopped following] repository'),
474 }
475
476 action_str = map.get(action, action)
477 return literal(action_str.replace('[', '<span class="journal_highlight">')\
478 .replace(']', '</span>'))
479
480 def action_parser_icon(user_log):
481 action = user_log.action
482 action_params = None
483 x = action.split(':')
484
485 if len(x) > 1:
486 action, action_params = x
487
488 tmpl = """<img src="/images/icons/%s" alt="%s"/>"""
489 map = {'user_deleted_repo':'database_delete.png',
490 'user_created_repo':'database_add.png',
491 'user_forked_repo':'arrow_divide.png',
492 'user_updated_repo':'database_edit.png',
493 'admin_deleted_repo':'database_delete.png',
494 'admin_created_repo':'database_ddd.png',
495 'admin_forked_repo':'arrow_divide.png',
496 'admin_updated_repo':'database_edit.png',
497 'push':'script_add.png',
498 'pull':'down_16.png',
499 'started_following_repo':'heart_add.png',
500 'stopped_following_repo':'heart_delete.png',
501 }
502 return literal(tmpl % (map.get(action, action), action))
503
504
505 #==============================================================================
343 # PERMS
506 # PERMS
344 #===============================================================================
507 #==============================================================================
345 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
508 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
346 HasRepoPermissionAny, HasRepoPermissionAll
509 HasRepoPermissionAny, HasRepoPermissionAll
347
510
348 #===============================================================================
511 #==============================================================================
349 # GRAVATAR URL
512 # GRAVATAR URL
350 #===============================================================================
513 #==============================================================================
351 import hashlib
514 import hashlib
352 import urllib
515 import urllib
353 from pylons import request
516 from pylons import request
@@ -22,12 +22,12 Created on Aug 6, 2010
22
22
23 @author: marcink
23 @author: marcink
24 """
24 """
25
25 from mercurial.cmdutil import revrange
26 import sys
26 from mercurial.node import nullrev
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.utils import action_logger
27 import os
29 import os
28 from rhodecode.lib import helpers as h
30 import sys
29 from rhodecode.model import meta
30 from rhodecode.model.db import UserLog, User
31
31
32 def repo_size(ui, repo, hooktype=None, **kwargs):
32 def repo_size(ui, repo, hooktype=None, **kwargs):
33
33
@@ -54,31 +54,52 def repo_size(ui, repo, hooktype=None, *
54 sys.stdout.write('Repository size .hg:%s repo:%s total:%s\n' \
54 sys.stdout.write('Repository size .hg:%s repo:%s total:%s\n' \
55 % (size_hg_f, size_root_f, size_total_f))
55 % (size_hg_f, size_root_f, size_total_f))
56
56
57 user_action_mapper(ui, repo, hooktype, **kwargs)
57 def log_pull_action(ui, repo, **kwargs):
58 """
59 Logs user last pull action
60 :param ui:
61 :param repo:
62 """
58
63
59 def user_action_mapper(ui, repo, hooktype=None, **kwargs):
64 extra_params = dict(repo.ui.configitems('rhodecode_extras'))
65 username = extra_params['username']
66 repository = extra_params['repository']
67 action = 'pull'
68
69 action_logger(username, action, repository, extra_params['ip'])
70
71 return 0
72
73 def log_push_action(ui, repo, **kwargs):
60 """
74 """
61 Maps user last push action to new changeset id, from mercurial
75 Maps user last push action to new changeset id, from mercurial
62 :param ui:
76 :param ui:
63 :param repo:
77 :param repo:
64 :param hooktype:
65 """
78 """
66
79
67 try:
80 extra_params = dict(repo.ui.configitems('rhodecode_extras'))
68 sa = meta.Session
81 username = extra_params['username']
69 username = kwargs['url'].split(':')[-1]
82 repository = extra_params['repository']
70 user_log = sa.query(UserLog)\
83 action = 'push:%s'
71 .filter(UserLog.user == sa.query(User)\
84 node = kwargs['node']
72 .filter(User.username == username).one())\
85
73 .order_by(UserLog.user_log_id.desc()).first()
86 def get_revs(repo, rev_opt):
87 if rev_opt:
88 revs = revrange(repo, rev_opt)
74
89
75 if user_log and not user_log.revision:
90 if len(revs) == 0:
76 user_log.revision = str(repo['tip'])
91 return (nullrev, nullrev)
77 sa.add(user_log)
92 return (max(revs), min(revs))
78 sa.commit()
93 else:
94 return (len(repo) - 1, 0)
95
96 stop, start = get_revs(repo, [node + ':'])
79
97
80 except Exception, e:
98 revs = (str(repo[r]) for r in xrange(start, stop + 1))
81 sa.rollback()
99
82 raise
100 action = action % ','.join(revs)
83 finally:
101
84 meta.Session.remove()
102 action_logger(username, action, repository, extra_params['ip'])
103
104 return 0
105
@@ -1,26 +1,28
1 import os
2 import sys
3 import traceback
1 from os.path import dirname as dn, join as jn
4 from os.path import dirname as dn, join as jn
5
6 #to get the rhodecode import
7 sys.path.append(dn(dn(dn(os.path.realpath(__file__)))))
8
9 from rhodecode.model import init_model
10 from rhodecode.model.scm import ScmModel
2 from rhodecode.config.environment import load_environment
11 from rhodecode.config.environment import load_environment
3 from rhodecode.model.hg_model import HgModel
12 from rhodecode.lib.utils import BasePasterCommand, Command, add_cache
13
4 from shutil import rmtree
14 from shutil import rmtree
5 from webhelpers.html.builder import escape
15 from webhelpers.html.builder import escape
6 from vcs.utils.lazy import LazyProperty
16 from vcs.utils.lazy import LazyProperty
7
17
18 from sqlalchemy import engine_from_config
19
8 from whoosh.analysis import RegexTokenizer, LowercaseFilter, StopFilter
20 from whoosh.analysis import RegexTokenizer, LowercaseFilter, StopFilter
9 from whoosh.fields import TEXT, ID, STORED, Schema, FieldType
21 from whoosh.fields import TEXT, ID, STORED, Schema, FieldType
10 from whoosh.index import create_in, open_dir
22 from whoosh.index import create_in, open_dir
11 from whoosh.formats import Characters
23 from whoosh.formats import Characters
12 from whoosh.highlight import highlight, SimpleFragmenter, HtmlFormatter
24 from whoosh.highlight import highlight, SimpleFragmenter, HtmlFormatter
13
25
14 import os
15 import sys
16 import traceback
17
18 #to get the rhodecode import
19 sys.path.append(dn(dn(dn(os.path.realpath(__file__)))))
20
21
22 #LOCATION WE KEEP THE INDEX
23 IDX_LOCATION = jn(dn(dn(dn(dn(os.path.abspath(__file__))))), 'data', 'index')
24
26
25 #EXTENSIONS WE WANT TO INDEX CONTENT OFF
27 #EXTENSIONS WE WANT TO INDEX CONTENT OFF
26 INDEX_EXTENSIONS = ['action', 'adp', 'ashx', 'asmx', 'aspx', 'asx', 'axd', 'c',
28 INDEX_EXTENSIONS = ['action', 'adp', 'ashx', 'asmx', 'aspx', 'asx', 'axd', 'c',
@@ -48,6 +50,55 IDX_NAME = 'HG_INDEX'
48 FORMATTER = HtmlFormatter('span', between='\n<span class="break">...</span>\n')
50 FORMATTER = HtmlFormatter('span', between='\n<span class="break">...</span>\n')
49 FRAGMENTER = SimpleFragmenter(200)
51 FRAGMENTER = SimpleFragmenter(200)
50
52
53
54 class MakeIndex(BasePasterCommand):
55
56 max_args = 1
57 min_args = 1
58
59 usage = "CONFIG_FILE"
60 summary = "Creates index for full text search given configuration file"
61 group_name = "RhodeCode"
62 takes_config_file = -1
63 parser = Command.standard_parser(verbose=True)
64
65 def command(self):
66
67 from pylons import config
68 add_cache(config)
69 engine = engine_from_config(config, 'sqlalchemy.db1.')
70 init_model(engine)
71
72 index_location = config['index_dir']
73 repo_location = self.options.repo_location
74
75 #======================================================================
76 # WHOOSH DAEMON
77 #======================================================================
78 from rhodecode.lib.pidlock import LockHeld, DaemonLock
79 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
80 try:
81 l = DaemonLock()
82 WhooshIndexingDaemon(index_location=index_location,
83 repo_location=repo_location)\
84 .run(full_index=self.options.full_index)
85 l.release()
86 except LockHeld:
87 sys.exit(1)
88
89 def update_parser(self):
90 self.parser.add_option('--repo-location',
91 action='store',
92 dest='repo_location',
93 help="Specifies repositories location to index REQUIRED",
94 )
95 self.parser.add_option('-f',
96 action='store_true',
97 dest='full_index',
98 help="Specifies that index should be made full i.e"
99 " destroy old and build from scratch",
100 default=False)
101
51 class ResultWrapper(object):
102 class ResultWrapper(object):
52 def __init__(self, search_type, searcher, matcher, highlight_items):
103 def __init__(self, search_type, searcher, matcher, highlight_items):
53 self.search_type = search_type
104 self.search_type = search_type
@@ -115,8 +166,8 class ResultWrapper(object):
115 Smart function that implements chunking the content
166 Smart function that implements chunking the content
116 but not overlap chunks so it doesn't highlight the same
167 but not overlap chunks so it doesn't highlight the same
117 close occurrences twice.
168 close occurrences twice.
118 :param matcher:
169 @param matcher:
119 :param size:
170 @param size:
120 """
171 """
121 memory = [(0, 0)]
172 memory = [(0, 0)]
122 for span in self.matcher.spans():
173 for span in self.matcher.spans():
@@ -32,12 +32,12 from os.path import join as jn
32 project_path = dn(dn(dn(dn(os.path.realpath(__file__)))))
32 project_path = dn(dn(dn(dn(os.path.realpath(__file__)))))
33 sys.path.append(project_path)
33 sys.path.append(project_path)
34
34
35 from rhodecode.lib.pidlock import LockHeld, DaemonLock
35
36 from rhodecode.model.hg_model import HgModel
36 from rhodecode.model.scm import ScmModel
37 from rhodecode.lib.helpers import safe_unicode
37 from rhodecode.lib.helpers import safe_unicode
38 from whoosh.index import create_in, open_dir
38 from whoosh.index import create_in, open_dir
39 from shutil import rmtree
39 from shutil import rmtree
40 from rhodecode.lib.indexers import INDEX_EXTENSIONS, IDX_LOCATION, SCHEMA, IDX_NAME
40 from rhodecode.lib.indexers import INDEX_EXTENSIONS, SCHEMA, IDX_NAME
41
41
42 from time import mktime
42 from time import mktime
43 from vcs.exceptions import ChangesetError, RepositoryError
43 from vcs.exceptions import ChangesetError, RepositoryError
@@ -61,35 +61,38 ch.setFormatter(formatter)
61 # add ch to logger
61 # add ch to logger
62 log.addHandler(ch)
62 log.addHandler(ch)
63
63
64 def scan_paths(root_location):
65 return HgModel.repo_scan('/', root_location, None, True)
66
67 class WhooshIndexingDaemon(object):
64 class WhooshIndexingDaemon(object):
68 """
65 """
69 Deamon for atomic jobs
66 Deamon for atomic jobs
70 """
67 """
71
68
72 def __init__(self, indexname='HG_INDEX', repo_location=None):
69 def __init__(self, indexname='HG_INDEX', index_location=None,
70 repo_location=None, sa=None):
73 self.indexname = indexname
71 self.indexname = indexname
72
73 self.index_location = index_location
74 if not index_location:
75 raise Exception('You have to provide index location')
76
74 self.repo_location = repo_location
77 self.repo_location = repo_location
75 self.repo_paths = scan_paths(self.repo_location)
78 if not repo_location:
79 raise Exception('You have to provide repositories location')
80
81 self.repo_paths = ScmModel(sa).repo_scan(self.repo_location, None)
76 self.initial = False
82 self.initial = False
77 if not os.path.isdir(IDX_LOCATION):
83 if not os.path.isdir(self.index_location):
78 os.mkdir(IDX_LOCATION)
84 os.makedirs(self.index_location)
79 log.info('Cannot run incremental index since it does not'
85 log.info('Cannot run incremental index since it does not'
80 ' yet exist running full build')
86 ' yet exist running full build')
81 self.initial = True
87 self.initial = True
82
88
83 def get_paths(self, repo):
89 def get_paths(self, repo):
84 """
90 """recursive walk in root dir and return a set of all path in that dir
85 recursive walk in root dir and return a set of all path in that dir
86 based on repository walk function
91 based on repository walk function
87 """
92 """
88 index_paths_ = set()
93 index_paths_ = set()
89 try:
94 try:
90 tip = repo.get_changeset()
95 for topnode, dirs, files in repo.walk('/', 'tip'):
91
92 for topnode, dirs, files in tip.walk('/'):
93 for f in files:
96 for f in files:
94 index_paths_.add(jn(repo.path, f.path))
97 index_paths_.add(jn(repo.path, f.path))
95 for dir in dirs:
98 for dir in dirs:
@@ -109,7 +112,8 class WhooshIndexingDaemon(object):
109 return mktime(node.last_changeset.date.timetuple())
112 return mktime(node.last_changeset.date.timetuple())
110
113
111 def add_doc(self, writer, path, repo):
114 def add_doc(self, writer, path, repo):
112 """Adding doc to writer"""
115 """Adding doc to writer this function itself fetches data from
116 the instance of vcs backend"""
113 node = self.get_node(repo, path)
117 node = self.get_node(repo, path)
114
118
115 #we just index the content of chosen files
119 #we just index the content of chosen files
@@ -130,14 +134,14 class WhooshIndexingDaemon(object):
130
134
131
135
132 def build_index(self):
136 def build_index(self):
133 if os.path.exists(IDX_LOCATION):
137 if os.path.exists(self.index_location):
134 log.debug('removing previous index')
138 log.debug('removing previous index')
135 rmtree(IDX_LOCATION)
139 rmtree(self.index_location)
136
140
137 if not os.path.exists(IDX_LOCATION):
141 if not os.path.exists(self.index_location):
138 os.mkdir(IDX_LOCATION)
142 os.mkdir(self.index_location)
139
143
140 idx = create_in(IDX_LOCATION, SCHEMA, indexname=IDX_NAME)
144 idx = create_in(self.index_location, SCHEMA, indexname=IDX_NAME)
141 writer = idx.writer()
145 writer = idx.writer()
142
146
143 for cnt, repo in enumerate(self.repo_paths.values()):
147 for cnt, repo in enumerate(self.repo_paths.values()):
@@ -154,7 +158,7 class WhooshIndexingDaemon(object):
154 def update_index(self):
158 def update_index(self):
155 log.debug('STARTING INCREMENTAL INDEXING UPDATE')
159 log.debug('STARTING INCREMENTAL INDEXING UPDATE')
156
160
157 idx = open_dir(IDX_LOCATION, indexname=self.indexname)
161 idx = open_dir(self.index_location, indexname=self.indexname)
158 # The set of all paths in the index
162 # The set of all paths in the index
159 indexed_paths = set()
163 indexed_paths = set()
160 # The set of all paths we need to re-index
164 # The set of all paths we need to re-index
@@ -209,40 +213,3 class WhooshIndexingDaemon(object):
209 self.build_index()
213 self.build_index()
210 else:
214 else:
211 self.update_index()
215 self.update_index()
212
213 if __name__ == "__main__":
214 arg = sys.argv[1:]
215 if len(arg) != 2:
216 sys.stderr.write('Please specify indexing type [full|incremental]'
217 'and path to repositories as script args \n')
218 sys.exit()
219
220
221 if arg[0] == 'full':
222 full_index = True
223 elif arg[0] == 'incremental':
224 # False means looking just for changes
225 full_index = False
226 else:
227 sys.stdout.write('Please use [full|incremental]'
228 ' as script first arg \n')
229 sys.exit()
230
231 if not os.path.isdir(arg[1]):
232 sys.stderr.write('%s is not a valid path \n' % arg[1])
233 sys.exit()
234 else:
235 if arg[1].endswith('/'):
236 repo_location = arg[1] + '*'
237 else:
238 repo_location = arg[1] + '/*'
239
240 try:
241 l = DaemonLock()
242 WhooshIndexingDaemon(repo_location=repo_location)\
243 .run(full_index=full_index)
244 l.release()
245 reload(logging)
246 except LockHeld:
247 sys.exit(1)
248
@@ -17,6 +17,14
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
19 # MA 02110-1301, USA.
20 """
21 Created on 2010-04-28
22
23 @author: marcink
24 SimpleGit middleware for handling git protocol request (push/clone etc.)
25 It's implemented with basic auth function
26 """
27
20 from dulwich import server as dulserver
28 from dulwich import server as dulserver
21
29
22 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
30 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
@@ -54,26 +62,28 from dulwich.repo import Repo
54 from dulwich.web import HTTPGitApplication
62 from dulwich.web import HTTPGitApplication
55 from paste.auth.basic import AuthBasicAuthenticator
63 from paste.auth.basic import AuthBasicAuthenticator
56 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
64 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
57 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware, \
65 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
58 get_user_cached
66 from rhodecode.lib.utils import invalidate_cache, check_repo_fast
59 from rhodecode.lib.utils import action_logger, is_git, invalidate_cache, \
67 from rhodecode.model.user import UserModel
60 check_repo_fast
61 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
68 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
62 import logging
69 import logging
63 import os
70 import os
64 import traceback
71 import traceback
65 """
66 Created on 2010-04-28
67
68 @author: marcink
69 SimpleGit middleware for handling git protocol request (push/clone etc.)
70 It's implemented with basic auth function
71 """
72
73
74
72
75 log = logging.getLogger(__name__)
73 log = logging.getLogger(__name__)
76
74
75 def is_git(environ):
76 """
77 Returns True if request's target is git server. ``HTTP_USER_AGENT`` would
78 then have git client version given.
79
80 :param environ:
81 """
82 http_user_agent = environ.get('HTTP_USER_AGENT')
83 if http_user_agent and http_user_agent.startswith('git'):
84 return True
85 return False
86
77 class SimpleGit(object):
87 class SimpleGit(object):
78
88
79 def __init__(self, application, config):
89 def __init__(self, application, config):
@@ -81,11 +91,19 class SimpleGit(object):
81 self.config = config
91 self.config = config
82 #authenticate this git request using
92 #authenticate this git request using
83 self.authenticate = AuthBasicAuthenticator('', authfunc)
93 self.authenticate = AuthBasicAuthenticator('', authfunc)
94 self.ipaddr = '0.0.0.0'
95 self.repository = None
96 self.username = None
97 self.action = None
84
98
85 def __call__(self, environ, start_response):
99 def __call__(self, environ, start_response):
86 if not is_git(environ):
100 if not is_git(environ):
87 return self.application(environ, start_response)
101 return self.application(environ, start_response)
88
102
103 proxy_key = 'HTTP_X_REAL_IP'
104 def_key = 'REMOTE_ADDR'
105 self.ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
106
89 #===================================================================
107 #===================================================================
90 # AUTHENTICATE THIS GIT REQUEST
108 # AUTHENTICATE THIS GIT REQUEST
91 #===================================================================
109 #===================================================================
@@ -99,10 +117,14 class SimpleGit(object):
99 else:
117 else:
100 return result.wsgi_application(environ, start_response)
118 return result.wsgi_application(environ, start_response)
101
119
120 #=======================================================================
121 # GET REPOSITORY
122 #=======================================================================
102 try:
123 try:
103 self.repo_name = environ['PATH_INFO'].split('/')[1]
124 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
104 if self.repo_name.endswith('/'):
125 if repo_name.endswith('/'):
105 self.repo_name = self.repo_name.rstrip('/')
126 repo_name = repo_name.rstrip('/')
127 self.repository = repo_name
106 except:
128 except:
107 log.error(traceback.format_exc())
129 log.error(traceback.format_exc())
108 return HTTPInternalServerError()(environ, start_response)
130 return HTTPInternalServerError()(environ, start_response)
@@ -110,20 +132,21 class SimpleGit(object):
110 #===================================================================
132 #===================================================================
111 # CHECK PERMISSIONS FOR THIS REQUEST
133 # CHECK PERMISSIONS FOR THIS REQUEST
112 #===================================================================
134 #===================================================================
113 action = self.__get_action(environ)
135 self.action = self.__get_action(environ)
114 if action:
136 if self.action:
115 username = self.__get_environ_user(environ)
137 username = self.__get_environ_user(environ)
116 try:
138 try:
117 user = self.__get_user(username)
139 user = self.__get_user(username)
140 self.username = user.username
118 except:
141 except:
119 log.error(traceback.format_exc())
142 log.error(traceback.format_exc())
120 return HTTPInternalServerError()(environ, start_response)
143 return HTTPInternalServerError()(environ, start_response)
121
144
122 #check permissions for this repository
145 #check permissions for this repository
123 if action == 'push':
146 if self.action == 'push':
124 if not HasPermissionAnyMiddleware('repository.write',
147 if not HasPermissionAnyMiddleware('repository.write',
125 'repository.admin')\
148 'repository.admin')\
126 (user, self.repo_name):
149 (user, repo_name):
127 return HTTPForbidden()(environ, start_response)
150 return HTTPForbidden()(environ, start_response)
128
151
129 else:
152 else:
@@ -131,15 +154,13 class SimpleGit(object):
131 if not HasPermissionAnyMiddleware('repository.read',
154 if not HasPermissionAnyMiddleware('repository.read',
132 'repository.write',
155 'repository.write',
133 'repository.admin')\
156 'repository.admin')\
134 (user, self.repo_name):
157 (user, repo_name):
135 return HTTPForbidden()(environ, start_response)
158 return HTTPForbidden()(environ, start_response)
136
159
137 #log action
160 self.extras = {'ip':self.ipaddr,
138 if action in ('push', 'pull', 'clone'):
161 'username':self.username,
139 proxy_key = 'HTTP_X_REAL_IP'
162 'action':self.action,
140 def_key = 'REMOTE_ADDR'
163 'repository':self.repository}
141 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
142 self.__log_user_action(user, action, self.repo_name, ipaddr)
143
164
144 #===================================================================
165 #===================================================================
145 # GIT REQUEST HANDLING
166 # GIT REQUEST HANDLING
@@ -151,12 +172,12 class SimpleGit(object):
151 return HTTPNotFound()(environ, start_response)
172 return HTTPNotFound()(environ, start_response)
152 try:
173 try:
153 app = self.__make_app()
174 app = self.__make_app()
154 except Exception:
175 except:
155 log.error(traceback.format_exc())
176 log.error(traceback.format_exc())
156 return HTTPInternalServerError()(environ, start_response)
177 return HTTPInternalServerError()(environ, start_response)
157
178
158 #invalidate cache on push
179 #invalidate cache on push
159 if action == 'push':
180 if self.action == 'push':
160 self.__invalidate_cache(self.repo_name)
181 self.__invalidate_cache(self.repo_name)
161 messages = []
182 messages = []
162 messages.append('thank you for using rhodecode')
183 messages.append('thank you for using rhodecode')
@@ -175,7 +196,7 class SimpleGit(object):
175 return environ.get('REMOTE_USER')
196 return environ.get('REMOTE_USER')
176
197
177 def __get_user(self, username):
198 def __get_user(self, username):
178 return get_user_cached(username)
199 return UserModel().get_by_username(username, cache=True)
179
200
180 def __get_action(self, environ):
201 def __get_action(self, environ):
181 """
202 """
@@ -193,12 +214,8 class SimpleGit(object):
193 else:
214 else:
194 return 'other'
215 return 'other'
195
216
196 def __log_user_action(self, user, action, repo, ipaddr):
197 action_logger(user, action, repo, ipaddr)
198
199 def __invalidate_cache(self, repo_name):
217 def __invalidate_cache(self, repo_name):
200 """we know that some change was made to repositories and we should
218 """we know that some change was made to repositories and we should
201 invalidate the cache to see the changes right away but only for
219 invalidate the cache to see the changes right away but only for
202 push requests"""
220 push requests"""
203 invalidate_cache('cached_repo_list')
221 invalidate_cache('get_repo_cached_%s' % repo_name)
204 invalidate_cache('full_changelog', repo_name)
@@ -24,40 +24,57 Created on 2010-04-28
24 SimpleHG middleware for handling mercurial protocol request (push/clone etc.)
24 SimpleHG middleware for handling mercurial protocol request (push/clone etc.)
25 It's implemented with basic auth function
25 It's implemented with basic auth function
26 """
26 """
27 from itertools import chain
28 from mercurial.error import RepoError
27 from mercurial.error import RepoError
29 from mercurial.hgweb import hgweb
28 from mercurial.hgweb import hgweb
30 from mercurial.hgweb.request import wsgiapplication
29 from mercurial.hgweb.request import wsgiapplication
31 from paste.auth.basic import AuthBasicAuthenticator
30 from paste.auth.basic import AuthBasicAuthenticator
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
31 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware, \
32 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
34 get_user_cached
33 from rhodecode.lib.utils import make_ui, invalidate_cache, \
35 from rhodecode.lib.utils import is_mercurial, make_ui, invalidate_cache, \
36 check_repo_fast, ui_sections
34 check_repo_fast, ui_sections
35 from rhodecode.model.user import UserModel
37 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
36 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
38 from rhodecode.lib.utils import action_logger
39 import logging
37 import logging
40 import os
38 import os
41 import traceback
39 import traceback
42
40
43 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
44
42
43 def is_mercurial(environ):
44 """
45 Returns True if request's target is mercurial server - header
46 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
47 """
48 http_accept = environ.get('HTTP_ACCEPT')
49 if http_accept and http_accept.startswith('application/mercurial'):
50 return True
51 return False
52
45 class SimpleHg(object):
53 class SimpleHg(object):
46
54
47 def __init__(self, application, config):
55 def __init__(self, application, config):
48 self.application = application
56 self.application = application
49 self.config = config
57 self.config = config
50 #authenticate this mercurial request using
58 #authenticate this mercurial request using authfunc
51 self.authenticate = AuthBasicAuthenticator('', authfunc)
59 self.authenticate = AuthBasicAuthenticator('', authfunc)
60 self.ipaddr = '0.0.0.0'
61 self.repository = None
62 self.username = None
63 self.action = None
52
64
53 def __call__(self, environ, start_response):
65 def __call__(self, environ, start_response):
54 if not is_mercurial(environ):
66 if not is_mercurial(environ):
55 return self.application(environ, start_response)
67 return self.application(environ, start_response)
56
68
69 proxy_key = 'HTTP_X_REAL_IP'
70 def_key = 'REMOTE_ADDR'
71 self.ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
72
57 #===================================================================
73 #===================================================================
58 # AUTHENTICATE THIS MERCURIAL REQUEST
74 # AUTHENTICATE THIS MERCURIAL REQUEST
59 #===================================================================
75 #===================================================================
60 username = REMOTE_USER(environ)
76 username = REMOTE_USER(environ)
77
61 if not username:
78 if not username:
62 self.authenticate.realm = self.config['rhodecode_realm']
79 self.authenticate.realm = self.config['rhodecode_realm']
63 result = self.authenticate(environ)
80 result = self.authenticate(environ)
@@ -67,10 +84,14 class SimpleHg(object):
67 else:
84 else:
68 return result.wsgi_application(environ, start_response)
85 return result.wsgi_application(environ, start_response)
69
86
87 #=======================================================================
88 # GET REPOSITORY
89 #=======================================================================
70 try:
90 try:
71 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
91 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
72 if repo_name.endswith('/'):
92 if repo_name.endswith('/'):
73 repo_name = repo_name.rstrip('/')
93 repo_name = repo_name.rstrip('/')
94 self.repository = repo_name
74 except:
95 except:
75 log.error(traceback.format_exc())
96 log.error(traceback.format_exc())
76 return HTTPInternalServerError()(environ, start_response)
97 return HTTPInternalServerError()(environ, start_response)
@@ -78,17 +99,18 class SimpleHg(object):
78 #===================================================================
99 #===================================================================
79 # CHECK PERMISSIONS FOR THIS REQUEST
100 # CHECK PERMISSIONS FOR THIS REQUEST
80 #===================================================================
101 #===================================================================
81 action = self.__get_action(environ)
102 self.action = self.__get_action(environ)
82 if action:
103 if self.action:
83 username = self.__get_environ_user(environ)
104 username = self.__get_environ_user(environ)
84 try:
105 try:
85 user = self.__get_user(username)
106 user = self.__get_user(username)
107 self.username = user.username
86 except:
108 except:
87 log.error(traceback.format_exc())
109 log.error(traceback.format_exc())
88 return HTTPInternalServerError()(environ, start_response)
110 return HTTPInternalServerError()(environ, start_response)
89
111
90 #check permissions for this repository
112 #check permissions for this repository
91 if action == 'push':
113 if self.action == 'push':
92 if not HasPermissionAnyMiddleware('repository.write',
114 if not HasPermissionAnyMiddleware('repository.write',
93 'repository.admin')\
115 'repository.admin')\
94 (user, repo_name):
116 (user, repo_name):
@@ -102,12 +124,10 class SimpleHg(object):
102 (user, repo_name):
124 (user, repo_name):
103 return HTTPForbidden()(environ, start_response)
125 return HTTPForbidden()(environ, start_response)
104
126
105 #log action
127 self.extras = {'ip':self.ipaddr,
106 if action in ('push', 'pull', 'clone'):
128 'username':self.username,
107 proxy_key = 'HTTP_X_REAL_IP'
129 'action':self.action,
108 def_key = 'REMOTE_ADDR'
130 'repository':self.repository}
109 ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
110 self.__log_user_action(user, action, repo_name, ipaddr)
111
131
112 #===================================================================
132 #===================================================================
113 # MERCURIAL REQUEST HANDLING
133 # MERCURIAL REQUEST HANDLING
@@ -130,40 +150,21 class SimpleHg(object):
130 return HTTPInternalServerError()(environ, start_response)
150 return HTTPInternalServerError()(environ, start_response)
131
151
132 #invalidate cache on push
152 #invalidate cache on push
133 if action == 'push':
153 if self.action == 'push':
134 self.__invalidate_cache(repo_name)
154 self.__invalidate_cache(repo_name)
135 messages = []
136 messages.append('thank you for using rhodecode')
137
155
138 return self.msg_wrapper(app, environ, start_response, messages)
139 else:
140 return app(environ, start_response)
156 return app(environ, start_response)
141
157
142
158
143 def msg_wrapper(self, app, environ, start_response, messages=[]):
144 """
145 Wrapper for custom messages that come out of mercurial respond messages
146 is a list of messages that the user will see at the end of response
147 from merurial protocol actions that involves remote answers
148 :param app:
149 :param environ:
150 :param start_response:
151 """
152 def custom_messages(msg_list):
153 for msg in msg_list:
154 yield msg + '\n'
155 org_response = app(environ, start_response)
156 return chain(org_response, custom_messages(messages))
157
158 def __make_app(self):
159 def __make_app(self):
159 hgserve = hgweb(str(self.repo_path), baseui=self.baseui)
160 hgserve = hgweb(str(self.repo_path), baseui=self.baseui)
160 return self.__load_web_settings(hgserve)
161 return self.__load_web_settings(hgserve, self.extras)
161
162
162 def __get_environ_user(self, environ):
163 def __get_environ_user(self, environ):
163 return environ.get('REMOTE_USER')
164 return environ.get('REMOTE_USER')
164
165
165 def __get_user(self, username):
166 def __get_user(self, username):
166 return get_user_cached(username)
167 return UserModel().get_by_username(username, cache=True)
167
168
168 def __get_action(self, environ):
169 def __get_action(self, environ):
169 """
170 """
@@ -174,7 +175,7 class SimpleHg(object):
174 mapping = {'changegroup': 'pull',
175 mapping = {'changegroup': 'pull',
175 'changegroupsubset': 'pull',
176 'changegroupsubset': 'pull',
176 'stream_out': 'pull',
177 'stream_out': 'pull',
177 #'listkeys': 'pull',
178 'listkeys': 'pull',
178 'unbundle': 'push',
179 'unbundle': 'push',
179 'pushkey': 'push', }
180 'pushkey': 'push', }
180 for qry in environ['QUERY_STRING'].split('&'):
181 for qry in environ['QUERY_STRING'].split('&'):
@@ -185,25 +186,26 class SimpleHg(object):
185 else:
186 else:
186 return cmd
187 return cmd
187
188
188 def __log_user_action(self, user, action, repo, ipaddr):
189 action_logger(user, action, repo, ipaddr)
190
191 def __invalidate_cache(self, repo_name):
189 def __invalidate_cache(self, repo_name):
192 """we know that some change was made to repositories and we should
190 """we know that some change was made to repositories and we should
193 invalidate the cache to see the changes right away but only for
191 invalidate the cache to see the changes right away but only for
194 push requests"""
192 push requests"""
195 invalidate_cache('cached_repo_list')
193 invalidate_cache('get_repo_cached_%s' % repo_name)
196 invalidate_cache('full_changelog', repo_name)
197
194
198
195
199 def __load_web_settings(self, hgserve):
196 def __load_web_settings(self, hgserve, extras={}):
200 #set the global ui for hgserve instance passed
197 #set the global ui for hgserve instance passed
201 hgserve.repo.ui = self.baseui
198 hgserve.repo.ui = self.baseui
202
199
203 hgrc = os.path.join(self.repo_path, '.hg', 'hgrc')
200 hgrc = os.path.join(self.repo_path, '.hg', 'hgrc')
201
202 #inject some additional parameters that will be available in ui
203 #for hooks
204 for k, v in extras.items():
205 hgserve.repo.ui.setconfig('rhodecode_extras', k, v)
206
204 repoui = make_ui('file', hgrc, False)
207 repoui = make_ui('file', hgrc, False)
205
208
206
207 if repoui:
209 if repoui:
208 #overwrite our ui instance with the section from hgrc file
210 #overwrite our ui instance with the section from hgrc file
209 for section in ui_sections:
211 for section in ui_sections:
@@ -47,7 +47,7 class SmtpMailer(object):
47 if self.debug:
47 if self.debug:
48 smtp_serv.set_debuglevel(1)
48 smtp_serv.set_debuglevel(1)
49
49
50 smtp_serv.ehlo("mailer")
50 smtp_serv.ehlo("rhodecode mailer")
51
51
52 #if server requires authorization you must provide login and password
52 #if server requires authorization you must provide login and password
53 smtp_serv.login(self.user, self.passwd)
53 smtp_serv.login(self.user, self.passwd)
@@ -1,7 +1,15
1 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 # encoding: utf-8
2 """
3 # Utilities for RhodeCode
3 rhodecode.lib.utils
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 ~~~~~~~~~~~~~~~~~~~
5
6 Utilities library for RhodeCode
7
8 :created_on: Apr 18, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
5 # This program is free software; you can redistribute it and/or
13 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
14 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; version 2
15 # as published by the Free Software Foundation; version 2
@@ -17,21 +25,28
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18 # MA 02110-1301, USA.
26 # MA 02110-1301, USA.
19
27
20 """
28 import os
21 Created on April 18, 2010
29 import logging
22 Utilities for RhodeCode
30 import datetime
23 @author: marcink
31 import traceback
24 """
32
25 from beaker.cache import cache_region
33 from UserDict import DictMixin
34
26 from mercurial import ui, config, hg
35 from mercurial import ui, config, hg
27 from mercurial.error import RepoError
36 from mercurial.error import RepoError
28 from rhodecode.model import meta
37
29 from rhodecode.model.db import Repository, User, RhodeCodeUi, RhodeCodeSettings, UserLog
38 import paste
39 import beaker
40 from paste.script.command import Command, BadCommand
41
30 from vcs.backends.base import BaseChangeset
42 from vcs.backends.base import BaseChangeset
31 from vcs.utils.lazy import LazyProperty
43 from vcs.utils.lazy import LazyProperty
32 import logging
44
33 import datetime
45 from rhodecode.model import meta
34 import os
46 from rhodecode.model.caching_query import FromCache
47 from rhodecode.model.db import Repository, User, RhodeCodeUi, UserLog
48 from rhodecode.model.repo import RepoModel
49 from rhodecode.model.user import UserModel
35
50
36 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
37
52
@@ -39,72 +54,95 log = logging.getLogger(__name__)
39 def get_repo_slug(request):
54 def get_repo_slug(request):
40 return request.environ['pylons.routes_dict'].get('repo_name')
55 return request.environ['pylons.routes_dict'].get('repo_name')
41
56
42 def is_mercurial(environ):
57 def action_logger(user, action, repo, ipaddr='', sa=None):
43 """
44 Returns True if request's target is mercurial server - header
45 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
46 """
47 http_accept = environ.get('HTTP_ACCEPT')
48 if http_accept and http_accept.startswith('application/mercurial'):
49 return True
50 return False
51
52 def is_git(environ):
53 """
58 """
54 Returns True if request's target is git server. ``HTTP_USER_AGENT`` would
59 Action logger for various actions made by users
55 then have git client version given.
56
60
57 :param environ:
61 :param user: user that made this action, can be a unique username string or
58 """
62 object containing user_id attribute
59 http_user_agent = environ.get('HTTP_USER_AGENT')
63 :param action: action to log, should be on of predefined unique actions for
60 if http_user_agent.startswith('git'):
64 easy translations
61 return True
65 :param repo: string name of repository or object containing repo_id,
62 return False
66 that action was made on
67 :param ipaddr: optional ip address from what the action was made
68 :param sa: optional sqlalchemy session
63
69
64 def action_logger(user, action, repo, ipaddr, sa=None):
65 """
66 Action logger for various action made by users
67 """
70 """
68
71
69 if not sa:
72 if not sa:
70 sa = meta.Session
73 sa = meta.Session()
71
74
72 try:
75 try:
76 um = UserModel()
73 if hasattr(user, 'user_id'):
77 if hasattr(user, 'user_id'):
74 user_id = user.user_id
78 user_obj = user
75 elif isinstance(user, basestring):
79 elif isinstance(user, basestring):
76 user_id = sa.query(User).filter(User.username == user).one()
80 user_obj = um.get_by_username(user, cache=False)
77 else:
81 else:
78 raise Exception('You have to provide user object or username')
82 raise Exception('You have to provide user object or username')
79
83
84
85 rm = RepoModel()
86 if hasattr(repo, 'repo_id'):
87 repo_obj = rm.get(repo.repo_id, cache=False)
88 repo_name = repo_obj.repo_name
89 elif isinstance(repo, basestring):
80 repo_name = repo.lstrip('/')
90 repo_name = repo.lstrip('/')
91 repo_obj = rm.get_by_repo_name(repo_name, cache=False)
92 else:
93 raise Exception('You have to provide repository to action logger')
94
95
81 user_log = UserLog()
96 user_log = UserLog()
82 user_log.user_id = user_id
97 user_log.user_id = user_obj.user_id
83 user_log.action = action
98 user_log.action = action
99
100 user_log.repository_id = repo_obj.repo_id
84 user_log.repository_name = repo_name
101 user_log.repository_name = repo_name
85 user_log.repository = sa.query(Repository)\
102
86 .filter(Repository.repo_name == repo_name).one()
87 user_log.action_date = datetime.datetime.now()
103 user_log.action_date = datetime.datetime.now()
88 user_log.user_ip = ipaddr
104 user_log.user_ip = ipaddr
89 sa.add(user_log)
105 sa.add(user_log)
90 sa.commit()
106 sa.commit()
91
107
92 log.info('Adding user %s, action %s on %s',
108 log.info('Adding user %s, action %s on %s', user_obj, action, repo)
93 user.username, action, repo)
109 except:
94 except Exception, e:
110 log.error(traceback.format_exc())
95 sa.rollback()
111 sa.rollback()
96 log.error('could not log user action:%s', str(e))
97
112
98 def check_repo_dir(paths):
113 def get_repos(path, recursive=False, initial=False):
99 repos_path = paths[0][1].split('/')
114 """
100 if repos_path[-1] in ['*', '**']:
115 Scans given path for repos and return (name,(type,path)) tuple
101 repos_path = repos_path[:-1]
116 :param prefix:
102 if repos_path[0] != '/':
117 :param path:
103 repos_path[0] = '/'
118 :param recursive:
104 if not os.path.isdir(os.path.join(*repos_path)):
119 :param initial:
105 raise Exception('Not a valid repository in %s' % paths[0][1])
120 """
121 from vcs.utils.helpers import get_scm
122 from vcs.exceptions import VCSError
123
124 try:
125 scm = get_scm(path)
126 except:
127 pass
128 else:
129 raise Exception('The given path %s should not be a repository got %s',
130 path, scm)
131
132 for dirpath in os.listdir(path):
133 try:
134 yield dirpath, get_scm(os.path.join(path, dirpath))
135 except VCSError:
136 pass
106
137
107 def check_repo_fast(repo_name, base_path):
138 def check_repo_fast(repo_name, base_path):
139 """
140 Check given path for existance of directory
141 :param repo_name:
142 :param base_path:
143
144 :return False: if this directory is present
145 """
108 if os.path.isdir(os.path.join(base_path, repo_name)):return False
146 if os.path.isdir(os.path.join(base_path, repo_name)):return False
109 return True
147 return True
110
148
@@ -135,57 +173,6 def ask_ok(prompt, retries=4, complaint=
135 if retries < 0: raise IOError
173 if retries < 0: raise IOError
136 print complaint
174 print complaint
137
175
138 @cache_region('super_short_term', 'cached_hg_ui')
139 def get_hg_ui_cached():
140 try:
141 sa = meta.Session
142 ret = sa.query(RhodeCodeUi).all()
143 finally:
144 meta.Session.remove()
145 return ret
146
147
148 def get_hg_settings():
149 try:
150 sa = meta.Session
151 ret = sa.query(RhodeCodeSettings).all()
152 finally:
153 meta.Session.remove()
154
155 if not ret:
156 raise Exception('Could not get application settings !')
157 settings = {}
158 for each in ret:
159 settings['rhodecode_' + each.app_settings_name] = each.app_settings_value
160
161 return settings
162
163 def get_hg_ui_settings():
164 try:
165 sa = meta.Session
166 ret = sa.query(RhodeCodeUi).all()
167 finally:
168 meta.Session.remove()
169
170 if not ret:
171 raise Exception('Could not get application ui settings !')
172 settings = {}
173 for each in ret:
174 k = each.ui_key
175 v = each.ui_value
176 if k == '/':
177 k = 'root_path'
178
179 if k.find('.') != -1:
180 k = k.replace('.', '_')
181
182 if each.ui_section == 'hooks':
183 v = each.ui_active
184
185 settings[each.ui_section + '_' + k] = v
186
187 return settings
188
189 #propagated from mercurial documentation
176 #propagated from mercurial documentation
190 ui_sections = ['alias', 'auth',
177 ui_sections = ['alias', 'auth',
191 'decode/encode', 'defaults',
178 'decode/encode', 'defaults',
@@ -210,6 +197,11 def make_ui(read_from='file', path=None,
210
197
211 baseui = ui.ui()
198 baseui = ui.ui()
212
199
200 #clean the baseui object
201 baseui._ocfg = config.config()
202 baseui._ucfg = config.config()
203 baseui._tcfg = config.config()
204
213 if read_from == 'file':
205 if read_from == 'file':
214 if not os.path.isfile(path):
206 if not os.path.isfile(path):
215 log.warning('Unable to read config file %s' % path)
207 log.warning('Unable to read config file %s' % path)
@@ -219,70 +211,69 def make_ui(read_from='file', path=None,
219 cfg.read(path)
211 cfg.read(path)
220 for section in ui_sections:
212 for section in ui_sections:
221 for k, v in cfg.items(section):
213 for k, v in cfg.items(section):
214 log.debug('settings ui from file[%s]%s:%s', section, k, v)
222 baseui.setconfig(section, k, v)
215 baseui.setconfig(section, k, v)
223 log.debug('settings ui from file[%s]%s:%s', section, k, v)
224
225 for k, v in baseui.configitems('extensions'):
226 baseui.setconfig('extensions', k, '0')
227 #just enable mq
228 baseui.setconfig('extensions', 'mq', '1')
229 if checkpaths:check_repo_dir(cfg.items('paths'))
230
216
231
217
232 elif read_from == 'db':
218 elif read_from == 'db':
233 hg_ui = get_hg_ui_cached()
219 sa = meta.Session()
220 ret = sa.query(RhodeCodeUi)\
221 .options(FromCache("sql_cache_short",
222 "get_hg_ui_settings")).all()
223
224 hg_ui = ret
234 for ui_ in hg_ui:
225 for ui_ in hg_ui:
235 if ui_.ui_active:
226 if ui_.ui_active:
236 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section, ui_.ui_key, ui_.ui_value)
227 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
228 ui_.ui_key, ui_.ui_value)
237 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
229 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
238
230
239
231 meta.Session.remove()
240 return baseui
232 return baseui
241
233
242
234
243 def set_rhodecode_config(config):
235 def set_rhodecode_config(config):
244 hgsettings = get_hg_settings()
236 """
237 Updates pylons config with new settings from database
238 :param config:
239 """
240 from rhodecode.model.settings import SettingsModel
241 hgsettings = SettingsModel().get_app_settings()
245
242
246 for k, v in hgsettings.items():
243 for k, v in hgsettings.items():
247 config[k] = v
244 config[k] = v
248
245
249 def invalidate_cache(name, *args):
246 def invalidate_cache(cache_key, *args):
250 """Invalidates given name cache"""
247 """
251
248 Puts cache invalidation task into db for
252 from beaker.cache import region_invalidate
249 further global cache invalidation
253 log.info('INVALIDATING CACHE FOR %s', name)
250 """
251 from rhodecode.model.scm import ScmModel
254
252
255 """propagate our arguments to make sure invalidation works. First
253 if cache_key.startswith('get_repo_cached_'):
256 argument has to be the name of cached func name give to cache decorator
254 name = cache_key.split('get_repo_cached_')[-1]
257 without that the invalidation would not work"""
255 ScmModel().mark_for_invalidation(name)
258 tmp = [name]
259 tmp.extend(args)
260 args = tuple(tmp)
261
262 if name == 'cached_repo_list':
263 from rhodecode.model.hg_model import _get_repos_cached
264 region_invalidate(_get_repos_cached, None, *args)
265
266 if name == 'full_changelog':
267 from rhodecode.model.hg_model import _full_changelog_cached
268 region_invalidate(_full_changelog_cached, None, *args)
269
256
270 class EmptyChangeset(BaseChangeset):
257 class EmptyChangeset(BaseChangeset):
271 """
258 """
272 An dummy empty changeset.
259 An dummy empty changeset. It's possible to pass hash when creating
260 an EmptyChangeset
273 """
261 """
274
262
275 revision = -1
263 def __init__(self, cs='0' * 40):
276 message = ''
264 self._empty_cs = cs
277 author = ''
265 self.revision = -1
278 date = ''
266 self.message = ''
267 self.author = ''
268 self.date = ''
269
279 @LazyProperty
270 @LazyProperty
280 def raw_id(self):
271 def raw_id(self):
281 """
272 """
282 Returns raw string identifing this changeset, useful for web
273 Returns raw string identifying this changeset, useful for web
283 representation.
274 representation.
284 """
275 """
285 return '0' * 40
276 return self._empty_cs
286
277
287 @LazyProperty
278 @LazyProperty
288 def short_id(self):
279 def short_id(self):
@@ -301,26 +292,25 def repo2db_mapper(initial_repo_list, re
301 """
292 """
302 maps all found repositories into db
293 maps all found repositories into db
303 """
294 """
304 from rhodecode.model.repo_model import RepoModel
305
295
306 sa = meta.Session
296 sa = meta.Session()
297 rm = RepoModel()
307 user = sa.query(User).filter(User.admin == True).first()
298 user = sa.query(User).filter(User.admin == True).first()
308
299
309 rm = RepoModel()
310
311 for name, repo in initial_repo_list.items():
300 for name, repo in initial_repo_list.items():
312 if not sa.query(Repository).filter(Repository.repo_name == name).scalar():
301 if not rm.get_by_repo_name(name, cache=False):
313 log.info('repository %s not found creating default', name)
302 log.info('repository %s not found creating default', name)
314
303
315 form_data = {
304 form_data = {
316 'repo_name':name,
305 'repo_name':name,
317 'description':repo.description if repo.description != 'unknown' else \
306 'repo_type':repo.alias,
318 'auto description for %s' % name,
307 'description':repo.description \
308 if repo.description != 'unknown' else \
309 '%s repository' % name,
319 'private':False
310 'private':False
320 }
311 }
321 rm.create(form_data, user, just_db=True)
312 rm.create(form_data, user, just_db=True)
322
313
323
324 if remove_obsolete:
314 if remove_obsolete:
325 #remove from database those repositories that are not in the filesystem
315 #remove from database those repositories that are not in the filesystem
326 for repo in sa.query(Repository).all():
316 for repo in sa.query(Repository).all():
@@ -328,11 +318,6 def repo2db_mapper(initial_repo_list, re
328 sa.delete(repo)
318 sa.delete(repo)
329 sa.commit()
319 sa.commit()
330
320
331
332 meta.Session.remove()
333
334 from UserDict import DictMixin
335
336 class OrderedDict(dict, DictMixin):
321 class OrderedDict(dict, DictMixin):
337
322
338 def __init__(self, *args, **kwds):
323 def __init__(self, *args, **kwds):
@@ -433,8 +418,51 class OrderedDict(dict, DictMixin):
433 return not self == other
418 return not self == other
434
419
435
420
421 #set cache regions for beaker so celery can utilise it
422 def add_cache(settings):
423 cache_settings = {'regions':None}
424 for key in settings.keys():
425 for prefix in ['beaker.cache.', 'cache.']:
426 if key.startswith(prefix):
427 name = key.split(prefix)[1].strip()
428 cache_settings[name] = settings[key].strip()
429 if cache_settings['regions']:
430 for region in cache_settings['regions'].split(','):
431 region = region.strip()
432 region_settings = {}
433 for key, value in cache_settings.items():
434 if key.startswith(region):
435 region_settings[key.split('.')[1]] = value
436 region_settings['expire'] = int(region_settings.get('expire',
437 60))
438 region_settings.setdefault('lock_dir',
439 cache_settings.get('lock_dir'))
440 if 'type' not in region_settings:
441 region_settings['type'] = cache_settings.get('type',
442 'memory')
443 beaker.cache.cache_regions[region] = region_settings
444
445 def get_current_revision():
446 """
447 Returns tuple of (number, id) from repository containing this package
448 or None if repository could not be found.
449 """
450 try:
451 from vcs import get_repo
452 from vcs.utils.helpers import get_scm
453 from vcs.exceptions import RepositoryError, VCSError
454 repopath = os.path.join(os.path.dirname(__file__), '..', '..')
455 scm = get_scm(repopath)[0]
456 repo = get_repo(path=repopath, alias=scm)
457 tip = repo.get_changeset()
458 return (tip.revision, tip.short_id)
459 except (ImportError, RepositoryError, VCSError), err:
460 logging.debug("Cannot retrieve rhodecode's revision. Original error "
461 "was: %s" % err)
462 return None
463
436 #===============================================================================
464 #===============================================================================
437 # TEST FUNCTIONS
465 # TEST FUNCTIONS AND CREATORS
438 #===============================================================================
466 #===============================================================================
439 def create_test_index(repo_location, full_index):
467 def create_test_index(repo_location, full_index):
440 """Makes default test index
468 """Makes default test index
@@ -443,15 +471,16 def create_test_index(repo_location, ful
443 """
471 """
444 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
472 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
445 from rhodecode.lib.pidlock import DaemonLock, LockHeld
473 from rhodecode.lib.pidlock import DaemonLock, LockHeld
446 from rhodecode.lib.indexers import IDX_LOCATION
447 import shutil
474 import shutil
448
475
449 if os.path.exists(IDX_LOCATION):
476 index_location = os.path.join(repo_location, 'index')
450 shutil.rmtree(IDX_LOCATION)
477 if os.path.exists(index_location):
478 shutil.rmtree(index_location)
451
479
452 try:
480 try:
453 l = DaemonLock()
481 l = DaemonLock()
454 WhooshIndexingDaemon(repo_location=repo_location)\
482 WhooshIndexingDaemon(index_location=index_location,
483 repo_location=repo_location)\
455 .run(full_index=full_index)
484 .run(full_index=full_index)
456 l.release()
485 l.release()
457 except LockHeld:
486 except LockHeld:
@@ -462,10 +491,11 def create_test_env(repos_test_path, con
462 install test repository into tmp dir
491 install test repository into tmp dir
463 """
492 """
464 from rhodecode.lib.db_manage import DbManage
493 from rhodecode.lib.db_manage import DbManage
494 from rhodecode.tests import HG_REPO, GIT_REPO, NEW_HG_REPO, NEW_GIT_REPO, \
495 HG_FORK, GIT_FORK, TESTS_TMP_PATH
465 import tarfile
496 import tarfile
466 import shutil
497 import shutil
467 from os.path import dirname as dn, join as jn, abspath
498 from os.path import dirname as dn, join as jn, abspath
468 from rhodecode.tests import REPO_PATH, NEW_REPO_PATH, FORK_REPO_PATH
469
499
470 log = logging.getLogger('TestEnvCreator')
500 log = logging.getLogger('TestEnvCreator')
471 # create logger
501 # create logger
@@ -485,10 +515,10 def create_test_env(repos_test_path, con
485 log.addHandler(ch)
515 log.addHandler(ch)
486
516
487 #PART ONE create db
517 #PART ONE create db
488 dbname = config['sqlalchemy.db1.url'].split('/')[-1]
518 dbconf = config['sqlalchemy.db1.url']
489 log.debug('making test db %s', dbname)
519 log.debug('making test db %s', dbconf)
490
520
491 dbmanage = DbManage(log_sql=True, dbname=dbname, root=config['here'],
521 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
492 tests=True)
522 tests=True)
493 dbmanage.create_tables(override=True)
523 dbmanage.create_tables(override=True)
494 dbmanage.config_prompt(repos_test_path)
524 dbmanage.config_prompt(repos_test_path)
@@ -498,18 +528,87 def create_test_env(repos_test_path, con
498 dbmanage.populate_default_permissions()
528 dbmanage.populate_default_permissions()
499
529
500 #PART TWO make test repo
530 #PART TWO make test repo
501 log.debug('making test vcs repo')
531 log.debug('making test vcs repositories')
502 if os.path.isdir(REPO_PATH):
532
503 log.debug('REMOVING %s', REPO_PATH)
533 #remove old one from previos tests
504 shutil.rmtree(REPO_PATH)
534 for r in [HG_REPO, GIT_REPO, NEW_HG_REPO, NEW_GIT_REPO, HG_FORK, GIT_FORK]:
505 if os.path.isdir(NEW_REPO_PATH):
535
506 log.debug('REMOVING %s', NEW_REPO_PATH)
536 if os.path.isdir(jn(TESTS_TMP_PATH, r)):
507 shutil.rmtree(NEW_REPO_PATH)
537 log.debug('removing %s', r)
508 if os.path.isdir(FORK_REPO_PATH):
538 shutil.rmtree(jn(TESTS_TMP_PATH, r))
509 log.debug('REMOVING %s', FORK_REPO_PATH)
539
510 shutil.rmtree(FORK_REPO_PATH)
540 #CREATE DEFAULT HG REPOSITORY
541 cur_dir = dn(dn(abspath(__file__)))
542 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
543 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
544 tar.close()
545
546
547 #==============================================================================
548 # PASTER COMMANDS
549 #==============================================================================
550
551 class BasePasterCommand(Command):
552 """
553 Abstract Base Class for paster commands.
554
555 The celery commands are somewhat aggressive about loading
556 celery.conf, and since our module sets the `CELERY_LOADER`
557 environment variable to our loader, we have to bootstrap a bit and
558 make sure we've had a chance to load the pylons config off of the
559 command line, otherwise everything fails.
560 """
561 min_args = 1
562 min_args_error = "Please provide a paster config file as an argument."
563 takes_config_file = 1
564 requires_config_file = True
565
566 def notify_msg(self, msg, log=False):
567 """Make a notification to user, additionally if logger is passed
568 it logs this action using given logger
569
570 :param msg: message that will be printed to user
571 :param log: logging instance, to use to additionally log this message
511
572
512 cur_dir = dn(dn(abspath(__file__)))
573 """
513 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test.tar.gz"))
574 print msg
514 tar.extractall('/tmp')
575 if log and isinstance(log, logging):
515 tar.close()
576 log(msg)
577
578
579 def run(self, args):
580 """
581 Overrides Command.run
582
583 Checks for a config file argument and loads it.
584 """
585 if len(args) < self.min_args:
586 raise BadCommand(
587 self.min_args_error % {'min_args': self.min_args,
588 'actual_args': len(args)})
589
590 # Decrement because we're going to lob off the first argument.
591 # @@ This is hacky
592 self.min_args -= 1
593 self.bootstrap_config(args[0])
594 self.update_parser()
595 return super(BasePasterCommand, self).run(args[1:])
596
597 def update_parser(self):
598 """
599 Abstract method. Allows for the class's parser to be updated
600 before the superclass's `run` method is called. Necessary to
601 allow options/arguments to be passed through to the underlying
602 celery command.
603 """
604 raise NotImplementedError("Abstract Method.")
605
606 def bootstrap_config(self, conf):
607 """
608 Loads the pylons configuration.
609 """
610 from pylons import config as pylonsconfig
611
612 path_to_ini_file = os.path.realpath(conf)
613 conf = paste.deploy.appconfig('config:' + path_to_ini_file)
614 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
@@ -1,23 +1,71
1 """The application's model objects"""
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.model.__init__
4 ~~~~~~~~~~~~~~~~~~~~~~~~
5
6 The application's model objects
7
8 :created_on: Nov 25, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12
13
14 :example:
15
16 .. code-block:: python
17
18 from paste.deploy import appconfig
19 from pylons import config
20 from sqlalchemy import engine_from_config
21 from rhodecode.config.environment import load_environment
22
23 conf = appconfig('config:development.ini', relative_to = './../../')
24 load_environment(conf.global_conf, conf.local_conf)
25
26 engine = engine_from_config(config, 'sqlalchemy.')
27 init_model(engine)
28 # RUN YOUR CODE HERE
29
30 """
31 # This program is free software; you can redistribute it and/or
32 # modify it under the terms of the GNU General Public License
33 # as published by the Free Software Foundation; version 2
34 # of the License or (at your opinion) any later version of the license.
35 #
36 # This program is distributed in the hope that it will be useful,
37 # but WITHOUT ANY WARRANTY; without even the implied warranty of
38 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
39 # GNU General Public License for more details.
40 #
41 # You should have received a copy of the GNU General Public License
42 # along with this program; if not, write to the Free Software
43 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
44 # MA 02110-1301, USA.
45
2 import logging
46 import logging
3 from rhodecode.model import meta
47 from rhodecode.model import meta
4 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
5
49
6 def init_model(engine):
50 def init_model(engine):
7 """Call me before using any of the tables or classes in the model"""
51 """Initializes db session, bind the engine with the metadata,
8 log.info("INITIALIZING DB MODELS")
52 Call this before using any of the tables or classes in the model, preferably
53 once in application start
54
55 :param engine: engine to bind to
56 """
57 log.info("initializing db models for %s", engine)
9 meta.Base.metadata.bind = engine
58 meta.Base.metadata.bind = engine
10 #meta.Base2.metadata.bind = engine2
11
59
12 #THIS IS A TEST FOR EXECUTING SCRIPT AND LOAD PYLONS APPLICATION GLOBALS
60 class BaseModel(object):
13 #from paste.deploy import appconfig
61 """Base Model for all RhodeCode models, it adds sql alchemy session
14 #from pylons import config
62 into instance of model
15 #from sqlalchemy import engine_from_config
63
16 #from rhodecode.config.environment import load_environment
64 :param sa: If passed it reuses this session instead of creating a new one
17 #
65 """
18 #conf = appconfig('config:development.ini', relative_to = './../../')
66
19 #load_environment(conf.global_conf, conf.local_conf)
67 def __init__(self, sa=None):
20 #
68 if sa is not None:
21 #engine = engine_from_config(config, 'sqlalchemy.')
69 self.sa = sa
22 #init_model(engine)
70 else:
23 # DO SOMETHING
71 self.sa = meta.Session()
@@ -110,7 +110,7 def query_callable(manager):
110
110
111 def get_cache_region(name, region):
111 def get_cache_region(name, region):
112 if region not in beaker.cache.cache_regions:
112 if region not in beaker.cache.cache_regions:
113 raise BeakerException('Cache region not configured: %s'
113 raise BeakerException('Cache region `%s` not configured '
114 'Check if proper cache settings are in the .ini files' % region)
114 'Check if proper cache settings are in the .ini files' % region)
115 kw = beaker.cache.cache_regions[region]
115 kw = beaker.cache.cache_regions[region]
116 return beaker.cache.Cache._get_cache(name, kw)
116 return beaker.cache.Cache._get_cache(name, kw)
@@ -1,46 +1,119
1 from rhodecode.model.meta import Base
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.model.db
4 ~~~~~~~~~~~~~~~~~~
5
6 Database Models for RhodeCode
7
8 :created_on: Apr 08, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 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
14 # modify it under the terms of the GNU General Public License
15 # as published by the Free Software Foundation; version 2
16 # of the License or (at your opinion) any later version of the license.
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, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 # MA 02110-1301, USA.
27 import logging
28 import datetime
29
2 from sqlalchemy import *
30 from sqlalchemy import *
3 from sqlalchemy.orm import relation, backref
31 from sqlalchemy.exc import DatabaseError
32 from sqlalchemy.orm import relation, backref, class_mapper
4 from sqlalchemy.orm.session import Session
33 from sqlalchemy.orm.session import Session
5 from vcs.utils.lazy import LazyProperty
34
6 import logging
35 from rhodecode.model.meta import Base
7
36
8 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
9
38
10 class RhodeCodeSettings(Base):
39 class BaseModel(object):
40
41 @classmethod
42 def _get_keys(cls):
43 """return column names for this model """
44 return class_mapper(cls).c.keys()
45
46 def get_dict(self):
47 """return dict with keys and values corresponding
48 to this model data """
49
50 d = {}
51 for k in self._get_keys():
52 d[k] = getattr(self, k)
53 return d
54
55 def get_appstruct(self):
56 """return list with keys and values tupples corresponding
57 to this model data """
58
59 l = []
60 for k in self._get_keys():
61 l.append((k, getattr(self, k),))
62 return l
63
64 def populate_obj(self, populate_dict):
65 """populate model with data from given populate_dict"""
66
67 for k in self._get_keys():
68 if k in populate_dict:
69 setattr(self, k, populate_dict[k])
70
71 class RhodeCodeSettings(Base, BaseModel):
11 __tablename__ = 'rhodecode_settings'
72 __tablename__ = 'rhodecode_settings'
12 __table_args__ = (UniqueConstraint('app_settings_name'), {'useexisting':True})
73 __table_args__ = (UniqueConstraint('app_settings_name'), {'useexisting':True})
13 app_settings_id = Column("app_settings_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
74 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
14 app_settings_name = Column("app_settings_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
75 app_settings_name = Column("app_settings_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
15 app_settings_value = Column("app_settings_value", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
76 app_settings_value = Column("app_settings_value", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
16
77
17 class RhodeCodeUi(Base):
78 def __init__(self, k, v):
79 self.app_settings_name = k
80 self.app_settings_value = v
81
82 def __repr__(self):
83 return "<RhodeCodeSetting('%s:%s')>" % (self.app_settings_name,
84 self.app_settings_value)
85
86 class RhodeCodeUi(Base, BaseModel):
18 __tablename__ = 'rhodecode_ui'
87 __tablename__ = 'rhodecode_ui'
19 __table_args__ = {'useexisting':True}
88 __table_args__ = {'useexisting':True}
20 ui_id = Column("ui_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
89 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
21 ui_section = Column("ui_section", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
90 ui_section = Column("ui_section", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
22 ui_key = Column("ui_key", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
91 ui_key = Column("ui_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
23 ui_value = Column("ui_value", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
92 ui_value = Column("ui_value", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
24 ui_active = Column("ui_active", BOOLEAN(), nullable=True, unique=None, default=True)
93 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
25
94
26
95
27 class User(Base):
96 class User(Base, BaseModel):
28 __tablename__ = 'users'
97 __tablename__ = 'users'
29 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'useexisting':True})
98 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'useexisting':True})
30 user_id = Column("user_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
99 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
31 username = Column("username", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
100 username = Column("username", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
32 password = Column("password", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
101 password = Column("password", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
33 active = Column("active", BOOLEAN(), nullable=True, unique=None, default=None)
102 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
34 admin = Column("admin", BOOLEAN(), nullable=True, unique=None, default=False)
103 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
35 name = Column("name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
104 name = Column("name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
36 lastname = Column("lastname", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
105 lastname = Column("lastname", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
37 email = Column("email", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
106 email = Column("email", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
38 last_login = Column("last_login", DATETIME(timezone=False), nullable=True, unique=None, default=None)
107 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
108 is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False)
39
109
40 user_log = relation('UserLog')
110 user_log = relation('UserLog', cascade='all')
41 user_perms = relation('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id")
111 user_perms = relation('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
42
112
43 @LazyProperty
113 repositories = relation('Repository')
114 user_followers = relation('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
115
116 @property
44 def full_contact(self):
117 def full_contact(self):
45 return '%s %s <%s>' % (self.name, self.lastname, self.email)
118 return '%s %s <%s>' % (self.name, self.lastname, self.email)
46
119
@@ -49,7 +122,6 class User(Base):
49
122
50 def update_lastlogin(self):
123 def update_lastlogin(self):
51 """Update user lastlogin"""
124 """Update user lastlogin"""
52 import datetime
53
125
54 try:
126 try:
55 session = Session.object_session(self)
127 session = Session.object_session(self)
@@ -57,85 +129,129 class User(Base):
57 session.add(self)
129 session.add(self)
58 session.commit()
130 session.commit()
59 log.debug('updated user %s lastlogin', self.username)
131 log.debug('updated user %s lastlogin', self.username)
60 except Exception:
132 except (DatabaseError,):
61 session.rollback()
133 session.rollback()
62
134
63
135
64 class UserLog(Base):
136 class UserLog(Base, BaseModel):
65 __tablename__ = 'user_logs'
137 __tablename__ = 'user_logs'
66 __table_args__ = {'useexisting':True}
138 __table_args__ = {'useexisting':True}
67 user_log_id = Column("user_log_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
139 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
68 user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
140 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
69 repository_id = Column("repository_id", INTEGER(length=None, convert_unicode=False, assert_unicode=None), ForeignKey(u'repositories.repo_id'), nullable=False, unique=None, default=None)
141 repository_id = Column("repository_id", Integer(length=None, convert_unicode=False, assert_unicode=None), ForeignKey(u'repositories.repo_id'), nullable=False, unique=None, default=None)
70 repository_name = Column("repository_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
142 repository_name = Column("repository_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
71 user_ip = Column("user_ip", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
143 user_ip = Column("user_ip", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
72 action = Column("action", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
144 action = Column("action", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
73 action_date = Column("action_date", DATETIME(timezone=False), nullable=True, unique=None, default=None)
145 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
74 revision = Column('revision', TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
75
146
76 user = relation('User')
147 user = relation('User')
77 repository = relation('Repository')
148 repository = relation('Repository')
78
149
79 class Repository(Base):
150 class Repository(Base, BaseModel):
80 __tablename__ = 'repositories'
151 __tablename__ = 'repositories'
81 __table_args__ = (UniqueConstraint('repo_name'), {'useexisting':True},)
152 __table_args__ = (UniqueConstraint('repo_name'), {'useexisting':True},)
82 repo_id = Column("repo_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
153 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
83 repo_name = Column("repo_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
154 repo_name = Column("repo_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
84 user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=False, default=None)
155 repo_type = Column("repo_type", String(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
85 private = Column("private", BOOLEAN(), nullable=True, unique=None, default=None)
156 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=False, default=None)
86 description = Column("description", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
157 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
87 fork_id = Column("fork_id", INTEGER(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=False, default=None)
158 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
159 description = Column("description", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
160 fork_id = Column("fork_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=False, default=None)
88
161
89 user = relation('User')
162 user = relation('User')
90 fork = relation('Repository', remote_side=repo_id)
163 fork = relation('Repository', remote_side=repo_id)
91 repo_to_perm = relation('RepoToPerm', cascade='all')
164 repo_to_perm = relation('RepoToPerm', cascade='all')
92 stats = relation('Statistics', cascade='all')
165 stats = relation('Statistics', cascade='all', uselist=False)
166
167 repo_followers = relation('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
93
168
94
169
95 def __repr__(self):
170 def __repr__(self):
96 return "<Repository('id:%s:%s')>" % (self.repo_id, self.repo_name)
171 return "<Repository('%s:%s')>" % (self.repo_id, self.repo_name)
97
172
98 class Permission(Base):
173 class Permission(Base, BaseModel):
99 __tablename__ = 'permissions'
174 __tablename__ = 'permissions'
100 __table_args__ = {'useexisting':True}
175 __table_args__ = {'useexisting':True}
101 permission_id = Column("permission_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
176 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
102 permission_name = Column("permission_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
177 permission_name = Column("permission_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
103 permission_longname = Column("permission_longname", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
178 permission_longname = Column("permission_longname", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
104
179
105 def __repr__(self):
180 def __repr__(self):
106 return "<Permission('%s:%s')>" % (self.permission_id, self.permission_name)
181 return "<Permission('%s:%s')>" % (self.permission_id, self.permission_name)
107
182
108 class RepoToPerm(Base):
183 class RepoToPerm(Base, BaseModel):
109 __tablename__ = 'repo_to_perm'
184 __tablename__ = 'repo_to_perm'
110 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'useexisting':True})
185 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'useexisting':True})
111 repo_to_perm_id = Column("repo_to_perm_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
186 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
112 user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
187 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
113 permission_id = Column("permission_id", INTEGER(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None)
188 permission_id = Column("permission_id", Integer(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None)
114 repository_id = Column("repository_id", INTEGER(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=None, default=None)
189 repository_id = Column("repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=None, default=None)
115
190
116 user = relation('User')
191 user = relation('User')
117 permission = relation('Permission')
192 permission = relation('Permission')
118 repository = relation('Repository')
193 repository = relation('Repository')
119
194
120 class UserToPerm(Base):
195 class UserToPerm(Base, BaseModel):
121 __tablename__ = 'user_to_perm'
196 __tablename__ = 'user_to_perm'
122 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'useexisting':True})
197 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'useexisting':True})
123 user_to_perm_id = Column("user_to_perm_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
198 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
124 user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
199 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
125 permission_id = Column("permission_id", INTEGER(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None)
200 permission_id = Column("permission_id", Integer(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None)
126
201
127 user = relation('User')
202 user = relation('User')
128 permission = relation('Permission')
203 permission = relation('Permission')
129
204
130 class Statistics(Base):
205 class Statistics(Base, BaseModel):
131 __tablename__ = 'statistics'
206 __tablename__ = 'statistics'
132 __table_args__ = (UniqueConstraint('repository_id'), {'useexisting':True})
207 __table_args__ = (UniqueConstraint('repository_id'), {'useexisting':True})
133 stat_id = Column("stat_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True)
208 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
134 repository_id = Column("repository_id", INTEGER(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=True, default=None)
209 repository_id = Column("repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=True, default=None)
135 stat_on_revision = Column("stat_on_revision", INTEGER(), nullable=False)
210 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
136 commit_activity = Column("commit_activity", BLOB(), nullable=False)#JSON data
211 commit_activity = Column("commit_activity", LargeBinary(), nullable=False)#JSON data
137 commit_activity_combined = Column("commit_activity_combined", BLOB(), nullable=False)#JSON data
212 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
138 languages = Column("languages", BLOB(), nullable=False)#JSON data
213 languages = Column("languages", LargeBinary(), nullable=False)#JSON data
139
214
140 repository = relation('Repository', single_parent=True)
215 repository = relation('Repository', single_parent=True)
141
216
217 class UserFollowing(Base, BaseModel):
218 __tablename__ = 'user_followings'
219 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
220 UniqueConstraint('user_id', 'follows_user_id')
221 , {'useexisting':True})
222
223 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
224 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
225 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=None, default=None)
226 follows_user_id = Column("follows_user_id", Integer(), ForeignKey(u'users.user_id'), nullable=True, unique=None, default=None)
227
228 user = relation('User', primaryjoin='User.user_id==UserFollowing.user_id')
229
230 follows_user = relation('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
231 follows_repository = relation('Repository')
232
233
234 class CacheInvalidation(Base, BaseModel):
235 __tablename__ = 'cache_invalidation'
236 __table_args__ = (UniqueConstraint('cache_key'), {'useexisting':True})
237 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
238 cache_key = Column("cache_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
239 cache_args = Column("cache_args", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
240 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
241
242
243 def __init__(self, cache_key, cache_args=''):
244 self.cache_key = cache_key
245 self.cache_args = cache_args
246 self.cache_active = False
247
248 def __repr__(self):
249 return "<CacheInvalidation('%s:%s')>" % (self.cache_id, self.cache_key)
250
251 class DbMigrateVersion(Base, BaseModel):
252 __tablename__ = 'db_migrate_version'
253 __table_args__ = {'useexisting':True}
254 repository_id = Column('repository_id', String(250), primary_key=True)
255 repository_path = Column('repository_path', Text)
256 version = Column('version', Integer)
257
@@ -19,25 +19,30 list=[1,2,3,4,5]
19 for SELECT use formencode.All(OneOf(list), Int())
19 for SELECT use formencode.All(OneOf(list), Int())
20
20
21 """
21 """
22 import os
23 import re
24 import logging
25
26 import formencode
22 from formencode import All
27 from formencode import All
23 from formencode.validators import UnicodeString, OneOf, Int, Number, Regex, \
28 from formencode.validators import UnicodeString, OneOf, Int, Number, Regex, \
24 Email, Bool, StringBoolean
29 Email, Bool, StringBoolean
25 from pylons import session
30
26 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
27 from rhodecode.lib.auth import check_password, get_crypt_password
32
33 import rhodecode.lib.helpers as h
34 from rhodecode.lib.auth import authenticate, get_crypt_password
35 from rhodecode.lib.exceptions import LdapImportError
28 from rhodecode.model import meta
36 from rhodecode.model import meta
29 from rhodecode.model.user_model import UserModel
37 from rhodecode.model.user import UserModel
30 from rhodecode.model.db import User, Repository
38 from rhodecode.model.repo import RepoModel
31 from sqlalchemy.exc import OperationalError
39 from rhodecode.model.db import User
32 from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
40 from rhodecode import BACKENDS
41
33 from webhelpers.pylonslib.secure_form import authentication_token
42 from webhelpers.pylonslib.secure_form import authentication_token
34 import formencode
43
35 import logging
36 import os
37 import rhodecode.lib.helpers as h
38 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
39
45
40
41 #this is needed to translate the messages using _() in validators
46 #this is needed to translate the messages using _() in validators
42 class State_obj(object):
47 class State_obj(object):
43 _ = staticmethod(_)
48 _ = staticmethod(_)
@@ -60,31 +65,74 def ValidUsername(edit, old_data):
60 def validate_python(self, value, state):
65 def validate_python(self, value, state):
61 if value in ['default', 'new_user']:
66 if value in ['default', 'new_user']:
62 raise formencode.Invalid(_('Invalid username'), value, state)
67 raise formencode.Invalid(_('Invalid username'), value, state)
63 #check if user is uniq
68 #check if user is unique
64 sa = meta.Session
65 old_un = None
69 old_un = None
66 if edit:
70 if edit:
67 old_un = sa.query(User).get(old_data.get('user_id')).username
71 old_un = UserModel().get(old_data.get('user_id')).username
68
72
69 if old_un != value or not edit:
73 if old_un != value or not edit:
70 if sa.query(User).filter(User.username == value).scalar():
74 if UserModel().get_by_username(value, cache=False,
75 case_insensitive=True):
71 raise formencode.Invalid(_('This username already exists') ,
76 raise formencode.Invalid(_('This username already exists') ,
72 value, state)
77 value, state)
73 meta.Session.remove()
78
79
80 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_]+$', value) is None:
81 raise formencode.Invalid(_('Username may only contain '
82 'alphanumeric characters underscores '
83 'or dashes and must begin with '
84 'alphanumeric character'),
85 value, state)
86
87
74
88
75 return _ValidUsername
89 return _ValidUsername
76
90
77 class ValidPassword(formencode.validators.FancyValidator):
91 class ValidPassword(formencode.validators.FancyValidator):
78
92
79 def to_python(self, value, state):
93 def to_python(self, value, state):
94
80 if value:
95 if value:
81 return get_crypt_password(value)
96
97 if value.get('password'):
98 try:
99 value['password'] = get_crypt_password(value['password'])
100 except UnicodeEncodeError:
101 e_dict = {'password':_('Invalid characters in password')}
102 raise formencode.Invalid('', value, state, error_dict=e_dict)
103
104 if value.get('password_confirmation'):
105 try:
106 value['password_confirmation'] = \
107 get_crypt_password(value['password_confirmation'])
108 except UnicodeEncodeError:
109 e_dict = {'password_confirmation':_('Invalid characters in password')}
110 raise formencode.Invalid('', value, state, error_dict=e_dict)
111
112 if value.get('new_password'):
113 try:
114 value['new_password'] = \
115 get_crypt_password(value['new_password'])
116 except UnicodeEncodeError:
117 e_dict = {'new_password':_('Invalid characters in password')}
118 raise formencode.Invalid('', value, state, error_dict=e_dict)
119
120 return value
121
122 class ValidPasswordsMatch(formencode.validators.FancyValidator):
123
124 def validate_python(self, value, state):
125
126 if value['password'] != value['password_confirmation']:
127 e_dict = {'password_confirmation':
128 _('Password do not match')}
129 raise formencode.Invalid('', value, state, error_dict=e_dict)
82
130
83 class ValidAuth(formencode.validators.FancyValidator):
131 class ValidAuth(formencode.validators.FancyValidator):
84 messages = {
132 messages = {
85 'invalid_password':_('invalid password'),
133 'invalid_password':_('invalid password'),
86 'invalid_login':_('invalid user name'),
134 'invalid_login':_('invalid user name'),
87 'disabled_account':_('Your acccount is disabled')
135 'disabled_account':_('Your account is disabled')
88
136
89 }
137 }
90 #error mapping
138 #error mapping
@@ -95,33 +143,29 class ValidAuth(formencode.validators.Fa
95 def validate_python(self, value, state):
143 def validate_python(self, value, state):
96 password = value['password']
144 password = value['password']
97 username = value['username']
145 username = value['username']
98 user = UserModel().get_user_by_name(username)
146 user = UserModel().get_by_username(username)
99 if user is None:
147
100 raise formencode.Invalid(self.message('invalid_password',
148 if authenticate(username, password):
101 state=State_obj), value, state,
102 error_dict=self.e_dict)
103 if user:
104 if user.active:
105 if user.username == username and check_password(password,
106 user.password):
107 return value
149 return value
108 else:
150 else:
151 if user and user.active is False:
152 log.warning('user %s is disabled', username)
153 raise formencode.Invalid(self.message('disabled_account',
154 state=State_obj),
155 value, state,
156 error_dict=self.e_dict_disable)
157 else:
109 log.warning('user %s not authenticated', username)
158 log.warning('user %s not authenticated', username)
110 raise formencode.Invalid(self.message('invalid_password',
159 raise formencode.Invalid(self.message('invalid_password',
111 state=State_obj), value, state,
160 state=State_obj), value, state,
112 error_dict=self.e_dict)
161 error_dict=self.e_dict)
113 else:
114 log.warning('user %s is disabled', username)
115 raise formencode.Invalid(self.message('disabled_account',
116 state=State_obj),
117 value, state,
118 error_dict=self.e_dict_disable)
119
162
120 class ValidRepoUser(formencode.validators.FancyValidator):
163 class ValidRepoUser(formencode.validators.FancyValidator):
121
164
122 def to_python(self, value, state):
165 def to_python(self, value, state):
166 sa = meta.Session()
123 try:
167 try:
124 self.user_db = meta.Session.query(User)\
168 self.user_db = sa.query(User)\
125 .filter(User.active == True)\
169 .filter(User.active == True)\
126 .filter(User.username == value).one()
170 .filter(User.username == value).one()
127 except Exception:
171 except Exception:
@@ -141,16 +185,24 def ValidRepoName(edit, old_data):
141 raise formencode.Invalid(_('This repository name is disallowed'),
185 raise formencode.Invalid(_('This repository name is disallowed'),
142 value, state)
186 value, state)
143 if old_data.get('repo_name') != value or not edit:
187 if old_data.get('repo_name') != value or not edit:
144 sa = meta.Session
188 if RepoModel().get_by_repo_name(slug, cache=False):
145 if sa.query(Repository).filter(Repository.repo_name == slug).scalar():
146 raise formencode.Invalid(_('This repository already exists') ,
189 raise formencode.Invalid(_('This repository already exists') ,
147 value, state)
190 value, state)
148 meta.Session.remove()
149 return slug
191 return slug
150
192
151
193
152 return _ValidRepoName
194 return _ValidRepoName
153
195
196 def ValidForkType(old_data):
197 class _ValidForkType(formencode.validators.FancyValidator):
198
199 def to_python(self, value, state):
200 if old_data['repo_type'] != value:
201 raise formencode.Invalid(_('Fork have to be the same type as original'),
202 value, state)
203 return value
204 return _ValidForkType
205
154 class ValidPerms(formencode.validators.FancyValidator):
206 class ValidPerms(formencode.validators.FancyValidator):
155 messages = {'perm_new_user_name':_('This username is not valid')}
207 messages = {'perm_new_user_name':_('This username is not valid')}
156
208
@@ -184,7 +236,8 class ValidPerms(formencode.validators.F
184 except Exception:
236 except Exception:
185 msg = self.message('perm_new_user_name',
237 msg = self.message('perm_new_user_name',
186 state=State_obj)
238 state=State_obj)
187 raise formencode.Invalid(msg, value, state, error_dict={'perm_new_user_name':msg})
239 raise formencode.Invalid(msg, value, state,
240 error_dict={'perm_new_user_name':msg})
188 return value
241 return value
189
242
190 class ValidSettings(formencode.validators.FancyValidator):
243 class ValidSettings(formencode.validators.FancyValidator):
@@ -198,26 +251,23 class ValidSettings(formencode.validator
198
251
199 class ValidPath(formencode.validators.FancyValidator):
252 class ValidPath(formencode.validators.FancyValidator):
200 def to_python(self, value, state):
253 def to_python(self, value, state):
201 isdir = os.path.isdir(value.replace('*', ''))
254
202 if (value.endswith('/*') or value.endswith('/**')) and isdir:
255 if not os.path.isdir(value):
203 return value
204 elif not isdir:
205 msg = _('This is not a valid path')
256 msg = _('This is not a valid path')
206 else:
207 msg = _('You need to specify * or ** at the end of path (ie. /tmp/*)')
208
209 raise formencode.Invalid(msg, value, state,
257 raise formencode.Invalid(msg, value, state,
210 error_dict={'paths_root_path':msg})
258 error_dict={'paths_root_path':msg})
259 return value
211
260
212 def UniqSystemEmail(old_data):
261 def UniqSystemEmail(old_data):
213 class _UniqSystemEmail(formencode.validators.FancyValidator):
262 class _UniqSystemEmail(formencode.validators.FancyValidator):
214 def to_python(self, value, state):
263 def to_python(self, value, state):
264 value = value.lower()
215 if old_data.get('email') != value:
265 if old_data.get('email') != value:
216 sa = meta.Session
266 sa = meta.Session()
217 try:
267 try:
218 user = sa.query(User).filter(User.email == value).scalar()
268 user = sa.query(User).filter(User.email == value).scalar()
219 if user:
269 if user:
220 raise formencode.Invalid(_("That e-mail address is already taken") ,
270 raise formencode.Invalid(_("This e-mail address is already taken") ,
221 value, state)
271 value, state)
222 finally:
272 finally:
223 meta.Session.remove()
273 meta.Session.remove()
@@ -228,17 +278,48 def UniqSystemEmail(old_data):
228
278
229 class ValidSystemEmail(formencode.validators.FancyValidator):
279 class ValidSystemEmail(formencode.validators.FancyValidator):
230 def to_python(self, value, state):
280 def to_python(self, value, state):
281 value = value.lower()
231 sa = meta.Session
282 sa = meta.Session
232 try:
283 try:
233 user = sa.query(User).filter(User.email == value).scalar()
284 user = sa.query(User).filter(User.email == value).scalar()
234 if user is None:
285 if user is None:
235 raise formencode.Invalid(_("That e-mail address doesn't exist.") ,
286 raise formencode.Invalid(_("This e-mail address doesn't exist.") ,
236 value, state)
287 value, state)
237 finally:
288 finally:
238 meta.Session.remove()
289 meta.Session.remove()
239
290
240 return value
291 return value
241
292
293 class LdapLibValidator(formencode.validators.FancyValidator):
294
295 def to_python(self, value, state):
296
297 try:
298 import ldap
299 except ImportError:
300 raise LdapImportError
301 return value
302
303 class BaseDnValidator(formencode.validators.FancyValidator):
304
305 def to_python(self, value, state):
306
307 try:
308 value % {'user':'valid'}
309
310 if value.find('%(user)s') == -1:
311 raise formencode.Invalid(_("You need to specify %(user)s in "
312 "template for example uid=%(user)s "
313 ",dc=company...") ,
314 value, state)
315
316 except KeyError:
317 raise formencode.Invalid(_("Wrong template used, only %(user)s "
318 "is an valid entry") ,
319 value, state)
320
321 return value
322
242 #===============================================================================
323 #===============================================================================
243 # FORMS
324 # FORMS
244 #===============================================================================
325 #===============================================================================
@@ -271,20 +352,38 def UserForm(edit=False, old_data={}):
271 class _UserForm(formencode.Schema):
352 class _UserForm(formencode.Schema):
272 allow_extra_fields = True
353 allow_extra_fields = True
273 filter_extra_fields = True
354 filter_extra_fields = True
274 username = All(UnicodeString(strip=True, min=1, not_empty=True), ValidUsername(edit, old_data))
355 username = All(UnicodeString(strip=True, min=1, not_empty=True),
356 ValidUsername(edit, old_data))
275 if edit:
357 if edit:
276 new_password = All(UnicodeString(strip=True, min=6, not_empty=False), ValidPassword)
358 new_password = All(UnicodeString(strip=True, min=6, not_empty=False))
277 admin = StringBoolean(if_missing=False)
359 admin = StringBoolean(if_missing=False)
278 else:
360 else:
279 password = All(UnicodeString(strip=True, min=6, not_empty=True), ValidPassword)
361 password = All(UnicodeString(strip=True, min=6, not_empty=True))
280 active = StringBoolean(if_missing=False)
362 active = StringBoolean(if_missing=False)
281 name = UnicodeString(strip=True, min=1, not_empty=True)
363 name = UnicodeString(strip=True, min=1, not_empty=True)
282 lastname = UnicodeString(strip=True, min=1, not_empty=True)
364 lastname = UnicodeString(strip=True, min=1, not_empty=True)
283 email = All(Email(not_empty=True), UniqSystemEmail(old_data))
365 email = All(Email(not_empty=True), UniqSystemEmail(old_data))
284
366
367 chained_validators = [ValidPassword]
368
285 return _UserForm
369 return _UserForm
286
370
287 RegisterForm = UserForm
371 def RegisterForm(edit=False, old_data={}):
372 class _RegisterForm(formencode.Schema):
373 allow_extra_fields = True
374 filter_extra_fields = True
375 username = All(ValidUsername(edit, old_data),
376 UnicodeString(strip=True, min=1, not_empty=True))
377 password = All(UnicodeString(strip=True, min=6, not_empty=True))
378 password_confirmation = All(UnicodeString(strip=True, min=6, not_empty=True))
379 active = StringBoolean(if_missing=False)
380 name = UnicodeString(strip=True, min=1, not_empty=True)
381 lastname = UnicodeString(strip=True, min=1, not_empty=True)
382 email = All(Email(not_empty=True), UniqSystemEmail(old_data))
383
384 chained_validators = [ValidPasswordsMatch, ValidPassword]
385
386 return _RegisterForm
288
387
289 def PasswordResetForm():
388 def PasswordResetForm():
290 class _PasswordResetForm(formencode.Schema):
389 class _PasswordResetForm(formencode.Schema):
@@ -293,35 +392,39 def PasswordResetForm():
293 email = All(ValidSystemEmail(), Email(not_empty=True))
392 email = All(ValidSystemEmail(), Email(not_empty=True))
294 return _PasswordResetForm
393 return _PasswordResetForm
295
394
296 def RepoForm(edit=False, old_data={}):
395 def RepoForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
297 class _RepoForm(formencode.Schema):
396 class _RepoForm(formencode.Schema):
298 allow_extra_fields = True
397 allow_extra_fields = True
299 filter_extra_fields = False
398 filter_extra_fields = False
300 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True), ValidRepoName(edit, old_data))
399 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
400 ValidRepoName(edit, old_data))
301 description = UnicodeString(strip=True, min=1, not_empty=True)
401 description = UnicodeString(strip=True, min=1, not_empty=True)
302 private = StringBoolean(if_missing=False)
402 private = StringBoolean(if_missing=False)
303
403 enable_statistics = StringBoolean(if_missing=False)
404 repo_type = OneOf(supported_backends)
304 if edit:
405 if edit:
305 user = All(Int(not_empty=True), ValidRepoUser)
406 user = All(Int(not_empty=True), ValidRepoUser)
306
407
307 chained_validators = [ValidPerms]
408 chained_validators = [ValidPerms]
308 return _RepoForm
409 return _RepoForm
309
410
310 def RepoForkForm(edit=False, old_data={}):
411 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
311 class _RepoForkForm(formencode.Schema):
412 class _RepoForkForm(formencode.Schema):
312 allow_extra_fields = True
413 allow_extra_fields = True
313 filter_extra_fields = False
414 filter_extra_fields = False
314 fork_name = All(UnicodeString(strip=True, min=1, not_empty=True), ValidRepoName(edit, old_data))
415 fork_name = All(UnicodeString(strip=True, min=1, not_empty=True),
416 ValidRepoName(edit, old_data))
315 description = UnicodeString(strip=True, min=1, not_empty=True)
417 description = UnicodeString(strip=True, min=1, not_empty=True)
316 private = StringBoolean(if_missing=False)
418 private = StringBoolean(if_missing=False)
317
419 repo_type = All(ValidForkType(old_data), OneOf(supported_backends))
318 return _RepoForkForm
420 return _RepoForkForm
319
421
320 def RepoSettingsForm(edit=False, old_data={}):
422 def RepoSettingsForm(edit=False, old_data={}):
321 class _RepoForm(formencode.Schema):
423 class _RepoForm(formencode.Schema):
322 allow_extra_fields = True
424 allow_extra_fields = True
323 filter_extra_fields = False
425 filter_extra_fields = False
324 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True), ValidRepoName(edit, old_data))
426 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
427 ValidRepoName(edit, old_data))
325 description = UnicodeString(strip=True, min=1, not_empty=True)
428 description = UnicodeString(strip=True, min=1, not_empty=True)
326 private = StringBoolean(if_missing=False)
429 private = StringBoolean(if_missing=False)
327
430
@@ -346,6 +449,8 def ApplicationUiSettingsForm():
346 paths_root_path = All(ValidPath(), UnicodeString(strip=True, min=1, not_empty=True))
449 paths_root_path = All(ValidPath(), UnicodeString(strip=True, min=1, not_empty=True))
347 hooks_changegroup_update = OneOf(['True', 'False'], if_missing=False)
450 hooks_changegroup_update = OneOf(['True', 'False'], if_missing=False)
348 hooks_changegroup_repo_size = OneOf(['True', 'False'], if_missing=False)
451 hooks_changegroup_repo_size = OneOf(['True', 'False'], if_missing=False)
452 hooks_pretxnchangegroup_push_logger = OneOf(['True', 'False'], if_missing=False)
453 hooks_preoutgoing_pull_logger = OneOf(['True', 'False'], if_missing=False)
349
454
350 return _ApplicationUiSettingsForm
455 return _ApplicationUiSettingsForm
351
456
@@ -353,9 +458,26 def DefaultPermissionsForm(perms_choices
353 class _DefaultPermissionsForm(formencode.Schema):
458 class _DefaultPermissionsForm(formencode.Schema):
354 allow_extra_fields = True
459 allow_extra_fields = True
355 filter_extra_fields = True
460 filter_extra_fields = True
356 overwrite_default = OneOf(['true', 'false'], if_missing='false')
461 overwrite_default = StringBoolean(if_missing=False)
462 anonymous = OneOf(['True', 'False'], if_missing=False)
357 default_perm = OneOf(perms_choices)
463 default_perm = OneOf(perms_choices)
358 default_register = OneOf(register_choices)
464 default_register = OneOf(register_choices)
359 default_create = OneOf(create_choices)
465 default_create = OneOf(create_choices)
360
466
361 return _DefaultPermissionsForm
467 return _DefaultPermissionsForm
468
469
470 def LdapSettingsForm():
471 class _LdapSettingsForm(formencode.Schema):
472 allow_extra_fields = True
473 filter_extra_fields = True
474 pre_validators = [LdapLibValidator]
475 ldap_active = StringBoolean(if_missing=False)
476 ldap_host = UnicodeString(strip=True,)
477 ldap_port = Number(strip=True,)
478 ldap_ldaps = StringBoolean(if_missing=False)
479 ldap_dn_user = UnicodeString(strip=True,)
480 ldap_dn_pass = UnicodeString(strip=True,)
481 ldap_base_dn = All(BaseDnValidator, UnicodeString(strip=True,))
482
483 return _LdapSettingsForm
@@ -1,8 +1,15
1 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 # encoding: utf-8
2 """
3 # Model for permissions
3 rhodecode.model.permission
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 permissions model for RhodeCode
7
8 :created_on: Aug 20, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
6 # This program is free software; you can redistribute it and/or
13 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
14 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; version 2
15 # as published by the Free Software Foundation; version 2
@@ -17,72 +24,91
17 # along with this program; if not, write to the Free Software
24 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 # MA 02110-1301, USA.
26 # MA 02110-1301, USA.
20 """
21 Created on Aug 20, 2010
22 Model for permissions
23 @author: marcink
24 """
25
27
26 from pylons.i18n.translation import _
27 from rhodecode.model.db import User, Permission, UserToPerm, RepoToPerm
28 from rhodecode.model.meta import Session
29 import logging
28 import logging
30 import traceback
29 import traceback
30
31 from sqlalchemy.exc import DatabaseError
32
33 from rhodecode.model import BaseModel
34 from rhodecode.model.db import User, Permission, UserToPerm, RepoToPerm
35 from rhodecode.model.caching_query import FromCache
36
31 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
32
38
33
39
34 class PermissionModel(object):
40 class PermissionModel(BaseModel):
41 """Permissions model for RhodeCode
42 """
35
43
36 def __init__(self):
44 def get_permission(self, permission_id, cache=False):
37 self.sa = Session()
45 """Get's permissions by id
38
39 def get_default(self):
40 return self.sa.query(User).filter(User.username == 'default').scalar()
41
46
42 def get_permission(self, id):
47 :param permission_id: id of permission to get from database
43 return self.sa.query(Permission).get(id)
48 :param cache: use Cache for this query
49 """
50 perm = self.sa.query(Permission)
51 if cache:
52 perm = perm.options(FromCache("sql_cache_short",
53 "get_permission_%s" % permission_id))
54 return perm.get(permission_id)
44
55
45 def get_permission_by_name(self, name):
56 def get_permission_by_name(self, name, cache=False):
46 return self.sa.query(Permission)\
57 """Get's permissions by given name
47 .filter(Permission.permission_name == name).scalar()
48
58
59 :param name: name to fetch
60 :param cache: Use cache for this query
61 """
62 perm = self.sa.query(Permission)\
63 .filter(Permission.permission_name == name)
64 if cache:
65 perm = perm.options(FromCache("sql_cache_short",
66 "get_permission_%s" % name))
67 return perm.scalar()
49
68
50 def update(self, form_result):
69 def update(self, form_result):
51 perm_user = self.sa.query(User)\
70 perm_user = self.sa.query(User)\
52 .filter(User.username == form_result['perm_user_name']).scalar()
71 .filter(User.username == form_result['perm_user_name']).scalar()
53 u2p = self.sa.query(UserToPerm).filter(UserToPerm.user == perm_user).all()
72 u2p = self.sa.query(UserToPerm).filter(UserToPerm.user == perm_user).all()
54 if len(u2p) != 3:
73 if len(u2p) != 3:
55 raise Exception('There is more than 3 defined \
74 raise Exception('Defined: %s should be 3 permissions for default'
56 permissions for defualt user. This should not happen please verify\
75 ' user. This should not happen please verify'
57 your database')
76 ' your database' % len(u2p))
58
77
59 try:
78 try:
60 #stage 1 change defaults
79 #stage 1 change defaults
61 for p in u2p:
80 for p in u2p:
62 if p.permission.permission_name.startswith('repository.'):
81 if p.permission.permission_name.startswith('repository.'):
63 p.permission = self.get_permission_by_name(form_result['default_perm'])
82 p.permission = self.get_permission_by_name(
83 form_result['default_perm'])
64 self.sa.add(p)
84 self.sa.add(p)
65
85
66 if p.permission.permission_name.startswith('hg.register.'):
86 if p.permission.permission_name.startswith('hg.register.'):
67 p.permission = self.get_permission_by_name(form_result['default_register'])
87 p.permission = self.get_permission_by_name(
88 form_result['default_register'])
68 self.sa.add(p)
89 self.sa.add(p)
69
90
70 if p.permission.permission_name.startswith('hg.create.'):
91 if p.permission.permission_name.startswith('hg.create.'):
71 p.permission = self.get_permission_by_name(form_result['default_create'])
92 p.permission = self.get_permission_by_name(
93 form_result['default_create'])
72 self.sa.add(p)
94 self.sa.add(p)
95
73 #stage 2 update all default permissions for repos if checked
96 #stage 2 update all default permissions for repos if checked
74 if form_result['overwrite_default'] == 'true':
97 if form_result['overwrite_default'] == True:
75 for r2p in self.sa.query(RepoToPerm).filter(RepoToPerm.user == perm_user).all():
98 for r2p in self.sa.query(RepoToPerm)\
76 r2p.permission = self.get_permission_by_name(form_result['default_perm'])
99 .filter(RepoToPerm.user == perm_user).all():
100 r2p.permission = self.get_permission_by_name(
101 form_result['default_perm'])
77 self.sa.add(r2p)
102 self.sa.add(r2p)
78
103
104 #stage 3 set anonymous access
105 if perm_user.username == 'default':
106 perm_user.active = bool(form_result['anonymous'])
107 self.sa.add(perm_user)
108
109
79 self.sa.commit()
110 self.sa.commit()
80 except:
111 except (DatabaseError,):
81 log.error(traceback.format_exc())
112 log.error(traceback.format_exc())
82 self.sa.rollback()
113 self.sa.rollback()
83 raise
114 raise
84
85
86
87
88
@@ -1,7 +1,15
1 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 # encoding: utf-8
2 """
3 # model for handling repositories actions
3 rhodecode.model.repo
4 # Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
4 ~~~~~~~~~~~~~~~~~~~~
5
6 Repository model for rhodecode
7
8 :created_on: Jun 5, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
5 # This program is free software; you can redistribute it and/or
13 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
14 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; version 2
15 # as published by the Free Software Foundation; version 2
@@ -16,35 +24,44
16 # along with this program; if not, write to the Free Software
24 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18 # MA 02110-1301, USA.
26 # MA 02110-1301, USA.
19 """
20 Created on Jun 5, 2010
21 model for handling repositories actions
22 @author: marcink
23 """
24 from datetime import datetime
25 from pylons import app_globals as g
26 from rhodecode.lib.utils import check_repo
27 from rhodecode.model.db import Repository, RepoToPerm, User, Permission
28 from rhodecode.model.meta import Session
29 from rhodecode.model.user_model import UserModel
30 from rhodecode.lib.celerylib.tasks import create_repo_fork, run_task
31 import logging
32 import os
27 import os
33 import shutil
28 import shutil
29 import logging
34 import traceback
30 import traceback
31 from datetime import datetime
32
33 from pylons import app_globals as g
34
35 from rhodecode.model import BaseModel
36 from rhodecode.model.caching_query import FromCache
37 from rhodecode.model.db import Repository, RepoToPerm, User, Permission, \
38 Statistics
39 from rhodecode.model.user import UserModel
40
41 from vcs.backends import get_backend
42
35 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
36
44
37 class RepoModel(object):
45 class RepoModel(BaseModel):
46
47 def get(self, repo_id, cache=False):
48 repo = self.sa.query(Repository)\
49 .filter(Repository.repo_id == repo_id)
38
50
39 def __init__(self, sa=None):
51 if cache:
40 if not sa:
52 repo = repo.options(FromCache("sql_cache_short",
41 self.sa = Session()
53 "get_repo_%s" % repo_id))
42 else:
54 return repo.scalar()
43 self.sa = sa
55
44
56
45 def get(self, id):
57 def get_by_repo_name(self, repo_name, cache=False):
46 return self.sa.query(Repository)\
58 repo = self.sa.query(Repository)\
47 .filter(Repository.repo_name == id).scalar()
59 .filter(Repository.repo_name == repo_name)
60
61 if cache:
62 repo = repo.options(FromCache("sql_cache_short",
63 "get_repo_%s" % repo_name))
64 return repo.scalar()
48
65
49 def get_users_js(self):
66 def get_users_js(self):
50
67
@@ -58,38 +75,36 class RepoModel(object):
58
75
59 def update(self, repo_name, form_data):
76 def update(self, repo_name, form_data):
60 try:
77 try:
78 cur_repo = self.get_by_repo_name(repo_name, cache=False)
79 user_model = UserModel(self.sa)
61
80
62 #update permissions
81 #update permissions
63 for username, perm in form_data['perms_updates']:
82 for username, perm in form_data['perms_updates']:
64 r2p = self.sa.query(RepoToPerm)\
83 r2p = self.sa.query(RepoToPerm)\
65 .filter(RepoToPerm.user == self.sa.query(User)\
84 .filter(RepoToPerm.user == user_model.get_by_username(username))\
66 .filter(User.username == username).one())\
85 .filter(RepoToPerm.repository == cur_repo)\
67 .filter(RepoToPerm.repository == self.get(repo_name))\
68 .one()
86 .one()
69
87
70 r2p.permission_id = self.sa.query(Permission).filter(
88 r2p.permission = self.sa.query(Permission)\
71 Permission.permission_name ==
89 .filter(Permission.permission_name == perm)\
72 perm).one().permission_id
90 .scalar()
73 self.sa.add(r2p)
91 self.sa.add(r2p)
74
92
75 #set new permissions
93 #set new permissions
76 for username, perm in form_data['perms_new']:
94 for username, perm in form_data['perms_new']:
77 r2p = RepoToPerm()
95 r2p = RepoToPerm()
78 r2p.repository = self.get(repo_name)
96 r2p.repository = cur_repo
79 r2p.user = self.sa.query(User)\
97 r2p.user = user_model.get_by_username(username, cache=False)
80 .filter(User.username == username).one()
81
98
82 r2p.permission_id = self.sa.query(Permission).filter(
99 r2p.permission = self.sa.query(Permission)\
83 Permission.permission_name == perm)\
100 .filter(Permission.permission_name == perm)\
84 .one().permission_id
101 .scalar()
85 self.sa.add(r2p)
102 self.sa.add(r2p)
86
103
87 #update current repo
104 #update current repo
88 cur_repo = self.get(repo_name)
89
90 for k, v in form_data.items():
105 for k, v in form_data.items():
91 if k == 'user':
106 if k == 'user':
92 cur_repo.user_id = v
107 cur_repo.user = user_model.get(v)
93 else:
108 else:
94 setattr(cur_repo, k, v)
109 setattr(cur_repo, k, v)
95
110
@@ -108,12 +123,14 class RepoModel(object):
108 def create(self, form_data, cur_user, just_db=False, fork=False):
123 def create(self, form_data, cur_user, just_db=False, fork=False):
109 try:
124 try:
110 if fork:
125 if fork:
126 #force str since hg doesn't go with unicode
111 repo_name = str(form_data['fork_name'])
127 repo_name = str(form_data['fork_name'])
112 org_name = str(form_data['repo_name'])
128 org_name = str(form_data['repo_name'])
113
129
114 else:
130 else:
115 org_name = repo_name = str(form_data['repo_name'])
131 org_name = repo_name = str(form_data['repo_name'])
116 new_repo = Repository()
132 new_repo = Repository()
133 new_repo.enable_statistics = True
117 for k, v in form_data.items():
134 for k, v in form_data.items():
118 if k == 'repo_name':
135 if k == 'repo_name':
119 v = repo_name
136 v = repo_name
@@ -130,7 +147,7 class RepoModel(object):
130 #create default permission
147 #create default permission
131 repo_to_perm = RepoToPerm()
148 repo_to_perm = RepoToPerm()
132 default = 'repository.read'
149 default = 'repository.read'
133 for p in UserModel(self.sa).get_default().user_perms:
150 for p in UserModel(self.sa).get_by_username('default', cache=False).user_perms:
134 if p.permission.permission_name.startswith('repository.'):
151 if p.permission.permission_name.startswith('repository.'):
135 default = p.permission.permission_name
152 default = p.permission.permission_name
136 break
153 break
@@ -142,26 +159,34 class RepoModel(object):
142 .one().permission_id
159 .one().permission_id
143
160
144 repo_to_perm.repository_id = new_repo.repo_id
161 repo_to_perm.repository_id = new_repo.repo_id
145 repo_to_perm.user_id = self.sa.query(User)\
162 repo_to_perm.user_id = UserModel(self.sa)\
146 .filter(User.username == 'default').one().user_id
163 .get_by_username('default', cache=False).user_id
147
164
148 self.sa.add(repo_to_perm)
165 self.sa.add(repo_to_perm)
149 self.sa.commit()
166 self.sa.commit()
167
168
169 #now automatically start following this repository as owner
170 from rhodecode.model.scm import ScmModel
171 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
172 cur_user.user_id)
173
150 if not just_db:
174 if not just_db:
151 self.__create_repo(repo_name)
175 self.__create_repo(repo_name, form_data['repo_type'])
152 except:
176 except:
153 log.error(traceback.format_exc())
177 log.error(traceback.format_exc())
154 self.sa.rollback()
178 self.sa.rollback()
155 raise
179 raise
156
180
157 def create_fork(self, form_data, cur_user):
181 def create_fork(self, form_data, cur_user):
158 run_task(create_repo_fork, form_data, cur_user)
182 from rhodecode.lib.celerylib import tasks, run_task
183 run_task(tasks.create_repo_fork, form_data, cur_user)
159
184
160 def delete(self, repo):
185 def delete(self, repo):
161 try:
186 try:
162 self.sa.delete(repo)
187 self.sa.delete(repo)
188 self.__delete_repo(repo)
163 self.sa.commit()
189 self.sa.commit()
164 self.__delete_repo(repo.repo_name)
165 except:
190 except:
166 log.error(traceback.format_exc())
191 log.error(traceback.format_exc())
167 self.sa.rollback()
192 self.sa.rollback()
@@ -170,7 +195,8 class RepoModel(object):
170 def delete_perm_user(self, form_data, repo_name):
195 def delete_perm_user(self, form_data, repo_name):
171 try:
196 try:
172 self.sa.query(RepoToPerm)\
197 self.sa.query(RepoToPerm)\
173 .filter(RepoToPerm.repository == self.get(repo_name))\
198 .filter(RepoToPerm.repository \
199 == self.get_by_repo_name(repo_name))\
174 .filter(RepoToPerm.user_id == form_data['user_id']).delete()
200 .filter(RepoToPerm.user_id == form_data['user_id']).delete()
175 self.sa.commit()
201 self.sa.commit()
176 except:
202 except:
@@ -178,14 +204,37 class RepoModel(object):
178 self.sa.rollback()
204 self.sa.rollback()
179 raise
205 raise
180
206
181 def __create_repo(self, repo_name):
207 def delete_stats(self, repo_name):
208 try:
209 self.sa.query(Statistics)\
210 .filter(Statistics.repository == \
211 self.get_by_repo_name(repo_name)).delete()
212 self.sa.commit()
213 except:
214 log.error(traceback.format_exc())
215 self.sa.rollback()
216 raise
217
218
219 def __create_repo(self, repo_name, alias):
220 """
221 makes repository on filesystem
222 :param repo_name:
223 :param alias:
224 """
225 from rhodecode.lib.utils import check_repo
182 repo_path = os.path.join(g.base_path, repo_name)
226 repo_path = os.path.join(g.base_path, repo_name)
183 if check_repo(repo_name, g.base_path):
227 if check_repo(repo_name, g.base_path):
184 log.info('creating repo %s in %s', repo_name, repo_path)
228 log.info('creating repo %s in %s', repo_name, repo_path)
185 from vcs.backends.hg import MercurialRepository
229 backend = get_backend(alias)
186 MercurialRepository(repo_path, create=True)
230 backend(repo_path, create=True)
187
231
188 def __rename_repo(self, old, new):
232 def __rename_repo(self, old, new):
233 """
234 renames repository on filesystem
235 :param old: old name
236 :param new: new name
237 """
189 log.info('renaming repo from %s to %s', old, new)
238 log.info('renaming repo from %s to %s', old, new)
190
239
191 old_path = os.path.join(g.base_path, old)
240 old_path = os.path.join(g.base_path, old)
@@ -195,11 +244,20 class RepoModel(object):
195 new_path)
244 new_path)
196 shutil.move(old_path, new_path)
245 shutil.move(old_path, new_path)
197
246
198 def __delete_repo(self, name):
247 def __delete_repo(self, repo):
199 rm_path = os.path.join(g.base_path, name)
248 """
249 removes repo from filesystem, the removal is acctually made by
250 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
251 repository is no longer valid for rhodecode, can be undeleted later on
252 by reverting the renames on this repository
253 :param repo: repo object
254 """
255 rm_path = os.path.join(g.base_path, repo.repo_name)
200 log.info("Removing %s", rm_path)
256 log.info("Removing %s", rm_path)
201 #disable hg
257 #disable hg/git
202 shutil.move(os.path.join(rm_path, '.hg'), os.path.join(rm_path, 'rm__.hg'))
258 alias = repo.repo_type
259 shutil.move(os.path.join(rm_path, '.%s' % alias),
260 os.path.join(rm_path, 'rm__.%s' % alias))
203 #disable repo
261 #disable repo
204 shutil.move(rm_path, os.path.join(g.base_path, 'rm__%s__%s' \
262 shutil.move(rm_path, os.path.join(g.base_path, 'rm__%s__%s' \
205 % (datetime.today(), name)))
263 % (datetime.today(), repo.repo_name)))
1 NO CONTENT: file renamed from rhodecode/model/hg_model.py to rhodecode/model/scm.py
NO CONTENT: file renamed from rhodecode/model/hg_model.py to rhodecode/model/scm.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rhodecode/model/user_model.py to rhodecode/model/user.py
NO CONTENT: file renamed from rhodecode/model/user_model.py to rhodecode/model/user.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rhodecode/tests/functional/test_permissions.py to rhodecode/tests/functional/test_admin_permissions.py
NO CONTENT: file renamed from rhodecode/tests/functional/test_permissions.py to rhodecode/tests/functional/test_admin_permissions.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rhodecode/tests/functional/test_repos.py to rhodecode/tests/functional/test_admin_repos.py
NO CONTENT: file renamed from rhodecode/tests/functional/test_repos.py to rhodecode/tests/functional/test_admin_repos.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rhodecode/tests/functional/test_users.py to rhodecode/tests/functional/test_admin_users.py
NO CONTENT: file renamed from rhodecode/tests/functional/test_users.py to rhodecode/tests/functional/test_admin_users.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rhodecode/tests/functional/test_hg.py to rhodecode/tests/functional/test_home.py
NO CONTENT: file renamed from rhodecode/tests/functional/test_hg.py to rhodecode/tests/functional/test_home.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed, binary diff hidden
NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed, binary diff hidden
NO CONTENT: file was removed, binary diff hidden
General Comments 0
You need to be logged in to leave comments. Login now