# HG changeset patch # User Marcin Kuzminski # Date 1292679958 -3600 # Node ID 3d0661b8aaa481f55ea3ade7d6f9ea09cb671736 # Parent 06cfcede13ab779e80029acd0bff40bfa07965f8# Parent 86cbf8e6d76a9011b0da2c9418f8f76346a457db merged with beta branch diff -r 06cfcede13ab -r 3d0661b8aaa4 .hgignore --- a/.hgignore Thu Nov 18 21:35:52 2010 +0100 +++ b/.hgignore Sat Dec 18 14:45:58 2010 +0100 @@ -1,5 +1,11 @@ +syntax: glob +*.pyc +*.swp syntax: regexp +^build +^docs/build/ +^docs/_build/ ^data$ ^\.settings$ ^\.project$ @@ -7,4 +13,4 @@ ^rhodecode\.db$ ^test\.db$ ^repositories\.config$ -^RhodeCode\.egg-info$ \ No newline at end of file +^RhodeCode\.egg-info$ diff -r 06cfcede13ab -r 3d0661b8aaa4 MANIFEST.in --- a/MANIFEST.in Thu Nov 18 21:35:52 2010 +0100 +++ b/MANIFEST.in Sat Dec 18 14:45:58 2010 +0100 @@ -1,4 +1,5 @@ include rhodecode/config/deployment.ini_tmpl +include rhodecode/lib/dbmigrate/migrate.cfg include README.rst recursive-include rhodecode/i18n/ * @@ -7,7 +8,7 @@ recursive-include rhodecode/public/css * recursive-include rhodecode/public/images * #js -include rhodecode/public/js/yui2.js +include rhodecode/public/js/yui2a.js include rhodecode/public/js/excanvas.min.js include rhodecode/public/js/yui.flot.js include rhodecode/public/js/graph.js diff -r 06cfcede13ab -r 3d0661b8aaa4 README.rst --- a/README.rst Thu Nov 18 21:35:52 2010 +0100 +++ b/README.rst Sat Dec 18 14:45:58 2010 +0100 @@ -1,23 +1,25 @@ -RhodeCode (RhodiumCode) -======================= +================================================= +Welcome to RhodeCode (RhodiumCode) documentation! +================================================= -``RhodeCode`` (formerly hg-app) is Pylons based repository management and -serving for mercurial_. It's similar to github or bitbucket, but it's suppose to run -as standalone app, it's open source and focuses more on restricted access to repositories -There's no default free access to RhodeCode You have to create an account in order -to use the application. It's powered by vcs_ library that we created to handle -many various version control systems. +``RhodeCode`` (formerly hg-app) is Pylons framework based Mercurial repository +browser/management with build in push/pull server and full text search. +It works on http/https, has build in permission/authentication(+ldap) features +It's similar to github or bitbucket, but it's suppose to run as standalone +hosted application, it's open source and focuses more on restricted access to +repositories. It's powered by vcs_ library that me and Lukasz Balcerzak created +to handle many various version control systems. RhodeCode uses `Semantic Versioning `_ - RhodeCode demo -------------- http://hg.python-works.com -The default access is +The default access is anonymous but You can login to administrative account +using those credentials - username: demo - password: demo @@ -25,14 +27,14 @@ Source code ----------- -Source code is along with issue tracker is available at +The most up to date sources can be obtained from my own RhodeCode instance +https://rhodecode.org + +Rarely updated source code and issue tracker is available at bitbcuket http://bitbucket.org/marcinkuzminski/rhodecode -Also a source codes can be obtained from demo rhodecode instance -http://hg.python-works.com/rhodecode/summary - -Instalation ------------ +Installation +------------ Please visit http://packages.python.org/RhodeCode/installation.html @@ -40,41 +42,49 @@ Features -------- -- Has it's own middleware to handle mercurial_ protocol request. Each request - can be logged and authenticated. Runs on threads unlikely to hgweb You can - make multiple pulls/pushes simultaneous. Supports http/https -- Full permissions and authentication per project private/read/write/admin. - One account for web interface and mercurial_ push/pull/clone. +- Has it's own middleware to handle mercurial_ protocol request. + Each request can be logged and authenticated. Runs on threads unlikely to + hgweb. You can make multiple pulls/pushes simultaneous. Supports http/https + and ldap +- Full permissions (private/read/write/admin) and authentication per project. + One account for web interface and mercurial_ push/pull/clone operations. - Mako templates let's you customize look and feel of application. - Beautiful diffs, annotations and source codes all colored by pygments. - Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics -- Admin interface with user/permission management. User activity journal logs - pulls, pushes, forks,registrations. Possible to disable built in hooks +- Admin interface with user/permission management. Admin activity journal, logs + pulls, pushes, forks, registrations and other actions made by all users. - Server side forks, it's possible to fork a project and hack it free without - breaking the main. -- Full text search on source codes, search on file names. All powered by whoosh - and build in indexing daemons + breaking the main repository. +- Full text search powered by Whoosh on source codes, and file names. + Build in indexing daemons, with optional incremental index build (no external search servers required all in one application) -- Rss / atom feeds, gravatar support, download sources as zip/tarballs +- Setup project descriptions and info inside built in db for easy, non + file-system operations +- Inteligent cache with invalidation after push or project change, provides high + performance and always up to date data. +- Rss / atom feeds, gravatar support, download sources as zip/tar/gz - Async tasks for speed and performance using celery_ (works without them too) - Backup scripts can do backup of whole app and send it over scp to desired - location -- Setup project descriptions and info inside built in db for easy, non - file-system operations -- Added cache with invalidation on push/repo management for high performance and - always up to date data. -- Based on pylons 1.0 / sqlalchemy 0.6 / sqlite + location +- Based on pylons / sqlalchemy / sqlite / whoosh / vcs -Incoming --------- +.. include:: ./docs/screenshots.rst + + +Incoming / Plans +---------------- +- project grouping +- User groups/teams - code review (probably based on hg-review) -- full git_ support, with push/pull server +- full git_ support, with push/pull server (currently in beta tests) +- redmine integration +- public accessible activity feeds - commit based build in wiki system - clone points and cloning from remote repositories into rhodecode (git_ and mercurial_) -- some cache optimizations +- more statistics and graph (global annotation + some more statistics) - other cools stuff that i can figure out (or You can help me figure out) License @@ -83,8 +93,18 @@ ``rhodecode`` is released under GPL_ license. -Documentation -------------- +Mailing group Q&A +----------------- + +join the `Google group `_ + +open an issue at `issue tracker `_ + +join #rhodecode on FreeNode (irc.freenode.net) +or use http://webchat.freenode.net/?channels=rhodecode for web access to irc. + +Online documentation +-------------------- Online documentation for current version is available at http://packages.python.org/RhodeCode/. @@ -92,13 +112,3 @@ make html -.. _virtualenv: http://pypi.python.org/pypi/virtualenv -.. _python: http://www.python.org/ -.. _django: http://www.djangoproject.com/ -.. _mercurial: http://mercurial.selenic.com/ -.. _subversion: http://subversion.tigris.org/ -.. _git: http://git-scm.com/ -.. _celery: http://celeryproject.org/ -.. _Sphinx: http://sphinx.pocoo.org/ -.. _GPL: http://www.gnu.org/licenses/gpl.html -.. _vcs: http://pypi.python.org/pypi/vcs \ No newline at end of file diff -r 06cfcede13ab -r 3d0661b8aaa4 celeryconfig.py --- a/celeryconfig.py Thu Nov 18 21:35:52 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,77 +0,0 @@ -# List of modules to import when celery starts. -import sys -import os -import ConfigParser -root = os.getcwd() - -PYLONS_CONFIG_NAME = 'production.ini' - -sys.path.append(root) -config = ConfigParser.ConfigParser({'here':root}) -config.read('%s/%s' % (root, PYLONS_CONFIG_NAME)) -PYLONS_CONFIG = config - -CELERY_IMPORTS = ("rhodecode.lib.celerylib.tasks",) - -## Result store settings. -CELERY_RESULT_BACKEND = "database" -CELERY_RESULT_DBURI = dict(config.items('app:main'))['sqlalchemy.db1.url'] -CELERY_RESULT_SERIALIZER = 'json' - - -BROKER_CONNECTION_MAX_RETRIES = 30 - -## Broker settings. -BROKER_HOST = "localhost" -BROKER_PORT = 5672 -BROKER_VHOST = "rabbitmqhost" -BROKER_USER = "rabbitmq" -BROKER_PASSWORD = "qweqwe" - -## Worker settings -## If you're doing mostly I/O you can have more processes, -## but if mostly spending CPU, try to keep it close to the -## number of CPUs on your machine. If not set, the number of CPUs/cores -## available will be used. -CELERYD_CONCURRENCY = 2 -# CELERYD_LOG_FILE = "celeryd.log" -CELERYD_LOG_LEVEL = "DEBUG" -CELERYD_MAX_TASKS_PER_CHILD = 3 - -#Tasks will never be sent to the queue, but executed locally instead. -CELERY_ALWAYS_EAGER = False -if PYLONS_CONFIG_NAME == 'test.ini': - #auto eager for tests - CELERY_ALWAYS_EAGER = True - -#=============================================================================== -# EMAIL SETTINGS -#=============================================================================== -pylons_email_config = dict(config.items('DEFAULT')) - -CELERY_SEND_TASK_ERROR_EMAILS = True - -#List of (name, email_address) tuples for the admins that should receive error e-mails. -ADMINS = [('Administrator', pylons_email_config.get('email_to'))] - -#The e-mail address this worker sends e-mails from. Default is "celery@localhost". -SERVER_EMAIL = pylons_email_config.get('error_email_from') - -#The mail server to use. Default is "localhost". -MAIL_HOST = pylons_email_config.get('smtp_server') - -#Username (if required) to log on to the mail server with. -MAIL_HOST_USER = pylons_email_config.get('smtp_username') - -#Password (if required) to log on to the mail server with. -MAIL_HOST_PASSWORD = pylons_email_config.get('smtp_password') - -MAIL_PORT = pylons_email_config.get('smtp_port') - - -#=============================================================================== -# INSTRUCTIONS FOR RABBITMQ -#=============================================================================== -# rabbitmqctl add_user rabbitmq qweqwe -# rabbitmqctl add_vhost rabbitmqhost -# rabbitmqctl set_permissions -p rabbitmqhost rabbitmq ".*" ".*" ".*" diff -r 06cfcede13ab -r 3d0661b8aaa4 development.ini --- a/development.ini Thu Nov 18 21:35:52 2010 +0100 +++ b/development.ini Sat Dec 18 14:45:58 2010 +0100 @@ -1,6 +1,6 @@ ################################################################################ ################################################################################ -# rhodecode - Pylons environment configuration # +# RhodeCode - Pylons environment configuration # # # # The %(here)s variable will be replaced with the parent directory of this file# ################################################################################ @@ -9,8 +9,8 @@ debug = true ################################################################################ ## Uncomment and replace with the address which should receive ## -## any error reports after application crash ## -## Additionally those settings will be used by rhodecode mailing system ## +## any error reports after application crash ## +## Additionally those settings will be used by RhodeCode mailing system ## ################################################################################ #email_to = admin@localhost #error_email_from = paste_error@localhost @@ -19,22 +19,23 @@ #smtp_server = mail.server.com #smtp_username = -#smtp_password = +#smtp_password = #smtp_port = -#smtp_use_tls = +#smtp_use_tls = false +#smtp_use_ssl = true [server:main] ##nr of threads to spawn threadpool_workers = 5 -##max request before +##max request before thread respawn threadpool_max_requests = 6 ##option to use threads of process use_threadpool = false use = egg:Paste#http -host = 127.0.0.1 +host = 0.0.0.0 port = 5000 [app:main] @@ -43,6 +44,35 @@ static_files = true lang=en cache_dir = %(here)s/data +index_dir = %(here)s/data/index +cut_off_limit = 256000 + +#################################### +### CELERY CONFIG #### +#################################### +use_celery = false +broker.host = localhost +broker.vhost = rabbitmqhost +broker.port = 5672 +broker.user = rabbitmq +broker.password = qweqwe + +celery.imports = rhodecode.lib.celerylib.tasks + +celery.result.backend = amqp +celery.result.dburi = amqp:// +celery.result.serialier = json + +#celery.send.task.error.emails = true +#celery.amqp.task.result.expires = 18000 + +celeryd.concurrency = 2 +#celeryd.log.file = celeryd.log +celeryd.log.level = debug +celeryd.max.tasks.per.child = 3 + +#tasks will never be sent to the queue, but executed locally instead. +celery.always.eager = false #################################### ### BEAKER CACHE #### @@ -60,9 +90,8 @@ beaker.cache.long_term.type=memory beaker.cache.long_term.expire=36000 - beaker.cache.sql_cache_short.type=memory -beaker.cache.sql_cache_short.expire=5 +beaker.cache.sql_cache_short.expire=10 beaker.cache.sql_cache_med.type=memory beaker.cache.sql_cache_med.expire=360 @@ -74,7 +103,7 @@ ### BEAKER SESSION #### #################################### ## Type of storage used for the session, current types are -## "dbm", "file", "memcached", "database", and "memory". +## dbm, file, memcached, database, and memory. ## The storage uses the Container API ##that is also used by the cache system. beaker.session.type = file @@ -116,7 +145,7 @@ ### LOGGING CONFIGURATION #### ################################ [loggers] -keys = root, routes, rhodecode, sqlalchemy +keys = root, routes, rhodecode, sqlalchemy,beaker,templates [handlers] keys = console @@ -136,6 +165,19 @@ handlers = console qualname = routes.middleware # "level = DEBUG" logs the route matched and routing variables. +propagate = 0 + +[logger_beaker] +level = ERROR +handlers = console +qualname = beaker.container +propagate = 0 + +[logger_templates] +level = INFO +handlers = console +qualname = pylons.templating +propagate = 0 [logger_rhodecode] level = DEBUG diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/api/index.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/api/index.rst Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,10 @@ +.. _api: + +API Reference +============= + +.. toctree:: + :maxdepth: 3 + + models + \ No newline at end of file diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/api/models.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/api/models.rst Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,19 @@ +.. _models: + +The :mod:`models` Module +======================== + +.. automodule:: rhodecode.model + :members: + +.. automodule:: rhodecode.model.permission + :members: + +.. automodule:: rhodecode.model.repo + :members: + +.. automodule:: rhodecode.model.scm + :members: + +.. automodule:: rhodecode.model.user + :members: diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/changelog.rst --- a/docs/changelog.rst Thu Nov 18 21:35:52 2010 +0100 +++ b/docs/changelog.rst Sat Dec 18 14:45:58 2010 +0100 @@ -3,22 +3,88 @@ Changelog ========= -1.0.2 (**2010-11-XX**) +1.1.0 (**2010-12-18**) ---------------------- +:status: in-progress +:branch: beta + +news +++++ + +- rewrite of internals for vcs >=0.1.10 +- uses mercurial 1.7 with dotencode disabled for maintaining compatibility + with older clients +- anonymous access, authentication via ldap +- performance upgrade for cached repos list - each repository has it's own + cache that's invalidated when needed. +- performance upgrades on repositories with large amount of commits (20K+) +- main page quick filter for filtering repositories +- user dashboards with ability to follow chosen repositories actions +- sends email to admin on new user registration +- added cache/statistics reset options into repository settings +- more detailed action logger (based on hooks) with pushed changesets lists + and options to disable those hooks from admin panel +- introduced new enhanced changelog for merges that shows more accurate results +- new improved and faster code stats (based on pygments lexers mapping tables, + showing up to 10 trending sources for each repository. Additionally stats + can be disabled in repository settings. +- gui optimizations, fixed application width to 1024px +- added cut off (for large files/changesets) limit into config files +- whoosh, celeryd, upgrade moved to paster command +- other than sqlite database backends can be used + +fixes ++++++ + +- fixes #61 forked repo was showing only after cache expired +- fixes #76 no confirmation on user deletes +- fixes #66 Name field misspelled +- fixes #72 block user removal when he owns repositories +- fixes #69 added password confirmation fields +- fixes #87 RhodeCode crashes occasionally on updating repository owner +- fixes #82 broken annotations on files with more than 1 blank line at the end +- a lot of fixes and tweaks for file browser +- fixed detached session issues +- fixed when user had no repos he would see all repos listed in my account +- fixed ui() instance bug when global hgrc settings was loaded for server + instance and all hgrc options were merged with our db ui() object +- numerous small bugfixes + +(special thanks for TkSoh for detailed feedback) + + +1.0.2 (**2010-11-12**) +---------------------- + +news +++++ + +- tested under python2.7 +- bumped sqlalchemy and celery versions + +fixes ++++++ + - fixed #59 missing graph.js - fixed repo_size crash when repository had broken symlinks - fixed python2.5 crashes. -- tested under python2.7 -- bumped sqlalcehmy and celery versions + 1.0.1 (**2010-11-10**) ---------------------- +news +++++ + +- small css updated + +fixes ++++++ + - fixed #53 python2.5 incompatible enumerate calls - fixed #52 disable mercurial extension for web - fixed #51 deleting repositories don't delete it's dependent objects -- small css updated 1.0.0 (**2010-11-02**) @@ -52,3 +118,4 @@ - Disabled dirsize in file browser, it's causing nasty bug when dir renames occure. After vcs is fixed it'll be put back again. - templating/css rewrites, optimized css. + diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/conf.py --- a/docs/conf.py Thu Nov 18 21:35:52 2010 +0100 +++ b/docs/conf.py Sat Dec 18 14:45:58 2010 +0100 @@ -16,7 +16,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('..')) # -- General configuration ----------------------------------------------------- diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/contributing.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/contributing.rst Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,9 @@ +.. _contributing: + +Contributing in RhodeCode +========================= + +If You would like to contribute to RhodeCode, please contact me, any help is +greatly appreciated. + +Thank You. diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/enable_git.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/enable_git.rst Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,21 @@ +.. _enable_git: + +Enabling GIT support (beta) +=========================== + + +Git support in RhodeCode 1.1 was disabled due to some instability issues, but +If You would like to test it fell free to re-enable it. To enable GIT just +uncomment git line in rhodecode/__init__.py file + +.. code-block:: python + + BACKENDS = { + 'hg': 'Mercurial repository', + #'git': 'Git repository', + } + +.. note:: + Please note that it's not fully stable and it might crash (that's why it + was disabled), so be careful about enabling git support. Don't use it in + production ! \ No newline at end of file diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/images/screenshot1_main_page.png Binary file docs/images/screenshot1_main_page.png has changed diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/images/screenshot2_summary_page.png Binary file docs/images/screenshot2_summary_page.png has changed diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/images/screenshot3_changelog_page.png Binary file docs/images/screenshot3_changelog_page.png has changed diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/index.rst --- a/docs/index.rst Thu Nov 18 21:35:52 2010 +0100 +++ b/docs/index.rst Sat Dec 18 14:45:58 2010 +0100 @@ -1,107 +1,43 @@ .. _index: -Welcome to RhodeCode (RhodiumCode) documentation! -================================================= - -``RhodeCode`` (formerly hg-app) is Pylons based repository management and -serving for mercurial_. It's similar to github or bitbucket, but it's suppose to run -as standalone app, it's open source and focuses more on restricted access to repositories -There's no default free access to RhodeCode You have to create an account in order -to use the application. It's powered by vcs_ library that we created to handle -many various version control systems. - -RhodeCode uses `Semantic Versioning `_ - - -RhodeCode demo --------------- - -http://hg.python-works.com - -The default access is - -- username: demo -- password: demo - -Source code ------------ - -Source code is along with issue tracker is available at -http://bitbucket.org/marcinkuzminski/rhodecode - -Also a source codes can be obtained from demo rhodecode instance -http://hg.python-works.com/rhodecode/summary - -Features --------- - -- Has it's own middleware to handle mercurial_ protocol request. Each request - can be logged and authenticated. Runs on threads unlikely to hgweb You can - make multiple pulls/pushes simultaneous. Supports http/https -- Full permissions and authentication per project private/read/write/admin. - One account for web interface and mercurial_ push/pull/clone. -- Mako templates let's you customize look and feel of application. -- Beautiful diffs, annotations and source codes all colored by pygments. -- Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics -- Admin interface with user/permission management. User activity journal logs - pulls, pushes, forks,registrations. Possible to disable built in hooks -- Server side forks, it's possible to fork a project and hack it free without - breaking the main. -- Full text search on source codes, search on file names. All powered by whoosh - and build in indexing daemons - (no external search servers required all in one application) -- Rss / atom feeds, gravatar support, download sources as zip/tarballs -- Async tasks for speed and performance using celery_ (works without them too) -- Backup scripts can do backup of whole app and send it over scp to desired - location -- Setup project descriptions and info inside built in db for easy, non - file-system operations -- Added cache with invalidation on push/repo management for high performance and - always up to date data. -- Based on pylons 1.0 / sqlalchemy 0.6 / sqlite - - -.. figure:: images/screenshot1_main_page.png - :align: left - - Main page of RhodeCode - -.. figure:: images/screenshot2_summary_page.png - :align: left - - Summary page - - -Incoming --------- - -- code review (probably based on hg-review) -- full git_ support, with push/pull server -- commit based build in wiki system -- clone points and cloning from remote repositories into rhodecode - (git_ and mercurial_) -- more statistics and graph (global annotation + some more statistics) -- user customized activity dashboards -- some cache optimizations -- other cools stuff that i can figure out (or You can help me figure out) - -License -------- - -``rhodecode`` is released under GPL_ license. - +.. include:: ./../README.rst Documentation ------------- +**Installation:** + .. toctree:: :maxdepth: 1 installation + setup upgrade - setup + +**Usage** + +.. toctree:: + :maxdepth: 1 + + enable_git + statistics + +**Develop** + +.. toctree:: + :maxdepth: 1 + + contributing changelog +**API** + +.. toctree:: + :maxdepth: 2 + + api/index + + Other topics ------------ diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/installation.rst --- a/docs/installation.rst Thu Nov 18 21:35:52 2010 +0100 +++ b/docs/installation.rst Sat Dec 18 14:45:58 2010 +0100 @@ -5,31 +5,20 @@ ``RhodeCode`` is written entirely in Python, but in order to use it's full potential there are some third-party requirements. When RhodeCode is used -together with celery_ You have to install some kind of message broker, +together with celery You have to install some kind of message broker, recommended one is rabbitmq_ to make the async tasks work. Of course RhodeCode works in sync mode also, then You don't have to install any third party apps. Celery_ will give You large speed improvement when using -many big repositories. If You plan to use it for 5 or 10 small repositories, it +many big repositories. If You plan to use it for 7 or 10 small repositories, it will work just fine without celery running. -After You decide to Run it with celery make sure You run celeryd and -message broker together with the application. - -Requirements for Celery ------------------------ - -**Message Broker** - -- preferred is `RabbitMq `_ -- possible other is `Redis `_ - -For installation instructions You can visit: -http://ask.github.com/celery/getting-started/index.html -It's very nice tutorial how to start celery_ with rabbitmq_ +After You decide to Run it with celery make sure You run celeryd using paster +and message broker together with the application. Install from Cheese Shop ------------------------ +Rhodecode requires python 2.x greater than version 2.5 Easiest way to install ``rhodecode`` is to run:: @@ -42,7 +31,7 @@ If you prefer to install manually simply grab latest release from http://pypi.python.org/pypi/rhodecode, decompres archive and run:: - python setup.py install + python setup.py install Step by step installation example @@ -62,7 +51,7 @@ :: - source /var/www/rhodecode-venv/bin/activate + source activate /var/www/rhodecode-venv/bin/activate - Make a folder for rhodecode somewhere on the filesystem for example @@ -80,8 +69,28 @@ - this will install rhodecode together with pylons and all other required python libraries +Requirements for Celery (optional) +---------------------------------- + +.. note:: + Installing message broker and using celery is optional, RhodeCode will + work without them perfectly fine. + + +**Message Broker** + +- preferred is `RabbitMq `_ +- possible other is `Redis `_ + +For installation instructions You can visit: +http://ask.github.com/celery/getting-started/index.html +It's very nice tutorial how to start celery_ with rabbitmq_ + You can now proceed to :ref:`setup` +----------------------------------- + + .. _virtualenv: http://pypi.python.org/pypi/virtualenv .. _python: http://www.python.org/ diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/screenshots.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/screenshots.rst Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,13 @@ +.. _screenshots: + +.. figure:: images/screenshot1_main_page.png + + Main page of RhodeCode + +.. figure:: images/screenshot2_summary_page.png + + Summary page + +.. figure:: images/screenshot3_changelog_page.png + + Changelog with DAG graph \ No newline at end of file diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/setup.rst --- a/docs/setup.rst Thu Nov 18 21:35:52 2010 +0100 +++ b/docs/setup.rst Sat Dec 18 14:45:58 2010 +0100 @@ -7,13 +7,20 @@ Setting up the application -------------------------- +First You'll ned to create RhodeCode config file. Run the following command +to do this + :: paster make-config RhodeCode production.ini - This will create `production.ini` config inside the directory - this config contain various settings for rhodecode, e.g port, email settings - static files, cache and logging. + this config contains various settings for RhodeCode, e.g proxy port, + email settings, usage of static files, cache, celery settings and logging. + + + +Next we need to create the database. :: @@ -24,55 +31,136 @@ existing ones. RhodeCode will simply add all new found repositories to it's database. Also make sure You specify correct path to repositories. - Remember that the given path for mercurial_ repositories must be write - accessible for the application. It's very important since RhodeCode web interface - will work even without such an access but, when trying to do a push it'll - eventually fail with permission denied errors. -- Run + accessible for the application. It's very important since RhodeCode web + interface will work even without such an access but, when trying to do a + push it'll eventually fail with permission denied errors. + +You are ready to use rhodecode, to run it simply execute :: paster serve production.ini -- This command runs the rhodecode server the app should be available at the +- This command runs the RhodeCode server the app should be available at the 127.0.0.1:5000. This ip and port is configurable via the production.ini - file created in previous step + file created in previous step - Use admin account you created to login. - Default permissions on each repository is read, and owner is admin. So - remember to update these if needed. + remember to update these if needed. In the admin panel You can toggle ldap, + anonymous, permissions settings. As well as edit more advanced options on + users and repositories -Note ----- + +Setting up Whoosh full text search +---------------------------------- -RhodeCode when running without the celery it's running all it's task in sync -mode, for first few times when visiting summary page You can notice few -slow downs, this is due the statistics building it's cache. After all changesets -are parsed it'll take the stats from cache and run much faster. Each summary -page display parse at most 250 changesets in order to not stress the cpu, so -the full stats are going to be loaded after total_number_of_changesets/250 -summary page visits. +Index for whoosh can be build starting from version 1.1 using paster command +passing repo locations to index, as well as Your config file that stores +whoosh index files locations. There is possible to pass `-f` to the options +to enable full index rebuild. Without that indexing will run always in in +incremental mode. + +:: + paster make-index production.ini --repo-location= - -Setting up Whoosh ------------------ +for full index rebuild You can use + +:: + + paster make-index production.ini -f --repo-location= - For full text search You can either put crontab entry for +This command can be run even from crontab in order to do periodical +index builds and keep Your index always up to date. An example entry might +look like this + :: - python /var/www/rhodecode//lib/indexers/daemon.py incremental + /path/to/python/bin/paster /path/to/rhodecode/production.ini --repo-location= -When using incremental mode whoosh will check last modification date of each file -and add it to reindex if newer file is available. Also indexing daemon checks -for removed files and removes them from index. Sometime You might want to rebuild -index from scratch, in admin panel You can check `build from scratch` flag -and in standalone daemon You can pass `full` instead on incremental to build -remove previous index and build new one. +When using incremental(default) mode whoosh will check last modification date +of each file and add it to reindex if newer file is available. Also indexing +daemon checks for removed files and removes them from index. + +Sometime You might want to rebuild index from scratch. You can do that using +the `-f` flag passed to paster command or, in admin panel You can check +`build from scratch` flag. + + +Setting up LDAP support +----------------------- + +RhodeCode starting from version 1.1 supports ldap authentication. In order +to use ldap, You have to install python-ldap package. This package is available +via pypi, so You can install it by running + +:: + + easy_install python-ldap + +:: + + pip install python-ldap + +.. note:: + python-ldap requires some certain libs on Your system, so before installing + it check that You have at least `openldap`, and `sasl` libraries. + +ldap settings are located in admin->ldap section, + +Here's a typical ldap setup:: + + Enable ldap = checked #controls if ldap access is enabled + Host = host.domain.org #actual ldap server to connect + Port = 389 or 689 for ldaps #ldap server ports + Enable LDAPS = unchecked #enable disable ldaps + Account = #access for ldap server(if required) + Password = #password for ldap server(if required) + Base DN = uid=%(user)s,CN=users,DC=host,DC=domain,DC=org + + +`Account` and `Password` are optional, and used for two-phase ldap +authentication so those are credentials to access Your ldap, if it doesn't +support anonymous search/user lookups. + +Base DN must have %(user)s template inside, it's a placer where Your uid used +to login would go, it allows admins to specify not standard schema for uid +variable + +If all data are entered correctly, and `python-ldap` is properly installed +Users should be granted to access RhodeCode wit ldap accounts. When +logging at the first time an special ldap account is created inside RhodeCode, +so You can control over permissions even on ldap users. If such user exists +already in RhodeCode database ldap user with the same username would be not +able to access RhodeCode. + +If You have problems with ldap access and believe You entered correct +information check out the RhodeCode logs,any error messages sent from +ldap will be saved there. + + + +Setting Up Celery +----------------- + +Since version 1.1 celery is configured by the rhodecode ini configuration files +simply set use_celery=true in the ini file then add / change the configuration +variables inside the ini file. + +Remember that the ini files uses format with '.' not with '_' like celery +so for example setting `BROKER_HOST` in celery means setting `broker.host` in +the config file. + +In order to make start using celery run:: + paster celeryd + Nginx virtual host example -------------------------- -Sample config for nginx:: +Sample config for nginx using proxy:: server { listen 80; @@ -122,6 +210,16 @@ To not have the statics served by the application. And improve speed. +Apache reverse proxy +-------------------- +Tutorial can be found here +http://wiki.pylonshq.com/display/pylonscookbook/Apache+as+a+reverse+proxy+for+Pylons + + +Apache's example FCGI config +---------------------------- + +TODO ! Other configuration files ------------------------- @@ -132,6 +230,29 @@ and also an celeryconfig file can be use from here: http://hg.python-works.com/rhodecode/files/tip/celeryconfig.py +Troubleshooting +--------------- + +- missing static files ? + + - make sure either to set the `static_files = true` in the .ini file or + double check the root path for Your http setup. It should point to + for example: + /home/my-virtual-python/lib/python2.6/site-packages/rhodecode/public + +- can't install celery/rabbitmq + + - don't worry RhodeCode works without them too. No extra setup required + +- long lasting push timeouts ? + + - make sure You set a longer timeouts in Your proxy/fcgi settings, timeouts + are caused by https server and not RhodeCode + +- large pushes timeouts ? + + - make sure You set a proper max_body_size for the http server + .. _virtualenv: http://pypi.python.org/pypi/virtualenv diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/statistics.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/statistics.rst Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,32 @@ +.. _statistics: + + +Statistics +========== + +RhodeCode statistics system is heavy on resources, so in order to keep a +balance between the usability and performance statistics are cached inside db +and are gathered incrementally, this is how RhodeCode does this: + +With Celery disabled +++++++++++++++++++++ + +- on each first visit on summary page a set of 250 commits are parsed and + updates statistics cache +- this happens on each single visit of statistics page until all commits are + fetched. Statistics are kept cached until some more commits are added to + repository, in such case RhodeCode will fetch only the ones added and will + update it's cache. + + +With Celery enabled ++++++++++++++++++++ + +- on first visit on summary page RhodeCode will create task that will execute + on celery workers, that will gather all stats until all commits are parsed, + each task will parse 250 commits, and run next task to parse next 250 + commits, until all are parsed. + +.. note:: + In any time You can disable statistics on each repository in repository edit + form in admin panel, just uncheck the statistics checkbox. \ No newline at end of file diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/theme/nature/layout.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/theme/nature/layout.html Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,14 @@ +{% extends "basic/layout.html" %} + +{% block sidebarlogo %} +

Support my development effort.

+
+
+ + + + +
+
+{% endblock %}} diff -r 06cfcede13ab -r 3d0661b8aaa4 docs/upgrade.rst --- a/docs/upgrade.rst Thu Nov 18 21:35:52 2010 +0100 +++ b/docs/upgrade.rst Sat Dec 18 14:45:58 2010 +0100 @@ -22,7 +22,26 @@ paster make-config RhodeCode production.ini This will display any changes made from new version of RhodeCode To your -current config. And tries to do an automerge. +current config. And tries to do an automerge. It's always better to do a backup +of config file and recheck the content after merge. + +It's also good to rebuild the whoosh index since after upgrading the whoosh +version there could be introduced incompatible index changes. + + +The last step is to upgrade the database. To do this simply run + +:: + + paster upgrade-db production.ini + +This will upgrade schema, as well as update some default on the database, +always recheck the settings of the application, if there are no new options +that need to be set. + +.. note:: + Always perform a database backup before doing upgrade. + .. _virtualenv: http://pypi.python.org/pypi/virtualenv diff -r 06cfcede13ab -r 3d0661b8aaa4 production.ini --- a/production.ini Thu Nov 18 21:35:52 2010 +0100 +++ b/production.ini Sat Dec 18 14:45:58 2010 +0100 @@ -22,6 +22,7 @@ #smtp_password = #smtp_port = #smtp_use_tls = false +#smtp_use_ssl = true [server:main] ##nr of threads to spawn @@ -43,6 +44,35 @@ static_files = false lang=en cache_dir = %(here)s/data +index_dir = %(here)s/data/index +cut_off_limit = 256000 + +#################################### +### CELERY CONFIG #### +#################################### +use_celery = false +broker.host = localhost +broker.vhost = rabbitmqhost +broker.port = 5672 +broker.user = rabbitmq +broker.password = qweqwe + +celery.imports = rhodecode.lib.celerylib.tasks + +celery.result.backend = amqp +celery.result.dburi = amqp:// +celery.result.serialier = json + +#celery.send.task.error.emails = true +#celery.amqp.task.result.expires = 18000 + +celeryd.concurrency = 2 +#celeryd.log.file = celeryd.log +celeryd.log.level = debug +celeryd.max.tasks.per.child = 3 + +#tasks will never be sent to the queue, but executed locally instead. +celery.always.eager = false #################################### ### BEAKER CACHE #### @@ -136,6 +166,7 @@ handlers = console qualname = routes.middleware # "level = DEBUG" logs the route matched and routing variables. +propagate = 0 [logger_rhodecode] level = DEBUG diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/__init__.py --- a/rhodecode/__init__.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/__init__.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,8 +1,16 @@ -#!/usr/bin/env python -# encoding: utf-8 -# RhodeCode, a web based repository management based on pylons -# Copyright (C) 2009-2010 Marcin Kuzminski -# +# -*- coding: utf-8 -*- +""" + rhodecode.__init__ + ~~~~~~~~~~~~~~~~~~ + + RhodeCode, a web based repository management based on pylons + versioning implementation: http://semver.org/ + + :created_on: Apr 9, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; version 2 @@ -17,19 +25,28 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. -""" -Created on April 9, 2010 -RhodeCode, a web based repository management based on pylons -versioning implementation: http://semver.org/ -@author: marcink -""" + + +VERSION = (1, 1, 0) +__version__ = '.'.join((str(each) for each in VERSION[:4])) +__dbversion__ = 2 #defines current db version for migrations -VERSION = (1, 0, 2,) +try: + from rhodecode.lib.utils import get_current_revision + _rev = get_current_revision() +except ImportError: + #this is needed when doing some setup.py operations + _rev = False -__version__ = '.'.join((str(each) for each in VERSION[:4])) +if len(VERSION) > 3 and _rev: + __version__ += ' [rev:%s]' % _rev[0] def get_version(): - """ - Returns shorter version (digit parts only) as string. - """ + """Returns shorter version (digit parts only) as string.""" + return '.'.join((str(each) for each in VERSION[:3])) + +BACKENDS = { + 'hg': 'Mercurial repository', + #'git': 'Git repository', +} diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/config/deployment.ini_tmpl --- a/rhodecode/config/deployment.ini_tmpl Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/config/deployment.ini_tmpl Sat Dec 18 14:45:58 2010 +0100 @@ -1,6 +1,6 @@ ################################################################################ ################################################################################ -# rhodecode - Pylons environment configuration # +# RhodeCode - Pylons environment configuration # # # # The %(here)s variable will be replaced with the parent directory of this file# ################################################################################ @@ -10,7 +10,7 @@ ################################################################################ ## Uncomment and replace with the address which should receive ## ## any error reports after application crash ## -## Additionally those settings will be used by rhodecode mailing system ## +## Additionally those settings will be used by RhodeCode mailing system ## ################################################################################ #email_to = admin@localhost #error_email_from = paste_error@localhost @@ -22,13 +22,14 @@ #smtp_password = #smtp_port = #smtp_use_tls = false +#smtp_use_ssl = true [server:main] ##nr of threads to spawn threadpool_workers = 5 ##max request before thread respawn -threadpool_max_requests = 2 +threadpool_max_requests = 10 ##option to use threads of process use_threadpool = true @@ -43,7 +44,36 @@ static_files = true lang=en cache_dir = %(here)s/data +index_dir = %(here)s/data/index app_instance_uuid = ${app_instance_uuid} +cut_off_limit = 256000 + +#################################### +### CELERY CONFIG #### +#################################### +use_celery = false +broker.host = localhost +broker.vhost = rabbitmqhost +broker.port = 5672 +broker.user = rabbitmq +broker.password = qweqwe + +celery.imports = rhodecode.lib.celerylib.tasks + +celery.result.backend = amqp +celery.result.dburi = amqp:// +celery.result.serialier = json + +#celery.send.task.error.emails = true +#celery.amqp.task.result.expires = 18000 + +celeryd.concurrency = 2 +#celeryd.log.file = celeryd.log +celeryd.log.level = debug +celeryd.max.tasks.per.child = 3 + +#tasks will never be sent to the queue, but executed locally instead. +celery.always.eager = false #################################### ### BEAKER CACHE #### @@ -62,7 +92,7 @@ beaker.cache.long_term.expire=36000 beaker.cache.sql_cache_short.type=memory -beaker.cache.sql_cache_short.expire=5 +beaker.cache.sql_cache_short.expire=10 beaker.cache.sql_cache_med.type=memory beaker.cache.sql_cache_med.expire=360 @@ -136,6 +166,7 @@ handlers = console qualname = routes.middleware # "level = DEBUG" logs the route matched and routing variables. +propagate = 0 [logger_rhodecode] level = DEBUG diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/config/environment.py --- a/rhodecode/config/environment.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/config/environment.py Sat Dec 18 14:45:58 2010 +0100 @@ -6,7 +6,7 @@ from rhodecode.lib.auth import set_available_permissions, set_base_path from rhodecode.lib.utils import repo2db_mapper, make_ui, set_rhodecode_config from rhodecode.model import init_model -from rhodecode.model.hg_model import _get_repos_cached_initial +from rhodecode.model.scm import ScmModel from sqlalchemy import engine_from_config import logging import os @@ -20,7 +20,7 @@ object """ config = PylonsConfig() - + # Pylons paths root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) paths = dict(root=root, @@ -34,11 +34,11 @@ config['routes.map'] = make_map(config) config['pylons.app_globals'] = app_globals.Globals(config) config['pylons.h'] = rhodecode.lib.helpers - + # Setup cache object as early as possible import pylons pylons.cache._push_object(config['pylons.app_globals'].cache) - + # Create the Mako TemplateLookup, with the default auto-escaping config['pylons.app_globals'].mako_lookup = TemplateLookup( directories=paths['templates'], @@ -52,9 +52,10 @@ test = os.path.split(config['__file__'])[-1] == 'test.ini' if test: from rhodecode.lib.utils import create_test_env, create_test_index - create_test_env('/tmp', config) - create_test_index('/tmp/*', True) - + from rhodecode.tests import TESTS_TMP_PATH + create_test_env(TESTS_TMP_PATH, config) + create_test_index(TESTS_TMP_PATH, True) + #MULTIPLE DB configs # Setup the SQLAlchemy database engine if config['debug'] and not test: @@ -68,12 +69,13 @@ init_model(sa_engine_db1) #init baseui config['pylons.app_globals'].baseui = make_ui('db') - - repo2db_mapper(_get_repos_cached_initial(config['pylons.app_globals'], initial)) + + g = config['pylons.app_globals'] + repo2db_mapper(ScmModel().repo_scan(g.paths[0][1], g.baseui)) set_available_permissions(config) set_base_path(config) set_rhodecode_config(config) # CONFIGURATION OPTIONS HERE (note: all config options will override # any Pylons config options) - + return config diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/config/middleware.py --- a/rhodecode/config/middleware.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/config/middleware.py Sat Dec 18 14:45:58 2010 +0100 @@ -8,8 +8,10 @@ from pylons.wsgiapp import PylonsApp from routes.middleware import RoutesMiddleware from rhodecode.lib.middleware.simplehg import SimpleHg +from rhodecode.lib.middleware.simplegit import SimpleGit from rhodecode.lib.middleware.https_fixup import HttpsFixup from rhodecode.config.environment import load_environment +from paste.gzipper import make_gzip_middleware def make_app(global_conf, full_stack=True, static_files=True, **app_conf): """Create a Pylons WSGI application and return it @@ -35,15 +37,16 @@ # The Pylons WSGI app app = PylonsApp(config=config) - + # Routing/Session/Cache Middleware app = RoutesMiddleware(app, config['routes.map']) app = SessionMiddleware(app, config) - + # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) - + app = SimpleHg(app, config) - + app = SimpleGit(app, config) + if asbool(full_stack): # Handle Python exceptions app = ErrorHandler(app, global_conf, **config['pylons.errorware']) @@ -54,10 +57,10 @@ app = StatusCodeRedirect(app) else: app = StatusCodeRedirect(app, [400, 401, 403, 404, 500]) - + #enable https redirets based on HTTP_X_URL_SCHEME set by proxy app = HttpsFixup(app) - + # Establish the Registry for this application app = RegistryManager(app) @@ -65,7 +68,8 @@ # Serve static files static_app = StaticURLParser(config['pylons.paths']['static_files']) app = Cascade([static_app, app]) - + app = make_gzip_middleware(app, global_conf, compress_level=1) + app.config = config return app diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/config/routing.py --- a/rhodecode/config/routing.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/config/routing.py Sat Dec 18 14:45:58 2010 +0100 @@ -35,7 +35,7 @@ #========================================================================== #MAIN PAGE - map.connect('hg_home', '/', controller='hg', action='index') + map.connect('home', '/', controller='home', action='index') map.connect('bugtracker', "http://bitbucket.org/marcinkuzminski/rhodecode/issues", _static=True) map.connect('gpl_license', "http://www.gnu.org/licenses/gpl.html", _static=True) #ADMIN REPOSITORY REST ROUTES @@ -73,13 +73,27 @@ m.connect('delete_repo_user', "/repos_delete_user/{repo_name:.*}", action="delete_perm_user", conditions=dict(method=["DELETE"], function=check_repo)) - + #settings actions + m.connect('repo_stats', "/repos_stats/{repo_name:.*}", + action="repo_stats", conditions=dict(method=["DELETE"], + function=check_repo)) + m.connect('repo_cache', "/repos_cache/{repo_name:.*}", + action="repo_cache", conditions=dict(method=["DELETE"], + function=check_repo)) #ADMIN USER REST ROUTES map.resource('user', 'users', controller='admin/users', path_prefix='/_admin') #ADMIN PERMISSIONS REST ROUTES map.resource('permission', 'permissions', controller='admin/permissions', path_prefix='/_admin') + + ##ADMIN LDAP SETTINGS + map.connect('ldap_settings', '/_admin/ldap', controller='admin/ldap_settings', + action='ldap_settings', conditions=dict(method=["POST"])) + map.connect('ldap_home', '/_admin/ldap', controller='admin/ldap_settings',) + + + #ADMIN SETTINGS REST ROUTES with map.submapper(path_prefix='/_admin', controller='admin/settings') as m: m.connect("admin_settings", "/settings", @@ -116,6 +130,14 @@ m.connect('admin_home', '', action='index')#main page m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}', action='add_repo') + + + #USER JOURNAL + map.connect('journal', '/_admin/journal', controller='journal',) + map.connect('toggle_following', '/_admin/toggle_following', controller='journal', + action='toggle_following', conditions=dict(method=["POST"])) + + #SEARCH map.connect('search', '/_admin/search', controller='search',) map.connect('search_repo', '/_admin/search/{search_repo:.*}', controller='search') diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/admin/admin.py --- a/rhodecode/controllers/admin/admin.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/admin/admin.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,8 +1,15 @@ -#!/usr/bin/env python -# encoding: utf-8 -# admin controller for pylons -# Copyright (C) 2009-2010 Marcin Kuzminski - +# -*- coding: utf-8 -*- +""" + rhodecode.controllers.admin.admin + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Controller for Admin panel of Rhodecode + + :created_on: Apr 7, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; version 2 @@ -17,15 +24,10 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. -""" -Created on April 7, 2010 -admin controller for pylons -@author: marcink -""" + import logging -from pylons import request, response, session, tmpl_context as c +from pylons import request, tmpl_context as c from rhodecode.lib.base import BaseController, render -from rhodecode.model import meta from rhodecode.model.db import UserLog from webhelpers.paginate import Page from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator @@ -33,19 +35,19 @@ log = logging.getLogger(__name__) class AdminController(BaseController): - + @LoginRequired() def __before__(self): super(AdminController, self).__before__() - - @HasPermissionAllDecorator('hg.admin') + + @HasPermissionAllDecorator('hg.admin') def index(self): - + users_log = self.sa.query(UserLog).order_by(UserLog.action_date.desc()) p = int(request.params.get('page', 1)) c.users_log = Page(users_log, page=p, items_per_page=10) c.log_data = render('admin/admin_log.html') if request.params.get('partial'): return c.log_data - return render('admin/admin.html') - + return render('admin/admin.html') + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/admin/ldap_settings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/controllers/admin/ldap_settings.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +""" + package.rhodecode.controllers.admin.ldap_settings + ~~~~~~~~~~~~~~ + + ldap controller for RhodeCode + :created_on: Nov 26, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +import logging +import formencode +import traceback + +from formencode import htmlfill + +from pylons import request, response, session, tmpl_context as c, url +from pylons.controllers.util import abort, redirect +from pylons.i18n.translation import _ + +from rhodecode.lib.base import BaseController, render +from rhodecode.lib import helpers as h +from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator +from rhodecode.lib.auth_ldap import LdapImportError +from rhodecode.model.settings import SettingsModel +from rhodecode.model.forms import LdapSettingsForm +from sqlalchemy.exc import DatabaseError + +log = logging.getLogger(__name__) + + + +class LdapSettingsController(BaseController): + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + def __before__(self): + c.admin_user = session.get('admin_user') + c.admin_username = session.get('admin_username') + super(LdapSettingsController, self).__before__() + + def index(self): + defaults = SettingsModel().get_ldap_settings() + + return htmlfill.render( + render('admin/ldap/ldap.html'), + defaults=defaults, + encoding="UTF-8", + force_defaults=True,) + + def ldap_settings(self): + """ + POST ldap create and store ldap settings + """ + + settings_model = SettingsModel() + _form = LdapSettingsForm()() + + try: + form_result = _form.to_python(dict(request.POST)) + try: + + for k, v in form_result.items(): + if k.startswith('ldap_'): + setting = settings_model.get(k) + setting.app_settings_value = v + self.sa.add(setting) + + self.sa.commit() + h.flash(_('Ldap settings updated successfully'), + category='success') + except (DatabaseError,): + raise + except LdapImportError: + h.flash(_('Unable to activate ldap. The "python-ldap" library ' + 'is missing.'), category='warning') + + except formencode.Invalid, errors: + + return htmlfill.render( + render('admin/ldap/ldap.html'), + defaults=errors.value, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8") + except Exception: + log.error(traceback.format_exc()) + h.flash(_('error occured during update of ldap settings'), + category='error') + + return redirect(url('ldap_home')) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/admin/permissions.py --- a/rhodecode/controllers/admin/permissions.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/admin/permissions.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,8 +1,15 @@ -#!/usr/bin/env python -# encoding: utf-8 -# permissions controller for pylons -# Copyright (C) 2009-2010 Marcin Kuzminski -# +# -*- coding: utf-8 -*- +""" + rhodecode.controllers.admin.permissions + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + permissions controller for Rhodecode + + :created_on: Apr 27, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; version 2 @@ -17,11 +24,6 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. -""" -Created on April 27, 2010 -permissions controller for pylons -@author: marcink -""" from formencode import htmlfill from pylons import request, session, tmpl_context as c, url @@ -29,11 +31,12 @@ from pylons.i18n.translation import _ from rhodecode.lib import helpers as h from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator +from rhodecode.lib.auth_ldap import LdapImportError from rhodecode.lib.base import BaseController, render -from rhodecode.model.db import User, UserLog -from rhodecode.model.forms import UserForm, DefaultPermissionsForm -from rhodecode.model.permission_model import PermissionModel -from rhodecode.model.user_model import UserModel +from rhodecode.model.forms import LdapSettingsForm, DefaultPermissionsForm +from rhodecode.model.permission import PermissionModel +from rhodecode.model.settings import SettingsModel +from rhodecode.model.user import UserModel import formencode import logging import traceback @@ -45,29 +48,30 @@ # To properly map this controller, ensure your config/routing.py # file has a resource setup: # map.resource('permission', 'permissions') - + @LoginRequired() @HasPermissionAllDecorator('hg.admin') def __before__(self): c.admin_user = session.get('admin_user') c.admin_username = session.get('admin_username') super(PermissionsController, self).__before__() - + self.perms_choices = [('repository.none', _('None'),), ('repository.read', _('Read'),), ('repository.write', _('Write'),), ('repository.admin', _('Admin'),)] self.register_choices = [ - ('hg.register.none', 'disabled'), + ('hg.register.none', + _('disabled')), ('hg.register.manual_activate', - _('allowed with manual account activation')), + _('allowed with manual account activation')), ('hg.register.auto_activate', - _('allowed with automatic account activation')), ] - + _('allowed with automatic account activation')), ] + self.create_choices = [('hg.create.none', _('Disabled')), - ('hg.create.repository', _('Enabled'))] + ('hg.create.repository', _('Enabled'))] - + def index(self, format='html'): """GET /permissions: All items in the collection""" # url('permissions') @@ -88,38 +92,39 @@ # h.form(url('permission', id=ID), # method='put') # url('permission', id=ID) - + permission_model = PermissionModel() - + _form = DefaultPermissionsForm([x[0] for x in self.perms_choices], [x[0] for x in self.register_choices], [x[0] for x in self.create_choices])() - + try: form_result = _form.to_python(dict(request.POST)) form_result.update({'perm_user_name':id}) permission_model.update(form_result) - h.flash(_('Default permissions updated succesfully'), + h.flash(_('Default permissions updated successfully'), category='success') - + except formencode.Invalid, errors: c.perms_choices = self.perms_choices c.register_choices = self.register_choices c.create_choices = self.create_choices - + defaults = errors.value + return htmlfill.render( render('admin/permissions/permissions.html'), - defaults=errors.value, + defaults=defaults, errors=errors.error_dict or {}, prefix_error=False, - encoding="UTF-8") + encoding="UTF-8") except Exception: log.error(traceback.format_exc()) h.flash(_('error occured during update of permissions'), category='error') - + return redirect(url('edit_permission', id=id)) - + def delete(self, id): @@ -141,23 +146,26 @@ c.perms_choices = self.perms_choices c.register_choices = self.register_choices c.create_choices = self.create_choices - + if id == 'default': - defaults = {'_method':'put'} - for p in UserModel().get_default().user_perms: + default_user = UserModel().get_by_username('default') + defaults = {'_method':'put', + 'anonymous':default_user.active} + + for p in default_user.user_perms: if p.permission.permission_name.startswith('repository.'): - defaults['default_perm'] = p.permission.permission_name - + defaults['default_perm'] = p.permission.permission_name + if p.permission.permission_name.startswith('hg.register.'): defaults['default_register'] = p.permission.permission_name - + if p.permission.permission_name.startswith('hg.create.'): defaults['default_create'] = p.permission.permission_name - + return htmlfill.render( render('admin/permissions/permissions.html'), defaults=defaults, encoding="UTF-8", - force_defaults=True,) + force_defaults=True,) else: return redirect(url('admin_home')) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/admin/repos.py --- a/rhodecode/controllers/admin/repos.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/admin/repos.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,8 +1,15 @@ -#!/usr/bin/env python -# encoding: utf-8 -# repos controller for pylons -# Copyright (C) 2009-2010 Marcin Kuzminski -# +# -*- coding: utf-8 -*- +""" + rhodecode.controllers.admin.repos + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Admin controller for RhodeCode + + :created_on: Apr 7, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; version 2 @@ -17,17 +24,18 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. -""" -Created on April 7, 2010 -admin controller for pylons -@author: marcink -""" + +import logging +import traceback +import formencode +from operator import itemgetter from formencode import htmlfill -from operator import itemgetter + from paste.httpexceptions import HTTPInternalServerError from pylons import request, response, session, tmpl_context as c, url from pylons.controllers.util import abort, redirect from pylons.i18n.translation import _ + from rhodecode.lib import helpers as h from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \ HasPermissionAnyDecorator @@ -35,11 +43,9 @@ from rhodecode.lib.utils import invalidate_cache, action_logger from rhodecode.model.db import User from rhodecode.model.forms import RepoForm -from rhodecode.model.hg_model import HgModel -from rhodecode.model.repo_model import RepoModel -import formencode -import logging -import traceback +from rhodecode.model.scm import ScmModel +from rhodecode.model.repo import RepoModel + log = logging.getLogger(__name__) @@ -48,22 +54,22 @@ # To properly map this controller, ensure your config/routing.py # file has a resource setup: # map.resource('repo', 'repos') - + @LoginRequired() @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository') def __before__(self): c.admin_user = session.get('admin_user') c.admin_username = session.get('admin_username') super(ReposController, self).__before__() - - @HasPermissionAllDecorator('hg.admin') + + @HasPermissionAllDecorator('hg.admin') def index(self, format='html'): """GET /repos: All items in the collection""" # url('repos') - cached_repo_list = HgModel().get_repos() + cached_repo_list = ScmModel().get_repos() c.repos_list = sorted(cached_repo_list, key=itemgetter('name_sort')) return render('admin/repos/repos.html') - + @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository') def create(self): """POST /repos: Create a new item""" @@ -74,7 +80,6 @@ try: form_result = _form.to_python(dict(request.POST)) repo_model.create(form_result, c.rhodecode_user) - invalidate_cache('cached_repo_list') h.flash(_('created repository %s') % form_result['repo_name'], category='success') @@ -83,22 +88,22 @@ form_result['repo_name'], '', self.sa) else: action_logger(self.rhodecode_user, 'admin_created_repo', - form_result['repo_name'], '', self.sa) - + form_result['repo_name'], '', self.sa) + except formencode.Invalid, errors: c.new_repo = errors.value['repo_name'] - + if request.POST.get('user_created'): r = render('admin/repos/repo_add_create_repository.html') - else: + else: r = render('admin/repos/repo_add.html') - + return htmlfill.render( r, defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, - encoding="UTF-8") + encoding="UTF-8") except Exception: log.error(traceback.format_exc()) @@ -106,9 +111,9 @@ % form_result.get('repo_name') h.flash(msg, category='error') if request.POST.get('user_created'): - return redirect(url('hg_home')) + return redirect(url('home')) return redirect(url('repos')) - + @HasPermissionAllDecorator('hg.admin') def new(self, format='html'): """GET /repos/new: Form to create a new item""" @@ -116,7 +121,7 @@ c.new_repo = h.repo_name_slug(new_repo) return render('admin/repos/repo_add.html') - + @HasPermissionAllDecorator('hg.admin') def update(self, repo_name): """PUT /repos/repo_name: Update an existing item""" @@ -129,16 +134,33 @@ repo_model = RepoModel() changed_name = repo_name _form = RepoForm(edit=True, old_data={'repo_name':repo_name})() - + try: form_result = _form.to_python(dict(request.POST)) repo_model.update(repo_name, form_result) - invalidate_cache('cached_repo_list') - h.flash(_('Repository %s updated succesfully' % repo_name), + invalidate_cache('get_repo_cached_%s' % repo_name) + h.flash(_('Repository %s updated successfully' % repo_name), category='success') changed_name = form_result['repo_name'] + action_logger(self.rhodecode_user, 'admin_updated_repo', + changed_name, '', self.sa) + except formencode.Invalid, errors: - c.repo_info = repo_model.get(repo_name) + c.repo_info = repo_model.get_by_repo_name(repo_name) + if c.repo_info.stats: + last_rev = c.repo_info.stats.stat_on_revision + else: + last_rev = 0 + c.stats_revision = last_rev + r = ScmModel().get(repo_name) + c.repo_last_rev = r.revisions[-1] if r.revisions else 0 + + if last_rev == 0: + c.stats_percentage = 0 + else: + c.stats_percentage = '%.2f' % ((float((last_rev)) / + c.repo_last_rev) * 100) + c.users_array = repo_model.get_users_js() errors.value.update({'user':c.repo_info.user.username}) return htmlfill.render( @@ -147,14 +169,14 @@ errors=errors.error_dict or {}, prefix_error=False, encoding="UTF-8") - + except Exception: log.error(traceback.format_exc()) - h.flash(_('error occured during update of repository %s') \ + h.flash(_('error occurred during update of repository %s') \ % repo_name, category='error') - + return redirect(url('edit_repo', repo_name=changed_name)) - + @HasPermissionAllDecorator('hg.admin') def delete(self, repo_name): """DELETE /repos/repo_name: Delete an existing item""" @@ -164,82 +186,128 @@ # h.form(url('repo', repo_name=ID), # method='delete') # url('repo', repo_name=ID) - + repo_model = RepoModel() - repo = repo_model.get(repo_name) + repo = repo_model.get_by_repo_name(repo_name) if not repo: - h.flash(_('%s repository is not mapped to db perhaps' + h.flash(_('%s repository is not mapped to db perhaps' ' it was moved or renamed from the filesystem' ' please run the application again' ' in order to rescan repositories') % repo_name, category='error') - + return redirect(url('repos')) try: action_logger(self.rhodecode_user, 'admin_deleted_repo', repo_name, '', self.sa) - repo_model.delete(repo) - invalidate_cache('cached_repo_list') + repo_model.delete(repo) + invalidate_cache('get_repo_cached_%s' % repo_name) h.flash(_('deleted repository %s') % repo_name, category='success') - + except Exception, e: log.error(traceback.format_exc()) h.flash(_('An error occured during deletion of %s') % repo_name, category='error') - + return redirect(url('repos')) - - @HasPermissionAllDecorator('hg.admin') + + @HasPermissionAllDecorator('hg.admin') def delete_perm_user(self, repo_name): """ DELETE an existing repository permission user :param repo_name: """ - + try: repo_model = RepoModel() - repo_model.delete_perm_user(request.POST, repo_name) + repo_model.delete_perm_user(request.POST, repo_name) except Exception, e: h.flash(_('An error occured during deletion of repository user'), category='error') raise HTTPInternalServerError() - - @HasPermissionAllDecorator('hg.admin') + + @HasPermissionAllDecorator('hg.admin') + def repo_stats(self, repo_name): + """ + DELETE an existing repository statistics + :param repo_name: + """ + + try: + repo_model = RepoModel() + repo_model.delete_stats(repo_name) + except Exception, e: + h.flash(_('An error occured during deletion of repository stats'), + category='error') + return redirect(url('edit_repo', repo_name=repo_name)) + + @HasPermissionAllDecorator('hg.admin') + def repo_cache(self, repo_name): + """ + INVALIDATE exisitings repository cache + :param repo_name: + """ + + try: + ScmModel().mark_for_invalidation(repo_name) + except Exception, e: + h.flash(_('An error occurred during cache invalidation'), + category='error') + return redirect(url('edit_repo', repo_name=repo_name)) + + @HasPermissionAllDecorator('hg.admin') def show(self, repo_name, format='html'): """GET /repos/repo_name: Show a specific item""" # url('repo', repo_name=ID) - - @HasPermissionAllDecorator('hg.admin') + + @HasPermissionAllDecorator('hg.admin') def edit(self, repo_name, format='html'): """GET /repos/repo_name/edit: Form to edit an existing item""" # url('edit_repo', repo_name=ID) repo_model = RepoModel() - c.repo_info = repo = repo_model.get(repo_name) - if not repo: - h.flash(_('%s repository is not mapped to db perhaps' + r = ScmModel().get(repo_name) + c.repo_info = repo_model.get_by_repo_name(repo_name) + + if c.repo_info is None: + h.flash(_('%s repository is not mapped to db perhaps' ' it was created or renamed from the filesystem' ' please run the application again' ' in order to rescan repositories') % repo_name, category='error') - - return redirect(url('repos')) - defaults = c.repo_info.__dict__ + + return redirect(url('repos')) + + if c.repo_info.stats: + last_rev = c.repo_info.stats.stat_on_revision + else: + last_rev = 0 + c.stats_revision = last_rev + + c.repo_last_rev = r.revisions[-1] if r.revisions else 0 + + if last_rev == 0: + c.stats_percentage = 0 + else: + c.stats_percentage = '%.2f' % ((float((last_rev)) / + c.repo_last_rev) * 100) + + defaults = c.repo_info.get_dict() if c.repo_info.user: defaults.update({'user':c.repo_info.user.username}) else: replacement_user = self.sa.query(User)\ .filter(User.admin == True).first().username defaults.update({'user':replacement_user}) - + c.users_array = repo_model.get_users_js() - + for p in c.repo_info.repo_to_perm: - defaults.update({'perm_%s' % p.user.username: + defaults.update({'perm_%s' % p.user.username: p.permission.permission_name}) - + return htmlfill.render( render('admin/repos/repo_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False - ) + ) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/admin/settings.py --- a/rhodecode/controllers/admin/settings.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/admin/settings.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,8 +1,14 @@ -#!/usr/bin/env python -# encoding: utf-8 -# settings controller for pylons -# Copyright (C) 2009-2010 Marcin Kuzminski -# +# -*- coding: utf-8 -*- +""" + package.rhodecode.controllers.admin.settings + ~~~~~~~~~~~~~~ + settings controller for rhodecode admin + + :created_on: Jul 14, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; version 2 @@ -17,11 +23,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. -""" -Created on July 14, 2010 -settings controller for pylons -@author: marcink -""" + from formencode import htmlfill from pylons import request, session, tmpl_context as c, url, app_globals as g, \ config @@ -29,20 +31,22 @@ from pylons.i18n.translation import _ from rhodecode.lib import helpers as h from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \ - HasPermissionAnyDecorator + HasPermissionAnyDecorator, NotAnonymous from rhodecode.lib.base import BaseController, render +from rhodecode.lib.celerylib import tasks, run_task from rhodecode.lib.utils import repo2db_mapper, invalidate_cache, \ - set_rhodecode_config, get_hg_settings, get_hg_ui_settings, make_ui -from rhodecode.model.db import User, UserLog, RhodeCodeSettings, RhodeCodeUi + set_rhodecode_config +from rhodecode.model.db import RhodeCodeUi, Repository from rhodecode.model.forms import UserForm, ApplicationSettingsForm, \ ApplicationUiSettingsForm -from rhodecode.model.hg_model import HgModel -from rhodecode.model.user_model import UserModel -from rhodecode.lib.celerylib import tasks, run_task +from rhodecode.model.scm import ScmModel +from rhodecode.model.settings import SettingsModel +from rhodecode.model.user import UserModel +from sqlalchemy import func import formencode import logging import traceback - + log = logging.getLogger(__name__) @@ -59,32 +63,32 @@ c.admin_user = session.get('admin_user') c.admin_username = session.get('admin_username') super(SettingsController, self).__before__() - - - @HasPermissionAllDecorator('hg.admin') + + + @HasPermissionAllDecorator('hg.admin') def index(self, format='html'): """GET /admin/settings: All items in the collection""" # url('admin_settings') - defaults = get_hg_settings() - defaults.update(get_hg_ui_settings()) + defaults = SettingsModel().get_app_settings() + defaults.update(self.get_hg_ui_settings()) return htmlfill.render( render('admin/settings/settings.html'), defaults=defaults, encoding="UTF-8", force_defaults=False - ) - + ) + @HasPermissionAllDecorator('hg.admin') def create(self): """POST /admin/settings: Create a new item""" # url('admin_settings') - + @HasPermissionAllDecorator('hg.admin') def new(self, format='html'): """GET /admin/settings/new: Form to create a new item""" # url('admin_new_setting') - + @HasPermissionAllDecorator('hg.admin') def update(self, setting_id): """PUT /admin/settings/setting_id: Update an existing item""" @@ -98,47 +102,48 @@ rm_obsolete = request.POST.get('destroy', False) log.debug('Rescanning directories with destroy=%s', rm_obsolete) - initial = HgModel.repo_scan(g.paths[0][0], g.paths[0][1], g.baseui) + initial = ScmModel().repo_scan(g.paths[0][1], g.baseui) + for repo_name in initial.keys(): + invalidate_cache('get_repo_cached_%s' % repo_name) + repo2db_mapper(initial, rm_obsolete) - invalidate_cache('cached_repo_list') - h.flash(_('Repositories successfully rescanned'), category='success') - + + h.flash(_('Repositories successfully rescanned'), category='success') + if setting_id == 'whoosh': - repo_location = get_hg_ui_settings()['paths_root_path'] + repo_location = self.get_hg_ui_settings()['paths_root_path'] full_index = request.POST.get('full_index', False) task = run_task(tasks.whoosh_index, repo_location, full_index) - + h.flash(_('Whoosh reindex task scheduled'), category='success') if setting_id == 'global': - + application_form = ApplicationSettingsForm()() try: form_result = application_form.to_python(dict(request.POST)) - + settings_model = SettingsModel() try: - hgsettings1 = self.sa.query(RhodeCodeSettings)\ - .filter(RhodeCodeSettings.app_settings_name == 'title').one() - hgsettings1.app_settings_value = form_result['rhodecode_title'] - - hgsettings2 = self.sa.query(RhodeCodeSettings)\ - .filter(RhodeCodeSettings.app_settings_name == 'realm').one() - hgsettings2.app_settings_value = form_result['rhodecode_realm'] - - + hgsettings1 = settings_model.get('title') + hgsettings1.app_settings_value = form_result['rhodecode_title'] + + hgsettings2 = settings_model.get('realm') + hgsettings2.app_settings_value = form_result['rhodecode_realm'] + + self.sa.add(hgsettings1) self.sa.add(hgsettings2) self.sa.commit() set_rhodecode_config(config) h.flash(_('Updated application settings'), category='success') - + except: log.error(traceback.format_exc()) h.flash(_('error occurred during updating application settings'), category='error') - + self.sa.rollback() - + except formencode.Invalid, errors: return htmlfill.render( @@ -146,52 +151,60 @@ defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, - encoding="UTF-8") - + encoding="UTF-8") + if setting_id == 'mercurial': application_form = ApplicationUiSettingsForm()() try: form_result = application_form.to_python(dict(request.POST)) - + try: - + hgsettings1 = self.sa.query(RhodeCodeUi)\ .filter(RhodeCodeUi.ui_key == 'push_ssl').one() hgsettings1.ui_value = form_result['web_push_ssl'] - + hgsettings2 = self.sa.query(RhodeCodeUi)\ .filter(RhodeCodeUi.ui_key == '/').one() - hgsettings2.ui_value = form_result['paths_root_path'] - - + hgsettings2.ui_value = form_result['paths_root_path'] + + #HOOKS hgsettings3 = self.sa.query(RhodeCodeUi)\ .filter(RhodeCodeUi.ui_key == 'changegroup.update').one() - hgsettings3.ui_active = bool(form_result['hooks_changegroup_update']) - + hgsettings3.ui_active = bool(form_result['hooks_changegroup_update']) + hgsettings4 = self.sa.query(RhodeCodeUi)\ .filter(RhodeCodeUi.ui_key == 'changegroup.repo_size').one() - hgsettings4.ui_active = bool(form_result['hooks_changegroup_repo_size']) - - - - + hgsettings4.ui_active = bool(form_result['hooks_changegroup_repo_size']) + + hgsettings5 = self.sa.query(RhodeCodeUi)\ + .filter(RhodeCodeUi.ui_key == 'pretxnchangegroup.push_logger').one() + hgsettings5.ui_active = bool(form_result['hooks_pretxnchangegroup_push_logger']) + + hgsettings6 = self.sa.query(RhodeCodeUi)\ + .filter(RhodeCodeUi.ui_key == 'preoutgoing.pull_logger').one() + hgsettings6.ui_active = bool(form_result['hooks_preoutgoing_pull_logger']) + + self.sa.add(hgsettings1) self.sa.add(hgsettings2) self.sa.add(hgsettings3) self.sa.add(hgsettings4) + self.sa.add(hgsettings5) + self.sa.add(hgsettings6) self.sa.commit() - + h.flash(_('Updated mercurial settings'), category='success') - + except: log.error(traceback.format_exc()) h.flash(_('error occurred during updating application settings'), category='error') - + self.sa.rollback() - + except formencode.Invalid, errors: return htmlfill.render( @@ -199,12 +212,12 @@ defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, - encoding="UTF-8") - - - + encoding="UTF-8") + + + return redirect(url('admin_settings')) - + @HasPermissionAllDecorator('hg.admin') def delete(self, setting_id): """DELETE /admin/settings/setting_id: Delete an existing item""" @@ -214,41 +227,44 @@ # h.form(url('admin_setting', setting_id=ID), # method='delete') # url('admin_setting', setting_id=ID) - + @HasPermissionAllDecorator('hg.admin') def show(self, setting_id, format='html'): """GET /admin/settings/setting_id: Show a specific item""" # url('admin_setting', setting_id=ID) - - @HasPermissionAllDecorator('hg.admin') + + @HasPermissionAllDecorator('hg.admin') def edit(self, setting_id, format='html'): """GET /admin/settings/setting_id/edit: Form to edit an existing item""" # url('admin_edit_setting', setting_id=ID) - + @NotAnonymous() def my_account(self): """ GET /_admin/my_account Displays info about my account """ # url('admin_settings_my_account') - c.user = self.sa.query(User).get(c.rhodecode_user.user_id) - c.user_repos = [] - for repo in c.cached_repo_list.values(): - if repo.dbrepo.user.username == c.user.username: - c.user_repos.append(repo) - + + c.user = UserModel().get(c.rhodecode_user.user_id, cache=False) + all_repos = self.sa.query(Repository)\ + .filter(Repository.user_id == c.user.user_id)\ + .order_by(func.lower(Repository.repo_name))\ + .all() + + c.user_repos = ScmModel().get_repos(all_repos) + if c.user.username == 'default': - h.flash(_("You can't edit this user since it's" + h.flash(_("You can't edit this user since it's" " crucial for entire application"), category='warning') return redirect(url('users')) - - defaults = c.user.__dict__ + + defaults = c.user.get_dict() return htmlfill.render( render('admin/users/user_edit_my_account.html'), defaults=defaults, encoding="UTF-8", force_defaults=False - ) + ) def my_account_update(self): """PUT /_admin/my_account_update: Update an existing item""" @@ -266,15 +282,18 @@ try: form_result = _form.to_python(dict(request.POST)) user_model.update_my_account(uid, form_result) - h.flash(_('Your account was updated succesfully'), + h.flash(_('Your account was updated successfully'), category='success') - + except formencode.Invalid, errors: - c.user = self.sa.query(User).get(c.rhodecode_user.user_id) - c.user_repos = [] - for repo in c.cached_repo_list.values(): - if repo.dbrepo.user.username == c.user.username: - c.user_repos.append(repo) + c.user = user_model.get(c.rhodecode_user.user_id, cache=False) + c.user = UserModel().get(c.rhodecode_user.user_id, cache=False) + all_repos = self.sa.query(Repository)\ + .filter(Repository.user_id == c.user.user_id)\ + .order_by(func.lower(Repository.repo_name))\ + .all() + c.user_repos = ScmModel().get_repos(all_repos) + return htmlfill.render( render('admin/users/user_edit_my_account.html'), defaults=errors.value, @@ -283,11 +302,12 @@ encoding="UTF-8") except Exception: log.error(traceback.format_exc()) - h.flash(_('error occured during update of user %s') \ + h.flash(_('error occurred during update of user %s') \ % form_result.get('username'), category='error') - + return redirect(url('my_account')) - + + @NotAnonymous() @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository') def create_repository(self): """GET /_admin/create_repository: Form to create a new item""" @@ -295,4 +315,25 @@ c.new_repo = h.repo_name_slug(new_repo) return render('admin/repos/repo_add_create_repository.html') - + + def get_hg_ui_settings(self): + ret = self.sa.query(RhodeCodeUi).all() + + if not ret: + raise Exception('Could not get application ui settings !') + settings = {} + for each in ret: + k = each.ui_key + v = each.ui_value + if k == '/': + k = 'root_path' + + if k.find('.') != -1: + k = k.replace('.', '_') + + if each.ui_section == 'hooks': + v = each.ui_active + + settings[each.ui_section + '_' + k] = v + + return settings diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/admin/users.py --- a/rhodecode/controllers/admin/users.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/admin/users.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,8 +1,15 @@ -#!/usr/bin/env python -# encoding: utf-8 -# users controller for pylons -# Copyright (C) 2009-2010 Marcin Kuzminski -# +# -*- coding: utf-8 -*- +""" + rhodecode.controllers.admin.users + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Users crud controller for pylons + + :created_on: Apr 4, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; version 2 @@ -17,26 +24,24 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. -from rhodecode.lib.utils import action_logger -""" -Created on April 4, 2010 -users controller for pylons -@author: marcink -""" + +import logging +import traceback +import formencode from formencode import htmlfill from pylons import request, session, tmpl_context as c, url from pylons.controllers.util import abort, redirect from pylons.i18n.translation import _ + +from rhodecode.lib.exceptions import * from rhodecode.lib import helpers as h from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator from rhodecode.lib.base import BaseController, render -from rhodecode.model.db import User, UserLog + +from rhodecode.model.db import User from rhodecode.model.forms import UserForm -from rhodecode.model.user_model import UserModel, DefaultUserException -import formencode -import logging -import traceback +from rhodecode.model.user import UserModel log = logging.getLogger(__name__) @@ -45,26 +50,26 @@ # To properly map this controller, ensure your config/routing.py # file has a resource setup: # map.resource('user', 'users') - + @LoginRequired() @HasPermissionAllDecorator('hg.admin') def __before__(self): c.admin_user = session.get('admin_user') c.admin_username = session.get('admin_username') super(UsersController, self).__before__() - + def index(self, format='html'): """GET /users: All items in the collection""" # url('users') - - c.users_list = self.sa.query(User).all() + + c.users_list = self.sa.query(User).all() return render('admin/users/users.html') - + def create(self): """POST /users: Create a new item""" # url('users') - + user_model = UserModel() login_form = UserForm()() try: @@ -79,13 +84,13 @@ defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, - encoding="UTF-8") + encoding="UTF-8") except Exception: log.error(traceback.format_exc()) h.flash(_('error occured during creation of user %s') \ - % request.POST.get('username'), category='error') + % request.POST.get('username'), category='error') return redirect(url('users')) - + def new(self, format='html'): """GET /users/new: Form to create a new item""" # url('new_user') @@ -100,8 +105,8 @@ # method='put') # url('user', id=ID) user_model = UserModel() - c.user = user_model.get_user(id) - + c.user = user_model.get(id) + _form = UserForm(edit=True, old_data={'user_id':id, 'email':c.user.email})() form_result = {} @@ -109,21 +114,21 @@ form_result = _form.to_python(dict(request.POST)) user_model.update(id, form_result) h.flash(_('User updated succesfully'), category='success') - + except formencode.Invalid, errors: return htmlfill.render( render('admin/users/user_edit.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, - encoding="UTF-8") + encoding="UTF-8") except Exception: log.error(traceback.format_exc()) - h.flash(_('error occured during update of user %s') \ + h.flash(_('error occurred during update of user %s') \ % form_result.get('username'), category='error') - + return redirect(url('users')) - + def delete(self, id): """DELETE /users/id: Delete an existing item""" # Forms posted to this method should contain a hidden field: @@ -136,18 +141,18 @@ try: user_model.delete(id) h.flash(_('sucessfully deleted user'), category='success') - except DefaultUserException, e: + except (UserOwnsReposException, DefaultUserException), e: h.flash(str(e), category='warning') except Exception: h.flash(_('An error occured during deletion of user'), - category='error') + category='error') return redirect(url('users')) - + def show(self, id, format='html'): """GET /users/id: Show a specific item""" # url('user', id=ID) - - + + def edit(self, id, format='html'): """GET /users/id/edit: Form to edit an existing item""" # url('edit_user', id=ID) @@ -155,14 +160,13 @@ if not c.user: return redirect(url('users')) if c.user.username == 'default': - h.flash(_("You can't edit this user since it's" - " crucial for entire application"), category='warning') + h.flash(_("You can't edit this user"), category='warning') return redirect(url('users')) - - defaults = c.user.__dict__ + + defaults = c.user.get_dict() return htmlfill.render( render('admin/users/user_edit.html'), defaults=defaults, encoding="UTF-8", force_defaults=False - ) + ) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/branches.py --- a/rhodecode/controllers/branches.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/branches.py Sat Dec 18 14:45:58 2010 +0100 @@ -26,7 +26,7 @@ from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator from rhodecode.lib.base import BaseController, render from rhodecode.lib.utils import OrderedDict -from rhodecode.model.hg_model import HgModel +from rhodecode.model.scm import ScmModel import logging log = logging.getLogger(__name__) @@ -38,7 +38,7 @@ super(BranchesController, self).__before__() def index(self): - hg_model = HgModel() + hg_model = ScmModel() c.repo_info = hg_model.get_repo(c.repo_name) c.repo_branches = OrderedDict() for name, hash_ in c.repo_info.branches.items(): diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/changelog.py --- a/rhodecode/controllers/changelog.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/changelog.py Sat Dec 18 14:45:58 2010 +0100 @@ -32,19 +32,19 @@ from pylons import request, session, tmpl_context as c from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator from rhodecode.lib.base import BaseController, render -from rhodecode.model.hg_model import HgModel +from rhodecode.model.scm import ScmModel from webhelpers.paginate import Page import logging log = logging.getLogger(__name__) class ChangelogController(BaseController): - + @LoginRequired() @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', - 'repository.admin') + 'repository.admin') def __before__(self): super(ChangelogController, self).__before__() - + def index(self): limit = 100 default = 20 @@ -53,43 +53,47 @@ int_size = int(request.params.get('size')) except ValueError: int_size = default - int_size = int_size if int_size <= limit else limit + int_size = int_size if int_size <= limit else limit c.size = int_size session['changelog_size'] = c.size session.save() else: c.size = int(session.get('changelog_size', default)) - changesets = HgModel().get_repo(c.repo_name) - + changesets = ScmModel().get_repo(c.repo_name) + p = int(request.params.get('page', 1)) c.total_cs = len(changesets) c.pagination = Page(changesets, page=p, item_count=c.total_cs, items_per_page=c.size) - + self._graph(changesets, c.size, p) - + return render('changelog/changelog.html') def _graph(self, repo, size, p): revcount = size - if not repo.revisions:return json.dumps([]), 0 - + if not repo.revisions or repo.alias == 'git': + c.jsdata = json.dumps([]) + return + max_rev = repo.revisions[-1] + offset = 1 if p == 1 else ((p - 1) * revcount + 1) + rev_start = repo.revisions[(-1 * offset)] - + revcount = min(max_rev, revcount) rev_end = max(0, rev_start - revcount) dag = graph_rev(repo.repo, rev_start, rev_end) - + c.dag = tree = list(colored(dag)) data = [] for (id, type, ctx, vtx, edges) in tree: if type != CHANGESET: continue data.append(('', vtx, edges)) - - c.jsdata = json.dumps(data) + c.jsdata = json.dumps(data) + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/changeset.py --- a/rhodecode/controllers/changeset.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/changeset.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,7 +1,15 @@ -#!/usr/bin/env python -# encoding: utf-8 -# changeset controller for pylons -# Copyright (C) 2009-2010 Marcin Kuzminski +# -*- coding: utf-8 -*- +""" + rhodecode.controllers.changeset + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + changeset controller for pylons + + :created_on: Apr 25, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; version 2 @@ -16,97 +24,96 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. -from rhodecode.lib.utils import EmptyChangeset -""" -Created on April 25, 2010 -changeset controller for pylons -@author: marcink -""" +import logging +import traceback + from pylons import tmpl_context as c, url, request, response from pylons.i18n.translation import _ from pylons.controllers.util import redirect + +import rhodecode.lib.helpers as h from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator from rhodecode.lib.base import BaseController, render -from rhodecode.model.hg_model import HgModel +from rhodecode.lib.utils import EmptyChangeset +from rhodecode.model.scm import ScmModel + from vcs.exceptions import RepositoryError, ChangesetError from vcs.nodes import FileNode from vcs.utils import diffs as differ -import logging -import traceback log = logging.getLogger(__name__) class ChangesetController(BaseController): - + @LoginRequired() @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', - 'repository.admin') + 'repository.admin') def __before__(self): super(ChangesetController, self).__before__() - + def index(self, revision): - hg_model = HgModel() - cut_off_limit = 1024 * 250 - + hg_model = ScmModel() + def wrap_to_table(str): - + return '''
%s
''' % str - + try: c.changeset = hg_model.get_repo(c.repo_name).get_changeset(revision) - except RepositoryError: + except RepositoryError, e: log.error(traceback.format_exc()) - return redirect(url('hg_home')) + h.flash(str(e), category='warning') + return redirect(url('home')) else: try: c.changeset_old = c.changeset.parents[0] except IndexError: c.changeset_old = None c.changes = [] - + #=================================================================== # ADDED FILES #=================================================================== c.sum_added = 0 for node in c.changeset.added: - + filenode_old = FileNode(node.path, '', EmptyChangeset()) if filenode_old.is_binary or node.is_binary: diff = wrap_to_table(_('binary file')) else: c.sum_added += node.size - if c.sum_added < cut_off_limit: + if c.sum_added < self.cut_off_limit: f_udiff = differ.get_udiff(filenode_old, node) diff = differ.DiffProcessor(f_udiff).as_html() - + else: diff = wrap_to_table(_('Changeset is to big and was cut' ' off, see raw changeset instead')) - + cs1 = None - cs2 = node.last_changeset.short_id + cs2 = node.last_changeset.raw_id c.changes.append(('added', node, diff, cs1, cs2)) - + #=================================================================== # CHANGED FILES #=================================================================== - c.sum_removed = 0 + c.sum_removed = 0 for node in c.changeset.changed: try: filenode_old = c.changeset_old.get_node(node.path) except ChangesetError: filenode_old = FileNode(node.path, '', EmptyChangeset()) - + if filenode_old.is_binary or node.is_binary: diff = wrap_to_table(_('binary file')) else: - - if c.sum_removed < cut_off_limit: + + if c.sum_removed < self.cut_off_limit: f_udiff = differ.get_udiff(filenode_old, node) diff = differ.DiffProcessor(f_udiff).as_html() if diff: @@ -114,68 +121,72 @@ else: diff = wrap_to_table(_('Changeset is to big and was cut' ' off, see raw changeset instead')) - - - cs1 = filenode_old.last_changeset.short_id - cs2 = node.last_changeset.short_id + + + cs1 = filenode_old.last_changeset.raw_id + cs2 = node.last_changeset.raw_id c.changes.append(('changed', node, diff, cs1, cs2)) - + #=================================================================== # REMOVED FILES #=================================================================== for node in c.changeset.removed: - c.changes.append(('removed', node, None, None, None)) - + c.changes.append(('removed', node, None, None, None)) + return render('changeset/changeset.html') def raw_changeset(self, revision): - - hg_model = HgModel() + + hg_model = ScmModel() method = request.GET.get('diff', 'show') try: - c.changeset = hg_model.get_repo(c.repo_name).get_changeset(revision) + r = hg_model.get_repo(c.repo_name) + c.scm_type = r.alias + c.changeset = r.get_changeset(revision) except RepositoryError: log.error(traceback.format_exc()) - return redirect(url('hg_home')) + return redirect(url('home')) else: try: c.changeset_old = c.changeset.parents[0] except IndexError: c.changeset_old = None c.changes = [] - + for node in c.changeset.added: filenode_old = FileNode(node.path, '') if filenode_old.is_binary or node.is_binary: - diff = _('binary file') - else: + diff = _('binary file') + '\n' + else: f_udiff = differ.get_udiff(filenode_old, node) diff = differ.DiffProcessor(f_udiff).raw_diff() cs1 = None - cs2 = node.last_changeset.short_id + cs2 = node.last_changeset.raw_id c.changes.append(('added', node, diff, cs1, cs2)) - + for node in c.changeset.changed: filenode_old = c.changeset_old.get_node(node.path) if filenode_old.is_binary or node.is_binary: diff = _('binary file') - else: + else: f_udiff = differ.get_udiff(filenode_old, node) diff = differ.DiffProcessor(f_udiff).raw_diff() - cs1 = filenode_old.last_changeset.short_id - cs2 = node.last_changeset.short_id - c.changes.append(('changed', node, diff, cs1, cs2)) - + cs1 = filenode_old.last_changeset.raw_id + cs2 = node.last_changeset.raw_id + c.changes.append(('changed', node, diff, cs1, cs2)) + response.content_type = 'text/plain' + if method == 'download': - response.content_disposition = 'attachment; filename=%s.patch' % revision + response.content_disposition = 'attachment; filename=%s.patch' % revision + parent = True if len(c.changeset.parents) > 0 else False c.parent_tmpl = 'Parent %s' % c.changeset.parents[0].raw_id if parent else '' - + c.diffs = '' for x in c.changes: c.diffs += x[2] - + return render('changeset/raw_changeset.html') diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/error.py --- a/rhodecode/controllers/error.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/error.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,33 +1,58 @@ -import logging +# -*- coding: utf-8 -*- +""" + package.rhodecode.controllers.error + ~~~~~~~~~~~~~~ + + RhodeCode error controller + + :created_on: Dec 8, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +import os import cgi -import os +import logging import paste.fileapp -from pylons import tmpl_context as c, app_globals as g, request, config -from pylons.controllers.util import forward + +from pylons import tmpl_context as c, request from pylons.i18n.translation import _ +from pylons.middleware import media_path + from rhodecode.lib.base import BaseController, render -from pylons.middleware import media_path -from rhodecode.lib.utils import check_repo -import rhodecode.lib.helpers as h -from rhodecode import __version__ + log = logging.getLogger(__name__) class ErrorController(BaseController): - """ - Generates error documents as and when they are required. + """Generates error documents as and when they are required. The ErrorDocuments middleware forwards to ErrorController when error related status codes are returned from the application. - This behaviour can be altered by changing the parameters to the + This behavior can be altered by changing the parameters to the ErrorDocuments middleware in your config/middleware.py file. """ + def __before__(self): pass#disable all base actions since we don't need them here - + def document(self): resp = request.environ.get('pylons.original_response') - + log.debug('### %s ###', resp.status) e = request.environ @@ -36,7 +61,7 @@ 'host':e.get('HTTP_HOST'), } - + c.error_message = cgi.escape(request.GET.get('code', str(resp.status))) c.error_explanation = self.get_error_explanation(resp.status_int) @@ -74,7 +99,7 @@ if code == 400: return _('The request could not be understood by the server due to malformed syntax.') if code == 401: - return _('Unathorized access to resource') + return _('Unauthorized access to resource') if code == 403: return _("You don't have permission to view this page") if code == 404: diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/feed.py --- a/rhodecode/controllers/feed.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/feed.py Sat Dec 18 14:45:58 2010 +0100 @@ -24,7 +24,7 @@ """ from pylons import tmpl_context as c, url, response from rhodecode.lib.base import BaseController, render -from rhodecode.model.hg_model import HgModel +from rhodecode.model.scm import ScmModel from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed import logging log = logging.getLogger(__name__) @@ -49,12 +49,12 @@ language=self.language, ttl=self.ttl) - changesets = HgModel().get_repo(repo_name) + changesets = ScmModel().get_repo(repo_name) for cs in changesets[:self.feed_nr]: feed.add_item(title=cs.message, link=url('changeset_home', repo_name=repo_name, - revision=cs.short_id, qualified=True), + revision=cs.raw_id, qualified=True), description=str(cs.date)) response.content_type = feed.mime_type @@ -69,11 +69,11 @@ language=self.language, ttl=self.ttl) - changesets = HgModel().get_repo(repo_name) + changesets = ScmModel().get_repo(repo_name) for cs in changesets[:self.feed_nr]: feed.add_item(title=cs.message, link=url('changeset_home', repo_name=repo_name, - revision=cs.short_id, qualified=True), + revision=cs.raw_id, qualified=True), description=str(cs.date)) response.content_type = feed.mime_type diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/files.py --- a/rhodecode/controllers/files.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/files.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,8 +1,15 @@ -#!/usr/bin/env python -# encoding: utf-8 -# files controller for pylons -# Copyright (C) 2009-2010 Marcin Kuzminski +# -*- coding: utf-8 -*- +""" + rhodecode.controllers.files + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Files controller for RhodeCode + + :created_on: Apr 21, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; version 2 @@ -17,107 +24,115 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. -""" -Created on April 21, 2010 -files controller for pylons -@author: marcink -""" +import tempfile +import logging +import rhodecode.lib.helpers as h + from mercurial import archival + from pylons import request, response, session, tmpl_context as c, url from pylons.i18n.translation import _ from pylons.controllers.util import redirect + from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator from rhodecode.lib.base import BaseController, render from rhodecode.lib.utils import EmptyChangeset -from rhodecode.model.hg_model import HgModel +from rhodecode.model.scm import ScmModel + from vcs.exceptions import RepositoryError, ChangesetError from vcs.nodes import FileNode from vcs.utils import diffs as differ -import logging -import rhodecode.lib.helpers as h -import tempfile - + log = logging.getLogger(__name__) class FilesController(BaseController): - + @LoginRequired() @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', - 'repository.admin') + 'repository.admin') def __before__(self): super(FilesController, self).__before__() - c.file_size_limit = 250 * 1024 #limit of file size to display + c.cut_off_limit = self.cut_off_limit def index(self, repo_name, revision, f_path): - hg_model = HgModel() - c.repo = repo = hg_model.get_repo(c.repo_name) + hg_model = ScmModel() + c.repo = hg_model.get_repo(c.repo_name) revision = request.POST.get('at_rev', None) or revision - + def get_next_rev(cur): max_rev = len(c.repo.revisions) - 1 r = cur + 1 if r > max_rev: r = max_rev return r - + def get_prev_rev(cur): r = cur - 1 return r c.f_path = f_path - - + + try: - cur_rev = repo.get_changeset(revision).revision - prev_rev = repo.get_changeset(get_prev_rev(cur_rev)).short_id - next_rev = repo.get_changeset(get_next_rev(cur_rev)).short_id - + c.changeset = c.repo.get_changeset(revision) + cur_rev = c.changeset.revision + prev_rev = c.repo.get_changeset(get_prev_rev(cur_rev)).raw_id + next_rev = c.repo.get_changeset(get_next_rev(cur_rev)).raw_id + c.url_prev = url('files_home', repo_name=c.repo_name, - revision=prev_rev, f_path=f_path) + revision=prev_rev, f_path=f_path) c.url_next = url('files_home', repo_name=c.repo_name, - revision=next_rev, f_path=f_path) - - c.changeset = repo.get_changeset(revision) - - c.cur_rev = c.changeset.short_id - c.rev_nr = c.changeset.revision - c.files_list = c.changeset.get_node(f_path) - c.file_history = self._get_history(repo, c.files_list, f_path) - - except (RepositoryError, ChangesetError): - c.files_list = None - + revision=next_rev, f_path=f_path) + + try: + c.files_list = c.changeset.get_node(f_path) + c.file_history = self._get_history(c.repo, c.files_list, f_path) + except RepositoryError, e: + h.flash(str(e), category='warning') + redirect(h.url('files_home', repo_name=repo_name, revision=revision)) + + except RepositoryError, e: + h.flash(str(e), category='warning') + redirect(h.url('files_home', repo_name=repo_name, revision='tip')) + + + return render('files/files.html') def rawfile(self, repo_name, revision, f_path): - hg_model = HgModel() + hg_model = ScmModel() c.repo = hg_model.get_repo(c.repo_name) file_node = c.repo.get_changeset(revision).get_node(f_path) response.content_type = file_node.mimetype response.content_disposition = 'attachment; filename=%s' \ - % f_path.split('/')[-1] + % f_path.split('/')[-1] return file_node.content def raw(self, repo_name, revision, f_path): - hg_model = HgModel() + hg_model = ScmModel() c.repo = hg_model.get_repo(c.repo_name) file_node = c.repo.get_changeset(revision).get_node(f_path) response.content_type = 'text/plain' - + return file_node.content - + def annotate(self, repo_name, revision, f_path): - hg_model = HgModel() + hg_model = ScmModel() c.repo = hg_model.get_repo(c.repo_name) - cs = c.repo.get_changeset(revision) - c.file = cs.get_node(f_path) - c.file_msg = cs.get_file_message(f_path) - c.cur_rev = cs.short_id - c.rev_nr = cs.revision + + try: + c.cs = c.repo.get_changeset(revision) + c.file = c.cs.get_node(f_path) + except RepositoryError, e: + h.flash(str(e), category='warning') + redirect(h.url('files_home', repo_name=repo_name, revision=revision)) + + c.file_history = self._get_history(c.repo, c.file, f_path) + c.f_path = f_path return render('files/files_annotate.html') - + def archivefile(self, repo_name, revision, fileformat): archive_specs = { '.tar.bz2': ('application/x-tar', 'tbz2'), @@ -126,7 +141,7 @@ } if not archive_specs.has_key(fileformat): return 'Unknown archive type %s' % fileformat - + def read_in_chunks(file_object, chunk_size=1024 * 40): """Lazy function (generator) to read a file piece by piece. Default chunk size: 40k.""" @@ -134,10 +149,10 @@ data = file_object.read(chunk_size) if not data: break - yield data - + yield data + archive = tempfile.TemporaryFile() - repo = HgModel().get_repo(repo_name).repo + repo = ScmModel().get_repo(repo_name).repo fname = '%s-%s%s' % (repo_name, revision, fileformat) archival.archive(repo, archive, revision, archive_specs[fileformat][1], prefix='%s-%s' % (repo_name, revision)) @@ -145,9 +160,9 @@ response.content_disposition = 'attachment; filename=%s' % fname archive.seek(0) return read_in_chunks(archive) - + def diff(self, repo_name, f_path): - hg_model = HgModel() + hg_model = ScmModel() diff1 = request.GET.get('diff1') diff2 = request.GET.get('diff2') c.action = request.GET.get('diff') @@ -162,7 +177,7 @@ else: c.changeset_1 = EmptyChangeset() node1 = FileNode('.', '', changeset=c.changeset_1) - + if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]: c.changeset_2 = c.repo.get_changeset(diff2) node2 = c.changeset_2.get_node(f_path) @@ -173,43 +188,66 @@ return redirect(url('files_home', repo_name=c.repo_name, f_path=f_path)) - c.diff1 = 'r%s:%s' % (c.changeset_1.revision, c.changeset_1.short_id) - c.diff2 = 'r%s:%s' % (c.changeset_2.revision, c.changeset_2.short_id) - f_udiff = differ.get_udiff(node1, node2) diff = differ.DiffProcessor(f_udiff) - + if c.action == 'download': diff_name = '%s_vs_%s.diff' % (diff1, diff2) response.content_type = 'text/plain' response.content_disposition = 'attachment; filename=%s' \ - % diff_name + % diff_name return diff.raw_diff() - + elif c.action == 'raw': - c.cur_diff = '
%s
' % h.escape(diff.raw_diff()) + response.content_type = 'text/plain' + return diff.raw_diff() + elif c.action == 'diff': - if node1.size > c.file_size_limit or node2.size > c.file_size_limit: + if node1.size > self.cut_off_limit or node2.size > self.cut_off_limit: c.cur_diff = _('Diff is to big to display') else: c.cur_diff = diff.as_html() else: #default option - if node1.size > c.file_size_limit or node2.size > c.file_size_limit: + if node1.size > self.cut_off_limit or node2.size > self.cut_off_limit: c.cur_diff = _('Diff is to big to display') else: c.cur_diff = diff.as_html() - - if not c.cur_diff: c.no_changes = True + + if not c.cur_diff: c.no_changes = True return render('files/file_diff.html') - + def _get_history(self, repo, node, f_path): from vcs.nodes import NodeKind if not node.kind is NodeKind.FILE: return [] changesets = node.history hist_l = [] + + changesets_group = ([], _("Changesets")) + branches_group = ([], _("Branches")) + tags_group = ([], _("Tags")) + for chs in changesets: n_desc = 'r%s:%s' % (chs.revision, chs.short_id) - hist_l.append((chs.short_id, n_desc,)) + changesets_group[0].append((chs.raw_id, n_desc,)) + + hist_l.append(changesets_group) + + for name, chs in c.repository_branches.items(): + #chs = chs.split(':')[-1] + branches_group[0].append((chs, name),) + hist_l.append(branches_group) + + for name, chs in c.repository_tags.items(): + #chs = chs.split(':')[-1] + tags_group[0].append((chs, name),) + hist_l.append(tags_group) + return hist_l + +# [ +# ([("u1", "User1"), ("u2", "User2")], "Users"), +# ([("g1", "Group1"), ("g2", "Group2")], "Groups") +# ] + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/hg.py --- a/rhodecode/controllers/hg.py Thu Nov 18 21:35:52 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,58 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -# hg controller for pylons -# Copyright (C) 2009-2010 Marcin Kuzminski -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; version 2 -# of the License or (at your opinion) any later version of the license. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. -""" -Created on February 18, 2010 -hg controller for pylons -@author: marcink -""" -from operator import itemgetter -from pylons import tmpl_context as c, request -from rhodecode.lib.auth import LoginRequired -from rhodecode.lib.base import BaseController, render -from rhodecode.model.hg_model import HgModel -import logging -log = logging.getLogger(__name__) - -class HgController(BaseController): - - @LoginRequired() - def __before__(self): - super(HgController, self).__before__() - - def index(self): - sortables = ['name', 'description', 'last_change', 'tip', 'contact'] - current_sort = request.GET.get('sort', 'name') - current_sort_slug = current_sort.replace('-', '') - - if current_sort_slug not in sortables: - c.sort_by = 'name' - current_sort_slug = c.sort_by - else: - c.sort_by = current_sort - c.sort_slug = current_sort_slug - cached_repo_list = HgModel().get_repos() - - sort_key = current_sort_slug + '_sort' - if c.sort_by.startswith('-'): - c.repos_list = sorted(cached_repo_list, key=itemgetter(sort_key), reverse=True) - else: - c.repos_list = sorted(cached_repo_list, key=itemgetter(sort_key), reverse=False) - - return render('/index.html') diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/home.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/controllers/home.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# encoding: utf-8 +# hg controller for pylons +# Copyright (C) 2009-2010 Marcin Kuzminski +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +""" +Created on February 18, 2010 +hg controller for pylons +@author: marcink +""" +from operator import itemgetter +from pylons import tmpl_context as c, request +from rhodecode.lib.auth import LoginRequired +from rhodecode.lib.base import BaseController, render +from rhodecode.model.scm import ScmModel +import logging +log = logging.getLogger(__name__) + +class HomeController(BaseController): + + @LoginRequired() + def __before__(self): + super(HomeController, self).__before__() + + def index(self): + sortables = ['name', 'description', 'last_change', 'tip', 'contact'] + current_sort = request.GET.get('sort', 'name') + current_sort_slug = current_sort.replace('-', '') + + if current_sort_slug not in sortables: + c.sort_by = 'name' + current_sort_slug = c.sort_by + else: + c.sort_by = current_sort + c.sort_slug = current_sort_slug + cached_repo_list = ScmModel().get_repos() + + sort_key = current_sort_slug + '_sort' + if c.sort_by.startswith('-'): + c.repos_list = sorted(cached_repo_list, key=itemgetter(sort_key), reverse=True) + else: + c.repos_list = sorted(cached_repo_list, key=itemgetter(sort_key), reverse=False) + + return render('/index.html') diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/journal.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/controllers/journal.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# encoding: utf-8 +# journal controller for pylons +# Copyright (C) 2009-2010 Marcin Kuzminski +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +""" +Created on November 21, 2010 +journal controller for pylons +@author: marcink +""" + +from pylons import request, response, session, tmpl_context as c, url +from pylons.controllers.util import abort, redirect +from rhodecode.lib.auth import LoginRequired, NotAnonymous +from rhodecode.lib.base import BaseController, render +from rhodecode.lib.helpers import get_token +from rhodecode.model.db import UserLog, UserFollowing +from rhodecode.model.scm import ScmModel +from sqlalchemy import or_ +import logging +from paste.httpexceptions import HTTPInternalServerError, HTTPNotFound + +log = logging.getLogger(__name__) + +class JournalController(BaseController): + + + @LoginRequired() + @NotAnonymous() + def __before__(self): + super(JournalController, self).__before__() + + def index(self): + # Return a rendered template + + c.following = self.sa.query(UserFollowing)\ + .filter(UserFollowing.user_id == c.rhodecode_user.user_id).all() + + repo_ids = [x.follows_repository.repo_id for x in c.following + if x.follows_repository is not None] + user_ids = [x.follows_user.user_id for x in c.following + if x.follows_user is not None] + + c.journal = self.sa.query(UserLog)\ + .filter(or_( + UserLog.repository_id.in_(repo_ids), + UserLog.user_id.in_(user_ids), + ))\ + .order_by(UserLog.action_date.desc())\ + .limit(20)\ + .all() + return render('/journal.html') + + def toggle_following(self): + + if request.POST.get('auth_token') == get_token(): + scm_model = ScmModel() + + user_id = request.POST.get('follows_user_id') + if user_id: + try: + scm_model.toggle_following_user(user_id, + c.rhodecode_user.user_id) + return 'ok' + except: + raise HTTPInternalServerError() + + repo_id = request.POST.get('follows_repo_id') + if repo_id: + try: + scm_model.toggle_following_repo(repo_id, + c.rhodecode_user.user_id) + return 'ok' + except: + raise HTTPInternalServerError() + + + + raise HTTPInternalServerError() diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/login.py --- a/rhodecode/controllers/login.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/login.py Sat Dec 18 14:45:58 2010 +0100 @@ -28,10 +28,10 @@ from pylons.controllers.util import abort, redirect from rhodecode.lib.auth import AuthUser, HasPermissionAnyDecorator from rhodecode.lib.base import BaseController, render -import rhodecode.lib.helpers as h +import rhodecode.lib.helpers as h from pylons.i18n.translation import _ from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm -from rhodecode.model.user_model import UserModel +from rhodecode.model.user import UserModel import formencode import logging @@ -45,17 +45,19 @@ def index(self): #redirect if already logged in c.came_from = request.GET.get('came_from', None) - - if c.rhodecode_user.is_authenticated: - return redirect(url('hg_home')) - + + if c.rhodecode_user.is_authenticated \ + and c.rhodecode_user.username != 'default': + + return redirect(url('home')) + if request.POST: #import Login Form validator class login_form = LoginForm() try: c.form_result = login_form.to_python(dict(request.POST)) username = c.form_result['username'] - user = UserModel().get_user_by_name(username) + user = UserModel().get_by_username(username, case_insensitive=True) auth_user = AuthUser() auth_user.username = user.username auth_user.is_authenticated = True @@ -66,14 +68,14 @@ session['rhodecode_user'] = auth_user session.save() log.info('user %s is now authenticated', username) - + user.update_lastlogin() - + if c.came_from: return redirect(c.came_from) else: - return redirect(url('hg_home')) - + return redirect(url('home')) + except formencode.Invalid, errors: return htmlfill.render( render('/login.html'), @@ -81,30 +83,30 @@ errors=errors.error_dict or {}, prefix_error=False, encoding="UTF-8") - + return render('/login.html') - + @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate') def register(self): user_model = UserModel() c.auto_active = False - for perm in user_model.get_default().user_perms: + for perm in user_model.get_by_username('default', cache=False).user_perms: if perm.permission.permission_name == 'hg.register.auto_activate': c.auto_active = True break - + if request.POST: - + register_form = RegisterForm()() try: form_result = register_form.to_python(dict(request.POST)) form_result['active'] = c.auto_active user_model.create_registration(form_result) h.flash(_('You have successfully registered into rhodecode'), - category='success') + category='success') return redirect(url('login_home')) - + except formencode.Invalid, errors: return htmlfill.render( render('/register.html'), @@ -112,21 +114,21 @@ errors=errors.error_dict or {}, prefix_error=False, encoding="UTF-8") - + return render('/register.html') def password_reset(self): user_model = UserModel() if request.POST: - + password_reset_form = PasswordResetForm()() try: form_result = password_reset_form.to_python(dict(request.POST)) user_model.reset_password(form_result) h.flash(_('Your new password was sent'), - category='success') + category='success') return redirect(url('login_home')) - + except formencode.Invalid, errors: return htmlfill.render( render('/password_reset.html'), @@ -134,11 +136,11 @@ errors=errors.error_dict or {}, prefix_error=False, encoding="UTF-8") - + return render('/password_reset.html') - + def logout(self): session['rhodecode_user'] = AuthUser() session.save() log.info('Logging out and setting user as Empty') - redirect(url('hg_home')) + redirect(url('home')) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/search.py --- a/rhodecode/controllers/search.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/search.py Sat Dec 18 14:45:58 2010 +0100 @@ -22,11 +22,11 @@ search controller for pylons @author: marcink """ -from pylons import request, response, session, tmpl_context as c, url +from pylons import request, response, config, session, tmpl_context as c, url from pylons.controllers.util import abort, redirect from rhodecode.lib.auth import LoginRequired from rhodecode.lib.base import BaseController, render -from rhodecode.lib.indexers import IDX_LOCATION, SCHEMA, IDX_NAME, ResultWrapper +from rhodecode.lib.indexers import SCHEMA, IDX_NAME, ResultWrapper from webhelpers.paginate import Page from webhelpers.util import update_params from pylons.i18n.translation import _ @@ -42,7 +42,7 @@ @LoginRequired() def __before__(self): - super(SearchController, self).__before__() + super(SearchController, self).__before__() def index(self, search_repo=None): c.repo_name = search_repo @@ -56,15 +56,16 @@ 'repository':'repository'}\ .get(c.cur_type, 'content') - + if c.cur_query: cur_query = c.cur_query.lower() - + if c.cur_query: p = int(request.params.get('page', 1)) highlight_items = set() try: - idx = open_dir(IDX_LOCATION, indexname=IDX_NAME) + idx = open_dir(config['app_conf']['index_dir'] + , indexname=IDX_NAME) searcher = idx.searcher() qp = QueryParser(search_type, schema=SCHEMA) @@ -72,7 +73,7 @@ cur_query = u'repository:%s %s' % (c.repo_name, cur_query) try: query = qp.parse(unicode(cur_query)) - + if isinstance(query, Phrase): highlight_items.update(query.words) else: @@ -81,14 +82,14 @@ highlight_items.add(i[1]) matcher = query.matcher(searcher) - + log.debug(query) log.debug(highlight_items) results = searcher.search(query) res_ln = len(results) c.runtime = '%s results (%.3f seconds)' \ % (res_ln, results.runtime) - + def url_generator(**kw): return update_params("?q=%s&type=%s" \ % (c.cur_query, c.cur_search), **kw) @@ -98,8 +99,8 @@ highlight_items), page=p, item_count=res_ln, items_per_page=10, url=url_generator) - - + + except QueryParserError: c.runtime = _('Invalid search query. Try quoting it.') searcher.close() @@ -107,6 +108,6 @@ log.error(traceback.format_exc()) log.error('Empty Index data') c.runtime = _('There is no index to search in. Please run whoosh indexer') - + # Return a rendered template return render('/search/search.html') diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/settings.py --- a/rhodecode/controllers/settings.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/settings.py Sat Dec 18 14:45:58 2010 +0100 @@ -30,7 +30,7 @@ from rhodecode.lib.base import BaseController, render from rhodecode.lib.utils import invalidate_cache, action_logger from rhodecode.model.forms import RepoSettingsForm, RepoForkForm -from rhodecode.model.repo_model import RepoModel +from rhodecode.model.repo import RepoModel import formencode import logging import rhodecode.lib.helpers as h @@ -41,35 +41,35 @@ class SettingsController(BaseController): @LoginRequired() - @HasRepoPermissionAllDecorator('repository.admin') + @HasRepoPermissionAllDecorator('repository.admin') def __before__(self): super(SettingsController, self).__before__() - + def index(self, repo_name): repo_model = RepoModel() - c.repo_info = repo = repo_model.get(repo_name) + c.repo_info = repo = repo_model.get_by_repo_name(repo_name) if not repo: - h.flash(_('%s repository is not mapped to db perhaps' + h.flash(_('%s repository is not mapped to db perhaps' ' it was created or renamed from the filesystem' ' please run the application again' ' in order to rescan repositories') % repo_name, category='error') - - return redirect(url('hg_home')) - defaults = c.repo_info.__dict__ + + return redirect(url('home')) + defaults = c.repo_info.get_dict() defaults.update({'user':c.repo_info.user.username}) c.users_array = repo_model.get_users_js() - + for p in c.repo_info.repo_to_perm: - defaults.update({'perm_%s' % p.user.username: + defaults.update({'perm_%s' % p.user.username: p.permission.permission_name}) - + return htmlfill.render( render('settings/repo_settings.html'), defaults=defaults, encoding="UTF-8", force_defaults=False - ) + ) def update(self, repo_name): repo_model = RepoModel() @@ -78,12 +78,14 @@ try: form_result = _form.to_python(dict(request.POST)) repo_model.update(repo_name, form_result) - invalidate_cache('cached_repo_list') + invalidate_cache('get_repo_cached_%s' % repo_name) h.flash(_('Repository %s updated successfully' % repo_name), category='success') - changed_name = form_result['repo_name'] + changed_name = form_result['repo_name'] + action_logger(self.rhodecode_user, 'user_updated_repo', + changed_name, '', self.sa) except formencode.Invalid, errors: - c.repo_info = repo_model.get(repo_name) + c.repo_info = repo_model.get_by_repo_name(repo_name) c.users_array = repo_model.get_users_js() errors.value.update({'user':c.repo_info.user.username}) return htmlfill.render( @@ -91,17 +93,17 @@ defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, - encoding="UTF-8") + encoding="UTF-8") except Exception: log.error(traceback.format_exc()) - h.flash(_('error occured during update of repository %s') \ + h.flash(_('error occurred during update of repository %s') \ % repo_name, category='error') - + return redirect(url('repo_settings_home', repo_name=changed_name)) - def delete(self, repo_name): + def delete(self, repo_name): """DELETE /repos/repo_name: Delete an existing item""" # Forms posted to this method should contain a hidden field: # @@ -109,67 +111,68 @@ # h.form(url('repo_settings_delete', repo_name=ID), # method='delete') # url('repo_settings_delete', repo_name=ID) - + repo_model = RepoModel() - repo = repo_model.get(repo_name) + repo = repo_model.get_by_repo_name(repo_name) if not repo: - h.flash(_('%s repository is not mapped to db perhaps' + h.flash(_('%s repository is not mapped to db perhaps' ' it was moved or renamed from the filesystem' ' please run the application again' ' in order to rescan repositories') % repo_name, category='error') - - return redirect(url('hg_home')) + + return redirect(url('home')) try: action_logger(self.rhodecode_user, 'user_deleted_repo', - repo_name, '', self.sa) - repo_model.delete(repo) - invalidate_cache('cached_repo_list') + repo_name, '', self.sa) + repo_model.delete(repo) + invalidate_cache('get_repo_cached_%s' % repo_name) h.flash(_('deleted repository %s') % repo_name, category='success') except Exception: h.flash(_('An error occurred during deletion of %s') % repo_name, category='error') - - return redirect(url('hg_home')) - + + return redirect(url('home')) + def fork(self, repo_name): repo_model = RepoModel() - c.repo_info = repo = repo_model.get(repo_name) + c.repo_info = repo = repo_model.get_by_repo_name(repo_name) if not repo: - h.flash(_('%s repository is not mapped to db perhaps' + h.flash(_('%s repository is not mapped to db perhaps' ' it was created or renamed from the filesystem' ' please run the application again' ' in order to rescan repositories') % repo_name, category='error') - - return redirect(url('hg_home')) - + + return redirect(url('home')) + return render('settings/repo_fork.html') - - - + + + def fork_create(self, repo_name): repo_model = RepoModel() - c.repo_info = repo_model.get(repo_name) - _form = RepoForkForm()() + c.repo_info = repo_model.get_by_repo_name(repo_name) + _form = RepoForkForm(old_data={'repo_type':c.repo_info.repo_type})() form_result = {} try: form_result = _form.to_python(dict(request.POST)) form_result.update({'repo_name':repo_name}) repo_model.create_fork(form_result, c.rhodecode_user) - h.flash(_('fork %s repository as %s task added') \ + h.flash(_('forked %s repository as %s') \ % (repo_name, form_result['fork_name']), category='success') - action_logger(self.rhodecode_user, 'user_forked_repo', - repo_name, '', self.sa) + action_logger(self.rhodecode_user, + 'user_forked_repo:%s' % form_result['fork_name'], + repo_name, '', self.sa) except formencode.Invalid, errors: c.new_repo = errors.value['fork_name'] r = render('settings/repo_fork.html') - + return htmlfill.render( r, defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, - encoding="UTF-8") - return redirect(url('hg_home')) + encoding="UTF-8") + return redirect(url('home')) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/shortlog.py --- a/rhodecode/controllers/shortlog.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/shortlog.py Sat Dec 18 14:45:58 2010 +0100 @@ -25,7 +25,7 @@ from pylons import tmpl_context as c, request from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator from rhodecode.lib.base import BaseController, render -from rhodecode.model.hg_model import HgModel +from rhodecode.model.scm import ScmModel from webhelpers.paginate import Page import logging log = logging.getLogger(__name__) @@ -40,7 +40,7 @@ def index(self): p = int(request.params.get('page', 1)) - repo = HgModel().get_repo(c.repo_name) + repo = ScmModel().get_repo(c.repo_name) c.repo_changesets = Page(repo, page=p, items_per_page=20) c.shortlog_data = render('shortlog/shortlog_data.html') if request.params.get('partial'): diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/summary.py --- a/rhodecode/controllers/summary.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/summary.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,8 +1,15 @@ -#!/usr/bin/env python -# encoding: utf-8 -# summary controller for pylons -# Copyright (C) 2009-2010 Marcin Kuzminski -# +# -*- coding: utf-8 -*- +""" + package.rhodecode.controllers.summary + ~~~~~~~~~~~~~~ + + Summary controller for Rhodecode + + :created_on: Apr 18, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; version 2 @@ -17,24 +24,29 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. -""" -Created on April 18, 2010 -summary controller for pylons -@author: marcink -""" + +import calendar +import logging +from time import mktime +from datetime import datetime, timedelta + +from vcs.exceptions import ChangesetError + from pylons import tmpl_context as c, request, url +from pylons.i18n.translation import _ + +from rhodecode.model.scm import ScmModel +from rhodecode.model.db import Statistics + from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator from rhodecode.lib.base import BaseController, render -from rhodecode.lib.utils import OrderedDict -from rhodecode.model.hg_model import HgModel -from rhodecode.model.db import Statistics -from webhelpers.paginate import Page +from rhodecode.lib.utils import OrderedDict, EmptyChangeset + from rhodecode.lib.celerylib import run_task from rhodecode.lib.celerylib.tasks import get_commits_stats -from datetime import datetime, timedelta -from time import mktime -import calendar -import logging + +from webhelpers.paginate import Page + try: import json except ImportError: @@ -43,66 +55,90 @@ log = logging.getLogger(__name__) class SummaryController(BaseController): - + @LoginRequired() @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', - 'repository.admin') + 'repository.admin') def __before__(self): super(SummaryController, self).__before__() - + def index(self): - hg_model = HgModel() - c.repo_info = hg_model.get_repo(c.repo_name) - c.repo_changesets = Page(list(c.repo_info[:10]), page=1, items_per_page=20) + scm_model = ScmModel() + c.repo_info = scm_model.get_repo(c.repo_name) + c.following = scm_model.is_following_repo(c.repo_name, + c.rhodecode_user.user_id) + def url_generator(**kw): + return url('shortlog_home', repo_name=c.repo_name, **kw) + + c.repo_changesets = Page(c.repo_info, page=1, items_per_page=10, + url=url_generator) + e = request.environ - - uri = u'%(protocol)s://%(user)s@%(host)s%(prefix)s/%(repo_name)s' % { + + if self.rhodecode_user.username == 'default': + password = ':default' + else: + password = '' + + uri = u'%(protocol)s://%(user)s%(password)s@%(host)s%(prefix)s/%(repo_name)s' % { 'protocol': e.get('wsgi.url_scheme'), 'user':str(c.rhodecode_user.username), + 'password':password, 'host':e.get('HTTP_HOST'), 'prefix':e.get('SCRIPT_NAME'), 'repo_name':c.repo_name, } c.clone_repo_url = uri c.repo_tags = OrderedDict() for name, hash in c.repo_info.tags.items()[:10]: - c.repo_tags[name] = c.repo_info.get_changeset(hash) - + try: + c.repo_tags[name] = c.repo_info.get_changeset(hash) + except ChangesetError: + c.repo_tags[name] = EmptyChangeset(hash) + c.repo_branches = OrderedDict() for name, hash in c.repo_info.branches.items()[:10]: - c.repo_branches[name] = c.repo_info.get_changeset(hash) - - td = datetime.today() + timedelta(days=1) + try: + c.repo_branches[name] = c.repo_info.get_changeset(hash) + except ChangesetError: + c.repo_branches[name] = EmptyChangeset(hash) + + td = datetime.today() + timedelta(days=1) y, m, d = td.year, td.month, td.day - + ts_min_y = mktime((y - 1, (td - timedelta(days=calendar.mdays[m])).month, d, 0, 0, 0, 0, 0, 0,)) ts_min_m = mktime((y, (td - timedelta(days=calendar.mdays[m])).month, d, 0, 0, 0, 0, 0, 0,)) - + ts_max_y = mktime((y, m, d, 0, 0, 0, 0, 0, 0,)) - - run_task(get_commits_stats, c.repo_info.name, ts_min_y, ts_max_y) + if c.repo_info.dbrepo.enable_statistics: + c.no_data_msg = _('No data loaded yet') + run_task(get_commits_stats, c.repo_info.name, ts_min_y, ts_max_y) + else: + c.no_data_msg = _('Statistics update are disabled for this repository') c.ts_min = ts_min_m c.ts_max = ts_max_y - + stats = self.sa.query(Statistics)\ .filter(Statistics.repository == c.repo_info.dbrepo)\ .scalar() - - + + if stats and stats.languages: + c.no_data = False is c.repo_info.dbrepo.enable_statistics lang_stats = json.loads(stats.languages) c.commit_data = stats.commit_activity c.overview_data = stats.commit_activity_combined c.trending_languages = json.dumps(OrderedDict( sorted(lang_stats.items(), reverse=True, - key=lambda k: k[1])[:2] + key=lambda k: k[1])[:10] ) ) else: c.commit_data = json.dumps({}) - c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 0] ]) + c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 10] ]) c.trending_languages = json.dumps({}) - + c.no_data = True + return render('summary/summary.html') diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/controllers/tags.py --- a/rhodecode/controllers/tags.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/controllers/tags.py Sat Dec 18 14:45:58 2010 +0100 @@ -26,7 +26,7 @@ from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator from rhodecode.lib.base import BaseController, render from rhodecode.lib.utils import OrderedDict -from rhodecode.model.hg_model import HgModel +from rhodecode.model.scm import ScmModel import logging log = logging.getLogger(__name__) @@ -38,7 +38,7 @@ super(TagsController, self).__before__() def index(self): - hg_model = HgModel() + hg_model = ScmModel() c.repo_info = hg_model.get_repo(c.repo_name) c.repo_tags = OrderedDict() for name, hash_ in c.repo_info.tags.items(): diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/app_globals.py --- a/rhodecode/lib/app_globals.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/lib/app_globals.py Sat Dec 18 14:45:58 2010 +0100 @@ -19,13 +19,13 @@ self.cache = CacheManager(**parse_cache_config_options(config)) self.available_permissions = None # propagated after init_model self.baseui = None # propagated after init_model - + @LazyProperty def paths(self): if self.baseui: return self.baseui.configitems('paths') - + @LazyProperty def base_path(self): if self.baseui: - return self.paths[0][1].replace('*', '') + return self.paths[0][1] diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/auth.py --- a/rhodecode/lib/auth.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/lib/auth.py Sat Dec 18 14:45:58 2010 +0100 @@ -22,22 +22,23 @@ @author: marcink """ -from beaker.cache import cache_region from pylons import config, session, url, request from pylons.controllers.util import abort, redirect +from rhodecode.lib.exceptions import * from rhodecode.lib.utils import get_repo_slug +from rhodecode.lib.auth_ldap import AuthLdap from rhodecode.model import meta +from rhodecode.model.user import UserModel from rhodecode.model.caching_query import FromCache from rhodecode.model.db import User, RepoToPerm, Repository, Permission, \ UserToPerm -from sqlalchemy.exc import OperationalError -from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound import bcrypt from decorator import decorator import logging import random +import traceback -log = logging.getLogger(__name__) +log = logging.getLogger(__name__) class PasswordGenerator(object): """This is a simple class for generating password from @@ -56,7 +57,7 @@ ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM#[6] ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM#[7] - + def __init__(self, passwd=''): self.passwd = passwd @@ -64,40 +65,93 @@ self.passwd = ''.join([random.choice(type) for _ in xrange(len)]) return self.passwd - + def get_crypt_password(password): """Cryptographic function used for password hashing based on sha1 :param password: password to hash - """ + """ return bcrypt.hashpw(password, bcrypt.gensalt(10)) def check_password(password, hashed): return bcrypt.hashpw(password, hashed) == hashed -@cache_region('super_short_term', 'cached_user') -def get_user_cached(username): - sa = meta.Session - try: - user = sa.query(User).filter(User.username == username).one() - finally: - meta.Session.remove() - return user +def authfunc(environ, username, password): + """ + Dummy authentication function used in Mercurial/Git/ and access control, + + :param environ: needed only for using in Basic auth + """ + return authenticate(username, password) + -def authfunc(environ, username, password): - try: - user = get_user_cached(username) - except (NoResultFound, MultipleResultsFound, OperationalError), e: - log.error(e) - user = None - - if user: +def authenticate(username, password): + """ + Authentication function used for access control, + firstly checks for db authentication then if ldap is enabled for ldap + authentication, also creates ldap user if not in database + + :param username: username + :param password: password + """ + user_model = UserModel() + user = user_model.get_by_username(username, cache=False) + + log.debug('Authenticating user using RhodeCode account') + if user is not None and user.is_ldap is False: if user.active: - if user.username == username and check_password(password, user.password): + + if user.username == 'default' and user.active: + log.info('user %s authenticated correctly as anonymous user', + username) + return True + + elif user.username == username and check_password(password, user.password): log.info('user %s authenticated correctly', username) return True else: - log.error('user %s is disabled', username) - + log.warning('user %s is disabled', username) + + else: + log.debug('Regular authentication failed') + user_obj = user_model.get_by_username(username, cache=False, + case_insensitive=True) + + if user_obj is not None and user_obj.is_ldap is False: + log.debug('this user already exists as non ldap') + return False + + from rhodecode.model.settings import SettingsModel + ldap_settings = SettingsModel().get_ldap_settings() + + #====================================================================== + # FALLBACK TO LDAP AUTH IN ENABLE + #====================================================================== + if ldap_settings.get('ldap_active', False): + log.debug("Authenticating user using ldap") + kwargs = { + 'server':ldap_settings.get('ldap_host', ''), + 'base_dn':ldap_settings.get('ldap_base_dn', ''), + 'port':ldap_settings.get('ldap_port'), + 'bind_dn':ldap_settings.get('ldap_dn_user'), + 'bind_pass':ldap_settings.get('ldap_dn_pass'), + 'use_ldaps':ldap_settings.get('ldap_ldaps'), + 'ldap_version':3, + } + log.debug('Checking for ldap authentication') + try: + aldap = AuthLdap(**kwargs) + res = aldap.authenticate_ldap(username, password) + log.debug('Got ldap response %s', res) + + if user_model.create_ldap(username, password): + log.info('created new ldap user') + + return True + except (LdapUsernameError, LdapPasswordError,): + pass + except (Exception,): + log.error(traceback.format_exc()) + pass return False class AuthUser(object): @@ -114,6 +168,8 @@ self.is_admin = False self.permissions = {} + def __repr__(self): + return "" % (self.user_id, self.username) def set_available_permissions(config): """ @@ -125,82 +181,62 @@ """ log.info('getting information about all available permissions') try: - sa = meta.Session + sa = meta.Session() all_perms = sa.query(Permission).all() + except: + pass finally: meta.Session.remove() - + config['available_permissions'] = [x.permission_name for x in all_perms] def set_base_path(config): config['base_path'] = config['pylons.app_globals'].base_path -def fill_data(user): - """ - Fills user data with those from database and log out user if not present - in database - :param user: - """ - sa = meta.Session - dbuser = sa.query(User).options(FromCache('sql_cache_short', - 'getuser_%s' % user.user_id))\ - .get(user.user_id) - if dbuser: - user.username = dbuser.username - user.is_admin = dbuser.admin - user.name = dbuser.name - user.lastname = dbuser.lastname - user.email = dbuser.email - else: - user.is_authenticated = False - meta.Session.remove() - return user - + def fill_perms(user): """ Fills user permission attribute with permissions taken from database :param user: """ - - sa = meta.Session + + sa = meta.Session() user.permissions['repositories'] = {} user.permissions['global'] = set() - + #=========================================================================== # fetch default permissions #=========================================================================== - default_user = sa.query(User)\ - .options(FromCache('sql_cache_short','getuser_%s' % 'default'))\ - .filter(User.username == 'default').scalar() - + default_user = UserModel().get_by_username('default', cache=True) + default_perms = sa.query(RepoToPerm, Repository, Permission)\ .join((Repository, RepoToPerm.repository_id == Repository.repo_id))\ .join((Permission, RepoToPerm.permission_id == Permission.permission_id))\ .filter(RepoToPerm.user == default_user).all() - + if user.is_admin: #======================================================================= # #admin have all default rights set to admin #======================================================================= user.permissions['global'].add('hg.admin') - + for perm in default_perms: p = 'repository.admin' user.permissions['repositories'][perm.RepoToPerm.repository.repo_name] = p - + else: #======================================================================= # set default permissions #======================================================================= - + #default global default_global_perms = sa.query(UserToPerm)\ - .filter(UserToPerm.user == sa.query(User).filter(User.username == - 'default').one()) - + .filter(UserToPerm.user == sa.query(User)\ + .filter(User.username == 'default').one()) + for perm in default_global_perms: user.permissions['global'].add(perm.permission.permission_name) - + #default repositories for perm in default_perms: if perm.Repository.private and not perm.Repository.user_id == user.user_id: @@ -211,9 +247,9 @@ p = 'repository.admin' else: p = perm.Permission.permission_name - + user.permissions['repositories'][perm.RepoToPerm.repository.repo_name] = p - + #======================================================================= # #overwrite default with user permissions if any #======================================================================= @@ -221,38 +257,52 @@ .join((Repository, RepoToPerm.repository_id == Repository.repo_id))\ .join((Permission, RepoToPerm.permission_id == Permission.permission_id))\ .filter(RepoToPerm.user_id == user.user_id).all() - + for perm in user_perms: if perm.Repository.user_id == user.user_id:#set admin if owner p = 'repository.admin' else: p = perm.Permission.permission_name user.permissions['repositories'][perm.RepoToPerm.repository.repo_name] = p - meta.Session.remove() + meta.Session.remove() return user - + def get_user(session): """ Gets user from session, and wraps permissions into user :param session: """ user = session.get('rhodecode_user', AuthUser()) + #if the user is not logged in we check for anonymous access + #if user is logged and it's a default user check if we still have anonymous + #access enabled + if user.user_id is None or user.username == 'default': + anonymous_user = UserModel().get_by_username('default', cache=True) + if anonymous_user.active is True: + #then we set this user is logged in + user.is_authenticated = True + user.user_id = anonymous_user.user_id + else: + user.is_authenticated = False + if user.is_authenticated: - user = fill_data(user) + user = UserModel().fill_data(user) + user = fill_perms(user) session['rhodecode_user'] = user session.save() return user - + #=============================================================================== # CHECK DECORATORS #=============================================================================== class LoginRequired(object): - """Must be logged in to execute this function else redirect to login page""" - + """Must be logged in to execute this function else + redirect to login page""" + def __call__(self, func): return decorator(self.__wrapper, func) - + def __wrapper(self, func, *fargs, **fkwargs): user = session.get('rhodecode_user', AuthUser()) log.debug('Checking login required for user:%s', user.username) @@ -261,21 +311,46 @@ return func(*fargs, **fkwargs) else: log.warn('user %s not authenticated', user.username) - + p = '' if request.environ.get('SCRIPT_NAME') != '/': p += request.environ.get('SCRIPT_NAME') - + p += request.environ.get('PATH_INFO') if request.environ.get('QUERY_STRING'): p += '?' + request.environ.get('QUERY_STRING') - - log.debug('redirecting to login page with %s', p) + + log.debug('redirecting to login page with %s', p) return redirect(url('login_home', came_from=p)) +class NotAnonymous(object): + """Must be logged in to execute this function else + redirect to login page""" + + def __call__(self, func): + return decorator(self.__wrapper, func) + + def __wrapper(self, func, *fargs, **fkwargs): + user = session.get('rhodecode_user', AuthUser()) + log.debug('Checking if user is not anonymous') + + anonymous = user.username == 'default' + + if anonymous: + p = '' + if request.environ.get('SCRIPT_NAME') != '/': + p += request.environ.get('SCRIPT_NAME') + + p += request.environ.get('PATH_INFO') + if request.environ.get('QUERY_STRING'): + p += '?' + request.environ.get('QUERY_STRING') + return redirect(url('login_home', came_from=p)) + else: + return func(*fargs, **fkwargs) + class PermsDecorator(object): """Base class for decorators""" - + def __init__(self, *required_perms): available_perms = config['available_permissions'] for perm in required_perms: @@ -283,32 +358,33 @@ raise Exception("'%s' permission is not defined" % perm) self.required_perms = set(required_perms) self.user_perms = None - + def __call__(self, func): return decorator(self.__wrapper, func) - - + + def __wrapper(self, func, *fargs, **fkwargs): # _wrapper.__name__ = func.__name__ # _wrapper.__dict__.update(func.__dict__) # _wrapper.__doc__ = func.__doc__ + self.user = session.get('rhodecode_user', AuthUser()) + self.user_perms = self.user.permissions + log.debug('checking %s permissions %s for %s %s', + self.__class__.__name__, self.required_perms, func.__name__, + self.user) - self.user_perms = session.get('rhodecode_user', AuthUser()).permissions - log.debug('checking %s permissions %s for %s', - self.__class__.__name__, self.required_perms, func.__name__) - if self.check_permissions(): - log.debug('Permission granted for %s', func.__name__) - + log.debug('Permission granted for %s %s', func.__name__, self.user) + return func(*fargs, **fkwargs) - + else: - log.warning('Permission denied for %s', func.__name__) + log.warning('Permission denied for %s %s', func.__name__, self.user) #redirect with forbidden ret code return abort(403) - - + + def check_permissions(self): """Dummy function for overriding""" raise Exception('You have to write this function in child class') @@ -317,18 +393,18 @@ """Checks for access permission for all given predicates. All of them have to be meet in order to fulfill the request """ - + def check_permissions(self): if self.required_perms.issubset(self.user_perms.get('global')): return True return False - + class HasPermissionAnyDecorator(PermsDecorator): """Checks for access permission for any of given predicates. In order to fulfill the request any of predicates must be meet """ - + def check_permissions(self): if self.required_perms.intersection(self.user_perms.get('global')): return True @@ -338,7 +414,7 @@ """Checks for access permission for all given predicates for specific repository. All of them have to be meet in order to fulfill the request """ - + def check_permissions(self): repo_name = get_repo_slug(request) try: @@ -348,16 +424,16 @@ if self.required_perms.issubset(user_perms): return True return False - + class HasRepoPermissionAnyDecorator(PermsDecorator): """Checks for access permission for any of given predicates for specific repository. In order to fulfill the request any of predicates must be meet """ - + def check_permissions(self): repo_name = get_repo_slug(request) - + try: user_perms = set([self.user_perms['repositories'][repo_name]]) except KeyError: @@ -371,10 +447,10 @@ class PermsFunction(object): """Base function for other check functions""" - + def __init__(self, *perms): available_perms = config['available_permissions'] - + for perm in perms: if perm not in available_perms: raise Exception("'%s' permission in not defined" % perm) @@ -382,29 +458,30 @@ self.user_perms = None self.granted_for = '' self.repo_name = None - + def __call__(self, check_Location=''): user = session.get('rhodecode_user', False) if not user: return False self.user_perms = user.permissions - self.granted_for = user.username - log.debug('checking %s %s', self.__class__.__name__, self.required_perms) - + self.granted_for = user.username + log.debug('checking %s %s %s', self.__class__.__name__, + self.required_perms, user) + if self.check_permissions(): - log.debug('Permission granted for %s @%s', self.granted_for, - check_Location) + log.debug('Permission granted for %s @ %s %s', self.granted_for, + check_Location, user) return True - + else: - log.warning('Permission denied for %s @%s', self.granted_for, - check_Location) - return False - + log.warning('Permission denied for %s @ %s %s', self.granted_for, + check_Location, user) + return False + def check_permissions(self): """Dummy function for overriding""" raise Exception('You have to write this function in child class') - + class HasPermissionAll(PermsFunction): def check_permissions(self): if self.required_perms.issubset(self.user_perms.get('global')): @@ -418,11 +495,11 @@ return False class HasRepoPermissionAll(PermsFunction): - + def __call__(self, repo_name=None, check_Location=''): self.repo_name = repo_name return super(HasRepoPermissionAll, self).__call__(check_Location) - + def check_permissions(self): if not self.repo_name: self.repo_name = get_repo_slug(request) @@ -432,17 +509,17 @@ [self.repo_name]]) except KeyError: return False - self.granted_for = self.repo_name + self.granted_for = self.repo_name if self.required_perms.issubset(self.user_perms): return True return False - + class HasRepoPermissionAny(PermsFunction): - + def __call__(self, repo_name=None, check_Location=''): self.repo_name = repo_name return super(HasRepoPermissionAny, self).__call__(check_Location) - + def check_permissions(self): if not self.repo_name: self.repo_name = get_repo_slug(request) @@ -464,13 +541,13 @@ class HasPermissionAnyMiddleware(object): def __init__(self, *perms): self.required_perms = set(perms) - + def __call__(self, user, repo_name): usr = AuthUser() usr.user_id = user.user_id usr.username = user.username usr.is_admin = user.admin - + try: self.user_perms = set([fill_perms(usr)\ .permissions['repositories'][repo_name]]) @@ -478,9 +555,9 @@ self.user_perms = set() self.granted_for = '' self.username = user.username - self.repo_name = repo_name + self.repo_name = repo_name return self.check_permissions() - + def check_permissions(self): log.debug('checking mercurial protocol ' 'permissions for user:%s repository:%s', diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/auth_ldap.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/auth_ldap.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ldap authentication lib +# Copyright (C) 2009-2010 Marcin Kuzminski +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +""" +Created on Nov 17, 2010 + +@author: marcink +""" + +from rhodecode.lib.exceptions import * +import logging + +log = logging.getLogger(__name__) + +try: + import ldap +except ImportError: + pass + +class AuthLdap(object): + + def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='', + use_ldaps=False, ldap_version=3): + self.ldap_version = ldap_version + if use_ldaps: + port = port or 689 + self.LDAP_USE_LDAPS = use_ldaps + self.LDAP_SERVER_ADDRESS = server + self.LDAP_SERVER_PORT = port + + #USE FOR READ ONLY BIND TO LDAP SERVER + self.LDAP_BIND_DN = bind_dn + self.LDAP_BIND_PASS = bind_pass + + ldap_server_type = 'ldap' + if self.LDAP_USE_LDAPS:ldap_server_type = ldap_server_type + 's' + self.LDAP_SERVER = "%s://%s:%s" % (ldap_server_type, + self.LDAP_SERVER_ADDRESS, + self.LDAP_SERVER_PORT) + + self.BASE_DN = base_dn + + def authenticate_ldap(self, username, password): + """Authenticate a user via LDAP and return his/her LDAP properties. + + Raises AuthenticationError if the credentials are rejected, or + EnvironmentError if the LDAP server can't be reached. + + :param username: username + :param password: password + """ + + from rhodecode.lib.helpers import chop_at + + uid = chop_at(username, "@%s" % self.LDAP_SERVER_ADDRESS) + + if "," in username: + raise LdapUsernameError("invalid character in username: ,") + try: + ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, '/etc/openldap/cacerts') + ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10) + server = ldap.initialize(self.LDAP_SERVER) + if self.ldap_version == 2: + server.protocol = ldap.VERSION2 + else: + server.protocol = ldap.VERSION3 + + if self.LDAP_BIND_DN and self.LDAP_BIND_PASS: + server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS) + + dn = self.BASE_DN % {'user':uid} + log.debug("Authenticating %r at %s", dn, self.LDAP_SERVER) + server.simple_bind_s(dn, password) + + properties = server.search_s(dn, ldap.SCOPE_SUBTREE) + if not properties: + raise ldap.NO_SUCH_OBJECT() + except ldap.NO_SUCH_OBJECT, e: + log.debug("LDAP says no such user '%s' (%s)", uid, username) + raise LdapUsernameError() + except ldap.INVALID_CREDENTIALS, e: + log.debug("LDAP rejected password for user '%s' (%s)", uid, username) + raise LdapPasswordError() + except ldap.SERVER_DOWN, e: + raise LdapConnectionError("LDAP can't access authentication server") + + return properties[0] + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/base.py --- a/rhodecode/lib/base.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/lib/base.py Sat Dec 18 14:45:58 2010 +0100 @@ -9,30 +9,36 @@ from rhodecode.lib import auth from rhodecode.lib.utils import get_repo_slug from rhodecode.model import meta -from rhodecode.model.hg_model import _get_repos_cached, \ - _get_repos_switcher_cached +from rhodecode.model.scm import ScmModel +from rhodecode import BACKENDS class BaseController(WSGIController): - + def __before__(self): c.rhodecode_version = __version__ c.rhodecode_name = config['rhodecode_title'] c.repo_name = get_repo_slug(request) - c.cached_repo_list = _get_repos_cached() - c.repo_switcher_list = _get_repos_switcher_cached(c.cached_repo_list) - + c.cached_repo_list = ScmModel().get_repos() + c.backends = BACKENDS.keys() + self.cut_off_limit = int(config['cut_off_limit']) + self.sa = meta.Session() + scm_model = ScmModel(self.sa) + #c.unread_journal = scm_model.get_unread_journal() + if c.repo_name: - cached_repo = c.cached_repo_list.get(c.repo_name) - + cached_repo = scm_model.get(c.repo_name) if cached_repo: c.repository_tags = cached_repo.tags c.repository_branches = cached_repo.branches + c.repository_followers = scm_model.get_followers(cached_repo.dbrepo.repo_id) + c.repository_forks = scm_model.get_forks(cached_repo.dbrepo.repo_id) else: c.repository_tags = {} c.repository_branches = {} - - self.sa = meta.Session - + c.repository_followers = 0 + c.repository_forks = 0 + + def __call__(self, environ, start_response): """Invoke the Controller""" # WSGIController.__call__ dispatches to the Controller method diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/celerylib/__init__.py --- a/rhodecode/lib/celerylib/__init__.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/lib/celerylib/__init__.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,14 +1,54 @@ -from rhodecode.lib.pidlock import DaemonLock, LockHeld -from vcs.utils.lazy import LazyProperty -from decorator import decorator -import logging +# -*- coding: utf-8 -*- +""" + package.rhodecode.lib.celerylib.__init__ + ~~~~~~~~~~~~~~ + + celery libs for RhodeCode + + :created_on: Nov 27, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + import os import sys +import socket import traceback +import logging + from hashlib import md5 -import socket +from decorator import decorator +from vcs.utils.lazy import LazyProperty + +from rhodecode.lib.pidlock import DaemonLock, LockHeld + +from pylons import config + log = logging.getLogger(__name__) +def str2bool(v): + return v.lower() in ["yes", "true", "t", "1"] if v else None + +try: + CELERY_ON = str2bool(config['app_conf'].get('use_celery')) +except KeyError: + CELERY_ON = False + class ResultWrapper(object): def __init__(self, task): self.task = task @@ -18,27 +58,22 @@ return self.task def run_task(task, *args, **kwargs): - try: - t = task.delay(*args, **kwargs) - log.info('running task %s', t.task_id) - return t - except socket.error, e: - + if CELERY_ON: try: - conn_failed = e.errno == 111 - except AttributeError: - conn_failed = False + t = task.delay(*args, **kwargs) + log.info('running task %s:%s', t.task_id, task) + return t + except socket.error, e: + if e.errno == 111: + log.debug('Unable to connect to celeryd. Sync execution') + else: + log.error(traceback.format_exc()) + except KeyError, e: + log.debug('Unable to connect to celeryd. Sync execution') + except Exception, e: + log.error(traceback.format_exc()) - if conn_failed: - log.debug('Unable to connect to celeryd. Sync execution') - else: - log.debug('Unable to connect to celeryd. Sync execution') - - except KeyError, e: - log.debug('Unable to connect to celeryd. Sync execution') - except Exception, e: - log.error(traceback.format_exc()) - + log.debug('executing task %s in sync mode', task) return ResultWrapper(task(*args, **kwargs)) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/celerylib/tasks.py --- a/rhodecode/lib/celerylib/tasks.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/lib/celerylib/tasks.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,14 +1,26 @@ from celery.decorators import task +import os +import traceback +from time import mktime from operator import itemgetter + +from pylons import config from pylons.i18n.translation import _ -from rhodecode.lib.celerylib import run_task, locked_task + +from rhodecode.lib.celerylib import run_task, locked_task, str2bool from rhodecode.lib.helpers import person from rhodecode.lib.smtp_mailer import SmtpMailer -from rhodecode.lib.utils import OrderedDict -from time import mktime -from vcs.backends.hg import MercurialRepository -import traceback +from rhodecode.lib.utils import OrderedDict, add_cache +from rhodecode.model import init_model +from rhodecode.model import meta +from rhodecode.model.db import RhodeCodeUi + +from vcs.backends import get_repo + +from sqlalchemy import engine_from_config + +add_cache(config) try: import json @@ -16,96 +28,56 @@ #python 2.5 compatibility import simplejson as json -try: - from celeryconfig import PYLONS_CONFIG as config - celery_on = True -except ImportError: - #if celeryconfig is not present let's just load our pylons - #config instead - from pylons import config - celery_on = False - - __all__ = ['whoosh_index', 'get_commits_stats', 'reset_user_password', 'send_email'] +CELERY_ON = str2bool(config['app_conf'].get('use_celery')) + def get_session(): - if celery_on: - from sqlalchemy import engine_from_config - from sqlalchemy.orm import sessionmaker, scoped_session - engine = engine_from_config(dict(config.items('app:main')), 'sqlalchemy.db1.') - sa = scoped_session(sessionmaker(bind=engine)) - else: - #If we don't use celery reuse our current application Session - from rhodecode.model.meta import Session - sa = Session - + if CELERY_ON: + engine = engine_from_config(config, 'sqlalchemy.db1.') + init_model(engine) + sa = meta.Session() return sa -def get_hg_settings(): - from rhodecode.model.db import RhodeCodeSettings - sa = get_session() - ret = sa.query(RhodeCodeSettings).all() - - if not ret: - raise Exception('Could not get application settings !') - settings = {} - for each in ret: - settings['rhodecode_' + each.app_settings_name] = each.app_settings_value - - return settings - -def get_hg_ui_settings(): - from rhodecode.model.db import RhodeCodeUi +def get_repos_path(): sa = get_session() - ret = sa.query(RhodeCodeUi).all() - - if not ret: - raise Exception('Could not get application ui settings !') - settings = {} - for each in ret: - k = each.ui_key - v = each.ui_value - if k == '/': - k = 'root_path' - - if k.find('.') != -1: - k = k.replace('.', '_') - - if each.ui_section == 'hooks': - v = each.ui_active - - settings[each.ui_section + '_' + k] = v - - return settings + q = sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one() + return q.ui_value @task @locked_task def whoosh_index(repo_location, full_index): log = whoosh_index.get_logger() from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon - WhooshIndexingDaemon(repo_location=repo_location).run(full_index=full_index) + index_location = config['index_dir'] + WhooshIndexingDaemon(index_location=index_location, + repo_location=repo_location, sa=get_session())\ + .run(full_index=full_index) @task @locked_task def get_commits_stats(repo_name, ts_min_y, ts_max_y): from rhodecode.model.db import Statistics, Repository log = get_commits_stats.get_logger() - author_key_cleaner = lambda k: person(k).replace('"', "") #for js data compatibilty - + + #for js data compatibilty + author_key_cleaner = lambda k: person(k).replace('"', "") + commits_by_day_author_aggregate = {} commits_by_day_aggregate = {} - repos_path = get_hg_ui_settings()['paths_root_path'].replace('*', '') - repo = MercurialRepository(repos_path + repo_name) + repos_path = get_repos_path() + p = os.path.join(repos_path, repo_name) + repo = get_repo(p) skip_date_limit = True - parse_limit = 350 #limit for single task changeset parsing optimal for + parse_limit = 250 #limit for single task changeset parsing optimal for last_rev = 0 last_cs = None timegetter = itemgetter('time') - + sa = get_session() - + dbrepo = sa.query(Repository)\ .filter(Repository.repo_name == repo_name).scalar() cur_stats = sa.query(Statistics)\ @@ -114,26 +86,27 @@ last_rev = cur_stats.stat_on_revision if not repo.revisions: return True - + if last_rev == repo.revisions[-1] and len(repo.revisions) > 1: - #pass silently without any work if we're not on first revision or current - #state of parsing revision(from db marker) is the last revision + #pass silently without any work if we're not on first revision or + #current state of parsing revision(from db marker) is the last revision return True - + if cur_stats: commits_by_day_aggregate = OrderedDict( json.loads( cur_stats.commit_activity_combined)) commits_by_day_author_aggregate = json.loads(cur_stats.commit_activity) - + log.debug('starting parsing %s', parse_limit) - for cnt, rev in enumerate(repo.revisions[last_rev:]): + lmktime = mktime + + last_rev = last_rev + 1 if last_rev > 0 else last_rev + for rev in repo.revisions[last_rev:last_rev + parse_limit]: last_cs = cs = repo.get_changeset(rev) - k = '%s-%s-%s' % (cs.date.timetuple()[0], cs.date.timetuple()[1], - cs.date.timetuple()[2]) - timetupple = [int(x) for x in k.split('-')] - timetupple.extend([0 for _ in xrange(6)]) - k = mktime(timetupple) + k = lmktime([cs.date.timetuple()[0], cs.date.timetuple()[1], + cs.date.timetuple()[2], 0, 0, 0, 0, 0, 0]) + if commits_by_day_author_aggregate.has_key(author_key_cleaner(cs.author)): try: l = [timegetter(x) for x in commits_by_day_author_aggregate\ @@ -141,20 +114,20 @@ time_pos = l.index(k) except ValueError: time_pos = False - + if time_pos >= 0 and time_pos is not False: - + datadict = commits_by_day_author_aggregate\ [author_key_cleaner(cs.author)]['data'][time_pos] - + datadict["commits"] += 1 datadict["added"] += len(cs.added) datadict["changed"] += len(cs.changed) datadict["removed"] += len(cs.removed) - + else: if k >= ts_min_y and k <= ts_max_y or skip_date_limit: - + datadict = {"time":k, "commits":1, "added":len(cs.added), @@ -163,7 +136,7 @@ } commits_by_day_author_aggregate\ [author_key_cleaner(cs.author)]['data'].append(datadict) - + else: if k >= ts_min_y and k <= ts_max_y or skip_date_limit: commits_by_day_author_aggregate[author_key_cleaner(cs.author)] = { @@ -175,23 +148,15 @@ "removed":len(cs.removed), }], "schema":["commits"], - } - + } + #gather all data by day if commits_by_day_aggregate.has_key(k): commits_by_day_aggregate[k] += 1 else: commits_by_day_aggregate[k] = 1 - - if cnt >= parse_limit: - #don't fetch to much data since we can freeze application - break - overview_data = [] - for k, v in commits_by_day_aggregate.items(): - overview_data.append([k, v]) - overview_data = sorted(overview_data, key=itemgetter(0)) - + overview_data = sorted(commits_by_day_aggregate.items(), key=itemgetter(0)) if not commits_by_day_author_aggregate: commits_by_day_author_aggregate[author_key_cleaner(repo.contact)] = { "label":author_key_cleaner(repo.contact), @@ -206,23 +171,24 @@ log.debug('last revison %s', last_rev) leftovers = len(repo.revisions[last_rev:]) log.debug('revisions to parse %s', leftovers) - - if last_rev == 0 or leftovers < parse_limit: + + if last_rev == 0 or leftovers < parse_limit: + log.debug('getting code trending stats') stats.languages = json.dumps(__get_codes_stats(repo_name)) - + stats.repository = dbrepo stats.stat_on_revision = last_cs.revision - + try: sa.add(stats) - sa.commit() + sa.commit() except: log.error(traceback.format_exc()) sa.rollback() return False if len(repo.revisions) > 1: run_task(get_commits_stats, repo_name, ts_min_y, ts_max_y) - + return True @task @@ -230,7 +196,7 @@ log = reset_user_password.get_logger() from rhodecode.lib import auth from rhodecode.model.db import User - + try: try: sa = get_session() @@ -244,38 +210,52 @@ log.info('change password for %s', user_email) if new_passwd is None: raise Exception('unable to generate new password') - + except: log.error(traceback.format_exc()) sa.rollback() - + run_task(send_email, user_email, "Your new rhodecode password", 'Your new rhodecode password:%s' % (new_passwd)) log.info('send new password mail to %s', user_email) - - + + except: log.error('Failed to update user password') log.error(traceback.format_exc()) + return True -@task +@task def send_email(recipients, subject, body): + """ + Sends an email with defined parameters from the .ini files. + + + :param recipients: list of recipients, it this is empty the defined email + address from field 'email_to' is used instead + :param subject: subject of the mail + :param body: body of the mail + """ log = send_email.get_logger() - email_config = dict(config.items('DEFAULT')) + email_config = config + + if not recipients: + recipients = [email_config.get('email_to')] + mail_from = email_config.get('app_email_from') user = email_config.get('smtp_username') passwd = email_config.get('smtp_password') mail_server = email_config.get('smtp_server') mail_port = email_config.get('smtp_port') - tls = email_config.get('smtp_use_tls') - ssl = False - + tls = str2bool(email_config.get('smtp_use_tls')) + ssl = str2bool(email_config.get('smtp_use_ssl')) + try: m = SmtpMailer(mail_from, user, passwd, mail_server, mail_port, ssl, tls) - m.send(recipients, subject, body) + m.send(recipients, subject, body) except: log.error('Mail sending failed') log.error(traceback.format_exc()) @@ -284,45 +264,96 @@ @task def create_repo_fork(form_data, cur_user): - import os - from rhodecode.model.repo_model import RepoModel - sa = get_session() - rm = RepoModel(sa) - - rm.create(form_data, cur_user, just_db=True, fork=True) - - repos_path = get_hg_ui_settings()['paths_root_path'].replace('*', '') - repo_path = os.path.join(repos_path, form_data['repo_name']) + from rhodecode.model.repo import RepoModel + from vcs import get_backend + log = create_repo_fork.get_logger() + repo_model = RepoModel(get_session()) + repo_model.create(form_data, cur_user, just_db=True, fork=True) + repo_name = form_data['repo_name'] + repos_path = get_repos_path() + repo_path = os.path.join(repos_path, repo_name) repo_fork_path = os.path.join(repos_path, form_data['fork_name']) - - MercurialRepository(str(repo_fork_path), True, clone_url=str(repo_path)) + alias = form_data['repo_type'] - + log.info('creating repo fork %s as %s', repo_name, repo_path) + backend = get_backend(alias) + backend(str(repo_fork_path), create=True, src_url=str(repo_path)) + def __get_codes_stats(repo_name): - LANGUAGES_EXTENSIONS = ['action', 'adp', 'ashx', 'asmx', 'aspx', 'asx', 'axd', 'c', - 'cfg', 'cfm', 'cpp', 'cs', 'diff', 'do', 'el', 'erl', - 'h', 'java', 'js', 'jsp', 'jspx', 'lisp', - 'lua', 'm', 'mako', 'ml', 'pas', 'patch', 'php', 'php3', - 'php4', 'phtml', 'pm', 'py', 'rb', 'rst', 's', 'sh', - 'tpl', 'txt', 'vim', 'wss', 'xhtml', 'xml', 'xsl', 'xslt', - 'yaws'] - repos_path = get_hg_ui_settings()['paths_root_path'].replace('*', '') - repo = MercurialRepository(repos_path + repo_name) + LANGUAGES_EXTENSIONS_MAP = {'scm': 'Scheme', 'asmx': 'VbNetAspx', 'Rout': + 'RConsole', 'rest': 'Rst', 'abap': 'ABAP', 'go': 'Go', 'phtml': 'HtmlPhp', + 'ns2': 'Newspeak', 'xml': 'EvoqueXml', 'sh-session': 'BashSession', 'ads': + 'Ada', 'clj': 'Clojure', 'll': 'Llvm', 'ebuild': 'Bash', 'adb': 'Ada', + 'ada': 'Ada', 'c++-objdump': 'CppObjdump', 'aspx': + 'VbNetAspx', 'ksh': 'Bash', 'coffee': 'CoffeeScript', 'vert': 'GLShader', + 'Makefile.*': 'Makefile', 'di': 'D', 'dpatch': 'DarcsPatch', 'rake': + 'Ruby', 'moo': 'MOOCode', 'erl-sh': 'ErlangShell', 'geo': 'GLShader', + 'pov': 'Povray', 'bas': 'VbNet', 'bat': 'Batch', 'd': 'D', 'lisp': + 'CommonLisp', 'h': 'C', 'rbx': 'Ruby', 'tcl': 'Tcl', 'c++': 'Cpp', 'md': + 'MiniD', '.vimrc': 'Vim', 'xsd': 'Xml', 'ml': 'Ocaml', 'el': 'CommonLisp', + 'befunge': 'Befunge', 'xsl': 'Xslt', 'pyx': 'Cython', 'cfm': + 'ColdfusionHtml', 'evoque': 'Evoque', 'cfg': 'Ini', 'htm': 'Html', + 'Makefile': 'Makefile', 'cfc': 'ColdfusionHtml', 'tex': 'Tex', 'cs': + 'CSharp', 'mxml': 'Mxml', 'patch': 'Diff', 'apache.conf': 'ApacheConf', + 'scala': 'Scala', 'applescript': 'AppleScript', 'GNUmakefile': 'Makefile', + 'c-objdump': 'CObjdump', 'lua': 'Lua', 'apache2.conf': 'ApacheConf', 'rb': + 'Ruby', 'gemspec': 'Ruby', 'rl': 'RagelObjectiveC', 'vala': 'Vala', 'tmpl': + 'Cheetah', 'bf': 'Brainfuck', 'plt': 'Gnuplot', 'G': 'AntlrRuby', 'xslt': + 'Xslt', 'flxh': 'Felix', 'asax': 'VbNetAspx', 'Rakefile': 'Ruby', 'S': 'S', + 'wsdl': 'Xml', 'js': 'Javascript', 'autodelegate': 'Myghty', 'properties': + 'Ini', 'bash': 'Bash', 'c': 'C', 'g': 'AntlrRuby', 'r3': 'Rebol', 's': + 'Gas', 'ashx': 'VbNetAspx', 'cxx': 'Cpp', 'boo': 'Boo', 'prolog': 'Prolog', + 'sqlite3-console': 'SqliteConsole', 'cl': 'CommonLisp', 'cc': 'Cpp', 'pot': + 'Gettext', 'vim': 'Vim', 'pxi': 'Cython', 'yaml': 'Yaml', 'SConstruct': + 'Python', 'diff': 'Diff', 'txt': 'Text', 'cw': 'Redcode', 'pxd': 'Cython', + 'plot': 'Gnuplot', 'java': 'Java', 'hrl': 'Erlang', 'py': 'Python', + 'makefile': 'Makefile', 'squid.conf': 'SquidConf', 'asm': 'Nasm', 'toc': + 'Tex', 'kid': 'Genshi', 'rhtml': 'Rhtml', 'po': 'Gettext', 'pl': 'Prolog', + 'pm': 'Perl', 'hx': 'Haxe', 'ascx': 'VbNetAspx', 'ooc': 'Ooc', 'asy': + 'Asymptote', 'hs': 'Haskell', 'SConscript': 'Python', 'pytb': + 'PythonTraceback', 'myt': 'Myghty', 'hh': 'Cpp', 'R': 'S', 'aux': 'Tex', + 'rst': 'Rst', 'cpp-objdump': 'CppObjdump', 'lgt': 'Logtalk', 'rss': 'Xml', + 'flx': 'Felix', 'b': 'Brainfuck', 'f': 'Fortran', 'rbw': 'Ruby', + '.htaccess': 'ApacheConf', 'cxx-objdump': 'CppObjdump', 'j': 'ObjectiveJ', + 'mll': 'Ocaml', 'yml': 'Yaml', 'mu': 'MuPAD', 'r': 'Rebol', 'ASM': 'Nasm', + 'erl': 'Erlang', 'mly': 'Ocaml', 'mo': 'Modelica', 'def': 'Modula2', 'ini': + 'Ini', 'control': 'DebianControl', 'vb': 'VbNet', 'vapi': 'Vala', 'pro': + 'Prolog', 'spt': 'Cheetah', 'mli': 'Ocaml', 'as': 'ActionScript3', 'cmd': + 'Batch', 'cpp': 'Cpp', 'io': 'Io', 'tac': 'Python', 'haml': 'Haml', 'rkt': + 'Racket', 'st':'Smalltalk', 'inc': 'Povray', 'pas': 'Delphi', 'cmake': + 'CMake', 'csh':'Tcsh', 'hpp': 'Cpp', 'feature': 'Gherkin', 'html': 'Html', + 'php':'Php', 'php3':'Php', 'php4':'Php', 'php5':'Php', 'xhtml': 'Html', + 'hxx': 'Cpp', 'eclass': 'Bash', 'css': 'Css', + 'frag': 'GLShader', 'd-objdump': 'DObjdump', 'weechatlog': 'IrcLogs', + 'tcsh': 'Tcsh', 'objdump': 'Objdump', 'pyw': 'Python', 'h++': 'Cpp', + 'py3tb': 'Python3Traceback', 'jsp': 'Jsp', 'sql': 'Sql', 'mak': 'Makefile', + 'php': 'Php', 'mao': 'Mako', 'man': 'Groff', 'dylan': 'Dylan', 'sass': + 'Sass', 'cfml': 'ColdfusionHtml', 'darcspatch': 'DarcsPatch', 'tpl': + 'Smarty', 'm': 'ObjectiveC', 'f90': 'Fortran', 'mod': 'Modula2', 'sh': + 'Bash', 'lhs': 'LiterateHaskell', 'sources.list': 'SourcesList', 'axd': + 'VbNetAspx', 'sc': 'Python'} + + repos_path = get_repos_path() + p = os.path.join(repos_path, repo_name) + repo = get_repo(p) tip = repo.get_changeset() - code_stats = {} - for topnode, dirs, files in tip.walk('/'): - for f in files: - k = f.mimetype - if f.extension in LANGUAGES_EXTENSIONS: - if code_stats.has_key(k): - code_stats[k] += 1 + + def aggregate(cs): + for f in cs[2]: + ext = f.extension + key = LANGUAGES_EXTENSIONS_MAP.get(ext, ext) + key = key or ext + if ext in LANGUAGES_EXTENSIONS_MAP.keys() and not f.is_binary: + if code_stats.has_key(key): + code_stats[key] += 1 else: - code_stats[k] = 1 - + code_stats[key] = 1 + + map(aggregate, tip.walk('/')) + return code_stats or {} - diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/celerypylons/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/celerypylons/__init__.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,16 @@ +""" +Automatically sets the environment variable `CELERY_LOADER` to +`celerypylons.loader:PylonsLoader`. This ensures the loader is +specified when accessing the rest of this package, and allows celery +to be installed in a webapp just by importing celerypylons:: + + import celerypylons + +""" +import os +import warnings + +CELERYPYLONS_LOADER = 'rhodecode.lib.celerypylons.loader.PylonsLoader' +if os.environ.get('CELERY_LOADER', CELERYPYLONS_LOADER) != CELERYPYLONS_LOADER: + warnings.warn("'CELERY_LOADER' environment variable will be overridden by celery-pylons.") +os.environ['CELERY_LOADER'] = CELERYPYLONS_LOADER diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/celerypylons/commands.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/celerypylons/commands.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,90 @@ +from rhodecode.lib.utils import BasePasterCommand, Command + + +__all__ = ['CeleryDaemonCommand', 'CeleryBeatCommand', + 'CAMQPAdminCommand', 'CeleryEventCommand'] + + +class CeleryDaemonCommand(BasePasterCommand): + """Start the celery worker + + Starts the celery worker that uses a paste.deploy configuration + file. + """ + usage = 'CONFIG_FILE [celeryd options...]' + summary = __doc__.splitlines()[0] + description = "".join(__doc__.splitlines()[2:]) + + parser = Command.standard_parser(quiet=True) + + def update_parser(self): + from celery.bin import celeryd + for x in celeryd.WorkerCommand().get_options(): + self.parser.add_option(x) + + def command(self): + from celery.bin import celeryd + return celeryd.WorkerCommand().run(**vars(self.options)) + + +class CeleryBeatCommand(BasePasterCommand): + """Start the celery beat server + + Starts the celery beat server using a paste.deploy configuration + file. + """ + usage = 'CONFIG_FILE [celerybeat options...]' + summary = __doc__.splitlines()[0] + description = "".join(__doc__.splitlines()[2:]) + + parser = Command.standard_parser(quiet=True) + + def update_parser(self): + from celery.bin import celerybeat + for x in celerybeat.BeatCommand().get_options(): + self.parser.add_option(x) + + def command(self): + from celery.bin import celerybeat + return celerybeat.BeatCommand(**vars(self.options)) + +class CAMQPAdminCommand(BasePasterCommand): + """CAMQP Admin + + CAMQP celery admin tool. + """ + usage = 'CONFIG_FILE [camqadm options...]' + summary = __doc__.splitlines()[0] + description = "".join(__doc__.splitlines()[2:]) + + parser = Command.standard_parser(quiet=True) + + def update_parser(self): + from celery.bin import camqadm + for x in camqadm.OPTION_LIST: + self.parser.add_option(x) + + def command(self): + from celery.bin import camqadm + return camqadm.camqadm(*self.args, **vars(self.options)) + + +class CeleryEventCommand(BasePasterCommand): + """Celery event commandd. + + Capture celery events. + """ + usage = 'CONFIG_FILE [celeryev options...]' + summary = __doc__.splitlines()[0] + description = "".join(__doc__.splitlines()[2:]) + + parser = Command.standard_parser(quiet=True) + + def update_parser(self): + from celery.bin import celeryev + for x in celeryev.OPTION_LIST: + self.parser.add_option(x) + + def command(self): + from celery.bin import celeryev + return celeryev.run_celeryev(**vars(self.options)) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/celerypylons/loader.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/celerypylons/loader.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,55 @@ +from celery.loaders.base import BaseLoader +from pylons import config + +to_pylons = lambda x: x.replace('_', '.').lower() +to_celery = lambda x: x.replace('.', '_').upper() + +LIST_PARAMS = """CELERY_IMPORTS ADMINS ROUTES""".split() + + +class PylonsSettingsProxy(object): + """Pylons Settings Proxy + + Proxies settings from pylons.config + + """ + def __getattr__(self, key): + pylons_key = to_pylons(key) + try: + value = config[pylons_key] + if key in LIST_PARAMS: return value.split() + return self.type_converter(value) + except KeyError: + raise AttributeError(pylons_key) + + def __setattr__(self, key, value): + pylons_key = to_pylons(key) + config[pylons_key] = value + + + def type_converter(self, value): + #cast to int + if value.isdigit(): + return int(value) + + #cast to bool + if value.lower() in ['true', 'false']: + return value.lower() == 'true' + + return value + +class PylonsLoader(BaseLoader): + """Pylons celery loader + + Maps the celery config onto pylons.config + + """ + def read_configuration(self): + self.configured = True + return PylonsSettingsProxy() + + def on_worker_init(self): + """ + Import task modules. + """ + self.import_default_modules() diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/db_manage.py --- a/rhodecode/lib/db_manage.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/lib/db_manage.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,8 +1,16 @@ -#!/usr/bin/env python -# encoding: utf-8 -# database management for RhodeCode -# Copyright (C) 2009-2010 Marcin Kuzminski -# +# -*- coding: utf-8 -*- +""" + rhodecode.lib.db_manage + ~~~~~~~~~~~~~~~~~~~~~~~ + + Database creation, and setup module for RhodeCode. Used for creation + of database as well as for migration operations + + :created_on: Apr 10, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; version 2 @@ -18,51 +26,50 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. -""" -Created on April 10, 2010 -database management and creation for RhodeCode -@author: marcink -""" - -from os.path import dirname as dn, join as jn import os import sys import uuid +import logging +from os.path import dirname as dn, join as jn + +from rhodecode import __dbversion__ +from rhodecode.model import meta from rhodecode.lib.auth import get_crypt_password from rhodecode.lib.utils import ask_ok from rhodecode.model import init_model from rhodecode.model.db import User, Permission, RhodeCodeUi, RhodeCodeSettings, \ - UserToPerm -from rhodecode.model import meta + UserToPerm, DbMigrateVersion + from sqlalchemy.engine import create_engine -import logging log = logging.getLogger(__name__) class DbManage(object): - def __init__(self, log_sql, dbname, root, tests=False): - self.dbname = dbname + def __init__(self, log_sql, dbconf, root, tests=False): + self.dbname = dbconf.split('/')[-1] self.tests = tests self.root = root - dburi = 'sqlite:////%s' % jn(self.root, self.dbname) - engine = create_engine(dburi, echo=log_sql) + self.dburi = dbconf + engine = create_engine(self.dburi, echo=log_sql) init_model(engine) - self.sa = meta.Session + self.sa = meta.Session() self.db_exists = False - + def check_for_db(self, override): db_path = jn(self.root, self.dbname) - log.info('checking for existing db in %s', db_path) - if os.path.isfile(db_path): - self.db_exists = True - if not override: - raise Exception('database already exists') + if self.dburi.startswith('sqlite'): + log.info('checking for existing db in %s', db_path) + if os.path.isfile(db_path): + + self.db_exists = True + if not override: + raise Exception('database already exists') def create_tables(self, override=False): + """Create a auth database """ - Create a auth database - """ + self.check_for_db(override) if self.db_exists: log.info("database exist and it's going to be destroyed") @@ -77,34 +84,163 @@ checkfirst = not override meta.Base.metadata.create_all(checkfirst=checkfirst) log.info('Created tables for %s', self.dbname) - + + + + def set_db_version(self): + try: + ver = DbMigrateVersion() + ver.version = __dbversion__ + ver.repository_id = 'rhodecode_db_migrations' + ver.repository_path = 'versions' + self.sa.add(ver) + self.sa.commit() + except: + self.sa.rollback() + raise + log.info('db version set to: %s', __dbversion__) + + + def upgrade(self): + """Upgrades given database schema to given revision following + all needed steps, + + :param revision: revision to upgrade to + """ + + from rhodecode.lib.dbmigrate.migrate.versioning import api + from rhodecode.lib.dbmigrate.migrate.exceptions import \ + DatabaseNotControlledError + + upgrade = ask_ok('You are about to perform database upgrade, make ' + 'sure You backed up your database before. ' + 'Continue ? [y/n]') + if not upgrade: + sys.exit('Nothing done') + + repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))), + 'rhodecode/lib/dbmigrate') + db_uri = self.dburi + + try: + curr_version = api.db_version(db_uri, repository_path) + msg = ('Found current database under version' + ' control with version %s' % curr_version) + + except (RuntimeError, DatabaseNotControlledError), e: + curr_version = 1 + msg = ('Current database is not under version control. Setting' + ' as version %s' % curr_version) + api.version_control(db_uri, repository_path, curr_version) + + print (msg) + + if curr_version == __dbversion__: + sys.exit('This database is already at the newest version') + + #====================================================================== + # UPGRADE STEPS + #====================================================================== + class UpgradeSteps(object): + + def __init__(self, klass): + self.klass = klass + + def step_0(self): + #step 0 is the schema upgrade, and than follow proper upgrades + print ('attempting to do database upgrade to version %s' \ + % __dbversion__) + api.upgrade(db_uri, repository_path, __dbversion__) + print ('Schema upgrade completed') + + def step_1(self): + pass + + def step_2(self): + print ('Patching repo paths for newer version of RhodeCode') + self.klass.fix_repo_paths() + + print ('Patching default user of RhodeCode') + self.klass.fix_default_user() + + log.info('Changing ui settings') + self.klass.create_ui_settings() + + + upgrade_steps = [0] + range(curr_version + 1, __dbversion__ + 1) + + #CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE + for step in upgrade_steps: + print ('performing upgrade step %s' % step) + callable = getattr(UpgradeSteps(self), 'step_%s' % step)() + + + + def fix_repo_paths(self): + """Fixes a old rhodecode version path into new one without a '*' + """ + + paths = self.sa.query(RhodeCodeUi)\ + .filter(RhodeCodeUi.ui_key == '/')\ + .scalar() + + paths.ui_value = paths.ui_value.replace('*', '') + + try: + self.sa.add(paths) + self.sa.commit() + except: + self.sa.rollback() + raise + + def fix_default_user(self): + """Fixes a old default user with some 'nicer' default values, + used mostly for anonymous access + """ + def_user = self.sa.query(User)\ + .filter(User.username == 'default')\ + .one() + + def_user.name = 'Anonymous' + def_user.lastname = 'User' + def_user.email = 'anonymous@rhodecode.org' + + try: + self.sa.add(def_user) + self.sa.commit() + except: + self.sa.rollback() + raise + + + def admin_prompt(self, second=False): if not self.tests: import getpass - - + + def get_password(): password = getpass.getpass('Specify admin password (min 6 chars):') confirm = getpass.getpass('Confirm password:') - + if password != confirm: log.error('passwords mismatch') return False if len(password) < 6: log.error('password is to short use at least 6 characters') return False - + return password - + username = raw_input('Specify admin username:') - + password = get_password() if not password: #second try password = get_password() if not password: sys.exit() - + email = raw_input('Specify admin email:') self.create_user(username, password, email, True) else: @@ -112,71 +248,121 @@ self.create_user('test_admin', 'test12', 'test_admin@mail.com', True) self.create_user('test_regular', 'test12', 'test_regular@mail.com', False) self.create_user('test_regular2', 'test12', 'test_regular2@mail.com', False) - + + def create_ui_settings(self): + """Creates ui settings, fills out hooks + and disables dotencode - + """ + #HOOKS + hooks1_key = 'changegroup.update' + hooks1_ = self.sa.query(RhodeCodeUi)\ + .filter(RhodeCodeUi.ui_key == hooks1_key).scalar() + + hooks1 = RhodeCodeUi() if hooks1_ is None else hooks1_ + hooks1.ui_section = 'hooks' + hooks1.ui_key = hooks1_key + hooks1.ui_value = 'hg update >&2' + hooks1.ui_active = False + + hooks2_key = 'changegroup.repo_size' + hooks2_ = self.sa.query(RhodeCodeUi)\ + .filter(RhodeCodeUi.ui_key == hooks2_key).scalar() + + hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_ + hooks2.ui_section = 'hooks' + hooks2.ui_key = hooks2_key + hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size' + + hooks3 = RhodeCodeUi() + hooks3.ui_section = 'hooks' + hooks3.ui_key = 'pretxnchangegroup.push_logger' + hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action' + + hooks4 = RhodeCodeUi() + hooks4.ui_section = 'hooks' + hooks4.ui_key = 'preoutgoing.pull_logger' + hooks4.ui_value = 'python:rhodecode.lib.hooks.log_pull_action' + + #For mercurial 1.7 set backward comapatibility with format + dotencode_disable = RhodeCodeUi() + dotencode_disable.ui_section = 'format' + dotencode_disable.ui_key = 'dotencode' + dotencode_disable.ui_value = 'false' + + try: + self.sa.add(hooks1) + self.sa.add(hooks2) + self.sa.add(hooks3) + self.sa.add(hooks4) + self.sa.add(dotencode_disable) + self.sa.commit() + except: + self.sa.rollback() + raise + + + def create_ldap_options(self): + """Creates ldap settings""" + + try: + for k in ['ldap_active', 'ldap_host', 'ldap_port', 'ldap_ldaps', + 'ldap_dn_user', 'ldap_dn_pass', 'ldap_base_dn']: + + setting = RhodeCodeSettings(k, '') + self.sa.add(setting) + self.sa.commit() + except: + self.sa.rollback() + raise + def config_prompt(self, test_repo_path=''): log.info('Setting up repositories config') - + if not self.tests and not test_repo_path: path = raw_input('Specify valid full path to your repositories' ' you can change this later in application settings:') else: path = test_repo_path - + if not os.path.isdir(path): log.error('You entered wrong path: %s', path) sys.exit() - - hooks1 = RhodeCodeUi() - hooks1.ui_section = 'hooks' - hooks1.ui_key = 'changegroup.update' - hooks1.ui_value = 'hg update >&2' - hooks1.ui_active = False - - hooks2 = RhodeCodeUi() - hooks2.ui_section = 'hooks' - hooks2.ui_key = 'changegroup.repo_size' - hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size' - + + self.create_ui_settings() + + #HG UI OPTIONS web1 = RhodeCodeUi() web1.ui_section = 'web' web1.ui_key = 'push_ssl' web1.ui_value = 'false' - + web2 = RhodeCodeUi() web2.ui_section = 'web' web2.ui_key = 'allow_archive' web2.ui_value = 'gz zip bz2' - + web3 = RhodeCodeUi() web3.ui_section = 'web' web3.ui_key = 'allow_push' web3.ui_value = '*' - + web4 = RhodeCodeUi() web4.ui_section = 'web' web4.ui_key = 'baseurl' - web4.ui_value = '/' - + web4.ui_value = '/' + paths = RhodeCodeUi() paths.ui_section = 'paths' paths.ui_key = '/' - paths.ui_value = os.path.join(path, '*') - - - hgsettings1 = RhodeCodeSettings() - - hgsettings1.app_settings_name = 'realm' - hgsettings1.app_settings_value = 'RhodeCode authentication' - - hgsettings2 = RhodeCodeSettings() - hgsettings2.app_settings_name = 'title' - hgsettings2.app_settings_value = 'RhodeCode' - + paths.ui_value = path + + + hgsettings1 = RhodeCodeSettings('realm', 'RhodeCode authentication') + hgsettings2 = RhodeCodeSettings('title', 'RhodeCode') + + try: - self.sa.add(hooks1) - self.sa.add(hooks2) self.sa.add(web1) self.sa.add(web2) self.sa.add(web3) @@ -184,12 +370,16 @@ self.sa.add(paths) self.sa.add(hgsettings1) self.sa.add(hgsettings2) + self.sa.commit() except: self.sa.rollback() - raise + raise + + self.create_ldap_options() + log.info('created ui config') - + def create_user(self, username, password, email='', admin=False): log.info('creating administrator user %s', username) new_user = User() @@ -200,7 +390,7 @@ new_user.email = email new_user.admin = admin new_user.active = True - + try: self.sa.add(new_user) self.sa.commit() @@ -214,9 +404,9 @@ def_user = User() def_user.username = 'default' def_user.password = get_crypt_password(str(uuid.uuid1())[:8]) - def_user.name = 'default' - def_user.lastname = 'default' - def_user.email = 'default@default.com' + def_user.name = 'Anonymous' + def_user.lastname = 'User' + def_user.email = 'anonymous@rhodecode.org' def_user.admin = False def_user.active = False try: @@ -225,7 +415,7 @@ except: self.sa.rollback() raise - + def create_permissions(self): #module.(access|create|change|delete)_[name] #module.(read|write|owner) @@ -240,7 +430,7 @@ ('hg.register.manual_activate', 'Register new user with rhodecode without manual activation'), ('hg.register.auto_activate', 'Register new user with rhodecode without auto activation'), ] - + for p in perms: new_perm = Permission() new_perm.permission_name = p[0] @@ -254,28 +444,28 @@ def populate_default_permissions(self): log.info('creating default user permissions') - + default_user = self.sa.query(User)\ .filter(User.username == 'default').scalar() - + reg_perm = UserToPerm() reg_perm.user = default_user reg_perm.permission = self.sa.query(Permission)\ .filter(Permission.permission_name == 'hg.register.manual_activate')\ - .scalar() - + .scalar() + create_repo_perm = UserToPerm() create_repo_perm.user = default_user create_repo_perm.permission = self.sa.query(Permission)\ .filter(Permission.permission_name == 'hg.create.repository')\ - .scalar() - + .scalar() + default_repo_perm = UserToPerm() default_repo_perm.user = default_user default_repo_perm.permission = self.sa.query(Permission)\ .filter(Permission.permission_name == 'repository.read')\ - .scalar() - + .scalar() + try: self.sa.add(reg_perm) self.sa.add(create_repo_perm) @@ -283,5 +473,5 @@ self.sa.commit() except: self.sa.rollback() - raise - + raise + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/__init__.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.lib.dbmigrate.__init__ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Database migration modules + + :created_on: Dec 11, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +import logging +from sqlalchemy import engine_from_config + + +from rhodecode.lib.utils import BasePasterCommand, Command, add_cache +from rhodecode.lib.db_manage import DbManage + +log = logging.getLogger(__name__) + +class UpgradeDb(BasePasterCommand): + """Command used for paster to upgrade our database to newer version + """ + + max_args = 1 + min_args = 1 + + usage = "CONFIG_FILE" + summary = "Upgrades current db to newer version given configuration file" + group_name = "RhodeCode" + + parser = Command.standard_parser(verbose=True) + + def command(self): + from pylons import config + + add_cache(config) + + db_uri = config['sqlalchemy.db1.url'] + + dbmanage = DbManage(log_sql=True, dbconf=db_uri, + root=config['here'], tests=False) + + dbmanage.upgrade() + + + + def update_parser(self): + self.parser.add_option('--sql', + action='store_true', + dest='just_sql', + help="Prints upgrade sql for further investigation", + default=False) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate.cfg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate.cfg Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,20 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=rhodecode_db_migrations + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=db_migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=['sqlite'] diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/__init__.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,9 @@ +""" + SQLAlchemy migrate provides two APIs :mod:`migrate.versioning` for + database schema version and repository management and + :mod:`migrate.changeset` that allows to define database schema changes + using Python. +""" + +from rhodecode.lib.dbmigrate.migrate.versioning import * +from rhodecode.lib.dbmigrate.migrate.changeset import * diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/changeset/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/changeset/__init__.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,28 @@ +""" + This module extends SQLAlchemy and provides additional DDL [#]_ + support. + + .. [#] SQL Data Definition Language +""" +import re +import warnings + +import sqlalchemy +from sqlalchemy import __version__ as _sa_version + +warnings.simplefilter('always', DeprecationWarning) + +_sa_version = tuple(int(re.match("\d+", x).group(0)) for x in _sa_version.split(".")) +SQLA_06 = _sa_version >= (0, 6) + +del re +del _sa_version + +from rhodecode.lib.dbmigrate.migrate.changeset.schema import * +from rhodecode.lib.dbmigrate.migrate.changeset.constraint import * + +sqlalchemy.schema.Table.__bases__ += (ChangesetTable, ) +sqlalchemy.schema.Column.__bases__ += (ChangesetColumn, ) +sqlalchemy.schema.Index.__bases__ += (ChangesetIndex, ) + +sqlalchemy.schema.DefaultClause.__bases__ += (ChangesetDefaultClause, ) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/changeset/ansisql.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/changeset/ansisql.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,358 @@ +""" + Extensions to SQLAlchemy for altering existing tables. + + At the moment, this isn't so much based off of ANSI as much as + things that just happen to work with multiple databases. +""" +import StringIO + +import sqlalchemy as sa +from sqlalchemy.schema import SchemaVisitor +from sqlalchemy.engine.default import DefaultDialect +from sqlalchemy.sql import ClauseElement +from sqlalchemy.schema import (ForeignKeyConstraint, + PrimaryKeyConstraint, + CheckConstraint, + UniqueConstraint, + Index) + +from rhodecode.lib.dbmigrate.migrate import exceptions +from rhodecode.lib.dbmigrate.migrate.changeset import constraint, SQLA_06 + +if not SQLA_06: + from sqlalchemy.sql.compiler import SchemaGenerator, SchemaDropper +else: + from sqlalchemy.schema import AddConstraint, DropConstraint + from sqlalchemy.sql.compiler import DDLCompiler + SchemaGenerator = SchemaDropper = DDLCompiler + + +class AlterTableVisitor(SchemaVisitor): + """Common operations for ``ALTER TABLE`` statements.""" + + if SQLA_06: + # engine.Compiler looks for .statement + # when it spawns off a new compiler + statement = ClauseElement() + + def append(self, s): + """Append content to the SchemaIterator's query buffer.""" + + self.buffer.write(s) + + def execute(self): + """Execute the contents of the SchemaIterator's buffer.""" + try: + return self.connection.execute(self.buffer.getvalue()) + finally: + self.buffer.truncate(0) + + def __init__(self, dialect, connection, **kw): + self.connection = connection + self.buffer = StringIO.StringIO() + self.preparer = dialect.identifier_preparer + self.dialect = dialect + + def traverse_single(self, elem): + ret = super(AlterTableVisitor, self).traverse_single(elem) + if ret: + # adapt to 0.6 which uses a string-returning + # object + self.append(" %s" % ret) + + def _to_table(self, param): + """Returns the table object for the given param object.""" + if isinstance(param, (sa.Column, sa.Index, sa.schema.Constraint)): + ret = param.table + else: + ret = param + return ret + + def start_alter_table(self, param): + """Returns the start of an ``ALTER TABLE`` SQL-Statement. + + Use the param object to determine the table name and use it + for building the SQL statement. + + :param param: object to determine the table from + :type param: :class:`sqlalchemy.Column`, :class:`sqlalchemy.Index`, + :class:`sqlalchemy.schema.Constraint`, :class:`sqlalchemy.Table`, + or string (table name) + """ + table = self._to_table(param) + self.append('\nALTER TABLE %s ' % self.preparer.format_table(table)) + return table + + +class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator): + """Extends ansisql generator for column creation (alter table add col)""" + + def visit_column(self, column): + """Create a column (table already exists). + + :param column: column object + :type column: :class:`sqlalchemy.Column` instance + """ + if column.default is not None: + self.traverse_single(column.default) + + table = self.start_alter_table(column) + self.append("ADD ") + self.append(self.get_column_specification(column)) + + for cons in column.constraints: + self.traverse_single(cons) + self.execute() + + # ALTER TABLE STATEMENTS + + # add indexes and unique constraints + if column.index_name: + Index(column.index_name,column).create() + elif column.unique_name: + constraint.UniqueConstraint(column, + name=column.unique_name).create() + + # SA bounds FK constraints to table, add manually + for fk in column.foreign_keys: + self.add_foreignkey(fk.constraint) + + # add primary key constraint if needed + if column.primary_key_name: + cons = constraint.PrimaryKeyConstraint(column, + name=column.primary_key_name) + cons.create() + + if SQLA_06: + def add_foreignkey(self, fk): + self.connection.execute(AddConstraint(fk)) + +class ANSIColumnDropper(AlterTableVisitor, SchemaDropper): + """Extends ANSI SQL dropper for column dropping (``ALTER TABLE + DROP COLUMN``). + """ + + def visit_column(self, column): + """Drop a column from its table. + + :param column: the column object + :type column: :class:`sqlalchemy.Column` + """ + table = self.start_alter_table(column) + self.append('DROP COLUMN %s' % self.preparer.format_column(column)) + self.execute() + + +class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator): + """Manages changes to existing schema elements. + + Note that columns are schema elements; ``ALTER TABLE ADD COLUMN`` + is in SchemaGenerator. + + All items may be renamed. Columns can also have many of their properties - + type, for example - changed. + + Each function is passed a tuple, containing (object, name); where + object is a type of object you'd expect for that function + (ie. table for visit_table) and name is the object's new + name. NONE means the name is unchanged. + """ + + def visit_table(self, table): + """Rename a table. Other ops aren't supported.""" + self.start_alter_table(table) + self.append("RENAME TO %s" % self.preparer.quote(table.new_name, + table.quote)) + self.execute() + + def visit_index(self, index): + """Rename an index""" + if hasattr(self, '_validate_identifier'): + # SA <= 0.6.3 + self.append("ALTER INDEX %s RENAME TO %s" % ( + self.preparer.quote( + self._validate_identifier( + index.name, True), index.quote), + self.preparer.quote( + self._validate_identifier( + index.new_name, True), index.quote))) + else: + # SA >= 0.6.5 + self.append("ALTER INDEX %s RENAME TO %s" % ( + self.preparer.quote( + self._index_identifier( + index.name), index.quote), + self.preparer.quote( + self._index_identifier( + index.new_name), index.quote))) + self.execute() + + def visit_column(self, delta): + """Rename/change a column.""" + # ALTER COLUMN is implemented as several ALTER statements + keys = delta.keys() + if 'type' in keys: + self._run_subvisit(delta, self._visit_column_type) + if 'nullable' in keys: + self._run_subvisit(delta, self._visit_column_nullable) + if 'server_default' in keys: + # Skip 'default': only handle server-side defaults, others + # are managed by the app, not the db. + self._run_subvisit(delta, self._visit_column_default) + if 'name' in keys: + self._run_subvisit(delta, self._visit_column_name, start_alter=False) + + def _run_subvisit(self, delta, func, start_alter=True): + """Runs visit method based on what needs to be changed on column""" + table = self._to_table(delta.table) + col_name = delta.current_name + if start_alter: + self.start_alter_column(table, col_name) + ret = func(table, delta.result_column, delta) + self.execute() + + def start_alter_column(self, table, col_name): + """Starts ALTER COLUMN""" + self.start_alter_table(table) + self.append("ALTER COLUMN %s " % self.preparer.quote(col_name, table.quote)) + + def _visit_column_nullable(self, table, column, delta): + nullable = delta['nullable'] + if nullable: + self.append("DROP NOT NULL") + else: + self.append("SET NOT NULL") + + def _visit_column_default(self, table, column, delta): + default_text = self.get_column_default_string(column) + if default_text is not None: + self.append("SET DEFAULT %s" % default_text) + else: + self.append("DROP DEFAULT") + + def _visit_column_type(self, table, column, delta): + type_ = delta['type'] + if SQLA_06: + type_text = str(type_.compile(dialect=self.dialect)) + else: + type_text = type_.dialect_impl(self.dialect).get_col_spec() + self.append("TYPE %s" % type_text) + + def _visit_column_name(self, table, column, delta): + self.start_alter_table(table) + col_name = self.preparer.quote(delta.current_name, table.quote) + new_name = self.preparer.format_column(delta.result_column) + self.append('RENAME COLUMN %s TO %s' % (col_name, new_name)) + + +class ANSIConstraintCommon(AlterTableVisitor): + """ + Migrate's constraints require a separate creation function from + SA's: Migrate's constraints are created independently of a table; + SA's are created at the same time as the table. + """ + + def get_constraint_name(self, cons): + """Gets a name for the given constraint. + + If the name is already set it will be used otherwise the + constraint's :meth:`autoname ` + method is used. + + :param cons: constraint object + """ + if cons.name is not None: + ret = cons.name + else: + ret = cons.name = cons.autoname() + return self.preparer.quote(ret, cons.quote) + + def visit_migrate_primary_key_constraint(self, *p, **k): + self._visit_constraint(*p, **k) + + def visit_migrate_foreign_key_constraint(self, *p, **k): + self._visit_constraint(*p, **k) + + def visit_migrate_check_constraint(self, *p, **k): + self._visit_constraint(*p, **k) + + def visit_migrate_unique_constraint(self, *p, **k): + self._visit_constraint(*p, **k) + +if SQLA_06: + class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator): + def _visit_constraint(self, constraint): + constraint.name = self.get_constraint_name(constraint) + self.append(self.process(AddConstraint(constraint))) + self.execute() + + class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper): + def _visit_constraint(self, constraint): + constraint.name = self.get_constraint_name(constraint) + self.append(self.process(DropConstraint(constraint, cascade=constraint.cascade))) + self.execute() + +else: + class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator): + + def get_constraint_specification(self, cons, **kwargs): + """Constaint SQL generators. + + We cannot use SA visitors because they append comma. + """ + + if isinstance(cons, PrimaryKeyConstraint): + if cons.name is not None: + self.append("CONSTRAINT %s " % self.preparer.format_constraint(cons)) + self.append("PRIMARY KEY ") + self.append("(%s)" % ', '.join(self.preparer.quote(c.name, c.quote) + for c in cons)) + self.define_constraint_deferrability(cons) + elif isinstance(cons, ForeignKeyConstraint): + self.define_foreign_key(cons) + elif isinstance(cons, CheckConstraint): + if cons.name is not None: + self.append("CONSTRAINT %s " % + self.preparer.format_constraint(cons)) + self.append("CHECK (%s)" % cons.sqltext) + self.define_constraint_deferrability(cons) + elif isinstance(cons, UniqueConstraint): + if cons.name is not None: + self.append("CONSTRAINT %s " % + self.preparer.format_constraint(cons)) + self.append("UNIQUE (%s)" % \ + (', '.join(self.preparer.quote(c.name, c.quote) for c in cons))) + self.define_constraint_deferrability(cons) + else: + raise exceptions.InvalidConstraintError(cons) + + def _visit_constraint(self, constraint): + + table = self.start_alter_table(constraint) + constraint.name = self.get_constraint_name(constraint) + self.append("ADD ") + self.get_constraint_specification(constraint) + self.execute() + + + class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper): + + def _visit_constraint(self, constraint): + self.start_alter_table(constraint) + self.append("DROP CONSTRAINT ") + constraint.name = self.get_constraint_name(constraint) + self.append(self.preparer.format_constraint(constraint)) + if constraint.cascade: + self.cascade_constraint(constraint) + self.execute() + + def cascade_constraint(self, constraint): + self.append(" CASCADE") + + +class ANSIDialect(DefaultDialect): + columngenerator = ANSIColumnGenerator + columndropper = ANSIColumnDropper + schemachanger = ANSISchemaChanger + constraintgenerator = ANSIConstraintGenerator + constraintdropper = ANSIConstraintDropper diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/changeset/constraint.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/changeset/constraint.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,202 @@ +""" + This module defines standalone schema constraint classes. +""" +from sqlalchemy import schema + +from rhodecode.lib.dbmigrate.migrate.exceptions import * +from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_06 + +class ConstraintChangeset(object): + """Base class for Constraint classes.""" + + def _normalize_columns(self, cols, table_name=False): + """Given: column objects or names; return col names and + (maybe) a table""" + colnames = [] + table = None + for col in cols: + if isinstance(col, schema.Column): + if col.table is not None and table is None: + table = col.table + if table_name: + col = '.'.join((col.table.name, col.name)) + else: + col = col.name + colnames.append(col) + return colnames, table + + def __do_imports(self, visitor_name, *a, **kw): + engine = kw.pop('engine', self.table.bind) + from rhodecode.lib.dbmigrate.migrate.changeset.databases.visitor import (get_engine_visitor, + run_single_visitor) + visitorcallable = get_engine_visitor(engine, visitor_name) + run_single_visitor(engine, visitorcallable, self, *a, **kw) + + def create(self, *a, **kw): + """Create the constraint in the database. + + :param engine: the database engine to use. If this is \ + :keyword:`None` the instance's engine will be used + :type engine: :class:`sqlalchemy.engine.base.Engine` + :param connection: reuse connection istead of creating new one. + :type connection: :class:`sqlalchemy.engine.base.Connection` instance + """ + # TODO: set the parent here instead of in __init__ + self.__do_imports('constraintgenerator', *a, **kw) + + def drop(self, *a, **kw): + """Drop the constraint from the database. + + :param engine: the database engine to use. If this is + :keyword:`None` the instance's engine will be used + :param cascade: Issue CASCADE drop if database supports it + :type engine: :class:`sqlalchemy.engine.base.Engine` + :type cascade: bool + :param connection: reuse connection istead of creating new one. + :type connection: :class:`sqlalchemy.engine.base.Connection` instance + :returns: Instance with cleared columns + """ + self.cascade = kw.pop('cascade', False) + self.__do_imports('constraintdropper', *a, **kw) + # the spirit of Constraint objects is that they + # are immutable (just like in a DB. they're only ADDed + # or DROPped). + #self.columns.clear() + return self + + +class PrimaryKeyConstraint(ConstraintChangeset, schema.PrimaryKeyConstraint): + """Construct PrimaryKeyConstraint + + Migrate's additional parameters: + + :param cols: Columns in constraint. + :param table: If columns are passed as strings, this kw is required + :type table: Table instance + :type cols: strings or Column instances + """ + + __migrate_visit_name__ = 'migrate_primary_key_constraint' + + def __init__(self, *cols, **kwargs): + colnames, table = self._normalize_columns(cols) + table = kwargs.pop('table', table) + super(PrimaryKeyConstraint, self).__init__(*colnames, **kwargs) + if table is not None: + self._set_parent(table) + + + def autoname(self): + """Mimic the database's automatic constraint names""" + return "%s_pkey" % self.table.name + + +class ForeignKeyConstraint(ConstraintChangeset, schema.ForeignKeyConstraint): + """Construct ForeignKeyConstraint + + Migrate's additional parameters: + + :param columns: Columns in constraint + :param refcolumns: Columns that this FK reffers to in another table. + :param table: If columns are passed as strings, this kw is required + :type table: Table instance + :type columns: list of strings or Column instances + :type refcolumns: list of strings or Column instances + """ + + __migrate_visit_name__ = 'migrate_foreign_key_constraint' + + def __init__(self, columns, refcolumns, *args, **kwargs): + colnames, table = self._normalize_columns(columns) + table = kwargs.pop('table', table) + refcolnames, reftable = self._normalize_columns(refcolumns, + table_name=True) + super(ForeignKeyConstraint, self).__init__(colnames, refcolnames, *args, + **kwargs) + if table is not None: + self._set_parent(table) + + @property + def referenced(self): + return [e.column for e in self.elements] + + @property + def reftable(self): + return self.referenced[0].table + + def autoname(self): + """Mimic the database's automatic constraint names""" + if hasattr(self.columns, 'keys'): + # SA <= 0.5 + firstcol = self.columns[self.columns.keys()[0]] + ret = "%(table)s_%(firstcolumn)s_fkey" % dict( + table=firstcol.table.name, + firstcolumn=firstcol.name,) + else: + # SA >= 0.6 + ret = "%(table)s_%(firstcolumn)s_fkey" % dict( + table=self.table.name, + firstcolumn=self.columns[0],) + return ret + + +class CheckConstraint(ConstraintChangeset, schema.CheckConstraint): + """Construct CheckConstraint + + Migrate's additional parameters: + + :param sqltext: Plain SQL text to check condition + :param columns: If not name is applied, you must supply this kw\ + to autoname constraint + :param table: If columns are passed as strings, this kw is required + :type table: Table instance + :type columns: list of Columns instances + :type sqltext: string + """ + + __migrate_visit_name__ = 'migrate_check_constraint' + + def __init__(self, sqltext, *args, **kwargs): + cols = kwargs.pop('columns', []) + if not cols and not kwargs.get('name', False): + raise InvalidConstraintError('You must either set "name"' + 'parameter or "columns" to autogenarate it.') + colnames, table = self._normalize_columns(cols) + table = kwargs.pop('table', table) + schema.CheckConstraint.__init__(self, sqltext, *args, **kwargs) + if table is not None: + if not SQLA_06: + self.table = table + self._set_parent(table) + self.colnames = colnames + + def autoname(self): + return "%(table)s_%(cols)s_check" % \ + dict(table=self.table.name, cols="_".join(self.colnames)) + + +class UniqueConstraint(ConstraintChangeset, schema.UniqueConstraint): + """Construct UniqueConstraint + + Migrate's additional parameters: + + :param cols: Columns in constraint. + :param table: If columns are passed as strings, this kw is required + :type table: Table instance + :type cols: strings or Column instances + + .. versionadded:: 0.6.0 + """ + + __migrate_visit_name__ = 'migrate_unique_constraint' + + def __init__(self, *cols, **kwargs): + self.colnames, table = self._normalize_columns(cols) + table = kwargs.pop('table', table) + super(UniqueConstraint, self).__init__(*self.colnames, **kwargs) + if table is not None: + self._set_parent(table) + + def autoname(self): + """Mimic the database's automatic constraint names""" + return "%s_%s_key" % (self.table.name, self.colnames[0]) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/changeset/databases/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/changeset/databases/__init__.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,10 @@ +""" + This module contains database dialect specific changeset + implementations. +""" +__all__ = [ + 'postgres', + 'sqlite', + 'mysql', + 'oracle', +] diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/changeset/databases/firebird.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/changeset/databases/firebird.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,80 @@ +""" + Firebird database specific implementations of changeset classes. +""" +from sqlalchemy.databases import firebird as sa_base + +from rhodecode.lib.dbmigrate.migrate import exceptions +from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06 + + +if SQLA_06: + FBSchemaGenerator = sa_base.FBDDLCompiler +else: + FBSchemaGenerator = sa_base.FBSchemaGenerator + +class FBColumnGenerator(FBSchemaGenerator, ansisql.ANSIColumnGenerator): + """Firebird column generator implementation.""" + + +class FBColumnDropper(ansisql.ANSIColumnDropper): + """Firebird column dropper implementation.""" + + def visit_column(self, column): + """Firebird supports 'DROP col' instead of 'DROP COLUMN col' syntax + + Drop primary key and unique constraints if dropped column is referencing it.""" + if column.primary_key: + if column.table.primary_key.columns.contains_column(column): + column.table.primary_key.drop() + # TODO: recreate primary key if it references more than this column + if column.unique or getattr(column, 'unique_name', None): + for cons in column.table.constraints: + if cons.contains_column(column): + cons.drop() + # TODO: recreate unique constraint if it refenrences more than this column + + table = self.start_alter_table(column) + self.append('DROP %s' % self.preparer.format_column(column)) + self.execute() + + +class FBSchemaChanger(ansisql.ANSISchemaChanger): + """Firebird schema changer implementation.""" + + def visit_table(self, table): + """Rename table not supported""" + raise exceptions.NotSupportedError( + "Firebird does not support renaming tables.") + + def _visit_column_name(self, table, column, delta): + self.start_alter_table(table) + col_name = self.preparer.quote(delta.current_name, table.quote) + new_name = self.preparer.format_column(delta.result_column) + self.append('ALTER COLUMN %s TO %s' % (col_name, new_name)) + + def _visit_column_nullable(self, table, column, delta): + """Changing NULL is not supported""" + # TODO: http://www.firebirdfaq.org/faq103/ + raise exceptions.NotSupportedError( + "Firebird does not support altering NULL bevahior.") + + +class FBConstraintGenerator(ansisql.ANSIConstraintGenerator): + """Firebird constraint generator implementation.""" + + +class FBConstraintDropper(ansisql.ANSIConstraintDropper): + """Firebird constaint dropper implementation.""" + + def cascade_constraint(self, constraint): + """Cascading constraints is not supported""" + raise exceptions.NotSupportedError( + "Firebird does not support cascading constraints") + + +class FBDialect(ansisql.ANSIDialect): + columngenerator = FBColumnGenerator + columndropper = FBColumnDropper + schemachanger = FBSchemaChanger + constraintgenerator = FBConstraintGenerator + constraintdropper = FBConstraintDropper diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/changeset/databases/mysql.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/changeset/databases/mysql.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,94 @@ +""" + MySQL database specific implementations of changeset classes. +""" + +from sqlalchemy.databases import mysql as sa_base +from sqlalchemy import types as sqltypes + +from rhodecode.lib.dbmigrate.migrate import exceptions +from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06 + + +if not SQLA_06: + MySQLSchemaGenerator = sa_base.MySQLSchemaGenerator +else: + MySQLSchemaGenerator = sa_base.MySQLDDLCompiler + +class MySQLColumnGenerator(MySQLSchemaGenerator, ansisql.ANSIColumnGenerator): + pass + + +class MySQLColumnDropper(ansisql.ANSIColumnDropper): + pass + + +class MySQLSchemaChanger(MySQLSchemaGenerator, ansisql.ANSISchemaChanger): + + def visit_column(self, delta): + table = delta.table + colspec = self.get_column_specification(delta.result_column) + if delta.result_column.autoincrement: + primary_keys = [c for c in table.primary_key.columns + if (c.autoincrement and + isinstance(c.type, sqltypes.Integer) and + not c.foreign_keys)] + + if primary_keys: + first = primary_keys.pop(0) + if first.name == delta.current_name: + colspec += " AUTO_INCREMENT" + old_col_name = self.preparer.quote(delta.current_name, table.quote) + + self.start_alter_table(table) + + self.append("CHANGE COLUMN %s " % old_col_name) + self.append(colspec) + self.execute() + + def visit_index(self, param): + # If MySQL can do this, I can't find how + raise exceptions.NotSupportedError("MySQL cannot rename indexes") + + +class MySQLConstraintGenerator(ansisql.ANSIConstraintGenerator): + pass + +if SQLA_06: + class MySQLConstraintDropper(MySQLSchemaGenerator, ansisql.ANSIConstraintDropper): + def visit_migrate_check_constraint(self, *p, **k): + raise exceptions.NotSupportedError("MySQL does not support CHECK" + " constraints, use triggers instead.") + +else: + class MySQLConstraintDropper(ansisql.ANSIConstraintDropper): + + def visit_migrate_primary_key_constraint(self, constraint): + self.start_alter_table(constraint) + self.append("DROP PRIMARY KEY") + self.execute() + + def visit_migrate_foreign_key_constraint(self, constraint): + self.start_alter_table(constraint) + self.append("DROP FOREIGN KEY ") + constraint.name = self.get_constraint_name(constraint) + self.append(self.preparer.format_constraint(constraint)) + self.execute() + + def visit_migrate_check_constraint(self, *p, **k): + raise exceptions.NotSupportedError("MySQL does not support CHECK" + " constraints, use triggers instead.") + + def visit_migrate_unique_constraint(self, constraint, *p, **k): + self.start_alter_table(constraint) + self.append('DROP INDEX ') + constraint.name = self.get_constraint_name(constraint) + self.append(self.preparer.format_constraint(constraint)) + self.execute() + + +class MySQLDialect(ansisql.ANSIDialect): + columngenerator = MySQLColumnGenerator + columndropper = MySQLColumnDropper + schemachanger = MySQLSchemaChanger + constraintgenerator = MySQLConstraintGenerator + constraintdropper = MySQLConstraintDropper diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/changeset/databases/oracle.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/changeset/databases/oracle.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,111 @@ +""" + Oracle database specific implementations of changeset classes. +""" +import sqlalchemy as sa +from sqlalchemy.databases import oracle as sa_base + +from rhodecode.lib.dbmigrate.migrate import exceptions +from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06 + + +if not SQLA_06: + OracleSchemaGenerator = sa_base.OracleSchemaGenerator +else: + OracleSchemaGenerator = sa_base.OracleDDLCompiler + + +class OracleColumnGenerator(OracleSchemaGenerator, ansisql.ANSIColumnGenerator): + pass + + +class OracleColumnDropper(ansisql.ANSIColumnDropper): + pass + + +class OracleSchemaChanger(OracleSchemaGenerator, ansisql.ANSISchemaChanger): + + def get_column_specification(self, column, **kwargs): + # Ignore the NOT NULL generated + override_nullable = kwargs.pop('override_nullable', None) + if override_nullable: + orig = column.nullable + column.nullable = True + ret = super(OracleSchemaChanger, self).get_column_specification( + column, **kwargs) + if override_nullable: + column.nullable = orig + return ret + + def visit_column(self, delta): + keys = delta.keys() + + if 'name' in keys: + self._run_subvisit(delta, + self._visit_column_name, + start_alter=False) + + if len(set(('type', 'nullable', 'server_default')).intersection(keys)): + self._run_subvisit(delta, + self._visit_column_change, + start_alter=False) + + def _visit_column_change(self, table, column, delta): + # Oracle cannot drop a default once created, but it can set it + # to null. We'll do that if default=None + # http://forums.oracle.com/forums/message.jspa?messageID=1273234#1273234 + dropdefault_hack = (column.server_default is None \ + and 'server_default' in delta.keys()) + # Oracle apparently doesn't like it when we say "not null" if + # the column's already not null. Fudge it, so we don't need a + # new function + notnull_hack = ((not column.nullable) \ + and ('nullable' not in delta.keys())) + # We need to specify NULL if we're removing a NOT NULL + # constraint + null_hack = (column.nullable and ('nullable' in delta.keys())) + + if dropdefault_hack: + column.server_default = sa.PassiveDefault(sa.sql.null()) + if notnull_hack: + column.nullable = True + colspec = self.get_column_specification(column, + override_nullable=null_hack) + if null_hack: + colspec += ' NULL' + if notnull_hack: + column.nullable = False + if dropdefault_hack: + column.server_default = None + + self.start_alter_table(table) + self.append("MODIFY (") + self.append(colspec) + self.append(")") + + +class OracleConstraintCommon(object): + + def get_constraint_name(self, cons): + # Oracle constraints can't guess their name like other DBs + if not cons.name: + raise exceptions.NotSupportedError( + "Oracle constraint names must be explicitly stated") + return cons.name + + +class OracleConstraintGenerator(OracleConstraintCommon, + ansisql.ANSIConstraintGenerator): + pass + + +class OracleConstraintDropper(OracleConstraintCommon, + ansisql.ANSIConstraintDropper): + pass + + +class OracleDialect(ansisql.ANSIDialect): + columngenerator = OracleColumnGenerator + columndropper = OracleColumnDropper + schemachanger = OracleSchemaChanger + constraintgenerator = OracleConstraintGenerator + constraintdropper = OracleConstraintDropper diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/changeset/databases/postgres.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/changeset/databases/postgres.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,46 @@ +""" + `PostgreSQL`_ database specific implementations of changeset classes. + + .. _`PostgreSQL`: http://www.postgresql.org/ +""" +from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06 + +if not SQLA_06: + from sqlalchemy.databases import postgres as sa_base + PGSchemaGenerator = sa_base.PGSchemaGenerator +else: + from sqlalchemy.databases import postgresql as sa_base + PGSchemaGenerator = sa_base.PGDDLCompiler + + +class PGColumnGenerator(PGSchemaGenerator, ansisql.ANSIColumnGenerator): + """PostgreSQL column generator implementation.""" + pass + + +class PGColumnDropper(ansisql.ANSIColumnDropper): + """PostgreSQL column dropper implementation.""" + pass + + +class PGSchemaChanger(ansisql.ANSISchemaChanger): + """PostgreSQL schema changer implementation.""" + pass + + +class PGConstraintGenerator(ansisql.ANSIConstraintGenerator): + """PostgreSQL constraint generator implementation.""" + pass + + +class PGConstraintDropper(ansisql.ANSIConstraintDropper): + """PostgreSQL constaint dropper implementation.""" + pass + + +class PGDialect(ansisql.ANSIDialect): + columngenerator = PGColumnGenerator + columndropper = PGColumnDropper + schemachanger = PGSchemaChanger + constraintgenerator = PGConstraintGenerator + constraintdropper = PGConstraintDropper diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/changeset/databases/sqlite.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/changeset/databases/sqlite.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,148 @@ +""" + `SQLite`_ database specific implementations of changeset classes. + + .. _`SQLite`: http://www.sqlite.org/ +""" +from UserDict import DictMixin +from copy import copy + +from sqlalchemy.databases import sqlite as sa_base + +from rhodecode.lib.dbmigrate.migrate import exceptions +from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06 + + +if not SQLA_06: + SQLiteSchemaGenerator = sa_base.SQLiteSchemaGenerator +else: + SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler + +class SQLiteCommon(object): + + def _not_supported(self, op): + raise exceptions.NotSupportedError("SQLite does not support " + "%s; see http://www.sqlite.org/lang_altertable.html" % op) + + +class SQLiteHelper(SQLiteCommon): + + def recreate_table(self,table,column=None,delta=None): + table_name = self.preparer.format_table(table) + + # we remove all indexes so as not to have + # problems during copy and re-create + for index in table.indexes: + index.drop() + + self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name) + self.execute() + + insertion_string = self._modify_table(table, column, delta) + + table.create() + self.append(insertion_string % {'table_name': table_name}) + self.execute() + self.append('DROP TABLE migration_tmp') + self.execute() + + def visit_column(self, delta): + if isinstance(delta, DictMixin): + column = delta.result_column + table = self._to_table(delta.table) + else: + column = delta + table = self._to_table(column.table) + self.recreate_table(table,column,delta) + +class SQLiteColumnGenerator(SQLiteSchemaGenerator, + ansisql.ANSIColumnGenerator, + # at the end so we get the normal + # visit_column by default + SQLiteHelper, + SQLiteCommon + ): + """SQLite ColumnGenerator""" + + def _modify_table(self, table, column, delta): + columns = ' ,'.join(map( + self.preparer.format_column, + [c for c in table.columns if c.name!=column.name])) + return ('INSERT INTO %%(table_name)s (%(cols)s) ' + 'SELECT %(cols)s from migration_tmp')%{'cols':columns} + + def visit_column(self,column): + if column.foreign_keys: + SQLiteHelper.visit_column(self,column) + else: + super(SQLiteColumnGenerator,self).visit_column(column) + +class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper): + """SQLite ColumnDropper""" + + def _modify_table(self, table, column, delta): + columns = ' ,'.join(map(self.preparer.format_column, table.columns)) + return 'INSERT INTO %(table_name)s SELECT ' + columns + \ + ' from migration_tmp' + + +class SQLiteSchemaChanger(SQLiteHelper, ansisql.ANSISchemaChanger): + """SQLite SchemaChanger""" + + def _modify_table(self, table, column, delta): + return 'INSERT INTO %(table_name)s SELECT * from migration_tmp' + + def visit_index(self, index): + """Does not support ALTER INDEX""" + self._not_supported('ALTER INDEX') + + +class SQLiteConstraintGenerator(ansisql.ANSIConstraintGenerator, SQLiteHelper, SQLiteCommon): + + def visit_migrate_primary_key_constraint(self, constraint): + tmpl = "CREATE UNIQUE INDEX %s ON %s ( %s )" + cols = ', '.join(map(self.preparer.format_column, constraint.columns)) + tname = self.preparer.format_table(constraint.table) + name = self.get_constraint_name(constraint) + msg = tmpl % (name, tname, cols) + self.append(msg) + self.execute() + + def _modify_table(self, table, column, delta): + return 'INSERT INTO %(table_name)s SELECT * from migration_tmp' + + def visit_migrate_foreign_key_constraint(self, *p, **k): + self.recreate_table(p[0].table) + + def visit_migrate_unique_constraint(self, *p, **k): + self.recreate_table(p[0].table) + + +class SQLiteConstraintDropper(ansisql.ANSIColumnDropper, + SQLiteCommon, + ansisql.ANSIConstraintCommon): + + def visit_migrate_primary_key_constraint(self, constraint): + tmpl = "DROP INDEX %s " + name = self.get_constraint_name(constraint) + msg = tmpl % (name) + self.append(msg) + self.execute() + + def visit_migrate_foreign_key_constraint(self, *p, **k): + self._not_supported('ALTER TABLE DROP CONSTRAINT') + + def visit_migrate_check_constraint(self, *p, **k): + self._not_supported('ALTER TABLE DROP CONSTRAINT') + + def visit_migrate_unique_constraint(self, *p, **k): + self._not_supported('ALTER TABLE DROP CONSTRAINT') + + +# TODO: technically primary key is a NOT NULL + UNIQUE constraint, should add NOT NULL to index + +class SQLiteDialect(ansisql.ANSIDialect): + columngenerator = SQLiteColumnGenerator + columndropper = SQLiteColumnDropper + schemachanger = SQLiteSchemaChanger + constraintgenerator = SQLiteConstraintGenerator + constraintdropper = SQLiteConstraintDropper diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/changeset/databases/visitor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/changeset/databases/visitor.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,78 @@ +""" + Module for visitor class mapping. +""" +import sqlalchemy as sa + +from rhodecode.lib.dbmigrate.migrate.changeset import ansisql +from rhodecode.lib.dbmigrate.migrate.changeset.databases import (sqlite, + postgres, + mysql, + oracle, + firebird) + + +# Map SA dialects to the corresponding Migrate extensions +DIALECTS = { + "default": ansisql.ANSIDialect, + "sqlite": sqlite.SQLiteDialect, + "postgres": postgres.PGDialect, + "postgresql": postgres.PGDialect, + "mysql": mysql.MySQLDialect, + "oracle": oracle.OracleDialect, + "firebird": firebird.FBDialect, +} + + +def get_engine_visitor(engine, name): + """ + Get the visitor implementation for the given database engine. + + :param engine: SQLAlchemy Engine + :param name: Name of the visitor + :type name: string + :type engine: Engine + :returns: visitor + """ + # TODO: link to supported visitors + return get_dialect_visitor(engine.dialect, name) + + +def get_dialect_visitor(sa_dialect, name): + """ + Get the visitor implementation for the given dialect. + + Finds the visitor implementation based on the dialect class and + returns and instance initialized with the given name. + + Binds dialect specific preparer to visitor. + """ + + # map sa dialect to migrate dialect and return visitor + sa_dialect_name = getattr(sa_dialect, 'name', 'default') + migrate_dialect_cls = DIALECTS[sa_dialect_name] + visitor = getattr(migrate_dialect_cls, name) + + # bind preparer + visitor.preparer = sa_dialect.preparer(sa_dialect) + + return visitor + +def run_single_visitor(engine, visitorcallable, element, + connection=None, **kwargs): + """Taken from :meth:`sqlalchemy.engine.base.Engine._run_single_visitor` + with support for migrate visitors. + """ + if connection is None: + conn = engine.contextual_connect(close_with_result=False) + else: + conn = connection + visitor = visitorcallable(engine.dialect, conn) + try: + if hasattr(element, '__migrate_visit_name__'): + fn = getattr(visitor, 'visit_' + element.__migrate_visit_name__) + else: + fn = getattr(visitor, 'visit_' + element.__visit_name__) + fn(element, **kwargs) + finally: + if connection is None: + conn.close() diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/changeset/schema.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/changeset/schema.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,669 @@ +""" + Schema module providing common schema operations. +""" +import warnings + +from UserDict import DictMixin + +import sqlalchemy + +from sqlalchemy.schema import ForeignKeyConstraint +from sqlalchemy.schema import UniqueConstraint + +from rhodecode.lib.dbmigrate.migrate.exceptions import * +from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_06 +from rhodecode.lib.dbmigrate.migrate.changeset.databases.visitor import (get_engine_visitor, + run_single_visitor) + + +__all__ = [ + 'create_column', + 'drop_column', + 'alter_column', + 'rename_table', + 'rename_index', + 'ChangesetTable', + 'ChangesetColumn', + 'ChangesetIndex', + 'ChangesetDefaultClause', + 'ColumnDelta', +] + +DEFAULT_ALTER_METADATA = True + + +def create_column(column, table=None, *p, **kw): + """Create a column, given the table. + + API to :meth:`ChangesetColumn.create`. + """ + if table is not None: + return table.create_column(column, *p, **kw) + return column.create(*p, **kw) + + +def drop_column(column, table=None, *p, **kw): + """Drop a column, given the table. + + API to :meth:`ChangesetColumn.drop`. + """ + if table is not None: + return table.drop_column(column, *p, **kw) + return column.drop(*p, **kw) + + +def rename_table(table, name, engine=None, **kw): + """Rename a table. + + If Table instance is given, engine is not used. + + API to :meth:`ChangesetTable.rename`. + + :param table: Table to be renamed. + :param name: New name for Table. + :param engine: Engine instance. + :type table: string or Table instance + :type name: string + :type engine: obj + """ + table = _to_table(table, engine) + table.rename(name, **kw) + + +def rename_index(index, name, table=None, engine=None, **kw): + """Rename an index. + + If Index instance is given, + table and engine are not used. + + API to :meth:`ChangesetIndex.rename`. + + :param index: Index to be renamed. + :param name: New name for index. + :param table: Table to which Index is reffered. + :param engine: Engine instance. + :type index: string or Index instance + :type name: string + :type table: string or Table instance + :type engine: obj + """ + index = _to_index(index, table, engine) + index.rename(name, **kw) + + +def alter_column(*p, **k): + """Alter a column. + + This is a helper function that creates a :class:`ColumnDelta` and + runs it. + + :argument column: + The name of the column to be altered or a + :class:`ChangesetColumn` column representing it. + + :param table: + A :class:`~sqlalchemy.schema.Table` or table name to + for the table where the column will be changed. + + :param engine: + The :class:`~sqlalchemy.engine.base.Engine` to use for table + reflection and schema alterations. + + :param alter_metadata: + If `True`, which is the default, the + :class:`~sqlalchemy.schema.Column` will also modified. + If `False`, the :class:`~sqlalchemy.schema.Column` will be left + as it was. + + :returns: A :class:`ColumnDelta` instance representing the change. + + + """ + + k.setdefault('alter_metadata', DEFAULT_ALTER_METADATA) + + if 'table' not in k and isinstance(p[0], sqlalchemy.Column): + k['table'] = p[0].table + if 'engine' not in k: + k['engine'] = k['table'].bind + + # deprecation + if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column): + warnings.warn( + "Passing a Column object to alter_column is deprecated." + " Just pass in keyword parameters instead.", + MigrateDeprecationWarning + ) + engine = k['engine'] + delta = ColumnDelta(*p, **k) + + visitorcallable = get_engine_visitor(engine, 'schemachanger') + engine._run_visitor(visitorcallable, delta) + + return delta + + +def _to_table(table, engine=None): + """Return if instance of Table, else construct new with metadata""" + if isinstance(table, sqlalchemy.Table): + return table + + # Given: table name, maybe an engine + meta = sqlalchemy.MetaData() + if engine is not None: + meta.bind = engine + return sqlalchemy.Table(table, meta) + + +def _to_index(index, table=None, engine=None): + """Return if instance of Index, else construct new with metadata""" + if isinstance(index, sqlalchemy.Index): + return index + + # Given: index name; table name required + table = _to_table(table, engine) + ret = sqlalchemy.Index(index) + ret.table = table + return ret + + +class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem): + """Extracts the differences between two columns/column-parameters + + May receive parameters arranged in several different ways: + + * **current_column, new_column, \*p, \*\*kw** + Additional parameters can be specified to override column + differences. + + * **current_column, \*p, \*\*kw** + Additional parameters alter current_column. Table name is extracted + from current_column object. + Name is changed to current_column.name from current_name, + if current_name is specified. + + * **current_col_name, \*p, \*\*kw** + Table kw must specified. + + :param table: Table at which current Column should be bound to.\ + If table name is given, reflection will be used. + :type table: string or Table instance + :param alter_metadata: If True, it will apply changes to metadata. + :type alter_metadata: bool + :param metadata: If `alter_metadata` is true, \ + metadata is used to reflect table names into + :type metadata: :class:`MetaData` instance + :param engine: When reflecting tables, either engine or metadata must \ + be specified to acquire engine object. + :type engine: :class:`Engine` instance + :returns: :class:`ColumnDelta` instance provides interface for altered attributes to \ + `result_column` through :func:`dict` alike object. + + * :class:`ColumnDelta`.result_column is altered column with new attributes + + * :class:`ColumnDelta`.current_name is current name of column in db + + + """ + + # Column attributes that can be altered + diff_keys = ('name', 'type', 'primary_key', 'nullable', + 'server_onupdate', 'server_default', 'autoincrement') + diffs = dict() + __visit_name__ = 'column' + + def __init__(self, *p, **kw): + self.alter_metadata = kw.pop("alter_metadata", False) + self.meta = kw.pop("metadata", None) + self.engine = kw.pop("engine", None) + + # Things are initialized differently depending on how many column + # parameters are given. Figure out how many and call the appropriate + # method. + if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column): + # At least one column specified + if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column): + # Two columns specified + diffs = self.compare_2_columns(*p, **kw) + else: + # Exactly one column specified + diffs = self.compare_1_column(*p, **kw) + else: + # Zero columns specified + if not len(p) or not isinstance(p[0], basestring): + raise ValueError("First argument must be column name") + diffs = self.compare_parameters(*p, **kw) + + self.apply_diffs(diffs) + + def __repr__(self): + return '' % (self.alter_metadata, + super(ColumnDelta, self).__repr__()) + + def __getitem__(self, key): + if key not in self.keys(): + raise KeyError("No such diff key, available: %s" % self.diffs) + return getattr(self.result_column, key) + + def __setitem__(self, key, value): + if key not in self.keys(): + raise KeyError("No such diff key, available: %s" % self.diffs) + setattr(self.result_column, key, value) + + def __delitem__(self, key): + raise NotImplementedError + + def keys(self): + return self.diffs.keys() + + def compare_parameters(self, current_name, *p, **k): + """Compares Column objects with reflection""" + self.table = k.pop('table') + self.result_column = self._table.c.get(current_name) + if len(p): + k = self._extract_parameters(p, k, self.result_column) + return k + + def compare_1_column(self, col, *p, **k): + """Compares one Column object""" + self.table = k.pop('table', None) + if self.table is None: + self.table = col.table + self.result_column = col + if len(p): + k = self._extract_parameters(p, k, self.result_column) + return k + + def compare_2_columns(self, old_col, new_col, *p, **k): + """Compares two Column objects""" + self.process_column(new_col) + self.table = k.pop('table', None) + # we cannot use bool() on table in SA06 + if self.table is None: + self.table = old_col.table + if self.table is None: + new_col.table + self.result_column = old_col + + # set differences + # leave out some stuff for later comp + for key in (set(self.diff_keys) - set(('type',))): + val = getattr(new_col, key, None) + if getattr(self.result_column, key, None) != val: + k.setdefault(key, val) + + # inspect types + if not self.are_column_types_eq(self.result_column.type, new_col.type): + k.setdefault('type', new_col.type) + + if len(p): + k = self._extract_parameters(p, k, self.result_column) + return k + + def apply_diffs(self, diffs): + """Populate dict and column object with new values""" + self.diffs = diffs + for key in self.diff_keys: + if key in diffs: + setattr(self.result_column, key, diffs[key]) + + self.process_column(self.result_column) + + # create an instance of class type if not yet + if 'type' in diffs and callable(self.result_column.type): + self.result_column.type = self.result_column.type() + + # add column to the table + if self.table is not None and self.alter_metadata: + self.result_column.add_to_table(self.table) + + def are_column_types_eq(self, old_type, new_type): + """Compares two types to be equal""" + ret = old_type.__class__ == new_type.__class__ + + # String length is a special case + if ret and isinstance(new_type, sqlalchemy.types.String): + ret = (getattr(old_type, 'length', None) == \ + getattr(new_type, 'length', None)) + return ret + + def _extract_parameters(self, p, k, column): + """Extracts data from p and modifies diffs""" + p = list(p) + while len(p): + if isinstance(p[0], basestring): + k.setdefault('name', p.pop(0)) + elif isinstance(p[0], sqlalchemy.types.AbstractType): + k.setdefault('type', p.pop(0)) + elif callable(p[0]): + p[0] = p[0]() + else: + break + + if len(p): + new_col = column.copy_fixed() + new_col._init_items(*p) + k = self.compare_2_columns(column, new_col, **k) + return k + + def process_column(self, column): + """Processes default values for column""" + # XXX: this is a snippet from SA processing of positional parameters + if not SQLA_06 and column.args: + toinit = list(column.args) + else: + toinit = list() + + if column.server_default is not None: + if isinstance(column.server_default, sqlalchemy.FetchedValue): + toinit.append(column.server_default) + else: + toinit.append(sqlalchemy.DefaultClause(column.server_default)) + if column.server_onupdate is not None: + if isinstance(column.server_onupdate, FetchedValue): + toinit.append(column.server_default) + else: + toinit.append(sqlalchemy.DefaultClause(column.server_onupdate, + for_update=True)) + if toinit: + column._init_items(*toinit) + + if not SQLA_06: + column.args = [] + + def _get_table(self): + return getattr(self, '_table', None) + + def _set_table(self, table): + if isinstance(table, basestring): + if self.alter_metadata: + if not self.meta: + raise ValueError("metadata must be specified for table" + " reflection when using alter_metadata") + meta = self.meta + if self.engine: + meta.bind = self.engine + else: + if not self.engine and not self.meta: + raise ValueError("engine or metadata must be specified" + " to reflect tables") + if not self.engine: + self.engine = self.meta.bind + meta = sqlalchemy.MetaData(bind=self.engine) + self._table = sqlalchemy.Table(table, meta, autoload=True) + elif isinstance(table, sqlalchemy.Table): + self._table = table + if not self.alter_metadata: + self._table.meta = sqlalchemy.MetaData(bind=self._table.bind) + + def _get_result_column(self): + return getattr(self, '_result_column', None) + + def _set_result_column(self, column): + """Set Column to Table based on alter_metadata evaluation.""" + self.process_column(column) + if not hasattr(self, 'current_name'): + self.current_name = column.name + if self.alter_metadata: + self._result_column = column + else: + self._result_column = column.copy_fixed() + + table = property(_get_table, _set_table) + result_column = property(_get_result_column, _set_result_column) + + +class ChangesetTable(object): + """Changeset extensions to SQLAlchemy tables.""" + + def create_column(self, column, *p, **kw): + """Creates a column. + + The column parameter may be a column definition or the name of + a column in this table. + + API to :meth:`ChangesetColumn.create` + + :param column: Column to be created + :type column: Column instance or string + """ + if not isinstance(column, sqlalchemy.Column): + # It's a column name + column = getattr(self.c, str(column)) + column.create(table=self, *p, **kw) + + def drop_column(self, column, *p, **kw): + """Drop a column, given its name or definition. + + API to :meth:`ChangesetColumn.drop` + + :param column: Column to be droped + :type column: Column instance or string + """ + if not isinstance(column, sqlalchemy.Column): + # It's a column name + try: + column = getattr(self.c, str(column)) + except AttributeError: + # That column isn't part of the table. We don't need + # its entire definition to drop the column, just its + # name, so create a dummy column with the same name. + column = sqlalchemy.Column(str(column), sqlalchemy.Integer()) + column.drop(table=self, *p, **kw) + + def rename(self, name, connection=None, **kwargs): + """Rename this table. + + :param name: New name of the table. + :type name: string + :param alter_metadata: If True, table will be removed from metadata + :type alter_metadata: bool + :param connection: reuse connection istead of creating new one. + :type connection: :class:`sqlalchemy.engine.base.Connection` instance + """ + self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA) + engine = self.bind + self.new_name = name + visitorcallable = get_engine_visitor(engine, 'schemachanger') + run_single_visitor(engine, visitorcallable, self, connection, **kwargs) + + # Fix metadata registration + if self.alter_metadata: + self.name = name + self.deregister() + self._set_parent(self.metadata) + + def _meta_key(self): + return sqlalchemy.schema._get_table_key(self.name, self.schema) + + def deregister(self): + """Remove this table from its metadata""" + key = self._meta_key() + meta = self.metadata + if key in meta.tables: + del meta.tables[key] + + +class ChangesetColumn(object): + """Changeset extensions to SQLAlchemy columns.""" + + def alter(self, *p, **k): + """Makes a call to :func:`alter_column` for the column this + method is called on. + """ + if 'table' not in k: + k['table'] = self.table + if 'engine' not in k: + k['engine'] = k['table'].bind + return alter_column(self, *p, **k) + + def create(self, table=None, index_name=None, unique_name=None, + primary_key_name=None, populate_default=True, connection=None, **kwargs): + """Create this column in the database. + + Assumes the given table exists. ``ALTER TABLE ADD COLUMN``, + for most databases. + + :param table: Table instance to create on. + :param index_name: Creates :class:`ChangesetIndex` on this column. + :param unique_name: Creates :class:\ +`~migrate.changeset.constraint.UniqueConstraint` on this column. + :param primary_key_name: Creates :class:\ +`~migrate.changeset.constraint.PrimaryKeyConstraint` on this column. + :param alter_metadata: If True, column will be added to table object. + :param populate_default: If True, created column will be \ +populated with defaults + :param connection: reuse connection istead of creating new one. + :type table: Table instance + :type index_name: string + :type unique_name: string + :type primary_key_name: string + :type alter_metadata: bool + :type populate_default: bool + :type connection: :class:`sqlalchemy.engine.base.Connection` instance + + :returns: self + """ + self.populate_default = populate_default + self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA) + self.index_name = index_name + self.unique_name = unique_name + self.primary_key_name = primary_key_name + for cons in ('index_name', 'unique_name', 'primary_key_name'): + self._check_sanity_constraints(cons) + + if self.alter_metadata: + self.add_to_table(table) + engine = self.table.bind + visitorcallable = get_engine_visitor(engine, 'columngenerator') + engine._run_visitor(visitorcallable, self, connection, **kwargs) + + # TODO: reuse existing connection + if self.populate_default and self.default is not None: + stmt = table.update().values({self: engine._execute_default(self.default)}) + engine.execute(stmt) + + return self + + def drop(self, table=None, connection=None, **kwargs): + """Drop this column from the database, leaving its table intact. + + ``ALTER TABLE DROP COLUMN``, for most databases. + + :param alter_metadata: If True, column will be removed from table object. + :type alter_metadata: bool + :param connection: reuse connection istead of creating new one. + :type connection: :class:`sqlalchemy.engine.base.Connection` instance + """ + self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA) + if table is not None: + self.table = table + engine = self.table.bind + if self.alter_metadata: + self.remove_from_table(self.table, unset_table=False) + visitorcallable = get_engine_visitor(engine, 'columndropper') + engine._run_visitor(visitorcallable, self, connection, **kwargs) + if self.alter_metadata: + self.table = None + return self + + def add_to_table(self, table): + if table is not None and self.table is None: + self._set_parent(table) + + def _col_name_in_constraint(self, cons, name): + return False + + def remove_from_table(self, table, unset_table=True): + # TODO: remove primary keys, constraints, etc + if unset_table: + self.table = None + + to_drop = set() + for index in table.indexes: + columns = [] + for col in index.columns: + if col.name != self.name: + columns.append(col) + if columns: + index.columns = columns + else: + to_drop.add(index) + table.indexes = table.indexes - to_drop + + to_drop = set() + for cons in table.constraints: + # TODO: deal with other types of constraint + if isinstance(cons, (ForeignKeyConstraint, + UniqueConstraint)): + for col_name in cons.columns: + if not isinstance(col_name, basestring): + col_name = col_name.name + if self.name == col_name: + to_drop.add(cons) + table.constraints = table.constraints - to_drop + + if table.c.contains_column(self): + table.c.remove(self) + + # TODO: this is fixed in 0.6 + def copy_fixed(self, **kw): + """Create a copy of this ``Column``, with all attributes.""" + return sqlalchemy.Column(self.name, self.type, self.default, + key=self.key, + primary_key=self.primary_key, + nullable=self.nullable, + quote=self.quote, + index=self.index, + unique=self.unique, + onupdate=self.onupdate, + autoincrement=self.autoincrement, + server_default=self.server_default, + server_onupdate=self.server_onupdate, + *[c.copy(**kw) for c in self.constraints]) + + def _check_sanity_constraints(self, name): + """Check if constraints names are correct""" + obj = getattr(self, name) + if (getattr(self, name[:-5]) and not obj): + raise InvalidConstraintError("Column.create() accepts index_name," + " primary_key_name and unique_name to generate constraints") + if not isinstance(obj, basestring) and obj is not None: + raise InvalidConstraintError( + "%s argument for column must be constraint name" % name) + + +class ChangesetIndex(object): + """Changeset extensions to SQLAlchemy Indexes.""" + + __visit_name__ = 'index' + + def rename(self, name, connection=None, **kwargs): + """Change the name of an index. + + :param name: New name of the Index. + :type name: string + :param alter_metadata: If True, Index object will be altered. + :type alter_metadata: bool + :param connection: reuse connection istead of creating new one. + :type connection: :class:`sqlalchemy.engine.base.Connection` instance + """ + self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA) + engine = self.table.bind + self.new_name = name + visitorcallable = get_engine_visitor(engine, 'schemachanger') + engine._run_visitor(visitorcallable, self, connection, **kwargs) + if self.alter_metadata: + self.name = name + + +class ChangesetDefaultClause(object): + """Implements comparison between :class:`DefaultClause` instances""" + + def __eq__(self, other): + if isinstance(other, self.__class__): + if self.arg == other.arg: + return True + + def __ne__(self, other): + return not self.__eq__(other) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/exceptions.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/exceptions.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,87 @@ +""" + Provide exception classes for :mod:`migrate` +""" + + +class Error(Exception): + """Error base class.""" + + +class ApiError(Error): + """Base class for API errors.""" + + +class KnownError(ApiError): + """A known error condition.""" + + +class UsageError(ApiError): + """A known error condition where help should be displayed.""" + + +class ControlledSchemaError(Error): + """Base class for controlled schema errors.""" + + +class InvalidVersionError(ControlledSchemaError): + """Invalid version number.""" + + +class DatabaseNotControlledError(ControlledSchemaError): + """Database should be under version control, but it's not.""" + + +class DatabaseAlreadyControlledError(ControlledSchemaError): + """Database shouldn't be under version control, but it is""" + + +class WrongRepositoryError(ControlledSchemaError): + """This database is under version control by another repository.""" + + +class NoSuchTableError(ControlledSchemaError): + """The table does not exist.""" + + +class PathError(Error): + """Base class for path errors.""" + + +class PathNotFoundError(PathError): + """A path with no file was required; found a file.""" + + +class PathFoundError(PathError): + """A path with a file was required; found no file.""" + + +class RepositoryError(Error): + """Base class for repository errors.""" + + +class InvalidRepositoryError(RepositoryError): + """Invalid repository error.""" + + +class ScriptError(Error): + """Base class for script errors.""" + + +class InvalidScriptError(ScriptError): + """Invalid script error.""" + + +class InvalidVersionError(Error): + """Invalid version error.""" + +# migrate.changeset + +class NotSupportedError(Error): + """Not supported error""" + + +class InvalidConstraintError(Error): + """Invalid constraint error""" + +class MigrateDeprecationWarning(DeprecationWarning): + """Warning for deprecated features in Migrate""" diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/__init__.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,5 @@ +""" + This package provides functionality to create and manage + repositories of database schema changesets and to apply these + changesets to databases. +""" diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/api.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/api.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,383 @@ +""" + This module provides an external API to the versioning system. + + .. versionchanged:: 0.6.0 + :func:`migrate.versioning.api.test` and schema diff functions + changed order of positional arguments so all accept `url` and `repository` + as first arguments. + + .. versionchanged:: 0.5.4 + ``--preview_sql`` displays source file when using SQL scripts. + If Python script is used, it runs the action with mocked engine and + returns captured SQL statements. + + .. versionchanged:: 0.5.4 + Deprecated ``--echo`` parameter in favour of new + :func:`migrate.versioning.util.construct_engine` behavior. +""" + +# Dear migrate developers, +# +# please do not comment this module using sphinx syntax because its +# docstrings are presented as user help and most users cannot +# interpret sphinx annotated ReStructuredText. +# +# Thanks, +# Jan Dittberner + +import sys +import inspect +import logging + +from rhodecode.lib.dbmigrate.migrate import exceptions +from rhodecode.lib.dbmigrate.migrate.versioning import repository, schema, version, \ + script as script_ # command name conflict +from rhodecode.lib.dbmigrate.migrate.versioning.util import catch_known_errors, with_engine + + +log = logging.getLogger(__name__) +command_desc = { + 'help': 'displays help on a given command', + 'create': 'create an empty repository at the specified path', + 'script': 'create an empty change Python script', + 'script_sql': 'create empty change SQL scripts for given database', + 'version': 'display the latest version available in a repository', + 'db_version': 'show the current version of the repository under version control', + 'source': 'display the Python code for a particular version in this repository', + 'version_control': 'mark a database as under this repository\'s version control', + 'upgrade': 'upgrade a database to a later version', + 'downgrade': 'downgrade a database to an earlier version', + 'drop_version_control': 'removes version control from a database', + 'manage': 'creates a Python script that runs Migrate with a set of default values', + 'test': 'performs the upgrade and downgrade command on the given database', + 'compare_model_to_db': 'compare MetaData against the current database state', + 'create_model': 'dump the current database as a Python model to stdout', + 'make_update_script_for_model': 'create a script changing the old MetaData to the new (current) MetaData', + 'update_db_from_model': 'modify the database to match the structure of the current MetaData', +} +__all__ = command_desc.keys() + +Repository = repository.Repository +ControlledSchema = schema.ControlledSchema +VerNum = version.VerNum +PythonScript = script_.PythonScript +SqlScript = script_.SqlScript + + +# deprecated +def help(cmd=None, **opts): + """%prog help COMMAND + + Displays help on a given command. + """ + if cmd is None: + raise exceptions.UsageError(None) + try: + func = globals()[cmd] + except: + raise exceptions.UsageError( + "'%s' isn't a valid command. Try 'help COMMAND'" % cmd) + ret = func.__doc__ + if sys.argv[0]: + ret = ret.replace('%prog', sys.argv[0]) + return ret + +@catch_known_errors +def create(repository, name, **opts): + """%prog create REPOSITORY_PATH NAME [--table=TABLE] + + Create an empty repository at the specified path. + + You can specify the version_table to be used; by default, it is + 'migrate_version'. This table is created in all version-controlled + databases. + """ + repo_path = Repository.create(repository, name, **opts) + + +@catch_known_errors +def script(description, repository, **opts): + """%prog script DESCRIPTION REPOSITORY_PATH + + Create an empty change script using the next unused version number + appended with the given description. + + For instance, manage.py script "Add initial tables" creates: + repository/versions/001_Add_initial_tables.py + """ + repo = Repository(repository) + repo.create_script(description, **opts) + + +@catch_known_errors +def script_sql(database, repository, **opts): + """%prog script_sql DATABASE REPOSITORY_PATH + + Create empty change SQL scripts for given DATABASE, where DATABASE + is either specific ('postgres', 'mysql', 'oracle', 'sqlite', etc.) + or generic ('default'). + + For instance, manage.py script_sql postgres creates: + repository/versions/001_postgres_upgrade.sql and + repository/versions/001_postgres_postgres.sql + """ + repo = Repository(repository) + repo.create_script_sql(database, **opts) + + +def version(repository, **opts): + """%prog version REPOSITORY_PATH + + Display the latest version available in a repository. + """ + repo = Repository(repository) + return repo.latest + + +@with_engine +def db_version(url, repository, **opts): + """%prog db_version URL REPOSITORY_PATH + + Show the current version of the repository with the given + connection string, under version control of the specified + repository. + + The url should be any valid SQLAlchemy connection string. + """ + engine = opts.pop('engine') + schema = ControlledSchema(engine, repository) + return schema.version + + +def source(version, dest=None, repository=None, **opts): + """%prog source VERSION [DESTINATION] --repository=REPOSITORY_PATH + + Display the Python code for a particular version in this + repository. Save it to the file at DESTINATION or, if omitted, + send to stdout. + """ + if repository is None: + raise exceptions.UsageError("A repository must be specified") + repo = Repository(repository) + ret = repo.version(version).script().source() + if dest is not None: + dest = open(dest, 'w') + dest.write(ret) + dest.close() + ret = None + return ret + + +def upgrade(url, repository, version=None, **opts): + """%prog upgrade URL REPOSITORY_PATH [VERSION] [--preview_py|--preview_sql] + + Upgrade a database to a later version. + + This runs the upgrade() function defined in your change scripts. + + By default, the database is updated to the latest available + version. You may specify a version instead, if you wish. + + You may preview the Python or SQL code to be executed, rather than + actually executing it, using the appropriate 'preview' option. + """ + err = "Cannot upgrade a database of version %s to version %s. "\ + "Try 'downgrade' instead." + return _migrate(url, repository, version, upgrade=True, err=err, **opts) + + +def downgrade(url, repository, version, **opts): + """%prog downgrade URL REPOSITORY_PATH VERSION [--preview_py|--preview_sql] + + Downgrade a database to an earlier version. + + This is the reverse of upgrade; this runs the downgrade() function + defined in your change scripts. + + You may preview the Python or SQL code to be executed, rather than + actually executing it, using the appropriate 'preview' option. + """ + err = "Cannot downgrade a database of version %s to version %s. "\ + "Try 'upgrade' instead." + return _migrate(url, repository, version, upgrade=False, err=err, **opts) + +@with_engine +def test(url, repository, **opts): + """%prog test URL REPOSITORY_PATH [VERSION] + + Performs the upgrade and downgrade option on the given + database. This is not a real test and may leave the database in a + bad state. You should therefore better run the test on a copy of + your database. + """ + engine = opts.pop('engine') + repos = Repository(repository) + script = repos.version(None).script() + + # Upgrade + log.info("Upgrading...") + script.run(engine, 1) + log.info("done") + + log.info("Downgrading...") + script.run(engine, -1) + log.info("done") + log.info("Success") + + +@with_engine +def version_control(url, repository, version=None, **opts): + """%prog version_control URL REPOSITORY_PATH [VERSION] + + Mark a database as under this repository's version control. + + Once a database is under version control, schema changes should + only be done via change scripts in this repository. + + This creates the table version_table in the database. + + The url should be any valid SQLAlchemy connection string. + + By default, the database begins at version 0 and is assumed to be + empty. If the database is not empty, you may specify a version at + which to begin instead. No attempt is made to verify this + version's correctness - the database schema is expected to be + identical to what it would be if the database were created from + scratch. + """ + engine = opts.pop('engine') + ControlledSchema.create(engine, repository, version) + + +@with_engine +def drop_version_control(url, repository, **opts): + """%prog drop_version_control URL REPOSITORY_PATH + + Removes version control from a database. + """ + engine = opts.pop('engine') + schema = ControlledSchema(engine, repository) + schema.drop() + + +def manage(file, **opts): + """%prog manage FILENAME [VARIABLES...] + + Creates a script that runs Migrate with a set of default values. + + For example:: + + %prog manage manage.py --repository=/path/to/repository \ +--url=sqlite:///project.db + + would create the script manage.py. The following two commands + would then have exactly the same results:: + + python manage.py version + %prog version --repository=/path/to/repository + """ + Repository.create_manage_file(file, **opts) + + +@with_engine +def compare_model_to_db(url, repository, model, **opts): + """%prog compare_model_to_db URL REPOSITORY_PATH MODEL + + Compare the current model (assumed to be a module level variable + of type sqlalchemy.MetaData) against the current database. + + NOTE: This is EXPERIMENTAL. + """ # TODO: get rid of EXPERIMENTAL label + engine = opts.pop('engine') + return ControlledSchema.compare_model_to_db(engine, model, repository) + + +@with_engine +def create_model(url, repository, **opts): + """%prog create_model URL REPOSITORY_PATH [DECLERATIVE=True] + + Dump the current database as a Python model to stdout. + + NOTE: This is EXPERIMENTAL. + """ # TODO: get rid of EXPERIMENTAL label + engine = opts.pop('engine') + declarative = opts.get('declarative', False) + return ControlledSchema.create_model(engine, repository, declarative) + + +@catch_known_errors +@with_engine +def make_update_script_for_model(url, repository, oldmodel, model, **opts): + """%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH + + Create a script changing the old Python model to the new (current) + Python model, sending to stdout. + + NOTE: This is EXPERIMENTAL. + """ # TODO: get rid of EXPERIMENTAL label + engine = opts.pop('engine') + return PythonScript.make_update_script_for_model( + engine, oldmodel, model, repository, **opts) + + +@with_engine +def update_db_from_model(url, repository, model, **opts): + """%prog update_db_from_model URL REPOSITORY_PATH MODEL + + Modify the database to match the structure of the current Python + model. This also sets the db_version number to the latest in the + repository. + + NOTE: This is EXPERIMENTAL. + """ # TODO: get rid of EXPERIMENTAL label + engine = opts.pop('engine') + schema = ControlledSchema(engine, repository) + schema.update_db_from_model(model) + +@with_engine +def _migrate(url, repository, version, upgrade, err, **opts): + engine = opts.pop('engine') + url = str(engine.url) + schema = ControlledSchema(engine, repository) + version = _migrate_version(schema, version, upgrade, err) + + changeset = schema.changeset(version) + for ver, change in changeset: + nextver = ver + changeset.step + log.info('%s -> %s... ', ver, nextver) + + if opts.get('preview_sql'): + if isinstance(change, PythonScript): + log.info(change.preview_sql(url, changeset.step, **opts)) + elif isinstance(change, SqlScript): + log.info(change.source()) + + elif opts.get('preview_py'): + if not isinstance(change, PythonScript): + raise exceptions.UsageError("Python source can be only displayed" + " for python migration files") + source_ver = max(ver, nextver) + module = schema.repository.version(source_ver).script().module + funcname = upgrade and "upgrade" or "downgrade" + func = getattr(module, funcname) + log.info(inspect.getsource(func)) + else: + schema.runchange(ver, change, changeset.step) + log.info('done') + + +def _migrate_version(schema, version, upgrade, err): + if version is None: + return version + # Version is specified: ensure we're upgrading in the right direction + # (current version < target version for upgrading; reverse for down) + version = VerNum(version) + cur = schema.version + if upgrade is not None: + if upgrade: + direction = cur <= version + else: + direction = cur >= version + if not direction: + raise exceptions.KnownError(err % (cur, version)) + return version diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/cfgparse.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/cfgparse.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,27 @@ +""" + Configuration parser module. +""" + +from ConfigParser import ConfigParser + +from rhodecode.lib.dbmigrate.migrate.versioning.config import * +from rhodecode.lib.dbmigrate.migrate.versioning import pathed + + +class Parser(ConfigParser): + """A project configuration file.""" + + def to_dict(self, sections=None): + """It's easier to access config values like dictionaries""" + return self._sections + + +class Config(pathed.Pathed, Parser): + """Configuration class.""" + + def __init__(self, path, *p, **k): + """Confirm the config file exists; read it.""" + self.require_found(path) + pathed.Pathed.__init__(self, path) + Parser.__init__(self, *p, **k) + self.read(path) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/config.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,14 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from sqlalchemy.util import OrderedDict + + +__all__ = ['databases', 'operations'] + +databases = ('sqlite', 'postgres', 'mysql', 'oracle', 'mssql', 'firebird') + +# Map operation names to function names +operations = OrderedDict() +operations['upgrade'] = 'upgrade' +operations['downgrade'] = 'downgrade' diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/genmodel.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/genmodel.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,253 @@ +""" + Code to generate a Python model from a database or differences + between a model and database. + + Some of this is borrowed heavily from the AutoCode project at: + http://code.google.com/p/sqlautocode/ +""" + +import sys +import logging + +import sqlalchemy + +from rhodecode.lib.dbmigrate import migrate +from rhodecode.lib.dbmigrate.migrate import changeset + +log = logging.getLogger(__name__) +HEADER = """ +## File autogenerated by genmodel.py + +from sqlalchemy import * +meta = MetaData() +""" + +DECLARATIVE_HEADER = """ +## File autogenerated by genmodel.py + +from sqlalchemy import * +from sqlalchemy.ext import declarative + +Base = declarative.declarative_base() +""" + + +class ModelGenerator(object): + + def __init__(self, diff, engine, declarative=False): + self.diff = diff + self.engine = engine + self.declarative = declarative + + def column_repr(self, col): + kwarg = [] + if col.key != col.name: + kwarg.append('key') + if col.primary_key: + col.primary_key = True # otherwise it dumps it as 1 + kwarg.append('primary_key') + if not col.nullable: + kwarg.append('nullable') + if col.onupdate: + kwarg.append('onupdate') + if col.default: + if col.primary_key: + # I found that PostgreSQL automatically creates a + # default value for the sequence, but let's not show + # that. + pass + else: + kwarg.append('default') + ks = ', '.join('%s=%r' % (k, getattr(col, k)) for k in kwarg) + + # crs: not sure if this is good idea, but it gets rid of extra + # u'' + name = col.name.encode('utf8') + + type_ = col.type + for cls in col.type.__class__.__mro__: + if cls.__module__ == 'sqlalchemy.types' and \ + not cls.__name__.isupper(): + if cls is not type_.__class__: + type_ = cls() + break + + data = { + 'name': name, + 'type': type_, + 'constraints': ', '.join([repr(cn) for cn in col.constraints]), + 'args': ks and ks or ''} + + if data['constraints']: + if data['args']: + data['args'] = ',' + data['args'] + + if data['constraints'] or data['args']: + data['maybeComma'] = ',' + else: + data['maybeComma'] = '' + + commonStuff = """ %(maybeComma)s %(constraints)s %(args)s)""" % data + commonStuff = commonStuff.strip() + data['commonStuff'] = commonStuff + if self.declarative: + return """%(name)s = Column(%(type)r%(commonStuff)s""" % data + else: + return """Column(%(name)r, %(type)r%(commonStuff)s""" % data + + def getTableDefn(self, table): + out = [] + tableName = table.name + if self.declarative: + out.append("class %(table)s(Base):" % {'table': tableName}) + out.append(" __tablename__ = '%(table)s'" % {'table': tableName}) + for col in table.columns: + out.append(" %s" % self.column_repr(col)) + else: + out.append("%(table)s = Table('%(table)s', meta," % \ + {'table': tableName}) + for col in table.columns: + out.append(" %s," % self.column_repr(col)) + out.append(")") + return out + + def _get_tables(self, missingA=False, missingB=False, modified=False): + to_process = [] + for bool_, names, metadata in ( + (missingA, self.diff.tables_missing_from_A, self.diff.metadataB), + (missingB, self.diff.tables_missing_from_B, self.diff.metadataA), + (modified, self.diff.tables_different, self.diff.metadataA), + ): + if bool_: + for name in names: + yield metadata.tables.get(name) + + def toPython(self): + """Assume database is current and model is empty.""" + out = [] + if self.declarative: + out.append(DECLARATIVE_HEADER) + else: + out.append(HEADER) + out.append("") + for table in self._get_tables(missingA=True): + out.extend(self.getTableDefn(table)) + out.append("") + return '\n'.join(out) + + def toUpgradeDowngradePython(self, indent=' '): + ''' Assume model is most current and database is out-of-date. ''' + decls = ['from rhodecode.lib.dbmigrate.migrate.changeset import schema', + 'meta = MetaData()'] + for table in self._get_tables( + missingA=True, missingB=True, modified=True + ): + decls.extend(self.getTableDefn(table)) + + upgradeCommands, downgradeCommands = [], [] + for tableName in self.diff.tables_missing_from_A: + upgradeCommands.append("%(table)s.drop()" % {'table': tableName}) + downgradeCommands.append("%(table)s.create()" % \ + {'table': tableName}) + for tableName in self.diff.tables_missing_from_B: + upgradeCommands.append("%(table)s.create()" % {'table': tableName}) + downgradeCommands.append("%(table)s.drop()" % {'table': tableName}) + + for tableName in self.diff.tables_different: + dbTable = self.diff.metadataB.tables[tableName] + missingInDatabase, missingInModel, diffDecl = \ + self.diff.colDiffs[tableName] + for col in missingInDatabase: + upgradeCommands.append('%s.columns[%r].create()' % ( + modelTable, col.name)) + downgradeCommands.append('%s.columns[%r].drop()' % ( + modelTable, col.name)) + for col in missingInModel: + upgradeCommands.append('%s.columns[%r].drop()' % ( + modelTable, col.name)) + downgradeCommands.append('%s.columns[%r].create()' % ( + modelTable, col.name)) + for modelCol, databaseCol, modelDecl, databaseDecl in diffDecl: + upgradeCommands.append( + 'assert False, "Can\'t alter columns: %s:%s=>%s"', + modelTable, modelCol.name, databaseCol.name) + downgradeCommands.append( + 'assert False, "Can\'t alter columns: %s:%s=>%s"', + modelTable, modelCol.name, databaseCol.name) + pre_command = ' meta.bind = migrate_engine' + + return ( + '\n'.join(decls), + '\n'.join([pre_command] + ['%s%s' % (indent, line) for line in upgradeCommands]), + '\n'.join([pre_command] + ['%s%s' % (indent, line) for line in downgradeCommands])) + + def _db_can_handle_this_change(self, td): + if (td.columns_missing_from_B + and not td.columns_missing_from_A + and not td.columns_different): + # Even sqlite can handle this. + return True + else: + return not self.engine.url.drivername.startswith('sqlite') + + def applyModel(self): + """Apply model to current database.""" + + meta = sqlalchemy.MetaData(self.engine) + + for table in self._get_tables(missingA=True): + table = table.tometadata(meta) + table.drop() + for table in self._get_tables(missingB=True): + table = table.tometadata(meta) + table.create() + for modelTable in self._get_tables(modified=True): + tableName = modelTable.name + modelTable = modelTable.tometadata(meta) + dbTable = self.diff.metadataB.tables[tableName] + + td = self.diff.tables_different[tableName] + + if self._db_can_handle_this_change(td): + + for col in td.columns_missing_from_B: + modelTable.columns[col].create() + for col in td.columns_missing_from_A: + dbTable.columns[col].drop() + # XXX handle column changes here. + else: + # Sqlite doesn't support drop column, so you have to + # do more: create temp table, copy data to it, drop + # old table, create new table, copy data back. + # + # I wonder if this is guaranteed to be unique? + tempName = '_temp_%s' % modelTable.name + + def getCopyStatement(): + preparer = self.engine.dialect.preparer + commonCols = [] + for modelCol in modelTable.columns: + if modelCol.name in dbTable.columns: + commonCols.append(modelCol.name) + commonColsStr = ', '.join(commonCols) + return 'INSERT INTO %s (%s) SELECT %s FROM %s' % \ + (tableName, commonColsStr, commonColsStr, tempName) + + # Move the data in one transaction, so that we don't + # leave the database in a nasty state. + connection = self.engine.connect() + trans = connection.begin() + try: + connection.execute( + 'CREATE TEMPORARY TABLE %s as SELECT * from %s' % \ + (tempName, modelTable.name)) + # make sure the drop takes place inside our + # transaction with the bind parameter + modelTable.drop(bind=connection) + modelTable.create(bind=connection) + connection.execute(getCopyStatement()) + connection.execute('DROP TABLE %s' % tempName) + trans.commit() + except: + trans.rollback() + raise diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/migrate_repository.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/migrate_repository.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,100 @@ +""" + Script to migrate repository from sqlalchemy <= 0.4.4 to the new + repository schema. This shouldn't use any other migrate modules, so + that it can work in any version. +""" + +import os +import sys +import logging + +log = logging.getLogger(__name__) + + +def usage(): + """Gives usage information.""" + print """Usage: %(prog)s repository-to-migrate + + Upgrade your repository to the new flat format. + + NOTE: You should probably make a backup before running this. + """ % {'prog': sys.argv[0]} + + sys.exit(1) + + +def delete_file(filepath): + """Deletes a file and prints a message.""" + log.info('Deleting file: %s' % filepath) + os.remove(filepath) + + +def move_file(src, tgt): + """Moves a file and prints a message.""" + log.info('Moving file %s to %s' % (src, tgt)) + if os.path.exists(tgt): + raise Exception( + 'Cannot move file %s because target %s already exists' % \ + (src, tgt)) + os.rename(src, tgt) + + +def delete_directory(dirpath): + """Delete a directory and print a message.""" + log.info('Deleting directory: %s' % dirpath) + os.rmdir(dirpath) + + +def migrate_repository(repos): + """Does the actual migration to the new repository format.""" + log.info('Migrating repository at: %s to new format' % repos) + versions = '%s/versions' % repos + dirs = os.listdir(versions) + # Only use int's in list. + numdirs = [int(dirname) for dirname in dirs if dirname.isdigit()] + numdirs.sort() # Sort list. + for dirname in numdirs: + origdir = '%s/%s' % (versions, dirname) + log.info('Working on directory: %s' % origdir) + files = os.listdir(origdir) + files.sort() + for filename in files: + # Delete compiled Python files. + if filename.endswith('.pyc') or filename.endswith('.pyo'): + delete_file('%s/%s' % (origdir, filename)) + + # Delete empty __init__.py files. + origfile = '%s/__init__.py' % origdir + if os.path.exists(origfile) and len(open(origfile).read()) == 0: + delete_file(origfile) + + # Move sql upgrade scripts. + if filename.endswith('.sql'): + version, dbms, operation = filename.split('.', 3)[0:3] + origfile = '%s/%s' % (origdir, filename) + # For instance: 2.postgres.upgrade.sql -> + # 002_postgres_upgrade.sql + tgtfile = '%s/%03d_%s_%s.sql' % ( + versions, int(version), dbms, operation) + move_file(origfile, tgtfile) + + # Move Python upgrade script. + pyfile = '%s.py' % dirname + pyfilepath = '%s/%s' % (origdir, pyfile) + if os.path.exists(pyfilepath): + tgtfile = '%s/%03d.py' % (versions, int(dirname)) + move_file(pyfilepath, tgtfile) + + # Try to remove directory. Will fail if it's not empty. + delete_directory(origdir) + + +def main(): + """Main function to be called when using this script.""" + if len(sys.argv) != 2: + usage() + migrate_repository(sys.argv[1]) + + +if __name__ == '__main__': + main() diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/pathed.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/pathed.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,75 @@ +""" + A path/directory class. +""" + +import os +import shutil +import logging + +from rhodecode.lib.dbmigrate.migrate import exceptions +from rhodecode.lib.dbmigrate.migrate.versioning.config import * +from rhodecode.lib.dbmigrate.migrate.versioning.util import KeyedInstance + + +log = logging.getLogger(__name__) + +class Pathed(KeyedInstance): + """ + A class associated with a path/directory tree. + + Only one instance of this class may exist for a particular file; + __new__ will return an existing instance if possible + """ + parent = None + + @classmethod + def _key(cls, path): + return str(path) + + def __init__(self, path): + self.path = path + if self.__class__.parent is not None: + self._init_parent(path) + + def _init_parent(self, path): + """Try to initialize this object's parent, if it has one""" + parent_path = self.__class__._parent_path(path) + self.parent = self.__class__.parent(parent_path) + log.debug("Getting parent %r:%r" % (self.__class__.parent, parent_path)) + self.parent._init_child(path, self) + + def _init_child(self, child, path): + """Run when a child of this object is initialized. + + Parameters: the child object; the path to this object (its + parent) + """ + + @classmethod + def _parent_path(cls, path): + """ + Fetch the path of this object's parent from this object's path. + """ + # os.path.dirname(), but strip directories like files (like + # unix basename) + # + # Treat directories like files... + if path[-1] == '/': + path = path[:-1] + ret = os.path.dirname(path) + return ret + + @classmethod + def require_notfound(cls, path): + """Ensures a given path does not already exist""" + if os.path.exists(path): + raise exceptions.PathFoundError(path) + + @classmethod + def require_found(cls, path): + """Ensures a given path already exists""" + if not os.path.exists(path): + raise exceptions.PathNotFoundError(path) + + def __str__(self): + return self.path diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/repository.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/repository.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,231 @@ +""" + SQLAlchemy migrate repository management. +""" +import os +import shutil +import string +import logging + +from pkg_resources import resource_filename +from tempita import Template as TempitaTemplate + +from rhodecode.lib.dbmigrate.migrate import exceptions +from rhodecode.lib.dbmigrate.migrate.versioning import version, pathed, cfgparse +from rhodecode.lib.dbmigrate.migrate.versioning.template import Template +from rhodecode.lib.dbmigrate.migrate.versioning.config import * + + +log = logging.getLogger(__name__) + +class Changeset(dict): + """A collection of changes to be applied to a database. + + Changesets are bound to a repository and manage a set of + scripts from that repository. + + Behaves like a dict, for the most part. Keys are ordered based on step value. + """ + + def __init__(self, start, *changes, **k): + """ + Give a start version; step must be explicitly stated. + """ + self.step = k.pop('step', 1) + self.start = version.VerNum(start) + self.end = self.start + for change in changes: + self.add(change) + + def __iter__(self): + return iter(self.items()) + + def keys(self): + """ + In a series of upgrades x -> y, keys are version x. Sorted. + """ + ret = super(Changeset, self).keys() + # Reverse order if downgrading + ret.sort(reverse=(self.step < 1)) + return ret + + def values(self): + return [self[k] for k in self.keys()] + + def items(self): + return zip(self.keys(), self.values()) + + def add(self, change): + """Add new change to changeset""" + key = self.end + self.end += self.step + self[key] = change + + def run(self, *p, **k): + """Run the changeset scripts""" + for version, script in self: + script.run(*p, **k) + + +class Repository(pathed.Pathed): + """A project's change script repository""" + + _config = 'migrate.cfg' + _versions = 'versions' + + def __init__(self, path): + log.debug('Loading repository %s...' % path) + self.verify(path) + super(Repository, self).__init__(path) + self.config = cfgparse.Config(os.path.join(self.path, self._config)) + self.versions = version.Collection(os.path.join(self.path, + self._versions)) + log.debug('Repository %s loaded successfully' % path) + log.debug('Config: %r' % self.config.to_dict()) + + @classmethod + def verify(cls, path): + """ + Ensure the target path is a valid repository. + + :raises: :exc:`InvalidRepositoryError ` + """ + # Ensure the existence of required files + try: + cls.require_found(path) + cls.require_found(os.path.join(path, cls._config)) + cls.require_found(os.path.join(path, cls._versions)) + except exceptions.PathNotFoundError, e: + raise exceptions.InvalidRepositoryError(path) + + @classmethod + def prepare_config(cls, tmpl_dir, name, options=None): + """ + Prepare a project configuration file for a new project. + + :param tmpl_dir: Path to Repository template + :param config_file: Name of the config file in Repository template + :param name: Repository name + :type tmpl_dir: string + :type config_file: string + :type name: string + :returns: Populated config file + """ + if options is None: + options = {} + options.setdefault('version_table', 'migrate_version') + options.setdefault('repository_id', name) + options.setdefault('required_dbs', []) + + tmpl = open(os.path.join(tmpl_dir, cls._config)).read() + ret = TempitaTemplate(tmpl).substitute(options) + + # cleanup + del options['__template_name__'] + + return ret + + @classmethod + def create(cls, path, name, **opts): + """Create a repository at a specified path""" + cls.require_notfound(path) + theme = opts.pop('templates_theme', None) + t_path = opts.pop('templates_path', None) + + # Create repository + tmpl_dir = Template(t_path).get_repository(theme=theme) + shutil.copytree(tmpl_dir, path) + + # Edit config defaults + config_text = cls.prepare_config(tmpl_dir, name, options=opts) + fd = open(os.path.join(path, cls._config), 'w') + fd.write(config_text) + fd.close() + + opts['repository_name'] = name + + # Create a management script + manager = os.path.join(path, 'manage.py') + Repository.create_manage_file(manager, templates_theme=theme, + templates_path=t_path, **opts) + + return cls(path) + + def create_script(self, description, **k): + """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`""" + self.versions.create_new_python_version(description, **k) + + def create_script_sql(self, database, **k): + """API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`""" + self.versions.create_new_sql_version(database, **k) + + @property + def latest(self): + """API to :attr:`migrate.versioning.version.Collection.latest`""" + return self.versions.latest + + @property + def version_table(self): + """Returns version_table name specified in config""" + return self.config.get('db_settings', 'version_table') + + @property + def id(self): + """Returns repository id specified in config""" + return self.config.get('db_settings', 'repository_id') + + def version(self, *p, **k): + """API to :attr:`migrate.versioning.version.Collection.version`""" + return self.versions.version(*p, **k) + + @classmethod + def clear(cls): + # TODO: deletes repo + super(Repository, cls).clear() + version.Collection.clear() + + def changeset(self, database, start, end=None): + """Create a changeset to migrate this database from ver. start to end/latest. + + :param database: name of database to generate changeset + :param start: version to start at + :param end: version to end at (latest if None given) + :type database: string + :type start: int + :type end: int + :returns: :class:`Changeset instance ` + """ + start = version.VerNum(start) + + if end is None: + end = self.latest + else: + end = version.VerNum(end) + + if start <= end: + step = 1 + range_mod = 1 + op = 'upgrade' + else: + step = -1 + range_mod = 0 + op = 'downgrade' + + versions = range(start + range_mod, end + range_mod, step) + changes = [self.version(v).script(database, op) for v in versions] + ret = Changeset(start, step=step, *changes) + return ret + + @classmethod + def create_manage_file(cls, file_, **opts): + """Create a project management script (manage.py) + + :param file_: Destination file to be written + :param opts: Options that are passed to :func:`migrate.versioning.shell.main` + """ + mng_file = Template(opts.pop('templates_path', None))\ + .get_manage(theme=opts.pop('templates_theme', None)) + + tmpl = open(mng_file).read() + fd = open(file_, 'w') + fd.write(TempitaTemplate(tmpl).substitute(opts)) + fd.close() diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/schema.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/schema.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,213 @@ +""" + Database schema version management. +""" +import sys +import logging + +from sqlalchemy import (Table, Column, MetaData, String, Text, Integer, + create_engine) +from sqlalchemy.sql import and_ +from sqlalchemy import exceptions as sa_exceptions +from sqlalchemy.sql import bindparam + +from rhodecode.lib.dbmigrate.migrate import exceptions +from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff +from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository +from rhodecode.lib.dbmigrate.migrate.versioning.util import load_model +from rhodecode.lib.dbmigrate.migrate.versioning.version import VerNum + + +log = logging.getLogger(__name__) + +class ControlledSchema(object): + """A database under version control""" + + def __init__(self, engine, repository): + if isinstance(repository, basestring): + repository = Repository(repository) + self.engine = engine + self.repository = repository + self.meta = MetaData(engine) + self.load() + + def __eq__(self, other): + """Compare two schemas by repositories and versions""" + return (self.repository is other.repository \ + and self.version == other.version) + + def load(self): + """Load controlled schema version info from DB""" + tname = self.repository.version_table + try: + if not hasattr(self, 'table') or self.table is None: + self.table = Table(tname, self.meta, autoload=True) + + result = self.engine.execute(self.table.select( + self.table.c.repository_id == str(self.repository.id))) + + data = list(result)[0] + except: + cls, exc, tb = sys.exc_info() + raise exceptions.DatabaseNotControlledError, exc.__str__(), tb + + self.version = data['version'] + return data + + def drop(self): + """ + Remove version control from a database. + """ + try: + self.table.drop() + except (sa_exceptions.SQLError): + raise exceptions.DatabaseNotControlledError(str(self.table)) + + def changeset(self, version=None): + """API to Changeset creation. + + Uses self.version for start version and engine.name + to get database name. + """ + database = self.engine.name + start_ver = self.version + changeset = self.repository.changeset(database, start_ver, version) + return changeset + + def runchange(self, ver, change, step): + startver = ver + endver = ver + step + # Current database version must be correct! Don't run if corrupt! + if self.version != startver: + raise exceptions.InvalidVersionError("%s is not %s" % \ + (self.version, startver)) + # Run the change + change.run(self.engine, step) + + # Update/refresh database version + self.update_repository_table(startver, endver) + self.load() + + def update_repository_table(self, startver, endver): + """Update version_table with new information""" + update = self.table.update(and_(self.table.c.version == int(startver), + self.table.c.repository_id == str(self.repository.id))) + self.engine.execute(update, version=int(endver)) + + def upgrade(self, version=None): + """ + Upgrade (or downgrade) to a specified version, or latest version. + """ + changeset = self.changeset(version) + for ver, change in changeset: + self.runchange(ver, change, changeset.step) + + def update_db_from_model(self, model): + """ + Modify the database to match the structure of the current Python model. + """ + model = load_model(model) + + diff = schemadiff.getDiffOfModelAgainstDatabase( + model, self.engine, excludeTables=[self.repository.version_table] + ) + genmodel.ModelGenerator(diff,self.engine).applyModel() + + self.update_repository_table(self.version, int(self.repository.latest)) + + self.load() + + @classmethod + def create(cls, engine, repository, version=None): + """ + Declare a database to be under a repository's version control. + + :raises: :exc:`DatabaseAlreadyControlledError` + :returns: :class:`ControlledSchema` + """ + # Confirm that the version # is valid: positive, integer, + # exists in repos + if isinstance(repository, basestring): + repository = Repository(repository) + version = cls._validate_version(repository, version) + table = cls._create_table_version(engine, repository, version) + # TODO: history table + # Load repository information and return + return cls(engine, repository) + + @classmethod + def _validate_version(cls, repository, version): + """ + Ensures this is a valid version number for this repository. + + :raises: :exc:`InvalidVersionError` if invalid + :return: valid version number + """ + if version is None: + version = 0 + try: + version = VerNum(version) # raises valueerror + if version < 0 or version > repository.latest: + raise ValueError() + except ValueError: + raise exceptions.InvalidVersionError(version) + return version + + @classmethod + def _create_table_version(cls, engine, repository, version): + """ + Creates the versioning table in a database. + + :raises: :exc:`DatabaseAlreadyControlledError` + """ + # Create tables + tname = repository.version_table + meta = MetaData(engine) + + table = Table( + tname, meta, + Column('repository_id', String(250), primary_key=True), + Column('repository_path', Text), + Column('version', Integer), ) + + # there can be multiple repositories/schemas in the same db + if not table.exists(): + table.create() + + # test for existing repository_id + s = table.select(table.c.repository_id == bindparam("repository_id")) + result = engine.execute(s, repository_id=repository.id) + if result.fetchone(): + raise exceptions.DatabaseAlreadyControlledError + + # Insert data + engine.execute(table.insert().values( + repository_id=repository.id, + repository_path=repository.path, + version=int(version))) + return table + + @classmethod + def compare_model_to_db(cls, engine, model, repository): + """ + Compare the current model against the current database. + """ + if isinstance(repository, basestring): + repository = Repository(repository) + model = load_model(model) + + diff = schemadiff.getDiffOfModelAgainstDatabase( + model, engine, excludeTables=[repository.version_table]) + return diff + + @classmethod + def create_model(cls, engine, repository, declarative=False): + """ + Dump the current database as a Python model. + """ + if isinstance(repository, basestring): + repository = Repository(repository) + + diff = schemadiff.getDiffOfModelAgainstDatabase( + MetaData(), engine, excludeTables=[repository.version_table] + ) + return genmodel.ModelGenerator(diff, engine, declarative).toPython() diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/schemadiff.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/schemadiff.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,285 @@ +""" + Schema differencing support. +""" + +import logging +import sqlalchemy + +from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_06 +from sqlalchemy.types import Float + +log = logging.getLogger(__name__) + +def getDiffOfModelAgainstDatabase(metadata, engine, excludeTables=None): + """ + Return differences of model against database. + + :return: object which will evaluate to :keyword:`True` if there \ + are differences else :keyword:`False`. + """ + return SchemaDiff(metadata, + sqlalchemy.MetaData(engine, reflect=True), + labelA='model', + labelB='database', + excludeTables=excludeTables) + + +def getDiffOfModelAgainstModel(metadataA, metadataB, excludeTables=None): + """ + Return differences of model against another model. + + :return: object which will evaluate to :keyword:`True` if there \ + are differences else :keyword:`False`. + """ + return SchemaDiff(metadataA, metadataB, excludeTables) + + +class ColDiff(object): + """ + Container for differences in one :class:`~sqlalchemy.schema.Column` + between two :class:`~sqlalchemy.schema.Table` instances, ``A`` + and ``B``. + + .. attribute:: col_A + + The :class:`~sqlalchemy.schema.Column` object for A. + + .. attribute:: col_B + + The :class:`~sqlalchemy.schema.Column` object for B. + + .. attribute:: type_A + + The most generic type of the :class:`~sqlalchemy.schema.Column` + object in A. + + .. attribute:: type_B + + The most generic type of the :class:`~sqlalchemy.schema.Column` + object in A. + + """ + + diff = False + + def __init__(self,col_A,col_B): + self.col_A = col_A + self.col_B = col_B + + self.type_A = col_A.type + self.type_B = col_B.type + + self.affinity_A = self.type_A._type_affinity + self.affinity_B = self.type_B._type_affinity + + if self.affinity_A is not self.affinity_B: + self.diff = True + return + + if isinstance(self.type_A,Float) or isinstance(self.type_B,Float): + if not (isinstance(self.type_A,Float) and isinstance(self.type_B,Float)): + self.diff=True + return + + for attr in ('precision','scale','length'): + A = getattr(self.type_A,attr,None) + B = getattr(self.type_B,attr,None) + if not (A is None or B is None) and A!=B: + self.diff=True + return + + def __nonzero__(self): + return self.diff + +class TableDiff(object): + """ + Container for differences in one :class:`~sqlalchemy.schema.Table` + between two :class:`~sqlalchemy.schema.MetaData` instances, ``A`` + and ``B``. + + .. attribute:: columns_missing_from_A + + A sequence of column names that were found in B but weren't in + A. + + .. attribute:: columns_missing_from_B + + A sequence of column names that were found in A but weren't in + B. + + .. attribute:: columns_different + + A dictionary containing information about columns that were + found to be different. + It maps column names to a :class:`ColDiff` objects describing the + differences found. + """ + __slots__ = ( + 'columns_missing_from_A', + 'columns_missing_from_B', + 'columns_different', + ) + + def __nonzero__(self): + return bool( + self.columns_missing_from_A or + self.columns_missing_from_B or + self.columns_different + ) + +class SchemaDiff(object): + """ + Compute the difference between two :class:`~sqlalchemy.schema.MetaData` + objects. + + The string representation of a :class:`SchemaDiff` will summarise + the changes found between the two + :class:`~sqlalchemy.schema.MetaData` objects. + + The length of a :class:`SchemaDiff` will give the number of + changes found, enabling it to be used much like a boolean in + expressions. + + :param metadataA: + First :class:`~sqlalchemy.schema.MetaData` to compare. + + :param metadataB: + Second :class:`~sqlalchemy.schema.MetaData` to compare. + + :param labelA: + The label to use in messages about the first + :class:`~sqlalchemy.schema.MetaData`. + + :param labelB: + The label to use in messages about the second + :class:`~sqlalchemy.schema.MetaData`. + + :param excludeTables: + A sequence of table names to exclude. + + .. attribute:: tables_missing_from_A + + A sequence of table names that were found in B but weren't in + A. + + .. attribute:: tables_missing_from_B + + A sequence of table names that were found in A but weren't in + B. + + .. attribute:: tables_different + + A dictionary containing information about tables that were found + to be different. + It maps table names to a :class:`TableDiff` objects describing the + differences found. + """ + + def __init__(self, + metadataA, metadataB, + labelA='metadataA', + labelB='metadataB', + excludeTables=None): + + self.metadataA, self.metadataB = metadataA, metadataB + self.labelA, self.labelB = labelA, labelB + self.label_width = max(len(labelA),len(labelB)) + excludeTables = set(excludeTables or []) + + A_table_names = set(metadataA.tables.keys()) + B_table_names = set(metadataB.tables.keys()) + + self.tables_missing_from_A = sorted( + B_table_names - A_table_names - excludeTables + ) + self.tables_missing_from_B = sorted( + A_table_names - B_table_names - excludeTables + ) + + self.tables_different = {} + for table_name in A_table_names.intersection(B_table_names): + + td = TableDiff() + + A_table = metadataA.tables[table_name] + B_table = metadataB.tables[table_name] + + A_column_names = set(A_table.columns.keys()) + B_column_names = set(B_table.columns.keys()) + + td.columns_missing_from_A = sorted( + B_column_names - A_column_names + ) + + td.columns_missing_from_B = sorted( + A_column_names - B_column_names + ) + + td.columns_different = {} + + for col_name in A_column_names.intersection(B_column_names): + + cd = ColDiff( + A_table.columns.get(col_name), + B_table.columns.get(col_name) + ) + + if cd: + td.columns_different[col_name]=cd + + # XXX - index and constraint differences should + # be checked for here + + if td: + self.tables_different[table_name]=td + + def __str__(self): + ''' Summarize differences. ''' + out = [] + column_template =' %%%is: %%r' % self.label_width + + for names,label in ( + (self.tables_missing_from_A,self.labelA), + (self.tables_missing_from_B,self.labelB), + ): + if names: + out.append( + ' tables missing from %s: %s' % ( + label,', '.join(sorted(names)) + ) + ) + + for name,td in sorted(self.tables_different.items()): + out.append( + ' table with differences: %s' % name + ) + for names,label in ( + (td.columns_missing_from_A,self.labelA), + (td.columns_missing_from_B,self.labelB), + ): + if names: + out.append( + ' %s missing these columns: %s' % ( + label,', '.join(sorted(names)) + ) + ) + for name,cd in td.columns_different.items(): + out.append(' column with differences: %s' % name) + out.append(column_template % (self.labelA,cd.col_A)) + out.append(column_template % (self.labelB,cd.col_B)) + + if out: + out.insert(0, 'Schema diffs:') + return '\n'.join(out) + else: + return 'No schema diffs' + + def __len__(self): + """ + Used in bool evaluation, return of 0 means no diffs. + """ + return ( + len(self.tables_missing_from_A) + + len(self.tables_missing_from_B) + + len(self.tables_different) + ) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/script/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/script/__init__.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from rhodecode.lib.dbmigrate.migrate.versioning.script.base import BaseScript +from rhodecode.lib.dbmigrate.migrate.versioning.script.py import PythonScript +from rhodecode.lib.dbmigrate.migrate.versioning.script.sql import SqlScript diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/script/base.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/script/base.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import logging + +from rhodecode.lib.dbmigrate.migrate import exceptions +from rhodecode.lib.dbmigrate.migrate.versioning.config import operations +from rhodecode.lib.dbmigrate.migrate.versioning import pathed + + +log = logging.getLogger(__name__) + +class BaseScript(pathed.Pathed): + """Base class for other types of scripts. + All scripts have the following properties: + + source (script.source()) + The source code of the script + version (script.version()) + The version number of the script + operations (script.operations()) + The operations defined by the script: upgrade(), downgrade() or both. + Returns a tuple of operations. + Can also check for an operation with ex. script.operation(Script.ops.up) + """ # TODO: sphinxfy this and implement it correctly + + def __init__(self, path): + log.debug('Loading script %s...' % path) + self.verify(path) + super(BaseScript, self).__init__(path) + log.debug('Script %s loaded successfully' % path) + + @classmethod + def verify(cls, path): + """Ensure this is a valid script + This version simply ensures the script file's existence + + :raises: :exc:`InvalidScriptError ` + """ + try: + cls.require_found(path) + except: + raise exceptions.InvalidScriptError(path) + + def source(self): + """:returns: source code of the script. + :rtype: string + """ + fd = open(self.path) + ret = fd.read() + fd.close() + return ret + + def run(self, engine): + """Core of each BaseScript subclass. + This method executes the script. + """ + raise NotImplementedError() diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/script/py.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/script/py.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,159 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import shutil +import warnings +import logging +from StringIO import StringIO + +from rhodecode.lib.dbmigrate import migrate +from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff +from rhodecode.lib.dbmigrate.migrate.versioning.config import operations +from rhodecode.lib.dbmigrate.migrate.versioning.template import Template +from rhodecode.lib.dbmigrate.migrate.versioning.script import base +from rhodecode.lib.dbmigrate.migrate.versioning.util import import_path, load_model, with_engine +from rhodecode.lib.dbmigrate.migrate.exceptions import MigrateDeprecationWarning, InvalidScriptError, ScriptError + +log = logging.getLogger(__name__) +__all__ = ['PythonScript'] + + +class PythonScript(base.BaseScript): + """Base for Python scripts""" + + @classmethod + def create(cls, path, **opts): + """Create an empty migration script at specified path + + :returns: :class:`PythonScript instance `""" + cls.require_notfound(path) + + src = Template(opts.pop('templates_path', None)).get_script(theme=opts.pop('templates_theme', None)) + shutil.copy(src, path) + + return cls(path) + + @classmethod + def make_update_script_for_model(cls, engine, oldmodel, + model, repository, **opts): + """Create a migration script based on difference between two SA models. + + :param repository: path to migrate repository + :param oldmodel: dotted.module.name:SAClass or SAClass object + :param model: dotted.module.name:SAClass or SAClass object + :param engine: SQLAlchemy engine + :type repository: string or :class:`Repository instance ` + :type oldmodel: string or Class + :type model: string or Class + :type engine: Engine instance + :returns: Upgrade / Downgrade script + :rtype: string + """ + + if isinstance(repository, basestring): + # oh dear, an import cycle! + from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository + repository = Repository(repository) + + oldmodel = load_model(oldmodel) + model = load_model(model) + + # Compute differences. + diff = schemadiff.getDiffOfModelAgainstModel( + oldmodel, + model, + excludeTables=[repository.version_table]) + # TODO: diff can be False (there is no difference?) + decls, upgradeCommands, downgradeCommands = \ + genmodel.ModelGenerator(diff, engine).toUpgradeDowngradePython() + + # Store differences into file. + src = Template(opts.pop('templates_path', None)).get_script(opts.pop('templates_theme', None)) + f = open(src) + contents = f.read() + f.close() + + # generate source + search = 'def upgrade(migrate_engine):' + contents = contents.replace(search, '\n\n'.join((decls, search)), 1) + if upgradeCommands: + contents = contents.replace(' pass', upgradeCommands, 1) + if downgradeCommands: + contents = contents.replace(' pass', downgradeCommands, 1) + return contents + + @classmethod + def verify_module(cls, path): + """Ensure path is a valid script + + :param path: Script location + :type path: string + :raises: :exc:`InvalidScriptError ` + :returns: Python module + """ + # Try to import and get the upgrade() func + module = import_path(path) + try: + assert callable(module.upgrade) + except Exception, e: + raise InvalidScriptError(path + ': %s' % str(e)) + return module + + def preview_sql(self, url, step, **args): + """Mocks SQLAlchemy Engine to store all executed calls in a string + and runs :meth:`PythonScript.run ` + + :returns: SQL file + """ + buf = StringIO() + args['engine_arg_strategy'] = 'mock' + args['engine_arg_executor'] = lambda s, p = '': buf.write(str(s) + p) + + @with_engine + def go(url, step, **kw): + engine = kw.pop('engine') + self.run(engine, step) + return buf.getvalue() + + return go(url, step, **args) + + def run(self, engine, step): + """Core method of Script file. + Exectues :func:`update` or :func:`downgrade` functions + + :param engine: SQLAlchemy Engine + :param step: Operation to run + :type engine: string + :type step: int + """ + if step > 0: + op = 'upgrade' + elif step < 0: + op = 'downgrade' + else: + raise ScriptError("%d is not a valid step" % step) + + funcname = base.operations[op] + script_func = self._func(funcname) + + try: + script_func(engine) + except TypeError: + warnings.warn("upgrade/downgrade functions must accept engine" + " parameter (since version > 0.5.4)", MigrateDeprecationWarning) + raise + + @property + def module(self): + """Calls :meth:`migrate.versioning.script.py.verify_module` + and returns it. + """ + if not hasattr(self, '_module'): + self._module = self.verify_module(self.path) + return self._module + + def _func(self, funcname): + if not hasattr(self.module, funcname): + msg = "Function '%s' is not defined in this script" + raise ScriptError(msg % funcname) + return getattr(self.module, funcname) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/script/sql.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/script/sql.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import logging +import shutil + +from rhodecode.lib.dbmigrate.migrate.versioning.script import base +from rhodecode.lib.dbmigrate.migrate.versioning.template import Template + + +log = logging.getLogger(__name__) + +class SqlScript(base.BaseScript): + """A file containing plain SQL statements.""" + + @classmethod + def create(cls, path, **opts): + """Create an empty migration script at specified path + + :returns: :class:`SqlScript instance `""" + cls.require_notfound(path) + src = Template(opts.pop('templates_path', None)).get_sql_script(theme=opts.pop('templates_theme', None)) + shutil.copy(src, path) + return cls(path) + + # TODO: why is step parameter even here? + def run(self, engine, step=None, executemany=True): + """Runs SQL script through raw dbapi execute call""" + text = self.source() + # Don't rely on SA's autocommit here + # (SA uses .startswith to check if a commit is needed. What if script + # starts with a comment?) + conn = engine.connect() + try: + trans = conn.begin() + try: + # HACK: SQLite doesn't allow multiple statements through + # its execute() method, but it provides executescript() instead + dbapi = conn.engine.raw_connection() + if executemany and getattr(dbapi, 'executescript', None): + dbapi.executescript(text) + else: + conn.execute(text) + trans.commit() + except: + trans.rollback() + raise + finally: + conn.close() diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/shell.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/shell.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,215 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""The migrate command-line tool.""" + +import sys +import inspect +import logging +from optparse import OptionParser, BadOptionError + +from rhodecode.lib.dbmigrate.migrate import exceptions +from rhodecode.lib.dbmigrate.migrate.versioning import api +from rhodecode.lib.dbmigrate.migrate.versioning.config import * +from rhodecode.lib.dbmigrate.migrate.versioning.util import asbool + + +alias = dict( + s=api.script, + vc=api.version_control, + dbv=api.db_version, + v=api.version, +) + +def alias_setup(): + global alias + for key, val in alias.iteritems(): + setattr(api, key, val) +alias_setup() + + +class PassiveOptionParser(OptionParser): + + def _process_args(self, largs, rargs, values): + """little hack to support all --some_option=value parameters""" + + while rargs: + arg = rargs[0] + if arg == "--": + del rargs[0] + return + elif arg[0:2] == "--": + # if parser does not know about the option + # pass it along (make it anonymous) + try: + opt = arg.split('=', 1)[0] + self._match_long_opt(opt) + except BadOptionError: + largs.append(arg) + del rargs[0] + else: + self._process_long_opt(rargs, values) + elif arg[:1] == "-" and len(arg) > 1: + self._process_short_opts(rargs, values) + elif self.allow_interspersed_args: + largs.append(arg) + del rargs[0] + +def main(argv=None, **kwargs): + """Shell interface to :mod:`migrate.versioning.api`. + + kwargs are default options that can be overriden with passing + --some_option as command line option + + :param disable_logging: Let migrate configure logging + :type disable_logging: bool + """ + if argv is not None: + argv = argv + else: + argv = list(sys.argv[1:]) + commands = list(api.__all__) + commands.sort() + + usage = """%%prog COMMAND ... + + Available commands: + %s + + Enter "%%prog help COMMAND" for information on a particular command. + """ % '\n\t'.join(["%s - %s" % (command.ljust(28), + api.command_desc.get(command)) for command in commands]) + + parser = PassiveOptionParser(usage=usage) + parser.add_option("-d", "--debug", + action="store_true", + dest="debug", + default=False, + help="Shortcut to turn on DEBUG mode for logging") + parser.add_option("-q", "--disable_logging", + action="store_true", + dest="disable_logging", + default=False, + help="Use this option to disable logging configuration") + help_commands = ['help', '-h', '--help'] + HELP = False + + try: + command = argv.pop(0) + if command in help_commands: + HELP = True + command = argv.pop(0) + except IndexError: + parser.print_help() + return + + command_func = getattr(api, command, None) + if command_func is None or command.startswith('_'): + parser.error("Invalid command %s" % command) + + parser.set_usage(inspect.getdoc(command_func)) + f_args, f_varargs, f_kwargs, f_defaults = inspect.getargspec(command_func) + for arg in f_args: + parser.add_option( + "--%s" % arg, + dest=arg, + action='store', + type="string") + + # display help of the current command + if HELP: + parser.print_help() + return + + options, args = parser.parse_args(argv) + + # override kwargs with anonymous parameters + override_kwargs = dict() + for arg in list(args): + if arg.startswith('--'): + args.remove(arg) + if '=' in arg: + opt, value = arg[2:].split('=', 1) + else: + opt = arg[2:] + value = True + override_kwargs[opt] = value + + # override kwargs with options if user is overwriting + for key, value in options.__dict__.iteritems(): + if value is not None: + override_kwargs[key] = value + + # arguments that function accepts without passed kwargs + f_required = list(f_args) + candidates = dict(kwargs) + candidates.update(override_kwargs) + for key, value in candidates.iteritems(): + if key in f_args: + f_required.remove(key) + + # map function arguments to parsed arguments + for arg in args: + try: + kw = f_required.pop(0) + except IndexError: + parser.error("Too many arguments for command %s: %s" % (command, + arg)) + kwargs[kw] = arg + + # apply overrides + kwargs.update(override_kwargs) + + # configure options + for key, value in options.__dict__.iteritems(): + kwargs.setdefault(key, value) + + # configure logging + if not asbool(kwargs.pop('disable_logging', False)): + # filter to log =< INFO into stdout and rest to stderr + class SingleLevelFilter(logging.Filter): + def __init__(self, min=None, max=None): + self.min = min or 0 + self.max = max or 100 + + def filter(self, record): + return self.min <= record.levelno <= self.max + + logger = logging.getLogger() + h1 = logging.StreamHandler(sys.stdout) + f1 = SingleLevelFilter(max=logging.INFO) + h1.addFilter(f1) + h2 = logging.StreamHandler(sys.stderr) + f2 = SingleLevelFilter(min=logging.WARN) + h2.addFilter(f2) + logger.addHandler(h1) + logger.addHandler(h2) + + if options.debug: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + log = logging.getLogger(__name__) + + # check if all args are given + try: + num_defaults = len(f_defaults) + except TypeError: + num_defaults = 0 + f_args_default = f_args[len(f_args) - num_defaults:] + required = list(set(f_required) - set(f_args_default)) + if required: + parser.error("Not enough arguments for command %s: %s not specified" \ + % (command, ', '.join(required))) + + # handle command + try: + ret = command_func(**kwargs) + if ret is not None: + log.info(ret) + except (exceptions.UsageError, exceptions.KnownError), e: + parser.error(e.args[0]) + +if __name__ == "__main__": + main() diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/template.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/template.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import shutil +import sys + +from pkg_resources import resource_filename + +from rhodecode.lib.dbmigrate.migrate.versioning.config import * +from rhodecode.lib.dbmigrate.migrate.versioning import pathed + + +class Collection(pathed.Pathed): + """A collection of templates of a specific type""" + _mask = None + + def get_path(self, file): + return os.path.join(self.path, str(file)) + + +class RepositoryCollection(Collection): + _mask = '%s' + +class ScriptCollection(Collection): + _mask = '%s.py_tmpl' + +class ManageCollection(Collection): + _mask = '%s.py_tmpl' + +class SQLScriptCollection(Collection): + _mask = '%s.py_tmpl' + +class Template(pathed.Pathed): + """Finds the paths/packages of various Migrate templates. + + :param path: Templates are loaded from rhodecode.lib.dbmigrate.migrate package + if `path` is not provided. + """ + pkg = 'rhodecode.lib.dbmigrate.migrate.versioning.templates' + _manage = 'manage.py_tmpl' + + def __new__(cls, path=None): + if path is None: + path = cls._find_path(cls.pkg) + return super(Template, cls).__new__(cls, path) + + def __init__(self, path=None): + if path is None: + path = Template._find_path(self.pkg) + super(Template, self).__init__(path) + self.repository = RepositoryCollection(os.path.join(path, 'repository')) + self.script = ScriptCollection(os.path.join(path, 'script')) + self.manage = ManageCollection(os.path.join(path, 'manage')) + self.sql_script = SQLScriptCollection(os.path.join(path, 'sql_script')) + + @classmethod + def _find_path(cls, pkg): + """Returns absolute path to dotted python package.""" + tmp_pkg = pkg.rsplit('.', 1) + + if len(tmp_pkg) != 1: + return resource_filename(tmp_pkg[0], tmp_pkg[1]) + else: + return resource_filename(tmp_pkg[0], '') + + def _get_item(self, collection, theme=None): + """Locates and returns collection. + + :param collection: name of collection to locate + :param type_: type of subfolder in collection (defaults to "_default") + :returns: (package, source) + :rtype: str, str + """ + item = getattr(self, collection) + theme_mask = getattr(item, '_mask') + theme = theme_mask % (theme or 'default') + return item.get_path(theme) + + def get_repository(self, *a, **kw): + """Calls self._get_item('repository', *a, **kw)""" + return self._get_item('repository', *a, **kw) + + def get_script(self, *a, **kw): + """Calls self._get_item('script', *a, **kw)""" + return self._get_item('script', *a, **kw) + + def get_sql_script(self, *a, **kw): + """Calls self._get_item('sql_script', *a, **kw)""" + return self._get_item('sql_script', *a, **kw) + + def get_manage(self, *a, **kw): + """Calls self._get_item('manage', *a, **kw)""" + return self._get_item('manage', *a, **kw) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/__init__.py diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/manage.py_tmpl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/templates/manage.py_tmpl Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,5 @@ +#!/usr/bin/env python +from migrate.versioning.shell import main + +if __name__ == '__main__': + main(%(defaults)s) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/manage/default.py_tmpl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/templates/manage/default.py_tmpl Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,10 @@ +#!/usr/bin/env python +from migrate.versioning.shell import main + +{{py: +_vars = locals().copy() +del _vars['__template_name__'] +_vars.pop('repository_name', None) +defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()]) +}} +main({{ defaults }}) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/manage/pylons.py_tmpl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/templates/manage/pylons.py_tmpl Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,29 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +import sys + +from sqlalchemy import engine_from_config +from paste.deploy.loadwsgi import ConfigLoader + +from migrate.versioning.shell import main +from {{ locals().pop('repository_name') }}.model import migrations + + +if '-c' in sys.argv: + pos = sys.argv.index('-c') + conf_path = sys.argv[pos + 1] + del sys.argv[pos:pos + 2] +else: + conf_path = 'development.ini' + +{{py: +_vars = locals().copy() +del _vars['__template_name__'] +defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()]) +}} + +conf_dict = ConfigLoader(conf_path).parser._sections['app:main'] + +# migrate supports passing url as an existing Engine instance (since 0.6.0) +# usage: migrate -c path/to/config.ini COMMANDS +main(url=engine_from_config(conf_dict), repository=migrations.__path__[0],{{ defaults }}) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/repository/__init__.py diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/repository/default/README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/templates/repository/default/README Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,4 @@ +This is a database migration repository. + +More information at +http://code.google.com/p/sqlalchemy-migrate/ diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/repository/default/__init__.py diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/repository/default/migrate.cfg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/templates/repository/default/migrate.cfg Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,20 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id={{ locals().pop('repository_id') }} + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table={{ locals().pop('version_table') }} + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs={{ locals().pop('required_dbs') }} diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/repository/default/versions/__init__.py diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/repository/pylons/README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/templates/repository/pylons/README Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,4 @@ +This is a database migration repository. + +More information at +http://code.google.com/p/sqlalchemy-migrate/ diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/repository/pylons/__init__.py diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/repository/pylons/migrate.cfg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/templates/repository/pylons/migrate.cfg Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,20 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id={{ locals().pop('repository_id') }} + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table={{ locals().pop('version_table') }} + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs={{ locals().pop('required_dbs') }} diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/repository/pylons/versions/__init__.py diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/script/__init__.py diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/script/default.py_tmpl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/templates/script/default.py_tmpl Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,11 @@ +from sqlalchemy import * +from migrate import * + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; bind migrate_engine + # to your metadata + pass + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + pass diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/script/pylons.py_tmpl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/templates/script/pylons.py_tmpl Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,11 @@ +from sqlalchemy import * +from migrate import * + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; bind migrate_engine + # to your metadata + pass + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + pass diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/sql_script/default.py_tmpl diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/templates/sql_script/pylons.py_tmpl diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/util/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/util/__init__.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,179 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""".. currentmodule:: migrate.versioning.util""" + +import warnings +import logging +from decorator import decorator +from pkg_resources import EntryPoint + +from sqlalchemy import create_engine +from sqlalchemy.engine import Engine +from sqlalchemy.pool import StaticPool + +from rhodecode.lib.dbmigrate.migrate import exceptions +from rhodecode.lib.dbmigrate.migrate.versioning.util.keyedinstance import KeyedInstance +from rhodecode.lib.dbmigrate.migrate.versioning.util.importpath import import_path + + +log = logging.getLogger(__name__) + +def load_model(dotted_name): + """Import module and use module-level variable". + + :param dotted_name: path to model in form of string: ``some.python.module:Class`` + + .. versionchanged:: 0.5.4 + + """ + if isinstance(dotted_name, basestring): + if ':' not in dotted_name: + # backwards compatibility + warnings.warn('model should be in form of module.model:User ' + 'and not module.model.User', exceptions.MigrateDeprecationWarning) + dotted_name = ':'.join(dotted_name.rsplit('.', 1)) + return EntryPoint.parse('x=%s' % dotted_name).load(False) + else: + # Assume it's already loaded. + return dotted_name + +def asbool(obj): + """Do everything to use object as bool""" + if isinstance(obj, basestring): + obj = obj.strip().lower() + if obj in ['true', 'yes', 'on', 'y', 't', '1']: + return True + elif obj in ['false', 'no', 'off', 'n', 'f', '0']: + return False + else: + raise ValueError("String is not true/false: %r" % obj) + if obj in (True, False): + return bool(obj) + else: + raise ValueError("String is not true/false: %r" % obj) + +def guess_obj_type(obj): + """Do everything to guess object type from string + + Tries to convert to `int`, `bool` and finally returns if not succeded. + + .. versionadded: 0.5.4 + """ + + result = None + + try: + result = int(obj) + except: + pass + + if result is None: + try: + result = asbool(obj) + except: + pass + + if result is not None: + return result + else: + return obj + +@decorator +def catch_known_errors(f, *a, **kw): + """Decorator that catches known api errors + + .. versionadded: 0.5.4 + """ + + try: + return f(*a, **kw) + except exceptions.PathFoundError, e: + raise exceptions.KnownError("The path %s already exists" % e.args[0]) + +def construct_engine(engine, **opts): + """.. versionadded:: 0.5.4 + + Constructs and returns SQLAlchemy engine. + + Currently, there are 2 ways to pass create_engine options to :mod:`migrate.versioning.api` functions: + + :param engine: connection string or a existing engine + :param engine_dict: python dictionary of options to pass to `create_engine` + :param engine_arg_*: keyword parameters to pass to `create_engine` (evaluated with :func:`migrate.versioning.util.guess_obj_type`) + :type engine_dict: dict + :type engine: string or Engine instance + :type engine_arg_*: string + :returns: SQLAlchemy Engine + + .. note:: + + keyword parameters override ``engine_dict`` values. + + """ + if isinstance(engine, Engine): + return engine + elif not isinstance(engine, basestring): + raise ValueError("you need to pass either an existing engine or a database uri") + + # get options for create_engine + if opts.get('engine_dict') and isinstance(opts['engine_dict'], dict): + kwargs = opts['engine_dict'] + else: + kwargs = dict() + + # DEPRECATED: handle echo the old way + echo = asbool(opts.get('echo', False)) + if echo: + warnings.warn('echo=True parameter is deprecated, pass ' + 'engine_arg_echo=True or engine_dict={"echo": True}', + exceptions.MigrateDeprecationWarning) + kwargs['echo'] = echo + + # parse keyword arguments + for key, value in opts.iteritems(): + if key.startswith('engine_arg_'): + kwargs[key[11:]] = guess_obj_type(value) + + log.debug('Constructing engine') + # TODO: return create_engine(engine, poolclass=StaticPool, **kwargs) + # seems like 0.5.x branch does not work with engine.dispose and staticpool + return create_engine(engine, **kwargs) + +@decorator +def with_engine(f, *a, **kw): + """Decorator for :mod:`migrate.versioning.api` functions + to safely close resources after function usage. + + Passes engine parameters to :func:`construct_engine` and + resulting parameter is available as kw['engine']. + + Engine is disposed after wrapped function is executed. + + .. versionadded: 0.6.0 + """ + url = a[0] + engine = construct_engine(url, **kw) + + try: + kw['engine'] = engine + return f(*a, **kw) + finally: + if isinstance(engine, Engine): + log.debug('Disposing SQLAlchemy engine %s', engine) + engine.dispose() + + +class Memoize: + """Memoize(fn) - an instance which acts like fn but memoizes its arguments + Will only work on functions with non-mutable arguments + + ActiveState Code 52201 + """ + def __init__(self, fn): + self.fn = fn + self.memo = {} + + def __call__(self, *args): + if not self.memo.has_key(args): + self.memo[args] = self.fn(*args) + return self.memo[args] diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/util/importpath.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/util/importpath.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,16 @@ +import os +import sys + +def import_path(fullpath): + """ Import a file with full path specification. Allows one to + import from anywhere, something __import__ does not do. + """ + # http://zephyrfalcon.org/weblog/arch_d7_2002_08_31.html + path, filename = os.path.split(fullpath) + filename, ext = os.path.splitext(filename) + sys.path.append(path) + module = __import__(filename) + reload(module) # Might be out of date during tests + del sys.path[-1] + return module + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/util/keyedinstance.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/util/keyedinstance.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +class KeyedInstance(object): + """A class whose instances have a unique identifier of some sort + No two instances with the same unique ID should exist - if we try to create + a second instance, the first should be returned. + """ + + _instances = dict() + + def __new__(cls, *p, **k): + instances = cls._instances + clskey = str(cls) + if clskey not in instances: + instances[clskey] = dict() + instances = instances[clskey] + + key = cls._key(*p, **k) + if key not in instances: + instances[key] = super(KeyedInstance, cls).__new__(cls) + return instances[key] + + @classmethod + def _key(cls, *p, **k): + """Given a unique identifier, return a dictionary key + This should be overridden by child classes, to specify which parameters + should determine an object's uniqueness + """ + raise NotImplementedError() + + @classmethod + def clear(cls): + # Allow cls.clear() as well as uniqueInstance.clear(cls) + if str(cls) in cls._instances: + del cls._instances[str(cls)] diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/migrate/versioning/version.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/version.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,215 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import re +import shutil +import logging + +from rhodecode.lib.dbmigrate.migrate import exceptions +from rhodecode.lib.dbmigrate.migrate.versioning import pathed, script + + +log = logging.getLogger(__name__) + +class VerNum(object): + """A version number that behaves like a string and int at the same time""" + + _instances = dict() + + def __new__(cls, value): + val = str(value) + if val not in cls._instances: + cls._instances[val] = super(VerNum, cls).__new__(cls) + ret = cls._instances[val] + return ret + + def __init__(self,value): + self.value = str(int(value)) + if self < 0: + raise ValueError("Version number cannot be negative") + + def __add__(self, value): + ret = int(self) + int(value) + return VerNum(ret) + + def __sub__(self, value): + return self + (int(value) * -1) + + def __cmp__(self, value): + return int(self) - int(value) + + def __repr__(self): + return "" % self.value + + def __str__(self): + return str(self.value) + + def __int__(self): + return int(self.value) + + +class Collection(pathed.Pathed): + """A collection of versioning scripts in a repository""" + + FILENAME_WITH_VERSION = re.compile(r'^(\d{3,}).*') + + def __init__(self, path): + """Collect current version scripts in repository + and store them in self.versions + """ + super(Collection, self).__init__(path) + + # Create temporary list of files, allowing skipped version numbers. + files = os.listdir(path) + if '1' in files: + # deprecation + raise Exception('It looks like you have a repository in the old ' + 'format (with directories for each version). ' + 'Please convert repository before proceeding.') + + tempVersions = dict() + for filename in files: + match = self.FILENAME_WITH_VERSION.match(filename) + if match: + num = int(match.group(1)) + tempVersions.setdefault(num, []).append(filename) + else: + pass # Must be a helper file or something, let's ignore it. + + # Create the versions member where the keys + # are VerNum's and the values are Version's. + self.versions = dict() + for num, files in tempVersions.items(): + self.versions[VerNum(num)] = Version(num, path, files) + + @property + def latest(self): + """:returns: Latest version in Collection""" + return max([VerNum(0)] + self.versions.keys()) + + def create_new_python_version(self, description, **k): + """Create Python files for new version""" + ver = self.latest + 1 + extra = str_to_filename(description) + + if extra: + if extra == '_': + extra = '' + elif not extra.startswith('_'): + extra = '_%s' % extra + + filename = '%03d%s.py' % (ver, extra) + filepath = self._version_path(filename) + + script.PythonScript.create(filepath, **k) + self.versions[ver] = Version(ver, self.path, [filename]) + + def create_new_sql_version(self, database, **k): + """Create SQL files for new version""" + ver = self.latest + 1 + self.versions[ver] = Version(ver, self.path, []) + + # Create new files. + for op in ('upgrade', 'downgrade'): + filename = '%03d_%s_%s.sql' % (ver, database, op) + filepath = self._version_path(filename) + script.SqlScript.create(filepath, **k) + self.versions[ver].add_script(filepath) + + def version(self, vernum=None): + """Returns latest Version if vernum is not given. + Otherwise, returns wanted version""" + if vernum is None: + vernum = self.latest + return self.versions[VerNum(vernum)] + + @classmethod + def clear(cls): + super(Collection, cls).clear() + + def _version_path(self, ver): + """Returns path of file in versions repository""" + return os.path.join(self.path, str(ver)) + + +class Version(object): + """A single version in a collection + :param vernum: Version Number + :param path: Path to script files + :param filelist: List of scripts + :type vernum: int, VerNum + :type path: string + :type filelist: list + """ + + def __init__(self, vernum, path, filelist): + self.version = VerNum(vernum) + + # Collect scripts in this folder + self.sql = dict() + self.python = None + + for script in filelist: + self.add_script(os.path.join(path, script)) + + def script(self, database=None, operation=None): + """Returns SQL or Python Script""" + for db in (database, 'default'): + # Try to return a .sql script first + try: + return self.sql[db][operation] + except KeyError: + continue # No .sql script exists + + # TODO: maybe add force Python parameter? + ret = self.python + + assert ret is not None, \ + "There is no script for %d version" % self.version + return ret + + def add_script(self, path): + """Add script to Collection/Version""" + if path.endswith(Extensions.py): + self._add_script_py(path) + elif path.endswith(Extensions.sql): + self._add_script_sql(path) + + SQL_FILENAME = re.compile(r'^(\d+)_([^_]+)_([^_]+).sql') + + def _add_script_sql(self, path): + basename = os.path.basename(path) + match = self.SQL_FILENAME.match(basename) + + if match: + version, dbms, op = match.group(1), match.group(2), match.group(3) + else: + raise exceptions.ScriptError( + "Invalid SQL script name %s " % basename + \ + "(needs to be ###_database_operation.sql)") + + # File the script into a dictionary + self.sql.setdefault(dbms, {})[op] = script.SqlScript(path) + + def _add_script_py(self, path): + if self.python is not None: + raise exceptions.ScriptError('You can only have one Python script ' + 'per version, but you have: %s and %s' % (self.python, path)) + self.python = script.PythonScript(path) + + +class Extensions: + """A namespace for file extensions""" + py = 'py' + sql = 'sql' + +def str_to_filename(s): + """Replaces spaces, (double and single) quotes + and double underscores to underscores + """ + + s = s.replace(' ', '_').replace('"', '_').replace("'", '_').replace(".", "_") + while '__' in s: + s = s.replace('__', '_') + return s diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/versions/001_initial_release.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/versions/001_initial_release.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,237 @@ +#============================================================================== +# DB INITIAL MODEL +#============================================================================== +import logging +import datetime + +from sqlalchemy import * +from sqlalchemy.exc import DatabaseError +from sqlalchemy.orm import relation, backref, class_mapper +from sqlalchemy.orm.session import Session +from rhodecode.model.meta import Base + +from rhodecode.lib.dbmigrate.migrate import * + +log = logging.getLogger(__name__) + +class BaseModel(object): + + @classmethod + def _get_keys(cls): + """return column names for this model """ + return class_mapper(cls).c.keys() + + def get_dict(self): + """return dict with keys and values corresponding + to this model data """ + + d = {} + for k in self._get_keys(): + d[k] = getattr(self, k) + return d + + def get_appstruct(self): + """return list with keys and values tupples corresponding + to this model data """ + + l = [] + for k in self._get_keys(): + l.append((k, getattr(self, k),)) + return l + + def populate_obj(self, populate_dict): + """populate model with data from given populate_dict""" + + for k in self._get_keys(): + if k in populate_dict: + setattr(self, k, populate_dict[k]) + +class RhodeCodeSettings(Base, BaseModel): + __tablename__ = 'rhodecode_settings' + __table_args__ = (UniqueConstraint('app_settings_name'), {'useexisting':True}) + app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + app_settings_name = Column("app_settings_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + app_settings_value = Column("app_settings_value", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + + def __init__(self, k, v): + self.app_settings_name = k + self.app_settings_value = v + + def __repr__(self): + return "" % (self.app_settings_name, + self.app_settings_value) + +class RhodeCodeUi(Base, BaseModel): + __tablename__ = 'rhodecode_ui' + __table_args__ = {'useexisting':True} + ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + ui_section = Column("ui_section", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + ui_key = Column("ui_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + ui_value = Column("ui_value", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True) + + +class User(Base, BaseModel): + __tablename__ = 'users' + __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'useexisting':True}) + user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + username = Column("username", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + password = Column("password", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + active = Column("active", Boolean(), nullable=True, unique=None, default=None) + admin = Column("admin", Boolean(), nullable=True, unique=None, default=False) + name = Column("name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + lastname = Column("lastname", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + email = Column("email", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None) + is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False) + + user_log = relation('UserLog', cascade='all') + user_perms = relation('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all') + + repositories = relation('Repository') + user_followers = relation('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all') + + @property + def full_contact(self): + return '%s %s <%s>' % (self.name, self.lastname, self.email) + + def __repr__(self): + return "" % (self.user_id, self.username) + + def update_lastlogin(self): + """Update user lastlogin""" + + try: + session = Session.object_session(self) + self.last_login = datetime.datetime.now() + session.add(self) + session.commit() + log.debug('updated user %s lastlogin', self.username) + except (DatabaseError,): + session.rollback() + + +class UserLog(Base, BaseModel): + __tablename__ = 'user_logs' + __table_args__ = {'useexisting':True} + user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None) + 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) + repository_name = Column("repository_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + user_ip = Column("user_ip", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + action = Column("action", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None) + + user = relation('User') + repository = relation('Repository') + +class Repository(Base, BaseModel): + __tablename__ = 'repositories' + __table_args__ = (UniqueConstraint('repo_name'), {'useexisting':True},) + repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + repo_name = Column("repo_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None) + repo_type = Column("repo_type", String(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None) + user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=False, default=None) + private = Column("private", Boolean(), nullable=True, unique=None, default=None) + enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True) + description = Column("description", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + fork_id = Column("fork_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=False, default=None) + + user = relation('User') + fork = relation('Repository', remote_side=repo_id) + repo_to_perm = relation('RepoToPerm', cascade='all') + stats = relation('Statistics', cascade='all', uselist=False) + + repo_followers = relation('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all') + + + def __repr__(self): + return "" % (self.repo_id, self.repo_name) + +class Permission(Base, BaseModel): + __tablename__ = 'permissions' + __table_args__ = {'useexisting':True} + permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + permission_name = Column("permission_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + permission_longname = Column("permission_longname", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + + def __repr__(self): + return "" % (self.permission_id, self.permission_name) + +class RepoToPerm(Base, BaseModel): + __tablename__ = 'repo_to_perm' + __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'useexisting':True}) + repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=None, default=None) + + user = relation('User') + permission = relation('Permission') + repository = relation('Repository') + +class UserToPerm(Base, BaseModel): + __tablename__ = 'user_to_perm' + __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'useexisting':True}) + user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None) + + user = relation('User') + permission = relation('Permission') + +class Statistics(Base, BaseModel): + __tablename__ = 'statistics' + __table_args__ = (UniqueConstraint('repository_id'), {'useexisting':True}) + stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + repository_id = Column("repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=True, default=None) + stat_on_revision = Column("stat_on_revision", Integer(), nullable=False) + commit_activity = Column("commit_activity", LargeBinary(), nullable=False)#JSON data + commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data + languages = Column("languages", LargeBinary(), nullable=False)#JSON data + + repository = relation('Repository', single_parent=True) + +class UserFollowing(Base, BaseModel): + __tablename__ = 'user_followings' + __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'), + UniqueConstraint('user_id', 'follows_user_id') + , {'useexisting':True}) + + user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None) + follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=None, default=None) + follows_user_id = Column("follows_user_id", Integer(), ForeignKey(u'users.user_id'), nullable=True, unique=None, default=None) + + user = relation('User', primaryjoin='User.user_id==UserFollowing.user_id') + + follows_user = relation('User', primaryjoin='User.user_id==UserFollowing.follows_user_id') + follows_repository = relation('Repository') + + +class CacheInvalidation(Base, BaseModel): + __tablename__ = 'cache_invalidation' + __table_args__ = (UniqueConstraint('cache_key'), {'useexisting':True}) + cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + cache_key = Column("cache_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + cache_args = Column("cache_args", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False) + + + def __init__(self, cache_key, cache_args=''): + self.cache_key = cache_key + self.cache_args = cache_args + self.cache_active = False + + def __repr__(self): + return "" % (self.cache_id, self.cache_key) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; bind migrate_engine + # to your metadata + Base.metadata.create_all(bind=migrate_engine, checkfirst=False) + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + Base.metadata.drop_all(bind=migrate_engine, checkfirst=False) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/versions/002_version_1_1_0.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/versions/002_version_1_1_0.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,127 @@ +import logging +import datetime + +from sqlalchemy import * +from sqlalchemy.exc import DatabaseError +from sqlalchemy.orm import relation, backref, class_mapper +from sqlalchemy.orm.session import Session +from rhodecode.model.meta import Base +from rhodecode.model.db import BaseModel + +from rhodecode.lib.dbmigrate.migrate import * + +log = logging.getLogger(__name__) + +def upgrade(migrate_engine): + """ Upgrade operations go here. + Don't create your own engine; bind migrate_engine to your metadata + """ + + #========================================================================== + # Upgrade of `users` table + #========================================================================== + tblname = 'users' + tbl = Table(tblname, MetaData(bind=migrate_engine), autoload=True, + autoload_with=migrate_engine) + + #ADD is_ldap column + is_ldap = Column("is_ldap", Boolean(), nullable=True, + unique=None, default=False) + is_ldap.create(tbl, populate_default=True) + is_ldap.alter(nullable=False) + + #========================================================================== + # Upgrade of `user_logs` table + #========================================================================== + + tblname = 'users' + tbl = Table(tblname, MetaData(bind=migrate_engine), autoload=True, + autoload_with=migrate_engine) + + #ADD revision column + revision = Column('revision', TEXT(length=None, convert_unicode=False, + assert_unicode=None), + nullable=True, unique=None, default=None) + revision.create(tbl) + + + + #========================================================================== + # Upgrade of `repositories` table + #========================================================================== + tblname = 'repositories' + tbl = Table(tblname, MetaData(bind=migrate_engine), autoload=True, + autoload_with=migrate_engine) + + #ADD repo_type column# + repo_type = Column("repo_type", String(length=None, convert_unicode=False, + assert_unicode=None), + nullable=True, unique=False, default='hg') + + repo_type.create(tbl, populate_default=True) + #repo_type.alter(nullable=False) + + #ADD statistics column# + enable_statistics = Column("statistics", Boolean(), nullable=True, + unique=None, default=True) + enable_statistics.create(tbl) + + #========================================================================== + # Add table `user_followings` + #========================================================================== + tblname = 'user_followings' + + class UserFollowing(Base, BaseModel): + __tablename__ = 'user_followings' + __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'), + UniqueConstraint('user_id', 'follows_user_id') + , {'useexisting':True}) + + user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None) + follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=None, default=None) + follows_user_id = Column("follows_user_id", Integer(), ForeignKey(u'users.user_id'), nullable=True, unique=None, default=None) + + user = relation('User', primaryjoin='User.user_id==UserFollowing.user_id') + + follows_user = relation('User', primaryjoin='User.user_id==UserFollowing.follows_user_id') + follows_repository = relation('Repository') + + Base.metadata.tables[tblname].create(migrate_engine) + + #========================================================================== + # Add table `cache_invalidation` + #========================================================================== + tblname = 'cache_invalidation' + + class CacheInvalidation(Base, BaseModel): + __tablename__ = 'cache_invalidation' + __table_args__ = (UniqueConstraint('cache_key'), {'useexisting':True}) + cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + cache_key = Column("cache_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + cache_args = Column("cache_args", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False) + + + def __init__(self, cache_key, cache_args=''): + self.cache_key = cache_key + self.cache_args = cache_args + self.cache_active = False + + def __repr__(self): + return "" % (self.cache_id, self.cache_key) + + Base.metadata.tables[tblname].create(migrate_engine) + + return + + + + + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/dbmigrate/versions/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/versions/__init__.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.lib.dbmigrate.versions.__init__ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Package containing new versions of database models + + :created_on: Dec 11, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/exceptions.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/exceptions.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# encoding: utf-8 +# Custom Exceptions modules +# Copyright (C) 2009-2010 Marcin Kuzminski +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +""" +Created on Nov 17, 2010 +Custom Exceptions modules +@author: marcink +""" + +class LdapUsernameError(Exception):pass +class LdapPasswordError(Exception):pass +class LdapConnectionError(Exception):pass +class LdapImportError(Exception):pass + +class DefaultUserException(Exception):pass +class UserOwnsReposException(Exception):pass diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/lib/helpers.py Sat Dec 18 14:45:58 2010 +0100 @@ -3,6 +3,8 @@ Consists of functions to typically be used within templates, but also available to Controllers. This module is available to both as 'h'. """ +import random +import hashlib from pygments.formatters import HtmlFormatter from pygments import highlight as code_highlight from pylons import url, app_globals as g @@ -23,6 +25,36 @@ from webhelpers.text import chop_at, collapse, convert_accented_entities, \ convert_misc_entities, lchop, plural, rchop, remove_formatting, \ replace_whitespace, urlify, truncate, wrap_paragraphs +from webhelpers.date import time_ago_in_words + +from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \ + convert_boolean_attrs, NotGiven + +def _reset(name, value=None, id=NotGiven, type="reset", **attrs): + _set_input_attrs(attrs, type, name, value) + _set_id_attr(attrs, id, name) + convert_boolean_attrs(attrs, ["disabled"]) + return HTML.input(**attrs) + +reset = _reset + + +def get_token(): + """Return the current authentication token, creating one if one doesn't + already exist. + """ + token_key = "_authentication_token" + from pylons import session + if not token_key in session: + try: + token = hashlib.sha1(str(random.getrandbits(128))).hexdigest() + except AttributeError: # Python < 2.4 + token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest() + session[token_key] = token + if hasattr(session, 'save'): + session.save() + return session[token_key] + #Custom helpers here :) class _Link(object): @@ -93,7 +125,7 @@ var tts = YAHOO.util.Dom.getElementsByClassName('tooltip'); for (var i = 0; i < tts.length; i++) { - //if element doesn not have and id autgenerate one for tooltip + //if element doesn't not have and id autgenerate one for tooltip if (!tts[i].id){ tts[i].id='tt'+i*100; @@ -111,7 +143,7 @@ showdelay:20, }); - //Mouse Over event disabled for new repositories since they dont + //Mouse Over event disabled for new repositories since they don't //have last commit message myToolTips.contextMouseOverEvent.subscribe( function(type, args) { @@ -270,13 +302,13 @@ tooltip_html = tooltip_html % (changeset.author, changeset.date, tooltip(changeset.message)) - lnk_format = 'r%-5s:%s' % (changeset.revision, - changeset.short_id) + lnk_format = '%5s:%s' % ('r%s' % changeset.revision, + short_id(changeset.raw_id)) uri = link_to( lnk_format, url('changeset_home', repo_name=changeset.repository.name, - revision=changeset.short_id), - style=get_color_string(changeset.short_id), + revision=changeset.raw_id), + style=get_color_string(changeset.raw_id), class_='tooltip', tooltip_title=tooltip_html ) @@ -317,37 +349,168 @@ flash = _Flash() -#=============================================================================== +#============================================================================== # MERCURIAL FILTERS available via h. -#=============================================================================== +#============================================================================== from mercurial import util -from mercurial.templatefilters import age as _age, person as _person +from mercurial.templatefilters import person as _person + + + +def _age(curdate): + """turns a datetime into an age string.""" + + if not curdate: + return '' + + from datetime import timedelta, datetime + + agescales = [("year", 3600 * 24 * 365), + ("month", 3600 * 24 * 30), + ("day", 3600 * 24), + ("hour", 3600), + ("minute", 60), + ("second", 1), ] + + age = datetime.now() - curdate + age_seconds = (age.days * agescales[2][1]) + age.seconds + pos = 1 + for scale in agescales: + if scale[1] <= age_seconds: + if pos == 6:pos = 5 + return time_ago_in_words(curdate, agescales[pos][0]) + ' ' + _('ago') + pos += 1 + + return _('just now') age = lambda x:_age(x) capitalize = lambda x: x.capitalize() -date = lambda x: util.datestr(x) email = util.email email_or_none = lambda x: util.email(x) if util.email(x) != x else None person = lambda x: _person(x) -hgdate = lambda x: "%d %d" % x -isodate = lambda x: util.datestr(x, '%Y-%m-%d %H:%M %1%2') -isodatesec = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2') -localdate = lambda x: (x[0], util.makedate()[1]) -rfc822date = lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S %1%2") -rfc822date_notz = lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S") -rfc3339date = lambda x: util.datestr(x, "%Y-%m-%dT%H:%M:%S%1:%2") -time_ago = lambda x: util.datestr(_age(x), "%a, %d %b %Y %H:%M:%S %1%2") +short_id = lambda x: x[:12] + + +def bool2icon(value): + """ + Returns True/False values represented as small html image of true/false + icons + :param value: bool value + """ + + if value is True: + return HTML.tag('img', src="/images/icons/accept.png", alt=_('True')) + + if value is False: + return HTML.tag('img', src="/images/icons/cancel.png", alt=_('False')) + + return value -#=============================================================================== +def action_parser(user_log): + """ + This helper will map the specified string action into translated + fancy names with icons and links + + @param action: + """ + action = user_log.action + action_params = ' ' + + x = action.split(':') + + if len(x) > 1: + action, action_params = x + + def get_cs_links(): + if action == 'push': + revs_limit = 5 + revs = action_params.split(',') + cs_links = " " + ', '.join ([link(rev, + url('changeset_home', + repo_name=user_log.repository.repo_name, + revision=rev)) for rev in revs[:revs_limit] ]) + if len(revs) > revs_limit: + uniq_id = revs[0] + html_tmpl = (' %s ' + '%s ' + '%s') + cs_links += html_tmpl % (_('and'), uniq_id, _('%s more') \ + % (len(revs) - revs_limit), + _('revisions')) + + html_tmpl = '' + cs_links += html_tmpl % (uniq_id, ', '.join([link(rev, + url('changeset_home', + repo_name=user_log.repository.repo_name, + revision=rev)) for rev in revs[:revs_limit] ])) + + return cs_links + return '' + + def get_fork_name(): + if action == 'user_forked_repo': + from rhodecode.model.scm import ScmModel + repo_name = action_params + repo = ScmModel().get(repo_name) + if repo is None: + return repo_name + return link_to(action_params, url('summary_home', + repo_name=repo.name,), + title=repo.dbrepo.description) + return '' + map = {'user_deleted_repo':_('User [deleted] repository'), + 'user_created_repo':_('User [created] repository'), + 'user_forked_repo':_('User [forked] repository as: %s') % get_fork_name(), + 'user_updated_repo':_('User [updated] repository'), + 'admin_deleted_repo':_('Admin [delete] repository'), + 'admin_created_repo':_('Admin [created] repository'), + 'admin_forked_repo':_('Admin [forked] repository'), + 'admin_updated_repo':_('Admin [updated] repository'), + 'push':_('[Pushed] %s') % get_cs_links(), + 'pull':_('[Pulled]'), + 'started_following_repo':_('User [started following] repository'), + 'stopped_following_repo':_('User [stopped following] repository'), + } + + action_str = map.get(action, action) + return literal(action_str.replace('[', '')\ + .replace(']', '')) + +def action_parser_icon(user_log): + action = user_log.action + action_params = None + x = action.split(':') + + if len(x) > 1: + action, action_params = x + + tmpl = """%s""" + map = {'user_deleted_repo':'database_delete.png', + 'user_created_repo':'database_add.png', + 'user_forked_repo':'arrow_divide.png', + 'user_updated_repo':'database_edit.png', + 'admin_deleted_repo':'database_delete.png', + 'admin_created_repo':'database_ddd.png', + 'admin_forked_repo':'arrow_divide.png', + 'admin_updated_repo':'database_edit.png', + 'push':'script_add.png', + 'pull':'down_16.png', + 'started_following_repo':'heart_add.png', + 'stopped_following_repo':'heart_delete.png', + } + return literal(tmpl % (map.get(action, action), action)) + + +#============================================================================== # PERMS -#=============================================================================== +#============================================================================== from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \ HasRepoPermissionAny, HasRepoPermissionAll -#=============================================================================== +#============================================================================== # GRAVATAR URL -#=============================================================================== +#============================================================================== import hashlib import urllib from pylons import request diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/hooks.py --- a/rhodecode/lib/hooks.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/lib/hooks.py Sat Dec 18 14:45:58 2010 +0100 @@ -22,12 +22,12 @@ @author: marcink """ - -import sys +from mercurial.cmdutil import revrange +from mercurial.node import nullrev +from rhodecode.lib import helpers as h +from rhodecode.lib.utils import action_logger import os -from rhodecode.lib import helpers as h -from rhodecode.model import meta -from rhodecode.model.db import UserLog, User +import sys def repo_size(ui, repo, hooktype=None, **kwargs): @@ -53,32 +53,53 @@ size_total_f = h.format_byte_size(size_root + size_hg) sys.stdout.write('Repository size .hg:%s repo:%s total:%s\n' \ % (size_hg_f, size_root_f, size_total_f)) - - user_action_mapper(ui, repo, hooktype, **kwargs) + +def log_pull_action(ui, repo, **kwargs): + """ + Logs user last pull action + :param ui: + :param repo: + """ -def user_action_mapper(ui, repo, hooktype=None, **kwargs): + extra_params = dict(repo.ui.configitems('rhodecode_extras')) + username = extra_params['username'] + repository = extra_params['repository'] + action = 'pull' + + action_logger(username, action, repository, extra_params['ip']) + + return 0 + +def log_push_action(ui, repo, **kwargs): """ Maps user last push action to new changeset id, from mercurial :param ui: :param repo: - :param hooktype: """ - - try: - sa = meta.Session - username = kwargs['url'].split(':')[-1] - user_log = sa.query(UserLog)\ - .filter(UserLog.user == sa.query(User)\ - .filter(User.username == username).one())\ - .order_by(UserLog.user_log_id.desc()).first() - - if user_log and not user_log.revision: - user_log.revision = str(repo['tip']) - sa.add(user_log) - sa.commit() - - except Exception, e: - sa.rollback() - raise - finally: - meta.Session.remove() + + extra_params = dict(repo.ui.configitems('rhodecode_extras')) + username = extra_params['username'] + repository = extra_params['repository'] + action = 'push:%s' + node = kwargs['node'] + + def get_revs(repo, rev_opt): + if rev_opt: + revs = revrange(repo, rev_opt) + + if len(revs) == 0: + return (nullrev, nullrev) + return (max(revs), min(revs)) + else: + return (len(repo) - 1, 0) + + stop, start = get_revs(repo, [node + ':']) + + revs = (str(repo[r]) for r in xrange(start, stop + 1)) + + action = action % ','.join(revs) + + action_logger(username, action, repository, extra_params['ip']) + + return 0 + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/indexers/__init__.py --- a/rhodecode/lib/indexers/__init__.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/lib/indexers/__init__.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,26 +1,28 @@ +import os +import sys +import traceback from os.path import dirname as dn, join as jn + +#to get the rhodecode import +sys.path.append(dn(dn(dn(os.path.realpath(__file__))))) + +from rhodecode.model import init_model +from rhodecode.model.scm import ScmModel from rhodecode.config.environment import load_environment -from rhodecode.model.hg_model import HgModel +from rhodecode.lib.utils import BasePasterCommand, Command, add_cache + from shutil import rmtree from webhelpers.html.builder import escape from vcs.utils.lazy import LazyProperty +from sqlalchemy import engine_from_config + from whoosh.analysis import RegexTokenizer, LowercaseFilter, StopFilter from whoosh.fields import TEXT, ID, STORED, Schema, FieldType from whoosh.index import create_in, open_dir from whoosh.formats import Characters -from whoosh.highlight import highlight, SimpleFragmenter, HtmlFormatter - -import os -import sys -import traceback +from whoosh.highlight import highlight, SimpleFragmenter, HtmlFormatter -#to get the rhodecode import -sys.path.append(dn(dn(dn(os.path.realpath(__file__))))) - - -#LOCATION WE KEEP THE INDEX -IDX_LOCATION = jn(dn(dn(dn(dn(os.path.abspath(__file__))))), 'data', 'index') #EXTENSIONS WE WANT TO INDEX CONTENT OFF INDEX_EXTENSIONS = ['action', 'adp', 'ashx', 'asmx', 'aspx', 'asx', 'axd', 'c', @@ -45,9 +47,58 @@ IDX_NAME = 'HG_INDEX' -FORMATTER = HtmlFormatter('span', between='\n...\n') +FORMATTER = HtmlFormatter('span', between='\n...\n') FRAGMENTER = SimpleFragmenter(200) - + + +class MakeIndex(BasePasterCommand): + + max_args = 1 + min_args = 1 + + usage = "CONFIG_FILE" + summary = "Creates index for full text search given configuration file" + group_name = "RhodeCode" + takes_config_file = -1 + parser = Command.standard_parser(verbose=True) + + def command(self): + + from pylons import config + add_cache(config) + engine = engine_from_config(config, 'sqlalchemy.db1.') + init_model(engine) + + index_location = config['index_dir'] + repo_location = self.options.repo_location + + #====================================================================== + # WHOOSH DAEMON + #====================================================================== + from rhodecode.lib.pidlock import LockHeld, DaemonLock + from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon + try: + l = DaemonLock() + WhooshIndexingDaemon(index_location=index_location, + repo_location=repo_location)\ + .run(full_index=self.options.full_index) + l.release() + except LockHeld: + sys.exit(1) + + def update_parser(self): + self.parser.add_option('--repo-location', + action='store', + dest='repo_location', + help="Specifies repositories location to index REQUIRED", + ) + self.parser.add_option('-f', + action='store_true', + dest='full_index', + help="Specifies that index should be made full i.e" + " destroy old and build from scratch", + default=False) + class ResultWrapper(object): def __init__(self, search_type, searcher, matcher, highlight_items): self.search_type = search_type @@ -55,7 +106,7 @@ self.matcher = matcher self.highlight_items = highlight_items self.fragment_size = 200 / 2 - + @LazyProperty def doc_ids(self): docs_id = [] @@ -64,8 +115,8 @@ chunks = [offsets for offsets in self.get_chunks()] docs_id.append([docnum, chunks]) self.matcher.next() - return docs_id - + return docs_id + def __str__(self): return '<%s at %s>' % (self.__class__.__name__, len(self.doc_ids)) @@ -91,32 +142,32 @@ slice = [] for docid in self.doc_ids[i:j]: slice.append(self.get_full_content(docid)) - return slice - + return slice + def get_full_content(self, docid): res = self.searcher.stored_fields(docid[0]) f_path = res['path'][res['path'].find(res['repository']) \ + len(res['repository']):].lstrip('/') - + content_short = self.get_short_content(res, docid[1]) res.update({'content_short':content_short, 'content_short_hl':self.highlight(content_short), 'f_path':f_path}) - - return res - + + return res + def get_short_content(self, res, chunks): - + return ''.join([res['content'][chunk[0]:chunk[1]] for chunk in chunks]) - + def get_chunks(self): """ Smart function that implements chunking the content but not overlap chunks so it doesn't highlight the same close occurrences twice. - :param matcher: - :param size: + @param matcher: + @param size: """ memory = [(0, 0)] for span in self.matcher.spans(): @@ -124,12 +175,12 @@ end = span.endchar or 0 start_offseted = max(0, start - self.fragment_size) end_offseted = end + self.fragment_size - + if start_offseted < memory[-1][1]: start_offseted = memory[-1][1] - memory.append((start_offseted, end_offseted,)) - yield (start_offseted, end_offseted,) - + memory.append((start_offseted, end_offseted,)) + yield (start_offseted, end_offseted,) + def highlight(self, content, top=5): if self.search_type != 'content': return '' @@ -139,4 +190,4 @@ fragmenter=FRAGMENTER, formatter=FORMATTER, top=top) - return hl + return hl diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/indexers/daemon.py --- a/rhodecode/lib/indexers/daemon.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/lib/indexers/daemon.py Sat Dec 18 14:45:58 2010 +0100 @@ -32,12 +32,12 @@ project_path = dn(dn(dn(dn(os.path.realpath(__file__))))) sys.path.append(project_path) -from rhodecode.lib.pidlock import LockHeld, DaemonLock -from rhodecode.model.hg_model import HgModel + +from rhodecode.model.scm import ScmModel from rhodecode.lib.helpers import safe_unicode from whoosh.index import create_in, open_dir from shutil import rmtree -from rhodecode.lib.indexers import INDEX_EXTENSIONS, IDX_LOCATION, SCHEMA, IDX_NAME +from rhodecode.lib.indexers import INDEX_EXTENSIONS, SCHEMA, IDX_NAME from time import mktime from vcs.exceptions import ChangesetError, RepositoryError @@ -61,55 +61,59 @@ # add ch to logger log.addHandler(ch) -def scan_paths(root_location): - return HgModel.repo_scan('/', root_location, None, True) - class WhooshIndexingDaemon(object): """ Deamon for atomic jobs """ - def __init__(self, indexname='HG_INDEX', repo_location=None): + def __init__(self, indexname='HG_INDEX', index_location=None, + repo_location=None, sa=None): self.indexname = indexname + + self.index_location = index_location + if not index_location: + raise Exception('You have to provide index location') + self.repo_location = repo_location - self.repo_paths = scan_paths(self.repo_location) + if not repo_location: + raise Exception('You have to provide repositories location') + + self.repo_paths = ScmModel(sa).repo_scan(self.repo_location, None) self.initial = False - if not os.path.isdir(IDX_LOCATION): - os.mkdir(IDX_LOCATION) + if not os.path.isdir(self.index_location): + os.makedirs(self.index_location) log.info('Cannot run incremental index since it does not' ' yet exist running full build') self.initial = True - + def get_paths(self, repo): - """ - recursive walk in root dir and return a set of all path in that dir + """recursive walk in root dir and return a set of all path in that dir based on repository walk function """ index_paths_ = set() try: - tip = repo.get_changeset() - - for topnode, dirs, files in tip.walk('/'): + for topnode, dirs, files in repo.walk('/', 'tip'): for f in files: index_paths_.add(jn(repo.path, f.path)) for dir in dirs: for f in files: index_paths_.add(jn(repo.path, f.path)) - + except RepositoryError: pass - return index_paths_ - + return index_paths_ + def get_node(self, repo, path): n_path = path[len(repo.path) + 1:] node = repo.get_changeset().get_node(n_path) return node - + def get_node_mtime(self, node): return mktime(node.last_changeset.date.timetuple()) - + def add_doc(self, writer, path, repo): - """Adding doc to writer""" + """Adding doc to writer this function itself fetches data from + the instance of vcs backend""" node = self.get_node(repo, path) #we just index the content of chosen files @@ -120,63 +124,63 @@ log.debug(' >> %s' % path) #just index file name without it's content u_content = u'' - + writer.add_document(owner=unicode(repo.contact), repository=safe_unicode(repo.name), path=safe_unicode(path), content=u_content, modtime=self.get_node_mtime(node), - extension=node.extension) + extension=node.extension) + - def build_index(self): - if os.path.exists(IDX_LOCATION): + if os.path.exists(self.index_location): log.debug('removing previous index') - rmtree(IDX_LOCATION) - - if not os.path.exists(IDX_LOCATION): - os.mkdir(IDX_LOCATION) - - idx = create_in(IDX_LOCATION, SCHEMA, indexname=IDX_NAME) + rmtree(self.index_location) + + if not os.path.exists(self.index_location): + os.mkdir(self.index_location) + + idx = create_in(self.index_location, SCHEMA, indexname=IDX_NAME) writer = idx.writer() - + for cnt, repo in enumerate(self.repo_paths.values()): log.debug('building index @ %s' % repo.path) - + for idx_path in self.get_paths(repo): self.add_doc(writer, idx_path, repo) - + log.debug('>> COMMITING CHANGES <<') writer.commit(merge=True) log.debug('>>> FINISHED BUILDING INDEX <<<') - - + + def update_index(self): log.debug('STARTING INCREMENTAL INDEXING UPDATE') - - idx = open_dir(IDX_LOCATION, indexname=self.indexname) + + idx = open_dir(self.index_location, indexname=self.indexname) # The set of all paths in the index indexed_paths = set() # The set of all paths we need to re-index to_index = set() - + reader = idx.reader() writer = idx.writer() - + # Loop over the stored fields in the index for fields in reader.all_stored_fields(): indexed_path = fields['path'] indexed_paths.add(indexed_path) - + repo = self.repo_paths[fields['repository']] - + try: node = self.get_node(repo, indexed_path) except ChangesetError: # This file was deleted since it was indexed log.debug('removing from index %s' % indexed_path) writer.delete_by_term('path', indexed_path) - + else: # Check if this file was changed since it was indexed indexed_time = fields['modtime'] @@ -187,7 +191,7 @@ log.debug('adding to reindex list %s' % indexed_path) writer.delete_by_term('path', indexed_path) to_index.add(indexed_path) - + # Loop over the files in the filesystem # Assume we have a function that gathers the filenames of the # documents to be indexed @@ -198,51 +202,14 @@ # that wasn't indexed before. So index it! self.add_doc(writer, path, repo) log.debug('re indexing %s' % path) - + log.debug('>> COMMITING CHANGES <<') writer.commit(merge=True) log.debug('>>> FINISHED REBUILDING INDEX <<<') - + def run(self, full_index=False): """Run daemon""" if full_index or self.initial: self.build_index() else: self.update_index() - -if __name__ == "__main__": - arg = sys.argv[1:] - if len(arg) != 2: - sys.stderr.write('Please specify indexing type [full|incremental]' - 'and path to repositories as script args \n') - sys.exit() - - - if arg[0] == 'full': - full_index = True - elif arg[0] == 'incremental': - # False means looking just for changes - full_index = False - else: - sys.stdout.write('Please use [full|incremental]' - ' as script first arg \n') - sys.exit() - - if not os.path.isdir(arg[1]): - sys.stderr.write('%s is not a valid path \n' % arg[1]) - sys.exit() - else: - if arg[1].endswith('/'): - repo_location = arg[1] + '*' - else: - repo_location = arg[1] + '/*' - - try: - l = DaemonLock() - WhooshIndexingDaemon(repo_location=repo_location)\ - .run(full_index=full_index) - l.release() - reload(logging) - except LockHeld: - sys.exit(1) - diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/middleware/simplegit.py --- a/rhodecode/lib/middleware/simplegit.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/lib/middleware/simplegit.py Sat Dec 18 14:45:58 2010 +0100 @@ -17,6 +17,14 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. +""" +Created on 2010-04-28 + +@author: marcink +SimpleGit middleware for handling git protocol request (push/clone etc.) +It's implemented with basic auth function +""" + from dulwich import server as dulserver class SimpleGitUploadPackHandler(dulserver.UploadPackHandler): @@ -54,26 +62,28 @@ from dulwich.web import HTTPGitApplication from paste.auth.basic import AuthBasicAuthenticator from paste.httpheaders import REMOTE_USER, AUTH_TYPE -from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware, \ - get_user_cached -from rhodecode.lib.utils import action_logger, is_git, invalidate_cache, \ - check_repo_fast +from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware +from rhodecode.lib.utils import invalidate_cache, check_repo_fast +from rhodecode.model.user import UserModel from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError import logging import os import traceback -""" -Created on 2010-04-28 - -@author: marcink -SimpleGit middleware for handling git protocol request (push/clone etc.) -It's implemented with basic auth function -""" - - log = logging.getLogger(__name__) +def is_git(environ): + """ + Returns True if request's target is git server. ``HTTP_USER_AGENT`` would + then have git client version given. + + :param environ: + """ + http_user_agent = environ.get('HTTP_USER_AGENT') + if http_user_agent and http_user_agent.startswith('git'): + return True + return False + class SimpleGit(object): def __init__(self, application, config): @@ -81,11 +91,19 @@ self.config = config #authenticate this git request using self.authenticate = AuthBasicAuthenticator('', authfunc) + self.ipaddr = '0.0.0.0' + self.repository = None + self.username = None + self.action = None def __call__(self, environ, start_response): if not is_git(environ): return self.application(environ, start_response) + proxy_key = 'HTTP_X_REAL_IP' + def_key = 'REMOTE_ADDR' + self.ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0')) + #=================================================================== # AUTHENTICATE THIS GIT REQUEST #=================================================================== @@ -99,10 +117,14 @@ else: return result.wsgi_application(environ, start_response) + #======================================================================= + # GET REPOSITORY + #======================================================================= try: - self.repo_name = environ['PATH_INFO'].split('/')[1] - if self.repo_name.endswith('/'): - self.repo_name = self.repo_name.rstrip('/') + repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:]) + if repo_name.endswith('/'): + repo_name = repo_name.rstrip('/') + self.repository = repo_name except: log.error(traceback.format_exc()) return HTTPInternalServerError()(environ, start_response) @@ -110,20 +132,21 @@ #=================================================================== # CHECK PERMISSIONS FOR THIS REQUEST #=================================================================== - action = self.__get_action(environ) - if action: + self.action = self.__get_action(environ) + if self.action: username = self.__get_environ_user(environ) try: user = self.__get_user(username) + self.username = user.username except: log.error(traceback.format_exc()) return HTTPInternalServerError()(environ, start_response) #check permissions for this repository - if action == 'push': + if self.action == 'push': if not HasPermissionAnyMiddleware('repository.write', 'repository.admin')\ - (user, self.repo_name): + (user, repo_name): return HTTPForbidden()(environ, start_response) else: @@ -131,15 +154,13 @@ if not HasPermissionAnyMiddleware('repository.read', 'repository.write', 'repository.admin')\ - (user, self.repo_name): + (user, repo_name): return HTTPForbidden()(environ, start_response) - #log action - if action in ('push', 'pull', 'clone'): - proxy_key = 'HTTP_X_REAL_IP' - def_key = 'REMOTE_ADDR' - ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0')) - self.__log_user_action(user, action, self.repo_name, ipaddr) + self.extras = {'ip':self.ipaddr, + 'username':self.username, + 'action':self.action, + 'repository':self.repository} #=================================================================== # GIT REQUEST HANDLING @@ -151,12 +172,12 @@ return HTTPNotFound()(environ, start_response) try: app = self.__make_app() - except Exception: + except: log.error(traceback.format_exc()) return HTTPInternalServerError()(environ, start_response) #invalidate cache on push - if action == 'push': + if self.action == 'push': self.__invalidate_cache(self.repo_name) messages = [] messages.append('thank you for using rhodecode') @@ -175,7 +196,7 @@ return environ.get('REMOTE_USER') def __get_user(self, username): - return get_user_cached(username) + return UserModel().get_by_username(username, cache=True) def __get_action(self, environ): """ @@ -193,12 +214,8 @@ else: return 'other' - def __log_user_action(self, user, action, repo, ipaddr): - action_logger(user, action, repo, ipaddr) - def __invalidate_cache(self, repo_name): """we know that some change was made to repositories and we should invalidate the cache to see the changes right away but only for push requests""" - invalidate_cache('cached_repo_list') - invalidate_cache('full_changelog', repo_name) + invalidate_cache('get_repo_cached_%s' % repo_name) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/middleware/simplehg.py --- a/rhodecode/lib/middleware/simplehg.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/lib/middleware/simplehg.py Sat Dec 18 14:45:58 2010 +0100 @@ -24,40 +24,57 @@ SimpleHG middleware for handling mercurial protocol request (push/clone etc.) It's implemented with basic auth function """ -from itertools import chain from mercurial.error import RepoError from mercurial.hgweb import hgweb from mercurial.hgweb.request import wsgiapplication from paste.auth.basic import AuthBasicAuthenticator from paste.httpheaders import REMOTE_USER, AUTH_TYPE -from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware, \ - get_user_cached -from rhodecode.lib.utils import is_mercurial, make_ui, invalidate_cache, \ +from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware +from rhodecode.lib.utils import make_ui, invalidate_cache, \ check_repo_fast, ui_sections +from rhodecode.model.user import UserModel from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError -from rhodecode.lib.utils import action_logger import logging import os import traceback log = logging.getLogger(__name__) +def is_mercurial(environ): + """ + Returns True if request's target is mercurial server - header + ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``. + """ + http_accept = environ.get('HTTP_ACCEPT') + if http_accept and http_accept.startswith('application/mercurial'): + return True + return False + class SimpleHg(object): def __init__(self, application, config): self.application = application self.config = config - #authenticate this mercurial request using + #authenticate this mercurial request using authfunc self.authenticate = AuthBasicAuthenticator('', authfunc) + self.ipaddr = '0.0.0.0' + self.repository = None + self.username = None + self.action = None def __call__(self, environ, start_response): if not is_mercurial(environ): return self.application(environ, start_response) + proxy_key = 'HTTP_X_REAL_IP' + def_key = 'REMOTE_ADDR' + self.ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0')) + #=================================================================== # AUTHENTICATE THIS MERCURIAL REQUEST #=================================================================== username = REMOTE_USER(environ) + if not username: self.authenticate.realm = self.config['rhodecode_realm'] result = self.authenticate(environ) @@ -67,10 +84,14 @@ else: return result.wsgi_application(environ, start_response) + #======================================================================= + # GET REPOSITORY + #======================================================================= try: repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:]) if repo_name.endswith('/'): repo_name = repo_name.rstrip('/') + self.repository = repo_name except: log.error(traceback.format_exc()) return HTTPInternalServerError()(environ, start_response) @@ -78,17 +99,18 @@ #=================================================================== # CHECK PERMISSIONS FOR THIS REQUEST #=================================================================== - action = self.__get_action(environ) - if action: + self.action = self.__get_action(environ) + if self.action: username = self.__get_environ_user(environ) try: user = self.__get_user(username) + self.username = user.username except: log.error(traceback.format_exc()) return HTTPInternalServerError()(environ, start_response) #check permissions for this repository - if action == 'push': + if self.action == 'push': if not HasPermissionAnyMiddleware('repository.write', 'repository.admin')\ (user, repo_name): @@ -102,12 +124,10 @@ (user, repo_name): return HTTPForbidden()(environ, start_response) - #log action - if action in ('push', 'pull', 'clone'): - proxy_key = 'HTTP_X_REAL_IP' - def_key = 'REMOTE_ADDR' - ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0')) - self.__log_user_action(user, action, repo_name, ipaddr) + self.extras = {'ip':self.ipaddr, + 'username':self.username, + 'action':self.action, + 'repository':self.repository} #=================================================================== # MERCURIAL REQUEST HANDLING @@ -130,40 +150,21 @@ return HTTPInternalServerError()(environ, start_response) #invalidate cache on push - if action == 'push': + if self.action == 'push': self.__invalidate_cache(repo_name) - messages = [] - messages.append('thank you for using rhodecode') - - return self.msg_wrapper(app, environ, start_response, messages) - else: - return app(environ, start_response) - - def msg_wrapper(self, app, environ, start_response, messages=[]): - """ - Wrapper for custom messages that come out of mercurial respond messages - is a list of messages that the user will see at the end of response - from merurial protocol actions that involves remote answers - :param app: - :param environ: - :param start_response: - """ - def custom_messages(msg_list): - for msg in msg_list: - yield msg + '\n' - org_response = app(environ, start_response) - return chain(org_response, custom_messages(messages)) + return app(environ, start_response) + def __make_app(self): hgserve = hgweb(str(self.repo_path), baseui=self.baseui) - return self.__load_web_settings(hgserve) + return self.__load_web_settings(hgserve, self.extras) def __get_environ_user(self, environ): return environ.get('REMOTE_USER') def __get_user(self, username): - return get_user_cached(username) + return UserModel().get_by_username(username, cache=True) def __get_action(self, environ): """ @@ -174,7 +175,7 @@ mapping = {'changegroup': 'pull', 'changegroupsubset': 'pull', 'stream_out': 'pull', - #'listkeys': 'pull', + 'listkeys': 'pull', 'unbundle': 'push', 'pushkey': 'push', } for qry in environ['QUERY_STRING'].split('&'): @@ -185,25 +186,26 @@ else: return cmd - def __log_user_action(self, user, action, repo, ipaddr): - action_logger(user, action, repo, ipaddr) - def __invalidate_cache(self, repo_name): """we know that some change was made to repositories and we should invalidate the cache to see the changes right away but only for push requests""" - invalidate_cache('cached_repo_list') - invalidate_cache('full_changelog', repo_name) + invalidate_cache('get_repo_cached_%s' % repo_name) - def __load_web_settings(self, hgserve): + def __load_web_settings(self, hgserve, extras={}): #set the global ui for hgserve instance passed hgserve.repo.ui = self.baseui hgrc = os.path.join(self.repo_path, '.hg', 'hgrc') + + #inject some additional parameters that will be available in ui + #for hooks + for k, v in extras.items(): + hgserve.repo.ui.setconfig('rhodecode_extras', k, v) + repoui = make_ui('file', hgrc, False) - if repoui: #overwrite our ui instance with the section from hgrc file for section in ui_sections: diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/smtp_mailer.py --- a/rhodecode/lib/smtp_mailer.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/lib/smtp_mailer.py Sat Dec 18 14:45:58 2010 +0100 @@ -22,7 +22,7 @@ def __init__(self, mail_from, user, passwd, mail_server, mail_port=None, ssl=False, tls=False): - + self.mail_from = mail_from self.mail_server = mail_server self.mail_port = mail_port @@ -31,7 +31,7 @@ self.ssl = ssl self.tls = tls self.debug = False - + def send(self, recipients=[], subject='', body='', attachment_files={}): if isinstance(recipients, basestring): @@ -43,11 +43,11 @@ if self.tls: smtp_serv.starttls() - - if self.debug: + + if self.debug: smtp_serv.set_debuglevel(1) - smtp_serv.ehlo("mailer") + smtp_serv.ehlo("rhodecode mailer") #if server requires authorization you must provide login and password smtp_serv.login(self.user, self.passwd) @@ -82,13 +82,13 @@ maintype, subtype = ctype.split('/', 1) if maintype == 'text': # Note: we should handle calculating the charset - file_part = MIMEText(self.get_content(msg_file), + file_part = MIMEText(self.get_content(msg_file), _subtype=subtype) elif maintype == 'image': - file_part = MIMEImage(self.get_content(msg_file), + file_part = MIMEImage(self.get_content(msg_file), _subtype=subtype) elif maintype == 'audio': - file_part = MIMEAudio(self.get_content(msg_file), + file_part = MIMEAudio(self.get_content(msg_file), _subtype=subtype) else: file_part = MIMEBase(maintype, subtype) @@ -96,13 +96,13 @@ # Encode the payload using Base64 encoders.encode_base64(msg) # Set the filename parameter - file_part.add_header('Content-Disposition', 'attachment', + file_part.add_header('Content-Disposition', 'attachment', filename=f_name) file_part.add_header('Content-Type', ctype, name=f_name) msg.attach(file_part) else: - raise Exception('Attachment files should be' - 'a dict in format {"filename":"filepath"}') + raise Exception('Attachment files should be' + 'a dict in format {"filename":"filepath"}') def get_content(self, msg_file): ''' diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/lib/utils.py --- a/rhodecode/lib/utils.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/lib/utils.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,7 +1,15 @@ -#!/usr/bin/env python -# encoding: utf-8 -# Utilities for RhodeCode -# Copyright (C) 2009-2010 Marcin Kuzminski +# -*- coding: utf-8 -*- +""" + rhodecode.lib.utils + ~~~~~~~~~~~~~~~~~~~ + + Utilities library for RhodeCode + + :created_on: Apr 18, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; version 2 @@ -17,21 +25,28 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. -""" -Created on April 18, 2010 -Utilities for RhodeCode -@author: marcink -""" -from beaker.cache import cache_region +import os +import logging +import datetime +import traceback + +from UserDict import DictMixin + from mercurial import ui, config, hg from mercurial.error import RepoError -from rhodecode.model import meta -from rhodecode.model.db import Repository, User, RhodeCodeUi, RhodeCodeSettings, UserLog + +import paste +import beaker +from paste.script.command import Command, BadCommand + from vcs.backends.base import BaseChangeset from vcs.utils.lazy import LazyProperty -import logging -import datetime -import os + +from rhodecode.model import meta +from rhodecode.model.caching_query import FromCache +from rhodecode.model.db import Repository, User, RhodeCodeUi, UserLog +from rhodecode.model.repo import RepoModel +from rhodecode.model.user import UserModel log = logging.getLogger(__name__) @@ -39,72 +54,95 @@ def get_repo_slug(request): return request.environ['pylons.routes_dict'].get('repo_name') -def is_mercurial(environ): - """ - Returns True if request's target is mercurial server - header - ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``. - """ - http_accept = environ.get('HTTP_ACCEPT') - if http_accept and http_accept.startswith('application/mercurial'): - return True - return False - -def is_git(environ): +def action_logger(user, action, repo, ipaddr='', sa=None): """ - Returns True if request's target is git server. ``HTTP_USER_AGENT`` would - then have git client version given. + Action logger for various actions made by users - :param environ: - """ - http_user_agent = environ.get('HTTP_USER_AGENT') - if http_user_agent.startswith('git'): - return True - return False - -def action_logger(user, action, repo, ipaddr, sa=None): - """ - Action logger for various action made by users + :param user: user that made this action, can be a unique username string or + object containing user_id attribute + :param action: action to log, should be on of predefined unique actions for + easy translations + :param repo: string name of repository or object containing repo_id, + that action was made on + :param ipaddr: optional ip address from what the action was made + :param sa: optional sqlalchemy session + """ if not sa: - sa = meta.Session + sa = meta.Session() try: + um = UserModel() if hasattr(user, 'user_id'): - user_id = user.user_id + user_obj = user elif isinstance(user, basestring): - user_id = sa.query(User).filter(User.username == user).one() + user_obj = um.get_by_username(user, cache=False) else: raise Exception('You have to provide user object or username') - repo_name = repo.lstrip('/') + + rm = RepoModel() + if hasattr(repo, 'repo_id'): + repo_obj = rm.get(repo.repo_id, cache=False) + repo_name = repo_obj.repo_name + elif isinstance(repo, basestring): + repo_name = repo.lstrip('/') + repo_obj = rm.get_by_repo_name(repo_name, cache=False) + else: + raise Exception('You have to provide repository to action logger') + + user_log = UserLog() - user_log.user_id = user_id + user_log.user_id = user_obj.user_id user_log.action = action + + user_log.repository_id = repo_obj.repo_id user_log.repository_name = repo_name - user_log.repository = sa.query(Repository)\ - .filter(Repository.repo_name == repo_name).one() + user_log.action_date = datetime.datetime.now() user_log.user_ip = ipaddr sa.add(user_log) sa.commit() - log.info('Adding user %s, action %s on %s', - user.username, action, repo) - except Exception, e: + log.info('Adding user %s, action %s on %s', user_obj, action, repo) + except: + log.error(traceback.format_exc()) sa.rollback() - log.error('could not log user action:%s', str(e)) -def check_repo_dir(paths): - repos_path = paths[0][1].split('/') - if repos_path[-1] in ['*', '**']: - repos_path = repos_path[:-1] - if repos_path[0] != '/': - repos_path[0] = '/' - if not os.path.isdir(os.path.join(*repos_path)): - raise Exception('Not a valid repository in %s' % paths[0][1]) +def get_repos(path, recursive=False, initial=False): + """ + Scans given path for repos and return (name,(type,path)) tuple + :param prefix: + :param path: + :param recursive: + :param initial: + """ + from vcs.utils.helpers import get_scm + from vcs.exceptions import VCSError + + try: + scm = get_scm(path) + except: + pass + else: + raise Exception('The given path %s should not be a repository got %s', + path, scm) + + for dirpath in os.listdir(path): + try: + yield dirpath, get_scm(os.path.join(path, dirpath)) + except VCSError: + pass def check_repo_fast(repo_name, base_path): + """ + Check given path for existance of directory + :param repo_name: + :param base_path: + + :return False: if this directory is present + """ if os.path.isdir(os.path.join(base_path, repo_name)):return False return True @@ -135,57 +173,6 @@ if retries < 0: raise IOError print complaint -@cache_region('super_short_term', 'cached_hg_ui') -def get_hg_ui_cached(): - try: - sa = meta.Session - ret = sa.query(RhodeCodeUi).all() - finally: - meta.Session.remove() - return ret - - -def get_hg_settings(): - try: - sa = meta.Session - ret = sa.query(RhodeCodeSettings).all() - finally: - meta.Session.remove() - - if not ret: - raise Exception('Could not get application settings !') - settings = {} - for each in ret: - settings['rhodecode_' + each.app_settings_name] = each.app_settings_value - - return settings - -def get_hg_ui_settings(): - try: - sa = meta.Session - ret = sa.query(RhodeCodeUi).all() - finally: - meta.Session.remove() - - if not ret: - raise Exception('Could not get application ui settings !') - settings = {} - for each in ret: - k = each.ui_key - v = each.ui_value - if k == '/': - k = 'root_path' - - if k.find('.') != -1: - k = k.replace('.', '_') - - if each.ui_section == 'hooks': - v = each.ui_active - - settings[each.ui_section + '_' + k] = v - - return settings - #propagated from mercurial documentation ui_sections = ['alias', 'auth', 'decode/encode', 'defaults', @@ -210,6 +197,11 @@ baseui = ui.ui() + #clean the baseui object + baseui._ocfg = config.config() + baseui._ucfg = config.config() + baseui._tcfg = config.config() + if read_from == 'file': if not os.path.isfile(path): log.warning('Unable to read config file %s' % path) @@ -219,70 +211,69 @@ cfg.read(path) for section in ui_sections: for k, v in cfg.items(section): + log.debug('settings ui from file[%s]%s:%s', section, k, v) baseui.setconfig(section, k, v) - log.debug('settings ui from file[%s]%s:%s', section, k, v) - - for k, v in baseui.configitems('extensions'): - baseui.setconfig('extensions', k, '0') - #just enable mq - baseui.setconfig('extensions', 'mq', '1') - if checkpaths:check_repo_dir(cfg.items('paths')) elif read_from == 'db': - hg_ui = get_hg_ui_cached() + sa = meta.Session() + ret = sa.query(RhodeCodeUi)\ + .options(FromCache("sql_cache_short", + "get_hg_ui_settings")).all() + + hg_ui = ret for ui_ in hg_ui: if ui_.ui_active: - log.debug('settings ui from db[%s]%s:%s', ui_.ui_section, ui_.ui_key, ui_.ui_value) + log.debug('settings ui from db[%s]%s:%s', ui_.ui_section, + ui_.ui_key, ui_.ui_value) baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value) - + meta.Session.remove() return baseui def set_rhodecode_config(config): - hgsettings = get_hg_settings() + """ + Updates pylons config with new settings from database + :param config: + """ + from rhodecode.model.settings import SettingsModel + hgsettings = SettingsModel().get_app_settings() for k, v in hgsettings.items(): config[k] = v -def invalidate_cache(name, *args): - """Invalidates given name cache""" - - from beaker.cache import region_invalidate - log.info('INVALIDATING CACHE FOR %s', name) +def invalidate_cache(cache_key, *args): + """ + Puts cache invalidation task into db for + further global cache invalidation + """ + from rhodecode.model.scm import ScmModel - """propagate our arguments to make sure invalidation works. First - argument has to be the name of cached func name give to cache decorator - without that the invalidation would not work""" - tmp = [name] - tmp.extend(args) - args = tuple(tmp) - - if name == 'cached_repo_list': - from rhodecode.model.hg_model import _get_repos_cached - region_invalidate(_get_repos_cached, None, *args) - - if name == 'full_changelog': - from rhodecode.model.hg_model import _full_changelog_cached - region_invalidate(_full_changelog_cached, None, *args) + if cache_key.startswith('get_repo_cached_'): + name = cache_key.split('get_repo_cached_')[-1] + ScmModel().mark_for_invalidation(name) class EmptyChangeset(BaseChangeset): """ - An dummy empty changeset. + An dummy empty changeset. It's possible to pass hash when creating + an EmptyChangeset """ - revision = -1 - message = '' - author = '' - date = '' + def __init__(self, cs='0' * 40): + self._empty_cs = cs + self.revision = -1 + self.message = '' + self.author = '' + self.date = '' + @LazyProperty def raw_id(self): """ - Returns raw string identifing this changeset, useful for web + Returns raw string identifying this changeset, useful for web representation. """ - return '0' * 40 + return self._empty_cs @LazyProperty def short_id(self): @@ -301,26 +292,25 @@ """ maps all found repositories into db """ - from rhodecode.model.repo_model import RepoModel - sa = meta.Session + sa = meta.Session() + rm = RepoModel() user = sa.query(User).filter(User.admin == True).first() - rm = RepoModel() - for name, repo in initial_repo_list.items(): - if not sa.query(Repository).filter(Repository.repo_name == name).scalar(): + if not rm.get_by_repo_name(name, cache=False): log.info('repository %s not found creating default', name) form_data = { 'repo_name':name, - 'description':repo.description if repo.description != 'unknown' else \ - 'auto description for %s' % name, + 'repo_type':repo.alias, + 'description':repo.description \ + if repo.description != 'unknown' else \ + '%s repository' % name, 'private':False } rm.create(form_data, user, just_db=True) - if remove_obsolete: #remove from database those repositories that are not in the filesystem for repo in sa.query(Repository).all(): @@ -328,11 +318,6 @@ sa.delete(repo) sa.commit() - - meta.Session.remove() - -from UserDict import DictMixin - class OrderedDict(dict, DictMixin): def __init__(self, *args, **kwds): @@ -433,8 +418,51 @@ return not self == other +#set cache regions for beaker so celery can utilise it +def add_cache(settings): + cache_settings = {'regions':None} + for key in settings.keys(): + for prefix in ['beaker.cache.', 'cache.']: + if key.startswith(prefix): + name = key.split(prefix)[1].strip() + cache_settings[name] = settings[key].strip() + if cache_settings['regions']: + for region in cache_settings['regions'].split(','): + region = region.strip() + region_settings = {} + for key, value in cache_settings.items(): + if key.startswith(region): + region_settings[key.split('.')[1]] = value + region_settings['expire'] = int(region_settings.get('expire', + 60)) + region_settings.setdefault('lock_dir', + cache_settings.get('lock_dir')) + if 'type' not in region_settings: + region_settings['type'] = cache_settings.get('type', + 'memory') + beaker.cache.cache_regions[region] = region_settings + +def get_current_revision(): + """ + Returns tuple of (number, id) from repository containing this package + or None if repository could not be found. + """ + try: + from vcs import get_repo + from vcs.utils.helpers import get_scm + from vcs.exceptions import RepositoryError, VCSError + repopath = os.path.join(os.path.dirname(__file__), '..', '..') + scm = get_scm(repopath)[0] + repo = get_repo(path=repopath, alias=scm) + tip = repo.get_changeset() + return (tip.revision, tip.short_id) + except (ImportError, RepositoryError, VCSError), err: + logging.debug("Cannot retrieve rhodecode's revision. Original error " + "was: %s" % err) + return None + #=============================================================================== -# TEST FUNCTIONS +# TEST FUNCTIONS AND CREATORS #=============================================================================== def create_test_index(repo_location, full_index): """Makes default test index @@ -443,15 +471,16 @@ """ from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon from rhodecode.lib.pidlock import DaemonLock, LockHeld - from rhodecode.lib.indexers import IDX_LOCATION import shutil - if os.path.exists(IDX_LOCATION): - shutil.rmtree(IDX_LOCATION) + index_location = os.path.join(repo_location, 'index') + if os.path.exists(index_location): + shutil.rmtree(index_location) try: l = DaemonLock() - WhooshIndexingDaemon(repo_location=repo_location)\ + WhooshIndexingDaemon(index_location=index_location, + repo_location=repo_location)\ .run(full_index=full_index) l.release() except LockHeld: @@ -462,10 +491,11 @@ install test repository into tmp dir """ from rhodecode.lib.db_manage import DbManage + from rhodecode.tests import HG_REPO, GIT_REPO, NEW_HG_REPO, NEW_GIT_REPO, \ + HG_FORK, GIT_FORK, TESTS_TMP_PATH import tarfile import shutil from os.path import dirname as dn, join as jn, abspath - from rhodecode.tests import REPO_PATH, NEW_REPO_PATH, FORK_REPO_PATH log = logging.getLogger('TestEnvCreator') # create logger @@ -485,10 +515,10 @@ log.addHandler(ch) #PART ONE create db - dbname = config['sqlalchemy.db1.url'].split('/')[-1] - log.debug('making test db %s', dbname) + dbconf = config['sqlalchemy.db1.url'] + log.debug('making test db %s', dbconf) - dbmanage = DbManage(log_sql=True, dbname=dbname, root=config['here'], + dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'], tests=True) dbmanage.create_tables(override=True) dbmanage.config_prompt(repos_test_path) @@ -498,18 +528,87 @@ dbmanage.populate_default_permissions() #PART TWO make test repo - log.debug('making test vcs repo') - if os.path.isdir(REPO_PATH): - log.debug('REMOVING %s', REPO_PATH) - shutil.rmtree(REPO_PATH) - if os.path.isdir(NEW_REPO_PATH): - log.debug('REMOVING %s', NEW_REPO_PATH) - shutil.rmtree(NEW_REPO_PATH) - if os.path.isdir(FORK_REPO_PATH): - log.debug('REMOVING %s', FORK_REPO_PATH) - shutil.rmtree(FORK_REPO_PATH) + log.debug('making test vcs repositories') + + #remove old one from previos tests + for r in [HG_REPO, GIT_REPO, NEW_HG_REPO, NEW_GIT_REPO, HG_FORK, GIT_FORK]: + + if os.path.isdir(jn(TESTS_TMP_PATH, r)): + log.debug('removing %s', r) + shutil.rmtree(jn(TESTS_TMP_PATH, r)) + + #CREATE DEFAULT HG REPOSITORY + cur_dir = dn(dn(abspath(__file__))) + tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz")) + tar.extractall(jn(TESTS_TMP_PATH, HG_REPO)) + tar.close() + + +#============================================================================== +# PASTER COMMANDS +#============================================================================== + +class BasePasterCommand(Command): + """ + Abstract Base Class for paster commands. + + The celery commands are somewhat aggressive about loading + celery.conf, and since our module sets the `CELERY_LOADER` + environment variable to our loader, we have to bootstrap a bit and + make sure we've had a chance to load the pylons config off of the + command line, otherwise everything fails. + """ + min_args = 1 + min_args_error = "Please provide a paster config file as an argument." + takes_config_file = 1 + requires_config_file = True - cur_dir = dn(dn(abspath(__file__))) - tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test.tar.gz")) - tar.extractall('/tmp') - tar.close() + def notify_msg(self, msg, log=False): + """Make a notification to user, additionally if logger is passed + it logs this action using given logger + + :param msg: message that will be printed to user + :param log: logging instance, to use to additionally log this message + + """ + print msg + if log and isinstance(log, logging): + log(msg) + + + def run(self, args): + """ + Overrides Command.run + + Checks for a config file argument and loads it. + """ + if len(args) < self.min_args: + raise BadCommand( + self.min_args_error % {'min_args': self.min_args, + 'actual_args': len(args)}) + + # Decrement because we're going to lob off the first argument. + # @@ This is hacky + self.min_args -= 1 + self.bootstrap_config(args[0]) + self.update_parser() + return super(BasePasterCommand, self).run(args[1:]) + + def update_parser(self): + """ + Abstract method. Allows for the class's parser to be updated + before the superclass's `run` method is called. Necessary to + allow options/arguments to be passed through to the underlying + celery command. + """ + raise NotImplementedError("Abstract Method.") + + def bootstrap_config(self, conf): + """ + Loads the pylons configuration. + """ + from pylons import config as pylonsconfig + + path_to_ini_file = os.path.realpath(conf) + conf = paste.deploy.appconfig('config:' + path_to_ini_file) + pylonsconfig.init_app(conf.global_conf, conf.local_conf) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/model/__init__.py --- a/rhodecode/model/__init__.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/model/__init__.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,23 +1,71 @@ -"""The application's model objects""" +# -*- coding: utf-8 -*- +""" + rhodecode.model.__init__ + ~~~~~~~~~~~~~~~~~~~~~~~~ + + The application's model objects + + :created_on: Nov 25, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. + + + :example: + + .. code-block:: python + + from paste.deploy import appconfig + from pylons import config + from sqlalchemy import engine_from_config + from rhodecode.config.environment import load_environment + + conf = appconfig('config:development.ini', relative_to = './../../') + load_environment(conf.global_conf, conf.local_conf) + + engine = engine_from_config(config, 'sqlalchemy.') + init_model(engine) + # RUN YOUR CODE HERE + +""" +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + import logging from rhodecode.model import meta log = logging.getLogger(__name__) def init_model(engine): - """Call me before using any of the tables or classes in the model""" - log.info("INITIALIZING DB MODELS") + """Initializes db session, bind the engine with the metadata, + Call this before using any of the tables or classes in the model, preferably + once in application start + + :param engine: engine to bind to + """ + log.info("initializing db models for %s", engine) meta.Base.metadata.bind = engine - #meta.Base2.metadata.bind = engine2 -#THIS IS A TEST FOR EXECUTING SCRIPT AND LOAD PYLONS APPLICATION GLOBALS -#from paste.deploy import appconfig -#from pylons import config -#from sqlalchemy import engine_from_config -#from rhodecode.config.environment import load_environment -# -#conf = appconfig('config:development.ini', relative_to = './../../') -#load_environment(conf.global_conf, conf.local_conf) -# -#engine = engine_from_config(config, 'sqlalchemy.') -#init_model(engine) -# DO SOMETHING +class BaseModel(object): + """Base Model for all RhodeCode models, it adds sql alchemy session + into instance of model + + :param sa: If passed it reuses this session instead of creating a new one + """ + + def __init__(self, sa=None): + if sa is not None: + self.sa = sa + else: + self.sa = meta.Session() diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/model/caching_query.py --- a/rhodecode/model/caching_query.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/model/caching_query.py Sat Dec 18 14:45:58 2010 +0100 @@ -55,11 +55,11 @@ the "public" method of configuring this state upon the CachingQuery. """ - + def __init__(self, manager, *args, **kw): self.cache_manager = manager Query.__init__(self, *args, **kw) - + def __iter__(self): """override __iter__ to pull results from Beaker if particular attributes have been configured. @@ -101,7 +101,7 @@ """Set the value in the cache for this query.""" cache, cache_key = _get_cache_parameters(self) - cache.put(cache_key, value) + cache.put(cache_key, value) def query_callable(manager): def query(*arg, **kw): @@ -110,11 +110,11 @@ def get_cache_region(name, region): if region not in beaker.cache.cache_regions: - raise BeakerException('Cache region not configured: %s' + raise BeakerException('Cache region `%s` not configured ' 'Check if proper cache settings are in the .ini files' % region) kw = beaker.cache.cache_regions[region] return beaker.cache.Cache._get_cache(name, kw) - + def _get_cache_parameters(query): """For a query with cache_region and cache_namespace configured, return the correspoinding Cache instance and cache key, based @@ -125,7 +125,7 @@ raise ValueError("This Query does not have caching parameters configured.") region, namespace, cache_key = query._cache_parameters - + namespace = _namespace_from_query(namespace, query) if cache_key is None: @@ -153,15 +153,15 @@ return namespace def _set_cache_parameters(query, region, namespace, cache_key): - + if hasattr(query, '_cache_parameters'): region, namespace, cache_key = query._cache_parameters raise ValueError("This query is already configured " - "for region %r namespace %r" % + "for region %r namespace %r" % (region, namespace) ) query._cache_parameters = region, namespace, cache_key - + class FromCache(MapperOption): """Specifies that a Query should load results from a cache.""" @@ -187,10 +187,10 @@ self.region = region self.namespace = namespace self.cache_key = cache_key - + def process_query(self, query): """Process a Query during normal loading operation.""" - + _set_cache_parameters(query, self.region, self.namespace, self.cache_key) class RelationshipCache(MapperOption): @@ -263,13 +263,13 @@ v = [] def visit_bindparam(bind): value = query._params.get(bind.key, bind.value) - + # lazyloader may dig a callable in here, intended # to late-evaluate params after autoflush is called. # convert to a scalar value. if callable(value): value = value() - + v.append(value) if query._criterion is not None: visitors.traverse(query._criterion, {}, {'bindparam':visit_bindparam}) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/model/db.py --- a/rhodecode/model/db.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/model/db.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,46 +1,119 @@ -from rhodecode.model.meta import Base +# -*- coding: utf-8 -*- +""" + rhodecode.model.db + ~~~~~~~~~~~~~~~~~~ + + Database Models for RhodeCode + + :created_on: Apr 08, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +import logging +import datetime + from sqlalchemy import * -from sqlalchemy.orm import relation, backref +from sqlalchemy.exc import DatabaseError +from sqlalchemy.orm import relation, backref, class_mapper from sqlalchemy.orm.session import Session -from vcs.utils.lazy import LazyProperty -import logging + +from rhodecode.model.meta import Base log = logging.getLogger(__name__) -class RhodeCodeSettings(Base): +class BaseModel(object): + + @classmethod + def _get_keys(cls): + """return column names for this model """ + return class_mapper(cls).c.keys() + + def get_dict(self): + """return dict with keys and values corresponding + to this model data """ + + d = {} + for k in self._get_keys(): + d[k] = getattr(self, k) + return d + + def get_appstruct(self): + """return list with keys and values tupples corresponding + to this model data """ + + l = [] + for k in self._get_keys(): + l.append((k, getattr(self, k),)) + return l + + def populate_obj(self, populate_dict): + """populate model with data from given populate_dict""" + + for k in self._get_keys(): + if k in populate_dict: + setattr(self, k, populate_dict[k]) + +class RhodeCodeSettings(Base, BaseModel): __tablename__ = 'rhodecode_settings' __table_args__ = (UniqueConstraint('app_settings_name'), {'useexisting':True}) - app_settings_id = Column("app_settings_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True) - app_settings_name = Column("app_settings_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - app_settings_value = Column("app_settings_value", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + app_settings_name = Column("app_settings_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + app_settings_value = Column("app_settings_value", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) -class RhodeCodeUi(Base): + def __init__(self, k, v): + self.app_settings_name = k + self.app_settings_value = v + + def __repr__(self): + return "" % (self.app_settings_name, + self.app_settings_value) + +class RhodeCodeUi(Base, BaseModel): __tablename__ = 'rhodecode_ui' __table_args__ = {'useexisting':True} - ui_id = Column("ui_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True) - ui_section = Column("ui_section", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - ui_key = Column("ui_key", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - ui_value = Column("ui_value", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - ui_active = Column("ui_active", BOOLEAN(), nullable=True, unique=None, default=True) + ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + ui_section = Column("ui_section", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + ui_key = Column("ui_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + ui_value = Column("ui_value", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True) -class User(Base): +class User(Base, BaseModel): __tablename__ = 'users' __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'useexisting':True}) - user_id = Column("user_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True) - username = Column("username", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - password = Column("password", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - active = Column("active", BOOLEAN(), nullable=True, unique=None, default=None) - admin = Column("admin", BOOLEAN(), nullable=True, unique=None, default=False) - name = Column("name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - lastname = Column("lastname", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - email = Column("email", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - last_login = Column("last_login", DATETIME(timezone=False), nullable=True, unique=None, default=None) + user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + username = Column("username", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + password = Column("password", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + active = Column("active", Boolean(), nullable=True, unique=None, default=None) + admin = Column("admin", Boolean(), nullable=True, unique=None, default=False) + name = Column("name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + lastname = Column("lastname", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + email = Column("email", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None) + is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False) - user_log = relation('UserLog') - user_perms = relation('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id") + user_log = relation('UserLog', cascade='all') + user_perms = relation('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all') - @LazyProperty + repositories = relation('Repository') + user_followers = relation('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all') + + @property def full_contact(self): return '%s %s <%s>' % (self.name, self.lastname, self.email) @@ -49,7 +122,6 @@ def update_lastlogin(self): """Update user lastlogin""" - import datetime try: session = Session.object_session(self) @@ -57,85 +129,129 @@ session.add(self) session.commit() log.debug('updated user %s lastlogin', self.username) - except Exception: + except (DatabaseError,): session.rollback() -class UserLog(Base): +class UserLog(Base, BaseModel): __tablename__ = 'user_logs' __table_args__ = {'useexisting':True} - user_log_id = Column("user_log_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True) - user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None) - 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) - repository_name = Column("repository_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - user_ip = Column("user_ip", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - action = Column("action", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - action_date = Column("action_date", DATETIME(timezone=False), nullable=True, unique=None, default=None) - revision = Column('revision', TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None) + 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) + repository_name = Column("repository_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + user_ip = Column("user_ip", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + action = Column("action", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None) user = relation('User') repository = relation('Repository') -class Repository(Base): +class Repository(Base, BaseModel): __tablename__ = 'repositories' __table_args__ = (UniqueConstraint('repo_name'), {'useexisting':True},) - repo_id = Column("repo_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True) - repo_name = Column("repo_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None) - user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=False, default=None) - private = Column("private", BOOLEAN(), nullable=True, unique=None, default=None) - description = Column("description", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - fork_id = Column("fork_id", INTEGER(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=False, default=None) + repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + repo_name = Column("repo_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None) + repo_type = Column("repo_type", String(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg') + user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=False, default=None) + private = Column("private", Boolean(), nullable=True, unique=None, default=None) + enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True) + description = Column("description", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + fork_id = Column("fork_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=False, default=None) user = relation('User') fork = relation('Repository', remote_side=repo_id) repo_to_perm = relation('RepoToPerm', cascade='all') - stats = relation('Statistics', cascade='all') + stats = relation('Statistics', cascade='all', uselist=False) + + repo_followers = relation('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all') def __repr__(self): - return "" % (self.repo_id, self.repo_name) + return "" % (self.repo_id, self.repo_name) -class Permission(Base): +class Permission(Base, BaseModel): __tablename__ = 'permissions' __table_args__ = {'useexisting':True} - permission_id = Column("permission_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True) - permission_name = Column("permission_name", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - permission_longname = Column("permission_longname", TEXT(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + permission_name = Column("permission_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + permission_longname = Column("permission_longname", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) def __repr__(self): return "" % (self.permission_id, self.permission_name) -class RepoToPerm(Base): +class RepoToPerm(Base, BaseModel): __tablename__ = 'repo_to_perm' __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'useexisting':True}) - repo_to_perm_id = Column("repo_to_perm_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True) - user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None) - permission_id = Column("permission_id", INTEGER(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None) - repository_id = Column("repository_id", INTEGER(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=None, default=None) + repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=None, default=None) user = relation('User') permission = relation('Permission') repository = relation('Repository') -class UserToPerm(Base): +class UserToPerm(Base, BaseModel): __tablename__ = 'user_to_perm' __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'useexisting':True}) - user_to_perm_id = Column("user_to_perm_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True) - user_id = Column("user_id", INTEGER(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None) - permission_id = Column("permission_id", INTEGER(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None) + user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None) user = relation('User') permission = relation('Permission') -class Statistics(Base): +class Statistics(Base, BaseModel): __tablename__ = 'statistics' __table_args__ = (UniqueConstraint('repository_id'), {'useexisting':True}) - stat_id = Column("stat_id", INTEGER(), nullable=False, unique=True, default=None, primary_key=True) - repository_id = Column("repository_id", INTEGER(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=True, default=None) - stat_on_revision = Column("stat_on_revision", INTEGER(), nullable=False) - commit_activity = Column("commit_activity", BLOB(), nullable=False)#JSON data - commit_activity_combined = Column("commit_activity_combined", BLOB(), nullable=False)#JSON data - languages = Column("languages", BLOB(), nullable=False)#JSON data + stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + repository_id = Column("repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=True, default=None) + stat_on_revision = Column("stat_on_revision", Integer(), nullable=False) + commit_activity = Column("commit_activity", LargeBinary(), nullable=False)#JSON data + commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data + languages = Column("languages", LargeBinary(), nullable=False)#JSON data repository = relation('Repository', single_parent=True) +class UserFollowing(Base, BaseModel): + __tablename__ = 'user_followings' + __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'), + UniqueConstraint('user_id', 'follows_user_id') + , {'useexisting':True}) + + user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None) + follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=None, default=None) + follows_user_id = Column("follows_user_id", Integer(), ForeignKey(u'users.user_id'), nullable=True, unique=None, default=None) + + user = relation('User', primaryjoin='User.user_id==UserFollowing.user_id') + + follows_user = relation('User', primaryjoin='User.user_id==UserFollowing.follows_user_id') + follows_repository = relation('Repository') + + +class CacheInvalidation(Base, BaseModel): + __tablename__ = 'cache_invalidation' + __table_args__ = (UniqueConstraint('cache_key'), {'useexisting':True}) + cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + cache_key = Column("cache_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + cache_args = Column("cache_args", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False) + + + def __init__(self, cache_key, cache_args=''): + self.cache_key = cache_key + self.cache_args = cache_args + self.cache_active = False + + def __repr__(self): + return "" % (self.cache_id, self.cache_key) + +class DbMigrateVersion(Base, BaseModel): + __tablename__ = 'db_migrate_version' + __table_args__ = {'useexisting':True} + repository_id = Column('repository_id', String(250), primary_key=True) + repository_path = Column('repository_path', Text) + version = Column('version', Integer) + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/model/forms.py --- a/rhodecode/model/forms.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/model/forms.py Sat Dec 18 14:45:58 2010 +0100 @@ -19,29 +19,34 @@ for SELECT use formencode.All(OneOf(list), Int()) """ +import os +import re +import logging + +import formencode from formencode import All from formencode.validators import UnicodeString, OneOf, Int, Number, Regex, \ Email, Bool, StringBoolean -from pylons import session + from pylons.i18n.translation import _ -from rhodecode.lib.auth import check_password, get_crypt_password + +import rhodecode.lib.helpers as h +from rhodecode.lib.auth import authenticate, get_crypt_password +from rhodecode.lib.exceptions import LdapImportError from rhodecode.model import meta -from rhodecode.model.user_model import UserModel -from rhodecode.model.db import User, Repository -from sqlalchemy.exc import OperationalError -from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound +from rhodecode.model.user import UserModel +from rhodecode.model.repo import RepoModel +from rhodecode.model.db import User +from rhodecode import BACKENDS + from webhelpers.pylonslib.secure_form import authentication_token -import formencode -import logging -import os -import rhodecode.lib.helpers as h + log = logging.getLogger(__name__) - #this is needed to translate the messages using _() in validators class State_obj(object): _ = staticmethod(_) - + #=============================================================================== # VALIDATORS #=============================================================================== @@ -53,75 +58,114 @@ if value != authentication_token(): raise formencode.Invalid(self.message('invalid_token', state, search_number=value), value, state) - -def ValidUsername(edit, old_data): + +def ValidUsername(edit, old_data): class _ValidUsername(formencode.validators.FancyValidator): - + def validate_python(self, value, state): if value in ['default', 'new_user']: raise formencode.Invalid(_('Invalid username'), value, state) - #check if user is uniq - sa = meta.Session + #check if user is unique old_un = None if edit: - old_un = sa.query(User).get(old_data.get('user_id')).username - - if old_un != value or not edit: - if sa.query(User).filter(User.username == value).scalar(): + old_un = UserModel().get(old_data.get('user_id')).username + + if old_un != value or not edit: + if UserModel().get_by_username(value, cache=False, + case_insensitive=True): raise formencode.Invalid(_('This username already exists') , value, state) - meta.Session.remove() - - return _ValidUsername - + + + if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_]+$', value) is None: + raise formencode.Invalid(_('Username may only contain ' + 'alphanumeric characters underscores ' + 'or dashes and must begin with ' + 'alphanumeric character'), + value, state) + + + + return _ValidUsername + class ValidPassword(formencode.validators.FancyValidator): - + def to_python(self, value, state): + if value: - return get_crypt_password(value) - + + if value.get('password'): + try: + value['password'] = get_crypt_password(value['password']) + except UnicodeEncodeError: + e_dict = {'password':_('Invalid characters in password')} + raise formencode.Invalid('', value, state, error_dict=e_dict) + + if value.get('password_confirmation'): + try: + value['password_confirmation'] = \ + get_crypt_password(value['password_confirmation']) + except UnicodeEncodeError: + e_dict = {'password_confirmation':_('Invalid characters in password')} + raise formencode.Invalid('', value, state, error_dict=e_dict) + + if value.get('new_password'): + try: + value['new_password'] = \ + get_crypt_password(value['new_password']) + except UnicodeEncodeError: + e_dict = {'new_password':_('Invalid characters in password')} + raise formencode.Invalid('', value, state, error_dict=e_dict) + + return value + +class ValidPasswordsMatch(formencode.validators.FancyValidator): + + def validate_python(self, value, state): + + if value['password'] != value['password_confirmation']: + e_dict = {'password_confirmation': + _('Password do not match')} + raise formencode.Invalid('', value, state, error_dict=e_dict) + class ValidAuth(formencode.validators.FancyValidator): messages = { 'invalid_password':_('invalid password'), 'invalid_login':_('invalid user name'), - 'disabled_account':_('Your acccount is disabled') - + 'disabled_account':_('Your account is disabled') + } #error mapping e_dict = {'username':messages['invalid_login'], 'password':messages['invalid_password']} e_dict_disable = {'username':messages['disabled_account']} - + def validate_python(self, value, state): password = value['password'] username = value['username'] - user = UserModel().get_user_by_name(username) - if user is None: - raise formencode.Invalid(self.message('invalid_password', - state=State_obj), value, state, - error_dict=self.e_dict) - if user: - if user.active: - if user.username == username and check_password(password, - user.password): - return value - else: - log.warning('user %s not authenticated', username) - raise formencode.Invalid(self.message('invalid_password', - state=State_obj), value, state, - error_dict=self.e_dict) - else: + user = UserModel().get_by_username(username) + + if authenticate(username, password): + return value + else: + if user and user.active is False: log.warning('user %s is disabled', username) raise formencode.Invalid(self.message('disabled_account', state=State_obj), value, state, error_dict=self.e_dict_disable) - + else: + log.warning('user %s not authenticated', username) + raise formencode.Invalid(self.message('invalid_password', + state=State_obj), value, state, + error_dict=self.e_dict) + class ValidRepoUser(formencode.validators.FancyValidator): - + def to_python(self, value, state): + sa = meta.Session() try: - self.user_db = meta.Session.query(User)\ + self.user_db = sa.query(User)\ .filter(User.active == True)\ .filter(User.username == value).one() except Exception: @@ -129,31 +173,39 @@ value, state) finally: meta.Session.remove() - + return self.user_db.user_id -def ValidRepoName(edit, old_data): +def ValidRepoName(edit, old_data): class _ValidRepoName(formencode.validators.FancyValidator): - + def to_python(self, value, state): slug = h.repo_name_slug(value) if slug in ['_admin']: raise formencode.Invalid(_('This repository name is disallowed'), value, state) - if old_data.get('repo_name') != value or not edit: - sa = meta.Session - if sa.query(Repository).filter(Repository.repo_name == slug).scalar(): + if old_data.get('repo_name') != value or not edit: + if RepoModel().get_by_repo_name(slug, cache=False): raise formencode.Invalid(_('This repository already exists') , value, state) - meta.Session.remove() - return slug - - + return slug + + return _ValidRepoName +def ValidForkType(old_data): + class _ValidForkType(formencode.validators.FancyValidator): + + def to_python(self, value, state): + if old_data['repo_type'] != value: + raise formencode.Invalid(_('Fork have to be the same type as original'), + value, state) + return value + return _ValidForkType + class ValidPerms(formencode.validators.FancyValidator): messages = {'perm_new_user_name':_('This username is not valid')} - + def to_python(self, value, state): perms_update = [] perms_new = [] @@ -167,7 +219,7 @@ if (new_user, new_perm) not in perms_new: perms_new.append((new_user, new_perm)) else: - usr = k[5:] + usr = k[5:] if usr == 'default': if value['private']: #set none for default when updating to private repo @@ -184,60 +236,89 @@ except Exception: msg = self.message('perm_new_user_name', state=State_obj) - raise formencode.Invalid(msg, value, state, error_dict={'perm_new_user_name':msg}) + raise formencode.Invalid(msg, value, state, + error_dict={'perm_new_user_name':msg}) return value - + class ValidSettings(formencode.validators.FancyValidator): - + def to_python(self, value, state): #settings form can't edit user if value.has_key('user'): del['value']['user'] - + return value - + class ValidPath(formencode.validators.FancyValidator): def to_python(self, value, state): - isdir = os.path.isdir(value.replace('*', '')) - if (value.endswith('/*') or value.endswith('/**')) and isdir: - return value - elif not isdir: - msg = _('This is not a valid path') - else: - msg = _('You need to specify * or ** at the end of path (ie. /tmp/*)') - - raise formencode.Invalid(msg, value, state, - error_dict={'paths_root_path':msg}) + + if not os.path.isdir(value): + msg = _('This is not a valid path') + raise formencode.Invalid(msg, value, state, + error_dict={'paths_root_path':msg}) + return value def UniqSystemEmail(old_data): class _UniqSystemEmail(formencode.validators.FancyValidator): def to_python(self, value, state): + value = value.lower() if old_data.get('email') != value: - sa = meta.Session + sa = meta.Session() try: user = sa.query(User).filter(User.email == value).scalar() if user: - raise formencode.Invalid(_("That e-mail address is already taken") , + raise formencode.Invalid(_("This e-mail address is already taken") , value, state) finally: meta.Session.remove() - + return value - + return _UniqSystemEmail - + class ValidSystemEmail(formencode.validators.FancyValidator): def to_python(self, value, state): + value = value.lower() sa = meta.Session try: user = sa.query(User).filter(User.email == value).scalar() if user is None: - raise formencode.Invalid(_("That e-mail address doesn't exist.") , + raise formencode.Invalid(_("This e-mail address doesn't exist.") , value, state) finally: meta.Session.remove() - - return value + + return value + +class LdapLibValidator(formencode.validators.FancyValidator): + + def to_python(self, value, state): + + try: + import ldap + except ImportError: + raise LdapImportError + return value + +class BaseDnValidator(formencode.validators.FancyValidator): + + def to_python(self, value, state): + + try: + value % {'user':'valid'} + + if value.find('%(user)s') == -1: + raise formencode.Invalid(_("You need to specify %(user)s in " + "template for example uid=%(user)s " + ",dc=company...") , + value, state) + + except KeyError: + raise formencode.Invalid(_("Wrong template used, only %(user)s " + "is an valid entry") , + value, state) + + return value #=============================================================================== # FORMS @@ -266,65 +347,87 @@ #chained validators have access to all data chained_validators = [ValidAuth] - + def UserForm(edit=False, old_data={}): class _UserForm(formencode.Schema): allow_extra_fields = True filter_extra_fields = True - username = All(UnicodeString(strip=True, min=1, not_empty=True), ValidUsername(edit, old_data)) + username = All(UnicodeString(strip=True, min=1, not_empty=True), + ValidUsername(edit, old_data)) if edit: - new_password = All(UnicodeString(strip=True, min=6, not_empty=False), ValidPassword) + new_password = All(UnicodeString(strip=True, min=6, not_empty=False)) admin = StringBoolean(if_missing=False) else: - password = All(UnicodeString(strip=True, min=6, not_empty=True), ValidPassword) + password = All(UnicodeString(strip=True, min=6, not_empty=True)) active = StringBoolean(if_missing=False) name = UnicodeString(strip=True, min=1, not_empty=True) lastname = UnicodeString(strip=True, min=1, not_empty=True) email = All(Email(not_empty=True), UniqSystemEmail(old_data)) - + + chained_validators = [ValidPassword] + return _UserForm -RegisterForm = UserForm +def RegisterForm(edit=False, old_data={}): + class _RegisterForm(formencode.Schema): + allow_extra_fields = True + filter_extra_fields = True + username = All(ValidUsername(edit, old_data), + UnicodeString(strip=True, min=1, not_empty=True)) + password = All(UnicodeString(strip=True, min=6, not_empty=True)) + password_confirmation = All(UnicodeString(strip=True, min=6, not_empty=True)) + active = StringBoolean(if_missing=False) + name = UnicodeString(strip=True, min=1, not_empty=True) + lastname = UnicodeString(strip=True, min=1, not_empty=True) + email = All(Email(not_empty=True), UniqSystemEmail(old_data)) + + chained_validators = [ValidPasswordsMatch, ValidPassword] + + return _RegisterForm def PasswordResetForm(): class _PasswordResetForm(formencode.Schema): allow_extra_fields = True filter_extra_fields = True - email = All(ValidSystemEmail(), Email(not_empty=True)) + email = All(ValidSystemEmail(), Email(not_empty=True)) return _PasswordResetForm -def RepoForm(edit=False, old_data={}): +def RepoForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()): class _RepoForm(formencode.Schema): allow_extra_fields = True filter_extra_fields = False - repo_name = All(UnicodeString(strip=True, min=1, not_empty=True), ValidRepoName(edit, old_data)) + repo_name = All(UnicodeString(strip=True, min=1, not_empty=True), + ValidRepoName(edit, old_data)) description = UnicodeString(strip=True, min=1, not_empty=True) private = StringBoolean(if_missing=False) - + enable_statistics = StringBoolean(if_missing=False) + repo_type = OneOf(supported_backends) if edit: user = All(Int(not_empty=True), ValidRepoUser) - + chained_validators = [ValidPerms] return _RepoForm -def RepoForkForm(edit=False, old_data={}): +def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()): class _RepoForkForm(formencode.Schema): allow_extra_fields = True filter_extra_fields = False - fork_name = All(UnicodeString(strip=True, min=1, not_empty=True), ValidRepoName(edit, old_data)) + fork_name = All(UnicodeString(strip=True, min=1, not_empty=True), + ValidRepoName(edit, old_data)) description = UnicodeString(strip=True, min=1, not_empty=True) private = StringBoolean(if_missing=False) - + repo_type = All(ValidForkType(old_data), OneOf(supported_backends)) return _RepoForkForm def RepoSettingsForm(edit=False, old_data={}): class _RepoForm(formencode.Schema): allow_extra_fields = True filter_extra_fields = False - repo_name = All(UnicodeString(strip=True, min=1, not_empty=True), ValidRepoName(edit, old_data)) + repo_name = All(UnicodeString(strip=True, min=1, not_empty=True), + ValidRepoName(edit, old_data)) description = UnicodeString(strip=True, min=1, not_empty=True) private = StringBoolean(if_missing=False) - + chained_validators = [ValidPerms, ValidSettings] return _RepoForm @@ -335,9 +438,9 @@ filter_extra_fields = False rhodecode_title = UnicodeString(strip=True, min=1, not_empty=True) rhodecode_realm = UnicodeString(strip=True, min=1, not_empty=True) - + return _ApplicationSettingsForm - + def ApplicationUiSettingsForm(): class _ApplicationUiSettingsForm(formencode.Schema): allow_extra_fields = True @@ -346,16 +449,35 @@ paths_root_path = All(ValidPath(), UnicodeString(strip=True, min=1, not_empty=True)) hooks_changegroup_update = OneOf(['True', 'False'], if_missing=False) hooks_changegroup_repo_size = OneOf(['True', 'False'], if_missing=False) - + hooks_pretxnchangegroup_push_logger = OneOf(['True', 'False'], if_missing=False) + hooks_preoutgoing_pull_logger = OneOf(['True', 'False'], if_missing=False) + return _ApplicationUiSettingsForm def DefaultPermissionsForm(perms_choices, register_choices, create_choices): class _DefaultPermissionsForm(formencode.Schema): allow_extra_fields = True filter_extra_fields = True - overwrite_default = OneOf(['true', 'false'], if_missing='false') + overwrite_default = StringBoolean(if_missing=False) + anonymous = OneOf(['True', 'False'], if_missing=False) default_perm = OneOf(perms_choices) default_register = OneOf(register_choices) default_create = OneOf(create_choices) - + return _DefaultPermissionsForm + + +def LdapSettingsForm(): + class _LdapSettingsForm(formencode.Schema): + allow_extra_fields = True + filter_extra_fields = True + pre_validators = [LdapLibValidator] + ldap_active = StringBoolean(if_missing=False) + ldap_host = UnicodeString(strip=True,) + ldap_port = Number(strip=True,) + ldap_ldaps = StringBoolean(if_missing=False) + ldap_dn_user = UnicodeString(strip=True,) + ldap_dn_pass = UnicodeString(strip=True,) + ldap_base_dn = All(BaseDnValidator, UnicodeString(strip=True,)) + + return _LdapSettingsForm diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/model/hg_model.py --- a/rhodecode/model/hg_model.py Thu Nov 18 21:35:52 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,186 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -# Model for RhodeCode -# Copyright (C) 2009-2010 Marcin Kuzminski -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; version 2 -# of the License or (at your opinion) any later version of the license. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. -""" -Created on April 9, 2010 -Model for RhodeCode -@author: marcink -""" -from beaker.cache import cache_region -from mercurial import ui -from mercurial.hgweb.hgwebdir_mod import findrepos -from pylons.i18n.translation import _ -from rhodecode.lib import helpers as h -from rhodecode.lib.utils import invalidate_cache -from rhodecode.lib.auth import HasRepoPermissionAny -from rhodecode.model import meta -from rhodecode.model.db import Repository, User -from sqlalchemy.orm import joinedload -from vcs.exceptions import RepositoryError, VCSError -import logging -import os -import sys -log = logging.getLogger(__name__) - -try: - from vcs.backends.hg import MercurialRepository -except ImportError: - sys.stderr.write('You have to import vcs module') - raise Exception('Unable to import vcs') - -def _get_repos_cached_initial(app_globals, initial): - """return cached dict with repos - """ - g = app_globals - return HgModel.repo_scan(g.paths[0][0], g.paths[0][1], g.baseui, initial) - -@cache_region('long_term', 'cached_repo_list') -def _get_repos_cached(): - """return cached dict with repos - """ - log.info('getting all repositories list') - from pylons import app_globals as g - return HgModel.repo_scan(g.paths[0][0], g.paths[0][1], g.baseui) - -@cache_region('super_short_term', 'cached_repos_switcher_list') -def _get_repos_switcher_cached(cached_repo_list): - repos_lst = [] - for repo in [x for x in cached_repo_list.values()]: - if HasRepoPermissionAny('repository.write', 'repository.read', - 'repository.admin')(repo.name, 'main page check'): - repos_lst.append((repo.name, repo.dbrepo.private,)) - - return sorted(repos_lst, key=lambda k:k[0].lower()) - -@cache_region('long_term', 'full_changelog') -def _full_changelog_cached(repo_name): - log.info('getting full changelog for %s', repo_name) - return list(reversed(list(HgModel().get_repo(repo_name)))) - -class HgModel(object): - """Mercurial Model - """ - - def __init__(self): - pass - - @staticmethod - def repo_scan(repos_prefix, repos_path, baseui, initial=False): - """ - Listing of repositories in given path. This path should not be a - repository itself. Return a dictionary of repository objects - :param repos_path: path to directory it could take syntax with - * or ** for deep recursive displaying repositories - """ - sa = meta.Session() - def check_repo_dir(path): - """Checks the repository - :param path: - """ - repos_path = path.split('/') - if repos_path[-1] in ['*', '**']: - repos_path = repos_path[:-1] - if repos_path[0] != '/': - repos_path[0] = '/' - if not os.path.isdir(os.path.join(*repos_path)): - raise RepositoryError('Not a valid repository in %s' % path) - if not repos_path.endswith('*'): - raise VCSError('You need to specify * or ** at the end of path ' - 'for recursive scanning') - - check_repo_dir(repos_path) - log.info('scanning for repositories in %s', repos_path) - repos = findrepos([(repos_prefix, repos_path)]) - if not isinstance(baseui, ui.ui): - baseui = ui.ui() - - repos_list = {} - for name, path in repos: - try: - #name = name.split('/')[-1] - if repos_list.has_key(name): - raise RepositoryError('Duplicate repository name %s found in' - ' %s' % (name, path)) - else: - - repos_list[name] = MercurialRepository(path, baseui=baseui) - repos_list[name].name = name - - dbrepo = None - if not initial: - #for initial scann on application first run we don't - #have db repos yet. - dbrepo = sa.query(Repository)\ - .options(joinedload(Repository.fork))\ - .filter(Repository.repo_name == name)\ - .scalar() - - if dbrepo: - log.info('Adding db instance to cached list') - repos_list[name].dbrepo = dbrepo - repos_list[name].description = dbrepo.description - if dbrepo.user: - repos_list[name].contact = dbrepo.user.full_contact - else: - repos_list[name].contact = sa.query(User)\ - .filter(User.admin == True).first().full_contact - except OSError: - continue - meta.Session.remove() - return repos_list - - def get_repos(self): - for name, repo in _get_repos_cached().items(): - if repo._get_hidden(): - #skip hidden web repository - continue - - last_change = repo.last_change - tip = h.get_changeset_safe(repo, 'tip') - - tmp_d = {} - tmp_d['name'] = repo.name - tmp_d['name_sort'] = tmp_d['name'].lower() - tmp_d['description'] = repo.description - tmp_d['description_sort'] = tmp_d['description'] - tmp_d['last_change'] = last_change - tmp_d['last_change_sort'] = last_change[1] - last_change[0] - tmp_d['tip'] = tip.short_id - tmp_d['tip_sort'] = tip.revision - tmp_d['rev'] = tip.revision - tmp_d['contact'] = repo.contact - tmp_d['contact_sort'] = tmp_d['contact'] - tmp_d['repo_archives'] = list(repo._get_archives()) - tmp_d['last_msg'] = tip.message - tmp_d['repo'] = repo - yield tmp_d - - def get_repo(self, repo_name): - try: - repo = _get_repos_cached()[repo_name] - return repo - except KeyError: - #i we're here and we got key errors let's try to invalidate the - #cahce and try again - invalidate_cache('cached_repo_list') - repo = _get_repos_cached()[repo_name] - return repo - - - diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/model/permission.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/model/permission.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.model.permission + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + permissions model for RhodeCode + + :created_on: Aug 20, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +import logging +import traceback + +from sqlalchemy.exc import DatabaseError + +from rhodecode.model import BaseModel +from rhodecode.model.db import User, Permission, UserToPerm, RepoToPerm +from rhodecode.model.caching_query import FromCache + +log = logging.getLogger(__name__) + + +class PermissionModel(BaseModel): + """Permissions model for RhodeCode + """ + + def get_permission(self, permission_id, cache=False): + """Get's permissions by id + + :param permission_id: id of permission to get from database + :param cache: use Cache for this query + """ + perm = self.sa.query(Permission) + if cache: + perm = perm.options(FromCache("sql_cache_short", + "get_permission_%s" % permission_id)) + return perm.get(permission_id) + + def get_permission_by_name(self, name, cache=False): + """Get's permissions by given name + + :param name: name to fetch + :param cache: Use cache for this query + """ + perm = self.sa.query(Permission)\ + .filter(Permission.permission_name == name) + if cache: + perm = perm.options(FromCache("sql_cache_short", + "get_permission_%s" % name)) + return perm.scalar() + + def update(self, form_result): + perm_user = self.sa.query(User)\ + .filter(User.username == form_result['perm_user_name']).scalar() + u2p = self.sa.query(UserToPerm).filter(UserToPerm.user == perm_user).all() + if len(u2p) != 3: + raise Exception('Defined: %s should be 3 permissions for default' + ' user. This should not happen please verify' + ' your database' % len(u2p)) + + try: + #stage 1 change defaults + for p in u2p: + if p.permission.permission_name.startswith('repository.'): + p.permission = self.get_permission_by_name( + form_result['default_perm']) + self.sa.add(p) + + if p.permission.permission_name.startswith('hg.register.'): + p.permission = self.get_permission_by_name( + form_result['default_register']) + self.sa.add(p) + + if p.permission.permission_name.startswith('hg.create.'): + p.permission = self.get_permission_by_name( + form_result['default_create']) + self.sa.add(p) + + #stage 2 update all default permissions for repos if checked + if form_result['overwrite_default'] == True: + for r2p in self.sa.query(RepoToPerm)\ + .filter(RepoToPerm.user == perm_user).all(): + r2p.permission = self.get_permission_by_name( + form_result['default_perm']) + self.sa.add(r2p) + + #stage 3 set anonymous access + if perm_user.username == 'default': + perm_user.active = bool(form_result['anonymous']) + self.sa.add(perm_user) + + + self.sa.commit() + except (DatabaseError,): + log.error(traceback.format_exc()) + self.sa.rollback() + raise diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/model/permission_model.py --- a/rhodecode/model/permission_model.py Thu Nov 18 21:35:52 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,88 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -# Model for permissions -# Copyright (C) 2009-2010 Marcin Kuzminski - -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; version 2 -# of the License or (at your opinion) any later version of the license. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. -""" -Created on Aug 20, 2010 -Model for permissions -@author: marcink -""" - -from pylons.i18n.translation import _ -from rhodecode.model.db import User, Permission, UserToPerm, RepoToPerm -from rhodecode.model.meta import Session -import logging -import traceback -log = logging.getLogger(__name__) - - -class PermissionModel(object): - - def __init__(self): - self.sa = Session() - - def get_default(self): - return self.sa.query(User).filter(User.username == 'default').scalar() - - def get_permission(self, id): - return self.sa.query(Permission).get(id) - - def get_permission_by_name(self, name): - return self.sa.query(Permission)\ - .filter(Permission.permission_name == name).scalar() - - - def update(self, form_result): - perm_user = self.sa.query(User)\ - .filter(User.username == form_result['perm_user_name']).scalar() - u2p = self.sa.query(UserToPerm).filter(UserToPerm.user == perm_user).all() - if len(u2p) != 3: - raise Exception('There is more than 3 defined \ - permissions for defualt user. This should not happen please verify\ - your database') - - try: - #stage 1 change defaults - for p in u2p: - if p.permission.permission_name.startswith('repository.'): - p.permission = self.get_permission_by_name(form_result['default_perm']) - self.sa.add(p) - - if p.permission.permission_name.startswith('hg.register.'): - p.permission = self.get_permission_by_name(form_result['default_register']) - self.sa.add(p) - - if p.permission.permission_name.startswith('hg.create.'): - p.permission = self.get_permission_by_name(form_result['default_create']) - self.sa.add(p) - #stage 2 update all default permissions for repos if checked - if form_result['overwrite_default'] == 'true': - for r2p in self.sa.query(RepoToPerm).filter(RepoToPerm.user == perm_user).all(): - r2p.permission = self.get_permission_by_name(form_result['default_perm']) - self.sa.add(r2p) - - self.sa.commit() - except: - log.error(traceback.format_exc()) - self.sa.rollback() - raise - - - - - diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/model/repo.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/model/repo.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.model.repo + ~~~~~~~~~~~~~~~~~~~~ + + Repository model for rhodecode + + :created_on: Jun 5, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +import os +import shutil +import logging +import traceback +from datetime import datetime + +from pylons import app_globals as g + +from rhodecode.model import BaseModel +from rhodecode.model.caching_query import FromCache +from rhodecode.model.db import Repository, RepoToPerm, User, Permission, \ + Statistics +from rhodecode.model.user import UserModel + +from vcs.backends import get_backend + +log = logging.getLogger(__name__) + +class RepoModel(BaseModel): + + def get(self, repo_id, cache=False): + repo = self.sa.query(Repository)\ + .filter(Repository.repo_id == repo_id) + + if cache: + repo = repo.options(FromCache("sql_cache_short", + "get_repo_%s" % repo_id)) + return repo.scalar() + + + def get_by_repo_name(self, repo_name, cache=False): + repo = self.sa.query(Repository)\ + .filter(Repository.repo_name == repo_name) + + if cache: + repo = repo.options(FromCache("sql_cache_short", + "get_repo_%s" % repo_name)) + return repo.scalar() + + def get_users_js(self): + + users = self.sa.query(User).filter(User.active == True).all() + u_tmpl = '''{id:%s, fname:"%s", lname:"%s", nname:"%s"},''' + users_array = '[%s];' % '\n'.join([u_tmpl % (u.user_id, u.name, + u.lastname, u.username) + for u in users]) + return users_array + + + def update(self, repo_name, form_data): + try: + cur_repo = self.get_by_repo_name(repo_name, cache=False) + user_model = UserModel(self.sa) + + #update permissions + for username, perm in form_data['perms_updates']: + r2p = self.sa.query(RepoToPerm)\ + .filter(RepoToPerm.user == user_model.get_by_username(username))\ + .filter(RepoToPerm.repository == cur_repo)\ + .one() + + r2p.permission = self.sa.query(Permission)\ + .filter(Permission.permission_name == perm)\ + .scalar() + self.sa.add(r2p) + + #set new permissions + for username, perm in form_data['perms_new']: + r2p = RepoToPerm() + r2p.repository = cur_repo + r2p.user = user_model.get_by_username(username, cache=False) + + r2p.permission = self.sa.query(Permission)\ + .filter(Permission.permission_name == perm)\ + .scalar() + self.sa.add(r2p) + + #update current repo + for k, v in form_data.items(): + if k == 'user': + cur_repo.user = user_model.get(v) + else: + setattr(cur_repo, k, v) + + self.sa.add(cur_repo) + + if repo_name != form_data['repo_name']: + #rename our data + self.__rename_repo(repo_name, form_data['repo_name']) + + self.sa.commit() + except: + log.error(traceback.format_exc()) + self.sa.rollback() + raise + + def create(self, form_data, cur_user, just_db=False, fork=False): + try: + if fork: + #force str since hg doesn't go with unicode + repo_name = str(form_data['fork_name']) + org_name = str(form_data['repo_name']) + + else: + org_name = repo_name = str(form_data['repo_name']) + new_repo = Repository() + new_repo.enable_statistics = True + for k, v in form_data.items(): + if k == 'repo_name': + v = repo_name + setattr(new_repo, k, v) + + if fork: + parent_repo = self.sa.query(Repository)\ + .filter(Repository.repo_name == org_name).scalar() + new_repo.fork = parent_repo + + new_repo.user_id = cur_user.user_id + self.sa.add(new_repo) + + #create default permission + repo_to_perm = RepoToPerm() + default = 'repository.read' + for p in UserModel(self.sa).get_by_username('default', cache=False).user_perms: + if p.permission.permission_name.startswith('repository.'): + default = p.permission.permission_name + break + + default_perm = 'repository.none' if form_data['private'] else default + + repo_to_perm.permission_id = self.sa.query(Permission)\ + .filter(Permission.permission_name == default_perm)\ + .one().permission_id + + repo_to_perm.repository_id = new_repo.repo_id + repo_to_perm.user_id = UserModel(self.sa)\ + .get_by_username('default', cache=False).user_id + + self.sa.add(repo_to_perm) + self.sa.commit() + + + #now automatically start following this repository as owner + from rhodecode.model.scm import ScmModel + ScmModel(self.sa).toggle_following_repo(new_repo.repo_id, + cur_user.user_id) + + if not just_db: + self.__create_repo(repo_name, form_data['repo_type']) + except: + log.error(traceback.format_exc()) + self.sa.rollback() + raise + + def create_fork(self, form_data, cur_user): + from rhodecode.lib.celerylib import tasks, run_task + run_task(tasks.create_repo_fork, form_data, cur_user) + + def delete(self, repo): + try: + self.sa.delete(repo) + self.__delete_repo(repo) + self.sa.commit() + except: + log.error(traceback.format_exc()) + self.sa.rollback() + raise + + def delete_perm_user(self, form_data, repo_name): + try: + self.sa.query(RepoToPerm)\ + .filter(RepoToPerm.repository \ + == self.get_by_repo_name(repo_name))\ + .filter(RepoToPerm.user_id == form_data['user_id']).delete() + self.sa.commit() + except: + log.error(traceback.format_exc()) + self.sa.rollback() + raise + + def delete_stats(self, repo_name): + try: + self.sa.query(Statistics)\ + .filter(Statistics.repository == \ + self.get_by_repo_name(repo_name)).delete() + self.sa.commit() + except: + log.error(traceback.format_exc()) + self.sa.rollback() + raise + + + def __create_repo(self, repo_name, alias): + """ + makes repository on filesystem + :param repo_name: + :param alias: + """ + from rhodecode.lib.utils import check_repo + repo_path = os.path.join(g.base_path, repo_name) + if check_repo(repo_name, g.base_path): + log.info('creating repo %s in %s', repo_name, repo_path) + backend = get_backend(alias) + backend(repo_path, create=True) + + def __rename_repo(self, old, new): + """ + renames repository on filesystem + :param old: old name + :param new: new name + """ + log.info('renaming repo from %s to %s', old, new) + + old_path = os.path.join(g.base_path, old) + new_path = os.path.join(g.base_path, new) + if os.path.isdir(new_path): + raise Exception('Was trying to rename to already existing dir %s', + new_path) + shutil.move(old_path, new_path) + + def __delete_repo(self, repo): + """ + removes repo from filesystem, the removal is acctually made by + added rm__ prefix into dir, and rename internat .hg/.git dirs so this + repository is no longer valid for rhodecode, can be undeleted later on + by reverting the renames on this repository + :param repo: repo object + """ + rm_path = os.path.join(g.base_path, repo.repo_name) + log.info("Removing %s", rm_path) + #disable hg/git + alias = repo.repo_type + shutil.move(os.path.join(rm_path, '.%s' % alias), + os.path.join(rm_path, 'rm__.%s' % alias)) + #disable repo + shutil.move(rm_path, os.path.join(g.base_path, 'rm__%s__%s' \ + % (datetime.today(), repo.repo_name))) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/model/repo_model.py --- a/rhodecode/model/repo_model.py Thu Nov 18 21:35:52 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,205 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -# model for handling repositories actions -# Copyright (C) 2009-2010 Marcin Kuzminski -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; version 2 -# of the License or (at your opinion) any later version of the license. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. -""" -Created on Jun 5, 2010 -model for handling repositories actions -@author: marcink -""" -from datetime import datetime -from pylons import app_globals as g -from rhodecode.lib.utils import check_repo -from rhodecode.model.db import Repository, RepoToPerm, User, Permission -from rhodecode.model.meta import Session -from rhodecode.model.user_model import UserModel -from rhodecode.lib.celerylib.tasks import create_repo_fork, run_task -import logging -import os -import shutil -import traceback -log = logging.getLogger(__name__) - -class RepoModel(object): - - def __init__(self, sa=None): - if not sa: - self.sa = Session() - else: - self.sa = sa - - def get(self, id): - return self.sa.query(Repository)\ - .filter(Repository.repo_name == id).scalar() - - def get_users_js(self): - - users = self.sa.query(User).filter(User.active == True).all() - u_tmpl = '''{id:%s, fname:"%s", lname:"%s", nname:"%s"},''' - users_array = '[%s];' % '\n'.join([u_tmpl % (u.user_id, u.name, - u.lastname, u.username) - for u in users]) - return users_array - - - def update(self, repo_name, form_data): - try: - - #update permissions - for username, perm in form_data['perms_updates']: - r2p = self.sa.query(RepoToPerm)\ - .filter(RepoToPerm.user == self.sa.query(User)\ - .filter(User.username == username).one())\ - .filter(RepoToPerm.repository == self.get(repo_name))\ - .one() - - r2p.permission_id = self.sa.query(Permission).filter( - Permission.permission_name == - perm).one().permission_id - self.sa.add(r2p) - - #set new permissions - for username, perm in form_data['perms_new']: - r2p = RepoToPerm() - r2p.repository = self.get(repo_name) - r2p.user = self.sa.query(User)\ - .filter(User.username == username).one() - - r2p.permission_id = self.sa.query(Permission).filter( - Permission.permission_name == perm)\ - .one().permission_id - self.sa.add(r2p) - - #update current repo - cur_repo = self.get(repo_name) - - for k, v in form_data.items(): - if k == 'user': - cur_repo.user_id = v - else: - setattr(cur_repo, k, v) - - self.sa.add(cur_repo) - - if repo_name != form_data['repo_name']: - #rename our data - self.__rename_repo(repo_name, form_data['repo_name']) - - self.sa.commit() - except: - log.error(traceback.format_exc()) - self.sa.rollback() - raise - - def create(self, form_data, cur_user, just_db=False, fork=False): - try: - if fork: - repo_name = str(form_data['fork_name']) - org_name = str(form_data['repo_name']) - - else: - org_name = repo_name = str(form_data['repo_name']) - new_repo = Repository() - for k, v in form_data.items(): - if k == 'repo_name': - v = repo_name - setattr(new_repo, k, v) - - if fork: - parent_repo = self.sa.query(Repository)\ - .filter(Repository.repo_name == org_name).scalar() - new_repo.fork = parent_repo - - new_repo.user_id = cur_user.user_id - self.sa.add(new_repo) - - #create default permission - repo_to_perm = RepoToPerm() - default = 'repository.read' - for p in UserModel(self.sa).get_default().user_perms: - if p.permission.permission_name.startswith('repository.'): - default = p.permission.permission_name - break - - default_perm = 'repository.none' if form_data['private'] else default - - repo_to_perm.permission_id = self.sa.query(Permission)\ - .filter(Permission.permission_name == default_perm)\ - .one().permission_id - - repo_to_perm.repository_id = new_repo.repo_id - repo_to_perm.user_id = self.sa.query(User)\ - .filter(User.username == 'default').one().user_id - - self.sa.add(repo_to_perm) - self.sa.commit() - if not just_db: - self.__create_repo(repo_name) - except: - log.error(traceback.format_exc()) - self.sa.rollback() - raise - - def create_fork(self, form_data, cur_user): - run_task(create_repo_fork, form_data, cur_user) - - def delete(self, repo): - try: - self.sa.delete(repo) - self.sa.commit() - self.__delete_repo(repo.repo_name) - except: - log.error(traceback.format_exc()) - self.sa.rollback() - raise - - def delete_perm_user(self, form_data, repo_name): - try: - self.sa.query(RepoToPerm)\ - .filter(RepoToPerm.repository == self.get(repo_name))\ - .filter(RepoToPerm.user_id == form_data['user_id']).delete() - self.sa.commit() - except: - log.error(traceback.format_exc()) - self.sa.rollback() - raise - - def __create_repo(self, repo_name): - repo_path = os.path.join(g.base_path, repo_name) - if check_repo(repo_name, g.base_path): - log.info('creating repo %s in %s', repo_name, repo_path) - from vcs.backends.hg import MercurialRepository - MercurialRepository(repo_path, create=True) - - def __rename_repo(self, old, new): - log.info('renaming repo from %s to %s', old, new) - - old_path = os.path.join(g.base_path, old) - new_path = os.path.join(g.base_path, new) - if os.path.isdir(new_path): - raise Exception('Was trying to rename to already existing dir %s', - new_path) - shutil.move(old_path, new_path) - - def __delete_repo(self, name): - rm_path = os.path.join(g.base_path, name) - log.info("Removing %s", rm_path) - #disable hg - shutil.move(os.path.join(rm_path, '.hg'), os.path.join(rm_path, 'rm__.hg')) - #disable repo - shutil.move(rm_path, os.path.join(g.base_path, 'rm__%s__%s' \ - % (datetime.today(), name))) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/model/scm.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/model/scm.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,377 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.model.scm + ~~~~~~~~~~~~~~~~~~~ + + Scm model for RhodeCode + + :created_on: Apr 9, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +import os +import time +import traceback +import logging + +from vcs import get_backend +from vcs.utils.helpers import get_scm +from vcs.exceptions import RepositoryError, VCSError +from vcs.utils.lazy import LazyProperty + +from mercurial import ui + +from beaker.cache import cache_region, region_invalidate + +from rhodecode import BACKENDS +from rhodecode.lib import helpers as h +from rhodecode.lib.auth import HasRepoPermissionAny +from rhodecode.lib.utils import get_repos, make_ui, action_logger +from rhodecode.model import BaseModel +from rhodecode.model.user import UserModel + +from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \ + UserFollowing, UserLog +from rhodecode.model.caching_query import FromCache + +from sqlalchemy.orm import joinedload +from sqlalchemy.orm.session import make_transient +from sqlalchemy.exc import DatabaseError + +log = logging.getLogger(__name__) + + +class UserTemp(object): + def __init__(self, user_id): + self.user_id = user_id +class RepoTemp(object): + def __init__(self, repo_id): + self.repo_id = repo_id + +class ScmModel(BaseModel): + """Generic Scm Model + """ + + @LazyProperty + def repos_path(self): + """Get's the repositories root path from database + """ + + q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one() + + return q.ui_value + + def repo_scan(self, repos_path, baseui): + """Listing of repositories in given path. This path should not be a + repository itself. Return a dictionary of repository objects + + :param repos_path: path to directory containing repositories + :param baseui: baseui instance to instantiate MercurialRepostitory with + """ + + log.info('scanning for repositories in %s', repos_path) + + if not isinstance(baseui, ui.ui): + baseui = make_ui('db') + repos_list = {} + + for name, path in get_repos(repos_path): + try: + if repos_list.has_key(name): + raise RepositoryError('Duplicate repository name %s ' + 'found in %s' % (name, path)) + else: + + klass = get_backend(path[0]) + + if path[0] == 'hg' and path[0] in BACKENDS.keys(): + repos_list[name] = klass(path[1], baseui=baseui) + + if path[0] == 'git' and path[0] in BACKENDS.keys(): + repos_list[name] = klass(path[1]) + except OSError: + continue + + return repos_list + + def get_repos(self, all_repos=None): + """Get all repos from db and for each repo create it's backend instance. + and fill that backed with information from database + + :param all_repos: give specific repositories list, good for filtering + """ + + if all_repos is None: + all_repos = self.sa.query(Repository)\ + .order_by(Repository.repo_name).all() + + #get the repositories that should be invalidated + invalidation_list = [str(x.cache_key) for x in \ + self.sa.query(CacheInvalidation.cache_key)\ + .filter(CacheInvalidation.cache_active == False)\ + .all()] + + for r in all_repos: + + repo = self.get(r.repo_name, invalidation_list) + + if repo is not None: + last_change = repo.last_change + tip = h.get_changeset_safe(repo, 'tip') + + tmp_d = {} + tmp_d['name'] = repo.name + tmp_d['name_sort'] = tmp_d['name'].lower() + tmp_d['description'] = repo.dbrepo.description + tmp_d['description_sort'] = tmp_d['description'] + tmp_d['last_change'] = last_change + tmp_d['last_change_sort'] = time.mktime(last_change.timetuple()) + tmp_d['tip'] = tip.raw_id + tmp_d['tip_sort'] = tip.revision + tmp_d['rev'] = tip.revision + tmp_d['contact'] = repo.dbrepo.user.full_contact + tmp_d['contact_sort'] = tmp_d['contact'] + tmp_d['repo_archives'] = list(repo._get_archives()) + tmp_d['last_msg'] = tip.message + tmp_d['repo'] = repo + yield tmp_d + + def get_repo(self, repo_name): + return self.get(repo_name) + + def get(self, repo_name, invalidation_list=None): + """Get's repository from given name, creates BackendInstance and + propagates it's data from database with all additional information + + :param repo_name: + :param invalidation_list: if a invalidation list is given the get + method should not manually check if this repository needs + invalidation and just invalidate the repositories in list + + """ + if not HasRepoPermissionAny('repository.read', 'repository.write', + 'repository.admin')(repo_name, 'get repo check'): + return + + #====================================================================== + # CACHE FUNCTION + #====================================================================== + @cache_region('long_term') + def _get_repo(repo_name): + + repo_path = os.path.join(self.repos_path, repo_name) + + try: + alias = get_scm(repo_path)[0] + + log.debug('Creating instance of %s repository', alias) + backend = get_backend(alias) + except VCSError: + log.error(traceback.format_exc()) + return + + if alias == 'hg': + from pylons import app_globals as g + repo = backend(repo_path, create=False, baseui=g.baseui) + #skip hidden web repository + if repo._get_hidden(): + return + else: + repo = backend(repo_path, create=False) + + dbrepo = self.sa.query(Repository)\ + .options(joinedload(Repository.fork))\ + .options(joinedload(Repository.user))\ + .filter(Repository.repo_name == repo_name)\ + .scalar() + + make_transient(dbrepo) + if dbrepo.user: + make_transient(dbrepo.user) + if dbrepo.fork: + make_transient(dbrepo.fork) + + repo.dbrepo = dbrepo + return repo + + pre_invalidate = True + if invalidation_list is not None: + pre_invalidate = repo_name in invalidation_list + + if pre_invalidate: + invalidate = self._should_invalidate(repo_name) + + if invalidate: + log.info('invalidating cache for repository %s', repo_name) + region_invalidate(_get_repo, None, repo_name) + self._mark_invalidated(invalidate) + + return _get_repo(repo_name) + + + + def mark_for_invalidation(self, repo_name): + """Puts cache invalidation task into db for + further global cache invalidation + + :param repo_name: this repo that should invalidation take place + """ + + log.debug('marking %s for invalidation', repo_name) + cache = self.sa.query(CacheInvalidation)\ + .filter(CacheInvalidation.cache_key == repo_name).scalar() + + if cache: + #mark this cache as inactive + cache.cache_active = False + else: + log.debug('cache key not found in invalidation db -> creating one') + cache = CacheInvalidation(repo_name) + + try: + self.sa.add(cache) + self.sa.commit() + except (DatabaseError,): + log.error(traceback.format_exc()) + self.sa.rollback() + + + def toggle_following_repo(self, follow_repo_id, user_id): + + f = self.sa.query(UserFollowing)\ + .filter(UserFollowing.follows_repo_id == follow_repo_id)\ + .filter(UserFollowing.user_id == user_id).scalar() + + if f is not None: + + try: + self.sa.delete(f) + self.sa.commit() + action_logger(UserTemp(user_id), + 'stopped_following_repo', + RepoTemp(follow_repo_id)) + return + except: + log.error(traceback.format_exc()) + self.sa.rollback() + raise + + + try: + f = UserFollowing() + f.user_id = user_id + f.follows_repo_id = follow_repo_id + self.sa.add(f) + self.sa.commit() + action_logger(UserTemp(user_id), + 'started_following_repo', + RepoTemp(follow_repo_id)) + except: + log.error(traceback.format_exc()) + self.sa.rollback() + raise + + def toggle_following_user(self, follow_user_id , user_id): + f = self.sa.query(UserFollowing)\ + .filter(UserFollowing.follows_user_id == follow_user_id)\ + .filter(UserFollowing.user_id == user_id).scalar() + + if f is not None: + try: + self.sa.delete(f) + self.sa.commit() + return + except: + log.error(traceback.format_exc()) + self.sa.rollback() + raise + + try: + f = UserFollowing() + f.user_id = user_id + f.follows_user_id = follow_user_id + self.sa.add(f) + self.sa.commit() + except: + log.error(traceback.format_exc()) + self.sa.rollback() + raise + + def is_following_repo(self, repo_name, user_id): + r = self.sa.query(Repository)\ + .filter(Repository.repo_name == repo_name).scalar() + + f = self.sa.query(UserFollowing)\ + .filter(UserFollowing.follows_repository == r)\ + .filter(UserFollowing.user_id == user_id).scalar() + + return f is not None + + def is_following_user(self, username, user_id): + u = UserModel(self.sa).get_by_username(username) + + f = self.sa.query(UserFollowing)\ + .filter(UserFollowing.follows_user == u)\ + .filter(UserFollowing.user_id == user_id).scalar() + + return f is not None + + def get_followers(self, repo_id): + return self.sa.query(UserFollowing)\ + .filter(UserFollowing.follows_repo_id == repo_id).count() + + def get_forks(self, repo_id): + return self.sa.query(Repository)\ + .filter(Repository.fork_id == repo_id).count() + + + def get_unread_journal(self): + return self.sa.query(UserLog).count() + + + def _should_invalidate(self, repo_name): + """Looks up database for invalidation signals for this repo_name + + :param repo_name: + """ + + ret = self.sa.query(CacheInvalidation)\ + .options(FromCache('sql_cache_short', + 'get_invalidation_%s' % repo_name))\ + .filter(CacheInvalidation.cache_key == repo_name)\ + .filter(CacheInvalidation.cache_active == False)\ + .scalar() + + return ret + + def _mark_invalidated(self, cache_key): + """ Marks all occurences of cache to invaldation as already invalidated + + :param cache_key: + """ + + if cache_key: + log.debug('marking %s as already invalidated', cache_key) + try: + cache_key.cache_active = True + self.sa.add(cache_key) + self.sa.commit() + except (DatabaseError,): + log.error(traceback.format_exc()) + self.sa.rollback() + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/model/settings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/model/settings.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# encoding: utf-8 +# Model for RhodeCode settings +# Copyright (C) 2009-2010 Marcin Kuzminski +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +""" +Created on Nov 17, 2010 +Model for RhodeCode +:author: marcink +""" + +from rhodecode.lib import helpers as h +from rhodecode.model import BaseModel +from rhodecode.model.caching_query import FromCache +from rhodecode.model.db import RhodeCodeSettings +from sqlalchemy.orm import joinedload +import logging + +log = logging.getLogger(__name__) + +class SettingsModel(BaseModel): + """ + Settings model + """ + + def get(self, settings_key, cache=False): + r = self.sa.query(RhodeCodeSettings)\ + .filter(RhodeCodeSettings.app_settings_name == settings_key).scalar() + if cache: + r = r.options(FromCache("sql_cache_short", + "get_setting_%s" % settings_key)) + return r + + def get_app_settings(self): + ret = self.sa.query(RhodeCodeSettings)\ + .options(FromCache("sql_cache_short", + "get_hg_settings")).all() + + if not ret: + raise Exception('Could not get application settings !') + settings = {} + for each in ret: + settings['rhodecode_' + each.app_settings_name] = each.app_settings_value + + return settings + + def get_ldap_settings(self): + """ + Returns ldap settings from database + :returns: + ldap_active + ldap_host + ldap_port + ldap_ldaps + ldap_dn_user + ldap_dn_pass + ldap_base_dn + """ + + r = self.sa.query(RhodeCodeSettings)\ + .filter(RhodeCodeSettings.app_settings_name\ + .startswith('ldap_'))\ + .all() + + fd = {} + + for row in r: + v = row.app_settings_value + if v in ['0', '1']: + v = v == '1' + fd.update({row.app_settings_name:v}) + + return fd diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/model/user.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/model/user.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +""" + package.rhodecode.model.user + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + users model for RhodeCode + + :created_on: Apr 9, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +import logging +import traceback + +from pylons.i18n.translation import _ + +from rhodecode.model import BaseModel +from rhodecode.model.caching_query import FromCache +from rhodecode.model.db import User + +from rhodecode.lib.exceptions import DefaultUserException, UserOwnsReposException + +from sqlalchemy.exc import DatabaseError + +log = logging.getLogger(__name__) + +class UserModel(BaseModel): + + def get(self, user_id, cache=False): + user = self.sa.query(User) + if cache: + user = user.options(FromCache("sql_cache_short", + "get_user_%s" % user_id)) + return user.get(user_id) + + + def get_by_username(self, username, cache=False, case_insensitive=False): + + if case_insensitive: + user = self.sa.query(User).filter(User.username.ilike(username)) + else: + user = self.sa.query(User)\ + .filter(User.username == username) + if cache: + user = user.options(FromCache("sql_cache_short", + "get_user_%s" % username)) + return user.scalar() + + def create(self, form_data): + try: + new_user = User() + for k, v in form_data.items(): + setattr(new_user, k, v) + + self.sa.add(new_user) + self.sa.commit() + except: + log.error(traceback.format_exc()) + self.sa.rollback() + raise + + def create_ldap(self, username, password): + """ + Checks if user is in database, if not creates this user marked + as ldap user + :param username: + :param password: + """ + from rhodecode.lib.auth import get_crypt_password + log.debug('Checking for such ldap account in RhodeCode database') + if self.get_by_username(username, case_insensitive=True) is None: + try: + new_user = User() + new_user.username = username.lower()#add ldap account always lowercase + new_user.password = get_crypt_password(password) + new_user.email = '%s@ldap.server' % username + new_user.active = True + new_user.is_ldap = True + new_user.name = '%s@ldap' % username + new_user.lastname = '' + + + self.sa.add(new_user) + self.sa.commit() + return True + except (DatabaseError,): + log.error(traceback.format_exc()) + self.sa.rollback() + raise + log.debug('this %s user exists skipping creation of ldap account', + username) + return False + + def create_registration(self, form_data): + from rhodecode.lib.celerylib import tasks, run_task + try: + new_user = User() + for k, v in form_data.items(): + if k != 'admin': + setattr(new_user, k, v) + + self.sa.add(new_user) + self.sa.commit() + body = ('New user registration\n' + 'username: %s\n' + 'email: %s\n') + body = body % (form_data['username'], form_data['email']) + + run_task(tasks.send_email, None, + _('[RhodeCode] New User registration'), + body) + except: + log.error(traceback.format_exc()) + self.sa.rollback() + raise + + def update(self, user_id, form_data): + try: + new_user = self.get(user_id, cache=False) + if new_user.username == 'default': + raise DefaultUserException( + _("You can't Edit this user since it's" + " crucial for entire application")) + + for k, v in form_data.items(): + if k == 'new_password' and v != '': + new_user.password = v + else: + setattr(new_user, k, v) + + self.sa.add(new_user) + self.sa.commit() + except: + log.error(traceback.format_exc()) + self.sa.rollback() + raise + + def update_my_account(self, user_id, form_data): + try: + new_user = self.get(user_id, cache=False) + if new_user.username == 'default': + raise DefaultUserException( + _("You can't Edit this user since it's" + " crucial for entire application")) + for k, v in form_data.items(): + if k == 'new_password' and v != '': + new_user.password = v + else: + if k not in ['admin', 'active']: + setattr(new_user, k, v) + + self.sa.add(new_user) + self.sa.commit() + except: + log.error(traceback.format_exc()) + self.sa.rollback() + raise + + def delete(self, user_id): + try: + user = self.get(user_id, cache=False) + if user.username == 'default': + raise DefaultUserException( + _("You can't remove this user since it's" + " crucial for entire application")) + if user.repositories: + raise UserOwnsReposException(_('This user still owns %s ' + 'repositories and cannot be ' + 'removed. Switch owners or ' + 'remove those repositories') \ + % user.repositories) + self.sa.delete(user) + self.sa.commit() + except: + log.error(traceback.format_exc()) + self.sa.rollback() + raise + + def reset_password(self, data): + from rhodecode.lib.celerylib import tasks, run_task + run_task(tasks.reset_user_password, data['email']) + + + def fill_data(self, user): + """ + Fills user data with those from database and log out user if not + present in database + :param user: + """ + + if not hasattr(user, 'user_id') or user.user_id is None: + raise Exception('passed in user has to have the user_id attribute') + + + log.debug('filling auth user data') + try: + dbuser = self.get(user.user_id) + user.username = dbuser.username + user.is_admin = dbuser.admin + user.name = dbuser.name + user.lastname = dbuser.lastname + user.email = dbuser.email + except: + log.error(traceback.format_exc()) + user.is_authenticated = False + + return user diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/model/user_model.py --- a/rhodecode/model/user_model.py Thu Nov 18 21:35:52 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,138 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -# Model for users -# Copyright (C) 2009-2010 Marcin Kuzminski -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; version 2 -# of the License or (at your opinion) any later version of the license. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. - -""" -Created on April 9, 2010 -Model for users -@author: marcink -""" -from rhodecode.lib import auth -from pylons.i18n.translation import _ -from rhodecode.lib.celerylib import tasks, run_task -from rhodecode.model.db import User -from rhodecode.model.meta import Session -import traceback -import logging -log = logging.getLogger(__name__) - -class DefaultUserException(Exception):pass - -class UserModel(object): - - def __init__(self, sa=None): - if not sa: - self.sa = Session() - else: - self.sa = sa - - def get_default(self): - return self.sa.query(User).filter(User.username == 'default').scalar() - - def get_user(self, id): - return self.sa.query(User).get(id) - - def get_user_by_name(self, name): - return self.sa.query(User).filter(User.username == name).scalar() - - def create(self, form_data): - try: - new_user = User() - for k, v in form_data.items(): - setattr(new_user, k, v) - - self.sa.add(new_user) - self.sa.commit() - except: - log.error(traceback.format_exc()) - self.sa.rollback() - raise - - def create_registration(self, form_data): - try: - new_user = User() - for k, v in form_data.items(): - if k != 'admin': - setattr(new_user, k, v) - - self.sa.add(new_user) - self.sa.commit() - except: - log.error(traceback.format_exc()) - self.sa.rollback() - raise - - def update(self, uid, form_data): - try: - new_user = self.sa.query(User).get(uid) - if new_user.username == 'default': - raise DefaultUserException( - _("You can't Edit this user since it's" - " crucial for entire application")) - for k, v in form_data.items(): - if k == 'new_password' and v != '': - new_user.password = v - else: - setattr(new_user, k, v) - - self.sa.add(new_user) - self.sa.commit() - except: - log.error(traceback.format_exc()) - self.sa.rollback() - raise - - def update_my_account(self, uid, form_data): - try: - new_user = self.sa.query(User).get(uid) - if new_user.username == 'default': - raise DefaultUserException( - _("You can't Edit this user since it's" - " crucial for entire application")) - for k, v in form_data.items(): - if k == 'new_password' and v != '': - new_user.password = v - else: - if k not in ['admin', 'active']: - setattr(new_user, k, v) - - self.sa.add(new_user) - self.sa.commit() - except: - log.error(traceback.format_exc()) - self.sa.rollback() - raise - - def delete(self, id): - try: - - user = self.sa.query(User).get(id) - if user.username == 'default': - raise DefaultUserException( - _("You can't remove this user since it's" - " crucial for entire application")) - self.sa.delete(user) - self.sa.commit() - except: - log.error(traceback.format_exc()) - self.sa.rollback() - raise - - def reset_password(self, data): - run_task(tasks.reset_user_password, data['email']) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/public/css/diff.css --- a/rhodecode/public/css/diff.css Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/public/css/diff.css Sat Dec 18 14:45:58 2010 +0100 @@ -62,7 +62,6 @@ .code-difftable .lineno{ background:none repeat scroll 0 0 #EEEEEE !important; - border-right:1px solid #DDDDDD; padding-left:2px; padding-right:2px; text-align:right; @@ -70,6 +69,12 @@ -moz-user-select:none; -webkit-user-select: none; } +.code-difftable .new { + border-right: 1px solid #CCC !important; +} +.code-difftable .old { + border-right: 1px solid #CCC !important; +} .code-difftable .lineno pre{ color:#747474 !important; font:11px "Bitstream Vera Sans Mono",Monaco,"Courier New",Courier,monospace !important; @@ -78,7 +83,8 @@ width:20px; } .code-difftable .lineno a{ - color:#0000CC !important; +font-weight: 700; +cursor: pointer; } .code-difftable .code td{ margin:0; diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/public/css/style.css --- a/rhodecode/public/css/style.css Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/public/css/style.css Sat Dec 18 14:45:58 2010 +0100 @@ -190,18 +190,32 @@ padding:0 30px; } + +#header ul#logged-user{ +margin-bottom:5px !important; +-webkit-border-radius: 0px 0px 8px 8px; +-khtml-border-radius: 0px 0px 8px 8px; +-moz-border-radius: 0px 0px 8px 8px; +border-radius: 0px 0px 8px 8px; +height:37px; +background:url("../images/header_inner.png") repeat-x scroll 0 0 #003367 +} + #header ul#logged-user li { list-style:none; float:left; -border-left:1px solid #bbb; -border-right:1px solid #a5a5a5; -margin:-2px 0 0; -padding:10px 12px; +margin:8px 0 0; +padding:4px 12px; +border-left: 1px solid #316293; } #header ul#logged-user li.first { border-left:none; -margin:-6px; +margin:4px; +} + +#header ul#logged-user li.first div.gravatar { +margin-top:-2px; } #header ul#logged-user li.first div.account { @@ -214,13 +228,12 @@ } #header ul#logged-user li a { -color:#4e4e4e; +color:#fff; font-weight:700; text-decoration:none; } #header ul#logged-user li a:hover { -color:#376ea6; text-decoration:underline; } @@ -229,7 +242,7 @@ } #header ul#logged-user li.highlight a:hover { -color:#376ea6; +color:#FFF; } #header #header-inner { @@ -258,9 +271,8 @@ #header #header-inner #logo h1 { color:#FFF; -font-size:14px; -text-transform:uppercase; -margin:13px 0 0 13px; +font-size:18px; +margin:10px 0 0 13px; padding:0; } @@ -303,6 +315,10 @@ padding:0; } +#header #header-inner #quick li span.short { +padding:9px 6px 8px 6px; +} + #header #header-inner #quick li span { top:0; right:0; @@ -329,6 +345,15 @@ padding:8px 8px 4px; } +#header #header-inner #quick li span.icon_short { +top:0; +left:0; +border-left:none; +background:url("../../images/quick_l.png") no-repeat top left; +border-right:1px solid #2e5c89; +padding:9px 4px 4px; +} + #header #header-inner #quick li a:hover { background:#4e4e4e url("../../images/quick_l_selected.png") no-repeat top left; } @@ -338,12 +363,13 @@ background:url("../../images/quick_r_selected.png") no-repeat top right; } -#header #header-inner #quick li a:hover span.icon { +#header #header-inner #quick li a:hover span.icon,#header #header-inner #quick li a:hover span.icon_short { border-left:none; border-right:1px solid #464646; background:url("../../images/quick_l_selected.png") no-repeat top left; } + #header #header-inner #quick ul { top:29px; right:0; @@ -364,6 +390,12 @@ overflow-y:auto; } +#header #header-inner #quick .repo_switcher_type{ +position:absolute; +left:0; +top:9px; + +} #header #header-inner #quick li ul li { border-bottom:1px solid #ddd; } @@ -394,7 +426,7 @@ max-height:275px; overflow:auto; overflow-x:hidden; -white-space:nowrap; +white-space:normal; } #header #header-inner #quick li ul li a.journal,#header #header-inner #quick li ul li a.journal:hover { @@ -418,6 +450,20 @@ padding:12px 9px 7px 24px; } +#header #header-inner #quick li ul li a.hg,#header #header-inner #quick li ul li a.hg:hover { +background:url("../images/icons/hgicon.png") no-repeat scroll 4px 9px #FFF; +min-width:167px; +margin:0 0 0 14px; +padding:12px 9px 7px 24px; +} + +#header #header-inner #quick li ul li a.git,#header #header-inner #quick li ul li a.git:hover { +background:url("../images/icons/giticon.png") no-repeat scroll 4px 9px #FFF; +min-width:167px; +margin:0 0 0 14px; +padding:12px 9px 7px 24px; +} + #header #header-inner #quick li ul li a.repos,#header #header-inner #quick li ul li a.repos:hover { background:url("../images/icons/database_edit.png") no-repeat scroll 4px 9px #FFF; width:167px; @@ -446,6 +492,13 @@ padding:12px 9px 7px 24px; } +#header #header-inner #quick li ul li a.ldap,#header #header-inner #quick li ul li a.ldap:hover { +background:#FFF url("../images/icons/server_key.png") no-repeat 4px 9px; +width:167px; +margin:0; +padding:12px 9px 7px 24px; +} + #header #header-inner #quick li ul li a.fork,#header #header-inner #quick li ul li a.fork:hover { background:#FFF url("../images/icons/arrow_divide.png") no-repeat 4px 9px; width:167px; @@ -787,7 +840,7 @@ } #content div.box div.form div.fields div.field div.label-select { -padding:2px 0 0 5px; +padding:5px 0 0 5px; } #content div.box-left div.form div.fields div.field div.label-select,#content div.box-right div.form div.fields div.field div.label-select { @@ -806,16 +859,8 @@ #content div.box div.form div.fields div.field div.input { margin:0 0 0 200px; } - #content div.box-left div.form div.fields div.field div.input,#content div.box-right div.form div.fields div.field div.input { -clear:both; -overflow:hidden; -border-top:1px solid #b3b3b3; -border-left:1px solid #b3b3b3; -border-right:1px solid #eaeaea; -border-bottom:1px solid #eaeaea; -margin:0; -padding:7px 7px 6px; +margin:0 0 0 0px; } #content div.box div.form div.fields div.field div.input input { @@ -831,11 +876,7 @@ padding:7px 7px 6px; } -#content div.box-left div.form div.fields div.field div.input input,#content div.box-right div.form div.fields div.field div.input input { -width:100%; -border:none; -padding:0; -} + #content div.box div.form div.fields div.field div.input input.small { width:30%; @@ -1320,7 +1361,6 @@ } #register div.title { -width:420px; clear:both; overflow:hidden; position:relative; @@ -1330,7 +1370,6 @@ } #register div.inner { -width:380px; background:#FFF; border-top:none; border-bottom:none; @@ -1339,7 +1378,7 @@ } #register div.form div.fields div.field div.label { -width:100px; +width:135px; float:left; text-align:right; margin:2px 10px 0 0; @@ -1347,7 +1386,7 @@ } #register div.form div.fields div.field div.input input { -width:245px; +width:300px; background:#FFF; border-top:1px solid #b3b3b3; border-left:1px solid #b3b3b3; @@ -1366,7 +1405,7 @@ border-top:1px solid #DDD; text-align:left; margin:0; -padding:10px 0 0 114px; +padding:10px 0 0 150px; } #register div.form div.fields div.buttons div.highlight input.ui-state-default { @@ -1525,6 +1564,7 @@ float:right; text-align:center; min-width:15px; +cursor: help; } .right .changes .added { @@ -1618,6 +1658,17 @@ padding-left:20px; text-align:left; } +.diffblock .changeset_file{ +background:url("../images/icons/file.png") no-repeat scroll 3px; +height:16px; +padding-left:22px; +text-align:left; +font-size: 14px; +} + +.diffblock .changeset_header{ +margin-left: 6px !important; +} table.code-browser .browser-dir { background:url("../images/icons/folder_16.png") no-repeat scroll 3px; @@ -1771,6 +1822,36 @@ color:#FFF; } +.follow{ +background:url("../images/icons/heart_add.png") no-repeat scroll 3px; +height: 16px; +width: 20px; +cursor: pointer; +display: block; +float: right; +margin-top: 2px; +} + +.following{ +background:url("../images/icons/heart_delete.png") no-repeat scroll 3px; +height: 16px; +width: 20px; +cursor: pointer; +display: block; +float: right; +margin-top: 2px; +} + +.currently_following{ +padding-left: 10px; +padding-bottom:5px; +} + +.journal_highlight{ +font-weight: bold; +text-decoration: underline; +} + .add_icon { background:url("../images/icons/add.png") no-repeat scroll 3px; height:16px; @@ -1795,6 +1876,14 @@ text-align:left; } +.refresh_icon { +background:url("../images/icons/arrow_refresh.png") no-repeat scroll 3px; +height:16px; +padding-left:20px; +padding-top:1px; +text-align:left; +} + .rss_icon { background:url("../images/icons/rss_16.png") no-repeat scroll 3px; height:16px; @@ -1944,7 +2033,7 @@ } #header,#content,#footer { -min-width:1224px; +min-width:1024px; } #content { @@ -2047,10 +2136,6 @@ padding:0; } -#content div.box div.form div.fields div.field div.label-checkbox,#content div.box div.form div.fields div.field div.label-radio,#content div.box div.form div.fields div.field div.label-textarea { -padding:0 0 0 5px !important; -} - #content div.box div.form div.fields div.field div.label span,#login div.form div.fields div.field div.label span,#register div.form div.fields div.field div.label span { height:1%; display:block; @@ -2079,11 +2164,17 @@ margin:0; } +#content div.box-left div.form div.fields div.field div.select,#content div.box-left div.form div.fields div.field div.checkboxes,#content div.box-left div.form div.fields div.field div.radios,#content div.box-right div.form div.fields div.field div.select,#content div.box-right div.form div.fields div.field div.checkboxes,#content div.box-right div.form div.fields div.field div.radios{ +margin:0 0 0 0px !important; +padding:0; +} + #content div.box div.form div.fields div.field div.select,#content div.box div.form div.fields div.field div.checkboxes,#content div.box div.form div.fields div.field div.radios { margin:0 0 0 200px; padding:0; } + #content div.box div.form div.fields div.field div.select a:hover,#content div.box div.form div.fields div.field div.select a.ui-selectmenu:hover,#content div.box div.action a:hover { color:#000; text-decoration:none; @@ -2097,7 +2188,7 @@ clear:both; overflow:hidden; margin:0; -padding:2px 0; +padding:8px 0 2px; } #content div.box div.form div.fields div.field div.checkboxes div.checkbox input,#content div.box div.form div.fields div.field div.radios div.radio input { @@ -2109,7 +2200,7 @@ height:1%; display:block; float:left; -margin:3px 0 0 4px; +margin:2px 0 0 4px; } div.form div.fields div.field div.button input,#content div.box div.form div.fields div.buttons input,div.form div.fields div.buttons input,#content div.box div.action div.button input { @@ -2218,7 +2309,7 @@ } #login,#register { -width:420px; +width:520px; margin:10% auto 0; padding:0; } @@ -2259,6 +2350,7 @@ color:red; margin:8px 0 0; padding:0; +max-width: 320px; } #login div.form div.fields div.field div.label label,#register div.form div.fields div.field div.label label { @@ -2310,4 +2402,13 @@ border:none !important; height:20px !important; padding:0 !important; -} \ No newline at end of file +} + +#q_filter{ +border:0 none; +color:#AAAAAA; +margin-bottom:-4px; +margin-top:-4px; +padding-left:3px; +} + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/public/images/hgicon.png Binary file rhodecode/public/images/hgicon.png has changed diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/public/images/icons/giticon.png Binary file rhodecode/public/images/icons/giticon.png has changed diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/public/images/icons/lock_open.png Binary file rhodecode/public/images/icons/lock_open.png has changed diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/public/images/icons/success.png Binary file rhodecode/public/images/icons/success.png has changed diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/public/images/icons/warning.png Binary file rhodecode/public/images/icons/warning.png has changed diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/public/images/title_tab_selected.png Binary file rhodecode/public/images/title_tab_selected.png has changed diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/public/js/yui2.js --- a/rhodecode/public/js/yui2.js Thu Nov 18 21:35:52 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,79 +0,0 @@ -/* -Copyright (c) 2010, Yahoo! Inc. All rights reserved. -Code licensed under the BSD License: -http://developer.yahoo.com/yui/license.html -version: 2.8.1 -*/ -if(typeof YAHOO=="undefined"||!YAHOO){var YAHOO={};}YAHOO.namespace=function(){var A=arguments,E=null,C,B,D;for(C=0;C0)?B.dump(I[K],N-1):Q);}else{P.push(I[K]);}P.push(O);}if(P.length>1){P.pop();}P.push("]");}else{P.push("{");for(K in I){if(B.hasOwnProperty(I,K)){P.push(K+L);if(B.isObject(I[K])){P.push((N>0)?B.dump(I[K],N-1):Q);}else{P.push(I[K]);}P.push(O);}}if(P.length>1){P.pop();}P.push("}");}return P.join("");},substitute:function(Y,J,R){var N,M,L,U,V,X,T=[],K,O="dump",S=" ",I="{",W="}",Q,P;for(;;){N=Y.lastIndexOf(I);if(N<0){break;}M=Y.indexOf(W,N);if(N+1>=M){break;}K=Y.substring(N+1,M);U=K;X=null;L=U.indexOf(S);if(L>-1){X=U.substring(L+1);U=U.substring(0,L);}V=J[U];if(R){V=R(U,V,X);}if(B.isObject(V)){if(B.isArray(V)){V=B.dump(V,parseInt(X,10));}else{X=X||"";Q=X.indexOf(O);if(Q>-1){X=X.substring(4);}P=V.toString();if(P===G||Q>-1){V=B.dump(V,parseInt(X,10));}else{V=P;}}}else{if(!B.isString(V)&&!B.isNumber(V)){V="~-"+T.length+"-~";T[T.length]=K;}}Y=Y.substring(0,N)+V+Y.substring(M+1);}for(N=T.length-1;N>=0;N=N-1){Y=Y.replace(new RegExp("~-"+N+"-~"),"{"+T[N]+"}","g");}return Y;},trim:function(I){try{return I.replace(/^\s+|\s+$/g,"");}catch(J){return I;}},merge:function(){var L={},J=arguments,I=J.length,K;for(K=0;K=420){X.addEventListener("load",function(){a(W,U);});}else{var T=M[W];if(T.varName){var V=YAHOO.util.Get.POLL_FREQ;T.maxattempts=YAHOO.util.Get.TIMEOUT/V;T.attempts=0;T._cache=T.varName[0].split(".");T.timer=S.later(V,T,function(j){var f=this._cache,e=f.length,d=this.win,g;for(g=0;gthis.maxattempts){var h="Over retry limit, giving up";T.timer.cancel();Q(W,h);}else{}return;}}T.timer.cancel();a(W,U);},null,true);}else{S.later(YAHOO.util.Get.POLL_FREQ,null,a,[W,U]);}}}}else{X.onload=function(){a(W,U);};}}};return{POLL_FREQ:10,PURGE_THRESH:20,TIMEOUT:2000,_finalize:function(T){S.later(0,null,C,T);},abort:function(U){var V=(S.isString(U))?U:U.tId;var T=M[V];if(T){T.aborted=true;}},script:function(T,U){return H("script",T,U);},css:function(T,U){return H("css",T,U);}};}();YAHOO.register("get",YAHOO.util.Get,{version:"2.8.1",build:"19"});(function(){var Y=YAHOO,util=Y.util,lang=Y.lang,env=Y.env,PROV="_provides",SUPER="_supersedes",REQ="expanded",AFTER="_after";var YUI={dupsAllowed:{"yahoo":true,"get":true},info:{"root":"2.8.1/build/","base":"http://yui.yahooapis.com/2.8.1/build/","comboBase":"http://yui.yahooapis.com/combo?","skin":{"defaultSkin":"sam","base":"assets/skins/","path":"skin.css","after":["reset","fonts","grids","base"],"rollup":3},dupsAllowed:["yahoo","get"],"moduleInfo":{"animation":{"type":"js","path":"animation/animation-min.js","requires":["dom","event"]},"autocomplete":{"type":"js","path":"autocomplete/autocomplete-min.js","requires":["dom","event","datasource"],"optional":["connection","animation"],"skinnable":true},"base":{"type":"css","path":"base/base-min.css","after":["reset","fonts","grids"]},"button":{"type":"js","path":"button/button-min.js","requires":["element"],"optional":["menu"],"skinnable":true},"calendar":{"type":"js","path":"calendar/calendar-min.js","requires":["event","dom"],supersedes:["datemeth"],"skinnable":true},"carousel":{"type":"js","path":"carousel/carousel-min.js","requires":["element"],"optional":["animation"],"skinnable":true},"charts":{"type":"js","path":"charts/charts-min.js","requires":["element","json","datasource","swf"]},"colorpicker":{"type":"js","path":"colorpicker/colorpicker-min.js","requires":["slider","element"],"optional":["animation"],"skinnable":true},"connection":{"type":"js","path":"connection/connection-min.js","requires":["event"],"supersedes":["connectioncore"]},"connectioncore":{"type":"js","path":"connection/connection_core-min.js","requires":["event"],"pkg":"connection"},"container":{"type":"js","path":"container/container-min.js","requires":["dom","event"],"optional":["dragdrop","animation","connection"],"supersedes":["containercore"],"skinnable":true},"containercore":{"type":"js","path":"container/container_core-min.js","requires":["dom","event"],"pkg":"container"},"cookie":{"type":"js","path":"cookie/cookie-min.js","requires":["yahoo"]},"datasource":{"type":"js","path":"datasource/datasource-min.js","requires":["event"],"optional":["connection"]},"datatable":{"type":"js","path":"datatable/datatable-min.js","requires":["element","datasource"],"optional":["calendar","dragdrop","paginator"],"skinnable":true},datemath:{"type":"js","path":"datemath/datemath-min.js","requires":["yahoo"]},"dom":{"type":"js","path":"dom/dom-min.js","requires":["yahoo"]},"dragdrop":{"type":"js","path":"dragdrop/dragdrop-min.js","requires":["dom","event"]},"editor":{"type":"js","path":"editor/editor-min.js","requires":["menu","element","button"],"optional":["animation","dragdrop"],"supersedes":["simpleeditor"],"skinnable":true},"element":{"type":"js","path":"element/element-min.js","requires":["dom","event"],"optional":["event-mouseenter","event-delegate"]},"element-delegate":{"type":"js","path":"element-delegate/element-delegate-min.js","requires":["element"]},"event":{"type":"js","path":"event/event-min.js","requires":["yahoo"]},"event-simulate":{"type":"js","path":"event-simulate/event-simulate-min.js","requires":["event"]},"event-delegate":{"type":"js","path":"event-delegate/event-delegate-min.js","requires":["event"],"optional":["selector"]},"event-mouseenter":{"type":"js","path":"event-mouseenter/event-mouseenter-min.js","requires":["dom","event"]},"fonts":{"type":"css","path":"fonts/fonts-min.css"},"get":{"type":"js","path":"get/get-min.js","requires":["yahoo"]},"grids":{"type":"css","path":"grids/grids-min.css","requires":["fonts"],"optional":["reset"]},"history":{"type":"js","path":"history/history-min.js","requires":["event"]},"imagecropper":{"type":"js","path":"imagecropper/imagecropper-min.js","requires":["dragdrop","element","resize"],"skinnable":true},"imageloader":{"type":"js","path":"imageloader/imageloader-min.js","requires":["event","dom"]},"json":{"type":"js","path":"json/json-min.js","requires":["yahoo"]},"layout":{"type":"js","path":"layout/layout-min.js","requires":["element"],"optional":["animation","dragdrop","resize","selector"],"skinnable":true},"logger":{"type":"js","path":"logger/logger-min.js","requires":["event","dom"],"optional":["dragdrop"],"skinnable":true},"menu":{"type":"js","path":"menu/menu-min.js","requires":["containercore"],"skinnable":true},"paginator":{"type":"js","path":"paginator/paginator-min.js","requires":["element"],"skinnable":true},"profiler":{"type":"js","path":"profiler/profiler-min.js","requires":["yahoo"]},"profilerviewer":{"type":"js","path":"profilerviewer/profilerviewer-min.js","requires":["profiler","yuiloader","element"],"skinnable":true},"progressbar":{"type":"js","path":"progressbar/progressbar-min.js","requires":["element"],"optional":["animation"],"skinnable":true},"reset":{"type":"css","path":"reset/reset-min.css"},"reset-fonts-grids":{"type":"css","path":"reset-fonts-grids/reset-fonts-grids.css","supersedes":["reset","fonts","grids","reset-fonts"],"rollup":4},"reset-fonts":{"type":"css","path":"reset-fonts/reset-fonts.css","supersedes":["reset","fonts"],"rollup":2},"resize":{"type":"js","path":"resize/resize-min.js","requires":["dragdrop","element"],"optional":["animation"],"skinnable":true},"selector":{"type":"js","path":"selector/selector-min.js","requires":["yahoo","dom"]},"simpleeditor":{"type":"js","path":"editor/simpleeditor-min.js","requires":["element"],"optional":["containercore","menu","button","animation","dragdrop"],"skinnable":true,"pkg":"editor"},"slider":{"type":"js","path":"slider/slider-min.js","requires":["dragdrop"],"optional":["animation"],"skinnable":true},"storage":{"type":"js","path":"storage/storage-min.js","requires":["yahoo","event","cookie"],"optional":["swfstore"]},"stylesheet":{"type":"js","path":"stylesheet/stylesheet-min.js","requires":["yahoo"]},"swf":{"type":"js","path":"swf/swf-min.js","requires":["element"],"supersedes":["swfdetect"]},"swfdetect":{"type":"js","path":"swfdetect/swfdetect-min.js","requires":["yahoo"]},"swfstore":{"type":"js","path":"swfstore/swfstore-min.js","requires":["element","cookie","swf"]},"tabview":{"type":"js","path":"tabview/tabview-min.js","requires":["element"],"optional":["connection"],"skinnable":true},"treeview":{"type":"js","path":"treeview/treeview-min.js","requires":["event","dom"],"optional":["json","animation","calendar"],"skinnable":true},"uploader":{"type":"js","path":"uploader/uploader-min.js","requires":["element"]},"utilities":{"type":"js","path":"utilities/utilities.js","supersedes":["yahoo","event","dragdrop","animation","dom","connection","element","yahoo-dom-event","get","yuiloader","yuiloader-dom-event"],"rollup":8},"yahoo":{"type":"js","path":"yahoo/yahoo-min.js"},"yahoo-dom-event":{"type":"js","path":"yahoo-dom-event/yahoo-dom-event.js","supersedes":["yahoo","event","dom"],"rollup":3},"yuiloader":{"type":"js","path":"yuiloader/yuiloader-min.js","supersedes":["yahoo","get"]},"yuiloader-dom-event":{"type":"js","path":"yuiloader-dom-event/yuiloader-dom-event.js","supersedes":["yahoo","dom","event","get","yuiloader","yahoo-dom-event"],"rollup":5},"yuitest":{"type":"js","path":"yuitest/yuitest-min.js","requires":["logger"],"optional":["event-simulate"],"skinnable":true}}},ObjectUtil:{appendArray:function(o,a){if(a){for(var i=0; -i=m.rollup);if(roll){break;}}}}}else{for(j=0;j=m.rollup);if(roll){break;}}}}}if(roll){r[i]=true;rolled=true;this.getRequires(m);}}}if(!rolled){break;}}},_reduce:function(){var i,j,s,m,r=this.required;for(i in r){if(i in this.loaded){delete r[i];}else{var skinDef=this.parseSkin(i);if(skinDef){if(!skinDef.module){var skin_pre=this.SKIN_PREFIX+skinDef.skin;for(j in r){if(lang.hasOwnProperty(r,j)){m=this.moduleInfo[j];var ext=m&&m.ext;if(!ext&&j!==i&&j.indexOf(skin_pre)>-1){delete r[j];}}}}}else{m=this.moduleInfo[i];s=m&&m.supersedes;if(s){for(j=0;j-1){return true;}if(after&&YUI.ArrayUtil.indexOf(after,bb)>-1){return true;}if(checkOptional&&optional&&YUI.ArrayUtil.indexOf(optional,bb)>-1){return true;}var ss=info[bb]&&info[bb].supersedes;if(ss){for(ii=0;iistartLen){YAHOO.util.Get.script(self._filter(js),{data:self._loading,onSuccess:callback,onFailure:self._onFailure,onTimeout:self._onTimeout,insertBefore:self.insertBefore,charset:self.charset,timeout:self.timeout,scope:self});}};if(css.length>startLen){YAHOO.util.Get.css(this._filter(css),{data:this._loading,onSuccess:loadScript,onFailure:this._onFailure,onTimeout:this._onTimeout,insertBefore:this.insertBefore,charset:this.charset,timeout:this.timeout,scope:self});}else{loadScript();}return;}else{this.loadNext(this._loading);}},insert:function(o,type){this.calculate(o);this._loading=true;this.loadType=type;if(this.combine){return this._combine();}if(!type){var self=this;this._internalCallback=function(){self._internalCallback=null;self.insert(null,"js");};this.insert(null,"css");return;}this.loadNext();},sandbox:function(o,type){this._config(o);if(!this.onSuccess){throw new Error("You must supply an onSuccess handler for your sandbox");}this._sandbox=true;var self=this;if(!type||type!=="js"){this._internalCallback=function(){self._internalCallback=null;self.sandbox(null,"js");};this.insert(null,"css");return;}if(!util.Connect){var ld=new YAHOO.util.YUILoader();ld.insert({base:this.base,filter:this.filter,require:"connection",insertBefore:this.insertBefore,charset:this.charset,onSuccess:function(){this.sandbox(null,"js");},scope:this},"js");return;}this._scriptText=[];this._loadCount=0;this._stopCount=this.sorted.length;this._xhr=[];this.calculate();var s=this.sorted,l=s.length,i,m,url;for(i=0;i=this._stopCount){var v=this.varName||"YAHOO";var t="(function() {\n";var b="\nreturn "+v+";\n})();";var ref=eval(t+this._scriptText.join("\n")+b);this._pushEvents(ref);if(ref){this.onSuccess.call(this.scope,{reference:ref,data:this.data});}else{this._onFailure.call(this.varName+" reference failure");}}},failure:function(o){this.onFailure.call(this.scope,{msg:"XHR failure",xhrResponse:o,data:this.data});},scope:this,argument:[i,url,s[i]]};this._xhr.push(util.Connect.asyncRequest("GET",url,xhrData));}},loadNext:function(mname){if(!this._loading){return;}if(mname){if(mname!==this._loading){return;}this.inserted[mname]=true;if(this.onProgress){this.onProgress.call(this.scope,{name:mname,data:this.data});}}var s=this.sorted,len=s.length,i,m;for(i=0;i519)?true:false);while((G=G[u])){z[0]+=G[b];z[1]+=G[P];if(AC){z=E.Dom._calcBorders(G,z);}}if(E.Dom._getStyle(y,p)!==f){G=y;while((G=G[Z])&&G[C]){AA=G[i];AB=G[O];if(H&&(E.Dom._getStyle(G,"overflow")!=="visible")){z=E.Dom._calcBorders(G,z);}if(AA||AB){z[0]-=AB;z[1]-=AA;}}z[0]+=x;z[1]+=Y;}else{if(D){z[0]-=x;z[1]-=Y;}else{if(I||H){z[0]+=x;z[1]+=Y;}}}z[0]=Math.floor(z[0]);z[1]=Math.floor(z[1]);}else{}return z;};}}(),getX:function(G){var Y=function(x){return E.Dom.getXY(x)[0];};return E.Dom.batch(G,Y,E.Dom,true);},getY:function(G){var Y=function(x){return E.Dom.getXY(x)[1];};return E.Dom.batch(G,Y,E.Dom,true);},setXY:function(G,x,Y){E.Dom.batch(G,E.Dom._setXY,{pos:x,noRetry:Y});},_setXY:function(G,z){var AA=E.Dom._getStyle(G,p),y=E.Dom.setStyle,AD=z.pos,Y=z.noRetry,AB=[parseInt(E.Dom.getComputedStyle(G,j),10),parseInt(E.Dom.getComputedStyle(G,o),10)],AC,x;if(AA=="static"){AA=V;y(G,p,AA);}AC=E.Dom._getXY(G);if(!AD||AC===false){return false;}if(isNaN(AB[0])){AB[0]=(AA==V)?0:G[b];}if(isNaN(AB[1])){AB[1]=(AA==V)?0:G[P];}if(AD[0]!==null){y(G,j,AD[0]-AC[0]+AB[0]+"px");}if(AD[1]!==null){y(G,o,AD[1]-AC[1]+AB[1]+"px");}if(!Y){x=E.Dom._getXY(G);if((AD[0]!==null&&x[0]!=AD[0])||(AD[1]!==null&&x[1]!=AD[1])){E.Dom._setXY(G,{pos:AD,noRetry:true});}}},setX:function(Y,G){E.Dom.setXY(Y,[G,null]);},setY:function(G,Y){E.Dom.setXY(G,[null,Y]);},getRegion:function(G){var Y=function(x){var y=false;if(E.Dom._canPosition(x)){y=E.Region.getRegion(x);}else{}return y;};return E.Dom.batch(G,Y,E.Dom,true);},getClientWidth:function(){return E.Dom.getViewportWidth();},getClientHeight:function(){return E.Dom.getViewportHeight();},getElementsByClassName:function(AB,AF,AC,AE,x,AD){AF=AF||"*";AC=(AC)?E.Dom.get(AC):null||K;if(!AC){return[];}var Y=[],G=AC.getElementsByTagName(AF),z=E.Dom.hasClass;for(var y=0,AA=G.length;y-1;}}else{}return G;},addClass:function(Y,G){return E.Dom.batch(Y,E.Dom._addClass,G);},_addClass:function(x,Y){var G=false,y;if(x&&Y){y=E.Dom._getAttribute(x,F)||J;if(!E.Dom._hasClass(x,Y)){E.Dom.setAttribute(x,F,A(y+B+Y));G=true;}}else{}return G;},removeClass:function(Y,G){return E.Dom.batch(Y,E.Dom._removeClass,G);},_removeClass:function(y,x){var Y=false,AA,z,G;if(y&&x){AA=E.Dom._getAttribute(y,F)||J;E.Dom.setAttribute(y,F,AA.replace(E.Dom._getClassRegex(x),J));z=E.Dom._getAttribute(y,F);if(AA!==z){E.Dom.setAttribute(y,F,A(z));Y=true;if(E.Dom._getAttribute(y,F)===""){G=(y.hasAttribute&&y.hasAttribute(g))?g:F; -y.removeAttribute(G);}}}else{}return Y;},replaceClass:function(x,Y,G){return E.Dom.batch(x,E.Dom._replaceClass,{from:Y,to:G});},_replaceClass:function(y,x){var Y,AB,AA,G=false,z;if(y&&x){AB=x.from;AA=x.to;if(!AA){G=false;}else{if(!AB){G=E.Dom._addClass(y,x.to);}else{if(AB!==AA){z=E.Dom._getAttribute(y,F)||J;Y=(B+z.replace(E.Dom._getClassRegex(AB),B+AA)).split(E.Dom._getClassRegex(AA));Y.splice(1,0,B+AA);E.Dom.setAttribute(y,F,A(Y.join(J)));G=true;}}}}else{}return G;},generateId:function(G,x){x=x||"yui-gen";var Y=function(y){if(y&&y.id){return y.id;}var z=x+YAHOO.env._id_counter++;if(y){if(y[e]&&y[e].getElementById(z)){return E.Dom.generateId(y,z+x);}y.id=z;}return z;};return E.Dom.batch(G,Y,E.Dom,true)||Y.apply(E.Dom,arguments);},isAncestor:function(Y,x){Y=E.Dom.get(Y);x=E.Dom.get(x);var G=false;if((Y&&x)&&(Y[l]&&x[l])){if(Y.contains&&Y!==x){G=Y.contains(x);}else{if(Y.compareDocumentPosition){G=!!(Y.compareDocumentPosition(x)&16);}}}else{}return G;},inDocument:function(G,Y){return E.Dom._inDoc(E.Dom.get(G),Y);},_inDoc:function(Y,x){var G=false;if(Y&&Y[C]){x=x||Y[e];G=E.Dom.isAncestor(x[v],Y);}else{}return G;},getElementsBy:function(Y,AF,AB,AD,y,AC,AE){AF=AF||"*";AB=(AB)?E.Dom.get(AB):null||K;if(!AB){return[];}var x=[],G=AB.getElementsByTagName(AF);for(var z=0,AA=G.length;z=8&&K.documentElement.hasAttribute){E.Dom.DOT_ATTRIBUTES.type=true;}})();YAHOO.util.Region=function(C,D,A,B){this.top=C;this.y=C;this[1]=C;this.right=D;this.bottom=A;this.left=B;this.x=B;this[0]=B; -this.width=this.right-this.left;this.height=this.bottom-this.top;};YAHOO.util.Region.prototype.contains=function(A){return(A.left>=this.left&&A.right<=this.right&&A.top>=this.top&&A.bottom<=this.bottom);};YAHOO.util.Region.prototype.getArea=function(){return((this.bottom-this.top)*(this.right-this.left));};YAHOO.util.Region.prototype.intersect=function(E){var C=Math.max(this.top,E.top),D=Math.min(this.right,E.right),A=Math.min(this.bottom,E.bottom),B=Math.max(this.left,E.left);if(A>=C&&D>=B){return new YAHOO.util.Region(C,D,A,B);}else{return null;}};YAHOO.util.Region.prototype.union=function(E){var C=Math.min(this.top,E.top),D=Math.max(this.right,E.right),A=Math.max(this.bottom,E.bottom),B=Math.min(this.left,E.left);return new YAHOO.util.Region(C,D,A,B);};YAHOO.util.Region.prototype.toString=function(){return("Region {"+"top: "+this.top+", right: "+this.right+", bottom: "+this.bottom+", left: "+this.left+", height: "+this.height+", width: "+this.width+"}");};YAHOO.util.Region.getRegion=function(D){var F=YAHOO.util.Dom.getXY(D),C=F[1],E=F[0]+D.offsetWidth,A=F[1]+D.offsetHeight,B=F[0];return new YAHOO.util.Region(C,E,A,B);};YAHOO.util.Point=function(A,B){if(YAHOO.lang.isArray(A)){B=A[1];A=A[0];}YAHOO.util.Point.superclass.constructor.call(this,B,A,B,A);};YAHOO.extend(YAHOO.util.Point,YAHOO.util.Region);(function(){var B=YAHOO.util,A="clientTop",F="clientLeft",J="parentNode",K="right",W="hasLayout",I="px",U="opacity",L="auto",D="borderLeftWidth",G="borderTopWidth",P="borderRightWidth",V="borderBottomWidth",S="visible",Q="transparent",N="height",E="width",H="style",T="currentStyle",R=/^width|height$/,O=/^(\d[.\d]*)+(em|ex|px|gd|rem|vw|vh|vm|ch|mm|cm|in|pt|pc|deg|rad|ms|s|hz|khz|%){1}?/i,M={get:function(X,Z){var Y="",a=X[T][Z];if(Z===U){Y=B.Dom.getStyle(X,U);}else{if(!a||(a.indexOf&&a.indexOf(I)>-1)){Y=a;}else{if(B.Dom.IE_COMPUTED[Z]){Y=B.Dom.IE_COMPUTED[Z](X,Z);}else{if(O.test(a)){Y=B.Dom.IE.ComputedStyle.getPixel(X,Z);}else{Y=a;}}}}return Y;},getOffset:function(Z,e){var b=Z[T][e],X=e.charAt(0).toUpperCase()+e.substr(1),c="offset"+X,Y="pixel"+X,a="",d;if(b==L){d=Z[c];if(d===undefined){a=0;}a=d;if(R.test(e)){Z[H][e]=d;if(Z[c]>d){a=d-(Z[c]-d);}Z[H][e]=L;}}else{if(!Z[H][Y]&&!Z[H][e]){Z[H][e]=b;}a=Z[H][Y];}return a+I;},getBorderWidth:function(X,Z){var Y=null;if(!X[T][W]){X[H].zoom=1;}switch(Z){case G:Y=X[A];break;case V:Y=X.offsetHeight-X.clientHeight-X[A];break;case D:Y=X[F];break;case P:Y=X.offsetWidth-X.clientWidth-X[F];break;}return Y+I;},getPixel:function(Y,X){var a=null,b=Y[T][K],Z=Y[T][X];Y[H][K]=Z;a=Y[H].pixelRight;Y[H][K]=b;return a+I;},getMargin:function(Y,X){var Z;if(Y[T][X]==L){Z=0+I;}else{Z=B.Dom.IE.ComputedStyle.getPixel(Y,X);}return Z;},getVisibility:function(Y,X){var Z;while((Z=Y[T])&&Z[X]=="inherit"){Y=Y[J];}return(Z)?Z[X]:S;},getColor:function(Y,X){return B.Dom.Color.toRGB(Y[T][X])||Q;},getBorderColor:function(Y,X){var Z=Y[T],a=Z[X]||Z.color;return B.Dom.Color.toRGB(B.Dom.Color.toHex(a));}},C={};C.top=C.right=C.bottom=C.left=C[E]=C[N]=M.getOffset;C.color=M.getColor;C[G]=C[P]=C[V]=C[D]=M.getBorderWidth;C.marginTop=C.marginRight=C.marginBottom=C.marginLeft=M.getMargin;C.visibility=M.getVisibility;C.borderColor=C.borderTopColor=C.borderRightColor=C.borderBottomColor=C.borderLeftColor=M.getBorderColor;B.Dom.IE_COMPUTED=C;B.Dom.IE_ComputedStyle=M;})();(function(){var C="toString",A=parseInt,B=RegExp,D=YAHOO.util;D.Dom.Color={KEYWORDS:{black:"000",silver:"c0c0c0",gray:"808080",white:"fff",maroon:"800000",red:"f00",purple:"800080",fuchsia:"f0f",green:"008000",lime:"0f0",olive:"808000",yellow:"ff0",navy:"000080",blue:"00f",teal:"008080",aqua:"0ff"},re_RGB:/^rgb\(([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\)$/i,re_hex:/^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i,re_hex3:/([0-9A-F])/gi,toRGB:function(E){if(!D.Dom.Color.re_RGB.test(E)){E=D.Dom.Color.toHex(E);}if(D.Dom.Color.re_hex.exec(E)){E="rgb("+[A(B.$1,16),A(B.$2,16),A(B.$3,16)].join(", ")+")";}return E;},toHex:function(H){H=D.Dom.Color.KEYWORDS[H]||H;if(D.Dom.Color.re_RGB.exec(H)){var G=(B.$1.length===1)?"0"+B.$1:Number(B.$1),F=(B.$2.length===1)?"0"+B.$2:Number(B.$2),E=(B.$3.length===1)?"0"+B.$3:Number(B.$3);H=[G[C](16),F[C](16),E[C](16)].join("");}if(H.length<6){H=H.replace(D.Dom.Color.re_hex3,"$1$1");}if(H!=="transparent"&&H.indexOf("#")<0){H="#"+H;}return H.toLowerCase();}};}());YAHOO.register("dom",YAHOO.util.Dom,{version:"2.8.1",build:"19"});YAHOO.util.CustomEvent=function(D,C,B,A,E){this.type=D;this.scope=C||window;this.silent=B;this.fireOnce=E;this.fired=false;this.firedWith=null;this.signature=A||YAHOO.util.CustomEvent.LIST;this.subscribers=[];if(!this.silent){}var F="_YUICEOnSubscribe";if(D!==F){this.subscribeEvent=new YAHOO.util.CustomEvent(F,this,true);}this.lastError=null;};YAHOO.util.CustomEvent.LIST=0;YAHOO.util.CustomEvent.FLAT=1;YAHOO.util.CustomEvent.prototype={subscribe:function(B,C,D){if(!B){throw new Error("Invalid callback for subscriber to '"+this.type+"'");}if(this.subscribeEvent){this.subscribeEvent.fire(B,C,D);}var A=new YAHOO.util.Subscriber(B,C,D);if(this.fireOnce&&this.fired){this.notify(A,this.firedWith);}else{this.subscribers.push(A);}},unsubscribe:function(D,F){if(!D){return this.unsubscribeAll();}var E=false;for(var B=0,A=this.subscribers.length;B0){H=C[0];}try{B=F.fn.call(E,H,F.obj);}catch(G){this.lastError=G;if(A){throw G;}}}else{try{B=F.fn.call(E,this.type,C,F.obj);}catch(D){this.lastError=D;if(A){throw D;}}}return B;},unsubscribeAll:function(){var A=this.subscribers.length,B;for(B=A-1;B>-1;B--){this._delete(B);}this.subscribers=[];return A;},_delete:function(A){var B=this.subscribers[A];if(B){delete B.fn;delete B.obj;}this.subscribers.splice(A,1);},toString:function(){return"CustomEvent: "+"'"+this.type+"', "+"context: "+this.scope;}};YAHOO.util.Subscriber=function(A,B,C){this.fn=A;this.obj=YAHOO.lang.isUndefined(B)?null:B;this.overrideContext=C;};YAHOO.util.Subscriber.prototype.getScope=function(A){if(this.overrideContext){if(this.overrideContext===true){return this.obj;}else{return this.overrideContext;}}return A;};YAHOO.util.Subscriber.prototype.contains=function(A,B){if(B){return(this.fn==A&&this.obj==B);}else{return(this.fn==A);}};YAHOO.util.Subscriber.prototype.toString=function(){return"Subscriber { obj: "+this.obj+", overrideContext: "+(this.overrideContext||"no")+" }";};if(!YAHOO.util.Event){YAHOO.util.Event=function(){var G=false,H=[],J=[],A=0,E=[],B=0,C={63232:38,63233:40,63234:37,63235:39,63276:33,63277:34,25:9},D=YAHOO.env.ua.ie,F="focusin",I="focusout";return{POLL_RETRYS:500,POLL_INTERVAL:40,EL:0,TYPE:1,FN:2,WFN:3,UNLOAD_OBJ:3,ADJ_SCOPE:4,OBJ:5,OVERRIDE:6,CAPTURE:7,lastError:null,isSafari:YAHOO.env.ua.webkit,webkit:YAHOO.env.ua.webkit,isIE:D,_interval:null,_dri:null,_specialTypes:{focusin:(D?"focusin":"focus"),focusout:(D?"focusout":"blur")},DOMReady:false,throwErrors:false,startInterval:function(){if(!this._interval){this._interval=YAHOO.lang.later(this.POLL_INTERVAL,this,this._tryPreloadAttach,null,true);}},onAvailable:function(Q,M,O,P,N){var K=(YAHOO.lang.isString(Q))?[Q]:Q;for(var L=0;L-1;M--){S=(this.removeListener(L[M],K,R)&&S);}return S;}}if(!R||!R.call){return this.purgeElement(L,false,K);}if("unload"==K){for(M=J.length-1;M>-1;M--){U=J[M];if(U&&U[0]==L&&U[1]==K&&U[2]==R){J.splice(M,1);return true;}}return false;}var N=null;var O=arguments[3];if("undefined"===typeof O){O=this._getCacheIndex(H,L,K,R);}if(O>=0){N=H[O];}if(!L||!N){return false;}var T=N[this.CAPTURE]===true?true:false;try{this._simpleRemove(L,K,N[this.WFN],T);}catch(Q){this.lastError=Q;return false;}delete H[O][this.WFN];delete H[O][this.FN];H.splice(O,1);return true;},getTarget:function(M,L){var K=M.target||M.srcElement;return this.resolveTextNode(K);},resolveTextNode:function(L){try{if(L&&3==L.nodeType){return L.parentNode;}}catch(K){}return L;},getPageX:function(L){var K=L.pageX;if(!K&&0!==K){K=L.clientX||0;if(this.isIE){K+=this._getScrollLeft();}}return K;},getPageY:function(K){var L=K.pageY;if(!L&&0!==L){L=K.clientY||0;if(this.isIE){L+=this._getScrollTop();}}return L;},getXY:function(K){return[this.getPageX(K),this.getPageY(K)];},getRelatedTarget:function(L){var K=L.relatedTarget;if(!K){if(L.type=="mouseout"){K=L.toElement; -}else{if(L.type=="mouseover"){K=L.fromElement;}}}return this.resolveTextNode(K);},getTime:function(M){if(!M.time){var L=new Date().getTime();try{M.time=L;}catch(K){this.lastError=K;return L;}}return M.time;},stopEvent:function(K){this.stopPropagation(K);this.preventDefault(K);},stopPropagation:function(K){if(K.stopPropagation){K.stopPropagation();}else{K.cancelBubble=true;}},preventDefault:function(K){if(K.preventDefault){K.preventDefault();}else{K.returnValue=false;}},getEvent:function(M,K){var L=M||window.event;if(!L){var N=this.getEvent.caller;while(N){L=N.arguments[0];if(L&&Event==L.constructor){break;}N=N.caller;}}return L;},getCharCode:function(L){var K=L.keyCode||L.charCode||0;if(YAHOO.env.ua.webkit&&(K in C)){K=C[K];}return K;},_getCacheIndex:function(M,P,Q,O){for(var N=0,L=M.length;N0&&E.length>0);}var P=[];var R=function(T,U){var S=T;if(U.overrideContext){if(U.overrideContext===true){S=U.obj;}else{S=U.overrideContext;}}U.fn.call(S,U.obj);};var L,K,O,N,M=[];for(L=0,K=E.length;L-1;L--){O=E[L];if(!O||!O.id){E.splice(L,1);}}this.startInterval();}else{if(this._interval){this._interval.cancel();this._interval=null;}}this.locked=false;},purgeElement:function(O,P,R){var M=(YAHOO.lang.isString(O))?this.getEl(O):O;var Q=this.getListeners(M,R),N,K;if(Q){for(N=Q.length-1;N>-1;N--){var L=Q[N];this.removeListener(M,L.type,L.fn);}}if(P&&M&&M.childNodes){for(N=0,K=M.childNodes.length;N-1;N--){M=H[N];if(M){L.removeListener(M[L.EL],M[L.TYPE],M[L.FN],N);}}M=null;}L._simpleRemove(window,"unload",L._unload);},_getScrollLeft:function(){return this._getScroll()[1];},_getScrollTop:function(){return this._getScroll()[0];},_getScroll:function(){var K=document.documentElement,L=document.body;if(K&&(K.scrollTop||K.scrollLeft)){return[K.scrollTop,K.scrollLeft];}else{if(L){return[L.scrollTop,L.scrollLeft];}else{return[0,0];}}},regCE:function(){},_simpleAdd:function(){if(window.addEventListener){return function(M,N,L,K){M.addEventListener(N,L,(K));};}else{if(window.attachEvent){return function(M,N,L,K){M.attachEvent("on"+N,L);};}else{return function(){};}}}(),_simpleRemove:function(){if(window.removeEventListener){return function(M,N,L,K){M.removeEventListener(N,L,(K));};}else{if(window.detachEvent){return function(L,M,K){L.detachEvent("on"+M,K);};}else{return function(){};}}}()};}();(function(){var EU=YAHOO.util.Event;EU.on=EU.addListener;EU.onFocus=EU.addFocusListener;EU.onBlur=EU.addBlurListener; -/* DOMReady: based on work by: Dean Edwards/John Resig/Matthias Miller/Diego Perini */ -if(EU.isIE){if(self!==self.top){document.onreadystatechange=function(){if(document.readyState=="complete"){document.onreadystatechange=null;EU._ready();}};}else{YAHOO.util.Event.onDOMReady(YAHOO.util.Event._tryPreloadAttach,YAHOO.util.Event,true);var n=document.createElement("p");EU._dri=setInterval(function(){try{n.doScroll("left");clearInterval(EU._dri);EU._dri=null;EU._ready();n=null;}catch(ex){}},EU.POLL_INTERVAL);}}else{if(EU.webkit&&EU.webkit<525){EU._dri=setInterval(function(){var rs=document.readyState;if("loaded"==rs||"complete"==rs){clearInterval(EU._dri);EU._dri=null;EU._ready();}},EU.POLL_INTERVAL);}else{EU._simpleAdd(document,"DOMContentLoaded",EU._ready);}}EU._simpleAdd(window,"load",EU._load);EU._simpleAdd(window,"unload",EU._unload);EU._tryPreloadAttach();})();}YAHOO.util.EventProvider=function(){};YAHOO.util.EventProvider.prototype={__yui_events:null,__yui_subscribers:null,subscribe:function(A,C,F,E){this.__yui_events=this.__yui_events||{};var D=this.__yui_events[A];if(D){D.subscribe(C,F,E);}else{this.__yui_subscribers=this.__yui_subscribers||{};var B=this.__yui_subscribers;if(!B[A]){B[A]=[];}B[A].push({fn:C,obj:F,overrideContext:E});}},unsubscribe:function(C,E,G){this.__yui_events=this.__yui_events||{};var A=this.__yui_events;if(C){var F=A[C];if(F){return F.unsubscribe(E,G);}}else{var B=true;for(var D in A){if(YAHOO.lang.hasOwnProperty(A,D)){B=B&&A[D].unsubscribe(E,G);}}return B;}return false;},unsubscribeAll:function(A){return this.unsubscribe(A); -},createEvent:function(B,G){this.__yui_events=this.__yui_events||{};var E=G||{},D=this.__yui_events,F;if(D[B]){}else{F=new YAHOO.util.CustomEvent(B,E.scope||this,E.silent,YAHOO.util.CustomEvent.FLAT,E.fireOnce);D[B]=F;if(E.onSubscribeCallback){F.subscribeEvent.subscribe(E.onSubscribeCallback);}this.__yui_subscribers=this.__yui_subscribers||{};var A=this.__yui_subscribers[B];if(A){for(var C=0;C=200&&E<300)||E===1223||C){A=B.xdr?B.r:this.createResponseObject(B,G);if(I&&I.success){if(!I.scope){I.success(A);}else{I.success.apply(I.scope,[A]);}}this.successEvent.fire(A);if(B.successEvent){B.successEvent.fire(A);}}else{switch(E){case 12002:case 12029:case 12030:case 12031:case 12152:case 13030:A=this.createExceptionObject(B.tId,G,(D?D:false));if(I&&I.failure){if(!I.scope){I.failure(A);}else{I.failure.apply(I.scope,[A]);}}break;default:A=(B.xdr)?B.response:this.createResponseObject(B,G);if(I&&I.failure){if(!I.scope){I.failure(A);}else{I.failure.apply(I.scope,[A]);}}}this.failureEvent.fire(A);if(B.failureEvent){B.failureEvent.fire(A);}}this.releaseObject(B);A=null;},createResponseObject:function(A,G){var D={},I={},E,C,F,B;try{C=A.conn.getAllResponseHeaders();F=C.split("\n");for(E=0;E'+''+''+"",K=document.createElement("div");document.body.appendChild(K);K.innerHTML=J;}function B(L,I,J,M,K){H[parseInt(L.tId)]={"o":L,"c":M};if(K){M.method=I;M.data=K;}L.conn.send(J,M,L.tId);}function E(I){D(I);G._transport=document.getElementById("YUIConnectionSwf");}function C(){G.xdrReadyEvent.fire();}function A(J,I){if(J){G.startEvent.fire(J,I.argument);if(J.startEvent){J.startEvent.fire(J,I.argument);}}}function F(J){var K=H[J.tId].o,I=H[J.tId].c;if(J.statusText==="xdr:start"){A(K,I);return;}J.responseText=decodeURI(J.responseText);K.r=J;if(I.argument){K.r.argument=I.argument;}this.handleTransactionResponse(K,I,J.statusText==="xdr:abort"?true:false);delete H[J.tId];}G.xdr=B;G.swf=D;G.transport=E;G.xdrReadyEvent=new YAHOO.util.CustomEvent("xdrReady");G.xdrReady=C;G.handleXdrResponse=F;})();(function(){var D=YAHOO.util.Connect,F=YAHOO.util.Event;D._isFormSubmit=false;D._isFileUpload=false;D._formNode=null;D._sFormData=null;D._submitElementValue=null;D.uploadEvent=new YAHOO.util.CustomEvent("upload"),D._hasSubmitListener=function(){if(F){F.addListener(document,"click",function(J){var I=F.getTarget(J),H=I.nodeName.toLowerCase();if((H==="input"||H==="button")&&(I.type&&I.type.toLowerCase()=="submit")){D._submitElementValue=encodeURIComponent(I.name)+"="+encodeURIComponent(I.value);}});return true;}return false;}();function G(T,O,J){var S,I,R,P,W,Q=false,M=[],V=0,L,N,K,U,H;this.resetFormState();if(typeof T=="string"){S=(document.getElementById(T)||document.forms[T]);}else{if(typeof T=="object"){S=T;}else{return;}}if(O){this.createFrame(J?J:null);this._isFormSubmit=true;this._isFileUpload=true;this._formNode=S;return;}for(L=0,N=S.elements.length;L-1){H=I.options[I.selectedIndex];M[V++]=R+encodeURIComponent((H.attributes.value&&H.attributes.value.specified)?H.value:H.text);}break;case"select-multiple":if(I.selectedIndex>-1){for(K=I.selectedIndex,U=I.options.length;K');if(typeof H=="boolean"){J.src="javascript:false";}}else{J=document.createElement("iframe");J.id=I;J.name=I;}J.style.position="absolute";J.style.top="-1000px";J.style.left="-1000px";document.body.appendChild(J);}function E(H){var K=[],I=H.split("&"),J,L;for(J=0;J0){for(P=0;P0)?F:0;}if(C in D&&!("style" in D&&C in D.style)){D[C]=F;}else{B.Dom.setStyle(D,C,F+E);}},getAttribute:function(C){var E=this.getEl();var G=B.Dom.getStyle(E,C);if(G!=="auto"&&!this.patterns.offsetUnit.test(G)){return parseFloat(G);}var D=this.patterns.offsetAttribute.exec(C)||[];var H=!!(D[3]);var F=!!(D[2]);if("style" in E){if(F||(B.Dom.getStyle(E,"position")=="absolute"&&H)){G=E["offset"+D[0].charAt(0).toUpperCase()+D[0].substr(1)];}else{G=0;}}else{if(C in E){G=E[C];}}return G;},getDefaultUnit:function(C){if(this.patterns.defaultUnit.test(C)){return"px";}return"";},setRuntimeAttribute:function(D){var I;var E;var F=this.attributes;this.runtimeAttributes[D]={};var H=function(J){return(typeof J!=="undefined");};if(!H(F[D]["to"])&&!H(F[D]["by"])){return false;}I=(H(F[D]["from"]))?F[D]["from"]:this.getAttribute(D);if(H(F[D]["to"])){E=F[D]["to"];}else{if(H(F[D]["by"])){if(I.constructor==Array){E=[];for(var G=0,C=I.length;G0&&isFinite(K)){if(G.currentFrame+K>=J){K=J-(I+1);}G.currentFrame+=K;}};this._queue=B;this._getIndex=E;};YAHOO.util.Bezier=new function(){this.getPosition=function(E,D){var F=E.length;var C=[];for(var B=0;B0&&!(L[0] instanceof Array)){L=[L];}else{var K=[];for(M=0,O=L.length;M0){this.runtimeAttributes[P]=this.runtimeAttributes[P].concat(L);}this.runtimeAttributes[P][this.runtimeAttributes[P].length]=I;}else{F.setRuntimeAttribute.call(this,P);}};var B=function(G,I){var H=E.Dom.getXY(this.getEl());G=[G[0]-H[0]+I[0],G[1]-H[1]+I[1]];return G;};var D=function(G){return(typeof G!=="undefined");};E.Motion=A;})();(function(){var D=function(F,E,G,H){if(F){D.superclass.constructor.call(this,F,E,G,H);}};D.NAME="Scroll";var B=YAHOO.util;YAHOO.extend(D,B.ColorAnim);var C=D.superclass;var A=D.prototype;A.doMethod=function(E,H,F){var G=null;if(E=="scroll"){G=[this.method(this.currentFrame,H[0],F[0]-H[0],this.totalFrames),this.method(this.currentFrame,H[1],F[1]-H[1],this.totalFrames)];}else{G=C.doMethod.call(this,E,H,F);}return G;};A.getAttribute=function(E){var G=null;var F=this.getEl();if(E=="scroll"){G=[F.scrollLeft,F.scrollTop];}else{G=C.getAttribute.call(this,E);}return G;};A.setAttribute=function(E,H,G){var F=this.getEl();if(E=="scroll"){F.scrollLeft=H[0];F.scrollTop=H[1];}else{C.setAttribute.call(this,E,H,G);}};B.Scroll=D;})();YAHOO.register("animation",YAHOO.util.Anim,{version:"2.8.1",build:"19"});if(!YAHOO.util.DragDropMgr){YAHOO.util.DragDropMgr=function(){var A=YAHOO.util.Event,B=YAHOO.util.Dom;return{useShim:false,_shimActive:false,_shimState:false,_debugShim:false,_createShim:function(){var C=document.createElement("div");C.id="yui-ddm-shim";if(document.body.firstChild){document.body.insertBefore(C,document.body.firstChild);}else{document.body.appendChild(C);}C.style.display="none";C.style.backgroundColor="red";C.style.position="absolute";C.style.zIndex="99999";B.setStyle(C,"opacity","0");this._shim=C;A.on(C,"mouseup",this.handleMouseUp,this,true);A.on(C,"mousemove",this.handleMouseMove,this,true);A.on(window,"scroll",this._sizeShim,this,true);},_sizeShim:function(){if(this._shimActive){var C=this._shim;C.style.height=B.getDocumentHeight()+"px";C.style.width=B.getDocumentWidth()+"px";C.style.top="0";C.style.left="0";}},_activateShim:function(){if(this.useShim){if(!this._shim){this._createShim();}this._shimActive=true;var C=this._shim,D="0";if(this._debugShim){D=".5";}B.setStyle(C,"opacity",D);this._sizeShim();C.style.display="block";}},_deactivateShim:function(){this._shim.style.display="none";this._shimActive=false;},_shim:null,ids:{},handleIds:{},dragCurrent:null,dragOvers:{},deltaX:0,deltaY:0,preventDefault:true,stopPropagation:true,initialized:false,locked:false,interactionInfo:null,init:function(){this.initialized=true;},POINT:0,INTERSECT:1,STRICT_INTERSECT:2,mode:0,_execOnAll:function(E,D){for(var F in this.ids){for(var C in this.ids[F]){var G=this.ids[F][C];if(!this.isTypeOfDD(G)){continue;}G[E].apply(G,D);}}},_onLoad:function(){this.init();A.on(document,"mouseup",this.handleMouseUp,this,true);A.on(document,"mousemove",this.handleMouseMove,this,true);A.on(window,"unload",this._onUnload,this,true);A.on(window,"resize",this._onResize,this,true);},_onResize:function(C){this._execOnAll("resetConstraints",[]);},lock:function(){this.locked=true;},unlock:function(){this.locked=false;},isLocked:function(){return this.locked;},locationCache:{},useCache:true,clickPixelThresh:3,clickTimeThresh:1000,dragThreshMet:false,clickTimeout:null,startX:0,startY:0,fromTimeout:false,regDragDrop:function(D,C){if(!this.initialized){this.init();}if(!this.ids[C]){this.ids[C]={};}this.ids[C][D.id]=D;},removeDDFromGroup:function(E,C){if(!this.ids[C]){this.ids[C]={};}var D=this.ids[C];if(D&&D[E.id]){delete D[E.id];}},_remove:function(E){for(var D in E.groups){if(D){var C=this.ids[D];if(C&&C[E.id]){delete C[E.id];}}}delete this.handleIds[E.id];},regHandle:function(D,C){if(!this.handleIds[D]){this.handleIds[D]={};}this.handleIds[D][C]=C;},isDragDrop:function(C){return(this.getDDById(C))?true:false;},getRelated:function(H,D){var G=[];for(var F in H.groups){for(var E in this.ids[F]){var C=this.ids[F][E];if(!this.isTypeOfDD(C)){continue;}if(!D||C.isTarget){G[G.length]=C;}}}return G;},isLegalTarget:function(G,F){var D=this.getRelated(G,true);for(var E=0,C=D.length;Ethis.clickPixelThresh||D>this.clickPixelThresh){this.startDrag(this.startX,this.startY);}}if(this.dragThreshMet){if(C&&C.events.b4Drag){C.b4Drag(F);C.fireEvent("b4DragEvent",{e:F});}if(C&&C.events.drag){C.onDrag(F);C.fireEvent("dragEvent",{e:F});}if(C){this.fireEvents(F,false);}}this.stopEvent(F);}},fireEvents:function(V,L){var a=this.dragCurrent;if(!a||a.isLocked()||a.dragOnly){return;}var N=YAHOO.util.Event.getPageX(V),M=YAHOO.util.Event.getPageY(V),P=new YAHOO.util.Point(N,M),K=a.getTargetCoord(P.x,P.y),F=a.getDragEl(),E=["out","over","drop","enter"],U=new YAHOO.util.Region(K.y,K.x+F.offsetWidth,K.y+F.offsetHeight,K.x),I=[],D={},Q=[],c={outEvts:[],overEvts:[],dropEvts:[],enterEvts:[]};for(var S in this.dragOvers){var d=this.dragOvers[S];if(!this.isTypeOfDD(d)){continue; -}if(!this.isOverTarget(P,d,this.mode,U)){c.outEvts.push(d);}I[S]=true;delete this.dragOvers[S];}for(var R in a.groups){if("string"!=typeof R){continue;}for(S in this.ids[R]){var G=this.ids[R][S];if(!this.isTypeOfDD(G)){continue;}if(G.isTarget&&!G.isLocked()&&G!=a){if(this.isOverTarget(P,G,this.mode,U)){D[R]=true;if(L){c.dropEvts.push(G);}else{if(!I[G.id]){c.enterEvts.push(G);}else{c.overEvts.push(G);}this.dragOvers[G.id]=G;}}}}}this.interactionInfo={out:c.outEvts,enter:c.enterEvts,over:c.overEvts,drop:c.dropEvts,point:P,draggedRegion:U,sourceRegion:this.locationCache[a.id],validDrop:L};for(var C in D){Q.push(C);}if(L&&!c.dropEvts.length){this.interactionInfo.validDrop=false;if(a.events.invalidDrop){a.onInvalidDrop(V);a.fireEvent("invalidDropEvent",{e:V});}}for(S=0;S2000){}else{setTimeout(C._addListeners,10);if(document&&document.body){C._timeoutCount+=1;}}}},handleWasClicked:function(C,E){if(this.isHandle(E,C.id)){return true;}else{var D=C.parentNode;while(D){if(this.isHandle(E,D.id)){return true;}else{D=D.parentNode;}}}return false;}};}();YAHOO.util.DDM=YAHOO.util.DragDropMgr;YAHOO.util.DDM._addListeners();}(function(){var A=YAHOO.util.Event;var B=YAHOO.util.Dom;YAHOO.util.DragDrop=function(E,C,D){if(E){this.init(E,C,D);}};YAHOO.util.DragDrop.prototype={events:null,on:function(){this.subscribe.apply(this,arguments);},id:null,config:null,dragElId:null,handleElId:null,invalidHandleTypes:null,invalidHandleIds:null,invalidHandleClasses:null,startPageX:0,startPageY:0,groups:null,locked:false,lock:function(){this.locked=true;},unlock:function(){this.locked=false;},isTarget:true,padding:null,dragOnly:false,useShim:false,_domRef:null,__ygDragDrop:true,constrainX:false,constrainY:false,minX:0,maxX:0,minY:0,maxY:0,deltaX:0,deltaY:0,maintainOffset:false,xTicks:null,yTicks:null,primaryButtonOnly:true,available:false,hasOuterHandles:false,cursorIsOver:false,overlap:null,b4StartDrag:function(C,D){},startDrag:function(C,D){},b4Drag:function(C){},onDrag:function(C){},onDragEnter:function(C,D){},b4DragOver:function(C){},onDragOver:function(C,D){},b4DragOut:function(C){},onDragOut:function(C,D){},b4DragDrop:function(C){},onDragDrop:function(C,D){},onInvalidDrop:function(C){},b4EndDrag:function(C){},endDrag:function(C){},b4MouseDown:function(C){},onMouseDown:function(C){},onMouseUp:function(C){},onAvailable:function(){},getEl:function(){if(!this._domRef){this._domRef=B.get(this.id); -}return this._domRef;},getDragEl:function(){return B.get(this.dragElId);},init:function(F,C,D){this.initTarget(F,C,D);A.on(this._domRef||this.id,"mousedown",this.handleMouseDown,this,true);for(var E in this.events){this.createEvent(E+"Event");}},initTarget:function(E,C,D){this.config=D||{};this.events={};this.DDM=YAHOO.util.DDM;this.groups={};if(typeof E!=="string"){this._domRef=E;E=B.generateId(E);}this.id=E;this.addToGroup((C)?C:"default");this.handleElId=E;A.onAvailable(E,this.handleOnAvailable,this,true);this.setDragElId(E);this.invalidHandleTypes={A:"A"};this.invalidHandleIds={};this.invalidHandleClasses=[];this.applyConfig();},applyConfig:function(){this.events={mouseDown:true,b4MouseDown:true,mouseUp:true,b4StartDrag:true,startDrag:true,b4EndDrag:true,endDrag:true,drag:true,b4Drag:true,invalidDrop:true,b4DragOut:true,dragOut:true,dragEnter:true,b4DragOver:true,dragOver:true,b4DragDrop:true,dragDrop:true};if(this.config.events){for(var C in this.config.events){if(this.config.events[C]===false){this.events[C]=false;}}}this.padding=this.config.padding||[0,0,0,0];this.isTarget=(this.config.isTarget!==false);this.maintainOffset=(this.config.maintainOffset);this.primaryButtonOnly=(this.config.primaryButtonOnly!==false);this.dragOnly=((this.config.dragOnly===true)?true:false);this.useShim=((this.config.useShim===true)?true:false);},handleOnAvailable:function(){this.available=true;this.resetConstraints();this.onAvailable();},setPadding:function(E,C,F,D){if(!C&&0!==C){this.padding=[E,E,E,E];}else{if(!F&&0!==F){this.padding=[E,C,E,C];}else{this.padding=[E,C,F,D];}}},setInitPosition:function(F,E){var G=this.getEl();if(!this.DDM.verifyEl(G)){if(G&&G.style&&(G.style.display=="none")){}else{}return;}var D=F||0;var C=E||0;var H=B.getXY(G);this.initPageX=H[0]-D;this.initPageY=H[1]-C;this.lastPageX=H[0];this.lastPageY=H[1];this.setStartPosition(H);},setStartPosition:function(D){var C=D||B.getXY(this.getEl());this.deltaSetXY=null;this.startPageX=C[0];this.startPageY=C[1];},addToGroup:function(C){this.groups[C]=true;this.DDM.regDragDrop(this,C);},removeFromGroup:function(C){if(this.groups[C]){delete this.groups[C];}this.DDM.removeDDFromGroup(this,C);},setDragElId:function(C){this.dragElId=C;},setHandleElId:function(C){if(typeof C!=="string"){C=B.generateId(C);}this.handleElId=C;this.DDM.regHandle(this.id,C);},setOuterHandleElId:function(C){if(typeof C!=="string"){C=B.generateId(C);}A.on(C,"mousedown",this.handleMouseDown,this,true);this.setHandleElId(C);this.hasOuterHandles=true;},unreg:function(){A.removeListener(this.id,"mousedown",this.handleMouseDown);this._domRef=null;this.DDM._remove(this);},isLocked:function(){return(this.DDM.isLocked()||this.locked);},handleMouseDown:function(J,I){var D=J.which||J.button;if(this.primaryButtonOnly&&D>1){return;}if(this.isLocked()){return;}var C=this.b4MouseDown(J),F=true;if(this.events.b4MouseDown){F=this.fireEvent("b4MouseDownEvent",J);}var E=this.onMouseDown(J),H=true;if(this.events.mouseDown){H=this.fireEvent("mouseDownEvent",J);}if((C===false)||(E===false)||(F===false)||(H===false)){return;}this.DDM.refreshCache(this.groups);var G=new YAHOO.util.Point(A.getPageX(J),A.getPageY(J));if(!this.hasOuterHandles&&!this.DDM.isOverTarget(G,this)){}else{if(this.clickValidator(J)){this.setStartPosition();this.DDM.handleMouseDown(J,this);this.DDM.stopEvent(J);}else{}}},clickValidator:function(D){var C=YAHOO.util.Event.getTarget(D);return(this.isValidHandleChild(C)&&(this.id==this.handleElId||this.DDM.handleWasClicked(C,this.id)));},getTargetCoord:function(E,D){var C=E-this.deltaX;var F=D-this.deltaY;if(this.constrainX){if(Cthis.maxX){C=this.maxX;}}if(this.constrainY){if(Fthis.maxY){F=this.maxY;}}C=this.getTick(C,this.xTicks);F=this.getTick(F,this.yTicks);return{x:C,y:F};},addInvalidHandleType:function(C){var D=C.toUpperCase();this.invalidHandleTypes[D]=D;},addInvalidHandleId:function(C){if(typeof C!=="string"){C=B.generateId(C);}this.invalidHandleIds[C]=C;},addInvalidHandleClass:function(C){this.invalidHandleClasses.push(C);},removeInvalidHandleType:function(C){var D=C.toUpperCase();delete this.invalidHandleTypes[D];},removeInvalidHandleId:function(C){if(typeof C!=="string"){C=B.generateId(C);}delete this.invalidHandleIds[C];},removeInvalidHandleClass:function(D){for(var E=0,C=this.invalidHandleClasses.length;E=this.minX;D=D-C){if(!E[D]){this.xTicks[this.xTicks.length]=D;E[D]=true;}}for(D=this.initPageX;D<=this.maxX;D=D+C){if(!E[D]){this.xTicks[this.xTicks.length]=D;E[D]=true;}}this.xTicks.sort(this.DDM.numericSort);},setYTicks:function(F,C){this.yTicks=[];this.yTickSize=C;var E={};for(var D=this.initPageY;D>=this.minY;D=D-C){if(!E[D]){this.yTicks[this.yTicks.length]=D;E[D]=true;}}for(D=this.initPageY;D<=this.maxY;D=D+C){if(!E[D]){this.yTicks[this.yTicks.length]=D;E[D]=true;}}this.yTicks.sort(this.DDM.numericSort);},setXConstraint:function(E,D,C){this.leftConstraint=parseInt(E,10);this.rightConstraint=parseInt(D,10);this.minX=this.initPageX-this.leftConstraint;this.maxX=this.initPageX+this.rightConstraint;if(C){this.setXTicks(this.initPageX,C);}this.constrainX=true;},clearConstraints:function(){this.constrainX=false;this.constrainY=false;this.clearTicks();},clearTicks:function(){this.xTicks=null;this.yTicks=null;this.xTickSize=0;this.yTickSize=0;},setYConstraint:function(C,E,D){this.topConstraint=parseInt(C,10);this.bottomConstraint=parseInt(E,10);this.minY=this.initPageY-this.topConstraint;this.maxY=this.initPageY+this.bottomConstraint;if(D){this.setYTicks(this.initPageY,D); -}this.constrainY=true;},resetConstraints:function(){if(this.initPageX||this.initPageX===0){var D=(this.maintainOffset)?this.lastPageX-this.initPageX:0;var C=(this.maintainOffset)?this.lastPageY-this.initPageY:0;this.setInitPosition(D,C);}else{this.setInitPosition();}if(this.constrainX){this.setXConstraint(this.leftConstraint,this.rightConstraint,this.xTickSize);}if(this.constrainY){this.setYConstraint(this.topConstraint,this.bottomConstraint,this.yTickSize);}},getTick:function(I,F){if(!F){return I;}else{if(F[0]>=I){return F[0];}else{for(var D=0,C=F.length;D=I){var H=I-F[D];var G=F[E]-I;return(G>H)?F[D]:F[E];}}return F[F.length-1];}}},toString:function(){return("DragDrop "+this.id);}};YAHOO.augment(YAHOO.util.DragDrop,YAHOO.util.EventProvider);})();YAHOO.util.DD=function(C,A,B){if(C){this.init(C,A,B);}};YAHOO.extend(YAHOO.util.DD,YAHOO.util.DragDrop,{scroll:true,autoOffset:function(C,B){var A=C-this.startPageX;var D=B-this.startPageY;this.setDelta(A,D);},setDelta:function(B,A){this.deltaX=B;this.deltaY=A;},setDragElPos:function(C,B){var A=this.getDragEl();this.alignElWithMouse(A,C,B);},alignElWithMouse:function(C,G,F){var E=this.getTargetCoord(G,F);if(!this.deltaSetXY){var H=[E.x,E.y];YAHOO.util.Dom.setXY(C,H);var D=parseInt(YAHOO.util.Dom.getStyle(C,"left"),10);var B=parseInt(YAHOO.util.Dom.getStyle(C,"top"),10);this.deltaSetXY=[D-E.x,B-E.y];}else{YAHOO.util.Dom.setStyle(C,"left",(E.x+this.deltaSetXY[0])+"px");YAHOO.util.Dom.setStyle(C,"top",(E.y+this.deltaSetXY[1])+"px");}this.cachePosition(E.x,E.y);var A=this;setTimeout(function(){A.autoScroll.call(A,E.x,E.y,C.offsetHeight,C.offsetWidth);},0);},cachePosition:function(B,A){if(B){this.lastPageX=B;this.lastPageY=A;}else{var C=YAHOO.util.Dom.getXY(this.getEl());this.lastPageX=C[0];this.lastPageY=C[1];}},autoScroll:function(J,I,E,K){if(this.scroll){var L=this.DDM.getClientHeight();var B=this.DDM.getClientWidth();var N=this.DDM.getScrollTop();var D=this.DDM.getScrollLeft();var H=E+I;var M=K+J;var G=(L+N-I-this.deltaY);var F=(B+D-J-this.deltaX);var C=40;var A=(document.all)?80:30;if(H>L&&G0&&I-NB&&F0&&J-D0){if(!aCache){this._aCache=[];}else{var nCacheLength=aCache.length;if(nCacheLength>0){var oResponse=null;this.fireEvent("cacheRequestEvent",{request:oRequest,callback:oCallback,caller:oCaller});for(var i=nCacheLength-1;i>=0;i--){var oCacheElem=aCache[i];if(this.isCacheHit(oRequest,oCacheElem.request)){oResponse=oCacheElem.response;this.fireEvent("cacheResponseEvent",{request:oRequest,response:oResponse,callback:oCallback,caller:oCaller});if(i=this.maxCacheEntries){aCache.shift();}var oCacheElem={request:oRequest,response:oResponse};aCache[aCache.length]=oCacheElem;this.fireEvent("responseCacheEvent",{request:oRequest,response:oResponse});},flushCache:function(){if(this._aCache){this._aCache=[];this.fireEvent("cacheFlushEvent");}},setInterval:function(nMsec,oRequest,oCallback,oCaller){if(lang.isNumber(nMsec)&&(nMsec>=0)){var oSelf=this;var nId=setInterval(function(){oSelf.makeConnection(oRequest,oCallback,oCaller);},nMsec);this._aIntervals.push(nId);return nId;}else{}},clearInterval:function(nId){var tracker=this._aIntervals||[];for(var i=tracker.length-1;i>-1;i--){if(tracker[i]===nId){tracker.splice(i,1);clearInterval(nId);}}},clearAllIntervals:function(){var tracker=this._aIntervals||[];for(var i=tracker.length-1;i>-1;i--){clearInterval(tracker[i]);}tracker=[];},sendRequest:function(oRequest,oCallback,oCaller){var oCachedResponse=this.getCachedResponse(oRequest,oCallback,oCaller);if(oCachedResponse){DS.issueCallback(oCallback,[oRequest,oCachedResponse],false,oCaller);return null;}return this.makeConnection(oRequest,oCallback,oCaller);},makeConnection:function(oRequest,oCallback,oCaller){var tId=DS._nTransactionId++;this.fireEvent("requestEvent",{tId:tId,request:oRequest,callback:oCallback,caller:oCaller});var oRawResponse=this.liveData;this.handleResponse(oRequest,oRawResponse,oCallback,oCaller,tId);return tId;},handleResponse:function(oRequest,oRawResponse,oCallback,oCaller,tId){this.fireEvent("responseEvent",{tId:tId,request:oRequest,response:oRawResponse,callback:oCallback,caller:oCaller});var xhr=(this.dataType==DS.TYPE_XHR)?true:false;var oParsedResponse=null;var oFullResponse=oRawResponse;if(this.responseType===DS.TYPE_UNKNOWN){var ctype=(oRawResponse&&oRawResponse.getResponseHeader)?oRawResponse.getResponseHeader["Content-Type"]:null;if(ctype){if(ctype.indexOf("text/xml")>-1){this.responseType=DS.TYPE_XML;}else{if(ctype.indexOf("application/json")>-1){this.responseType=DS.TYPE_JSON;}else{if(ctype.indexOf("text/plain")>-1){this.responseType=DS.TYPE_TEXT;}}}}else{if(YAHOO.lang.isArray(oRawResponse)){this.responseType=DS.TYPE_JSARRAY; -}else{if(oRawResponse&&oRawResponse.nodeType&&(oRawResponse.nodeType===9||oRawResponse.nodeType===1||oRawResponse.nodeType===11)){this.responseType=DS.TYPE_XML;}else{if(oRawResponse&&oRawResponse.nodeName&&(oRawResponse.nodeName.toLowerCase()=="table")){this.responseType=DS.TYPE_HTMLTABLE;}else{if(YAHOO.lang.isObject(oRawResponse)){this.responseType=DS.TYPE_JSON;}else{if(YAHOO.lang.isString(oRawResponse)){this.responseType=DS.TYPE_TEXT;}}}}}}}switch(this.responseType){case DS.TYPE_JSARRAY:if(xhr&&oRawResponse&&oRawResponse.responseText){oFullResponse=oRawResponse.responseText;}try{if(lang.isString(oFullResponse)){var parseArgs=[oFullResponse].concat(this.parseJSONArgs);if(lang.JSON){oFullResponse=lang.JSON.parse.apply(lang.JSON,parseArgs);}else{if(window.JSON&&JSON.parse){oFullResponse=JSON.parse.apply(JSON,parseArgs);}else{if(oFullResponse.parseJSON){oFullResponse=oFullResponse.parseJSON.apply(oFullResponse,parseArgs.slice(1));}else{while(oFullResponse.length>0&&(oFullResponse.charAt(0)!="{")&&(oFullResponse.charAt(0)!="[")){oFullResponse=oFullResponse.substring(1,oFullResponse.length);}if(oFullResponse.length>0){var arrayEnd=Math.max(oFullResponse.lastIndexOf("]"),oFullResponse.lastIndexOf("}"));oFullResponse=oFullResponse.substring(0,arrayEnd+1);oFullResponse=eval("("+oFullResponse+")");}}}}}}catch(e1){}oFullResponse=this.doBeforeParseData(oRequest,oFullResponse,oCallback);oParsedResponse=this.parseArrayData(oRequest,oFullResponse);break;case DS.TYPE_JSON:if(xhr&&oRawResponse&&oRawResponse.responseText){oFullResponse=oRawResponse.responseText;}try{if(lang.isString(oFullResponse)){var parseArgs=[oFullResponse].concat(this.parseJSONArgs);if(lang.JSON){oFullResponse=lang.JSON.parse.apply(lang.JSON,parseArgs);}else{if(window.JSON&&JSON.parse){oFullResponse=JSON.parse.apply(JSON,parseArgs);}else{if(oFullResponse.parseJSON){oFullResponse=oFullResponse.parseJSON.apply(oFullResponse,parseArgs.slice(1));}else{while(oFullResponse.length>0&&(oFullResponse.charAt(0)!="{")&&(oFullResponse.charAt(0)!="[")){oFullResponse=oFullResponse.substring(1,oFullResponse.length);}if(oFullResponse.length>0){var objEnd=Math.max(oFullResponse.lastIndexOf("]"),oFullResponse.lastIndexOf("}"));oFullResponse=oFullResponse.substring(0,objEnd+1);oFullResponse=eval("("+oFullResponse+")");}}}}}}catch(e){}oFullResponse=this.doBeforeParseData(oRequest,oFullResponse,oCallback);oParsedResponse=this.parseJSONData(oRequest,oFullResponse);break;case DS.TYPE_HTMLTABLE:if(xhr&&oRawResponse.responseText){var el=document.createElement("div");el.innerHTML=oRawResponse.responseText;oFullResponse=el.getElementsByTagName("table")[0];}oFullResponse=this.doBeforeParseData(oRequest,oFullResponse,oCallback);oParsedResponse=this.parseHTMLTableData(oRequest,oFullResponse);break;case DS.TYPE_XML:if(xhr&&oRawResponse.responseXML){oFullResponse=oRawResponse.responseXML;}oFullResponse=this.doBeforeParseData(oRequest,oFullResponse,oCallback);oParsedResponse=this.parseXMLData(oRequest,oFullResponse);break;case DS.TYPE_TEXT:if(xhr&&lang.isString(oRawResponse.responseText)){oFullResponse=oRawResponse.responseText;}oFullResponse=this.doBeforeParseData(oRequest,oFullResponse,oCallback);oParsedResponse=this.parseTextData(oRequest,oFullResponse);break;default:oFullResponse=this.doBeforeParseData(oRequest,oFullResponse,oCallback);oParsedResponse=this.parseData(oRequest,oFullResponse);break;}oParsedResponse=oParsedResponse||{};if(!oParsedResponse.results){oParsedResponse.results=[];}if(!oParsedResponse.meta){oParsedResponse.meta={};}if(!oParsedResponse.error){oParsedResponse=this.doBeforeCallback(oRequest,oFullResponse,oParsedResponse,oCallback);this.fireEvent("responseParseEvent",{request:oRequest,response:oParsedResponse,callback:oCallback,caller:oCaller});this.addToCache(oRequest,oParsedResponse);}else{oParsedResponse.error=true;this.fireEvent("dataErrorEvent",{request:oRequest,response:oRawResponse,callback:oCallback,caller:oCaller,message:DS.ERROR_DATANULL});}oParsedResponse.tId=tId;DS.issueCallback(oCallback,[oRequest,oParsedResponse],oParsedResponse.error,oCaller);},doBeforeParseData:function(oRequest,oFullResponse,oCallback){return oFullResponse;},doBeforeCallback:function(oRequest,oFullResponse,oParsedResponse,oCallback){return oParsedResponse;},parseData:function(oRequest,oFullResponse){if(lang.isValue(oFullResponse)){var oParsedResponse={results:oFullResponse,meta:{}};return oParsedResponse;}return null;},parseArrayData:function(oRequest,oFullResponse){if(lang.isArray(oFullResponse)){var results=[],i,j,rec,field,data;if(lang.isArray(this.responseSchema.fields)){var fields=this.responseSchema.fields;for(i=fields.length-1;i>=0;--i){if(typeof fields[i]!=="object"){fields[i]={key:fields[i]};}}var parsers={},p;for(i=fields.length-1;i>=0;--i){p=(typeof fields[i].parser==="function"?fields[i].parser:DS.Parser[fields[i].parser+""])||fields[i].converter;if(p){parsers[fields[i].key]=p;}}var arrType=lang.isArray(oFullResponse[0]);for(i=oFullResponse.length-1;i>-1;i--){var oResult={};rec=oFullResponse[i];if(typeof rec==="object"){for(j=fields.length-1;j>-1;j--){field=fields[j];data=arrType?rec[j]:rec[field.key];if(parsers[field.key]){data=parsers[field.key].call(this,data);}if(data===undefined){data=null;}oResult[field.key]=data;}}else{if(lang.isString(rec)){for(j=fields.length-1;j>-1;j--){field=fields[j];data=rec;if(parsers[field.key]){data=parsers[field.key].call(this,data);}if(data===undefined){data=null;}oResult[field.key]=data;}}}results[i]=oResult;}}else{results=oFullResponse;}var oParsedResponse={results:results};return oParsedResponse;}return null;},parseTextData:function(oRequest,oFullResponse){if(lang.isString(oFullResponse)){if(lang.isString(this.responseSchema.recordDelim)&&lang.isString(this.responseSchema.fieldDelim)){var oParsedResponse={results:[]};var recDelim=this.responseSchema.recordDelim;var fieldDelim=this.responseSchema.fieldDelim;if(oFullResponse.length>0){var newLength=oFullResponse.length-recDelim.length;if(oFullResponse.substr(newLength)==recDelim){oFullResponse=oFullResponse.substr(0,newLength); -}if(oFullResponse.length>0){var recordsarray=oFullResponse.split(recDelim);for(var i=0,len=recordsarray.length,recIdx=0;i0)){var fielddataarray=recordsarray[i].split(fieldDelim);var oResult={};if(lang.isArray(this.responseSchema.fields)){var fields=this.responseSchema.fields;for(var j=fields.length-1;j>-1;j--){try{var data=fielddataarray[j];if(lang.isString(data)){if(data.charAt(0)=='"'){data=data.substr(1);}if(data.charAt(data.length-1)=='"'){data=data.substr(0,data.length-1);}var field=fields[j];var key=(lang.isValue(field.key))?field.key:field;if(!field.parser&&field.converter){field.parser=field.converter;}var parser=(typeof field.parser==="function")?field.parser:DS.Parser[field.parser+""];if(parser){data=parser.call(this,data);}if(data===undefined){data=null;}oResult[key]=data;}else{bError=true;}}catch(e){bError=true;}}}else{oResult=fielddataarray;}if(!bError){oParsedResponse.results[recIdx++]=oResult;}}}}}return oParsedResponse;}}return null;},parseXMLResult:function(result){var oResult={},schema=this.responseSchema;try{for(var m=schema.fields.length-1;m>=0;m--){var field=schema.fields[m];var key=(lang.isValue(field.key))?field.key:field;var data=null;if(this.useXPath){data=YAHOO.util.DataSource._getLocationValue(field,result);}else{var xmlAttr=result.attributes.getNamedItem(key);if(xmlAttr){data=xmlAttr.value;}else{var xmlNode=result.getElementsByTagName(key);if(xmlNode&&xmlNode.item(0)){var item=xmlNode.item(0);data=(item)?((item.text)?item.text:(item.textContent)?item.textContent:null):null;if(!data){var datapieces=[];for(var j=0,len=item.childNodes.length;j0){data=datapieces.join("");}}}}}if(data===null){data="";}if(!field.parser&&field.converter){field.parser=field.converter;}var parser=(typeof field.parser==="function")?field.parser:DS.Parser[field.parser+""];if(parser){data=parser.call(this,data);}if(data===undefined){data=null;}oResult[key]=data;}}catch(e){}return oResult;},parseXMLData:function(oRequest,oFullResponse){var bError=false,schema=this.responseSchema,oParsedResponse={meta:{}},xmlList=null,metaNode=schema.metaNode,metaLocators=schema.metaFields||{},i,k,loc,v;try{if(this.useXPath){for(k in metaLocators){oParsedResponse.meta[k]=YAHOO.util.DataSource._getLocationValue(metaLocators[k],oFullResponse);}}else{metaNode=metaNode?oFullResponse.getElementsByTagName(metaNode)[0]:oFullResponse;if(metaNode){for(k in metaLocators){if(lang.hasOwnProperty(metaLocators,k)){loc=metaLocators[k];v=metaNode.getElementsByTagName(loc)[0];if(v){v=v.firstChild.nodeValue;}else{v=metaNode.attributes.getNamedItem(loc);if(v){v=v.value;}}if(lang.isValue(v)){oParsedResponse.meta[k]=v;}}}}}xmlList=(schema.resultNode)?oFullResponse.getElementsByTagName(schema.resultNode):null;}catch(e){}if(!xmlList||!lang.isArray(schema.fields)){bError=true;}else{oParsedResponse.results=[];for(i=xmlList.length-1;i>=0;--i){var oResult=this.parseXMLResult(xmlList.item(i));oParsedResponse.results[i]=oResult;}}if(bError){oParsedResponse.error=true;}else{}return oParsedResponse;},parseJSONData:function(oRequest,oFullResponse){var oParsedResponse={results:[],meta:{}};if(lang.isObject(oFullResponse)&&this.responseSchema.resultsList){var schema=this.responseSchema,fields=schema.fields,resultsList=oFullResponse,results=[],metaFields=schema.metaFields||{},fieldParsers=[],fieldPaths=[],simpleFields=[],bError=false,i,len,j,v,key,parser,path;var buildPath=function(needle){var path=null,keys=[],i=0;if(needle){needle=needle.replace(/\[(['"])(.*?)\1\]/g,function(x,$1,$2){keys[i]=$2;return".@"+(i++);}).replace(/\[(\d+)\]/g,function(x,$1){keys[i]=parseInt($1,10)|0;return".@"+(i++);}).replace(/^\./,"");if(!/[^\w\.\$@]/.test(needle)){path=needle.split(".");for(i=path.length-1;i>=0;--i){if(path[i].charAt(0)==="@"){path[i]=keys[parseInt(path[i].substr(1),10)];}}}else{}}return path;};var walkPath=function(path,origin){var v=origin,i=0,len=path.length;for(;i1){fieldPaths[fieldPaths.length]={key:key,path:path};}else{simpleFields[simpleFields.length]={key:key,path:path[0]};}}else{}}for(i=resultsList.length-1;i>=0;--i){var r=resultsList[i],rec={};if(r){for(j=simpleFields.length-1;j>=0;--j){rec[simpleFields[j].key]=(r[simpleFields[j].path]!==undefined)?r[simpleFields[j].path]:r[j];}for(j=fieldPaths.length-1;j>=0;--j){rec[fieldPaths[j].key]=walkPath(fieldPaths[j].path,r);}for(j=fieldParsers.length-1;j>=0;--j){var p=fieldParsers[j].key;rec[p]=fieldParsers[j].parser(rec[p]);if(rec[p]===undefined){rec[p]=null;}}}results[i]=rec;}}else{results=resultsList;}for(key in metaFields){if(lang.hasOwnProperty(metaFields,key)){path=buildPath(metaFields[key]);if(path){v=walkPath(path,oFullResponse);oParsedResponse.meta[key]=v;}}}}else{oParsedResponse.error=true;}oParsedResponse.results=results;}else{oParsedResponse.error=true;}return oParsedResponse;},parseHTMLTableData:function(oRequest,oFullResponse){var bError=false;var elTable=oFullResponse;var fields=this.responseSchema.fields;var oParsedResponse={results:[]};if(lang.isArray(fields)){for(var i=0;i-1;j--){var elRow=elTbody.rows[j];var oResult={};for(var k=fields.length-1;k>-1;k--){var field=fields[k];var key=(lang.isValue(field.key))?field.key:field; -var data=elRow.cells[k].innerHTML;if(!field.parser&&field.converter){field.parser=field.converter;}var parser=(typeof field.parser==="function")?field.parser:DS.Parser[field.parser+""];if(parser){data=parser.call(this,data);}if(data===undefined){data=null;}oResult[key]=data;}oParsedResponse.results[j]=oResult;}}}else{bError=true;}if(bError){oParsedResponse.error=true;}else{}return oParsedResponse;}};lang.augmentProto(DS,util.EventProvider);util.LocalDataSource=function(oLiveData,oConfigs){this.dataType=DS.TYPE_LOCAL;if(oLiveData){if(YAHOO.lang.isArray(oLiveData)){this.responseType=DS.TYPE_JSARRAY;}else{if(oLiveData.nodeType&&oLiveData.nodeType==9){this.responseType=DS.TYPE_XML;}else{if(oLiveData.nodeName&&(oLiveData.nodeName.toLowerCase()=="table")){this.responseType=DS.TYPE_HTMLTABLE;oLiveData=oLiveData.cloneNode(true);}else{if(YAHOO.lang.isString(oLiveData)){this.responseType=DS.TYPE_TEXT;}else{if(YAHOO.lang.isObject(oLiveData)){this.responseType=DS.TYPE_JSON;}}}}}}else{oLiveData=[];this.responseType=DS.TYPE_JSARRAY;}util.LocalDataSource.superclass.constructor.call(this,oLiveData,oConfigs);};lang.extend(util.LocalDataSource,DS);lang.augmentObject(util.LocalDataSource,DS);util.FunctionDataSource=function(oLiveData,oConfigs){this.dataType=DS.TYPE_JSFUNCTION;oLiveData=oLiveData||function(){};util.FunctionDataSource.superclass.constructor.call(this,oLiveData,oConfigs);};lang.extend(util.FunctionDataSource,DS,{scope:null,makeConnection:function(oRequest,oCallback,oCaller){var tId=DS._nTransactionId++;this.fireEvent("requestEvent",{tId:tId,request:oRequest,callback:oCallback,caller:oCaller});var oRawResponse=(this.scope)?this.liveData.call(this.scope,oRequest,this):this.liveData(oRequest);if(this.responseType===DS.TYPE_UNKNOWN){if(YAHOO.lang.isArray(oRawResponse)){this.responseType=DS.TYPE_JSARRAY;}else{if(oRawResponse&&oRawResponse.nodeType&&oRawResponse.nodeType==9){this.responseType=DS.TYPE_XML;}else{if(oRawResponse&&oRawResponse.nodeName&&(oRawResponse.nodeName.toLowerCase()=="table")){this.responseType=DS.TYPE_HTMLTABLE;}else{if(YAHOO.lang.isObject(oRawResponse)){this.responseType=DS.TYPE_JSON;}else{if(YAHOO.lang.isString(oRawResponse)){this.responseType=DS.TYPE_TEXT;}}}}}}this.handleResponse(oRequest,oRawResponse,oCallback,oCaller,tId);return tId;}});lang.augmentObject(util.FunctionDataSource,DS);util.ScriptNodeDataSource=function(oLiveData,oConfigs){this.dataType=DS.TYPE_SCRIPTNODE;oLiveData=oLiveData||"";util.ScriptNodeDataSource.superclass.constructor.call(this,oLiveData,oConfigs);};lang.extend(util.ScriptNodeDataSource,DS,{getUtility:util.Get,asyncMode:"allowAll",scriptCallbackParam:"callback",generateRequestCallback:function(id){return"&"+this.scriptCallbackParam+"=YAHOO.util.ScriptNodeDataSource.callbacks["+id+"]";},doBeforeGetScriptNode:function(sUri){return sUri;},makeConnection:function(oRequest,oCallback,oCaller){var tId=DS._nTransactionId++;this.fireEvent("requestEvent",{tId:tId,request:oRequest,callback:oCallback,caller:oCaller});if(util.ScriptNodeDataSource._nPending===0){util.ScriptNodeDataSource.callbacks=[];util.ScriptNodeDataSource._nId=0;}var id=util.ScriptNodeDataSource._nId;util.ScriptNodeDataSource._nId++;var oSelf=this;util.ScriptNodeDataSource.callbacks[id]=function(oRawResponse){if((oSelf.asyncMode!=="ignoreStaleResponses")||(id===util.ScriptNodeDataSource.callbacks.length-1)){if(oSelf.responseType===DS.TYPE_UNKNOWN){if(YAHOO.lang.isArray(oRawResponse)){oSelf.responseType=DS.TYPE_JSARRAY;}else{if(oRawResponse.nodeType&&oRawResponse.nodeType==9){oSelf.responseType=DS.TYPE_XML;}else{if(oRawResponse.nodeName&&(oRawResponse.nodeName.toLowerCase()=="table")){oSelf.responseType=DS.TYPE_HTMLTABLE;}else{if(YAHOO.lang.isObject(oRawResponse)){oSelf.responseType=DS.TYPE_JSON;}else{if(YAHOO.lang.isString(oRawResponse)){oSelf.responseType=DS.TYPE_TEXT;}}}}}}oSelf.handleResponse(oRequest,oRawResponse,oCallback,oCaller,tId);}else{}delete util.ScriptNodeDataSource.callbacks[id];};util.ScriptNodeDataSource._nPending++;var sUri=this.liveData+oRequest+this.generateRequestCallback(id);sUri=this.doBeforeGetScriptNode(sUri);this.getUtility.script(sUri,{autopurge:true,onsuccess:util.ScriptNodeDataSource._bumpPendingDown,onfail:util.ScriptNodeDataSource._bumpPendingDown});return tId;}});lang.augmentObject(util.ScriptNodeDataSource,DS);lang.augmentObject(util.ScriptNodeDataSource,{_nId:0,_nPending:0,callbacks:[]});util.XHRDataSource=function(oLiveData,oConfigs){this.dataType=DS.TYPE_XHR;this.connMgr=this.connMgr||util.Connect;oLiveData=oLiveData||"";util.XHRDataSource.superclass.constructor.call(this,oLiveData,oConfigs);};lang.extend(util.XHRDataSource,DS,{connMgr:null,connXhrMode:"allowAll",connMethodPost:false,connTimeout:0,makeConnection:function(oRequest,oCallback,oCaller){var oRawResponse=null;var tId=DS._nTransactionId++;this.fireEvent("requestEvent",{tId:tId,request:oRequest,callback:oCallback,caller:oCaller});var oSelf=this;var oConnMgr=this.connMgr;var oQueue=this._oQueue;var _xhrSuccess=function(oResponse){if(oResponse&&(this.connXhrMode=="ignoreStaleResponses")&&(oResponse.tId!=oQueue.conn.tId)){return null;}else{if(!oResponse){this.fireEvent("dataErrorEvent",{request:oRequest,response:null,callback:oCallback,caller:oCaller,message:DS.ERROR_DATANULL});DS.issueCallback(oCallback,[oRequest,{error:true}],true,oCaller);return null;}else{if(this.responseType===DS.TYPE_UNKNOWN){var ctype=(oResponse.getResponseHeader)?oResponse.getResponseHeader["Content-Type"]:null;if(ctype){if(ctype.indexOf("text/xml")>-1){this.responseType=DS.TYPE_XML;}else{if(ctype.indexOf("application/json")>-1){this.responseType=DS.TYPE_JSON;}else{if(ctype.indexOf("text/plain")>-1){this.responseType=DS.TYPE_TEXT;}}}}}this.handleResponse(oRequest,oResponse,oCallback,oCaller,tId);}}};var _xhrFailure=function(oResponse){this.fireEvent("dataErrorEvent",{request:oRequest,response:oResponse,callback:oCallback,caller:oCaller,message:DS.ERROR_DATAINVALID});if(lang.isString(this.liveData)&&lang.isString(oRequest)&&(this.liveData.lastIndexOf("?")!==this.liveData.length-1)&&(oRequest.indexOf("?")!==0)){}oResponse=oResponse||{}; -oResponse.error=true;DS.issueCallback(oCallback,[oRequest,oResponse],true,oCaller);return null;};var _xhrCallback={success:_xhrSuccess,failure:_xhrFailure,scope:this};if(lang.isNumber(this.connTimeout)){_xhrCallback.timeout=this.connTimeout;}if(this.connXhrMode=="cancelStaleRequests"){if(oQueue.conn){if(oConnMgr.abort){oConnMgr.abort(oQueue.conn);oQueue.conn=null;}else{}}}if(oConnMgr&&oConnMgr.asyncRequest){var sLiveData=this.liveData;var isPost=this.connMethodPost;var sMethod=(isPost)?"POST":"GET";var sUri=(isPost||!lang.isValue(oRequest))?sLiveData:sLiveData+oRequest;var sRequest=(isPost)?oRequest:null;if(this.connXhrMode!="queueRequests"){oQueue.conn=oConnMgr.asyncRequest(sMethod,sUri,_xhrCallback,sRequest);}else{if(oQueue.conn){var allRequests=oQueue.requests;allRequests.push({request:oRequest,callback:_xhrCallback});if(!oQueue.interval){oQueue.interval=setInterval(function(){if(oConnMgr.isCallInProgress(oQueue.conn)){return;}else{if(allRequests.length>0){sUri=(isPost||!lang.isValue(allRequests[0].request))?sLiveData:sLiveData+allRequests[0].request;sRequest=(isPost)?allRequests[0].request:null;oQueue.conn=oConnMgr.asyncRequest(sMethod,sUri,allRequests[0].callback,sRequest);allRequests.shift();}else{clearInterval(oQueue.interval);oQueue.interval=null;}}},50);}}else{oQueue.conn=oConnMgr.asyncRequest(sMethod,sUri,_xhrCallback,sRequest);}}}else{DS.issueCallback(oCallback,[oRequest,{error:true}],true,oCaller);}return tId;}});lang.augmentObject(util.XHRDataSource,DS);util.DataSource=function(oLiveData,oConfigs){oConfigs=oConfigs||{};var dataType=oConfigs.dataType;if(dataType){if(dataType==DS.TYPE_LOCAL){lang.augmentObject(util.DataSource,util.LocalDataSource);return new util.LocalDataSource(oLiveData,oConfigs);}else{if(dataType==DS.TYPE_XHR){lang.augmentObject(util.DataSource,util.XHRDataSource);return new util.XHRDataSource(oLiveData,oConfigs);}else{if(dataType==DS.TYPE_SCRIPTNODE){lang.augmentObject(util.DataSource,util.ScriptNodeDataSource);return new util.ScriptNodeDataSource(oLiveData,oConfigs);}else{if(dataType==DS.TYPE_JSFUNCTION){lang.augmentObject(util.DataSource,util.FunctionDataSource);return new util.FunctionDataSource(oLiveData,oConfigs);}}}}}if(YAHOO.lang.isString(oLiveData)){lang.augmentObject(util.DataSource,util.XHRDataSource);return new util.XHRDataSource(oLiveData,oConfigs);}else{if(YAHOO.lang.isFunction(oLiveData)){lang.augmentObject(util.DataSource,util.FunctionDataSource);return new util.FunctionDataSource(oLiveData,oConfigs);}else{lang.augmentObject(util.DataSource,util.LocalDataSource);return new util.LocalDataSource(oLiveData,oConfigs);}}};lang.augmentObject(util.DataSource,DS);})();YAHOO.util.Number={format:function(B,E){if(!isFinite(+B)){return"";}B=!isFinite(+B)?0:+B;E=YAHOO.lang.merge(YAHOO.util.Number.format.defaults,(E||{}));var C=B<0,F=Math.abs(B),A=E.decimalPlaces,I=E.thousandsSeparator,H,G,D;if(A<0){H=F-(F%1)+"";D=H.length+A;if(D>0){H=Number("."+H).toFixed(D).slice(2)+new Array(H.length-D+1).join("0");}else{H="0";}}else{H=F<1&&F>=0.5&&!A?"1":F.toFixed(A);}if(F>1000){G=H.split(/\D/);D=G[0].length%3||3;G[0]=G[0].slice(0,D)+G[0].slice(D).replace(/(\d{3})/g,I+"$1");H=G.join(E.decimalSeparator);}H=E.prefix+H+E.suffix;return C?E.negativeFormat.replace(/#/,H):H;}};YAHOO.util.Number.format.defaults={decimalSeparator:".",decimalPlaces:null,thousandsSeparator:"",prefix:"",suffix:"",negativeFormat:"-#"};(function(){var A=function(C,E,D){if(typeof D==="undefined"){D=10;}for(;parseInt(C,10)1;D/=10){C=E.toString()+C;}return C.toString();};var B={formats:{a:function(D,C){return C.a[D.getDay()];},A:function(D,C){return C.A[D.getDay()];},b:function(D,C){return C.b[D.getMonth()];},B:function(D,C){return C.B[D.getMonth()];},C:function(C){return A(parseInt(C.getFullYear()/100,10),0);},d:["getDate","0"],e:["getDate"," "],g:function(C){return A(parseInt(B.formats.G(C)%100,10),0);},G:function(E){var F=E.getFullYear();var D=parseInt(B.formats.V(E),10);var C=parseInt(B.formats.W(E),10);if(C>D){F++;}else{if(C===0&&D>=52){F--;}}return F;},H:["getHours","0"],I:function(D){var C=D.getHours()%12;return A(C===0?12:C,0);},j:function(G){var F=new Date(""+G.getFullYear()+"/1/1 GMT");var D=new Date(""+G.getFullYear()+"/"+(G.getMonth()+1)+"/"+G.getDate()+" GMT");var C=D-F;var E=parseInt(C/60000/60/24,10)+1;return A(E,0,100);},k:["getHours"," "],l:function(D){var C=D.getHours()%12;return A(C===0?12:C," ");},m:function(C){return A(C.getMonth()+1,0);},M:["getMinutes","0"],p:function(D,C){return C.p[D.getHours()>=12?1:0];},P:function(D,C){return C.P[D.getHours()>=12?1:0];},s:function(D,C){return parseInt(D.getTime()/1000,10);},S:["getSeconds","0"],u:function(C){var D=C.getDay();return D===0?7:D;},U:function(F){var C=parseInt(B.formats.j(F),10);var E=6-F.getDay();var D=parseInt((C+E)/7,10);return A(D,0);},V:function(F){var E=parseInt(B.formats.W(F),10);var C=(new Date(""+F.getFullYear()+"/1/1")).getDay();var D=E+(C>4||C<=1?0:1);if(D===53&&(new Date(""+F.getFullYear()+"/12/31")).getDay()<4){D=1;}else{if(D===0){D=B.formats.V(new Date(""+(F.getFullYear()-1)+"/12/31"));}}return A(D,0);},w:"getDay",W:function(F){var C=parseInt(B.formats.j(F),10);var E=7-B.formats.u(F);var D=parseInt((C+E)/7,10);return A(D,0,10);},y:function(C){return A(C.getFullYear()%100,0);},Y:"getFullYear",z:function(E){var D=E.getTimezoneOffset();var C=A(parseInt(Math.abs(D/60),10),0);var F=A(Math.abs(D%60),0);return(D>0?"-":"+")+C+F;},Z:function(C){var D=C.toString().replace(/^.*:\d\d( GMT[+-]\d+)? \(?([A-Za-z ]+)\)?\d*$/,"$2").replace(/[a-z ]/g,"");if(D.length>4){D=B.formats.z(C);}return D;},"%":function(C){return"%";}},aggregates:{c:"locale",D:"%m/%d/%y",F:"%Y-%m-%d",h:"%b",n:"\n",r:"locale",R:"%H:%M",t:"\t",T:"%H:%M:%S",x:"locale",X:"locale"},format:function(G,F,D){F=F||{};if(!(G instanceof Date)){return YAHOO.lang.isValue(G)?G:"";}var H=F.format||"%m/%d/%Y";if(H==="YYYY/MM/DD"){H="%Y/%m/%d";}else{if(H==="DD/MM/YYYY"){H="%d/%m/%Y";}else{if(H==="MM/DD/YYYY"){H="%m/%d/%Y";}}}D=D||"en";if(!(D in YAHOO.util.DateLocale)){if(D.replace(/-[a-zA-Z]+$/,"") in YAHOO.util.DateLocale){D=D.replace(/-[a-zA-Z]+$/,""); -}else{D="en";}}var J=YAHOO.util.DateLocale[D];var C=function(L,K){var M=B.aggregates[K];return(M==="locale"?J[K]:M);};var E=function(L,K){var M=B.formats[K];if(typeof M==="string"){return G[M]();}else{if(typeof M==="function"){return M.call(G,G,J);}else{if(typeof M==="object"&&typeof M[0]==="string"){return A(G[M[0]](),M[1]);}else{return K;}}}};while(H.match(/%[cDFhnrRtTxX]/)){H=H.replace(/%([cDFhnrRtTxX])/g,C);}var I=H.replace(/%([aAbBCdegGHIjklmMpPsSuUVwWyYzZ%])/g,E);C=E=undefined;return I;}};YAHOO.namespace("YAHOO.util");YAHOO.util.Date=B;YAHOO.util.DateLocale={a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a %d %b %Y %T %Z",p:["AM","PM"],P:["am","pm"],r:"%I:%M:%S %p",x:"%d/%m/%y",X:"%T"};YAHOO.util.DateLocale["en"]=YAHOO.lang.merge(YAHOO.util.DateLocale,{});YAHOO.util.DateLocale["en-US"]=YAHOO.lang.merge(YAHOO.util.DateLocale["en"],{c:"%a %d %b %Y %I:%M:%S %p %Z",x:"%m/%d/%Y",X:"%I:%M:%S %p"});YAHOO.util.DateLocale["en-GB"]=YAHOO.lang.merge(YAHOO.util.DateLocale["en"],{r:"%l:%M:%S %P %Z"});YAHOO.util.DateLocale["en-AU"]=YAHOO.lang.merge(YAHOO.util.DateLocale["en"]);})();YAHOO.register("datasource",YAHOO.util.DataSource,{version:"2.8.1",build:"19"});/* -Copyright (c) 2010, Yahoo! Inc. All rights reserved. -Code licensed under the BSD License: -http://developer.yahoo.com/yui/license.html -version: 2.8.1 -*/ -YAHOO.widget.DS_JSArray=YAHOO.util.LocalDataSource;YAHOO.widget.DS_JSFunction=YAHOO.util.FunctionDataSource;YAHOO.widget.DS_XHR=function(B,A,D){var C=new YAHOO.util.XHRDataSource(B,D);C._aDeprecatedSchema=A;return C;};YAHOO.widget.DS_ScriptNode=function(B,A,D){var C=new YAHOO.util.ScriptNodeDataSource(B,D);C._aDeprecatedSchema=A;return C;};YAHOO.widget.DS_XHR.TYPE_JSON=YAHOO.util.DataSourceBase.TYPE_JSON;YAHOO.widget.DS_XHR.TYPE_XML=YAHOO.util.DataSourceBase.TYPE_XML;YAHOO.widget.DS_XHR.TYPE_FLAT=YAHOO.util.DataSourceBase.TYPE_TEXT;YAHOO.widget.AutoComplete=function(G,B,J,C){if(G&&B&&J){if(J&&YAHOO.lang.isFunction(J.sendRequest)){this.dataSource=J;}else{return;}this.key=0;var D=J.responseSchema;if(J._aDeprecatedSchema){var K=J._aDeprecatedSchema;if(YAHOO.lang.isArray(K)){if((J.responseType===YAHOO.util.DataSourceBase.TYPE_JSON)||(J.responseType===YAHOO.util.DataSourceBase.TYPE_UNKNOWN)){D.resultsList=K[0];this.key=K[1];D.fields=(K.length<3)?null:K.slice(1);}else{if(J.responseType===YAHOO.util.DataSourceBase.TYPE_XML){D.resultNode=K[0];this.key=K[1];D.fields=K.slice(1);}else{if(J.responseType===YAHOO.util.DataSourceBase.TYPE_TEXT){D.recordDelim=K[0];D.fieldDelim=K[1];}}}J.responseSchema=D;}}if(YAHOO.util.Dom.inDocument(G)){if(YAHOO.lang.isString(G)){this._sName="instance"+YAHOO.widget.AutoComplete._nIndex+" "+G;this._elTextbox=document.getElementById(G);}else{this._sName=(G.id)?"instance"+YAHOO.widget.AutoComplete._nIndex+" "+G.id:"instance"+YAHOO.widget.AutoComplete._nIndex;this._elTextbox=G;}YAHOO.util.Dom.addClass(this._elTextbox,"yui-ac-input");}else{return;}if(YAHOO.util.Dom.inDocument(B)){if(YAHOO.lang.isString(B)){this._elContainer=document.getElementById(B);}else{this._elContainer=B;}if(this._elContainer.style.display=="none"){}var E=this._elContainer.parentNode;var A=E.tagName.toLowerCase();if(A=="div"){YAHOO.util.Dom.addClass(E,"yui-ac");}else{}}else{return;}if(this.dataSource.dataType===YAHOO.util.DataSourceBase.TYPE_LOCAL){this.applyLocalFilter=true;}if(C&&(C.constructor==Object)){for(var I in C){if(I){this[I]=C[I];}}}this._initContainerEl();this._initProps();this._initListEl();this._initContainerHelperEls();var H=this;var F=this._elTextbox;YAHOO.util.Event.addListener(F,"keyup",H._onTextboxKeyUp,H);YAHOO.util.Event.addListener(F,"keydown",H._onTextboxKeyDown,H);YAHOO.util.Event.addListener(F,"focus",H._onTextboxFocus,H);YAHOO.util.Event.addListener(F,"blur",H._onTextboxBlur,H);YAHOO.util.Event.addListener(B,"mouseover",H._onContainerMouseover,H);YAHOO.util.Event.addListener(B,"mouseout",H._onContainerMouseout,H);YAHOO.util.Event.addListener(B,"click",H._onContainerClick,H);YAHOO.util.Event.addListener(B,"scroll",H._onContainerScroll,H);YAHOO.util.Event.addListener(B,"resize",H._onContainerResize,H);YAHOO.util.Event.addListener(F,"keypress",H._onTextboxKeyPress,H);YAHOO.util.Event.addListener(window,"unload",H._onWindowUnload,H);this.textboxFocusEvent=new YAHOO.util.CustomEvent("textboxFocus",this);this.textboxKeyEvent=new YAHOO.util.CustomEvent("textboxKey",this);this.dataRequestEvent=new YAHOO.util.CustomEvent("dataRequest",this);this.dataReturnEvent=new YAHOO.util.CustomEvent("dataReturn",this);this.dataErrorEvent=new YAHOO.util.CustomEvent("dataError",this);this.containerPopulateEvent=new YAHOO.util.CustomEvent("containerPopulate",this);this.containerExpandEvent=new YAHOO.util.CustomEvent("containerExpand",this);this.typeAheadEvent=new YAHOO.util.CustomEvent("typeAhead",this);this.itemMouseOverEvent=new YAHOO.util.CustomEvent("itemMouseOver",this);this.itemMouseOutEvent=new YAHOO.util.CustomEvent("itemMouseOut",this);this.itemArrowToEvent=new YAHOO.util.CustomEvent("itemArrowTo",this);this.itemArrowFromEvent=new YAHOO.util.CustomEvent("itemArrowFrom",this);this.itemSelectEvent=new YAHOO.util.CustomEvent("itemSelect",this);this.unmatchedItemSelectEvent=new YAHOO.util.CustomEvent("unmatchedItemSelect",this);this.selectionEnforceEvent=new YAHOO.util.CustomEvent("selectionEnforce",this);this.containerCollapseEvent=new YAHOO.util.CustomEvent("containerCollapse",this);this.textboxBlurEvent=new YAHOO.util.CustomEvent("textboxBlur",this);this.textboxChangeEvent=new YAHOO.util.CustomEvent("textboxChange",this);F.setAttribute("autocomplete","off");YAHOO.widget.AutoComplete._nIndex++;}else{}};YAHOO.widget.AutoComplete.prototype.dataSource=null;YAHOO.widget.AutoComplete.prototype.applyLocalFilter=null;YAHOO.widget.AutoComplete.prototype.queryMatchCase=false;YAHOO.widget.AutoComplete.prototype.queryMatchContains=false;YAHOO.widget.AutoComplete.prototype.queryMatchSubset=false;YAHOO.widget.AutoComplete.prototype.minQueryLength=1;YAHOO.widget.AutoComplete.prototype.maxResultsDisplayed=10;YAHOO.widget.AutoComplete.prototype.queryDelay=0.2;YAHOO.widget.AutoComplete.prototype.typeAheadDelay=0.5;YAHOO.widget.AutoComplete.prototype.queryInterval=500;YAHOO.widget.AutoComplete.prototype.highlightClassName="yui-ac-highlight";YAHOO.widget.AutoComplete.prototype.prehighlightClassName=null;YAHOO.widget.AutoComplete.prototype.delimChar=null;YAHOO.widget.AutoComplete.prototype.autoHighlight=true;YAHOO.widget.AutoComplete.prototype.typeAhead=false;YAHOO.widget.AutoComplete.prototype.animHoriz=false;YAHOO.widget.AutoComplete.prototype.animVert=true;YAHOO.widget.AutoComplete.prototype.animSpeed=0.3;YAHOO.widget.AutoComplete.prototype.forceSelection=false;YAHOO.widget.AutoComplete.prototype.allowBrowserAutocomplete=true;YAHOO.widget.AutoComplete.prototype.alwaysShowContainer=false;YAHOO.widget.AutoComplete.prototype.useIFrame=false;YAHOO.widget.AutoComplete.prototype.useShadow=false;YAHOO.widget.AutoComplete.prototype.suppressInputUpdate=false;YAHOO.widget.AutoComplete.prototype.resultTypeList=true;YAHOO.widget.AutoComplete.prototype.queryQuestionMark=true;YAHOO.widget.AutoComplete.prototype.autoSnapContainer=true;YAHOO.widget.AutoComplete.prototype.toString=function(){return"AutoComplete "+this._sName;};YAHOO.widget.AutoComplete.prototype.getInputEl=function(){return this._elTextbox;};YAHOO.widget.AutoComplete.prototype.getContainerEl=function(){return this._elContainer; -};YAHOO.widget.AutoComplete.prototype.isFocused=function(){return this._bFocused;};YAHOO.widget.AutoComplete.prototype.isContainerOpen=function(){return this._bContainerOpen;};YAHOO.widget.AutoComplete.prototype.getListEl=function(){return this._elList;};YAHOO.widget.AutoComplete.prototype.getListItemMatch=function(A){if(A._sResultMatch){return A._sResultMatch;}else{return null;}};YAHOO.widget.AutoComplete.prototype.getListItemData=function(A){if(A._oResultData){return A._oResultData;}else{return null;}};YAHOO.widget.AutoComplete.prototype.getListItemIndex=function(A){if(YAHOO.lang.isNumber(A._nItemIndex)){return A._nItemIndex;}else{return null;}};YAHOO.widget.AutoComplete.prototype.setHeader=function(B){if(this._elHeader){var A=this._elHeader;if(B){A.innerHTML=B;A.style.display="";}else{A.innerHTML="";A.style.display="none";}}};YAHOO.widget.AutoComplete.prototype.setFooter=function(B){if(this._elFooter){var A=this._elFooter;if(B){A.innerHTML=B;A.style.display="";}else{A.innerHTML="";A.style.display="none";}}};YAHOO.widget.AutoComplete.prototype.setBody=function(A){if(this._elBody){var B=this._elBody;YAHOO.util.Event.purgeElement(B,true);if(A){B.innerHTML=A;B.style.display="";}else{B.innerHTML="";B.style.display="none";}this._elList=null;}};YAHOO.widget.AutoComplete.prototype.generateRequest=function(B){var A=this.dataSource.dataType;if(A===YAHOO.util.DataSourceBase.TYPE_XHR){if(!this.dataSource.connMethodPost){B=(this.queryQuestionMark?"?":"")+(this.dataSource.scriptQueryParam||"query")+"="+B+(this.dataSource.scriptQueryAppend?("&"+this.dataSource.scriptQueryAppend):"");}else{B=(this.dataSource.scriptQueryParam||"query")+"="+B+(this.dataSource.scriptQueryAppend?("&"+this.dataSource.scriptQueryAppend):"");}}else{if(A===YAHOO.util.DataSourceBase.TYPE_SCRIPTNODE){B="&"+(this.dataSource.scriptQueryParam||"query")+"="+B+(this.dataSource.scriptQueryAppend?("&"+this.dataSource.scriptQueryAppend):"");}}return B;};YAHOO.widget.AutoComplete.prototype.sendQuery=function(B){this._bFocused=true;var A=(this.delimChar)?this._elTextbox.value+B:B;this._sendQuery(A);};YAHOO.widget.AutoComplete.prototype.snapContainer=function(){var A=this._elTextbox,B=YAHOO.util.Dom.getXY(A);B[1]+=YAHOO.util.Dom.get(A).offsetHeight+2;YAHOO.util.Dom.setXY(this._elContainer,B);};YAHOO.widget.AutoComplete.prototype.expandContainer=function(){this._toggleContainer(true);};YAHOO.widget.AutoComplete.prototype.collapseContainer=function(){this._toggleContainer(false);};YAHOO.widget.AutoComplete.prototype.clearList=function(){var B=this._elList.childNodes,A=B.length-1;for(;A>-1;A--){B[A].style.display="none";}};YAHOO.widget.AutoComplete.prototype.getSubsetMatches=function(E){var D,C,A;for(var B=E.length;B>=this.minQueryLength;B--){A=this.generateRequest(E.substr(0,B));this.dataRequestEvent.fire(this,D,A);C=this.dataSource.getCachedResponse(A);if(C){return this.filterResults.apply(this.dataSource,[E,C,C,{scope:this}]);}}return null;};YAHOO.widget.AutoComplete.prototype.preparseRawResponse=function(C,B,A){var D=((this.responseStripAfter!=="")&&(B.indexOf))?B.indexOf(this.responseStripAfter):-1;if(D!=-1){B=B.substring(0,D);}return B;};YAHOO.widget.AutoComplete.prototype.filterResults=function(K,M,Q,L){if(L&&L.argument&&L.argument.query){K=L.argument.query;}if(K&&K!==""){Q=YAHOO.widget.AutoComplete._cloneObject(Q);var I=L.scope,P=this,C=Q.results,N=[],B=I.maxResultsDisplayed,J=(P.queryMatchCase||I.queryMatchCase),A=(P.queryMatchContains||I.queryMatchContains);for(var D=0,H=C.length;D-1))){N.push(F);}}if(H>B&&N.length===B){break;}}Q.results=N;}else{}return Q;};YAHOO.widget.AutoComplete.prototype.handleResponse=function(C,A,B){if((this instanceof YAHOO.widget.AutoComplete)&&this._sName){this._populateList(C,A,B);}};YAHOO.widget.AutoComplete.prototype.doBeforeLoadData=function(C,A,B){return true;};YAHOO.widget.AutoComplete.prototype.formatResult=function(B,D,A){var C=(A)?A:"";return C;};YAHOO.widget.AutoComplete.prototype.doBeforeExpandContainer=function(D,A,C,B){return true;};YAHOO.widget.AutoComplete.prototype.destroy=function(){var B=this.toString();var A=this._elTextbox;var D=this._elContainer;this.textboxFocusEvent.unsubscribeAll();this.textboxKeyEvent.unsubscribeAll();this.dataRequestEvent.unsubscribeAll();this.dataReturnEvent.unsubscribeAll();this.dataErrorEvent.unsubscribeAll();this.containerPopulateEvent.unsubscribeAll();this.containerExpandEvent.unsubscribeAll();this.typeAheadEvent.unsubscribeAll();this.itemMouseOverEvent.unsubscribeAll();this.itemMouseOutEvent.unsubscribeAll();this.itemArrowToEvent.unsubscribeAll();this.itemArrowFromEvent.unsubscribeAll();this.itemSelectEvent.unsubscribeAll();this.unmatchedItemSelectEvent.unsubscribeAll();this.selectionEnforceEvent.unsubscribeAll();this.containerCollapseEvent.unsubscribeAll();this.textboxBlurEvent.unsubscribeAll();this.textboxChangeEvent.unsubscribeAll();YAHOO.util.Event.purgeElement(A,true);YAHOO.util.Event.purgeElement(D,true);D.innerHTML="";for(var C in this){if(YAHOO.lang.hasOwnProperty(this,C)){this[C]=null;}}};YAHOO.widget.AutoComplete.prototype.textboxFocusEvent=null;YAHOO.widget.AutoComplete.prototype.textboxKeyEvent=null;YAHOO.widget.AutoComplete.prototype.dataRequestEvent=null;YAHOO.widget.AutoComplete.prototype.dataReturnEvent=null;YAHOO.widget.AutoComplete.prototype.dataErrorEvent=null;YAHOO.widget.AutoComplete.prototype.containerPopulateEvent=null;YAHOO.widget.AutoComplete.prototype.containerExpandEvent=null;YAHOO.widget.AutoComplete.prototype.typeAheadEvent=null;YAHOO.widget.AutoComplete.prototype.itemMouseOverEvent=null;YAHOO.widget.AutoComplete.prototype.itemMouseOutEvent=null; -YAHOO.widget.AutoComplete.prototype.itemArrowToEvent=null;YAHOO.widget.AutoComplete.prototype.itemArrowFromEvent=null;YAHOO.widget.AutoComplete.prototype.itemSelectEvent=null;YAHOO.widget.AutoComplete.prototype.unmatchedItemSelectEvent=null;YAHOO.widget.AutoComplete.prototype.selectionEnforceEvent=null;YAHOO.widget.AutoComplete.prototype.containerCollapseEvent=null;YAHOO.widget.AutoComplete.prototype.textboxBlurEvent=null;YAHOO.widget.AutoComplete.prototype.textboxChangeEvent=null;YAHOO.widget.AutoComplete._nIndex=0;YAHOO.widget.AutoComplete.prototype._sName=null;YAHOO.widget.AutoComplete.prototype._elTextbox=null;YAHOO.widget.AutoComplete.prototype._elContainer=null;YAHOO.widget.AutoComplete.prototype._elContent=null;YAHOO.widget.AutoComplete.prototype._elHeader=null;YAHOO.widget.AutoComplete.prototype._elBody=null;YAHOO.widget.AutoComplete.prototype._elFooter=null;YAHOO.widget.AutoComplete.prototype._elShadow=null;YAHOO.widget.AutoComplete.prototype._elIFrame=null;YAHOO.widget.AutoComplete.prototype._bFocused=false;YAHOO.widget.AutoComplete.prototype._oAnim=null;YAHOO.widget.AutoComplete.prototype._bContainerOpen=false;YAHOO.widget.AutoComplete.prototype._bOverContainer=false;YAHOO.widget.AutoComplete.prototype._elList=null;YAHOO.widget.AutoComplete.prototype._nDisplayedItems=0;YAHOO.widget.AutoComplete.prototype._sCurQuery=null;YAHOO.widget.AutoComplete.prototype._sPastSelections="";YAHOO.widget.AutoComplete.prototype._sInitInputValue=null;YAHOO.widget.AutoComplete.prototype._elCurListItem=null;YAHOO.widget.AutoComplete.prototype._elCurPrehighlightItem=null;YAHOO.widget.AutoComplete.prototype._bItemSelected=false;YAHOO.widget.AutoComplete.prototype._nKeyCode=null;YAHOO.widget.AutoComplete.prototype._nDelayID=-1;YAHOO.widget.AutoComplete.prototype._nTypeAheadDelayID=-1;YAHOO.widget.AutoComplete.prototype._iFrameSrc="javascript:false;";YAHOO.widget.AutoComplete.prototype._queryInterval=null;YAHOO.widget.AutoComplete.prototype._sLastTextboxValue=null;YAHOO.widget.AutoComplete.prototype._initProps=function(){var B=this.minQueryLength;if(!YAHOO.lang.isNumber(B)){this.minQueryLength=1;}var E=this.maxResultsDisplayed;if(!YAHOO.lang.isNumber(E)||(E<1)){this.maxResultsDisplayed=10;}var F=this.queryDelay;if(!YAHOO.lang.isNumber(F)||(F<0)){this.queryDelay=0.2;}var C=this.typeAheadDelay;if(!YAHOO.lang.isNumber(C)||(C<0)){this.typeAheadDelay=0.2;}var A=this.delimChar;if(YAHOO.lang.isString(A)&&(A.length>0)){this.delimChar=[A];}else{if(!YAHOO.lang.isArray(A)){this.delimChar=null;}}var D=this.animSpeed;if((this.animHoriz||this.animVert)&&YAHOO.util.Anim){if(!YAHOO.lang.isNumber(D)||(D<0)){this.animSpeed=0.3;}if(!this._oAnim){this._oAnim=new YAHOO.util.Anim(this._elContent,{},this.animSpeed);}else{this._oAnim.duration=this.animSpeed;}}if(this.forceSelection&&A){}};YAHOO.widget.AutoComplete.prototype._initContainerHelperEls=function(){if(this.useShadow&&!this._elShadow){var A=document.createElement("div");A.className="yui-ac-shadow";A.style.width=0;A.style.height=0;this._elShadow=this._elContainer.appendChild(A);}if(this.useIFrame&&!this._elIFrame){var B=document.createElement("iframe");B.src=this._iFrameSrc;B.frameBorder=0;B.scrolling="no";B.style.position="absolute";B.style.width=0;B.style.height=0;B.style.padding=0;B.tabIndex=-1;B.role="presentation";B.title="Presentational iframe shim";this._elIFrame=this._elContainer.appendChild(B);}};YAHOO.widget.AutoComplete.prototype._initContainerEl=function(){YAHOO.util.Dom.addClass(this._elContainer,"yui-ac-container");if(!this._elContent){var C=document.createElement("div");C.className="yui-ac-content";C.style.display="none";this._elContent=this._elContainer.appendChild(C);var B=document.createElement("div");B.className="yui-ac-hd";B.style.display="none";this._elHeader=this._elContent.appendChild(B);var D=document.createElement("div");D.className="yui-ac-bd";this._elBody=this._elContent.appendChild(D);var A=document.createElement("div");A.className="yui-ac-ft";A.style.display="none";this._elFooter=this._elContent.appendChild(A);}else{}};YAHOO.widget.AutoComplete.prototype._initListEl=function(){var C=this.maxResultsDisplayed,A=this._elList||document.createElement("ul"),B;while(A.childNodes.length=18&&A<=20)||(A==27)||(A>=33&&A<=35)||(A>=36&&A<=40)||(A>=44&&A<=45)||(A==229)){return true;}return false;};YAHOO.widget.AutoComplete.prototype._sendQuery=function(D){if(this.minQueryLength<0){this._toggleContainer(false);return;}if(this.delimChar){var A=this._extractQuery(D);D=A.query;this._sPastSelections=A.previous;}if((D&&(D.length0)){if(this._nDelayID!=-1){clearTimeout(this._nDelayID);}this._toggleContainer(false);return;}D=encodeURIComponent(D);this._nDelayID=-1;if(this.dataSource.queryMatchSubset||this.queryMatchSubset){var C=this.getSubsetMatches(D);if(C){this.handleResponse(D,C,{query:D});return; -}}if(this.dataSource.responseStripAfter){this.dataSource.doBeforeParseData=this.preparseRawResponse;}if(this.applyLocalFilter){this.dataSource.doBeforeCallback=this.filterResults;}var B=this.generateRequest(D);this.dataRequestEvent.fire(this,D,B);this.dataSource.sendRequest(B,{success:this.handleResponse,failure:this.handleResponse,scope:this,argument:{query:D}});};YAHOO.widget.AutoComplete.prototype._populateListItem=function(B,A,C){B.innerHTML=this.formatResult(A,C,B._sResultMatch);};YAHOO.widget.AutoComplete.prototype._populateList=function(K,F,C){if(this._nTypeAheadDelayID!=-1){clearTimeout(this._nTypeAheadDelayID);}K=(C&&C.query)?C.query:K;var H=this.doBeforeLoadData(K,F,C);if(H&&!F.error){this.dataReturnEvent.fire(this,K,F.results);if(this._bFocused){var M=decodeURIComponent(K);this._sCurQuery=M;this._bItemSelected=false;var R=F.results,A=Math.min(R.length,this.maxResultsDisplayed),J=(this.dataSource.responseSchema.fields)?(this.dataSource.responseSchema.fields[0].key||this.dataSource.responseSchema.fields[0]):0;if(A>0){if(!this._elList||(this._elList.childNodes.length=0;Q--){var P=I[Q],E=R[Q];if(this.resultTypeList){var B=[];B[0]=(YAHOO.lang.isString(E))?E:E[J]||E[this.key];var L=this.dataSource.responseSchema.fields;if(YAHOO.lang.isArray(L)&&(L.length>1)){for(var N=1,S=L.length;N=A;O--){G=I[O];G.style.display="none";}}this._nDisplayedItems=A;this.containerPopulateEvent.fire(this,K,R);if(this.autoHighlight){var D=this._elList.firstChild;this._toggleHighlight(D,"to");this.itemArrowToEvent.fire(this,D);this._typeAhead(D,K);}else{this._toggleHighlight(this._elCurListItem,"from");}H=this._doBeforeExpandContainer(this._elTextbox,this._elContainer,K,R);this._toggleContainer(H);}else{this._toggleContainer(false);}return;}}else{this.dataErrorEvent.fire(this,K,F);}};YAHOO.widget.AutoComplete.prototype._doBeforeExpandContainer=function(D,A,C,B){if(this.autoSnapContainer){this.snapContainer();}return this.doBeforeExpandContainer(D,A,C,B);};YAHOO.widget.AutoComplete.prototype._clearSelection=function(){var A=(this.delimChar)?this._extractQuery(this._elTextbox.value):{previous:"",query:this._elTextbox.value};this._elTextbox.value=A.previous;this.selectionEnforceEvent.fire(this,A.query);};YAHOO.widget.AutoComplete.prototype._textMatchesOption=function(){var A=null;for(var B=0;B=0;B--){G=H.lastIndexOf(C[B]);if(G>F){F=G;}}if(C[B]==" "){for(var A=C.length-1;A>=0;A--){if(H[F-1]==C[A]){F--;break;}}}if(F>-1){E=F+1;while(H.charAt(E)==" "){E+=1;}D=H.substring(0,E);H=H.substr(E);}else{D="";}return{previous:D,query:H};};YAHOO.widget.AutoComplete.prototype._toggleContainerHelpers=function(D){var E=this._elContent.offsetWidth+"px";var B=this._elContent.offsetHeight+"px";if(this.useIFrame&&this._elIFrame){var C=this._elIFrame;if(D){C.style.width=E;C.style.height=B;C.style.padding="";}else{C.style.width=0;C.style.height=0;C.style.padding=0;}}if(this.useShadow&&this._elShadow){var A=this._elShadow;if(D){A.style.width=E;A.style.height=B;}else{A.style.width=0;A.style.height=0;}}};YAHOO.widget.AutoComplete.prototype._toggleContainer=function(I){var D=this._elContainer;if(this.alwaysShowContainer&&this._bContainerOpen){return;}if(!I){this._toggleHighlight(this._elCurListItem,"from");this._nDisplayedItems=0;this._sCurQuery=null;if(this._elContent.style.display=="none"){return;}}var A=this._oAnim;if(A&&A.getEl()&&(this.animHoriz||this.animVert)){if(A.isAnimated()){A.stop(true);}var G=this._elContent.cloneNode(true);D.appendChild(G);G.style.top="-9000px";G.style.width="";G.style.height="";G.style.display="";var F=G.offsetWidth;var C=G.offsetHeight;var B=(this.animHoriz)?0:F;var E=(this.animVert)?0:C;A.attributes=(I)?{width:{to:F},height:{to:C}}:{width:{to:B},height:{to:E}};if(I&&!this._bContainerOpen){this._elContent.style.width=B+"px";this._elContent.style.height=E+"px";}else{this._elContent.style.width=F+"px";this._elContent.style.height=C+"px";}D.removeChild(G);G=null;var H=this;var J=function(){A.onComplete.unsubscribeAll();if(I){H._toggleContainerHelpers(true);H._bContainerOpen=I;H.containerExpandEvent.fire(H);}else{H._elContent.style.display="none";H._bContainerOpen=I;H.containerCollapseEvent.fire(H);}};this._toggleContainerHelpers(false);this._elContent.style.display="";A.onComplete.subscribe(J);A.animate();}else{if(I){this._elContent.style.display="";this._toggleContainerHelpers(true);this._bContainerOpen=I;this.containerExpandEvent.fire(this);}else{this._toggleContainerHelpers(false);this._elContent.style.display="none";this._bContainerOpen=I;this.containerCollapseEvent.fire(this);}}};YAHOO.widget.AutoComplete.prototype._toggleHighlight=function(A,C){if(A){var B=this.highlightClassName; -if(this._elCurListItem){YAHOO.util.Dom.removeClass(this._elCurListItem,B);this._elCurListItem=null;}if((C=="to")&&B){YAHOO.util.Dom.addClass(A,B);this._elCurListItem=A;}}};YAHOO.widget.AutoComplete.prototype._togglePrehighlight=function(B,C){var A=this.prehighlightClassName;if(this._elCurPrehighlightItem){YAHOO.util.Dom.removeClass(this._elCurPrehighlightItem,A);}if(B==this._elCurListItem){return;}if((C=="mouseover")&&A){YAHOO.util.Dom.addClass(B,A);this._elCurPrehighlightItem=B;}else{YAHOO.util.Dom.removeClass(B,A);}};YAHOO.widget.AutoComplete.prototype._updateValue=function(C){if(!this.suppressInputUpdate){var F=this._elTextbox;var E=(this.delimChar)?(this.delimChar[0]||this.delimChar):null;var B=C._sResultMatch;var D="";if(E){D=this._sPastSelections;D+=B+E;if(E!=" "){D+=" ";}}else{D=B;}F.value=D;if(F.type=="textarea"){F.scrollTop=F.scrollHeight;}var A=F.value.length;this._selectText(F,A,A);this._elCurListItem=C;}};YAHOO.widget.AutoComplete.prototype._selectItem=function(A){this._bItemSelected=true;this._updateValue(A);this._sPastSelections=this._elTextbox.value;this._clearInterval();this.itemSelectEvent.fire(this,A,A._oResultData);this._toggleContainer(false);};YAHOO.widget.AutoComplete.prototype._jumpSelection=function(){if(this._elCurListItem){this._selectItem(this._elCurListItem);}else{this._toggleContainer(false);}};YAHOO.widget.AutoComplete.prototype._moveSelection=function(G){if(this._bContainerOpen){var H=this._elCurListItem,D=-1;if(H){D=H._nItemIndex;}var E=(G==40)?(D+1):(D-1);if(E<-2||E>=this._nDisplayedItems){return;}if(H){this._toggleHighlight(H,"from");this.itemArrowFromEvent.fire(this,H);}if(E==-1){if(this.delimChar){this._elTextbox.value=this._sPastSelections+this._sCurQuery;}else{this._elTextbox.value=this._sCurQuery;}return;}if(E==-2){this._toggleContainer(false);return;}var F=this._elList.childNodes[E],B=this._elContent,C=YAHOO.util.Dom.getStyle(B,"overflow"),I=YAHOO.util.Dom.getStyle(B,"overflowY"),A=((C=="auto")||(C=="scroll")||(I=="auto")||(I=="scroll"));if(A&&(E>-1)&&(E(B.scrollTop+B.offsetHeight)){B.scrollTop=(F.offsetTop+F.offsetHeight)-B.offsetHeight;}else{if((F.offsetTop+F.offsetHeight)(B.scrollTop+B.offsetHeight)){this._elContent.scrollTop=(F.offsetTop+F.offsetHeight)-B.offsetHeight;}}}}this._toggleHighlight(F,"to");this.itemArrowToEvent.fire(this,F);if(this.typeAhead){this._updateValue(F);}}};YAHOO.widget.AutoComplete.prototype._onContainerMouseover=function(A,C){var D=YAHOO.util.Event.getTarget(A);var B=D.nodeName.toLowerCase();while(D&&(B!="table")){switch(B){case"body":return;case"li":if(C.prehighlightClassName){C._togglePrehighlight(D,"mouseover");}else{C._toggleHighlight(D,"to");}C.itemMouseOverEvent.fire(C,D);break;case"div":if(YAHOO.util.Dom.hasClass(D,"yui-ac-container")){C._bOverContainer=true;return;}break;default:break;}D=D.parentNode;if(D){B=D.nodeName.toLowerCase();}}};YAHOO.widget.AutoComplete.prototype._onContainerMouseout=function(A,C){var D=YAHOO.util.Event.getTarget(A);var B=D.nodeName.toLowerCase();while(D&&(B!="table")){switch(B){case"body":return;case"li":if(C.prehighlightClassName){C._togglePrehighlight(D,"mouseout");}else{C._toggleHighlight(D,"from");}C.itemMouseOutEvent.fire(C,D);break;case"ul":C._toggleHighlight(C._elCurListItem,"to");break;case"div":if(YAHOO.util.Dom.hasClass(D,"yui-ac-container")){C._bOverContainer=false;return;}break;default:break;}D=D.parentNode;if(D){B=D.nodeName.toLowerCase();}}};YAHOO.widget.AutoComplete.prototype._onContainerClick=function(A,C){var D=YAHOO.util.Event.getTarget(A);var B=D.nodeName.toLowerCase();while(D&&(B!="table")){switch(B){case"body":return;case"li":C._toggleHighlight(D,"to");C._selectItem(D);return;default:break;}D=D.parentNode;if(D){B=D.nodeName.toLowerCase();}}};YAHOO.widget.AutoComplete.prototype._onContainerScroll=function(A,B){B._focus();};YAHOO.widget.AutoComplete.prototype._onContainerResize=function(A,B){B._toggleContainerHelpers(B._bContainerOpen);};YAHOO.widget.AutoComplete.prototype._onTextboxKeyDown=function(A,B){var C=A.keyCode;if(B._nTypeAheadDelayID!=-1){clearTimeout(B._nTypeAheadDelayID);}switch(C){case 9:if(!YAHOO.env.ua.opera&&(navigator.userAgent.toLowerCase().indexOf("mac")==-1)||(YAHOO.env.ua.webkit>420)){if(B._elCurListItem){if(B.delimChar&&(B._nKeyCode!=C)){if(B._bContainerOpen){YAHOO.util.Event.stopEvent(A);}}B._selectItem(B._elCurListItem);}else{B._toggleContainer(false);}}break;case 13:if(!YAHOO.env.ua.opera&&(navigator.userAgent.toLowerCase().indexOf("mac")==-1)||(YAHOO.env.ua.webkit>420)){if(B._elCurListItem){if(B._nKeyCode!=C){if(B._bContainerOpen){YAHOO.util.Event.stopEvent(A);}}B._selectItem(B._elCurListItem);}else{B._toggleContainer(false);}}break;case 27:B._toggleContainer(false);return;case 39:B._jumpSelection();break;case 38:if(B._bContainerOpen){YAHOO.util.Event.stopEvent(A);B._moveSelection(C);}break;case 40:if(B._bContainerOpen){YAHOO.util.Event.stopEvent(A);B._moveSelection(C);}break;default:B._bItemSelected=false;B._toggleHighlight(B._elCurListItem,"from");B.textboxKeyEvent.fire(B,C);break;}if(C===18){B._enableIntervalDetection();}B._nKeyCode=C;};YAHOO.widget.AutoComplete.prototype._onTextboxKeyPress=function(A,B){var C=A.keyCode;if(YAHOO.env.ua.opera||(navigator.userAgent.toLowerCase().indexOf("mac")!=-1)&&(YAHOO.env.ua.webkit<420)){switch(C){case 9:if(B._bContainerOpen){if(B.delimChar){YAHOO.util.Event.stopEvent(A);}if(B._elCurListItem){B._selectItem(B._elCurListItem);}else{B._toggleContainer(false);}}break;case 13:if(B._bContainerOpen){YAHOO.util.Event.stopEvent(A);if(B._elCurListItem){B._selectItem(B._elCurListItem);}else{B._toggleContainer(false);}}break;default:break;}}else{if(C==229){B._enableIntervalDetection();}}};YAHOO.widget.AutoComplete.prototype._onTextboxKeyUp=function(A,C){var B=this.value;C._initProps();var D=A.keyCode;if(C._isIgnoreKey(D)){return; -}if(C._nDelayID!=-1){clearTimeout(C._nDelayID);}C._nDelayID=setTimeout(function(){C._sendQuery(B);},(C.queryDelay*1000));};YAHOO.widget.AutoComplete.prototype._onTextboxFocus=function(A,B){if(!B._bFocused){B._elTextbox.setAttribute("autocomplete","off");B._bFocused=true;B._sInitInputValue=B._elTextbox.value;B.textboxFocusEvent.fire(B);}};YAHOO.widget.AutoComplete.prototype._onTextboxBlur=function(A,C){if(!C._bOverContainer||(C._nKeyCode==9)){if(!C._bItemSelected){var B=C._textMatchesOption();if(!C._bContainerOpen||(C._bContainerOpen&&(B===null))){if(C.forceSelection){C._clearSelection();}else{C.unmatchedItemSelectEvent.fire(C,C._sCurQuery);}}else{if(C.forceSelection){C._selectItem(B);}}}C._clearInterval();C._bFocused=false;if(C._sInitInputValue!==C._elTextbox.value){C.textboxChangeEvent.fire(C);}C.textboxBlurEvent.fire(C);C._toggleContainer(false);}else{C._focus();}};YAHOO.widget.AutoComplete.prototype._onWindowUnload=function(A,B){if(B&&B._elTextbox&&B.allowBrowserAutocomplete){B._elTextbox.setAttribute("autocomplete","on");}};YAHOO.widget.AutoComplete.prototype.doBeforeSendQuery=function(A){return this.generateRequest(A);};YAHOO.widget.AutoComplete.prototype.getListItems=function(){var C=[],B=this._elList.childNodes;for(var A=B.length-1;A>=0;A--){C[A]=B[A];}return C;};YAHOO.widget.AutoComplete._cloneObject=function(D){if(!YAHOO.lang.isValue(D)){return D;}var F={};if(YAHOO.lang.isFunction(D)){F=D;}else{if(YAHOO.lang.isArray(D)){var E=[];for(var C=0,B=D.length;C0){G=F-1;do{D=E.subscribers[G];if(D&&D.obj==I&&D.fn==H){return true;}}while(G--);}return false;};YAHOO.lang.augmentProto(A,YAHOO.util.EventProvider);}());(function(){YAHOO.widget.Module=function(R,Q){if(R){this.init(R,Q);}else{}};var F=YAHOO.util.Dom,D=YAHOO.util.Config,N=YAHOO.util.Event,M=YAHOO.util.CustomEvent,G=YAHOO.widget.Module,I=YAHOO.env.ua,H,P,O,E,A={"BEFORE_INIT":"beforeInit","INIT":"init","APPEND":"append","BEFORE_RENDER":"beforeRender","RENDER":"render","CHANGE_HEADER":"changeHeader","CHANGE_BODY":"changeBody","CHANGE_FOOTER":"changeFooter","CHANGE_CONTENT":"changeContent","DESTROY":"destroy","BEFORE_SHOW":"beforeShow","SHOW":"show","BEFORE_HIDE":"beforeHide","HIDE":"hide"},J={"VISIBLE":{key:"visible",value:true,validator:YAHOO.lang.isBoolean},"EFFECT":{key:"effect",suppressEvent:true,supercedes:["visible"]},"MONITOR_RESIZE":{key:"monitorresize",value:true},"APPEND_TO_DOCUMENT_BODY":{key:"appendtodocumentbody",value:false}};G.IMG_ROOT=null;G.IMG_ROOT_SSL=null;G.CSS_MODULE="yui-module";G.CSS_HEADER="hd";G.CSS_BODY="bd";G.CSS_FOOTER="ft";G.RESIZE_MONITOR_SECURE_URL="javascript:false;";G.RESIZE_MONITOR_BUFFER=1;G.textResizeEvent=new M("textResize");G.forceDocumentRedraw=function(){var Q=document.documentElement;if(Q){Q.className+=" ";Q.className=YAHOO.lang.trim(Q.className);}};function L(){if(!H){H=document.createElement("div");H.innerHTML=('
'+'
');P=H.firstChild;O=P.nextSibling;E=O.nextSibling;}return H;}function K(){if(!P){L();}return(P.cloneNode(false));}function B(){if(!O){L();}return(O.cloneNode(false));}function C(){if(!E){L();}return(E.cloneNode(false));}G.prototype={constructor:G,element:null,header:null,body:null,footer:null,id:null,imageRoot:G.IMG_ROOT,initEvents:function(){var Q=M.LIST; -this.beforeInitEvent=this.createEvent(A.BEFORE_INIT);this.beforeInitEvent.signature=Q;this.initEvent=this.createEvent(A.INIT);this.initEvent.signature=Q;this.appendEvent=this.createEvent(A.APPEND);this.appendEvent.signature=Q;this.beforeRenderEvent=this.createEvent(A.BEFORE_RENDER);this.beforeRenderEvent.signature=Q;this.renderEvent=this.createEvent(A.RENDER);this.renderEvent.signature=Q;this.changeHeaderEvent=this.createEvent(A.CHANGE_HEADER);this.changeHeaderEvent.signature=Q;this.changeBodyEvent=this.createEvent(A.CHANGE_BODY);this.changeBodyEvent.signature=Q;this.changeFooterEvent=this.createEvent(A.CHANGE_FOOTER);this.changeFooterEvent.signature=Q;this.changeContentEvent=this.createEvent(A.CHANGE_CONTENT);this.changeContentEvent.signature=Q;this.destroyEvent=this.createEvent(A.DESTROY);this.destroyEvent.signature=Q;this.beforeShowEvent=this.createEvent(A.BEFORE_SHOW);this.beforeShowEvent.signature=Q;this.showEvent=this.createEvent(A.SHOW);this.showEvent.signature=Q;this.beforeHideEvent=this.createEvent(A.BEFORE_HIDE);this.beforeHideEvent.signature=Q;this.hideEvent=this.createEvent(A.HIDE);this.hideEvent.signature=Q;},platform:function(){var Q=navigator.userAgent.toLowerCase();if(Q.indexOf("windows")!=-1||Q.indexOf("win32")!=-1){return"windows";}else{if(Q.indexOf("macintosh")!=-1){return"mac";}else{return false;}}}(),browser:function(){var Q=navigator.userAgent.toLowerCase();if(Q.indexOf("opera")!=-1){return"opera";}else{if(Q.indexOf("msie 7")!=-1){return"ie7";}else{if(Q.indexOf("msie")!=-1){return"ie";}else{if(Q.indexOf("safari")!=-1){return"safari";}else{if(Q.indexOf("gecko")!=-1){return"gecko";}else{return false;}}}}}}(),isSecure:function(){if(window.location.href.toLowerCase().indexOf("https")===0){return true;}else{return false;}}(),initDefaultConfig:function(){this.cfg.addProperty(J.VISIBLE.key,{handler:this.configVisible,value:J.VISIBLE.value,validator:J.VISIBLE.validator});this.cfg.addProperty(J.EFFECT.key,{suppressEvent:J.EFFECT.suppressEvent,supercedes:J.EFFECT.supercedes});this.cfg.addProperty(J.MONITOR_RESIZE.key,{handler:this.configMonitorResize,value:J.MONITOR_RESIZE.value});this.cfg.addProperty(J.APPEND_TO_DOCUMENT_BODY.key,{value:J.APPEND_TO_DOCUMENT_BODY.value});},init:function(V,U){var S,W;this.initEvents();this.beforeInitEvent.fire(G);this.cfg=new D(this);if(this.isSecure){this.imageRoot=G.IMG_ROOT_SSL;}if(typeof V=="string"){S=V;V=document.getElementById(V);if(!V){V=(L()).cloneNode(false);V.id=S;}}this.id=F.generateId(V);this.element=V;W=this.element.firstChild;if(W){var R=false,Q=false,T=false;do{if(1==W.nodeType){if(!R&&F.hasClass(W,G.CSS_HEADER)){this.header=W;R=true;}else{if(!Q&&F.hasClass(W,G.CSS_BODY)){this.body=W;Q=true;}else{if(!T&&F.hasClass(W,G.CSS_FOOTER)){this.footer=W;T=true;}}}}}while((W=W.nextSibling));}this.initDefaultConfig();F.addClass(this.element,G.CSS_MODULE);if(U){this.cfg.applyConfig(U,true);}if(!D.alreadySubscribed(this.renderEvent,this.cfg.fireQueue,this.cfg)){this.renderEvent.subscribe(this.cfg.fireQueue,this.cfg,true);}this.initEvent.fire(G);},initResizeMonitor:function(){var R=(I.gecko&&this.platform=="windows");if(R){var Q=this;setTimeout(function(){Q._initResizeMonitor();},0);}else{this._initResizeMonitor();}},_initResizeMonitor:function(){var Q,S,U;function W(){G.textResizeEvent.fire();}if(!I.opera){S=F.get("_yuiResizeMonitor");var V=this._supportsCWResize();if(!S){S=document.createElement("iframe");if(this.isSecure&&G.RESIZE_MONITOR_SECURE_URL&&I.ie){S.src=G.RESIZE_MONITOR_SECURE_URL;}if(!V){U=["
${c.users_log.pager('$link_previous ~2~ $link_next', onclick="""YAHOO.util.Connect.asyncRequest('GET','$partial_url',{ -success:function(o){YAHOO.util.Dom.get(data_div).innerHTML=o.responseText; -YAHOO.util.Event.addListener(YAHOO.util.Dom.getElementsByClassName('pager_link'),"click",function(){ - YAHOO.util.Dom.setStyle(data_div,'opacity','0.3');}); -YAHOO.util.Dom.setStyle(data_div,'opacity','1');}},null); return false;""")} +success:function(o){ + YUD.get(data_div).innerHTML=o.responseText; + YUE.on(YUD.getElementsByClassName('pager_link'),"click",function(){ + YUD.setStyle(data_div,'opacity','0.3'); + }); + YUE.on(YUD.getElementsByClassName('show_more'),'click',function(e){ + var el = e.target; + YUD.setStyle(YUD.get(el.id.substring(1)),'display',''); + YUD.setStyle(el.parentNode,'display','none'); + }); + YUD.setStyle(data_div,'opacity','1');} + +},null); return false;""")}
%else: ${_('No actions yet')} diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/admin/ldap/ldap.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/templates/admin/ldap/ldap.html Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,73 @@ +## -*- coding: utf-8 -*- +<%inherit file="/base/base.html"/> + +<%def name="title()"> + ${_('LDAP administration')} - ${c.rhodecode_name} + + +<%def name="breadcrumbs_links()"> + ${h.link_to(_('Admin'),h.url('admin_home'))} + » + ${_('Ldap')} + + +<%def name="page_nav()"> + ${self.menu('admin')} + + +<%def name="main()"> +
+ +
+ ${self.breadcrumbs()} +
+

${_('LDAP administration')}

+ ${h.form(url('ldap_settings'))} +
+
+ +
+
+
${h.checkbox('ldap_active',True,class_='small')}
+
+
+
+
${h.text('ldap_host',class_='small')}
+
+
+
+
${h.text('ldap_port',class_='small')}
+
+
+
+
${h.checkbox('ldap_ldaps',True,class_='small')}
+
+
+
+
${h.text('ldap_dn_user',class_='small')}
+
+
+
+
${h.password('ldap_dn_pass',class_='small')}
+
+
+
+
${h.text('ldap_base_dn',class_='small')}
+
+ +
+ ${h.submit('save','Save',class_="ui-button ui-widget ui-state-default ui-corner-all")} +
+
+
+ ${h.end_form()} +
+ + + + + + + + + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/admin/permissions/permissions.html --- a/rhodecode/templates/admin/permissions/permissions.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/admin/permissions/permissions.html Sat Dec 18 14:45:58 2010 +0100 @@ -26,9 +26,18 @@
- +
+
+ +
+
+
+ ${h.checkbox('anonymous',True)} +
+
+
-
+
@@ -66,3 +75,10 @@ ${h.end_form()}
+ + + + + + + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/admin/repos/repo_add.html --- a/rhodecode/templates/admin/repos/repo_add.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/admin/repos/repo_add.html Sat Dec 18 14:45:58 2010 +0100 @@ -35,6 +35,14 @@
+
+ +
+
+ ${h.select('repo_type','hg',c.backends,class_="small")} +
+
+
diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/admin/repos/repo_add_create_repository.html --- a/rhodecode/templates/admin/repos/repo_add_create_repository.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/admin/repos/repo_add_create_repository.html Sat Dec 18 14:45:58 2010 +0100 @@ -32,6 +32,14 @@
+
+ +
+
+ ${h.select('repo_type','hg',c.backends,class_="small")} +
+
+
diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/admin/repos/repo_edit.html --- a/rhodecode/templates/admin/repos/repo_edit.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/admin/repos/repo_edit.html Sat Dec 18 14:45:58 2010 +0100 @@ -18,7 +18,7 @@ <%def name="main()"> -
+
${self.breadcrumbs()} @@ -31,11 +31,18 @@
-
- ${h.text('repo_name',class_="small")} +
+ ${h.text('repo_name',class_="medium")}
- +
+
+ +
+
+ ${h.select('repo_type','hg',c.backends,class_="medium")} +
+
@@ -53,7 +60,14 @@ ${h.checkbox('private',value="True")}
- +
+
+ +
+
+ ${h.checkbox('enable_statistics',value="True")} +
+
@@ -142,7 +156,8 @@
- ${h.submit('save','save',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.submit('save','Save',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.reset('reset','Reset',class_="ui-button ui-widget ui-state-default ui-corner-all")}
@@ -272,4 +287,50 @@
+ +
+
+
${_('Administration')}
+
+ +

${_('Statistics')}

+ + ${h.form(url('repo_stats', repo_name=c.repo_info.repo_name),method='delete')} +
+
+ ${h.submit('reset_stats_%s' % c.repo_info.repo_name,_('Reset current statistics'),class_="refresh_icon action_button",onclick="return confirm('Confirm to remove current statistics');")} + +
+
    +
  • ${_('Fetched to rev')}: ${c.stats_revision}/${c.repo_last_rev}
  • +
  • ${_('Percentage of stats gathered')}: ${c.stats_percentage} %
  • +
+
+ +
+
+ ${h.end_form()} + +

${_('Cache')}

+ ${h.form(url('repo_cache', repo_name=c.repo_info.repo_name),method='delete')} +
+
+ ${h.submit('reset_cache_%s' % c.repo_info.repo_name,_('Invalidate repository cache'),class_="refresh_icon action_button",onclick="return confirm('Confirm to invalidate repository cache');")} +
+
+ ${h.end_form()} + + +

${_('Delete')}

+ ${h.form(url('repo', repo_name=c.repo_info.repo_name),method='delete')} +
+
+ ${h.submit('remove_%s' % c.repo_info.repo_name,_('Remove this repository'),class_="delete_icon action_button",onclick="return confirm('Confirm to delete this repository');")} +
+
+ ${h.end_form()} + +
+ + \ No newline at end of file diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/admin/repos/repos.html --- a/rhodecode/templates/admin/repos/repos.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/admin/repos/repos.html Sat Dec 18 14:45:58 2010 +0100 @@ -37,6 +37,16 @@ %for cnt,repo in enumerate(c.repos_list): + ## TYPE OF REPO + %if repo['repo'].dbrepo.repo_type =='hg': + ${_('Mercurial repository')} + %elif repo['repo'].dbrepo.repo_type =='git': + ${_('Git repository')} + %else: + + %endif + + ## PRIVATE/PUBLIC REPO %if repo['repo'].dbrepo.private: ${_('private')} %else: @@ -55,7 +65,7 @@ ${h.age(repo['last_change'])} %if repo['rev']>=0: - ${h.link_to('r%s:%s' % (repo['rev'],repo['tip']), + ${h.link_to('r%s:%s' % (repo['rev'],h.short_id(repo['tip'])), h.url('changeset_home',repo_name=repo['name'],revision=repo['tip']), class_="tooltip", tooltip_title=h.tooltip(repo['last_msg']))} diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/admin/settings/settings.html --- a/rhodecode/templates/admin/settings/settings.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/admin/settings/settings.html Sat Dec 18 14:45:58 2010 +0100 @@ -42,7 +42,7 @@
- ${h.submit('rescan','rescan repositories',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.submit('rescan','Rescan repositories',class_="ui-button ui-widget ui-state-default ui-corner-all")}
@@ -67,7 +67,7 @@
- ${h.submit('reindex','reindex',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.submit('reindex','Reindex',class_="ui-button ui-widget ui-state-default ui-corner-all")}
@@ -99,7 +99,8 @@
- ${h.submit('save','save settings',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.submit('save','Save settings',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.reset('reset','Reset',class_="ui-button ui-widget ui-state-default ui-corner-all")}
@@ -136,7 +137,15 @@
${h.checkbox('hooks_changegroup_repo_size','True')} -
+ +
+ ${h.checkbox('hooks_pretxnchangegroup_push_logger','True')} + +
+
+ ${h.checkbox('hooks_preoutgoing_pull_logger','True')} + +
@@ -153,7 +162,8 @@
- ${h.submit('save','save settings',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.submit('save','Save settings',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.reset('reset','Reset',class_="ui-button ui-widget ui-state-default ui-corner-all")}
diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/admin/users/user_add.html --- a/rhodecode/templates/admin/users/user_add.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/admin/users/user_add.html Sat Dec 18 14:45:58 2010 +0100 @@ -47,7 +47,7 @@
- +
${h.text('name',class_='small')} @@ -56,7 +56,7 @@
- +
${h.text('lastname',class_='small')} diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/admin/users/user_edit.html --- a/rhodecode/templates/admin/users/user_edit.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/admin/users/user_edit.html Sat Dec 18 14:45:58 2010 +0100 @@ -58,7 +58,7 @@
- +
${h.text('name',class_='small')} @@ -67,7 +67,7 @@
- +
${h.text('lastname',class_='small')} @@ -101,7 +101,8 @@
- ${h.submit('save','save',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.submit('save','Save',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.reset('reset','Reset',class_="ui-button ui-widget ui-state-default ui-corner-all")}
diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/admin/users/user_edit_my_account.html --- a/rhodecode/templates/admin/users/user_edit_my_account.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/admin/users/user_edit_my_account.html Sat Dec 18 14:45:58 2010 +0100 @@ -24,13 +24,24 @@
${h.form(url('admin_settings_my_account_update'),method='put')}
+ +
+
+
gravatar
+

+ Change your avatar at gravatar.com
+ ${_('Using')} ${c.user.email} +

+
+
+
- ${h.text('username')} + ${h.text('username',class_="medium")}
@@ -39,25 +50,25 @@
- ${h.password('new_password')} + ${h.password('new_password',class_="medium")}
- +
- ${h.text('name')} + ${h.text('name',class_="medium")}
- +
- ${h.text('lastname')} + ${h.text('lastname',class_="medium")}
@@ -66,12 +77,16 @@
- ${h.text('email')} + ${h.text('email',class_="medium")}
- ${h.submit('save','save',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.submit('save','Save',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.reset('reset','Reset',class_="ui-button ui-widget ui-state-default ui-corner-all")} + + +
@@ -82,36 +97,57 @@
-
${_('My repositories')}
+
${_('My repositories')} + +
+ %if h.HasPermissionAny('hg.admin','hg.create.repository')(): + + %endif
+ + + + + + %if c.user_repos: %for repo in c.user_repos: - - - + + @@ -127,4 +163,48 @@ + \ No newline at end of file diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/admin/users/users.html --- a/rhodecode/templates/admin/users/users.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/admin/users/users.html Sat Dec 18 14:45:58 2010 +0100 @@ -35,20 +35,23 @@ + %for cnt,user in enumerate(c.users_list): %if user.name !='default': - + - - + + + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/base/base.html --- a/rhodecode/templates/base/base.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/base/base.html Sat Dec 18 14:45:58 2010 +0100 @@ -3,7 +3,7 @@ ${next.title()} - + @@ -16,22 +16,43 @@
${_('Name')}${_('revision')}${_('action')}
- %if repo.dbrepo.private: + %if repo['repo'].dbrepo.repo_type =='hg': + ${_('Mercurial repository')} + %elif repo['repo'].dbrepo.repo_type =='git': + ${_('Git repository')} + %else: + + %endif + %if repo['repo'].dbrepo.private: ${_('private')} %else: ${_('public')} %endif - ${h.link_to(repo.name, h.url('summary_home',repo_name=repo.name))} - %if repo.dbrepo.fork: - + ${h.link_to(repo['repo'].name, h.url('summary_home',repo_name=repo['repo'].name),class_="repo_name")} + %if repo['repo'].dbrepo.fork: + ${_('public')} %endif ${_('revision')}: ${h.get_changeset_safe(repo,'tip').revision}${_('last changed')}: ${h.age(repo.last_change)}${_('private')} ${h.link_to(_('edit'),h.url('repo_settings_home',repo_name=repo.name))}${("r%s:%s") % (h.get_changeset_safe(repo['repo'],'tip').revision,h.short_id(h.get_changeset_safe(repo['repo'],'tip').raw_id))}${_('private')} - ${h.form(url('repo_settings_delete', repo_name=repo.name),method='delete')} - ${h.submit('remove_%s' % repo.name,'delete',class_="delete_icon action_button",onclick="return confirm('Confirm to delete this repository');")} + ${h.form(url('repo_settings_delete', repo_name=repo['repo'].name),method='delete')} + ${h.submit('remove_%s' % repo['repo'].name,'',class_="delete_icon action_button",onclick="return confirm('Confirm to delete this repository');")} ${h.end_form()}
${_('lastname')} ${_('active')} ${_('admin')}${_('ldap')} ${_('action')}
gravatar
gravatar
${h.link_to(user.username,h.url('edit_user', id=user.user_id))} ${user.name} ${user.lastname}${user.active}${user.admin}${h.bool2icon(user.active)}${h.bool2icon(user.admin)}${h.bool2icon(user.is_ldap)} ${h.form(url('user', id=user.user_id),method='delete')} - ${h.submit('remove','delete',class_="delete_icon action_button")} + ${h.submit('remove_','delete',id="remove_user_%s" % user.user_id, + class_="delete_icon action_button",onclick="return confirm('Confirm to delete this user');")} ${h.end_form()}
+ + - %for cnt,branch in enumerate(c.repo_branches.items()): - - - + + + + %endfor diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/changelog/changelog.html --- a/rhodecode/templates/changelog/changelog.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/changelog/changelog.html Sat Dec 18 14:45:58 2010 +0100 @@ -46,7 +46,7 @@ %for cnt,cs in enumerate(c.pagination):
-
${_('commit')} ${cs.revision}: ${cs.short_id}@${cs.date}
+
${_('commit')} ${cs.revision}: ${h.short_id(cs.raw_id)}@${cs.date}
gravatar @@ -54,13 +54,26 @@ ${h.person(cs.author)}
${h.email_or_none(cs.author)}
-
${h.link_to(h.wrap_paragraphs(cs.message),h.url('changeset_home',repo_name=c.repo_name,revision=cs.short_id))}
+
${h.link_to(h.wrap_paragraphs(cs.message),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
- ${len(cs.removed)} - ${len(cs.changed)} - ${len(cs.added)} + + <% + def changed_tooltip(cs): + if cs: + pref = ': ' + suf = '' + if len(cs) > 30: + suf='
'+_(' and %s more') % (len(cs) - 30) + return pref+'
'.join([x.path for x in cs[:30]]) + suf + else: + return ': '+_('No Files') + %> + + ${len(cs.removed)} + ${len(cs.changed)} + ${len(cs.added)}
%if len(cs.parents)>1:
@@ -69,8 +82,8 @@ %endif %if cs.parents: %for p_cs in reversed(cs.parents): -
${_('Parent')} ${p_cs.revision}: ${h.link_to(p_cs.short_id, - h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.short_id),title=p_cs.message)} +
${_('Parent')} ${p_cs.revision}: ${h.link_to(h.short_id(p_cs.raw_id), + h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}
%endfor %else: @@ -78,11 +91,13 @@ %endif + %if cs.branch: - ${h.link_to(cs.branch,h.url('files_home',repo_name=c.repo_name,revision=cs.short_id))} + ${h.link_to(cs.branch,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))} + %endif %for tag in cs.tags: - ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=cs.short_id))} + ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))} %endfor
diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/changeset/changeset.html --- a/rhodecode/templates/changeset/changeset.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/changeset/changeset.html Sat Dec 18 14:45:58 2010 +0100 @@ -1,7 +1,7 @@ <%inherit file="/base/base.html"/> <%def name="title()"> - ${c.repo_name} ${_('Changeset')} - r${c.changeset.revision}:${c.changeset.short_id} - ${c.rhodecode_name} + ${c.repo_name} ${_('Changeset')} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} - ${c.rhodecode_name} <%def name="breadcrumbs_links()"> @@ -9,7 +9,7 @@ » ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))} » - ${_('Changeset')} - r${c.changeset.revision}:${c.changeset.short_id} + ${_('Changeset')} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} <%def name="page_nav()"> @@ -26,18 +26,18 @@
- ${_('Changeset')} - r${c.changeset.revision}:${c.changeset.short_id} + ${_('Changeset')} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} » ${h.link_to(_('raw diff'), - h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.short_id,diff='show'))} + h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='show'))} » ${h.link_to(_('download diff'), - h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.short_id,diff='download'))} + h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='download'))}
-
${_('commit')} ${c.changeset.revision}: ${c.changeset.short_id}@${c.changeset.date}
+
${_('commit')} ${c.changeset.revision}: ${h.short_id(c.changeset.raw_id)}@${c.changeset.date}
gravatar @@ -45,7 +45,7 @@ ${h.person(c.changeset.author)}
${h.email_or_none(c.changeset.author)}
-
${h.link_to(h.wrap_paragraphs(c.changeset.message),h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset.short_id))}
+
${h.link_to(h.wrap_paragraphs(c.changeset.message),h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}
@@ -61,8 +61,8 @@ %if c.changeset.parents: %for p_cs in reversed(c.changeset.parents): -
${_('Parent')} ${p_cs.revision}: ${h.link_to(p_cs.short_id, - h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.short_id),title=p_cs.message)} +
${_('Parent')} ${p_cs.revision}: ${h.link_to(h.short_id(p_cs.raw_id), + h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}
%endfor %else: @@ -70,10 +70,10 @@ %endif - ${h.link_to(c.changeset.branch,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.short_id))} + ${h.link_to(c.changeset.branch,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))} %for tag in c.changeset.tags: - ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.short_id))} + ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))} %endfor
@@ -93,10 +93,10 @@
-
- +
+ ${h.link_to_if(change!='removed',filenode.path,h.url('files_home',repo_name=c.repo_name, - revision=filenode.changeset.short_id,f_path=filenode.path))} + revision=filenode.changeset.raw_id,f_path=filenode.path))} %if 1: » ${h.link_to(_('diff'), diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/changeset/raw_changeset.html --- a/rhodecode/templates/changeset/raw_changeset.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/changeset/raw_changeset.html Sat Dec 18 14:45:58 2010 +0100 @@ -1,6 +1,6 @@ -# HG changeset patch +# ${c.scm_type} changeset patch # User ${c.changeset.author|n} -# Date ${"%d %d" % c.changeset._ctx.date()} +# Date ${c.changeset.date} # Node ID ${c.changeset.raw_id} # ${c.parent_tmpl} ${c.changeset.message} diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/errors/error_404.html --- a/rhodecode/templates/errors/error_404.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/errors/error_404.html Sat Dec 18 14:45:58 2010 +0100 @@ -6,7 +6,7 @@ <%def name="breadcrumbs()"> - ${h.link_to(u'Home',h.url('hg_home'))} + ${h.link_to(u'Home',h.url('home'))} / ${h.link_to(u'Admin',h.url('admin_home'))} @@ -26,7 +26,7 @@ ${_('Create "%s" repository as %s' % (c.repo_name,c.repo_name_cleaned))}

-

${h.link_to(_('Go back to the main repository list page'),h.url('hg_home'))}

+

${h.link_to(_('Go back to the main repository list page'),h.url('home'))}

\ No newline at end of file diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/files/file_diff.html --- a/rhodecode/templates/files/file_diff.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/files/file_diff.html Sat Dec 18 14:45:58 2010 +0100 @@ -9,7 +9,7 @@ » ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))} » - ${'%s: %s %s %s' % (_('File diff'),c.diff2,'→',c.diff1)|n} + ${_('File diff')} r${c.changeset_1.revision}:${h.short_id(c.changeset_1.raw_id)} → r${c.changeset_2.revision}:${h.short_id(c.changeset_2.raw_id)} <%def name="page_nav()"> @@ -24,15 +24,15 @@
-
- ${h.link_to(c.f_path,h.url('files_home',repo_name=c.repo_name, - revision=c.diff2.split(':')[1],f_path=c.f_path))} +
+ ${h.link_to(c.f_path,h.url('files_home',repo_name=c.repo_name, + revision=c.changeset_2.raw_id,f_path=c.f_path))} » ${h.link_to(_('diff'), - h.url.current(diff2=c.diff2.split(':')[-1],diff1=c.diff1.split(':')[-1],diff='diff'))} + h.url.current(diff2=c.changeset_2.raw_id,diff1=c.changeset_1.raw_id,diff='diff'))} » ${h.link_to(_('raw diff'), - h.url.current(diff2=c.diff2.split(':')[-1],diff1=c.diff1.split(':')[-1],diff='raw'))} + h.url.current(diff2=c.changeset_2.raw_id,diff1=c.changeset_1.raw_id,diff='raw'))} » ${h.link_to(_('download diff'), - h.url.current(diff2=c.diff2.split(':')[-1],diff1=c.diff1.split(':')[-1],diff='download'))} + h.url.current(diff2=c.changeset_2.raw_id,diff1=c.changeset_1.raw_id,diff='download'))}
diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/files/files.html --- a/rhodecode/templates/files/files.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/files/files.html Sat Dec 18 14:45:58 2010 +0100 @@ -11,7 +11,7 @@ » ${_('files')} %if c.files_list: - @ R${c.rev_nr}:${c.cur_rev} + @ r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} %endif @@ -23,12 +23,19 @@
- ${self.breadcrumbs()} + ${self.breadcrumbs()} +
%if c.files_list: -

${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.cur_rev,c.files_list.path)}

+

+ ${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.changeset.raw_id,c.files_list.path)} +

%if c.files_list.is_dir(): <%include file='files_browser.html'/> %else: diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/files/files_annotate.html --- a/rhodecode/templates/files/files_annotate.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/files/files_annotate.html Sat Dec 18 14:45:58 2010 +0100 @@ -9,7 +9,7 @@ » ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))} » - ${_('annotate')} @ R${c.rev_nr}:${c.cur_rev} + ${_('annotate')} @ R${c.cs.revision}:${h.short_id(c.cs.raw_id)} <%def name="page_nav()"> @@ -20,41 +20,67 @@
${self.breadcrumbs()} +
-

${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.cur_rev,c.file.path)}

+

${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.cs.revision,c.file.path)}

-
${_('Last revision')}
-
${h.link_to("r%s:%s" % (c.file.last_changeset.revision,c.file.last_changeset.short_id), - h.url('files_annotate_home',repo_name=c.repo_name,revision=c.file.last_changeset.short_id,f_path=c.f_path))}
+
${_('Revision')}
+
${h.link_to("r%s:%s" % (c.file.last_changeset.revision,h.short_id(c.file.last_changeset.raw_id)), + h.url('changeset_home',repo_name=c.repo_name,revision=c.file.last_changeset.raw_id))}
${_('Size')}
${h.format_byte_size(c.file.size,binary=True)}
${_('Mimetype')}
${c.file.mimetype}
${_('Options')}
${h.link_to(_('show source'), - h.url('files_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} + h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path))} / ${h.link_to(_('show as raw'), - h.url('files_raw_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} + h.url('files_raw_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path))} / ${h.link_to(_('download as raw'), - h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} -
+ h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path))} + +
${_('History')}
+
+
+ ${h.form(h.url('files_diff_home',repo_name=c.repo_name,f_path=c.f_path),method='get')} + ${h.hidden('diff2',c.file.last_changeset.raw_id)} + ${h.select('diff1',c.file.last_changeset.raw_id,c.file_history)} + ${h.submit('diff','diff to revision',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.submit('show_rev','show at revision',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.end_form()} +
+
-
${c.file.name}@r${c.file.last_changeset.revision}:${c.file.last_changeset.short_id}
-
"${c.file_msg}"
+
${c.file.name}@r${c.file.last_changeset.revision}:${h.short_id(c.file.last_changeset.raw_id)}
+
"${c.file.message}"
- % if c.file.size < c.file_size_limit: + % if c.file.size < c.cut_off_limit: ${h.pygmentize_annotation(c.file,linenos=True,anchorlinenos=True,lineanchors='S',cssclass="code-highlight")} %else: ${_('File is to big to display')} ${h.link_to(_('show as raw'), - h.url('files_raw_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} + h.url('files_raw_home',repo_name=c.repo_name,revision=c.cs.revision,f_path=c.f_path))} %endif
+
diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/files/files_browser.html --- a/rhodecode/templates/files/files_browser.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/files/files_browser.html Sat Dec 18 14:45:58 2010 +0100 @@ -10,9 +10,9 @@ ${h.form(h.url.current())}
${_('view')}@rev - « - ${h.text('at_rev',value=c.rev_nr,size=3)} - » + « + ${h.text('at_rev',value=c.changeset.revision,size=3)} + » ${h.submit('view','view')}
${h.end_form()} @@ -30,23 +30,23 @@ - % if c.files_list.parent: + %if c.files_list.parent:
- + %endif %for cnt,node in enumerate(c.files_list): - + %endfor diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/files/files_source.html --- a/rhodecode/templates/files/files_source.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/files/files_source.html Sat Dec 18 14:45:58 2010 +0100 @@ -1,8 +1,8 @@
-
${_('Last revision')}
+
${_('Revision')}
- ${h.link_to("r%s:%s" % (c.files_list.last_changeset.revision,c.files_list.last_changeset.short_id), - h.url('files_home',repo_name=c.repo_name,revision=c.files_list.last_changeset.short_id,f_path=c.f_path))} + ${h.link_to("r%s:%s" % (c.files_list.last_changeset.revision,h.short_id(c.files_list.last_changeset.raw_id)), + h.url('changeset_home',repo_name=c.repo_name,revision=c.files_list.last_changeset.raw_id))}
${_('Size')}
${h.format_byte_size(c.files_list.size,binary=True)}
@@ -10,18 +10,18 @@
${c.files_list.mimetype}
${_('Options')}
${h.link_to(_('show annotation'), - h.url('files_annotate_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} + h.url('files_annotate_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))} / ${h.link_to(_('show as raw'), - h.url('files_raw_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} + h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))} / ${h.link_to(_('download as raw'), - h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} + h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))}
${_('History')}
${h.form(h.url('files_diff_home',repo_name=c.repo_name,f_path=c.f_path),method='get')} - ${h.hidden('diff2',c.files_list.last_changeset.short_id)} - ${h.select('diff1',c.files_list.last_changeset.short_id,c.file_history)} + ${h.hidden('diff2',c.files_list.last_changeset.raw_id)} + ${h.select('diff1',c.files_list.last_changeset.raw_id,c.file_history)} ${h.submit('diff','diff to revision',class_="ui-button ui-widget ui-state-default ui-corner-all")} ${h.submit('show_rev','show at revision',class_="ui-button ui-widget ui-state-default ui-corner-all")} ${h.end_form()} @@ -32,15 +32,15 @@
-
${c.files_list.name}@r${c.files_list.last_changeset.revision}:${c.files_list.last_changeset.short_id}
+
${c.files_list.name}@r${c.files_list.last_changeset.revision}:${h.short_id(c.files_list.last_changeset.raw_id)}
"${c.files_list.last_changeset.message}"
- % if c.files_list.size < c.file_size_limit: + % if c.files_list.size < c.cut_off_limit: ${h.pygmentize(c.files_list,linenos=True,anchorlinenos=True,lineanchors='S',cssclass="code-highlight")} %else: ${_('File is to big to display')} ${h.link_to(_('show as raw'), - h.url('files_raw_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.f_path))} + h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))} %endif
diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/index.html --- a/rhodecode/templates/index.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/index.html Sat Dec 18 14:45:58 2010 +0100 @@ -27,72 +27,142 @@
-
${_('Dashboard')}
- %if h.HasPermissionAny('hg.admin','hg.create.repository')(): - - %endif +
${_('Dashboard')} + +
+ %if c.rhodecode_user.username != 'default': + %if h.HasPermissionAny('hg.admin','hg.create.repository')(): + + %endif + %endif
-
${_('date')}${_('name')}${_('author')} ${_('revision')}${_('name')} ${_('links')}
${h.age(branch[1]._ctx.date())}r${branch[1].revision}:${branch[1].short_id} - - ${h.link_to(branch[0], - h.url('changeset_home',repo_name=c.repo_name,revision=branch[1].short_id))} - - + ${branch[1].date} + + + ${h.link_to(branch[0], + h.url('changeset_home',repo_name=c.repo_name,revision=branch[1].raw_id))} + + ${h.person(branch[1].author)}r${branch[1].revision}:${h.short_id(branch[1].raw_id)} - ${h.link_to(_('changeset'),h.url('changeset_home',repo_name=c.repo_name,revision=branch[1].short_id))} + ${h.link_to(_('changeset'),h.url('changeset_home',repo_name=c.repo_name,revision=branch[1].raw_id))} | - ${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=branch[1].short_id))} + ${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=branch[1].raw_id))}
- ${h.link_to('..',h.url('files_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=c.files_list.parent.path),class_="browser-dir")} + ${h.link_to('..',h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.files_list.parent.path),class_="browser-dir")}
- ${h.link_to(node.name,h.url('files_home',repo_name=c.repo_name,revision=c.cur_rev,f_path=node.path),class_=file_class(node))} + ${h.link_to(node.name,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=node.path),class_=file_class(node))} %if node.is_file(): @@ -59,19 +59,19 @@ %endif - %if node.is_file(): - ${node.last_changeset.revision} - %endif + %if node.is_file(): + ${node.last_changeset.revision} + %endif - %if node.is_file(): - ${h.age(node.last_changeset._ctx.date())} - ${node.last_changeset.date} - %endif + %if node.is_file(): + ${node.last_changeset.date} - ${h.age(node.last_changeset.date)} + %endif - %if node.is_file(): - ${node.last_changeset.author} - %endif + %if node.is_file(): + ${node.last_changeset.author} + %endif
+
- + - - %for cnt,repo in enumerate(c.repos_list): - %if h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(repo['name'],'main page check'): - - - - - - - - - - %endif - %endfor - -
${get_sort(_('Name'))} ${get_sort(_('Description'))} ${get_sort(_('Last change'))} ${get_sort(_('Tip'))}${get_sort(_('Contact'))}${get_sort(_('Owner'))} ${_('RSS')} ${_('Atom')}
- %if repo['repo'].dbrepo.private: - ${_('private')} - %else: - ${_('public')} - %endif - ${h.link_to(repo['name'], - h.url('summary_home',repo_name=repo['name']))} - %if repo['repo'].dbrepo.fork: - - ${_('public')} - %endif - ${h.truncate(repo['description'],60)}${h.age(repo['last_change'])} - %if repo['rev']>=0: - ${h.link_to('r%s:%s' % (repo['rev'],repo['tip']), - h.url('changeset_home',repo_name=repo['name'],revision=repo['tip']), - class_="tooltip", - tooltip_title=h.tooltip(repo['last_msg']))} - %else: - ${_('No changesets yet')} - %endif - ${h.person(repo['contact'])} - - - -
+ + %for cnt,repo in enumerate(c.repos_list): + + +
+ ## TYPE OF REPO + %if repo['repo'].dbrepo.repo_type =='hg': + ${_('Mercurial repository')} + %elif repo['repo'].dbrepo.repo_type =='git': + ${_('Git repository')} + %else: + + %endif + + ##PRIVATE/PUBLIC + %if repo['repo'].dbrepo.private: + ${_('private repository')} + %else: + ${_('public repository')} + %endif + + ##NAME + ${h.link_to(repo['name'], + h.url('summary_home',repo_name=repo['name']),class_="repo_name")} + %if repo['repo'].dbrepo.fork: + + ${_('fork')} + %endif +
+ + ##DESCRIPTION + + ${h.truncate(repo['description'],60)} + + ##LAST CHANGE + + + ${h.age(repo['last_change'])} + + + %if repo['rev']>=0: + ${h.link_to('r%s:%s' % (repo['rev'],h.short_id(repo['tip'])), + h.url('changeset_home',repo_name=repo['name'],revision=repo['tip']), + class_="tooltip", + tooltip_title=h.tooltip(repo['last_msg']))} + %else: + ${_('No changesets yet')} + %endif + + ${h.person(repo['contact'])} + + + + + + + + %endfor + +
-
+
+ + + + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/journal.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/templates/journal.html Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,92 @@ +## -*- coding: utf-8 -*- +<%inherit file="base/base.html"/> +<%def name="title()"> + ${_('Journal')} - ${c.rhodecode_name} + +<%def name="breadcrumbs()"> + ${c.rhodecode_name} + +<%def name="page_nav()"> + ${self.menu('home')} + +<%def name="main()"> + +
+ +
+
${_('Journal')}
+
+
+ %if c.journal: + %for entry in c.journal: +
+
+ gravatar +
+
${entry.user.name} ${entry.user.lastname}
+
${h.action_parser(entry)}
+
+ ${h.action_parser_icon(entry)} +
+
+ + %if entry.repository: + ${h.link_to(entry.repository.repo_name, + h.url('summary_home',repo_name=entry.repository.repo_name))} + %else: + ${entry.repository_name} + %endif + - ${h.age(entry.action_date)} +
+
+
+ %endfor + %else: + ${_('No entries yet')} + %endif +
+
+ +
+ +
+
${_('Following')}
+
+
+ %if c.following: + %for entry in c.following: +
+ %if entry.follows_user_id: + ${_('user')} + ${entry.follows_user.full_contact} + %endif + + %if entry.follows_repo_id: + + %if entry.follows_repository.private: + ${_('private repository')} + %else: + ${_('public repository')} + %endif + + ${h.link_to(entry.follows_repository.repo_name,h.url('summary_home', + repo_name=entry.follows_repository.repo_name))} + + %endif +
+ %endfor + %else: + ${_('You are not following any users or repositories')} + %endif +
+
+ + + + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/login.html --- a/rhodecode/templates/login.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/login.html Sat Dec 18 14:45:58 2010 +0100 @@ -4,7 +4,7 @@ ${_('Sign In')} - ${c.rhodecode_name} - + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/register.html --- a/rhodecode/templates/register.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/register.html Sat Dec 18 14:45:58 2010 +0100 @@ -15,7 +15,7 @@
-
${_('Sign Up to rhodecode')}
+
${_('Sign Up to RhodeCode')}
${h.form(url('register'))} @@ -27,25 +27,34 @@
- ${h.text('username')} + ${h.text('username',class_="medium")}
- +
- ${h.password('password')} + ${h.password('password',class_="medium")}
- + +
+
+ +
+
+ ${h.password('password_confirmation',class_="medium")} +
+
+
- ${h.text('name')} + ${h.text('name',class_="medium")}
@@ -54,7 +63,7 @@
- ${h.text('lastname')} + ${h.text('lastname',class_="medium")}
@@ -63,7 +72,7 @@
- ${h.text('email')} + ${h.text('email',class_="medium")}
diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/settings/repo_fork.html --- a/rhodecode/templates/settings/repo_fork.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/settings/repo_fork.html Sat Dec 18 14:45:58 2010 +0100 @@ -30,6 +30,7 @@
${h.text('fork_name',class_="small")} + ${h.hidden('repo_type',c.repo_info.repo_type)}
diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/settings/repo_settings.html --- a/rhodecode/templates/settings/repo_settings.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/settings/repo_settings.html Sat Dec 18 14:45:58 2010 +0100 @@ -128,7 +128,8 @@
- ${h.submit('update','update',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.submit('update','Update',class_="ui-button ui-widget ui-state-default ui-corner-all")} + ${h.reset('reset','Reset',class_="ui-button ui-widget ui-state-default ui-corner-all")}
diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/shortlog/shortlog_data.html --- a/rhodecode/templates/shortlog/shortlog_data.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/shortlog/shortlog_data.html Sat Dec 18 14:45:58 2010 +0100 @@ -2,10 +2,10 @@ % if c.repo_changesets: - + + - @@ -13,14 +13,16 @@ %for cnt,cs in enumerate(c.repo_changesets): - + + - - + %endfor diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/summary/summary.html --- a/rhodecode/templates/summary/summary.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/summary/summary.html Sat Dec 18 14:45:58 2010 +0100 @@ -17,17 +17,6 @@ <%def name="main()"> -
@@ -42,12 +31,30 @@
+ %if c.repo_info.dbrepo.repo_type =='hg': + ${_('Mercurial repository')} + %endif + %if c.repo_info.dbrepo.repo_type =='git': + ${_('Git repository')} + %endif + %if c.repo_info.dbrepo.private: - ${_('private')} + ${_('private repository')} %else: - ${_('public')} + ${_('public repository')} %endif ${c.repo_info.name} + %if c.rhodecode_user.username != 'default': + %if c.following: + + + %else: + + %endif + %endif:
%if c.repo_info.dbrepo.fork: @@ -68,7 +75,7 @@
- ${c.repo_info.description} + ${c.repo_info.dbrepo.description}
@@ -92,7 +99,7 @@
- ${h.age(c.repo_info.last_change)} - ${h.rfc822date_notz(c.repo_info.last_change)} + ${h.age(c.repo_info.last_change)} - ${c.repo_info.last_change} ${_('by')} ${h.get_changeset_safe(c.repo_info,'tip').author}
@@ -109,13 +116,19 @@
- +
@@ -199,9 +248,13 @@
-
+ + %if c.no_data: +
${c.no_data_msg}
+ %endif: +
-
+
@@ -228,10 +281,10 @@ }; var dataset = dataset; var overview_dataset = [overview_dataset]; - var choiceContainer = YAHOO.util.Dom.get("legend_choices"); - var choiceContainerTable = YAHOO.util.Dom.get("legend_choices_tables"); - var plotContainer = YAHOO.util.Dom.get('commit_history'); - var overviewContainer = YAHOO.util.Dom.get('overview'); + var choiceContainer = YUD.get("legend_choices"); + var choiceContainerTable = YUD.get("legend_choices_tables"); + var plotContainer = YUD.get('commit_history'); + var overviewContainer = YUD.get('overview'); var plot_options = { bars: {show:true,align:'center',lineWidth:4}, @@ -257,7 +310,7 @@ bars: {show:true,barWidth: 2,}, shadowSize: 0, xaxis: {mode: "time", timeformat: "%d/%m/%y",}, - yaxis: {ticks: 3, min: 0,}, + yaxis: {ticks: 3, min: 0,tickDecimals:0,}, grid: {color: "#999",}, selection: {mode: "x"} }; @@ -314,7 +367,7 @@ div.style.backgroundColor='#fee'; document.body.appendChild(div); } - YAHOO.util.Dom.setStyle(div, 'opacity', 0); + YUD.setStyle(div, 'opacity', 0); div.innerHTML = contents; div.style.top=(y + 5) + "px"; div.style.left=(x + 5) + "px"; @@ -324,9 +377,9 @@ } /** - * This function will detect if selected period has some changesets for this user - if it does this data is then pushed for displaying - Additionally it will only display users that are selected by the checkbox + * This function will detect if selected period has some changesets + for this user if it does this data is then pushed for displaying + Additionally it will only display users that are selected by the checkbox */ function getDataAccordingToRanges(ranges) { @@ -334,31 +387,28 @@ var keys = []; for(var key in dataset){ var push = false; + //method1 slow !! - ///* + //* for(var ds in dataset[key].data){ commit_data = dataset[key].data[ds]; - //console.log(key); - //console.log(new Date(commit_data.time*1000)); - //console.log(new Date(ranges.xaxis.from*1000)); - //console.log(new Date(ranges.xaxis.to*1000)); if (commit_data.time >= ranges.xaxis.from && commit_data.time <= ranges.xaxis.to){ push = true; break; } } - //*/ + //*/ + /*//method2 sorted commit data !!! + var first_commit = dataset[key].data[0].time; var last_commit = dataset[key].data[dataset[key].data.length-1].time; - console.log(first_commit); - console.log(last_commit); - if (first_commit >= ranges.xaxis.from && last_commit <= ranges.xaxis.to){ push = true; } - */ + //*/ + if(push){ data.push(dataset[key]); } @@ -399,14 +449,14 @@ new_data.push(getDummyData(checkbox_key)); } } - + var new_options = YAHOO.lang.merge(plot_options, { xaxis: { min: cur_ranges.xaxis.from, max: cur_ranges.xaxis.to, mode:"time", timeformat: "%d/%m", - } + }, }); if (!new_data){ new_data = [[0,1]]; @@ -440,7 +490,12 @@ max: ranges.xaxis.to, mode:"time", timeformat: "%d/%m", - } + }, + yaxis: { + min: ranges.yaxis.from, + max: ranges.yaxis.to, + }, + }); // do the zooming plot = YAHOO.widget.Flot(plotContainer, data, new_options); @@ -454,7 +509,7 @@ overview.setSelection(ranges, true); //resubscribe choiced - YAHOO.util.Event.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, ranges]); + YUE.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, ranges]); } var previousPoint = null; @@ -463,13 +518,13 @@ var pos = o.pos; var item = o.item; - //YAHOO.util.Dom.get("x").innerHTML = pos.x.toFixed(2); - //YAHOO.util.Dom.get("y").innerHTML = pos.y.toFixed(2); + //YUD.get("x").innerHTML = pos.x.toFixed(2); + //YUD.get("y").innerHTML = pos.y.toFixed(2); if (item) { if (previousPoint != item.datapoint) { previousPoint = item.datapoint; - var tooltip = YAHOO.util.Dom.get("tooltip"); + var tooltip = YUD.get("tooltip"); if(tooltip) { tooltip.parentNode.removeChild(tooltip); } @@ -508,7 +563,7 @@ } } else { - var tooltip = YAHOO.util.Dom.get("tooltip"); + var tooltip = YUD.get("tooltip"); if(tooltip) { tooltip.parentNode.removeChild(tooltip); @@ -541,7 +596,7 @@ plot.subscribe("plothover", plothover); - YAHOO.util.Event.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, initial_ranges]); + YUE.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, initial_ranges]); } SummaryPlot(${c.ts_min},${c.ts_max},${c.commit_data|n},${c.overview_data|n}); @@ -551,18 +606,20 @@
- +
- <%include file='../shortlog/shortlog_data.html'/> - %if c.repo_changesets: - ${h.link_to(_('show more'),h.url('changelog_home',repo_name=c.repo_name))} - %endif +
+ <%include file='../shortlog/shortlog_data.html'/> +
+ ##%if c.repo_changesets: + ## ${h.link_to(_('show more'),h.url('changelog_home',repo_name=c.repo_name))} + ##%endif
- +
<%include file='../tags/tags_data.html'/> @@ -573,7 +630,7 @@
- +
<%include file='../branches/branches_data.html'/> diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/templates/tags/tags_data.html --- a/rhodecode/templates/tags/tags_data.html Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/templates/tags/tags_data.html Sat Dec 18 14:45:58 2010 +0100 @@ -1,25 +1,29 @@ %if c.repo_tags:
${_('date')}${_('commit message')}${_('age')} ${_('author')} ${_('revision')}${_('commit message')} ${_('branch')} ${_('tags')} ${_('links')}
${h.age(cs._ctx.date())} - ${h.rfc822date_notz(cs._ctx.date())} + ${h.link_to(h.truncate(cs.message,50), + h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id), + title=cs.message)} + + ${h.age(cs.date)} + ${h.person(cs.author)}r${cs.revision}:${cs.short_id} - ${h.link_to(h.truncate(cs.message,60), - h.url('changeset_home',repo_name=c.repo_name,revision=cs.short_id), - title=cs.message)} - r${cs.revision}:${h.short_id(cs.raw_id)} ${cs.branch} @@ -34,9 +36,9 @@ - ${h.link_to(_('changeset'),h.url('changeset_home',repo_name=c.repo_name,revision=cs.short_id))} + ${h.link_to(_('changeset'),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))} | - ${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=cs.short_id))} + ${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
- - - + + + + %for cnt,tag in enumerate(c.repo_tags.items()): - - - - + + + + + %endfor diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/__init__.py --- a/rhodecode/tests/__init__.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/tests/__init__.py Sat Dec 18 14:45:58 2010 +0100 @@ -18,33 +18,44 @@ from rhodecode.model import meta import logging + log = logging.getLogger(__name__) import pylons.test -__all__ = ['environ', 'url', 'TestController'] +__all__ = ['environ', 'url', 'TestController', 'TESTS_TMP_PATH', 'HG_REPO', + 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO', 'HG_FORK', 'GIT_FORK', ] # Invoke websetup with the current config file #SetupCommand('setup-app').run([config_file]) ##RUNNING DESIRED TESTS -#nosetests rhodecode.tests.functional.test_admin_settings:TestSettingsController.test_my_account +#nosetests -x rhodecode.tests.functional.test_admin_settings:TestSettingsController.test_my_account environ = {} -TEST_DIR = '/tmp' -REPO_PATH = os.path.join(TEST_DIR, 'vcs_test') -NEW_REPO_PATH = os.path.join(TEST_DIR, 'vcs_test_new') -FORK_REPO_PATH = os.path.join(TEST_DIR, 'vcs_test_fork') + +#SOME GLOBALS FOR TESTS +TESTS_TMP_PATH = '/tmp' + +HG_REPO = 'vcs_test_hg' +GIT_REPO = 'vcs_test_git' + +NEW_HG_REPO = 'vcs_test_hg_new' +NEW_GIT_REPO = 'vcs_test_git_new' + +HG_FORK = 'vcs_test_hg_fork' +GIT_FORK = 'vcs_test_git_fork' class TestController(TestCase): def __init__(self, *args, **kwargs): wsgiapp = pylons.test.pylonsapp config = wsgiapp.config + self.app = TestApp(wsgiapp) url._push_object(URLGenerator(config['routes.map'], environ)) self.sa = meta.Session - + self.index_location = config['app_conf']['index_dir'] TestCase.__init__(self, *args, **kwargs) def log_user(self, username='test_admin', password='test12'): diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_admin_ldap_settings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/tests/functional/test_admin_ldap_settings.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,7 @@ +from rhodecode.tests import * + +class TestLdapSettingsController(TestController): + + def test_index(self): + response = self.app.get(url(controller='admin/ldap_settings', action='index')) + # Test response... diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_admin_permissions.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/tests/functional/test_admin_permissions.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,43 @@ +from rhodecode.tests import * + +class TestAdminPermissionsController(TestController): + + def test_index(self): + response = self.app.get(url('permissions')) + # Test response... + + def test_index_as_xml(self): + response = self.app.get(url('formatted_permissions', format='xml')) + + def test_create(self): + response = self.app.post(url('permissions')) + + def test_new(self): + response = self.app.get(url('new_permission')) + + def test_new_as_xml(self): + response = self.app.get(url('formatted_new_permission', format='xml')) + + def test_update(self): + response = self.app.put(url('permission', id=1)) + + def test_update_browser_fakeout(self): + response = self.app.post(url('permission', id=1), params=dict(_method='put')) + + def test_delete(self): + response = self.app.delete(url('permission', id=1)) + + def test_delete_browser_fakeout(self): + response = self.app.post(url('permission', id=1), params=dict(_method='delete')) + + def test_show(self): + response = self.app.get(url('permission', id=1)) + + def test_show_as_xml(self): + response = self.app.get(url('formatted_permission', id=1, format='xml')) + + def test_edit(self): + response = self.app.get(url('edit_permission', id=1)) + + def test_edit_as_xml(self): + response = self.app.get(url('formatted_edit_permission', id=1, format='xml')) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_admin_repos.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/tests/functional/test_admin_repos.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,131 @@ +from rhodecode.model.db import Repository +from rhodecode.tests import * + +class TestAdminReposController(TestController): + + def test_index(self): + self.log_user() + response = self.app.get(url('repos')) + # Test response... + + def test_index_as_xml(self): + response = self.app.get(url('formatted_repos', format='xml')) + + def test_create_hg(self): + self.log_user() + repo_name = NEW_HG_REPO + description = 'description for newly created repo' + private = False + response = self.app.post(url('repos'), {'repo_name':repo_name, + 'repo_type':'hg', + 'description':description, + 'private':private}) + + + #test if we have a message for that repository + assert '''created repository %s''' % (repo_name) in response.session['flash'][0], 'No flash message about new repo' + + #test if the fork was created in the database + new_repo = self.sa.query(Repository).filter(Repository.repo_name == repo_name).one() + + assert new_repo.repo_name == repo_name, 'wrong name of repo name in db' + assert new_repo.description == description, 'wrong description' + + #test if repository is visible in the list ? + response = response.follow() + + assert repo_name in response.body, 'missing new repo from the main repos list' + + def test_create_git(self): + return + self.log_user() + repo_name = NEW_GIT_REPO + description = 'description for newly created repo' + private = False + response = self.app.post(url('repos'), {'repo_name':repo_name, + 'repo_type':'git', + 'description':description, + 'private':private}) + + + #test if we have a message for that repository + assert '''created repository %s''' % (repo_name) in response.session['flash'][0], 'No flash message about new repo' + + #test if the fork was created in the database + new_repo = self.sa.query(Repository).filter(Repository.repo_name == repo_name).one() + + assert new_repo.repo_name == repo_name, 'wrong name of repo name in db' + assert new_repo.description == description, 'wrong description' + + #test if repository is visible in the list ? + response = response.follow() + + assert repo_name in response.body, 'missing new repo from the main repos list' + + + def test_new(self): + self.log_user() + response = self.app.get(url('new_repo')) + + def test_new_as_xml(self): + response = self.app.get(url('formatted_new_repo', format='xml')) + + def test_update(self): + response = self.app.put(url('repo', repo_name=HG_REPO)) + + def test_update_browser_fakeout(self): + response = self.app.post(url('repo', repo_name=HG_REPO), params=dict(_method='put')) + + def test_delete(self): + self.log_user() + repo_name = 'vcs_test_new_to_delete' + description = 'description for newly created repo' + private = False + response = self.app.post(url('repos'), {'repo_name':repo_name, + 'repo_type':'hg', + 'description':description, + 'private':private}) + + + #test if we have a message for that repository + assert '''created repository %s''' % (repo_name) in response.session['flash'][0], 'No flash message about new repo' + + #test if the repo was created in the database + new_repo = self.sa.query(Repository).filter(Repository.repo_name == repo_name).one() + + assert new_repo.repo_name == repo_name, 'wrong name of repo name in db' + assert new_repo.description == description, 'wrong description' + + #test if repository is visible in the list ? + response = response.follow() + + assert repo_name in response.body, 'missing new repo from the main repos list' + + + response = self.app.delete(url('repo', repo_name=repo_name)) + + assert '''deleted repository %s''' % (repo_name) in response.session['flash'][0], 'No flash message about delete repo' + + response.follow() + + #check if repo was deleted from db + deleted_repo = self.sa.query(Repository).filter(Repository.repo_name == repo_name).scalar() + + assert deleted_repo is None, 'Deleted repository was found in db' + + + def test_delete_browser_fakeout(self): + response = self.app.post(url('repo', repo_name=HG_REPO), params=dict(_method='delete')) + + def test_show(self): + self.log_user() + response = self.app.get(url('repo', repo_name=HG_REPO)) + + def test_show_as_xml(self): + response = self.app.get(url('formatted_repo', repo_name=HG_REPO, format='xml')) + + def test_edit(self): + response = self.app.get(url('edit_repo', repo_name=HG_REPO)) + + def test_edit_as_xml(self): + response = self.app.get(url('formatted_edit_repo', repo_name=HG_REPO, format='xml')) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_admin_settings.py --- a/rhodecode/tests/functional/test_admin_settings.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/tests/functional/test_admin_settings.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,7 +1,8 @@ -from rhodecode.tests import * +from rhodecode.lib.auth import get_crypt_password, check_password from rhodecode.model.db import User +from rhodecode.tests import * -class TestSettingsController(TestController): +class TestAdminSettingsController(TestController): def test_index(self): response = self.app.get(url('admin_settings')) @@ -48,45 +49,64 @@ response = self.app.get(url('admin_settings_my_account')) print response assert 'value="test_admin' in response.body - - - + + + def test_my_account_update(self): self.log_user() + new_email = 'new@mail.pl' + new_name = 'NewName' + new_lastname = 'NewLastname' + new_password = 'test123' + + response = self.app.post(url('admin_settings_my_account_update'), params=dict( _method='put', username='test_admin', - new_password='test12', + new_password=new_password, password='', - name='NewName', - lastname='NewLastname', + name=new_name, + lastname=new_lastname, email=new_email,)) response.follow() - print response - - print 'x' * 100 - print response.session - assert 'Your account was updated succesfully' in response.session['flash'][0][1], 'no flash message about success of change' + + assert 'Your account was updated successfully' in response.session['flash'][0][1], 'no flash message about success of change' user = self.sa.query(User).filter(User.username == 'test_admin').one() assert user.email == new_email , 'incorrect user email after update got %s vs %s' % (user.email, new_email) - - def test_my_account_update_own_email_ok(self): - self.log_user() - - new_email = 'new@mail.pl' + assert user.name == new_name, 'updated field mismatch %s vs %s' % (user.name, new_name) + assert user.lastname == new_lastname, 'updated field mismatch %s vs %s' % (user.lastname, new_lastname) + assert check_password(new_password, user.password) is True, 'password field mismatch %s vs %s' % (user.password, new_password) + + #bring back the admin settings + old_email = 'test_admin@mail.com' + old_name = 'RhodeCode' + old_lastname = 'Admin' + old_password = 'test12' + response = self.app.post(url('admin_settings_my_account_update'), params=dict( _method='put', username='test_admin', - new_password='test12', - name='NewName', - lastname='NewLastname', - email=new_email,)) - print response - + new_password=old_password, + password='', + name=old_name, + lastname=old_lastname, + email=old_email,)) + + response.follow() + assert 'Your account was updated successfully' in response.session['flash'][0][1], 'no flash message about success of change' + user = self.sa.query(User).filter(User.username == 'test_admin').one() + assert user.email == old_email , 'incorrect user email after update got %s vs %s' % (user.email, old_email) + + assert user.email == old_email , 'incorrect user email after update got %s vs %s' % (user.email, old_email) + assert user.name == old_name, 'updated field mismatch %s vs %s' % (user.name, old_name) + assert user.lastname == old_lastname, 'updated field mismatch %s vs %s' % (user.lastname, old_lastname) + assert check_password(old_password, user.password) is True , 'password updated field mismatch %s vs %s' % (user.password, old_password) + + def test_my_account_update_err_email_exists(self): self.log_user() - + new_email = 'test_regular@mail.com'#already exisitn email response = self.app.post(url('admin_settings_my_account_update'), params=dict( _method='put', @@ -96,13 +116,13 @@ lastname='NewLastname', email=new_email,)) print response - - assert 'That e-mail address is already taken' in response.body, 'Missing error message about existing email' - - + + assert 'This e-mail address is already taken' in response.body, 'Missing error message about existing email' + + def test_my_account_update_err(self): self.log_user('test_regular2', 'test12') - + new_email = 'newmail.pl' response = self.app.post(url('admin_settings_my_account_update'), params=dict( _method='put', diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_admin_users.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/tests/functional/test_admin_users.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,119 @@ +from rhodecode.tests import * +from rhodecode.model.db import User +from rhodecode.lib.auth import check_password +from sqlalchemy.orm.exc import NoResultFound + +class TestAdminUsersController(TestController): + + def test_index(self): + response = self.app.get(url('users')) + # Test response... + + def test_index_as_xml(self): + response = self.app.get(url('formatted_users', format='xml')) + + def test_create(self): + self.log_user() + username = 'newtestuser' + password = 'test12' + name = 'name' + lastname = 'lastname' + email = 'mail@mail.com' + + response = self.app.post(url('users'), {'username':username, + 'password':password, + 'name':name, + 'active':True, + 'lastname':lastname, + 'email':email}) + + + assert '''created user %s''' % (username) in response.session['flash'][0], 'No flash message about new user' + + new_user = self.sa.query(User).filter(User.username == username).one() + + + assert new_user.username == username, 'wrong info about username' + assert check_password(password, new_user.password) == True , 'wrong info about password' + assert new_user.name == name, 'wrong info about name' + assert new_user.lastname == lastname, 'wrong info about lastname' + assert new_user.email == email, 'wrong info about email' + + + response.follow() + response = response.follow() + assert """edit">newtestuser""" in response.body + + def test_create_err(self): + self.log_user() + username = 'new_user' + password = '' + name = 'name' + lastname = 'lastname' + email = 'errmail.com' + + response = self.app.post(url('users'), {'username':username, + 'password':password, + 'name':name, + 'active':False, + 'lastname':lastname, + 'email':email}) + + assert """Invalid username""" in response.body + assert """Please enter a value""" in response.body + assert """An email address must contain a single @""" in response.body + + def get_user(): + self.sa.query(User).filter(User.username == username).one() + + self.assertRaises(NoResultFound, get_user), 'found user in database' + + def test_new(self): + response = self.app.get(url('new_user')) + + def test_new_as_xml(self): + response = self.app.get(url('formatted_new_user', format='xml')) + + def test_update(self): + response = self.app.put(url('user', id=1)) + + def test_update_browser_fakeout(self): + response = self.app.post(url('user', id=1), params=dict(_method='put')) + + def test_delete(self): + self.log_user() + username = 'newtestuserdeleteme' + password = 'test12' + name = 'name' + lastname = 'lastname' + email = 'todeletemail@mail.com' + + response = self.app.post(url('users'), {'username':username, + 'password':password, + 'name':name, + 'active':True, + 'lastname':lastname, + 'email':email}) + + response = response.follow() + + new_user = self.sa.query(User).filter(User.username == username).one() + response = self.app.delete(url('user', id=new_user.user_id)) + + assert """sucessfully deleted user""" in response.session['flash'][0], 'No info about user deletion' + + + def test_delete_browser_fakeout(self): + response = self.app.post(url('user', id=1), params=dict(_method='delete')) + + def test_show(self): + response = self.app.get(url('user', id=1)) + + def test_show_as_xml(self): + response = self.app.get(url('formatted_user', id=1, format='xml')) + + def test_edit(self): + response = self.app.get(url('edit_user', id=1)) + + def test_edit_as_xml(self): + response = self.app.get(url('formatted_edit_user', id=1, format='xml')) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_branches.py --- a/rhodecode/tests/functional/test_branches.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/tests/functional/test_branches.py Sat Dec 18 14:45:58 2010 +0100 @@ -4,5 +4,15 @@ def test_index(self): self.log_user() - response = self.app.get(url(controller='branches', action='index',repo_name='vcs_test')) + response = self.app.get(url(controller='branches', action='index', repo_name=HG_REPO)) + + assert """default""" % HG_REPO in response.body, 'wrong info about default branch' + assert """git""" % HG_REPO in response.body, 'wrong info about default git' + assert """web""" % HG_REPO in response.body, 'wrong info about default web' + + + + + + # Test response... diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_changelog.py --- a/rhodecode/tests/functional/test_changelog.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/tests/functional/test_changelog.py Sat Dec 18 14:45:58 2010 +0100 @@ -2,29 +2,37 @@ class TestChangelogController(TestController): - def test_index(self): + def test_index_hg(self): self.log_user() - response = self.app.get(url(controller='changelog', action='index', repo_name='vcs_test')) + response = self.app.get(url(controller='changelog', action='index', repo_name=HG_REPO)) + print response.body assert """
""" in response.body, 'wrong info about number of changes' + assert """
commit 154: 5e204e7583b9@2010-08-10 01:18:46
""" in response.body , 'no info on this commit' assert """Small update at simplevcs app""" in response.body, 'missing info about commit message' - assert """0""" in response.body, 'wrong info about removed nodes' - assert """2""" in response.body, 'wrong info about changed nodes' - assert """1""" in response.body, 'wrong info about added nodes' + assert """0""" in response.body, 'wrong info about removed nodes' + assert """2""" in response.body, 'wrong info about changed nodes' + assert """1""" in response.body, 'wrong info about added nodes' #pagination - response = self.app.get(url(controller='changelog', action='index', repo_name='vcs_test'), {'page':1}) - response = self.app.get(url(controller='changelog', action='index', repo_name='vcs_test'), {'page':2}) - response = self.app.get(url(controller='changelog', action='index', repo_name='vcs_test'), {'page':3}) - response = self.app.get(url(controller='changelog', action='index', repo_name='vcs_test'), {'page':4}) - response = self.app.get(url(controller='changelog', action='index', repo_name='vcs_test'), {'page':5}) - response = self.app.get(url(controller='changelog', action='index', repo_name='vcs_test'), {'page':6}) + response = self.app.get(url(controller='changelog', action='index', repo_name=HG_REPO), {'page':1}) + response = self.app.get(url(controller='changelog', action='index', repo_name=HG_REPO), {'page':2}) + response = self.app.get(url(controller='changelog', action='index', repo_name=HG_REPO), {'page':3}) + response = self.app.get(url(controller='changelog', action='index', repo_name=HG_REPO), {'page':4}) + response = self.app.get(url(controller='changelog', action='index', repo_name=HG_REPO), {'page':5}) + response = self.app.get(url(controller='changelog', action='index', repo_name=HG_REPO), {'page':6}) + # Test response after pagination... + print response.body + assert """
commit 64: 46ad32a4f974@2010-04-20 00:33:21
"""in response.body, 'wrong info about commit 64' + assert """1"""in response.body, 'wrong info about number of removed' + assert """13"""in response.body, 'wrong info about number of changes' + assert """20"""in response.body, 'wrong info about number of added' + assert """""" % HG_REPO in response.body, 'wrong info about commit 64 is a merge' - assert """20"""in response.body, 'wrong info about number of removed' - assert """1"""in response.body, 'wrong info about number of changes' - assert """0"""in response.body, 'wrong info about number of added' - assert """
commit 64: 46ad32a4f974@2010-04-20 00:33:21
"""in response.body, 'wrong info about commit 64' + - assert """"""in response.body, 'wrong info about commit 64 is a merge' + #def test_index_git(self): + # self.log_user() + # response = self.app.get(url(controller='changelog', action='index', repo_name=GIT_REPO)) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_changeset.py --- a/rhodecode/tests/functional/test_changeset.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/tests/functional/test_changeset.py Sat Dec 18 14:45:58 2010 +0100 @@ -4,5 +4,5 @@ def test_index(self): response = self.app.get(url(controller='changeset', action='index', - repo_name='vcs_test',revision='tip')) + repo_name=HG_REPO,revision='tip')) # Test response... diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_feed.py --- a/rhodecode/tests/functional/test_feed.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/tests/functional/test_feed.py Sat Dec 18 14:45:58 2010 +0100 @@ -5,11 +5,11 @@ def test_rss(self): self.log_user() response = self.app.get(url(controller='feed', action='rss', - repo_name='vcs_test')) + repo_name=HG_REPO)) # Test response... def test_atom(self): self.log_user() response = self.app.get(url(controller='feed', action='atom', - repo_name='vcs_test')) + repo_name=HG_REPO)) # Test response... \ No newline at end of file diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_files.py --- a/rhodecode/tests/functional/test_files.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/tests/functional/test_files.py Sat Dec 18 14:45:58 2010 +0100 @@ -5,7 +5,186 @@ def test_index(self): self.log_user() response = self.app.get(url(controller='files', action='index', - repo_name='vcs_test', + repo_name=HG_REPO, revision='tip', f_path='/')) # Test response... + assert 'docs' in response.body, 'missing dir' + assert 'tests' in response.body, 'missing dir' + assert 'vcs' in response.body, 'missing dir' + assert '.hgignore' in response.body, 'missing file' + assert 'MANIFEST.in' in response.body, 'missing file' + + + def test_index_revision(self): + self.log_user() + + response = self.app.get(url(controller='files', action='index', + repo_name=HG_REPO, + revision='7ba66bec8d6dbba14a2155be32408c435c5f4492', + f_path='/')) + + + + #Test response... + + assert 'docs' in response.body, 'missing dir' + assert 'tests' in response.body, 'missing dir' + assert 'README.rst' in response.body, 'missing file' + assert '1.1 KiB' in response.body, 'missing size of setup.py' + assert 'text/x-python' in response.body, 'missing mimetype of setup.py' + + + + def test_index_different_branch(self): + self.log_user() + + response = self.app.get(url(controller='files', action='index', + repo_name=HG_REPO, + revision='97e8b885c04894463c51898e14387d80c30ed1ee', + f_path='/')) + + + + assert """branch: git""" in response.body, 'missing or wrong branch info' + + + + def test_index_paging(self): + self.log_user() + + for r in [(73, 'a066b25d5df7016b45a41b7e2a78c33b57adc235'), + (92, 'cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e'), + (109, '75feb4c33e81186c87eac740cee2447330288412'), + (1, '3d8f361e72ab303da48d799ff1ac40d5ac37c67e'), + (0, 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]: + + response = self.app.get(url(controller='files', action='index', + repo_name=HG_REPO, + revision=r[1], + f_path='/')) + + assert """@ r%s:%s""" % (r[0], r[1][:12]) in response.body, 'missing info about current revision' + + def test_file_source(self): + self.log_user() + response = self.app.get(url(controller='files', action='index', + repo_name=HG_REPO, + revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc', + f_path='vcs/nodes.py')) + + #test or history + assert """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" in response.body + + + assert """
"Partially implemented #16. filecontent/commit message/author/node name are safe_unicode now. +In addition some other __str__ are unicode as well +Added test for unicode +Improved test to clone into uniq repository. +removed extra unicode conversion in diff."
""" in response.body + + assert """branch: default""" in response.body, 'missing or wrong branch info' + + def test_file_annotation(self): + self.log_user() + response = self.app.get(url(controller='files', action='annotate', + repo_name=HG_REPO, + revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc', + f_path='vcs/nodes.py')) + + print response.body + assert """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" in response.body, 'missing or wrong history in annotation' + + assert """branch: default""" in response.body, 'missing or wrong branch info' diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_hg.py --- a/rhodecode/tests/functional/test_hg.py Thu Nov 18 21:35:52 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +0,0 @@ -from rhodecode.tests import * - -class TestAdminController(TestController): - - def test_index(self): - self.log_user() - response = self.app.get(url(controller='hg', action='index')) - #if global permission is set - assert 'ADD NEW REPOSITORY' in response.body, 'Wrong main page' - assert 'href="/vcs_test/summary"' in response.body, ' mising repository in list' - # Test response... diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_home.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/tests/functional/test_home.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,11 @@ +from rhodecode.tests import * + +class TestHomeController(TestController): + + def test_index(self): + self.log_user() + response = self.app.get(url(controller='home', action='index')) + #if global permission is set + assert 'ADD NEW REPOSITORY' in response.body, 'Wrong main page' + assert 'href="/%s/summary"' % HG_REPO in response.body, ' mising repository in list' + # Test response... diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_journal.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/tests/functional/test_journal.py Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,7 @@ +from rhodecode.tests import * + +class TestJournalController(TestController): + + def test_index(self): + response = self.app.get(url(controller='journal', action='index')) + # Test response... diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_login.py --- a/rhodecode/tests/functional/test_login.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/tests/functional/test_login.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from rhodecode.tests import * from rhodecode.model.db import User from rhodecode.lib.auth import check_password @@ -17,8 +18,8 @@ assert response.status == '302 Found', 'Wrong response code from login got %s' % response.status assert response.session['rhodecode_user'].username == 'test_admin', 'wrong logged in user' response = response.follow() - assert 'auto description for vcs_test' in response.body - + assert '%s repository' % HG_REPO in response.body + def test_login_regular_ok(self): response = self.app.post(url(controller='login', action='index'), {'username':'test_regular', @@ -27,9 +28,9 @@ assert response.status == '302 Found', 'Wrong response code from login got %s' % response.status assert response.session['rhodecode_user'].username == 'test_regular', 'wrong logged in user' response = response.follow() - assert 'auto description for vcs_test' in response.body + assert '%s repository' % HG_REPO in response.body assert '' not in response.body - + def test_login_ok_came_from(self): test_came_from = '/_admin/users' response = self.app.post(url(controller='login', action='index', came_from=test_came_from), @@ -37,11 +38,11 @@ 'password':'test12'}) assert response.status == '302 Found', 'Wrong response code from came from redirection' response = response.follow() - + assert response.status == '200 OK', 'Wrong response from login page got %s' % response.status assert 'Users administration' in response.body, 'No proper title in response' - - + + def test_login_short_password(self): response = self.app.post(url(controller='login', action='index'), {'username':'error', @@ -55,71 +56,152 @@ {'username':'error', 'password':'test12'}) assert response.status == '200 OK', 'Wrong response from login page' - + assert 'invalid user name' in response.body, 'No error username message in response' assert 'invalid password' in response.body, 'No error password message in response' - - + + #========================================================================== + # REGISTRATIONS + #========================================================================== def test_register(self): response = self.app.get(url(controller='login', action='register')) - assert 'Sign Up to rhodecode' in response.body, 'wrong page for user registration' - + assert 'Sign Up to RhodeCode' in response.body, 'wrong page for user registration' + def test_register_err_same_username(self): response = self.app.post(url(controller='login', action='register'), {'username':'test_admin', - 'password':'test', + 'password':'test12', + 'password_confirmation':'test12', 'email':'goodmail@domain.com', 'name':'test', 'lastname':'test'}) - + + assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status + assert 'This username already exists' in response.body + + def test_register_err_same_email(self): + response = self.app.post(url(controller='login', action='register'), + {'username':'test_admin_0', + 'password':'test12', + 'password_confirmation':'test12', + 'email':'test_admin@mail.com', + 'name':'test', + 'lastname':'test'}) + assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status - assert 'This username already exists' in response.body - + assert 'This e-mail address is already taken' in response.body + + def test_register_err_same_email_case_sensitive(self): + response = self.app.post(url(controller='login', action='register'), + {'username':'test_admin_1', + 'password':'test12', + 'password_confirmation':'test12', + 'email':'TesT_Admin@mail.COM', + 'name':'test', + 'lastname':'test'}) + assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status + assert 'This e-mail address is already taken' in response.body + def test_register_err_wrong_data(self): response = self.app.post(url(controller='login', action='register'), {'username':'xs', - 'password':'', + 'password':'test', + 'password_confirmation':'test', + 'email':'goodmailm', + 'name':'test', + 'lastname':'test'}) + assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status + assert 'An email address must contain a single @' in response.body + assert 'Enter a value 6 characters long or more' in response.body + + + def test_register_err_username(self): + response = self.app.post(url(controller='login', action='register'), + {'username':'error user', + 'password':'test12', + 'password_confirmation':'test12', + 'email':'goodmailm', + 'name':'test', + 'lastname':'test'}) + + print response.body + assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status + assert 'An email address must contain a single @' in response.body + assert 'Username may only contain alphanumeric characters underscores or dashes and must begin with alphanumeric character' in response.body + + def test_register_err_case_sensitive(self): + response = self.app.post(url(controller='login', action='register'), + {'username':'Test_Admin', + 'password':'test12', + 'password_confirmation':'test12', 'email':'goodmailm', 'name':'test', 'lastname':'test'}) - + assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status assert 'An email address must contain a single @' in response.body - assert 'Please enter a value' in response.body - - - + assert 'This username already exists' in response.body + + + + def test_register_special_chars(self): + response = self.app.post(url(controller='login', action='register'), + {'username':'xxxaxn', + 'password':'ąćźżąśśśś', + 'password_confirmation':'ąćźżąśśśś', + 'email':'goodmailm@test.plx', + 'name':'test', + 'lastname':'test'}) + + print response.body + assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status + assert 'Invalid characters in password' in response.body + + + def test_register_password_mismatch(self): + response = self.app.post(url(controller='login', action='register'), + {'username':'xs', + 'password':'123qwe', + 'password_confirmation':'qwe123', + 'email':'goodmailm@test.plxa', + 'name':'test', + 'lastname':'test'}) + + assert response.status == '200 OK', 'Wrong response from register page got %s' % response.status + print response.body + assert 'Password do not match' in response.body + def test_register_ok(self): username = 'test_regular4' password = 'qweqwe' email = 'marcin@test.com' name = 'testname' lastname = 'testlastname' - + response = self.app.post(url(controller='login', action='register'), {'username':username, 'password':password, + 'password_confirmation':password, 'email':email, 'name':name, 'lastname':lastname}) - print response.body - assert response.status == '302 Found', 'Wrong response from register page got %s' % response.status + assert response.status == '302 Found', 'Wrong response from register page got %s' % response.status assert 'You have successfully registered into rhodecode' in response.session['flash'][0], 'No flash message about user registration' - + ret = self.sa.query(User).filter(User.username == 'test_regular4').one() assert ret.username == username , 'field mismatch %s %s' % (ret.username, username) assert check_password(password, ret.password) == True , 'password mismatch' assert ret.email == email , 'field mismatch %s %s' % (ret.email, email) assert ret.name == name , 'field mismatch %s %s' % (ret.name, name) assert ret.lastname == lastname , 'field mismatch %s %s' % (ret.lastname, lastname) - - - def test_forgot_password_wrong_mail(self): + + + def test_forgot_password_wrong_mail(self): response = self.app.post(url(controller='login', action='password_reset'), {'email':'marcin@wrongmail.org', }) - - assert "That e-mail address doesn't exist" in response.body, 'Missing error message about wrong email' - + + assert "This e-mail address doesn't exist" in response.body, 'Missing error message about wrong email' + def test_forgot_password(self): response = self.app.get(url(controller='login', action='password_reset')) assert response.status == '200 OK', 'Wrong response from login page got %s' % response.status @@ -129,19 +211,20 @@ email = 'marcin@python-works.com' name = 'passwd' lastname = 'reset' - + response = self.app.post(url(controller='login', action='register'), {'username':username, 'password':password, + 'password_confirmation':password, 'email':email, 'name':name, - 'lastname':lastname}) + 'lastname':lastname}) #register new user for email test response = self.app.post(url(controller='login', action='password_reset'), {'email':email, }) print response.session['flash'] assert 'You have successfully registered into rhodecode' in response.session['flash'][0], 'No flash message about user registration' assert 'Your new password was sent' in response.session['flash'][1], 'No flash message about password reset' - - - + + + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_permissions.py --- a/rhodecode/tests/functional/test_permissions.py Thu Nov 18 21:35:52 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,43 +0,0 @@ -from rhodecode.tests import * - -class TestPermissionsController(TestController): - - def test_index(self): - response = self.app.get(url('permissions')) - # Test response... - - def test_index_as_xml(self): - response = self.app.get(url('formatted_permissions', format='xml')) - - def test_create(self): - response = self.app.post(url('permissions')) - - def test_new(self): - response = self.app.get(url('new_permission')) - - def test_new_as_xml(self): - response = self.app.get(url('formatted_new_permission', format='xml')) - - def test_update(self): - response = self.app.put(url('permission', id=1)) - - def test_update_browser_fakeout(self): - response = self.app.post(url('permission', id=1), params=dict(_method='put')) - - def test_delete(self): - response = self.app.delete(url('permission', id=1)) - - def test_delete_browser_fakeout(self): - response = self.app.post(url('permission', id=1), params=dict(_method='delete')) - - def test_show(self): - response = self.app.get(url('permission', id=1)) - - def test_show_as_xml(self): - response = self.app.get(url('formatted_permission', id=1, format='xml')) - - def test_edit(self): - response = self.app.get(url('edit_permission', id=1)) - - def test_edit_as_xml(self): - response = self.app.get(url('formatted_edit_permission', id=1, format='xml')) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_repos.py --- a/rhodecode/tests/functional/test_repos.py Thu Nov 18 21:35:52 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,113 +0,0 @@ -from rhodecode.model.db import Repository -from rhodecode.tests import * - -class TestReposController(TestController): - - def test_index(self): - self.log_user() - response = self.app.get(url('repos')) - # Test response... - - def test_index_as_xml(self): - response = self.app.get(url('formatted_repos', format='xml')) - - def test_create(self): - self.log_user() - repo_name = 'vcs_test_new' - description = 'description for newly created repo' - private = False - response = self.app.post(url('repos'), {'repo_name':repo_name, - 'description':description, - 'private':private}) - - print response - - #test if we have a message for that repository - print '-' * 100 - print response.session - assert '''created repository %s''' % (repo_name) in response.session['flash'][0], 'No flash message about new repo' - - #test if the fork was created in the database - new_repo = self.sa.query(Repository).filter(Repository.repo_name == repo_name).one() - - assert new_repo.repo_name == repo_name, 'wrong name of repo name in db' - assert new_repo.description == description, 'wrong description' - - #test if repository is visible in the list ? - response = response.follow() - - assert repo_name in response.body, 'missing new repo from the main repos list' - - - - - def test_new(self): - self.log_user() - response = self.app.get(url('new_repo')) - - def test_new_as_xml(self): - response = self.app.get(url('formatted_new_repo', format='xml')) - - def test_update(self): - response = self.app.put(url('repo', repo_name='vcs_test')) - - def test_update_browser_fakeout(self): - response = self.app.post(url('repo', repo_name='vcs_test'), params=dict(_method='put')) - - def test_delete(self): - self.log_user() - repo_name = 'vcs_test_new_to_delete' - description = 'description for newly created repo' - private = False - response = self.app.post(url('repos'), {'repo_name':repo_name, - 'description':description, - 'private':private}) - - print response - - #test if we have a message for that repository - print '-' * 100 - print response.session - assert '''created repository %s''' % (repo_name) in response.session['flash'][0], 'No flash message about new repo' - - #test if the repo was created in the database - new_repo = self.sa.query(Repository).filter(Repository.repo_name == repo_name).one() - - assert new_repo.repo_name == repo_name, 'wrong name of repo name in db' - assert new_repo.description == description, 'wrong description' - - #test if repository is visible in the list ? - response = response.follow() - - assert repo_name in response.body, 'missing new repo from the main repos list' - - - response = self.app.delete(url('repo', repo_name=repo_name)) - - print '-' * 100 - print response.session - assert '''deleted repository %s''' % (repo_name) in response.session['flash'][0], 'No flash message about delete repo' - - response.follow() - - #check if repo was deleted from db - deleted_repo = self.sa.query(Repository).filter(Repository.repo_name == repo_name).scalar() - - assert deleted_repo is None, 'Deleted repository was found in db' - - - def test_delete_browser_fakeout(self): - response = self.app.post(url('repo', repo_name='vcs_test'), params=dict(_method='delete')) - - def test_show(self): - self.log_user() - response = self.app.get(url('repo', repo_name='vcs_test')) - - def test_show_as_xml(self): - response = self.app.get(url('formatted_repo', repo_name='vcs_test', format='xml')) - - def test_edit(self): - response = self.app.get(url('edit_repo', repo_name='vcs_test')) - - def test_edit_as_xml(self): - response = self.app.get(url('formatted_edit_repo', repo_name='vcs_test', format='xml')) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_search.py --- a/rhodecode/tests/functional/test_search.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/tests/functional/test_search.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,5 +1,4 @@ from rhodecode.tests import * -from rhodecode.lib.indexers import IDX_LOCATION import os from nose.plugins.skip import SkipTest @@ -13,26 +12,25 @@ # Test response... def test_empty_search(self): - - if os.path.isdir(IDX_LOCATION): + if os.path.isdir(self.index_location): raise SkipTest('skipped due to existing index') else: self.log_user() - response = self.app.get(url(controller='search', action='index'), {'q':'vcs_test'}) + response = self.app.get(url(controller='search', action='index'), {'q':HG_REPO}) assert 'There is no index to search in. Please run whoosh indexer' in response.body, 'No error message about empty index' - + def test_normal_search(self): self.log_user() response = self.app.get(url(controller='search', action='index'), {'q':'def repo'}) print response.body assert '10 results' in response.body, 'no message about proper search results' assert 'Permission denied' not in response.body, 'Wrong permissions settings for that repo and user' - - + + def test_repo_search(self): self.log_user() - response = self.app.get(url(controller='search', action='index'), {'q':'repository:vcs_test def test'}) + response = self.app.get(url(controller='search', action='index'), {'q':'repository:%s def test' % HG_REPO}) print response.body assert '4 results' in response.body, 'no message about proper search results' assert 'Permission denied' not in response.body, 'Wrong permissions settings for that repo and user' - + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_settings.py --- a/rhodecode/tests/functional/test_settings.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/tests/functional/test_settings.py Sat Dec 18 14:45:58 2010 +0100 @@ -6,40 +6,38 @@ def test_index(self): self.log_user() response = self.app.get(url(controller='settings', action='index', - repo_name='vcs_test')) + repo_name=HG_REPO)) # Test response... - + def test_fork(self): self.log_user() response = self.app.get(url(controller='settings', action='fork', - repo_name='vcs_test')) - + repo_name=HG_REPO)) + def test_fork_create(self): self.log_user() - fork_name = 'vcs_test_fork' + fork_name = HG_FORK description = 'fork of vcs test' - repo_name = 'vcs_test' + repo_name = HG_REPO response = self.app.post(url(controller='settings', action='fork_create', repo_name=repo_name), {'fork_name':fork_name, + 'repo_type':'hg', 'description':description, 'private':'False'}) - - - print response - + #test if we have a message that fork is ok - assert 'fork %s repository as %s task added' \ + assert 'forked %s repository as %s' \ % (repo_name, fork_name) in response.session['flash'][0], 'No flash message about fork' - + #test if the fork was created in the database fork_repo = self.sa.query(Repository).filter(Repository.repo_name == fork_name).one() - + assert fork_repo.repo_name == fork_name, 'wrong name of repo name in new db fork repo' assert fork_repo.fork.repo_name == repo_name, 'wrong fork parrent' - - + + #test if fork is visible in the list ? response = response.follow() @@ -47,9 +45,6 @@ #check if fork is marked as fork response = self.app.get(url(controller='summary', action='index', repo_name=fork_name)) - - - print response - + assert 'Fork of %s' % repo_name in response.body, 'no message about that this repo is a fork' - + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_shortlog.py --- a/rhodecode/tests/functional/test_shortlog.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/tests/functional/test_shortlog.py Sat Dec 18 14:45:58 2010 +0100 @@ -4,5 +4,5 @@ def test_index(self): self.log_user() - response = self.app.get(url(controller='shortlog', action='index',repo_name='vcs_test')) + response = self.app.get(url(controller='shortlog', action='index',repo_name=HG_REPO)) # Test response... diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_summary.py --- a/rhodecode/tests/functional/test_summary.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/tests/functional/test_summary.py Sat Dec 18 14:45:58 2010 +0100 @@ -4,8 +4,16 @@ def test_index(self): self.log_user() - response = self.app.get(url(controller='summary', action='index', repo_name='vcs_test')) - print response - assert """public""" in response.body - - # Test response... + response = self.app.get(url(controller='summary', action='index', repo_name=HG_REPO)) + + #repo type + assert """Mercurial repository""" in response.body + assert """public repository""" in response.body + + #codes stats + assert """var data = {"Python": 42, "Rst": 11, "Bash": 2, "Makefile": 1, "Batch": 1, "Ini": 1, "Css": 1};""" in response.body, 'wrong info about % of codes stats' + + # clone url... + assert """""" % HG_REPO in response.body + + diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_tags.py --- a/rhodecode/tests/functional/test_tags.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/tests/functional/test_tags.py Sat Dec 18 14:45:58 2010 +0100 @@ -4,5 +4,10 @@ def test_index(self): self.log_user() - response = self.app.get(url(controller='tags', action='index',repo_name='vcs_test')) + response = self.app.get(url(controller='tags', action='index', repo_name=HG_REPO)) + assert """tip""" % HG_REPO, 'wrong info about tip tag' + assert """0.1.4""" % HG_REPO, 'wrong info about 0.1.4 tag' + assert """0.1.3""" % HG_REPO, 'wrong info about 0.1.3 tag' + assert """0.1.2""" % HG_REPO, 'wrong info about 0.1.2 tag' + assert """0.1.1""" % HG_REPO, 'wrong info about 0.1.1 tag' # Test response... diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/functional/test_users.py --- a/rhodecode/tests/functional/test_users.py Thu Nov 18 21:35:52 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,43 +0,0 @@ -from rhodecode.tests import * - -class TestUsersController(TestController): - - def test_index(self): - response = self.app.get(url('users')) - # Test response... - - def test_index_as_xml(self): - response = self.app.get(url('formatted_users', format='xml')) - - def test_create(self): - response = self.app.post(url('users')) - - def test_new(self): - response = self.app.get(url('new_user')) - - def test_new_as_xml(self): - response = self.app.get(url('formatted_new_user', format='xml')) - - def test_update(self): - response = self.app.put(url('user', id=1)) - - def test_update_browser_fakeout(self): - response = self.app.post(url('user', id=1), params=dict(_method='put')) - - def test_delete(self): - response = self.app.delete(url('user', id=1)) - - def test_delete_browser_fakeout(self): - response = self.app.post(url('user', id=1), params=dict(_method='delete')) - - def test_show(self): - response = self.app.get(url('user', id=1)) - - def test_show_as_xml(self): - response = self.app.get(url('formatted_user', id=1, format='xml')) - - def test_edit(self): - response = self.app.get(url('edit_user', id=1)) - - def test_edit_as_xml(self): - response = self.app.get(url('formatted_edit_user', id=1, format='xml')) diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/test_hg_operations.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/tests/test_hg_operations.sh Sat Dec 18 14:45:58 2010 +0100 @@ -0,0 +1,24 @@ +#!/bin/bash +repo=/tmp/vcs_test_hg_clone +repo_name=vcs_test_hg +user=test_admin +password=test12 +echo 'removing repo '$repo +rm -rf '$repo' +hg clone http://$user:$password@127.0.0.1:5000/$repo_name $repo +cd $repo +echo 'some' >> $repo/setup.py && hg ci -m 'ci1' && \ +echo 'some' >> $repo/setup.py && hg ci -m 'ci2' && \ +echo 'some' >> $repo/setup.py && hg ci -m 'ci3' && \ +echo 'some' >> $repo/setup.py && hg ci -m 'ci4' && \ +hg push + +echo 'new file' >> $repo/new_file.py +hg add $repo/new_file.py + +for i in {1..15} +do + echo "line $i" >> $repo/new_file.py && hg ci -m "autocommit $i" +done + +hg push \ No newline at end of file diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/vcs_test.tar.gz Binary file rhodecode/tests/vcs_test.tar.gz has changed diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/tests/vcs_test_hg.tar.gz Binary file rhodecode/tests/vcs_test_hg.tar.gz has changed diff -r 06cfcede13ab -r 3d0661b8aaa4 rhodecode/websetup.py --- a/rhodecode/websetup.py Thu Nov 18 21:35:52 2010 +0100 +++ b/rhodecode/websetup.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,23 +1,51 @@ -"""Setup the rhodecode application""" +# -*- coding: utf-8 -*- +""" + rhodecode.websetup + ~~~~~~~~~~~~~~~~~~ + + Weboperations and setup for rhodecode + + :created_on: Dec 11, 2010 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +import os +import logging + from rhodecode.config.environment import load_environment from rhodecode.lib.db_manage import DbManage -import logging -import os + log = logging.getLogger(__name__) def setup_app(command, conf, vars): """Place any commands to setup rhodecode here""" - dbname = os.path.split(conf['sqlalchemy.db1.url'])[-1] - dbmanage = DbManage(log_sql=True, dbname=dbname, root=conf['here'], - tests=False) + dbconf = conf['sqlalchemy.db1.url'] + dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=conf['here'], tests=False) dbmanage.create_tables(override=True) + dbmanage.set_db_version() dbmanage.config_prompt(None) dbmanage.create_default_user() dbmanage.admin_prompt() dbmanage.create_permissions() dbmanage.populate_default_permissions() - + load_environment(conf.global_conf, conf.local_conf, initial=True) diff -r 06cfcede13ab -r 3d0661b8aaa4 setup.cfg diff -r 06cfcede13ab -r 3d0661b8aaa4 setup.py --- a/setup.py Thu Nov 18 21:35:52 2010 +0100 +++ b/setup.py Sat Dec 18 14:45:58 2010 +0100 @@ -1,27 +1,28 @@ -from rhodecode import get_version import sys py_version = sys.version_info +from rhodecode import get_version + requirements = [ - "Pylons>=1.0.0", + "Pylons==1.0.0", "SQLAlchemy==0.6.5", - "Mako>=0.3.2", - "vcs==0.1.8", - "pygments>=1.3.0", - "mercurial==1.6.4", - "whoosh==1.2.5", - "celery==2.1.3", + "Mako==0.3.6", + "vcs==0.1.10", + "pygments==1.3.1", + "mercurial==1.7.2", + "whoosh==1.3.4", + "celery==2.1.4", "py-bcrypt", "babel", ] -classifiers = ["Development Status :: 5 - Production/Stable", - 'Environment :: Web Environment', - 'Framework :: Pylons', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', ] +classifiers = ['Development Status :: 5 - Production/Stable', + 'Environment :: Web Environment', + 'Framework :: Pylons', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', ] if sys.version_info < (2, 6): requirements.append("simplejson") @@ -34,14 +35,19 @@ #additional files that goes into package itself package_data = {'rhodecode': ['i18n/*/LC_MESSAGES/*.mo', ], } -description = 'Mercurial repository serving and browsing app' +description = ('Mercurial repository browser/management with ' + 'build in push/pull server and full text search') #long description try: readme_file = 'README.rst' - long_description = open(readme_file).read() + changelog_file = 'docs/changelog.rst' + long_description = open(readme_file).read() + '/n/n' + \ + open(changelog_file).read() + except IOError, err: sys.stderr.write("[WARNING] Cannot find file specified as " - "long_description (%s)\n skipping that file" % readme_file) + "long_description (%s)\n or changelog (%s) skipping that file" \ + % (readme_file, changelog_file)) long_description = description @@ -59,7 +65,7 @@ version=get_version(), description=description, long_description=long_description, - keywords='rhodiumcode mercurial web hgwebdir replacement serving hgweb rhodecode', + keywords='rhodiumcode mercurial web hgwebdir gitweb git replacement serving hgweb rhodecode', license='BSD', author='Marcin Kuzminski', author_email='marcin@python-works.com', @@ -84,5 +90,14 @@ [paste.app_install] main = pylons.util:PylonsInstaller + + [paste.global_paster_command] + make-index = rhodecode.lib.indexers:MakeIndex + upgrade-db = rhodecode.lib.dbmigrate:UpgradeDb + celeryd=rhodecode.lib.celerypylons.commands:CeleryDaemonCommand + celerybeat=rhodecode.lib.celerypylons.commands:CeleryBeatCommand + camqadm=rhodecode.lib.celerypylons.commands:CAMQPAdminCommand + celeryev=rhodecode.lib.celerypylons.commands:CeleryEventCommand + """, ) diff -r 06cfcede13ab -r 3d0661b8aaa4 test.ini --- a/test.ini Thu Nov 18 21:35:52 2010 +0100 +++ b/test.ini Sat Dec 18 14:45:58 2010 +0100 @@ -43,6 +43,35 @@ static_files = true lang=en cache_dir = %(here)s/data +index_dir = /tmp/index +cut_off_limit = 256000 + +#################################### +### CELERY CONFIG #### +#################################### +use_celery = false +broker.host = localhost +broker.vhost = rabbitmqhost +broker.port = 5672 +broker.user = rabbitmq +broker.password = qweqwe + +celery.imports = rhodecode.lib.celerylib.tasks + +celery.result.backend = amqp +celery.result.dburi = amqp:// +celery.result.serialier = json + +#celery.send.task.error.emails = true +#celery.amqp.task.result.expires = 18000 + +celeryd.concurrency = 2 +#celeryd.log.file = celeryd.log +celeryd.log.level = debug +celeryd.max.tasks.per.child = 3 + +#tasks will never be sent to the queue, but executed locally instead. +celery.always.eager = false #################################### ### BEAKER CACHE ####
${_('date')}${_('revision')}${_('name')}${_('date')}${_('name')}${_('author')}${_('revision')} ${_('links')}
${h.age(tag[1]._ctx.date())}r${tag[1].revision}:${tag[1].short_id} - - ${h.link_to(tag[0], - h.url('changeset_home',repo_name=c.repo_name,revision=tag[1].short_id))} - -
+ ${tag[1].date} + + + ${h.link_to(tag[0], + h.url('changeset_home',repo_name=c.repo_name,revision=tag[1].raw_id))} + + ${h.person(tag[1].author)}r${tag[1].revision}:${h.short_id(tag[1].raw_id)} - ${h.link_to(_('changeset'),h.url('changeset_home',repo_name=c.repo_name,revision=tag[1].short_id))} + ${h.link_to(_('changeset'),h.url('changeset_home',repo_name=c.repo_name,revision=tag[1].raw_id))} | - ${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=tag[1].short_id))} + ${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=tag[1].raw_id))}