diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -1,6 +1,7 @@ syntax: glob *.pyc *.swp +*.sqlite *.egg-info *.egg @@ -12,6 +13,9 @@ syntax: regexp ^\.settings$ ^\.project$ ^\.pydevproject$ +^\.coverage$ ^rhodecode\.db$ ^test\.db$ -^repositories\.config$ +^RhodeCode\.egg-info$ +^rc\.ini$ +^fabfile.py diff --git a/CONTRIBUTORS b/CONTRIBUTORS --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -13,3 +13,6 @@ List of contributors to RhodeCode projec Ankit Solanki Liad Shani Les Peabody + Jonas Oberschweiber + Matt Zuba + Aras Pranckevicius \ No newline at end of file diff --git a/README.rst b/README.rst --- a/README.rst +++ b/README.rst @@ -1,22 +1,40 @@ -================================================= -Welcome to RhodeCode (RhodiumCode) documentation! -================================================= +========= +RhodeCode +========= -``RhodeCode`` is a Pylons framework based Mercurial repository -browser/management tool with a built in push/pull server and full text search. +About +----- + +``RhodeCode`` is a fast and powerful management tool for Mercurial_ and GIT_ +with a built in push/pull server and full text search and code-review. It works on http/https and has a built in permission/authentication system with -the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also supports -simple API so it's easy integrable with existing systems. +the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also provides +simple API so it's easy integrable with existing external systems. RhodeCode is similar in some respects to github or bitbucket_, -however RhodeCode can be run as standalone hosted application on your own server. +however RhodeCode can be run as standalone hosted application on your own server. It is open source and donation ware and focuses more on providing a customized, -self administered interface for Mercurial(and soon GIT) repositories. +self administered interface for Mercurial and GIT repositories. RhodeCode is powered by a vcs_ library that Lukasz Balcerzak and I created to handle multiple different version control systems. RhodeCode uses `Semantic Versioning `_ +Installation +------------ +Stable releases of RhodeCode are best installed via:: + + easy_install rhodecode + +Or:: + + pip install rhodecode + +Detailed instructions and links may be found on the Installation page. + +Please visit http://packages.python.org/RhodeCode/installation.html for +more details + RhodeCode demo -------------- @@ -45,16 +63,11 @@ Sources at github_ https://github.com/marcinkuzminski/rhodecode -Installation ------------- - -Please visit http://packages.python.org/RhodeCode/installation.html - RhodeCode Features ------------------ -- Has it's own middleware to handle mercurial_ protocol requests. +- Has its own middleware to handle mercurial_ protocol requests. Each request can be logged and authenticated. - Runs on threads unlike hgweb. You can make multiple pulls/pushes simultaneous. Supports http/https and LDAP @@ -75,6 +88,9 @@ RhodeCode Features - Server side forks. It is possible to fork a project and modify it freely without breaking the main repository. You can even write Your own hooks and install them +- code review with notification system, inline commenting, all parsed using + rst syntax +- rst and markdown README support for repositories - Full text search powered by Whoosh on the source files, and file names. Build in indexing daemons, with optional incremental index build (no external search servers required all in one application) @@ -88,20 +104,14 @@ RhodeCode Features location - Based on pylons / sqlalchemy / sqlite / whoosh / vcs - -.. include:: ./docs/screenshots.rst - Incoming / Plans ---------------- - Finer granular permissions per branch, repo group or subrepo - pull requests and web based merges -- notification and message system +- per line file history - SSH based authentication with server side key management -- Code review (probably based on hg-review) -- Full git_ support, with push/pull server (currently in beta tests) -- Redmine and other bugtrackers integration - Commit based built in wiki system - More statistics and graph (global annotation + some more statistics) - Other advancements as development continues (or you can of course make @@ -113,21 +123,35 @@ License ``RhodeCode`` is released under the GPLv3 license. -Mailing group Q&A ------------------ +Getting help +------------ -Join the `Google group `_ +Listed bellow are various support resources that should help. -Open an issue at `issue tracker `_ +.. note:: + + Please try to read the documentation before posting any issues + +- Join the `Google group `_ and ask + any questions. -Join #rhodecode on FreeNode (irc.freenode.net) -or use http://webchat.freenode.net/?channels=rhodecode for web access to irc. +- 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. + +- You can also follow me on twitter @marcinkuzminski where i often post some + news about RhodeCode + Online documentation -------------------- Online documentation for the current version of RhodeCode is available at -http://packages.python.org/RhodeCode/. + - http://packages.python.org/RhodeCode/ + - http://rhodecode.readthedocs.org/en/latest/index.html + You may also build the documentation for yourself - go into ``docs/`` and run:: make html diff --git a/development.ini b/development.ini --- a/development.ini +++ b/development.ini @@ -17,6 +17,7 @@ pdebug = false #error_email_from = paste_error@localhost #app_email_from = rhodecode-noreply@localhost #error_message = +#email_prefix = [RhodeCode] #smtp_server = mail.server.com #smtp_username = @@ -32,7 +33,7 @@ pdebug = false threadpool_workers = 5 ##max request before thread respawn -threadpool_max_requests = 6 +threadpool_max_requests = 10 ##option to use threads of process use_threadpool = true @@ -45,14 +46,52 @@ port = 5000 use = egg:rhodecode full_stack = true static_files = true -lang=en +lang = en cache_dir = %(here)s/data index_dir = %(here)s/data/index -app_instance_uuid = develop +app_instance_uuid = rc-develop cut_off_limit = 256000 force_https = false commit_parse_limit = 25 use_gravatar = true +container_auth_enabled = false +proxypass_auth_enabled = false +default_encoding = utf8 + +## overwrite schema of clone url +## available vars: +## scheme - http/https +## user - current user +## pass - password +## netloc - network location +## path - usually repo_name + +#clone_uri = {scheme}://{user}{pass}{netloc}{path} + +## issue tracking mapping for commits messages +## comment out issue_pat, issue_server, issue_prefix to enable + +## pattern to get the issues from commit messages +## default one used here is # with a regex passive group for `#` +## {id} will be all groups matched from this pattern + +issue_pat = (?:\s*#)(\d+) + +## server url to the issue, each {id} will be replaced with match +## fetched from the regex and {repo} is replaced with repository name + +issue_server_link = https://myissueserver.com/{repo}/issue/{id} + +## prefix to add to link to indicate it's an url +## #314 will be replaced by + +issue_prefix = # + +## instance-id prefix +## a prefix key for this instance used for cache invalidation when running +## multiple instances of rhodecode, make sure it's globally unique for +## all running rhodecode instances. Leave empty if you don't use it +instance_id = #################################### ### CELERY CONFIG #### @@ -91,21 +130,27 @@ beaker.cache.regions=super_short_term,sh beaker.cache.super_short_term.type=memory beaker.cache.super_short_term.expire=10 +beaker.cache.super_short_term.key_length = 256 beaker.cache.short_term.type=memory beaker.cache.short_term.expire=60 +beaker.cache.short_term.key_length = 256 beaker.cache.long_term.type=memory beaker.cache.long_term.expire=36000 +beaker.cache.long_term.key_length = 256 beaker.cache.sql_cache_short.type=memory beaker.cache.sql_cache_short.expire=10 +beaker.cache.sql_cache_short.key_length = 256 beaker.cache.sql_cache_med.type=memory beaker.cache.sql_cache_med.expire=360 +beaker.cache.sql_cache_med.key_length = 256 beaker.cache.sql_cache_long.type=file beaker.cache.sql_cache_long.expire=3600 +beaker.cache.sql_cache_long.key_length = 256 #################################### ### BEAKER SESSION #### @@ -113,12 +158,26 @@ beaker.cache.sql_cache_long.expire=3600 ## Type of storage used for the session, current types are ## dbm, file, memcached, database, and memory. ## The storage uses the Container API -##that is also used by the cache system. -beaker.session.type = file +## that is also used by the cache system. + +## db session example + +#beaker.session.type = ext:database +#beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode +#beaker.session.table_name = db_session +## encrypted cookie session, good for many instances +#beaker.session.type = cookie + +beaker.session.type = file beaker.session.key = rhodecode -beaker.session.secret = g654dcno0-9873jhgfreyu +#beaker.session.encrypt_key = g654dcno0-9873jhgfreyu +#beaker.session.validate_key = 9712sds2212c--zxc123 beaker.session.timeout = 36000 +beaker.session.httponly = true + +## uncomment for https secure cookie +beaker.session.secure = false ##auto save the session to not to use .save() beaker.session.auto = False @@ -126,7 +185,7 @@ beaker.session.auto = False ##true exire at browser close #beaker.session.cookie_expires = 3600 - + ################################################################################ ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ## ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ## diff --git a/docs/api/api.rst b/docs/api/api.rst --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -26,16 +26,18 @@ API ACCESS All clients are required to send JSON-RPC spec JSON data:: - { + { + "id:, "api_key":"", "method":"", "args":{"":""} } Example call for autopulling remotes repos using curl:: - curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}' + curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"id":1,"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}' Simply provide + - *id* A value of any type, which is used to match the response with the request that it is replying to. - *api_key* for access and permission validation. - *method* is name of method to call - *args* is an key:value list of arguments to pass to method @@ -47,7 +49,8 @@ Simply provide RhodeCode API will return always a JSON-RPC response:: - { + { + "id":, "result": "", "error": null } @@ -72,21 +75,55 @@ INPUT:: api_key : "" method : "pull" args : { - "repo" : "" + "repo_name" : "" } OUTPUT:: - result : "Pulled from " + result : "Pulled from " error : null +get_user +-------- + +Get's an user by username or user_id, Returns empty result if user is not found. +This command can be executed only using api_key belonging to user with admin +rights. + + +INPUT:: + + api_key : "" + method : "get_user" + args : { + "userid" : "" + } + +OUTPUT:: + + result: None if user does not exist or + { + "id" : "", + "username" : "", + "firstname": "", + "lastname" : "", + "email" : "", + "active" : "", + "admin" :  "", + "ldap_dn" : "" + } + + error: null + + get_users --------- Lists all existing users. This command can be executed only using api_key belonging to user with admin rights. + INPUT:: api_key : "" @@ -104,18 +141,20 @@ OUTPUT:: "email" : "", "active" : "", "admin" :  "", - "ldap" : "" + "ldap_dn" : "" }, … ] error: null + create_user ----------- -Creates new user or updates current one if such user exists. This command can +Creates new user. This command can be executed only using api_key belonging to user with admin rights. + INPUT:: api_key : "" @@ -123,9 +162,9 @@ INPUT:: args : { "username" : "", "password" : "", - "firstname" : "", - "lastname" : "", - "email" : "" + "email" : "", + "firstname" : " = None", + "lastname" : " = None", "active" : " = True", "admin" : " = False", "ldap_dn" : " = None" @@ -134,15 +173,88 @@ INPUT:: OUTPUT:: result: { + "id" : "", "msg" : "created new user " } error: null + +update_user +----------- + +updates current one if such user exists. This command can +be executed only using api_key belonging to user with admin rights. + + +INPUT:: + + api_key : "" + method : "update_user" + args : { + "userid" : "", + "username" : "", + "password" : "", + "email" : "", + "firstname" : "", + "lastname" : "", + "active" : "", + "admin" : "", + "ldap_dn" : "" + } + +OUTPUT:: + + result: { + "id" : "", + "msg" : "updated user " + } + error: null + + +get_users_group +--------------- + +Gets an existing users group. This command can be executed only using api_key +belonging to user with admin rights. + + +INPUT:: + + api_key : "" + method : "get_users_group" + args : { + "group_name" : "" + } + +OUTPUT:: + + result : None if group not exist + { + "id" : "", + "group_name" : "", + "active": "", + "members" : [ + { "id" : "", + "username" : "", + "firstname": "", + "lastname" : "", + "email" : "", + "active" : "", + "admin" :  "", + "ldap" : "" + }, + … + ] + } + error : null + + get_users_groups ---------------- -Lists all existing users groups. This command can be executed only using api_key -belonging to user with admin rights. +Lists all existing users groups. This command can be executed only using +api_key belonging to user with admin rights. + INPUT:: @@ -154,9 +266,9 @@ OUTPUT:: result : [ { - "id" : "", - "name" : "", - "active": "", + "id" : "", + "group_name" : "", + "active": "", "members" : [ { "id" : "", @@ -174,41 +286,6 @@ OUTPUT:: ] error : null -get_users_group ---------------- - -Gets an existing users group. This command can be executed only using api_key -belonging to user with admin rights. - -INPUT:: - - api_key : "" - method : "get_users_group" - args : { - "group_name" : "" - } - -OUTPUT:: - - result : None if group not exist - { - "id" : "", - "name" : "", - "active": "", - "members" : [ - { "id" : "", - "username" : "", - "firstname": "", - "lastname" : "", - "email" : "", - "active" : "", - "admin" :  "", - "ldap" : "" - }, - … - ] - } - error : null create_users_group ------------------ @@ -216,12 +293,13 @@ create_users_group Creates new users group. This command can be executed only using api_key belonging to user with admin rights + INPUT:: api_key : "" method : "create_users_group" args: { - "name": "", + "group_name": "", "active":" = True" } @@ -229,39 +307,120 @@ OUTPUT:: result: { "id": "", - "msg": "created new users group " + "msg": "created new users group " } error: null + add_user_to_users_group ----------------------- -Adds a user to a users group. This command can be executed only using api_key +Adds a user to a users group. If user exists in that group success will be +`false`. This command can be executed only using api_key belonging to user with admin rights + INPUT:: api_key : "" method : "add_user_users_group" args: { "group_name" : "", - "user_name" : "" + "username" : "" } OUTPUT:: result: { "id": "", - "msg": "created new users group member" + "success": True|False # depends on if member is in group + "msg": "added member to users group | + User is already in that group" + } + error: null + + +remove_user_from_users_group +---------------------------- + +Removes a user from a users group. If user is not in given group success will +be `false`. This command can be executed only +using api_key belonging to user with admin rights + + +INPUT:: + + api_key : "" + method : "remove_user_from_users_group" + args: { + "group_name" : "", + "username" : "" + } + +OUTPUT:: + + result: { + "success": True|False, # depends on if member is in group + "msg": "removed member from users group | + User wasn't in group" } error: null + +get_repo +-------- + +Gets an existing repository by it's name or repository_id. This command can +be executed only using api_key belonging to user with admin rights. + + +INPUT:: + + api_key : "" + method : "get_repo" + args: { + "repoid" : "" + } + +OUTPUT:: + + result: None if repository does not exist or + { + "id" : "", + "repo_name" : "" + "type" : "", + "description" : "", + "members" : [ + { "id" : "", + "username" : "", + "firstname": "", + "lastname" : "", + "email" : "", + "active" : "", + "admin" :  "", + "ldap" : "", + "permission" : "repository.(read|write|admin)" + }, + … + { + "id" : "", + "name" : "", + "active": "", + "permission" : "repository.(read|write|admin)" + }, + … + ] + } + error: null + + get_repos --------- Lists all existing repositories. This command can be executed only using api_key belonging to user with admin rights + INPUT:: api_key : "" @@ -273,7 +432,7 @@ OUTPUT:: result: [ { "id" : "", - "name" : "" + "repo_name" : "" "type" : "", "description" : "" }, @@ -281,51 +440,39 @@ OUTPUT:: ] error: null -get_repo --------- + +get_repo_nodes +-------------- -Gets an existing repository. This command can be executed only using api_key -belonging to user with admin rights +returns a list of nodes and it's children in a flat list for a given path +at given revision. It's possible to specify ret_type to show only `files` or +`dirs`. This command can be executed only using api_key belonging to user +with admin rights + INPUT:: api_key : "" - method : "get_repo" + method : "get_repo_nodes" args: { - "name" : "" + "repo_name" : "", + "revision" : "", + "root_path" : "", + "ret_type" : "" = 'all' } OUTPUT:: - result: None if repository not exist - { - "id" : "", + result: [ + { "name" : "" "type" : "", - "description" : "", - "members" : [ - { "id" : "", - "username" : "", - "firstname": "", - "lastname" : "", - "email" : "", - "active" : "", - "admin" :  "", - "ldap" : "", - "permission" : "repository.(read|write|admin)" - }, - … - { - "id" : "", - "name" : "", - "active": "", - "permission" : "repository.(read|write|admin)" - }, - … - ] - } + }, + … + ] error: null + create_repo ----------- @@ -335,58 +482,146 @@ If repository name contains "/", all nee For example "foo/bar/baz" will create groups "foo", "bar" (with "foo" as parent), and create "baz" repository with "bar" as group. + INPUT:: api_key : "" method : "create_repo" args: { - "name" : "", + "repo_name" : "", "owner_name" : "", "description" : " = ''", "repo_type" : " = 'hg'", - "private" : " = False" + "private" : " = False", + "clone_uri" : " = None", } OUTPUT:: - result: None + result: { + "id": "", + "msg": "Created new repository ", + } error: null -add_user_to_repo ----------------- + +delete_repo +----------- + +Deletes a repository. This command can be executed only using api_key +belonging to user with admin rights. + + +INPUT:: + + api_key : "" + method : "delete_repo" + args: { + "repo_name" : "", + } -Add a user to a repository. This command can be executed only using api_key -belonging to user with admin rights. -If "perm" is None, user will be removed from the repository. +OUTPUT:: + + result: { + "msg": "Deleted repository ", + } + error: null + + +grant_user_permission +--------------------- + +Grant permission for user on given repository, or update existing one +if found. This command can be executed only using api_key belonging to user +with admin rights. + INPUT:: api_key : "" - method : "add_user_to_repo" + method : "grant_user_permission" args: { "repo_name" : "", - "user_name" : "", - "perm" : "(None|repository.(read|write|admin))", + "username" : "", + "perm" : "(repository.(none|read|write|admin))", + } + +OUTPUT:: + + result: { + "msg" : "Granted perm: for user: in repo: " + } + error: null + + +revoke_user_permission +---------------------- + +Revoke permission for user on given repository. This command can be executed +only using api_key belonging to user with admin rights. + + +INPUT:: + + api_key : "" + method : "revoke_user_permission" + args: { + "repo_name" : "", + "username" : "", } OUTPUT:: - result: None + result: { + "msg" : "Revoked perm for user: in repo: " + } error: null -add_users_group_to_repo ------------------------ + +grant_users_group_permission +---------------------------- -Add a users group to a repository. This command can be executed only using -api_key belonging to user with admin rights. If "perm" is None, group will -be removed from the repository. +Grant permission for users group on given repository, or update +existing one if found. This command can be executed only using +api_key belonging to user with admin rights. + INPUT:: api_key : "" - method : "add_users_group_to_repo" + method : "grant_users_group_permission" + args: { + "repo_name" : "", + "group_name" : "", + "perm" : "(repository.(none|read|write|admin))", + } + +OUTPUT:: + + result: { + "msg" : "Granted perm: for group: in repo: " + } + error: null + + +revoke_users_group_permission +----------------------------- + +Revoke permission for users group on given repository.This command can be +executed only using api_key belonging to user with admin rights. + +INPUT:: + + api_key : "" + method : "revoke_users_group_permission" args: { "repo_name" : "", - "group_name" : "", - "perm" : "(None|repository.(read|write|admin))", - } \ No newline at end of file + "users_group" : "", + } + +OUTPUT:: + + result: { + "msg" : "Revoked perm for group: in repo: " + } + error: null \ No newline at end of file diff --git a/docs/api/index.rst b/docs/api/index.rst deleted file mode 100644 --- a/docs/api/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. _indexapi: - -API Reference -============= - -.. toctree:: - :maxdepth: 3 - - models - api \ No newline at end of file diff --git a/docs/api/models.rst b/docs/api/models.rst --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -6,14 +6,29 @@ The :mod:`models` Module .. automodule:: rhodecode.model :members: +.. automodule:: rhodecode.model.comment + :members: + +.. automodule:: rhodecode.model.notification + :members: + .. automodule:: rhodecode.model.permission :members: - + +.. automodule:: rhodecode.model.repo_permission + :members: + .. automodule:: rhodecode.model.repo :members: +.. automodule:: rhodecode.model.repos_group + :members: + .. automodule:: rhodecode.model.scm :members: - + .. automodule:: rhodecode.model.user :members: + +.. automodule:: rhodecode.model.users_group + :members: \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,14 +4,73 @@ Changelog ========= -1.2.5 (**2012-01-28**) -====================== +1.3.0 (**2012-02-XX**) +---------------------- + +:status: in-progress +:branch: beta news ----- +++++ + +- code review, inspired by github code-comments +- #215 rst and markdown README files support +- #252 Container-based and proxy pass-through authentication support +- #44 branch browser. Filtering of changelog by branches +- mercurial bookmarks support +- new hover top menu, optimized to add maximum size for important views +- configurable clone url template with possibility to specify protocol like + ssh:// or http:// and also manually alter other parts of clone_url. +- enabled largefiles extension by default +- optimized summary file pages and saved a lot of unused space in them +- #239 option to manually mark repository as fork +- #320 mapping of commit authors to RhodeCode users +- #304 hashes are displayed using monospace font +- diff configuration, toggle white lines and context lines +- #307 configurable diffs, whitespace toggle, increasing context lines +- sorting on branches, tags and bookmarks using YUI datatable +- improved file filter on files page +- implements #330 api method for listing nodes ar particular revision +- #73 added linking issues in commit messages to chosen issue tracker url + based on user defined regular expression +- added linking of changesets in commit messages +- new compact changelog with expandable commit messages +- firstname and lastname are optional in user creation +- #348 added post-create repository hook +- #212 global encoding settings is now configurable from .ini files +- #227 added repository groups permissions +- markdown gets codehilite extensions +- new API methods, delete_repositories, grante/revoke permissions for groups + and repos + + +fixes ++++++ + +- rewrote dbsession management for atomic operations, and better error handling +- fixed sorting of repo tables +- #326 escape of special html entities in diffs +- normalized user_name => username in api attributes +- fixes #298 ldap created users with mixed case emails created conflicts + on saving a form +- fixes issue when owner of a repo couldn't revoke permissions for users + and groups +- fixes #271 rare JSON serialization problem with statistics +- fixes #337 missing validation check for conflicting names of a group with a + repositories group +- #340 fixed session problem for mysql and celery tasks +- fixed #331 RhodeCode mangles repository names if the a repository group + contains the "full path" to the repositories +- #355 RhodeCode doesn't store encrypted LDAP passwords + +1.2.5 (**2012-01-28**) +---------------------- + +news +++++ fixes ------ ++++++ - #340 Celery complains about MySQL server gone away, added session cleanup for celery tasks @@ -24,10 +83,10 @@ fixes forking on windows impossible 1.2.4 (**2012-01-19**) -====================== +---------------------- news ----- +++++ - RhodeCode is bundled with mercurial series 2.0.X by default, with full support to largefiles extension. Enabled by default in new installations @@ -35,7 +94,7 @@ news - added requires.txt file with requirements fixes ------ ++++++ - fixes db session issues with celery when emailing admins - #331 RhodeCode mangles repository names if the a repository group @@ -52,10 +111,10 @@ fixes - #316 fixes issues with web description in hgrc files 1.2.3 (**2011-11-02**) -====================== +---------------------- news ----- +++++ - added option to manage repos group for non admin users - added following API methods for get_users, create_user, get_users_groups, @@ -67,24 +126,23 @@ news administrator users, and global config email. fixes ------ ++++++ - added option for passing auth method for smtp mailer - #276 issue with adding a single user with id>10 to usergroups - #277 fixes windows LDAP settings in which missing values breaks the ldap auth - #288 fixes managing of repos in a group for non admin user - 1.2.2 (**2011-10-17**) -====================== +---------------------- news ----- +++++ - #226 repo groups are available by path instead of numerical id fixes ------ ++++++ - #259 Groups with the same name but with different parent group - #260 Put repo in group, then move group to another group -> repo becomes unavailable @@ -98,27 +156,25 @@ fixes - fixes #248 cannot edit repos inside a group on windows - fixes #219 forking problems on windows - 1.2.1 (**2011-10-08**) -====================== +---------------------- news ----- +++++ fixes ------ ++++++ - fixed problems with basic auth and push problems - gui fixes - fixed logger - 1.2.0 (**2011-10-07**) -====================== +---------------------- news ----- +++++ - implemented #47 repository groups - implemented #89 Can setup google analytics code from settings menu @@ -158,7 +214,7 @@ news - Implemented advanced hook management fixes ------ ++++++ - fixed file browser bug, when switching into given form revision the url was not changing @@ -185,18 +241,17 @@ fixes - fixes #218 os.kill patch for windows was missing sig param - improved rendering of dag (they are not trimmed anymore when number of heads exceeds 5) - - + 1.1.8 (**2011-04-12**) -====================== +---------------------- news ----- +++++ - improved windows support fixes ------ ++++++ - fixed #140 freeze of python dateutil library, since new version is python2.x incompatible @@ -219,40 +274,40 @@ fixes 1.1.7 (**2011-03-23**) -====================== +---------------------- news ----- +++++ fixes ------ ++++++ - fixed (again) #136 installation support for FreeBSD 1.1.6 (**2011-03-21**) -====================== +---------------------- news ----- +++++ fixes ------ ++++++ - fixed #136 installation support for FreeBSD - RhodeCode will check for python version during installation 1.1.5 (**2011-03-17**) -====================== +---------------------- news ----- +++++ - basic windows support, by exchanging pybcrypt into sha256 for windows only highly inspired by idea of mantis406 fixes ------ ++++++ - fixed sorting by author in main page - fixed crashes with diffs on binary files @@ -264,13 +319,13 @@ fixes - cleaned out docs, big thanks to Jason Harris 1.1.4 (**2011-02-19**) -====================== +---------------------- news ----- +++++ fixes ------ ++++++ - fixed formencode import problem on settings page, that caused server crash when that page was accessed as first after server start @@ -278,17 +333,17 @@ fixes - fixed option to access repository just by entering http://server/ 1.1.3 (**2011-02-16**) -====================== +---------------------- news ----- +++++ - implemented #102 allowing the '.' character in username - added option to access repository just by entering http://server/ - celery task ignores result for better performance fixes ------ ++++++ - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to apollo13 and Johan Walles @@ -304,31 +359,31 @@ fixes - fixed static files paths links to use of url() method 1.1.2 (**2011-01-12**) -====================== +---------------------- news ----- +++++ fixes ------ ++++++ - fixes #98 protection against float division of percentage stats - fixed graph bug - forced webhelpers version since it was making troubles during installation 1.1.1 (**2011-01-06**) -====================== +---------------------- news ----- +++++ - added force https option into ini files for easier https usage (no need to set server headers with this options) - small css updates fixes ------ ++++++ - fixed #96 redirect loop on files view on repositories without changesets - fixed #97 unicode string passed into server header in special cases (mod_wsgi) @@ -337,16 +392,16 @@ fixes - fixed #92 whoosh indexer is more error proof 1.1.0 (**2010-12-18**) -====================== +---------------------- 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 +- performance upgrade for cached repos list - each repository has its own cache that's invalidated when needed. - performance upgrades on repositories with large amount of commits (20K+) - main page quick filter for filtering repositories @@ -365,7 +420,7 @@ news - 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 @@ -385,16 +440,16 @@ fixes 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 @@ -402,15 +457,15 @@ fixes 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 @@ -418,7 +473,7 @@ fixes 1.0.0 (**2010-11-02**) -====================== +---------------------- - security bugfix simplehg wasn't checking for permissions on commands other than pull or push. @@ -428,7 +483,7 @@ 1.0.0 (**2010-11-02**) - permissions cached queries 1.0.0rc4 (**2010-10-12**) -========================== +-------------------------- - fixed python2.5 missing simplejson imports (thanks to Jens Bäckman) - removed cache_manager settings from sqlalchemy meta @@ -438,12 +493,12 @@ 1.0.0rc4 (**2010-10-12**) 1.0.0rc3 (**2010-10-11**) -========================= +------------------------- - fixed i18n during installation. 1.0.0rc2 (**2010-10-11**) -========================= +------------------------- - Disabled dirsize in file browser, it's causing nasty bug when dir renames occure. After vcs is fixed it'll be put back again. diff --git a/docs/images/.img b/docs/images/.img new file mode 100644 diff --git a/docs/images/screenshot1_main_page.png b/docs/images/screenshot1_main_page.png deleted file mode 100644 index 033cca6b9e97054700fe9e6c0e92fa724ebd5f1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@ > + ProxyPass http://127.0.0.1:5000/ + ProxyPassReverse http://127.0.0.1:5000/ + SetEnvIf X-Url-Scheme https HTTPS=1 + + AuthType Basic + AuthName "RhodeCode authentication" + AuthUserFile /home/web/rhodecode/.htpasswd + require valid-user + + RequestHeader unset X-Forwarded-User + + RewriteEngine On + RewriteCond %{LA-U:REMOTE_USER} (.+) + RewriteRule .* - [E=RU:%1] + RequestHeader set X-Forwarded-User %{RU}e + + +In order for RhodeCode to start using the forwarded username, you should set +the following in the [app:main] section of your .ini file:: + + proxypass_auth_enabled = true + +.. note:: + If you enable proxy pass-through authentication, make sure your server is + only accessible through the proxy. Otherwise, any client would be able to + forge the authentication header and could effectively become authenticated + using any account of their liking. + +Integration with Issue trackers +------------------------------- + +RhodeCode provides a simple integration with issue trackers. It's possible +to define a regular expression that will fetch issue id stored in commit +messages and replace that with an url to this issue. To enable this simply +uncomment following variables in the ini file:: + + url_pat = (?:^#|\s#)(\w+) + issue_server_link = https://myissueserver.com/{repo}/issue/{id} + issue_prefix = # + +`url_pat` is the regular expression that will fetch issues from commit messages. +Default regex will match issues in format of # eg. #300. + +Matched issues will be replace with the link specified as `issue_server_link` +{id} will be replaced with issue id, and {repo} with repository name. +Since the # is striped `issue_prefix` is added as a prefix to url. +`issue_prefix` can be something different than # if you pass +ISSUE- as issue prefix this will generate an url in format:: + + ISSUE-300 Hook management --------------- @@ -361,6 +461,17 @@ To add another custom hook simply fill i can be found at *rhodecode.lib.hooks*. +Changing default encoding +------------------------- + +By default RhodeCode uses utf8 encoding, starting from 1.3 series this +can be changed, simply edit default_encoding in .ini file to desired one. +This affects many parts in rhodecode including commiters names, filenames, +encoding of commit messages. In addition RhodeCode can detect if `chardet` +library is installed. If `chardet` is detected RhodeCode will fallback to it +when there are encode/decode errors. + + Setting Up Celery ----------------- @@ -397,27 +508,36 @@ Nginx virtual host example Sample config for nginx using proxy:: + upstream rc { + server 127.0.0.1:5000; + # add more instances for load balancing + #server 127.0.0.1:5001; + #server 127.0.0.1:5002; + } + server { listen 80; server_name hg.myserver.com; access_log /var/log/nginx/rhodecode.access.log; error_log /var/log/nginx/rhodecode.error.log; + location / { - root /var/www/rhodecode/rhodecode/public/; - if (!-f $request_filename){ - proxy_pass http://127.0.0.1:5000; - } - #this is important if you want to use https !!! - proxy_set_header X-Url-Scheme $scheme; - include /etc/nginx/proxy.conf; + try_files $uri @rhode; } + + location @rhode { + proxy_pass http://rc; + include /etc/nginx/proxy.conf; + } + } Here's the proxy.conf. It's tuned so it will not timeout on long pushes or large pushes:: - + proxy_redirect off; proxy_set_header Host $host; + proxy_set_header X-Url-Scheme $scheme; proxy_set_header X-Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/docs/theme/nature/layout.html b/docs/theme/nature/layout.html --- a/docs/theme/nature/layout.html +++ b/docs/theme/nature/layout.html @@ -1,12 +1,12 @@ {% extends "basic/layout.html" %} {% block sidebarlogo %} -

Support my development effort.

+

Support RhodeCode development.

-
diff --git a/docs/usage/api_key_access.rst b/docs/usage/api_key_access.rst deleted file mode 100644 --- a/docs/usage/api_key_access.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. _api_key_access: - -Access to RhodeCode via API KEY -=============================== - -Starting from version 1.2 rss/atom feeds and journal feeds -can be accessed via **api_key**. This unique key is automatically generated for -each user in RhodeCode application. Using this key it is possible to access -feeds without having to log in. When user changes his password a new API KEY -is generated for him automatically. You can check your API KEY in account -settings page. \ No newline at end of file diff --git a/docs/usage/general.rst b/docs/usage/general.rst --- a/docs/usage/general.rst +++ b/docs/usage/general.rst @@ -36,6 +36,31 @@ Compare view is also available from the one changeset +Non changeable repository urls +------------------------------ + +Due to complicated nature of repository grouping, often urls of repositories +can change. + +example:: + + #before + http://server.com/repo_name + # after insertion to test_group group the url will be + http://server.com/test_group/repo_name + +This can be an issue for build systems and any other hardcoded scripts, moving +repository to a group leads to a need for changing external systems. To +overcome this RhodeCode introduces a non changable replacement url. It's +simply an repository ID prefixed with `_` above urls are also accessible as:: + + http://server.com/_ + +Since ID are always the same moving the repository will not affect such url. +the _ syntax can be used anywhere in the system so urls with repo_name +for changelogs, files and other can be exchanged with _ syntax. + + Mailing ------- diff --git a/docs/usage/enable_git.rst b/docs/usage/git_support.rst rename from docs/usage/enable_git.rst rename to docs/usage/git_support.rst --- a/docs/usage/enable_git.rst +++ b/docs/usage/git_support.rst @@ -1,13 +1,41 @@ -.. _enable_git: +.. _git_support: -Enabling GIT support (beta) -=========================== +GIT support +=========== -Git support in RhodeCode 1.1 was disabled due to current instability issues. -However,if you would like to test git support please feel free to re-enable it. -To re-enable GIT support just uncomment the git line in the -file **rhodecode/__init__.py** +Git support in RhodeCode 1.3 was enabled by default. +Although There are some limitations on git usage. + +- No hooks are runned for git push/pull actions. +- logs in action journals don't have git operations +- large pushes needs http server with chunked encoding support. + +if you plan to use git you need to run RhodeCode with some +http server that supports chunked encoding which git http protocol uses, +i recommend using waitress_ or gunicorn_ (linux only) for `paste` wsgi app +replacement. + +To use waitress simply change change the following in the .ini file:: + + use = egg:Paste#http + +To:: + + use = egg:waitress#main + +And comment out bellow options:: + + threadpool_workers = + threadpool_max_requests = + use_threadpool = + + +You can simply run `paster serve` as usual. + + +You can always disable git/hg support by editing a +file **rhodecode/__init__.py** and commenting out backends .. code-block:: python @@ -16,9 +44,5 @@ file **rhodecode/__init__.py** #'git': 'Git repository', } -.. note:: - Please note that the git support provided by RhodeCode is not yet fully - stable and RhodeCode might crash while using git repositories. (That is why - it is currently disabled.) Thus be careful about enabling git support, and - certainly don't use it in a production setting! - \ No newline at end of file +.. _waitress: http://pypi.python.org/pypi/waitress +.. _gunicorn: http://pypi.python.org/pypi/gunicorn \ No newline at end of file diff --git a/init.d/celeryd-upstart.conf b/init.d/celeryd-upstart.conf new file mode 100644 --- /dev/null +++ b/init.d/celeryd-upstart.conf @@ -0,0 +1,34 @@ +# celeryd - run the celeryd daemon as an upstart job for rhodecode +# Change variables/paths as necessary and place file /etc/init/celeryd.conf +# start/stop/restart as normal upstart job (ie: $ start celeryd) + +description "Celery for RhodeCode Mercurial Server" +author "Matt Zuba with a regex passive group for `#` +## {id} will be all groups matched from this pattern + +issue_pat = (?:\s*#)(\d+) + +## server url to the issue, each {id} will be replaced with match +## fetched from the regex and {repo} is replaced with repository name + +issue_server_link = https://myissueserver.com/{repo}/issue/{id} + +## prefix to add to link to indicate it's an url +## #314 will be replaced by + +issue_prefix = # + +## instance-id prefix +## a prefix key for this instance used for cache invalidation when running +## multiple instances of rhodecode, make sure it's globally unique for +## all running rhodecode instances. Leave empty if you don't use it +instance_id = #################################### ### CELERY CONFIG #### @@ -91,21 +130,27 @@ beaker.cache.regions=super_short_term,sh beaker.cache.super_short_term.type=memory beaker.cache.super_short_term.expire=10 +beaker.cache.super_short_term.key_length = 256 beaker.cache.short_term.type=memory beaker.cache.short_term.expire=60 +beaker.cache.short_term.key_length = 256 beaker.cache.long_term.type=memory beaker.cache.long_term.expire=36000 +beaker.cache.long_term.key_length = 256 beaker.cache.sql_cache_short.type=memory beaker.cache.sql_cache_short.expire=10 +beaker.cache.sql_cache_short.key_length = 256 beaker.cache.sql_cache_med.type=memory beaker.cache.sql_cache_med.expire=360 +beaker.cache.sql_cache_med.key_length = 256 beaker.cache.sql_cache_long.type=file beaker.cache.sql_cache_long.expire=3600 +beaker.cache.sql_cache_long.key_length = 256 #################################### ### BEAKER SESSION #### @@ -113,12 +158,27 @@ beaker.cache.sql_cache_long.expire=3600 ## Type of storage used for the session, current types are ## dbm, file, memcached, database, and memory. ## The storage uses the Container API -##that is also used by the cache system. -beaker.session.type = file +## that is also used by the cache system. + +## db session example + +#beaker.session.type = ext:database +#beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode +#beaker.session.table_name = db_session + +## encrypted cookie session, good for many instances +#beaker.session.type = cookie +beaker.session.type = file beaker.session.key = rhodecode -beaker.session.secret = g654dcno0-9873jhgfreyu +# secure cookie requires AES python libraries +#beaker.session.encrypt_key = g654dcno0-9873jhgfreyu +#beaker.session.validate_key = 9712sds2212c--zxc123 beaker.session.timeout = 36000 +beaker.session.httponly = true + +## uncomment for https secure cookie +beaker.session.secure = false ##auto save the session to not to use .save() beaker.session.auto = False @@ -126,7 +186,7 @@ beaker.session.auto = False ##true exire at browser close #beaker.session.cookie_expires = 3600 - + ################################################################################ ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ## ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ## @@ -232,4 +292,4 @@ datefmt = %Y-%m-%d %H:%M:%S [formatter_color_formatter_sql] class=rhodecode.lib.colored_formatter.ColorFormatterSql format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %Y-%m-%d %H:%M:%S \ No newline at end of file +datefmt = %Y-%m-%d %H:%M:%S diff --git a/requires.txt b/requires.txt --- a/requires.txt +++ b/requires.txt @@ -1,16 +1,17 @@ Pylons==1.0.0 -Beaker==1.5.4 +Beaker==1.6.2 WebHelpers>=1.2 formencode==1.2.4 SQLAlchemy==0.7.4 Mako==0.5.0 pygments>=1.4 -whoosh<1.8 +whoosh>=2.3.0,<2.4 celery>=2.2.5,<2.3 babel python-dateutil>=1.5.0,<2.0.0 dulwich>=0.8.0,<0.9.0 -vcs==0.2.2 webob==1.0.8 +markdown==2.1.1 +docutils==0.8.1 py-bcrypt -mercurial==2.0.2 \ No newline at end of file +mercurial>=2.1,<2.2 \ No newline at end of file diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py --- a/rhodecode/__init__.py +++ b/rhodecode/__init__.py @@ -26,9 +26,9 @@ import sys import platform -VERSION = (1, 2, 5) +VERSION = (1, 3, 0) __version__ = '.'.join((str(each) for each in VERSION[:4])) -__dbversion__ = 3 # defines current db version for migrations +__dbversion__ = 5 # defines current db version for migrations __platform__ = platform.system() __license__ = 'GPLv3' __py_version__ = sys.version_info @@ -38,19 +38,20 @@ PLATFORM_OTHERS = ('Linux', 'Darwin', 'F requirements = [ "Pylons==1.0.0", - "Beaker==1.5.4", + "Beaker==1.6.2", "WebHelpers>=1.2", "formencode==1.2.4", "SQLAlchemy==0.7.4", "Mako==0.5.0", "pygments>=1.4", - "whoosh<1.8", + "whoosh>=2.3.0,<2.4", "celery>=2.2.5,<2.3", "babel", "python-dateutil>=1.5.0,<2.0.0", "dulwich>=0.8.0,<0.9.0", - "vcs==0.2.2", - "webob==1.0.8" + "webob==1.0.8", + "markdown==2.1.1", + "docutils==0.8.1", ] if __py_version__ < (2, 6): @@ -58,15 +59,15 @@ if __py_version__ < (2, 6): requirements.append("pysqlite") if __platform__ in PLATFORM_WIN: - requirements.append("mercurial==2.0.1") + requirements.append("mercurial>=2.1,<2.2") else: requirements.append("py-bcrypt") - requirements.append("mercurial==2.0.2") + requirements.append("mercurial>=2.1,<2.2") try: from rhodecode.lib import get_current_revision - _rev = get_current_revision(quiet=True) + _rev = get_current_revision() except ImportError: # this is needed when doing some setup.py operations _rev = False @@ -82,5 +83,10 @@ def get_version(): BACKENDS = { 'hg': 'Mercurial repository', - #'git': 'Git repository', + 'git': 'Git repository', } + +CELERY_ON = False + +# link to config for pylons +CONFIG = {} diff --git a/rhodecode/config/deployment.ini_tmpl b/rhodecode/config/deployment.ini_tmpl --- a/rhodecode/config/deployment.ini_tmpl +++ b/rhodecode/config/deployment.ini_tmpl @@ -17,6 +17,7 @@ pdebug = false #error_email_from = paste_error@localhost #app_email_from = rhodecode-noreply@localhost #error_message = +#email_prefix = [RhodeCode] #smtp_server = mail.server.com #smtp_username = @@ -45,14 +46,52 @@ port = 5000 use = egg:rhodecode full_stack = true static_files = true -lang=en +lang = en cache_dir = %(here)s/data index_dir = %(here)s/data/index app_instance_uuid = ${app_instance_uuid} cut_off_limit = 256000 -force_https = false +force_https = false commit_parse_limit = 50 use_gravatar = true +container_auth_enabled = false +proxypass_auth_enabled = false +default_encoding = utf8 + +## overwrite schema of clone url +## available vars: +## scheme - http/https +## user - current user +## pass - password +## netloc - network location +## path - usually repo_name + +#clone_uri = {scheme}://{user}{pass}{netloc}{path} + +## issue tracking mapping for commits messages +## comment out issue_pat, issue_server, issue_prefix to enable + +## pattern to get the issues from commit messages +## default one used here is # with a regex passive group for `#` +## {id} will be all groups matched from this pattern + +issue_pat = (?:\s*#)(\d+) + +## server url to the issue, each {id} will be replaced with match +## fetched from the regex and {repo} is replaced with repository name + +issue_server_link = https://myissueserver.com/{repo}/issue/{id} + +## prefix to add to link to indicate it's an url +## #314 will be replaced by + +issue_prefix = # + +## instance-id prefix +## a prefix key for this instance used for cache invalidation when running +## multiple instances of rhodecode, make sure it's globally unique for +## all running rhodecode instances. Leave empty if you don't use it +instance_id = #################################### ### CELERY CONFIG #### @@ -91,21 +130,27 @@ beaker.cache.regions=super_short_term,sh beaker.cache.super_short_term.type=memory beaker.cache.super_short_term.expire=10 +beaker.cache.super_short_term.key_length = 256 beaker.cache.short_term.type=memory beaker.cache.short_term.expire=60 +beaker.cache.short_term.key_length = 256 beaker.cache.long_term.type=memory beaker.cache.long_term.expire=36000 +beaker.cache.long_term.key_length = 256 beaker.cache.sql_cache_short.type=memory beaker.cache.sql_cache_short.expire=10 +beaker.cache.sql_cache_short.key_length = 256 beaker.cache.sql_cache_med.type=memory beaker.cache.sql_cache_med.expire=360 +beaker.cache.sql_cache_med.key_length = 256 beaker.cache.sql_cache_long.type=file beaker.cache.sql_cache_long.expire=3600 +beaker.cache.sql_cache_long.key_length = 256 #################################### ### BEAKER SESSION #### @@ -113,12 +158,27 @@ beaker.cache.sql_cache_long.expire=3600 ## Type of storage used for the session, current types are ## dbm, file, memcached, database, and memory. ## The storage uses the Container API -##that is also used by the cache system. -beaker.session.type = file +## that is also used by the cache system. + +## db session example + +#beaker.session.type = ext:database +#beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode +#beaker.session.table_name = db_session + +## encrypted cookie session, good for many instances +#beaker.session.type = cookie +beaker.session.type = file beaker.session.key = rhodecode -beaker.session.secret = ${app_instance_secret} +# secure cookie requires AES python libraries +#beaker.session.encrypt_key = ${app_instance_secret} +#beaker.session.validate_key = ${app_instance_secret} beaker.session.timeout = 36000 +beaker.session.httponly = true + +## uncomment for https secure cookie +beaker.session.secure = false ##auto save the session to not to use .save() beaker.session.auto = False @@ -126,7 +186,7 @@ beaker.session.auto = False ##true exire at browser close #beaker.session.cookie_expires = 3600 - + ################################################################################ ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ## ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ## @@ -154,6 +214,7 @@ sqlalchemy.db1.url = sqlite:///%(here)s/ # MySQL # sqlalchemy.db1.url = mysql://user:pass@localhost/rhodecode +# see sqlalchemy docs for others sqlalchemy.db1.echo = false sqlalchemy.db1.pool_recycle = 3600 @@ -217,13 +278,13 @@ propagate = 0 class = StreamHandler args = (sys.stderr,) level = INFO -formatter = color_formatter +formatter = generic [handler_console_sql] class = StreamHandler args = (sys.stderr,) level = WARN -formatter = color_formatter_sql +formatter = generic ################ ## FORMATTERS ## @@ -241,4 +302,4 @@ datefmt = %Y-%m-%d %H:%M:%S [formatter_color_formatter_sql] class=rhodecode.lib.colored_formatter.ColorFormatterSql format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %Y-%m-%d %H:%M:%S \ No newline at end of file +datefmt = %Y-%m-%d %H:%M:%S diff --git a/rhodecode/config/environment.py b/rhodecode/config/environment.py --- a/rhodecode/config/environment.py +++ b/rhodecode/config/environment.py @@ -7,13 +7,14 @@ from mako.lookup import TemplateLookup from pylons.configuration import PylonsConfig from pylons.error import handle_mako_error +import rhodecode import rhodecode.lib.app_globals as app_globals import rhodecode.lib.helpers from rhodecode.config.routing import make_map -from rhodecode.lib import celerypylons +# don't remove this import it does magic for celery +from rhodecode.lib import celerypylons, str2bool from rhodecode.lib import engine_from_config -from rhodecode.lib.timerproxy import TimerProxy from rhodecode.lib.auth import set_available_permissions from rhodecode.lib.utils import repo2db_mapper, make_ui, set_rhodecode_config from rhodecode.model import init_model @@ -38,10 +39,13 @@ def load_environment(global_conf, app_co # Initialize config with the basic options config.init_app(global_conf, app_conf, package='rhodecode', paths=paths) + # store some globals into rhodecode + rhodecode.CELERY_ON = str2bool(config['app_conf'].get('use_celery')) + config['routes.map'] = make_map(config) config['pylons.app_globals'] = app_globals.Globals(config) config['pylons.h'] = rhodecode.lib.helpers - + rhodecode.CONFIG = config # Setup cache object as early as possible import pylons pylons.cache._push_object(config['pylons.app_globals'].cache) @@ -54,7 +58,7 @@ def load_environment(global_conf, app_co input_encoding='utf-8', default_filters=['escape'], imports=['from webhelpers.html import escape']) - #sets the c attribute access when don't existing attribute are accessed + # sets the c attribute access when don't existing attribute are accessed config['pylons.strict_tmpl_context'] = True test = os.path.split(config['__file__'])[-1] == 'test.ini' if test: @@ -63,7 +67,7 @@ def load_environment(global_conf, app_co create_test_env(TESTS_TMP_PATH, config) create_test_index(TESTS_TMP_PATH, config, True) - #MULTIPLE DB configs + # MULTIPLE DB configs # Setup the SQLAlchemy database engine sa_engine_db1 = engine_from_config(config, 'sqlalchemy.db1.') @@ -77,4 +81,7 @@ def load_environment(global_conf, app_co # CONFIGURATION OPTIONS HERE (note: all config options will override # any Pylons config options) + # store config reference into our module to skip import magic of + # pylons + rhodecode.CONFIG.update(config) return config diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -51,15 +51,16 @@ def make_app(global_conf, full_stack=Tru from rhodecode.lib.profiler import ProfilingMiddleware app = ProfilingMiddleware(app) - # we want our low level middleware to get to the request ASAP. We don't - # need any pylons stack middleware in them - app = SimpleHg(app, config) - app = SimpleGit(app, config) + if asbool(full_stack): - if asbool(full_stack): # Handle Python exceptions app = ErrorHandler(app, global_conf, **config['pylons.errorware']) + # we want our low level middleware to get to the request ASAP. We don't + # need any pylons stack middleware in them + app = SimpleHg(app, config) + app = SimpleGit(app, config) + # Display error documents for 401, 403, 404 status codes (and # 500 when debug is disabled) if asbool(config['debug']): diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -8,7 +8,6 @@ refer to the routes manual at http://rou from __future__ import with_statement from routes import Mapper - # prefix for non repository related links needs to be prefixed with `/` ADMIN_PREFIX = '/_admin' @@ -26,18 +25,27 @@ def make_map(config): def check_repo(environ, match_dict): """ check for valid repository for proper 404 handling - + :param environ: :param match_dict: """ + from rhodecode.model.db import Repository + repo_name = match_dict.get('repo_name') - repo_name = match_dict.get('repo_name') + try: + by_id = repo_name.split('_') + if len(by_id) == 2 and by_id[1].isdigit(): + repo_name = Repository.get(by_id[1]).repo_name + match_dict['repo_name'] = repo_name + except: + pass + return is_valid_repo(repo_name, config['base_path']) def check_group(environ, match_dict): """ check for valid repositories group for proper 404 handling - + :param environ: :param match_dict: """ @@ -45,7 +53,6 @@ def make_map(config): return is_valid_repos_group(repos_group_name, config['base_path']) - def check_int(environ, match_dict): return match_dict.get('id').isdigit() @@ -62,9 +69,14 @@ def make_map(config): rmap.connect('home', '/', controller='home', action='index') rmap.connect('repo_switcher', '/repos', controller='home', action='repo_switcher') + rmap.connect('branch_tag_switcher', '/branches-tags/{repo_name:.*}', + controller='home', action='branch_tag_switcher') rmap.connect('bugtracker', "http://bitbucket.org/marcinkuzminski/rhodecode/issues", _static=True) + rmap.connect('rst_help', + "http://docutils.sourceforge.net/docs/user/rst/quickref.html", + _static=True) rmap.connect('rhodecode_official', "http://rhodecode.org", _static=True) #ADMIN REPOSITORY REST ROUTES @@ -101,8 +113,9 @@ def make_map(config): function=check_repo)) #ajax delete repo perm user m.connect('delete_repo_user', "/repos_delete_user/{repo_name:.*}", - action="delete_perm_user", conditions=dict(method=["DELETE"], - function=check_repo)) + action="delete_perm_user", + conditions=dict(method=["DELETE"], function=check_repo)) + #ajax delete repo perm users_group m.connect('delete_repo_users_group', "/repos_delete_users_group/{repo_name:.*}", @@ -111,18 +124,20 @@ def make_map(config): #settings actions m.connect('repo_stats', "/repos_stats/{repo_name:.*}", - action="repo_stats", conditions=dict(method=["DELETE"], - function=check_repo)) + 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"], + action="repo_cache", conditions=dict(method=["DELETE"], + function=check_repo)) + m.connect('repo_public_journal', "/repos_public_journal/{repo_name:.*}", + action="repo_public_journal", conditions=dict(method=["PUT"], function=check_repo)) - m.connect('repo_public_journal', - "/repos_public_journal/{repo_name:.*}", - action="repo_public_journal", conditions=dict(method=["PUT"], - function=check_repo)) m.connect('repo_pull', "/repo_pull/{repo_name:.*}", - action="repo_pull", conditions=dict(method=["PUT"], - function=check_repo)) + action="repo_pull", conditions=dict(method=["PUT"], + function=check_repo)) + m.connect('repo_as_fork', "/repo_as_fork/{repo_name:.*}", + action="repo_as_fork", conditions=dict(method=["PUT"], + function=check_repo)) with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='admin/repos_groups') as m: @@ -155,6 +170,17 @@ def make_map(config): m.connect("formatted_repos_group", "/repos_groups/{id}.{format}", action="show", conditions=dict(method=["GET"], function=check_int)) + # ajax delete repos group perm user + m.connect('delete_repos_group_user_perm', + "/delete_repos_group_user_perm/{group_name:.*}", + action="delete_repos_group_user_perm", + conditions=dict(method=["DELETE"], function=check_group)) + + # ajax delete repos group perm users_group + m.connect('delete_repos_group_users_group_perm', + "/delete_repos_group_users_group_perm/{group_name:.*}", + action="delete_repos_group_users_group_perm", + conditions=dict(method=["DELETE"], function=check_group)) #ADMIN USER REST ROUTES with rmap.submapper(path_prefix=ADMIN_PREFIX, @@ -267,6 +293,34 @@ def make_map(config): m.connect("admin_settings_create_repository", "/create_repository", action="create_repository", conditions=dict(method=["GET"])) + #NOTIFICATION REST ROUTES + with rmap.submapper(path_prefix=ADMIN_PREFIX, + controller='admin/notifications') as m: + m.connect("notifications", "/notifications", + action="create", conditions=dict(method=["POST"])) + m.connect("notifications", "/notifications", + action="index", conditions=dict(method=["GET"])) + m.connect("notifications_mark_all_read", "/notifications/mark_all_read", + action="mark_all_read", conditions=dict(method=["GET"])) + m.connect("formatted_notifications", "/notifications.{format}", + action="index", conditions=dict(method=["GET"])) + m.connect("new_notification", "/notifications/new", + action="new", conditions=dict(method=["GET"])) + m.connect("formatted_new_notification", "/notifications/new.{format}", + action="new", conditions=dict(method=["GET"])) + m.connect("/notification/{notification_id}", + action="update", conditions=dict(method=["PUT"])) + m.connect("/notification/{notification_id}", + action="delete", conditions=dict(method=["DELETE"])) + m.connect("edit_notification", "/notification/{notification_id}/edit", + action="edit", conditions=dict(method=["GET"])) + m.connect("formatted_edit_notification", + "/notification/{notification_id}.{format}/edit", + action="edit", conditions=dict(method=["GET"])) + m.connect("notification", "/notification/{notification_id}", + action="show", conditions=dict(method=["GET"])) + m.connect("formatted_notification", "/notifications/{notification_id}.{format}", + action="show", conditions=dict(method=["GET"])) #ADMIN MAIN PAGES with rmap.submapper(path_prefix=ADMIN_PREFIX, @@ -276,13 +330,12 @@ def make_map(config): action='add_repo') #========================================================================== - # API V1 + # API V2 #========================================================================== with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='api/api') as m: m.connect('api', '/api') - #USER JOURNAL rmap.connect('journal', '%s/journal' % ADMIN_PREFIX, controller='journal') @@ -344,6 +397,16 @@ def make_map(config): controller='changeset', revision='tip', conditions=dict(function=check_repo)) + rmap.connect('changeset_comment', + '/{repo_name:.*}/changeset/{revision}/comment', + controller='changeset', revision='tip', action='comment', + conditions=dict(function=check_repo)) + + rmap.connect('changeset_comment_delete', + '/{repo_name:.*}/changeset/comment/{comment_id}/delete', + controller='changeset', action='delete_comment', + conditions=dict(function=check_repo, method=["DELETE"])) + rmap.connect('raw_changeset_home', '/{repo_name:.*}/raw-changeset/{revision}', controller='changeset', action='raw_changeset', @@ -361,6 +424,9 @@ def make_map(config): rmap.connect('tags_home', '/{repo_name:.*}/tags', controller='tags', conditions=dict(function=check_repo)) + rmap.connect('bookmarks_home', '/{repo_name:.*}/bookmarks', + controller='bookmarks', conditions=dict(function=check_repo)) + rmap.connect('changelog_home', '/{repo_name:.*}/changelog', controller='changelog', conditions=dict(function=check_repo)) @@ -423,19 +489,19 @@ def make_map(config): conditions=dict(function=check_repo)) rmap.connect('repo_fork_create_home', '/{repo_name:.*}/fork', - controller='settings', action='fork_create', + controller='forks', action='fork_create', conditions=dict(function=check_repo, method=["POST"])) rmap.connect('repo_fork_home', '/{repo_name:.*}/fork', - controller='settings', action='fork', + controller='forks', action='fork', conditions=dict(function=check_repo)) + rmap.connect('repo_forks_home', '/{repo_name:.*}/forks', + controller='forks', action='forks', + conditions=dict(function=check_repo)) + rmap.connect('repo_followers_home', '/{repo_name:.*}/followers', controller='followers', action='followers', conditions=dict(function=check_repo)) - rmap.connect('repo_forks_home', '/{repo_name:.*}/forks', - controller='forks', action='forks', - conditions=dict(function=check_repo)) - return rmap diff --git a/rhodecode/controllers/admin/admin.py b/rhodecode/controllers/admin/admin.py --- a/rhodecode/controllers/admin/admin.py +++ b/rhodecode/controllers/admin/admin.py @@ -7,7 +7,7 @@ :created_on: Apr 7, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify diff --git a/rhodecode/controllers/admin/ldap_settings.py b/rhodecode/controllers/admin/ldap_settings.py --- a/rhodecode/controllers/admin/ldap_settings.py +++ b/rhodecode/controllers/admin/ldap_settings.py @@ -7,7 +7,7 @@ :created_on: Nov 26, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -39,7 +39,7 @@ from rhodecode.lib import helpers as h from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator from rhodecode.lib.exceptions import LdapImportError from rhodecode.model.forms import LdapSettingsForm -from rhodecode.model.db import RhodeCodeSettings +from rhodecode.model.db import RhodeCodeSetting log = logging.getLogger(__name__) @@ -83,7 +83,7 @@ class LdapSettingsController(BaseControl super(LdapSettingsController, self).__before__() def index(self): - defaults = RhodeCodeSettings.get_ldap_settings() + defaults = RhodeCodeSetting.get_ldap_settings() c.search_scope_cur = defaults.get('ldap_search_scope') c.tls_reqcert_cur = defaults.get('ldap_tls_reqcert') c.tls_kind_cur = defaults.get('ldap_tls_kind') @@ -107,7 +107,7 @@ class LdapSettingsController(BaseControl for k, v in form_result.items(): if k.startswith('ldap_'): - setting = RhodeCodeSettings.get_by_name(k) + setting = RhodeCodeSetting.get_by_name(k) setting.app_settings_value = v self.sa.add(setting) diff --git a/rhodecode/controllers/admin/notifications.py b/rhodecode/controllers/admin/notifications.py new file mode 100644 --- /dev/null +++ b/rhodecode/controllers/admin/notifications.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.controllers.admin.notifications + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + notifications controller for RhodeCode + + :created_on: Nov 23, 2010 + :author: marcink + :copyright: (C) 2010-2012 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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, see . + +import logging +import traceback + +from pylons import request +from pylons import tmpl_context as c, url +from pylons.controllers.util import redirect + +from rhodecode.lib.base import BaseController, render +from rhodecode.model.db import Notification + +from rhodecode.model.notification import NotificationModel +from rhodecode.lib.auth import LoginRequired, NotAnonymous +from rhodecode.lib import helpers as h +from rhodecode.model.meta import Session + + +log = logging.getLogger(__name__) + + +class NotificationsController(BaseController): + """REST Controller styled on the Atom Publishing Protocol""" + # To properly map this controller, ensure your config/routing.py + # file has a resource setup: + # map.resource('notification', 'notifications', controller='_admin/notifications', + # path_prefix='/_admin', name_prefix='_admin_') + + @LoginRequired() + @NotAnonymous() + def __before__(self): + super(NotificationsController, self).__before__() + + def index(self, format='html'): + """GET /_admin/notifications: All items in the collection""" + # url('notifications') + c.user = self.rhodecode_user + c.notifications = NotificationModel()\ + .get_for_user(self.rhodecode_user.user_id) + return render('admin/notifications/notifications.html') + + def mark_all_read(self): + if request.environ.get('HTTP_X_PARTIAL_XHR'): + nm = NotificationModel() + # mark all read + nm.mark_all_read_for_user(self.rhodecode_user.user_id) + Session.commit() + c.user = self.rhodecode_user + c.notifications = nm.get_for_user(self.rhodecode_user.user_id) + return render('admin/notifications/notifications_data.html') + + def create(self): + """POST /_admin/notifications: Create a new item""" + # url('notifications') + + def new(self, format='html'): + """GET /_admin/notifications/new: Form to create a new item""" + # url('new_notification') + + def update(self, notification_id): + """PUT /_admin/notifications/id: Update an existing item""" + # Forms posted to this method should contain a hidden field: + # + # Or using helpers: + # h.form(url('notification', notification_id=ID), + # method='put') + # url('notification', notification_id=ID) + + def delete(self, notification_id): + """DELETE /_admin/notifications/id: Delete an existing item""" + # Forms posted to this method should contain a hidden field: + # + # Or using helpers: + # h.form(url('notification', notification_id=ID), + # method='delete') + # url('notification', notification_id=ID) + + try: + no = Notification.get(notification_id) + owner = lambda: (no.notifications_to_users.user.user_id + == c.rhodecode_user.user_id) + if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner: + NotificationModel().delete(c.rhodecode_user.user_id, no) + Session.commit() + return 'ok' + except Exception: + Session.rollback() + log.error(traceback.format_exc()) + return 'fail' + + def show(self, notification_id, format='html'): + """GET /_admin/notifications/id: Show a specific item""" + # url('notification', notification_id=ID) + c.user = self.rhodecode_user + no = Notification.get(notification_id) + + owner = lambda: (no.notifications_to_users.user.user_id + == c.user.user_id) + if no and (h.HasPermissionAny('hg.admin', 'repository.admin')() or owner): + unotification = NotificationModel()\ + .get_user_notification(c.user.user_id, no) + + # if this association to user is not valid, we don't want to show + # this message + if unotification: + if unotification.read is False: + unotification.mark_as_read() + Session.commit() + c.notification = no + + return render('admin/notifications/show_notification.html') + + return redirect(url('notifications')) + + def edit(self, notification_id, format='html'): + """GET /_admin/notifications/id/edit: Form to edit an existing item""" + # url('edit_notification', notification_id=ID) diff --git a/rhodecode/controllers/admin/permissions.py b/rhodecode/controllers/admin/permissions.py --- a/rhodecode/controllers/admin/permissions.py +++ b/rhodecode/controllers/admin/permissions.py @@ -7,7 +7,7 @@ :created_on: Apr 27, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -38,6 +38,7 @@ from rhodecode.lib.base import BaseContr from rhodecode.model.forms import DefaultPermissionsForm from rhodecode.model.permission import PermissionModel from rhodecode.model.db import User +from rhodecode.model.meta import Session log = logging.getLogger(__name__) @@ -101,6 +102,7 @@ class PermissionsController(BaseControll form_result = _form.to_python(dict(request.POST)) form_result.update({'perm_user_name': id}) permission_model.update(form_result) + Session.commit() h.flash(_('Default permissions updated successfully'), category='success') diff --git a/rhodecode/controllers/admin/repos.py b/rhodecode/controllers/admin/repos.py --- a/rhodecode/controllers/admin/repos.py +++ b/rhodecode/controllers/admin/repos.py @@ -3,11 +3,11 @@ rhodecode.controllers.admin.repos ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Admin controller for RhodeCode + Repositories controller for RhodeCode :created_on: Apr 7, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -29,9 +29,10 @@ import formencode from formencode import htmlfill 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 import request, session, tmpl_context as c, url +from pylons.controllers.util import redirect from pylons.i18n.translation import _ +from sqlalchemy.exc import IntegrityError from rhodecode.lib import helpers as h from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \ @@ -39,11 +40,11 @@ from rhodecode.lib.auth import LoginRequ from rhodecode.lib.base import BaseController, render from rhodecode.lib.utils import invalidate_cache, action_logger, repo_name_slug from rhodecode.lib.helpers import get_token -from rhodecode.model.db import User, Repository, UserFollowing, Group +from rhodecode.model.meta import Session +from rhodecode.model.db import User, Repository, UserFollowing, RepoGroup from rhodecode.model.forms import RepoForm from rhodecode.model.scm import ScmModel from rhodecode.model.repo import RepoModel -from sqlalchemy.exc import IntegrityError log = logging.getLogger(__name__) @@ -63,9 +64,9 @@ class ReposController(BaseController): super(ReposController, self).__before__() def __load_defaults(self): - c.repo_groups = Group.groups_choices() + c.repo_groups = RepoGroup.groups_choices() c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups) - + repo_model = RepoModel() c.users_array = repo_model.get_users_js() c.users_groups_array = repo_model.get_users_groups_js() @@ -96,12 +97,13 @@ class ReposController(BaseController): .filter(UserFollowing.follows_repository == c.repo_info).scalar() if c.repo_info.stats: - last_rev = c.repo_info.stats.stat_on_revision + # this is on what revision we ended up so we add +1 for count + last_rev = c.repo_info.stats.stat_on_revision + 1 else: last_rev = 0 c.stats_revision = last_rev - c.repo_last_rev = repo.count() - 1 if repo.revisions else 0 + c.repo_last_rev = repo.count() if repo.revisions else 0 if last_rev == 0 or c.repo_last_rev == 0: c.stats_percentage = 0 @@ -110,6 +112,10 @@ class ReposController(BaseController): c.repo_last_rev) * 100) defaults = RepoModel()._get_defaults(repo_name) + + c.repos_list = [('', _('--REMOVE FORK--'))] + c.repos_list += [(x.repo_id, x.repo_name) for x in + Repository.query().order_by(Repository.repo_name).all()] return defaults @HasPermissionAllDecorator('hg.admin') @@ -127,13 +133,13 @@ class ReposController(BaseController): """ POST /repos: Create a new item""" # url('repos') - repo_model = RepoModel() + self.__load_defaults() form_result = {} try: form_result = RepoForm(repo_groups=c.repo_groups_choices)()\ .to_python(dict(request.POST)) - repo_model.create(form_result, self.rhodecode_user) + RepoModel().create(form_result, self.rhodecode_user) if form_result['clone_uri']: h.flash(_('created repository %s from %s') \ % (form_result['repo_name'], form_result['clone_uri']), @@ -143,13 +149,13 @@ class ReposController(BaseController): category='success') if request.POST.get('user_created'): - #created by regular non admin user + # created by regular non admin user action_logger(self.rhodecode_user, 'user_created_repo', form_result['repo_name_full'], '', self.sa) else: action_logger(self.rhodecode_user, 'admin_created_repo', form_result['repo_name_full'], '', self.sa) - + Session.commit() except formencode.Invalid, errors: c.new_repo = errors.value['repo_name'] @@ -207,7 +213,7 @@ class ReposController(BaseController): changed_name = repo.repo_name action_logger(self.rhodecode_user, 'admin_updated_repo', changed_name, '', self.sa) - + Session.commit() except formencode.Invalid, errors: defaults = self.__load_data(repo_name) defaults.update(errors.value) @@ -251,9 +257,9 @@ class ReposController(BaseController): repo_model.delete(repo) invalidate_cache('get_repo_cached_%s' % repo_name) h.flash(_('deleted repository %s') % repo_name, category='success') - + Session.commit() except IntegrityError, e: - if e.message.find('repositories_fork_id_fkey'): + if e.message.find('repositories_fork_id_fkey') != -1: log.error(traceback.format_exc()) h.flash(_('Cannot delete %s it still contains attached ' 'forks') % repo_name, @@ -271,8 +277,7 @@ class ReposController(BaseController): return redirect(url('repos')) - - @HasRepoPermissionAllDecorator('repository.admin') + @HasRepoPermissionAllDecorator('repository.admin') def delete_perm_user(self, repo_name): """ DELETE an existing repository permission user @@ -281,9 +286,11 @@ class ReposController(BaseController): """ try: - repo_model = RepoModel() - repo_model.delete_perm_user(request.POST, repo_name) - except Exception, e: + RepoModel().revoke_user_permission(repo=repo_name, + user=request.POST['user_id']) + Session.commit() + except Exception: + log.error(traceback.format_exc()) h.flash(_('An error occurred during deletion of repository user'), category='error') raise HTTPInternalServerError() @@ -295,10 +302,14 @@ class ReposController(BaseController): :param repo_name: """ + try: - repo_model = RepoModel() - repo_model.delete_perm_users_group(request.POST, repo_name) - except Exception, e: + RepoModel().revoke_users_group_permission( + repo=repo_name, group_name=request.POST['users_group_id'] + ) + Session.commit() + except Exception: + log.error(traceback.format_exc()) h.flash(_('An error occurred during deletion of repository' ' users groups'), category='error') @@ -313,8 +324,8 @@ class ReposController(BaseController): """ try: - repo_model = RepoModel() - repo_model.delete_stats(repo_name) + RepoModel().delete_stats(repo_name) + Session.commit() except Exception, e: h.flash(_('An error occurred during deletion of repository stats'), category='error') @@ -330,6 +341,7 @@ class ReposController(BaseController): try: ScmModel().mark_for_invalidation(repo_name) + Session.commit() except Exception, e: h.flash(_('An error occurred during cache invalidation'), category='error') @@ -353,6 +365,7 @@ class ReposController(BaseController): self.scm_model.toggle_following_repo(repo_id, user_id) h.flash(_('Updated repository visibility in public journal'), category='success') + Session.commit() except: h.flash(_('An error occurred during setting this' ' repository in public journal'), @@ -380,6 +393,28 @@ class ReposController(BaseController): return redirect(url('edit_repo', repo_name=repo_name)) @HasPermissionAllDecorator('hg.admin') + def repo_as_fork(self, repo_name): + """ + Mark given repository as a fork of another + + :param repo_name: + """ + try: + fork_id = request.POST.get('id_fork_of') + repo = ScmModel().mark_as_fork(repo_name, fork_id, + self.rhodecode_user.username) + fork = repo.fork.repo_name if repo.fork else _('Nothing') + Session.commit() + h.flash(_('Marked repo %s as fork of %s' % (repo_name,fork)), + category='success') + except Exception, e: + raise + h.flash(_('An error occurred during this operation'), + 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) diff --git a/rhodecode/controllers/admin/repos_groups.py b/rhodecode/controllers/admin/repos_groups.py --- a/rhodecode/controllers/admin/repos_groups.py +++ b/rhodecode/controllers/admin/repos_groups.py @@ -1,22 +1,50 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.controllers.admin.repos_groups + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Repositories groups controller for RhodeCode + + :created_on: Mar 23, 2010 + :author: marcink + :copyright: (C) 2010-2012 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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, see . + import logging import traceback import formencode from formencode import htmlfill -from operator import itemgetter -from pylons import request, response, session, tmpl_context as c, url -from pylons.controllers.util import abort, redirect +from pylons import request, tmpl_context as c, url +from pylons.controllers.util import redirect from pylons.i18n.translation import _ from sqlalchemy.exc import IntegrityError from rhodecode.lib import helpers as h -from rhodecode.lib.auth import LoginRequired, HasPermissionAnyDecorator +from rhodecode.lib.auth import LoginRequired, HasPermissionAnyDecorator,\ + HasReposGroupPermissionAnyDecorator from rhodecode.lib.base import BaseController, render -from rhodecode.model.db import Group +from rhodecode.model.db import RepoGroup from rhodecode.model.repos_group import ReposGroupModel from rhodecode.model.forms import ReposGroupForm +from rhodecode.model.meta import Session +from rhodecode.model.repo import RepoModel +from webob.exc import HTTPInternalServerError log = logging.getLogger(__name__) @@ -32,9 +60,13 @@ class ReposGroupsController(BaseControll super(ReposGroupsController, self).__before__() def __load_defaults(self): - c.repo_groups = Group.groups_choices() + c.repo_groups = RepoGroup.groups_choices() c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups) + repo_model = RepoModel() + c.users_array = repo_model.get_users_js() + c.users_groups_array = repo_model.get_users_groups_js() + def __load_data(self, group_id): """ Load defaults settings for edit, and update @@ -43,21 +75,30 @@ class ReposGroupsController(BaseControll """ self.__load_defaults() - repo_group = Group.get(group_id) + repo_group = RepoGroup.get(group_id) data = repo_group.get_dict() data['group_name'] = repo_group.name + # fill repository users + for p in repo_group.repo_group_to_perm: + data.update({'u_perm_%s' % p.user.username: + p.permission.permission_name}) + + # fill repository groups + for p in repo_group.users_group_to_perm: + data.update({'g_perm_%s' % p.users_group.users_group_name: + p.permission.permission_name}) + return data @HasPermissionAnyDecorator('hg.admin') def index(self, format='html'): """GET /repos_groups: All items in the collection""" # url('repos_groups') - - sk = lambda g:g.parents[0].group_name if g.parents else g.group_name - c.groups = sorted(Group.query().all(), key=sk) + sk = lambda g: g.parents[0].group_name if g.parents else g.group_name + c.groups = sorted(RepoGroup.query().all(), key=sk) return render('admin/repos_groups/repos_groups_show.html') @HasPermissionAnyDecorator('hg.admin') @@ -65,12 +106,16 @@ class ReposGroupsController(BaseControll """POST /repos_groups: Create a new item""" # url('repos_groups') self.__load_defaults() - repos_group_model = ReposGroupModel() - repos_group_form = ReposGroupForm(available_groups= + repos_group_form = ReposGroupForm(available_groups = c.repo_groups_choices)() try: form_result = repos_group_form.to_python(dict(request.POST)) - repos_group_model.create(form_result) + ReposGroupModel().create( + group_name=form_result['group_name'], + group_description=form_result['group_description'], + parent=form_result['group_parent_id'] + ) + Session.commit() h.flash(_('created repos group %s') \ % form_result['group_name'], category='success') #TODO: in futureaction_logger(, '', '', '', self.sa) @@ -89,7 +134,6 @@ class ReposGroupsController(BaseControll return redirect(url('repos_groups')) - @HasPermissionAnyDecorator('hg.admin') def new(self, format='html'): """GET /repos_groups/new: Form to create a new item""" @@ -108,16 +152,17 @@ class ReposGroupsController(BaseControll # url('repos_group', id=ID) self.__load_defaults() - c.repos_group = Group.get(id) + c.repos_group = RepoGroup.get(id) - repos_group_model = ReposGroupModel() - repos_group_form = ReposGroupForm(edit=True, - old_data=c.repos_group.get_dict(), - available_groups= - c.repo_groups_choices)() + repos_group_form = ReposGroupForm( + edit=True, + old_data=c.repos_group.get_dict(), + available_groups=c.repo_groups_choices + )() try: form_result = repos_group_form.to_python(dict(request.POST)) - repos_group_model.update(id, form_result) + ReposGroupModel().update(id, form_result) + Session.commit() h.flash(_('updated repos group %s') \ % form_result['group_name'], category='success') #TODO: in futureaction_logger(, '', '', '', self.sa) @@ -136,7 +181,6 @@ class ReposGroupsController(BaseControll return redirect(url('repos_groups')) - @HasPermissionAnyDecorator('hg.admin') def delete(self, id): """DELETE /repos_groups/id: Delete an existing item""" @@ -147,8 +191,7 @@ class ReposGroupsController(BaseControll # method='delete') # url('repos_group', id=ID) - repos_group_model = ReposGroupModel() - gr = Group.get(id) + gr = RepoGroup.get(id) repos = gr.repositories.all() if repos: h.flash(_('This group contains %s repositores and cannot be ' @@ -157,11 +200,12 @@ class ReposGroupsController(BaseControll return redirect(url('repos_groups')) try: - repos_group_model.delete(id) + ReposGroupModel().delete(id) + Session.commit() h.flash(_('removed repos group %s' % gr.group_name), category='success') #TODO: in future action_logger(, '', '', '', self.sa) except IntegrityError, e: - if e.message.find('groups_group_parent_id_fkey'): + if e.message.find('groups_group_parent_id_fkey') != -1: log.error(traceback.format_exc()) h.flash(_('Cannot delete this group it still contains ' 'subgroups'), @@ -178,15 +222,57 @@ class ReposGroupsController(BaseControll return redirect(url('repos_groups')) + @HasReposGroupPermissionAnyDecorator('group.admin') + def delete_repos_group_user_perm(self, group_name): + """ + DELETE an existing repositories group permission user + + :param group_name: + """ + + try: + ReposGroupModel().revoke_user_permission( + repos_group=group_name, user=request.POST['user_id'] + ) + Session.commit() + except Exception: + log.error(traceback.format_exc()) + h.flash(_('An error occurred during deletion of group user'), + category='error') + raise HTTPInternalServerError() + + @HasReposGroupPermissionAnyDecorator('group.admin') + def delete_repos_group_users_group_perm(self, group_name): + """ + DELETE an existing repositories group permission users group + + :param group_name: + """ + + try: + ReposGroupModel().revoke_users_group_permission( + repos_group=group_name, + group_name=request.POST['users_group_id'] + ) + Session.commit() + except Exception: + log.error(traceback.format_exc()) + h.flash(_('An error occurred during deletion of group' + ' users groups'), + category='error') + raise HTTPInternalServerError() + def show_by_name(self, group_name): - id_ = Group.get_by_group_name(group_name).group_id + id_ = RepoGroup.get_by_group_name(group_name).group_id return self.show(id_) + @HasReposGroupPermissionAnyDecorator('group.read', 'group.write', + 'group.admin') def show(self, id, format='html'): """GET /repos_groups/id: Show a specific item""" # url('repos_group', id=ID) - c.group = Group.get(id) + c.group = RepoGroup.get(id) if c.group: c.group_repos = c.group.repositories.all() @@ -201,8 +287,8 @@ class ReposGroupsController(BaseControll c.repo_cnt = 0 - c.groups = self.sa.query(Group).order_by(Group.group_name)\ - .filter(Group.group_parent_id == id).all() + c.groups = self.sa.query(RepoGroup).order_by(RepoGroup.group_name)\ + .filter(RepoGroup.group_parent_id == id).all() return render('admin/repos_groups/repos_groups.html') @@ -213,11 +299,11 @@ class ReposGroupsController(BaseControll id_ = int(id) - c.repos_group = Group.get(id_) + c.repos_group = RepoGroup.get(id_) defaults = self.__load_data(id_) # we need to exclude this group from the group list for editing - c.repo_groups = filter(lambda x:x[0] != id_, c.repo_groups) + c.repo_groups = filter(lambda x: x[0] != id_, c.repo_groups) return htmlfill.render( render('admin/repos_groups/repos_groups_edit.html'), @@ -225,5 +311,3 @@ class ReposGroupsController(BaseControll encoding="UTF-8", force_defaults=False ) - - diff --git a/rhodecode/controllers/admin/settings.py b/rhodecode/controllers/admin/settings.py --- a/rhodecode/controllers/admin/settings.py +++ b/rhodecode/controllers/admin/settings.py @@ -7,7 +7,7 @@ :created_on: Jul 14, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -40,13 +40,15 @@ from rhodecode.lib.base import BaseContr from rhodecode.lib.celerylib import tasks, run_task from rhodecode.lib.utils import repo2db_mapper, invalidate_cache, \ set_rhodecode_config, repo_name_slug -from rhodecode.model.db import RhodeCodeUi, Repository, Group, \ - RhodeCodeSettings +from rhodecode.model.db import RhodeCodeUi, Repository, RepoGroup, \ + RhodeCodeSetting from rhodecode.model.forms import UserForm, ApplicationSettingsForm, \ ApplicationUiSettingsForm from rhodecode.model.scm import ScmModel from rhodecode.model.user import UserModel from rhodecode.model.db import User +from rhodecode.model.notification import EmailNotificationModel +from rhodecode.model.meta import Session log = logging.getLogger(__name__) @@ -69,7 +71,7 @@ class SettingsController(BaseController) """GET /admin/settings: All items in the collection""" # url('admin_settings') - defaults = RhodeCodeSettings.get_app_settings() + defaults = RhodeCodeSetting.get_app_settings() defaults.update(self.get_hg_ui_settings()) return htmlfill.render( render('admin/settings/settings.html'), @@ -99,7 +101,7 @@ class SettingsController(BaseController) # url('admin_setting', setting_id=ID) if setting_id == 'mapping': rm_obsolete = request.POST.get('destroy', False) - log.debug('Rescanning directories with destroy=%s', rm_obsolete) + log.debug('Rescanning directories with destroy=%s' % rm_obsolete) initial = ScmModel().repo_scan() log.debug('invalidating all repositories') for repo_name in initial.keys(): @@ -124,15 +126,15 @@ class SettingsController(BaseController) form_result = application_form.to_python(dict(request.POST)) try: - hgsettings1 = RhodeCodeSettings.get_by_name('title') + hgsettings1 = RhodeCodeSetting.get_by_name('title') hgsettings1.app_settings_value = \ form_result['rhodecode_title'] - hgsettings2 = RhodeCodeSettings.get_by_name('realm') + hgsettings2 = RhodeCodeSetting.get_by_name('realm') hgsettings2.app_settings_value = \ form_result['rhodecode_realm'] - hgsettings3 = RhodeCodeSettings.get_by_name('ga_code') + hgsettings3 = RhodeCodeSetting.get_by_name('ga_code') hgsettings3.app_settings_value = \ form_result['rhodecode_ga_code'] @@ -226,12 +228,11 @@ class SettingsController(BaseController) prefix_error=False, encoding="UTF-8") - if setting_id == 'hooks': ui_key = request.POST.get('new_hook_ui_key') ui_value = request.POST.get('new_hook_ui_value') try: - + if ui_value and ui_key: RhodeCodeUi.create_or_update_hook(ui_key, ui_value) h.flash(_('Added new hook'), @@ -240,13 +241,14 @@ class SettingsController(BaseController) # check for edits update = False _d = request.POST.dict_of_lists() - for k, v in zip(_d.get('hook_ui_key',[]), _d.get('hook_ui_value_new',[])): + for k, v in zip(_d.get('hook_ui_key', []), + _d.get('hook_ui_value_new', [])): RhodeCodeUi.create_or_update_hook(k, v) update = True if update: h.flash(_('Updated hooks'), category='success') - + Session.commit() except: log.error(traceback.format_exc()) h.flash(_('error occurred during hook creation'), @@ -254,6 +256,21 @@ class SettingsController(BaseController) return redirect(url('admin_edit_setting', setting_id='hooks')) + if setting_id == 'email': + test_email = request.POST.get('test_email') + test_email_subj = 'RhodeCode TestEmail' + test_email_body = 'RhodeCode Email test' + + test_email_html_body = EmailNotificationModel()\ + .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT, + body=test_email_body) + + recipients = [test_email] if [test_email] else None + + run_task(tasks.send_email, recipients, test_email_subj, + test_email_body, test_email_html_body) + + h.flash(_('Email task created'), category='success') return redirect(url('admin_settings')) @HasPermissionAllDecorator('hg.admin') @@ -268,8 +285,8 @@ class SettingsController(BaseController) if setting_id == 'hooks': hook_id = request.POST.get('hook_id') RhodeCodeUi.delete(hook_id) - - + + @HasPermissionAllDecorator('hg.admin') def show(self, setting_id, format='html'): """ @@ -339,7 +356,7 @@ class SettingsController(BaseController) user_model.update_my_account(uid, form_result) h.flash(_('Your account was updated successfully'), category='success') - + Session.commit() except formencode.Invalid, errors: c.user = User.get(self.rhodecode_user.user_id) all_repos = self.sa.query(Repository)\ @@ -366,7 +383,7 @@ class SettingsController(BaseController) def create_repository(self): """GET /_admin/create_repository: Form to create a new item""" - c.repo_groups = Group.groups_choices() + c.repo_groups = RepoGroup.groups_choices() c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups) new_repo = request.GET.get('repo', '') diff --git a/rhodecode/controllers/admin/users.py b/rhodecode/controllers/admin/users.py --- a/rhodecode/controllers/admin/users.py +++ b/rhodecode/controllers/admin/users.py @@ -7,7 +7,7 @@ :created_on: Apr 4, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -29,7 +29,7 @@ import formencode from formencode import htmlfill from pylons import request, session, tmpl_context as c, url, config -from pylons.controllers.util import abort, redirect +from pylons.controllers.util import redirect from pylons.i18n.translation import _ from rhodecode.lib.exceptions import DefaultUserException, \ @@ -38,9 +38,10 @@ 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, RepoToPerm, UserToPerm, Permission +from rhodecode.model.db import User, Permission from rhodecode.model.forms import UserForm from rhodecode.model.user import UserModel +from rhodecode.model.meta import Session log = logging.getLogger(__name__) @@ -71,12 +72,13 @@ class UsersController(BaseController): # url('users') user_model = UserModel() - login_form = UserForm()() + user_form = UserForm()() try: - form_result = login_form.to_python(dict(request.POST)) + form_result = user_form.to_python(dict(request.POST)) user_model.create(form_result) h.flash(_('created user %s') % form_result['username'], category='success') + Session.commit() #action_logger(self.rhodecode_user, 'new_user', '', '', self.sa) except formencode.Invalid, errors: return htmlfill.render( @@ -114,11 +116,11 @@ class UsersController(BaseController): form_result = _form.to_python(dict(request.POST)) user_model.update(id, form_result) h.flash(_('User updated successfully'), category='success') - + Session.commit() except formencode.Invalid, errors: e = errors.error_dict or {} perm = Permission.get_by_key('hg.create.repository') - e.update({'create_repo_perm': UserToPerm.has_perm(id, perm)}) + e.update({'create_repo_perm': user_model.has_perm(id, perm)}) return htmlfill.render( render('admin/users/user_edit.html'), defaults=errors.value, @@ -144,6 +146,7 @@ class UsersController(BaseController): try: user_model.delete(id) h.flash(_('successfully deleted user'), category='success') + Session.commit() except (UserOwnsReposException, DefaultUserException), e: h.flash(str(e), category='warning') except Exception: @@ -158,20 +161,19 @@ class UsersController(BaseController): def edit(self, id, format='html'): """GET /users/id/edit: Form to edit an existing item""" # url('edit_user', id=ID) - user_model = UserModel() - c.user = user_model.get(id) + c.user = User.get(id) if not c.user: return redirect(url('users')) if c.user.username == 'default': h.flash(_("You can't edit this user"), category='warning') return redirect(url('users')) c.user.permissions = {} - c.granted_permissions = user_model.fill_perms(c.user)\ + c.granted_permissions = UserModel().fill_perms(c.user)\ .permissions['global'] defaults = c.user.get_dict() perm = Permission.get_by_key('hg.create.repository') - defaults.update({'create_repo_perm': UserToPerm.has_perm(id, perm)}) + defaults.update({'create_repo_perm': UserModel().has_perm(id, perm)}) return htmlfill.render( render('admin/users/user_edit.html'), @@ -185,23 +187,24 @@ class UsersController(BaseController): # url('user_perm', id=ID, method='put') grant_perm = request.POST.get('create_repo_perm', False) + user_model = UserModel() if grant_perm: perm = Permission.get_by_key('hg.create.none') - UserToPerm.revoke_perm(id, perm) + user_model.revoke_perm(id, perm) perm = Permission.get_by_key('hg.create.repository') - UserToPerm.grant_perm(id, perm) + user_model.grant_perm(id, perm) h.flash(_("Granted 'repository create' permission to user"), category='success') - + Session.commit() else: perm = Permission.get_by_key('hg.create.repository') - UserToPerm.revoke_perm(id, perm) + user_model.revoke_perm(id, perm) perm = Permission.get_by_key('hg.create.none') - UserToPerm.grant_perm(id, perm) + user_model.grant_perm(id, perm) h.flash(_("Revoked 'repository create' permission to user"), category='success') - + Session.commit() return redirect(url('edit_user', id=id)) diff --git a/rhodecode/controllers/admin/users_groups.py b/rhodecode/controllers/admin/users_groups.py --- a/rhodecode/controllers/admin/users_groups.py +++ b/rhodecode/controllers/admin/users_groups.py @@ -7,7 +7,7 @@ :created_on: Jan 25, 2011 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -33,12 +33,15 @@ from pylons.controllers.util import abor from pylons.i18n.translation import _ from rhodecode.lib.exceptions import UsersGroupsAssignedException -from rhodecode.lib import helpers as h +from rhodecode.lib import helpers as h, safe_unicode from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator from rhodecode.lib.base import BaseController, render +from rhodecode.model.users_group import UsersGroupModel + from rhodecode.model.db import User, UsersGroup, Permission, UsersGroupToPerm -from rhodecode.model.forms import UserForm, UsersGroupForm +from rhodecode.model.forms import UsersGroupForm +from rhodecode.model.meta import Session log = logging.getLogger(__name__) @@ -70,10 +73,12 @@ class UsersGroupsController(BaseControll users_group_form = UsersGroupForm()() try: form_result = users_group_form.to_python(dict(request.POST)) - UsersGroup.create(form_result) + UsersGroupModel().create(name=form_result['users_group_name'], + active=form_result['users_group_active']) h.flash(_('created users group %s') \ % form_result['users_group_name'], category='success') #action_logger(self.rhodecode_user, 'new_user', '', '', self.sa) + Session.commit() except formencode.Invalid, errors: return htmlfill.render( render('admin/users_groups/users_group_add.html'), @@ -103,29 +108,33 @@ class UsersGroupsController(BaseControll # url('users_group', id=ID) c.users_group = UsersGroup.get(id) - c.group_members = [(x.user_id, x.user.username) for x in - c.users_group.members] + c.group_members_obj = [x.user for x in c.users_group.members] + c.group_members = [(x.user_id, x.username) for x in + c.group_members_obj] c.available_members = [(x.user_id, x.username) for x in self.sa.query(User).all()] + + available_members = [safe_unicode(x[0]) for x in c.available_members] + users_group_form = UsersGroupForm(edit=True, old_data=c.users_group.get_dict(), - available_members=[str(x[0]) for x - in c.available_members])() + available_members=available_members)() try: form_result = users_group_form.to_python(request.POST) - UsersGroup.update(id, form_result) + UsersGroupModel().update(c.users_group, form_result) h.flash(_('updated users group %s') \ % form_result['users_group_name'], category='success') #action_logger(self.rhodecode_user, 'new_user', '', '', self.sa) + Session.commit() except formencode.Invalid, errors: e = errors.error_dict or {} perm = Permission.get_by_key('hg.create.repository') e.update({'create_repo_perm': - UsersGroupToPerm.has_perm(id, perm)}) + UsersGroupModel().has_perm(id, perm)}) return htmlfill.render( render('admin/users_groups/users_group_edit.html'), @@ -150,8 +159,9 @@ class UsersGroupsController(BaseControll # url('users_group', id=ID) try: - UsersGroup.delete(id) + UsersGroupModel().delete(id) h.flash(_('successfully deleted users group'), category='success') + Session.commit() except UsersGroupsAssignedException, e: h.flash(e, category='error') except Exception: @@ -172,14 +182,15 @@ class UsersGroupsController(BaseControll return redirect(url('users_groups')) c.users_group.permissions = {} - c.group_members = [(x.user_id, x.user.username) for x in - c.users_group.members] + c.group_members_obj = [x.user for x in c.users_group.members] + c.group_members = [(x.user_id, x.username) for x in + c.group_members_obj] c.available_members = [(x.user_id, x.username) for x in self.sa.query(User).all()] defaults = c.users_group.get_dict() perm = Permission.get_by_key('hg.create.repository') defaults.update({'create_repo_perm': - UsersGroupToPerm.has_perm(id, perm)}) + UsersGroupModel().has_perm(c.users_group, perm)}) return htmlfill.render( render('admin/users_groups/users_group_edit.html'), defaults=defaults, @@ -195,20 +206,21 @@ class UsersGroupsController(BaseControll if grant_perm: perm = Permission.get_by_key('hg.create.none') - UsersGroupToPerm.revoke_perm(id, perm) + UsersGroupModel().revoke_perm(id, perm) perm = Permission.get_by_key('hg.create.repository') - UsersGroupToPerm.grant_perm(id, perm) + UsersGroupModel().grant_perm(id, perm) h.flash(_("Granted 'repository create' permission to user"), category='success') + Session.commit() else: perm = Permission.get_by_key('hg.create.repository') - UsersGroupToPerm.revoke_perm(id, perm) + UsersGroupModel().revoke_perm(id, perm) perm = Permission.get_by_key('hg.create.none') - UsersGroupToPerm.grant_perm(id, perm) + UsersGroupModel().grant_perm(id, perm) h.flash(_("Revoked 'repository create' permission to user"), category='success') - + Session.commit() return redirect(url('edit_users_group', id=id)) diff --git a/rhodecode/controllers/api/__init__.py b/rhodecode/controllers/api/__init__.py --- a/rhodecode/controllers/api/__init__.py +++ b/rhodecode/controllers/api/__init__.py @@ -7,19 +7,19 @@ :created_on: Aug 20, 2011 :author: marcink - :copyright: (C) 2009-2010 Marcin Kuzminski + :copyright: (C) 2011-2012 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, @@ -62,7 +62,7 @@ def jsonrpc_error(message, code=None): Generate a Response object with a JSON-RPC error body """ from pylons.controllers.util import Response - resp = Response(body=json.dumps(dict(result=None, error=message)), + resp = Response(body=json.dumps(dict(id=None, result=None, error=message)), status=code, content_type='application/json') return resp @@ -100,7 +100,7 @@ class JSONRPCController(WSGIController): else: length = environ['CONTENT_LENGTH'] or 0 length = int(environ['CONTENT_LENGTH']) - log.debug('Content-Length: %s', length) + log.debug('Content-Length: %s' % length) if length == 0: log.debug("Content-Length is 0") @@ -118,11 +118,13 @@ class JSONRPCController(WSGIController): # check AUTH based on API KEY try: self._req_api_key = json_body['api_key'] + self._req_id = json_body['id'] self._req_method = json_body['method'] self._request_params = json_body['args'] - log.debug('method: %s, params: %s', - self._req_method, - self._request_params) + log.debug( + 'method: %s, params: %s' % (self._req_method, + self._request_params) + ) except KeyError, e: return jsonrpc_error(message='Incorrect JSON query missing %s' % e) @@ -225,21 +227,26 @@ class JSONRPCController(WSGIController): if self._error is not None: raw_response = None - response = dict(result=raw_response, + response = dict(id=self._req_id, result=raw_response, error=self._error) try: return json.dumps(response) except TypeError, e: - log.debug('Error encoding response: %s', e) - return json.dumps(dict(result=None, - error="Error encoding response")) + log.debug('Error encoding response: %s' % e) + return json.dumps( + dict( + self._req_id, + result=None, + error="Error encoding response" + ) + ) def _find_method(self): """ Return method named by `self._req_method` in controller if able """ - log.debug('Trying to find JSON-RPC method: %s', self._req_method) + log.debug('Trying to find JSON-RPC method: %s' % self._req_method) if self._req_method.startswith('_'): raise AttributeError("Method not allowed") @@ -253,4 +260,3 @@ class JSONRPCController(WSGIController): return func else: raise AttributeError("No such method: %s" % self._req_method) - diff --git a/rhodecode/controllers/api/api.py b/rhodecode/controllers/api/api.py --- a/rhodecode/controllers/api/api.py +++ b/rhodecode/controllers/api/api.py @@ -30,17 +30,15 @@ import logging from rhodecode.controllers.api import JSONRPCController, JSONRPCError from rhodecode.lib.auth import HasPermissionAllDecorator, \ - HasPermissionAnyDecorator + HasPermissionAnyDecorator, PasswordGenerator + +from rhodecode.model.meta import Session from rhodecode.model.scm import ScmModel - -from rhodecode.model.db import User, UsersGroup, Group, Repository +from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository from rhodecode.model.repo import RepoModel from rhodecode.model.user import UserModel -from rhodecode.model.repo_permission import RepositoryPermissionModel from rhodecode.model.users_group import UsersGroupModel -from rhodecode.model import users_group from rhodecode.model.repos_group import ReposGroupModel -from sqlalchemy.orm.exc import NoResultFound log = logging.getLogger(__name__) @@ -63,26 +61,26 @@ class ApiController(JSONRPCController): """ @HasPermissionAllDecorator('hg.admin') - def pull(self, apiuser, repo): + def pull(self, apiuser, repo_name): """ Dispatch pull action on given repo :param user: - :param repo: + :param repo_name: """ - if Repository.is_valid(repo) is False: - raise JSONRPCError('Unknown repo "%s"' % repo) + if Repository.is_valid(repo_name) is False: + raise JSONRPCError('Unknown repo "%s"' % repo_name) try: - ScmModel().pull_changes(repo, self.rhodecode_user.username) - return 'Pulled from %s' % repo + ScmModel().pull_changes(repo_name, self.rhodecode_user.username) + return 'Pulled from %s' % repo_name except Exception: - raise JSONRPCError('Unable to pull changes from "%s"' % repo) + raise JSONRPCError('Unable to pull changes from "%s"' % repo_name) @HasPermissionAllDecorator('hg.admin') - def get_user(self, apiuser, username): + def get_user(self, apiuser, userid): """" Get a user by username @@ -90,9 +88,9 @@ class ApiController(JSONRPCController): :param username: """ - user = User.get_by_username(username) - if not user: - return None + user = UserModel().get_user(userid) + if user is None: + return user return dict( id=user.user_id, @@ -102,7 +100,7 @@ class ApiController(JSONRPCController): email=user.email, active=user.active, admin=user.admin, - ldap=user.ldap_dn + ldap_dn=user.ldap_dn ) @HasPermissionAllDecorator('hg.admin') @@ -124,47 +122,85 @@ class ApiController(JSONRPCController): email=user.email, active=user.active, admin=user.admin, - ldap=user.ldap_dn + ldap_dn=user.ldap_dn ) ) return result @HasPermissionAllDecorator('hg.admin') - def create_user(self, apiuser, username, password, firstname, - lastname, email, active=True, admin=False, ldap_dn=None): + def create_user(self, apiuser, username, email, password, firstname=None, + lastname=None, active=True, admin=False, ldap_dn=None): """ Create new user :param apiuser: :param username: :param password: + :param email: :param name: :param lastname: - :param email: :param active: :param admin: :param ldap_dn: """ - if User.get_by_username(username): raise JSONRPCError("user %s already exist" % username) + if User.get_by_email(email, case_insensitive=True): + raise JSONRPCError("email %s already exist" % email) + + if ldap_dn: + # generate temporary password if ldap_dn + password = PasswordGenerator().gen_password(length=8) + try: - form_data = dict(username=username, - password=password, - active=active, - admin=admin, - name=firstname, - lastname=lastname, - email=email, - ldap_dn=ldap_dn) - UserModel().create_ldap(username, password, ldap_dn, form_data) - return dict(msg='created new user %s' % username) + usr = UserModel().create_or_update( + username, password, email, firstname, + lastname, active, admin, ldap_dn + ) + Session.commit() + return dict( + id=usr.user_id, + msg='created new user %s' % username + ) except Exception: log.error(traceback.format_exc()) raise JSONRPCError('failed to create user %s' % username) @HasPermissionAllDecorator('hg.admin') + def update_user(self, apiuser, userid, username, password, email, + firstname, lastname, active, admin, ldap_dn): + """ + Updates given user + + :param apiuser: + :param username: + :param password: + :param email: + :param name: + :param lastname: + :param active: + :param admin: + :param ldap_dn: + """ + if not UserModel().get_user(userid): + raise JSONRPCError("user %s does not exist" % username) + + try: + usr = UserModel().create_or_update( + username, password, email, firstname, + lastname, active, admin, ldap_dn + ) + Session.commit() + return dict( + id=usr.user_id, + msg='updated user %s' % username + ) + except Exception: + log.error(traceback.format_exc()) + raise JSONRPCError('failed to update user %s' % username) + + @HasPermissionAllDecorator('hg.admin') def get_users_group(self, apiuser, group_name): """" Get users group by name @@ -190,7 +226,7 @@ class ApiController(JSONRPCController): ldap=user.ldap_dn)) return dict(id=users_group.users_group_id, - name=users_group.users_group_name, + group_name=users_group.users_group_name, active=users_group.users_group_active, members=members) @@ -217,41 +253,40 @@ class ApiController(JSONRPCController): ldap=user.ldap_dn)) result.append(dict(id=users_group.users_group_id, - name=users_group.users_group_name, + group_name=users_group.users_group_name, active=users_group.users_group_active, members=members)) return result @HasPermissionAllDecorator('hg.admin') - def create_users_group(self, apiuser, name, active=True): + def create_users_group(self, apiuser, group_name, active=True): """ Creates an new usergroup - :param name: + :param group_name: :param active: """ - if self.get_users_group(apiuser, name): - raise JSONRPCError("users group %s already exist" % name) + if self.get_users_group(apiuser, group_name): + raise JSONRPCError("users group %s already exist" % group_name) try: - form_data = dict(users_group_name=name, - users_group_active=active) - ug = UsersGroup.create(form_data) + ug = UsersGroupModel().create(name=group_name, active=active) + Session.commit() return dict(id=ug.users_group_id, - msg='created new users group %s' % name) + msg='created new users group %s' % group_name) except Exception: log.error(traceback.format_exc()) - raise JSONRPCError('failed to create group %s' % name) + raise JSONRPCError('failed to create group %s' % group_name) @HasPermissionAllDecorator('hg.admin') - def add_user_to_users_group(self, apiuser, group_name, user_name): + def add_user_to_users_group(self, apiuser, group_name, username): """" Add a user to a group - :param apiuser - :param group_name - :param user_name + :param apiuser: + :param group_name: + :param username: """ try: @@ -259,32 +294,65 @@ class ApiController(JSONRPCController): if not users_group: raise JSONRPCError('unknown users group %s' % group_name) - try: - user = User.get_by_username(user_name) - except NoResultFound: - raise JSONRPCError('unknown user %s' % user_name) + user = User.get_by_username(username) + if user is None: + raise JSONRPCError('unknown user %s' % username) ugm = UsersGroupModel().add_user_to_group(users_group, user) + success = True if ugm != True else False + msg = 'added member %s to users group %s' % (username, group_name) + msg = msg if success else 'User is already in that group' + Session.commit() - return dict(id=ugm.users_group_member_id, - msg='created new users group member') + return dict( + id=ugm.users_group_member_id if ugm != True else None, + success=success, + msg=msg + ) except Exception: log.error(traceback.format_exc()) - raise JSONRPCError('failed to create users group member') + raise JSONRPCError('failed to add users group member') + + @HasPermissionAllDecorator('hg.admin') + def remove_user_from_users_group(self, apiuser, group_name, username): + """ + Remove user from a group + + :param apiuser + :param group_name + :param username + """ + + try: + users_group = UsersGroup.get_by_group_name(group_name) + if not users_group: + raise JSONRPCError('unknown users group %s' % group_name) + + user = User.get_by_username(username) + if user is None: + raise JSONRPCError('unknown user %s' % username) + + success = UsersGroupModel().remove_user_from_group(users_group, user) + msg = 'removed member %s from users group %s' % (username, group_name) + msg = msg if success else "User wasn't in group" + Session.commit() + return dict(success=success, msg=msg) + except Exception: + log.error(traceback.format_exc()) + raise JSONRPCError('failed to remove user from group') @HasPermissionAnyDecorator('hg.admin') - def get_repo(self, apiuser, name): + def get_repo(self, apiuser, repoid): """" Get repository by name - :param apiuser - :param repo_name + :param apiuser: + :param repo_name: """ - try: - repo = Repository.get_by_repo_name(name) - except NoResultFound: - return None + repo = RepoModel().get_repo(repoid) + if repo is None: + raise JSONRPCError('unknown repository %s' % repo) members = [] for user in repo.repo_to_perm: @@ -319,7 +387,7 @@ class ApiController(JSONRPCController): return dict( id=repo.repo_id, - name=repo.repo_name, + repo_name=repo.repo_name, type=repo.repo_type, description=repo.description, members=members @@ -330,7 +398,7 @@ class ApiController(JSONRPCController): """" Get all repositories - :param apiuser + :param apiuser: """ result = [] @@ -338,85 +406,255 @@ class ApiController(JSONRPCController): result.append( dict( id=repository.repo_id, - name=repository.repo_name, + repo_name=repository.repo_name, type=repository.repo_type, description=repository.description ) ) return result - @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository') - def create_repo(self, apiuser, name, owner_name, description='', - repo_type='hg', private=False): + @HasPermissionAnyDecorator('hg.admin') + def get_repo_nodes(self, apiuser, repo_name, revision, root_path, + ret_type='all'): """ - Create a repository + returns a list of nodes and it's children + for a given path at given revision. It's possible to specify ret_type + to show only files or dirs - :param apiuser - :param name - :param description - :param type - :param private - :param owner_name + :param apiuser: + :param repo_name: name of repository + :param revision: revision for which listing should be done + :param root_path: path from which start displaying + :param ret_type: return type 'all|files|dirs' nodes + """ + try: + _d, _f = ScmModel().get_nodes(repo_name, revision, root_path, + flat=False) + _map = { + 'all': _d + _f, + 'files': _f, + 'dirs': _d, + } + return _map[ret_type] + except KeyError: + raise JSONRPCError('ret_type must be one of %s' % _map.keys()) + except Exception, e: + raise JSONRPCError(e) + + @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository') + def create_repo(self, apiuser, repo_name, owner_name, description='', + repo_type='hg', private=False, clone_uri=None): + """ + Create repository, if clone_url is given it makes a remote clone + + :param apiuser: + :param repo_name: + :param owner_name: + :param description: + :param repo_type: + :param private: + :param clone_uri: """ try: - try: - owner = User.get_by_username(owner_name) - except NoResultFound: - raise JSONRPCError('unknown user %s' % owner) + owner = User.get_by_username(owner_name) + if owner is None: + raise JSONRPCError('unknown user %s' % owner_name) - if self.get_repo(apiuser, name): - raise JSONRPCError("repo %s already exist" % name) + if Repository.get_by_repo_name(repo_name): + raise JSONRPCError("repo %s already exist" % repo_name) - groups = name.split('/') + groups = repo_name.split('/') real_name = groups[-1] groups = groups[:-1] parent_id = None for g in groups: - group = Group.get_by_group_name(g) + group = RepoGroup.get_by_group_name(g) if not group: - group = ReposGroupModel().create(dict(group_name=g, - group_description='', - group_parent_id=parent_id)) + group = ReposGroupModel().create(g, '', parent_id) parent_id = group.group_id - RepoModel().create(dict(repo_name=real_name, - repo_name_full=name, - description=description, - private=private, - repo_type=repo_type, - repo_group=parent_id, - clone_uri=None), owner) + repo = RepoModel().create( + dict( + repo_name=real_name, + repo_name_full=repo_name, + description=description, + private=private, + repo_type=repo_type, + repo_group=parent_id, + clone_uri=clone_uri + ), + owner + ) + Session.commit() + + return dict( + id=repo.repo_id, + msg="Created new repository %s" % repo.repo_name + ) + + except Exception: + log.error(traceback.format_exc()) + raise JSONRPCError('failed to create repository %s' % repo_name) + + @HasPermissionAnyDecorator('hg.admin') + def delete_repo(self, apiuser, repo_name): + """ + Deletes a given repository + + :param repo_name: + """ + if not Repository.get_by_repo_name(repo_name): + raise JSONRPCError("repo %s does not exist" % repo_name) + try: + RepoModel().delete(repo_name) + Session.commit() + return dict( + msg='Deleted repository %s' % repo_name + ) except Exception: log.error(traceback.format_exc()) - raise JSONRPCError('failed to create repository %s' % name) + raise JSONRPCError('failed to delete repository %s' % repo_name) @HasPermissionAnyDecorator('hg.admin') - def add_user_to_repo(self, apiuser, repo_name, user_name, perm): + def grant_user_permission(self, apiuser, repo_name, username, perm): + """ + Grant permission for user on given repository, or update existing one + if found + + :param repo_name: + :param username: + :param perm: """ - Add permission for a user to a repository + + try: + repo = Repository.get_by_repo_name(repo_name) + if repo is None: + raise JSONRPCError('unknown repository %s' % repo) + + user = User.get_by_username(username) + if user is None: + raise JSONRPCError('unknown user %s' % username) + + RepoModel().grant_user_permission(repo=repo, user=user, perm=perm) - :param apiuser - :param repo_name - :param user_name - :param perm + Session.commit() + return dict( + msg='Granted perm: %s for user: %s in repo: %s' % ( + perm, username, repo_name + ) + ) + except Exception: + log.error(traceback.format_exc()) + raise JSONRPCError( + 'failed to edit permission %(repo)s for %(user)s' % dict( + user=username, repo=repo_name + ) + ) + + @HasPermissionAnyDecorator('hg.admin') + def revoke_user_permission(self, apiuser, repo_name, username): + """ + Revoke permission for user on given repository + + :param repo_name: + :param username: """ try: - try: - repo = Repository.get_by_repo_name(repo_name) - except NoResultFound: + repo = Repository.get_by_repo_name(repo_name) + if repo is None: + raise JSONRPCError('unknown repository %s' % repo) + + user = User.get_by_username(username) + if user is None: + raise JSONRPCError('unknown user %s' % username) + + RepoModel().revoke_user_permission(repo=repo_name, user=username) + + Session.commit() + return dict( + msg='Revoked perm for user: %s in repo: %s' % ( + username, repo_name + ) + ) + except Exception: + log.error(traceback.format_exc()) + raise JSONRPCError( + 'failed to edit permission %(repo)s for %(user)s' % dict( + user=username, repo=repo_name + ) + ) + + @HasPermissionAnyDecorator('hg.admin') + def grant_users_group_permission(self, apiuser, repo_name, group_name, perm): + """ + Grant permission for users group on given repository, or update + existing one if found + + :param repo_name: + :param group_name: + :param perm: + """ + + try: + repo = Repository.get_by_repo_name(repo_name) + if repo is None: raise JSONRPCError('unknown repository %s' % repo) - try: - user = User.get_by_username(user_name) - except NoResultFound: - raise JSONRPCError('unknown user %s' % user) + user_group = UsersGroup.get_by_group_name(group_name) + if user_group is None: + raise JSONRPCError('unknown users group %s' % user_group) - RepositoryPermissionModel()\ - .update_or_delete_user_permission(repo, user, perm) + RepoModel().grant_users_group_permission(repo=repo_name, + group_name=group_name, + perm=perm) + + Session.commit() + return dict( + msg='Granted perm: %s for group: %s in repo: %s' % ( + perm, group_name, repo_name + ) + ) except Exception: log.error(traceback.format_exc()) - raise JSONRPCError('failed to edit permission %(repo)s for %(user)s' - % dict(user=user_name, repo=repo_name)) + raise JSONRPCError( + 'failed to edit permission %(repo)s for %(usersgr)s' % dict( + usersgr=group_name, repo=repo_name + ) + ) + + @HasPermissionAnyDecorator('hg.admin') + def revoke_users_group_permission(self, apiuser, repo_name, group_name): + """ + Revoke permission for users group on given repository + + :param repo_name: + :param group_name: + """ + + try: + repo = Repository.get_by_repo_name(repo_name) + if repo is None: + raise JSONRPCError('unknown repository %s' % repo) + user_group = UsersGroup.get_by_group_name(group_name) + if user_group is None: + raise JSONRPCError('unknown users group %s' % user_group) + + RepoModel().revoke_users_group_permission(repo=repo_name, + group_name=group_name) + + Session.commit() + return dict( + msg='Revoked perm for group: %s in repo: %s' % ( + group_name, repo_name + ) + ) + except Exception: + log.error(traceback.format_exc()) + raise JSONRPCError( + 'failed to edit permission %(repo)s for %(usersgr)s' % dict( + usersgr=group_name, repo=repo_name + ) + ) diff --git a/rhodecode/controllers/bookmarks.py b/rhodecode/controllers/bookmarks.py new file mode 100644 --- /dev/null +++ b/rhodecode/controllers/bookmarks.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.controllers.bookmarks + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Bookmarks controller for rhodecode + + :created_on: Dec 1, 2011 + :author: marcink + :copyright: (C) 2011-2012 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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, see . +import logging + +from pylons import tmpl_context as c + +from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator +from rhodecode.lib.base import BaseRepoController, render +from rhodecode.lib.compat import OrderedDict +from webob.exc import HTTPNotFound + +log = logging.getLogger(__name__) + + +class BookmarksController(BaseRepoController): + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', + 'repository.admin') + def __before__(self): + super(BookmarksController, self).__before__() + + def index(self): + if c.rhodecode_repo.alias != 'hg': + raise HTTPNotFound() + + c.repo_bookmarks = OrderedDict() + + bookmarks = [(name, c.rhodecode_repo.get_changeset(hash_)) for \ + name, hash_ in c.rhodecode_repo._repo._bookmarks.items()] + ordered_tags = sorted(bookmarks, key=lambda x: x[1].date, reverse=True) + for name, cs_book in ordered_tags: + c.repo_bookmarks[name] = cs_book + + return render('bookmarks/bookmarks.html') diff --git a/rhodecode/controllers/branches.py b/rhodecode/controllers/branches.py --- a/rhodecode/controllers/branches.py +++ b/rhodecode/controllers/branches.py @@ -7,7 +7,7 @@ :created_on: Apr 21, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -46,33 +46,30 @@ class BranchesController(BaseRepoControl def index(self): def _branchtags(localrepo): - - bt = {} bt_closed = {} - for bn, heads in localrepo.branchmap().iteritems(): tip = heads[-1] - if 'close' not in localrepo.changelog.read(tip)[5]: - bt[bn] = tip - else: + if 'close' in localrepo.changelog.read(tip)[5]: bt_closed[bn] = tip - return bt, bt_closed + return bt_closed + cs_g = c.rhodecode_repo.get_changeset - bt, bt_closed = _branchtags(c.rhodecode_repo._repo) - cs_g = c.rhodecode_repo.get_changeset - _branches = [(safe_unicode(n), cs_g(binascii.hexlify(h)),) for n, h in - bt.items()] + c.repo_closed_branches = {} + if c.rhodecode_db_repo.repo_type == 'hg': + bt_closed = _branchtags(c.rhodecode_repo._repo) + _closed_branches = [(safe_unicode(n), cs_g(binascii.hexlify(h)),) + for n, h in bt_closed.items()] - _closed_branches = [(safe_unicode(n), cs_g(binascii.hexlify(h)),) for n, h in - bt_closed.items()] + c.repo_closed_branches = OrderedDict(sorted(_closed_branches, + key=lambda ctx: ctx[0], + reverse=False)) + _branches = [(safe_unicode(n), cs_g(h)) + for n, h in c.rhodecode_repo.branches.items()] c.repo_branches = OrderedDict(sorted(_branches, key=lambda ctx: ctx[0], reverse=False)) - c.repo_closed_branches = OrderedDict(sorted(_closed_branches, - key=lambda ctx: ctx[0], - reverse=False)) return render('branches/branches.html') diff --git a/rhodecode/controllers/changelog.py b/rhodecode/controllers/changelog.py --- a/rhodecode/controllers/changelog.py +++ b/rhodecode/controllers/changelog.py @@ -7,7 +7,7 @@ :created_on: Apr 21, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -24,15 +24,22 @@ # along with this program. If not, see . import logging +import traceback from mercurial import graphmod -from pylons import request, session, tmpl_context as c +from pylons import request, url, session, tmpl_context as c +from pylons.controllers.util import redirect +from pylons.i18n.translation import _ +import rhodecode.lib.helpers as h from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator from rhodecode.lib.base import BaseRepoController, render from rhodecode.lib.helpers import RepoPage from rhodecode.lib.compat import json +from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError +from rhodecode.model.db import Repository + log = logging.getLogger(__name__) @@ -62,12 +69,32 @@ class ChangelogController(BaseRepoContro p = int(request.params.get('page', 1)) branch_name = request.params.get('branch', None) - c.total_cs = len(c.rhodecode_repo) - c.pagination = RepoPage(c.rhodecode_repo, page=p, - item_count=c.total_cs, items_per_page=c.size, - branch_name=branch_name) + try: + if branch_name: + collection = [z for z in + c.rhodecode_repo.get_changesets(start=0, + branch_name=branch_name)] + c.total_cs = len(collection) + else: + collection = c.rhodecode_repo + c.total_cs = len(c.rhodecode_repo) - self._graph(c.rhodecode_repo, c.total_cs, c.size, p) + c.pagination = RepoPage(collection, page=p, item_count=c.total_cs, + items_per_page=c.size, branch=branch_name) + collection = list(c.pagination) + page_revisions = [x.raw_id for x in collection] + c.comments = c.rhodecode_db_repo.comments(page_revisions) + + except (RepositoryError, ChangesetDoesNotExistError, Exception), e: + log.error(traceback.format_exc()) + h.flash(str(e), category='warning') + return redirect(url('home')) + + self._graph(c.rhodecode_repo, collection, c.total_cs, c.size, p) + + c.branch_name = branch_name + c.branch_filters = [('', _('All Branches'))] + \ + [(k, k) for k in c.rhodecode_repo.branches.keys()] return render('changelog/changelog.html') @@ -76,7 +103,7 @@ class ChangelogController(BaseRepoContro c.cs = c.rhodecode_repo.get_changeset(cs) return render('changelog/changelog_details.html') - def _graph(self, repo, repo_size, size, p): + def _graph(self, repo, collection, repo_size, size, p): """ Generates a DAG graph for mercurial @@ -84,29 +111,20 @@ class ChangelogController(BaseRepoContro :param size: number of commits to show :param p: page number """ - if not repo.revisions: + if not collection: c.jsdata = json.dumps([]) return - revcount = min(repo_size, size) - offset = 1 if p == 1 else ((p - 1) * revcount + 1) - try: - rev_end = repo.revisions.index(repo.revisions[(-1 * offset)]) - except IndexError: - rev_end = repo.revisions.index(repo.revisions[-1]) - rev_start = max(0, rev_end - revcount) - data = [] - rev_end += 1 + revs = [x.revision for x in collection] if repo.alias == 'git': - for _ in xrange(rev_start, rev_end): + for _ in revs: vtx = [0, 1] edges = [[0, 0, 1]] data.append(['', vtx, edges]) elif repo.alias == 'hg': - revs = list(reversed(xrange(rev_start, rev_end))) c.dag = graphmod.colored(graphmod.dagwalker(repo._repo, revs)) for (id, type, ctx, vtx, edges) in c.dag: if type != graphmod.CHANGESET: diff --git a/rhodecode/controllers/changeset.py b/rhodecode/controllers/changeset.py --- a/rhodecode/controllers/changeset.py +++ b/rhodecode/controllers/changeset.py @@ -8,7 +8,7 @@ :created_on: Apr 25, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -25,25 +25,129 @@ # along with this program. If not, see . import logging import traceback +from collections import defaultdict +from webob.exc import HTTPForbidden from pylons import tmpl_context as c, url, request, response from pylons.i18n.translation import _ from pylons.controllers.util import redirect +from pylons.decorators import jsonify + +from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetError, \ + ChangesetDoesNotExistError +from rhodecode.lib.vcs.nodes import FileNode import rhodecode.lib.helpers as h from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator from rhodecode.lib.base import BaseRepoController, render from rhodecode.lib.utils import EmptyChangeset from rhodecode.lib.compat import OrderedDict - -from vcs.exceptions import RepositoryError, ChangesetError, \ -ChangesetDoesNotExistError -from vcs.nodes import FileNode -from vcs.utils import diffs as differ +from rhodecode.lib import diffs +from rhodecode.model.db import ChangesetComment +from rhodecode.model.comment import ChangesetCommentsModel +from rhodecode.model.meta import Session +from rhodecode.lib.diffs import wrapped_diff log = logging.getLogger(__name__) +def anchor_url(revision, path): + fid = h.FID(revision, path) + return h.url.current(anchor=fid, **request.GET) + + +def get_ignore_ws(fid, GET): + ig_ws_global = request.GET.get('ignorews') + ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid)) + if ig_ws: + try: + return int(ig_ws[0].split(':')[-1]) + except: + pass + return ig_ws_global + + +def _ignorews_url(fileid=None): + + params = defaultdict(list) + lbl = _('show white space') + ig_ws = get_ignore_ws(fileid, request.GET) + ln_ctx = get_line_ctx(fileid, request.GET) + # global option + if fileid is None: + if ig_ws is None: + params['ignorews'] += [1] + lbl = _('ignore white space') + ctx_key = 'context' + ctx_val = ln_ctx + # per file options + else: + if ig_ws is None: + params[fileid] += ['WS:1'] + lbl = _('ignore white space') + + ctx_key = fileid + ctx_val = 'C:%s' % ln_ctx + # if we have passed in ln_ctx pass it along to our params + if ln_ctx: + params[ctx_key] += [ctx_val] + + params['anchor'] = fileid + img = h.image('/images/icons/text_strikethrough.png', lbl, class_='icon') + return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip') + + +def get_line_ctx(fid, GET): + ln_ctx_global = request.GET.get('context') + ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid)) + + if ln_ctx: + retval = ln_ctx[0].split(':')[-1] + else: + retval = ln_ctx_global + + try: + return int(retval) + except: + return + + +def _context_url(fileid=None): + """ + Generates url for context lines + + :param fileid: + """ + ig_ws = get_ignore_ws(fileid, request.GET) + ln_ctx = (get_line_ctx(fileid, request.GET) or 3) * 2 + + params = defaultdict(list) + + # global option + if fileid is None: + if ln_ctx > 0: + params['context'] += [ln_ctx] + + if ig_ws: + ig_ws_key = 'ignorews' + ig_ws_val = 1 + + # per file option + else: + params[fileid] += ['C:%s' % ln_ctx] + ig_ws_key = fileid + ig_ws_val = 'WS:%s' % 1 + + if ig_ws: + params[ig_ws_key] += [ig_ws_val] + + lbl = _('%s line context') % ln_ctx + + params['anchor'] = fileid + img = h.image('/images/icons/table_add.png', lbl, class_='icon') + return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip') + + class ChangesetController(BaseRepoController): @LoginRequired() @@ -55,20 +159,16 @@ class ChangesetController(BaseRepoContro def index(self, revision): - def wrap_to_table(str): - - return ''' - - - - -
%s
''' % str + c.anchor_url = anchor_url + c.ignorews_url = _ignorews_url + c.context_url = _context_url #get ranges of revisions if preset rev_range = revision.split('...')[:2] - + enable_comments = True try: if len(rev_range) == 2: + enable_comments = False rev_start = rev_range[0] rev_end = rev_range[1] rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start, @@ -77,6 +177,8 @@ class ChangesetController(BaseRepoContro rev_ranges = [c.rhodecode_repo.get_changeset(revision)] c.cs_ranges = list(rev_ranges) + if not c.cs_ranges: + raise RepositoryError('Changeset range returned empty result') except (RepositoryError, ChangesetDoesNotExistError, Exception), e: log.error(traceback.format_exc()) @@ -84,14 +186,25 @@ class ChangesetController(BaseRepoContro return redirect(url('home')) c.changes = OrderedDict() - c.sum_added = 0 - c.sum_removed = 0 - c.lines_added = 0 - c.lines_deleted = 0 + + c.lines_added = 0 # count of lines added + c.lines_deleted = 0 # count of lines removes + + cumulative_diff = 0 c.cut_off = False # defines if cut off limit is reached + c.comments = [] + c.inline_comments = [] + c.inline_cnt = 0 # Iterate over ranges (default changeset view is always one changeset) for changeset in c.cs_ranges: + c.comments.extend(ChangesetCommentsModel()\ + .get_comments(c.rhodecode_db_repo.repo_id, + changeset.raw_id)) + inlines = ChangesetCommentsModel()\ + .get_inline_comments(c.rhodecode_db_repo.repo_id, + changeset.raw_id) + c.inline_comments.extend(inlines) c.changes[changeset.raw_id] = [] try: changeset_parent = changeset.parents[0] @@ -102,32 +215,19 @@ class ChangesetController(BaseRepoContro # ADDED FILES #================================================================== for node in changeset.added: - - filenode_old = FileNode(node.path, '', EmptyChangeset()) - if filenode_old.is_binary or node.is_binary: - diff = wrap_to_table(_('binary file')) - st = (0, 0) - else: - # in this case node.size is good parameter since those are - # added nodes and their size defines how many changes were - # made - c.sum_added += node.size - if c.sum_added < self.cut_off_limit: - f_gitdiff = differ.get_gitdiff(filenode_old, node) - d = differ.DiffProcessor(f_gitdiff, format='gitdiff') - - st = d.stat() - diff = d.as_html() - - else: - diff = wrap_to_table(_('Changeset is to big and ' - 'was cut off, see raw ' - 'changeset instead')) - c.cut_off = True - break - - cs1 = None - cs2 = node.last_changeset.raw_id + fid = h.FID(revision, node.path) + line_context_lcl = get_line_ctx(fid, request.GET) + ign_whitespace_lcl = get_ignore_ws(fid, request.GET) + lim = self.cut_off_limit + if cumulative_diff > self.cut_off_limit: + lim = -1 + size, cs1, cs2, diff, st = wrapped_diff(filenode_old=None, + filenode_new=node, + cut_off_limit=lim, + ignore_whitespace=ign_whitespace_lcl, + line_context=line_context_lcl, + enable_comments=enable_comments) + cumulative_diff += size c.lines_added += st[0] c.lines_deleted += st[1] c.changes[changeset.raw_id].append(('added', node, diff, @@ -136,55 +236,42 @@ class ChangesetController(BaseRepoContro #================================================================== # CHANGED FILES #================================================================== - if not c.cut_off: - for node in changeset.changed: - try: - filenode_old = changeset_parent.get_node(node.path) - except ChangesetError: - log.warning('Unable to fetch parent node for diff') - filenode_old = FileNode(node.path, '', - EmptyChangeset()) - - if filenode_old.is_binary or node.is_binary: - diff = wrap_to_table(_('binary file')) - st = (0, 0) - else: + for node in changeset.changed: + try: + filenode_old = changeset_parent.get_node(node.path) + except ChangesetError: + log.warning('Unable to fetch parent node for diff') + filenode_old = FileNode(node.path, '', EmptyChangeset()) - if c.sum_removed < self.cut_off_limit: - f_gitdiff = differ.get_gitdiff(filenode_old, node) - d = differ.DiffProcessor(f_gitdiff, - format='gitdiff') - st = d.stat() - if (st[0] + st[1]) * 256 > self.cut_off_limit: - diff = wrap_to_table(_('Diff is to big ' - 'and was cut off, see ' - 'raw diff instead')) - else: - diff = d.as_html() - - if diff: - c.sum_removed += len(diff) - else: - diff = wrap_to_table(_('Changeset is to big and ' - 'was cut off, see raw ' - 'changeset instead')) - c.cut_off = True - break - - cs1 = filenode_old.last_changeset.raw_id - cs2 = node.last_changeset.raw_id - c.lines_added += st[0] - c.lines_deleted += st[1] - c.changes[changeset.raw_id].append(('changed', node, diff, - cs1, cs2, st)) + fid = h.FID(revision, node.path) + line_context_lcl = get_line_ctx(fid, request.GET) + ign_whitespace_lcl = get_ignore_ws(fid, request.GET) + lim = self.cut_off_limit + if cumulative_diff > self.cut_off_limit: + lim = -1 + size, cs1, cs2, diff, st = wrapped_diff(filenode_old=filenode_old, + filenode_new=node, + cut_off_limit=lim, + ignore_whitespace=ign_whitespace_lcl, + line_context=line_context_lcl, + enable_comments=enable_comments) + cumulative_diff += size + c.lines_added += st[0] + c.lines_deleted += st[1] + c.changes[changeset.raw_id].append(('changed', node, diff, + cs1, cs2, st)) #================================================================== # REMOVED FILES #================================================================== - if not c.cut_off: - for node in changeset.removed: - c.changes[changeset.raw_id].append(('removed', node, None, - None, None, (0, 0))) + for node in changeset.removed: + c.changes[changeset.raw_id].append(('removed', node, None, + None, None, (0, 0))) + + # count inline comments + for path, lines in c.inline_comments: + for comments in lines.values(): + c.inline_cnt += len(comments) if len(c.cs_ranges) == 1: c.changeset = c.cs_ranges[0] @@ -197,6 +284,8 @@ class ChangesetController(BaseRepoContro def raw_changeset(self, revision): method = request.GET.get('diff', 'show') + ignore_whitespace = request.GET.get('ignorews') == '1' + line_context = request.GET.get('context', 3) try: c.scm_type = c.rhodecode_repo.alias c.changeset = c.rhodecode_repo.get_changeset(revision) @@ -215,8 +304,10 @@ class ChangesetController(BaseRepoContro if filenode_old.is_binary or node.is_binary: diff = _('binary file') + '\n' else: - f_gitdiff = differ.get_gitdiff(filenode_old, node) - diff = differ.DiffProcessor(f_gitdiff, + f_gitdiff = diffs.get_gitdiff(filenode_old, node, + ignore_whitespace=ignore_whitespace, + context=line_context) + diff = diffs.DiffProcessor(f_gitdiff, format='gitdiff').raw_diff() cs1 = None @@ -228,8 +319,10 @@ class ChangesetController(BaseRepoContro if filenode_old.is_binary or node.is_binary: diff = _('binary file') else: - f_gitdiff = differ.get_gitdiff(filenode_old, node) - diff = differ.DiffProcessor(f_gitdiff, + f_gitdiff = diffs.get_gitdiff(filenode_old, node, + ignore_whitespace=ignore_whitespace, + context=line_context) + diff = diffs.DiffProcessor(f_gitdiff, format='gitdiff').raw_diff() cs1 = filenode_old.last_changeset.raw_id @@ -250,3 +343,25 @@ class ChangesetController(BaseRepoContro c.diffs += x[2] return render('changeset/raw_changeset.html') + + def comment(self, repo_name, revision): + ChangesetCommentsModel().create(text=request.POST.get('text'), + repo_id=c.rhodecode_db_repo.repo_id, + user_id=c.rhodecode_user.user_id, + revision=revision, + f_path=request.POST.get('f_path'), + line_no=request.POST.get('line')) + Session.commit() + return redirect(h.url('changeset_home', repo_name=repo_name, + revision=revision)) + + @jsonify + def delete_comment(self, repo_name, comment_id): + co = ChangesetComment.get(comment_id) + owner = lambda: co.author.user_id == c.rhodecode_user.user_id + if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner: + ChangesetCommentsModel().delete(comment=co) + Session.commit() + return True + else: + raise HTTPForbidden() diff --git a/rhodecode/controllers/error.py b/rhodecode/controllers/error.py --- a/rhodecode/controllers/error.py +++ b/rhodecode/controllers/error.py @@ -7,7 +7,7 @@ :created_on: Dec 8, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -54,7 +54,7 @@ class ErrorController(BaseController): resp = request.environ.get('pylons.original_response') c.rhodecode_name = config.get('rhodecode_title') - log.debug('### %s ###', resp.status) + log.debug('### %s ###' % resp.status) e = request.environ c.serv_p = r'%(protocol)s://%(host)s/' \ diff --git a/rhodecode/controllers/feed.py b/rhodecode/controllers/feed.py --- a/rhodecode/controllers/feed.py +++ b/rhodecode/controllers/feed.py @@ -7,7 +7,7 @@ :created_on: Apr 23, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -51,6 +51,11 @@ class FeedController(BaseRepoController) self.ttl = "5" self.feed_nr = 10 + def _get_title(self, cs): + return "R%s:%s - %s" % ( + cs.revision, cs.short_id, cs.message + ) + def __changes(self, cs): changes = [] @@ -72,18 +77,21 @@ class FeedController(BaseRepoController) def atom(self, repo_name): """Produce an atom-1.0 feed via feedgenerator module""" - feed = Atom1Feed(title=self.title % repo_name, - link=url('summary_home', repo_name=repo_name, - qualified=True), - description=self.description % repo_name, - language=self.language, - ttl=self.ttl) - desc_msg = [] + feed = Atom1Feed( + title=self.title % repo_name, + link=url('summary_home', repo_name=repo_name, + qualified=True), + description=self.description % repo_name, + language=self.language, + ttl=self.ttl + ) + for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])): + desc_msg = [] desc_msg.append('%s - %s
' % (cs.author, cs.date))
             desc_msg.append(self.__changes(cs))
 
-            feed.add_item(title=cs.message,
+            feed.add_item(title=self._get_title(cs),
                           link=url('changeset_home', repo_name=repo_name,
                                    revision=cs.raw_id, qualified=True),
                           author_name=cs.author,
@@ -94,18 +102,21 @@ class FeedController(BaseRepoController)
 
     def rss(self, repo_name):
         """Produce an rss2 feed via feedgenerator module"""
-        feed = Rss201rev2Feed(title=self.title % repo_name,
-                         link=url('summary_home', repo_name=repo_name,
-                                  qualified=True),
-                         description=self.description % repo_name,
-                         language=self.language,
-                         ttl=self.ttl)
-        desc_msg = []
+        feed = Rss201rev2Feed(
+            title=self.title % repo_name,
+            link=url('summary_home', repo_name=repo_name,
+                     qualified=True),
+            description=self.description % repo_name,
+            language=self.language,
+            ttl=self.ttl
+        )
+
         for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
+            desc_msg = []
             desc_msg.append('%s - %s
' % (cs.author, cs.date))
             desc_msg.append(self.__changes(cs))
 
-            feed.add_item(title=cs.message,
+            feed.add_item(title=self._get_title(cs),
                           link=url('changeset_home', repo_name=repo_name,
                                    revision=cs.raw_id, qualified=True),
                           author_name=cs.author,
diff --git a/rhodecode/controllers/files.py b/rhodecode/controllers/files.py
--- a/rhodecode/controllers/files.py
+++ b/rhodecode/controllers/files.py
@@ -27,25 +27,29 @@ import os
 import logging
 import traceback
 
-from os.path import join as jn
-
-from pylons import request, response, session, tmpl_context as c, url
+from pylons import request, response, tmpl_context as c, url
 from pylons.i18n.translation import _
 from pylons.controllers.util import redirect
 from pylons.decorators import jsonify
 
-from vcs.conf import settings
-from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
-    EmptyRepositoryError, ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
-from vcs.nodes import FileNode, NodeKind
-from vcs.utils import diffs as differ
+from rhodecode.lib.vcs.conf import settings
+from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
+    EmptyRepositoryError, ImproperArchiveTypeError, VCSError, \
+    NodeAlreadyExistsError
+from rhodecode.lib.vcs.nodes import FileNode
 
+from rhodecode.lib.compat import OrderedDict
 from rhodecode.lib import convert_line_endings, detect_mode, safe_str
 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
 from rhodecode.lib.base import BaseRepoController, render
 from rhodecode.lib.utils import EmptyChangeset
+from rhodecode.lib import diffs
 import rhodecode.lib.helpers as h
 from rhodecode.model.repo import RepoModel
+from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
+    _context_url, get_line_ctx, get_ignore_ws
+from rhodecode.lib.diffs import wrapped_diff
+from rhodecode.model.scm import ScmModel
 
 log = logging.getLogger(__name__)
 
@@ -104,26 +108,6 @@ class FilesController(BaseRepoController
 
         return file_node
 
-
-    def __get_paths(self, changeset, starting_path):
-        """recursive walk in root dir and return a set of all path in that dir
-        based on repository walk function
-        """
-        _files = list()
-        _dirs = list()
-
-        try:
-            tip = changeset
-            for topnode, dirs, files in tip.walk(starting_path):
-                for f in files:
-                    _files.append(f.path)
-                for d in dirs:
-                    _dirs.append(d.path)
-        except RepositoryError, e:
-            log.debug(traceback.format_exc())
-            pass
-        return _dirs, _files
-
     @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
                                    'repository.admin')
     def index(self, repo_name, revision, f_path):
@@ -162,9 +146,9 @@ class FilesController(BaseRepoController
 
         # files or dirs
         try:
-            c.files_list = c.changeset.get_node(f_path)
+            c.file = c.changeset.get_node(f_path)
 
-            if c.files_list.is_file():
+            if c.file.is_file():
                 c.file_history = self._get_node_history(c.changeset, f_path)
             else:
                 c.file_history = []
@@ -405,13 +389,19 @@ class FilesController(BaseRepoController
     @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
                                    'repository.admin')
     def diff(self, repo_name, f_path):
-        diff1 = request.GET.get('diff1')
-        diff2 = request.GET.get('diff2')
+        ignore_whitespace = request.GET.get('ignorews') == '1'
+        line_context = request.GET.get('context', 3)
+        diff1 = request.GET.get('diff1', '')
+        diff2 = request.GET.get('diff2', '')
         c.action = request.GET.get('diff')
         c.no_changes = diff1 == diff2
         c.f_path = f_path
         c.big_diff = False
-
+        c.anchor_url = anchor_url
+        c.ignorews_url = _ignorews_url
+        c.context_url = _context_url
+        c.changes = OrderedDict()
+        c.changes[diff2] = []
         try:
             if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
                 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
@@ -427,12 +417,14 @@ class FilesController(BaseRepoController
                 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
                 node2 = FileNode('.', '', changeset=c.changeset_2)
         except RepositoryError:
-            return redirect(url('files_home',
-                                repo_name=c.repo_name, f_path=f_path))
+            return redirect(url('files_home', repo_name=c.repo_name,
+                                f_path=f_path))
 
         if c.action == 'download':
-            diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
-                                        format='gitdiff')
+            _diff = diffs.get_gitdiff(node1, node2,
+                                      ignore_whitespace=ignore_whitespace,
+                                      context=line_context)
+            diff = diffs.DiffProcessor(_diff, format='gitdiff')
 
             diff_name = '%s_vs_%s.diff' % (diff1, diff2)
             response.content_type = 'text/plain'
@@ -441,39 +433,28 @@ class FilesController(BaseRepoController
             return diff.raw_diff()
 
         elif c.action == 'raw':
-            diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
-                                        format='gitdiff')
+            _diff = diffs.get_gitdiff(node1, node2,
+                                      ignore_whitespace=ignore_whitespace,
+                                      context=line_context)
+            diff = diffs.DiffProcessor(_diff, format='gitdiff')
             response.content_type = 'text/plain'
             return diff.raw_diff()
 
-        elif c.action == 'diff':
-            if node1.is_binary or node2.is_binary:
-                c.cur_diff = _('Binary file')
-            elif node1.size > self.cut_off_limit or \
-                    node2.size > self.cut_off_limit:
-                c.cur_diff = ''
-                c.big_diff = True
-            else:
-                diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
-                                        format='gitdiff')
-                c.cur_diff = diff.as_html()
         else:
+            fid = h.FID(diff2, node2.path)
+            line_context_lcl = get_line_ctx(fid, request.GET)
+            ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
 
-            #default option
-            if node1.is_binary or node2.is_binary:
-                c.cur_diff = _('Binary file')
-            elif node1.size > self.cut_off_limit or \
-                    node2.size > self.cut_off_limit:
-                c.cur_diff = ''
-                c.big_diff = True
+            lim = request.GET.get('fulldiff') or self.cut_off_limit
+            _, cs1, cs2, diff, st = wrapped_diff(filenode_old=node1,
+                                         filenode_new=node2,
+                                         cut_off_limit=lim,
+                                         ignore_whitespace=ign_whitespace_lcl,
+                                         line_context=line_context_lcl,
+                                         enable_comments=False)
 
-            else:
-                diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
-                                        format='gitdiff')
-                c.cur_diff = diff.as_html()
+            c.changes = [('', node2, diff, cs1, cs2, st,)]
 
-        if not c.cur_diff and not c.big_diff:
-            c.no_changes = True
         return render('files/file_diff.html')
 
     def _get_node_history(self, cs, f_path):
@@ -485,18 +466,16 @@ class FilesController(BaseRepoController
         tags_group = ([], _("Tags"))
 
         for chs in changesets:
-            n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
+            n_desc = 'r%s:%s (%s)' % (chs.revision, chs.short_id, chs.branch)
             changesets_group[0].append((chs.raw_id, n_desc,))
 
         hist_l.append(changesets_group)
 
         for name, chs in c.rhodecode_repo.branches.items():
-            #chs = chs.split(':')[-1]
             branches_group[0].append((chs, name),)
         hist_l.append(branches_group)
 
         for name, chs in c.rhodecode_repo.tags.items():
-            #chs = chs.split(':')[-1]
             tags_group[0].append((chs, name),)
         hist_l.append(tags_group)
 
@@ -508,6 +487,6 @@ class FilesController(BaseRepoController
     def nodelist(self, repo_name, revision, f_path):
         if request.environ.get('HTTP_X_PARTIAL_XHR'):
             cs = self.__get_cs_or_redirect(revision, repo_name)
-            _d, _f = self.__get_paths(cs, f_path)
+            _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
+                                          flat=False)
             return _d + _f
-
diff --git a/rhodecode/controllers/followers.py b/rhodecode/controllers/followers.py
--- a/rhodecode/controllers/followers.py
+++ b/rhodecode/controllers/followers.py
@@ -7,7 +7,7 @@
 
     :created_on: Apr 23, 2011
     :author: marcink
-    :copyright: (C) 2009-2011 Marcin Kuzminski 
+    :copyright: (C) 2011-2012 Marcin Kuzminski 
     :license: GPLv3, see COPYING for more details.
 """
 # This program is free software: you can redistribute it and/or modify
diff --git a/rhodecode/controllers/forks.py b/rhodecode/controllers/forks.py
--- a/rhodecode/controllers/forks.py
+++ b/rhodecode/controllers/forks.py
@@ -7,7 +7,7 @@
 
     :created_on: Apr 23, 2011
     :author: marcink
-    :copyright: (C) 2009-2011 Marcin Kuzminski 
+    :copyright: (C) 2011-2012 Marcin Kuzminski 
     :license: GPLv3, see COPYING for more details.
 """
 # This program is free software: you can redistribute it and/or modify
@@ -23,13 +23,23 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see .
 import logging
+import formencode
+import traceback
+from formencode import htmlfill
 
-from pylons import tmpl_context as c, request
+from pylons import tmpl_context as c, request, url
+from pylons.controllers.util import redirect
+from pylons.i18n.translation import _
+
+import rhodecode.lib.helpers as h
 
 from rhodecode.lib.helpers import Page
-from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
+from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
+    NotAnonymous
 from rhodecode.lib.base import BaseRepoController, render
-from rhodecode.model.db import Repository, User, UserFollowing
+from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User
+from rhodecode.model.repo import RepoModel
+from rhodecode.model.forms import RepoForkForm
 
 log = logging.getLogger(__name__)
 
@@ -37,11 +47,59 @@ log = logging.getLogger(__name__)
 class ForksController(BaseRepoController):
 
     @LoginRequired()
-    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
-                                   'repository.admin')
     def __before__(self):
         super(ForksController, self).__before__()
 
+    def __load_defaults(self):
+        c.repo_groups = RepoGroup.groups_choices()
+        c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
+
+    def __load_data(self, repo_name=None):
+        """
+        Load defaults settings for edit, and update
+
+        :param repo_name:
+        """
+        self.__load_defaults()
+
+        c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
+        repo = db_repo.scm_instance
+
+        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'))
+
+        c.default_user_id = User.get_by_username('default').user_id
+        c.in_public_journal = UserFollowing.query()\
+            .filter(UserFollowing.user_id == c.default_user_id)\
+            .filter(UserFollowing.follows_repository == c.repo_info).scalar()
+
+        if c.repo_info.stats:
+            last_rev = c.repo_info.stats.stat_on_revision+1
+        else:
+            last_rev = 0
+        c.stats_revision = last_rev
+
+        c.repo_last_rev = repo.count() if repo.revisions else 0
+
+        if last_rev == 0 or c.repo_last_rev == 0:
+            c.stats_percentage = 0
+        else:
+            c.stats_percentage = '%.2f' % ((float((last_rev)) /
+                                            c.repo_last_rev) * 100)
+
+        defaults = RepoModel()._get_defaults(repo_name)
+        # add prefix to fork
+        defaults['repo_name'] = 'fork-' + defaults['repo_name']
+        return defaults
+
+    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
+                                   'repository.admin')
     def forks(self, repo_name):
         p = int(request.params.get('page', 1))
         repo_id = c.rhodecode_db_repo.repo_id
@@ -54,3 +112,63 @@ class ForksController(BaseRepoController
             return c.forks_data
 
         return render('/forks/forks.html')
+
+    @NotAnonymous()
+    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
+                                   'repository.admin')
+    def fork(self, repo_name):
+        c.repo_info = Repository.get_by_repo_name(repo_name)
+        if not c.repo_info:
+            h.flash(_('%s repository is not mapped to db perhaps'
+                      ' it was created or renamed from the file system'
+                      ' please run the application again'
+                      ' in order to rescan repositories') % repo_name,
+                      category='error')
+
+            return redirect(url('home'))
+
+        defaults = self.__load_data(repo_name)
+
+        return htmlfill.render(
+            render('forks/fork.html'),
+            defaults=defaults,
+            encoding="UTF-8",
+            force_defaults=False
+        )
+
+
+    @NotAnonymous()
+    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
+                                   'repository.admin')
+    def fork_create(self, repo_name):
+        self.__load_defaults()
+        c.repo_info = Repository.get_by_repo_name(repo_name)
+        _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
+                             repo_groups=c.repo_groups_choices,)()
+        form_result = {}
+        try:
+            form_result = _form.to_python(dict(request.POST))
+            # add org_path of repo so we can do a clone from it later
+            form_result['org_path'] = c.repo_info.repo_name
+
+            # create fork is done sometimes async on celery, db transaction
+            # management is handled there.
+            RepoModel().create_fork(form_result, self.rhodecode_user)
+            h.flash(_('forked %s repository as %s') \
+                      % (repo_name, form_result['repo_name']),
+                    category='success')
+        except formencode.Invalid, errors:
+            c.new_repo = errors.value['repo_name']
+
+            return htmlfill.render(
+                render('forks/fork.html'),
+                defaults=errors.value,
+                errors=errors.error_dict or {},
+                prefix_error=False,
+                encoding="UTF-8")
+        except Exception:
+            log.error(traceback.format_exc())
+            h.flash(_('An error occurred during repository forking %s') %
+                    repo_name, category='error')
+
+        return redirect(url('home'))
diff --git a/rhodecode/controllers/home.py b/rhodecode/controllers/home.py
--- a/rhodecode/controllers/home.py
+++ b/rhodecode/controllers/home.py
@@ -7,7 +7,7 @@
 
     :created_on: Feb 18, 2010
     :author: marcink
-    :copyright: (C) 2009-2011 Marcin Kuzminski 
+    :copyright: (C) 2010-2012 Marcin Kuzminski 
     :license: GPLv3, see COPYING for more details.
 """
 # This program is free software: you can redistribute it and/or modify
@@ -24,14 +24,13 @@
 # along with this program.  If not, see .
 
 import logging
-from operator import itemgetter
 
 from pylons import tmpl_context as c, request
 from paste.httpexceptions import HTTPBadRequest
 
 from rhodecode.lib.auth import LoginRequired
 from rhodecode.lib.base import BaseController, render
-from rhodecode.model.db import Group, Repository
+from rhodecode.model.db import Repository
 
 log = logging.getLogger(__name__)
 
@@ -43,10 +42,8 @@ class HomeController(BaseController):
         super(HomeController, self).__before__()
 
     def index(self):
-
         c.repos_list = self.scm_model.get_repos()
-
-        c.groups = Group.query().filter(Group.group_parent_id == None).all()
+        c.groups = self.scm_model.get_repos_groups()
 
         return render('/index.html')
 
@@ -58,3 +55,11 @@ class HomeController(BaseController):
             return render('/repo_switcher_list.html')
         else:
             return HTTPBadRequest()
+
+    def branch_tag_switcher(self, repo_name):
+        if request.is_xhr:
+            c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
+            c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
+            return render('/switch_to_list.html')
+        else:
+            return HTTPBadRequest()
diff --git a/rhodecode/controllers/journal.py b/rhodecode/controllers/journal.py
--- a/rhodecode/controllers/journal.py
+++ b/rhodecode/controllers/journal.py
@@ -7,7 +7,7 @@
 
     :created_on: Nov 21, 2010
     :author: marcink
-    :copyright: (C) 2009-2011 Marcin Kuzminski 
+    :copyright: (C) 2010-2012 Marcin Kuzminski 
     :license: GPLv3, see COPYING for more details.
 """
 # This program is free software: you can redistribute it and/or modify
@@ -23,21 +23,24 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see .
 import logging
+from itertools import groupby
 
 from sqlalchemy import or_
-from sqlalchemy.orm import joinedload, make_transient
+from sqlalchemy.orm import joinedload
 from webhelpers.paginate import Page
-from itertools import groupby
+from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
 
 from paste.httpexceptions import HTTPBadRequest
 from pylons import request, tmpl_context as c, response, url
 from pylons.i18n.translation import _
-from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
 
 import rhodecode.lib.helpers as h
 from rhodecode.lib.auth import LoginRequired, NotAnonymous
 from rhodecode.lib.base import BaseController, render
-from rhodecode.model.db import UserLog, UserFollowing
+from rhodecode.model.db import UserLog, UserFollowing, Repository, User
+from rhodecode.model.meta import Session
+from sqlalchemy.sql.expression import func
+from rhodecode.model.scm import ScmModel
 
 log = logging.getLogger(__name__)
 
@@ -58,6 +61,13 @@ class JournalController(BaseController):
         # Return a rendered template
         p = int(request.params.get('page', 1))
 
+        c.user = User.get(self.rhodecode_user.user_id)
+        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)
+
         c.following = self.sa.query(UserFollowing)\
             .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
             .options(joinedload(UserFollowing.follows_repository))\
@@ -124,6 +134,7 @@ class JournalController(BaseController):
                 try:
                     self.scm_model.toggle_following_user(user_id,
                                                 self.rhodecode_user.user_id)
+                    Session.commit()
                     return 'ok'
                 except:
                     raise HTTPBadRequest()
@@ -133,11 +144,12 @@ class JournalController(BaseController):
                 try:
                     self.scm_model.toggle_following_repo(repo_id,
                                                 self.rhodecode_user.user_id)
+                    Session.commit()
                     return 'ok'
                 except:
                     raise HTTPBadRequest()
 
-        log.debug('token mismatch %s vs %s', cur_token, token)
+        log.debug('token mismatch %s vs %s' % (cur_token, token))
         raise HTTPBadRequest()
 
     @LoginRequired()
diff --git a/rhodecode/controllers/login.py b/rhodecode/controllers/login.py
--- a/rhodecode/controllers/login.py
+++ b/rhodecode/controllers/login.py
@@ -7,7 +7,7 @@
 
     :created_on: Apr 22, 2010
     :author: marcink
-    :copyright: (C) 2009-2011 Marcin Kuzminski 
+    :copyright: (C) 2010-2012 Marcin Kuzminski 
     :license: GPLv3, see COPYING for more details.
 """
 # This program is free software: you can redistribute it and/or modify
@@ -38,6 +38,7 @@ from rhodecode.lib.base import BaseContr
 from rhodecode.model.db import User
 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
 from rhodecode.model.user import UserModel
+from rhodecode.model.meta import Session
 
 
 log = logging.getLogger(__name__)
@@ -49,7 +50,7 @@ class LoginController(BaseController):
         super(LoginController, self).__before__()
 
     def index(self):
-        #redirect if already logged in
+        # redirect if already logged in
         c.came_from = request.GET.get('came_from', None)
 
         if self.rhodecode_user.is_authenticated \
@@ -58,21 +59,28 @@ class LoginController(BaseController):
             return redirect(url('home'))
 
         if request.POST:
-            #import Login Form validator class
+            # import Login Form validator class
             login_form = LoginForm()
             try:
                 c.form_result = login_form.to_python(dict(request.POST))
-                #form checks for username/password, now we're authenticated
+                # form checks for username/password, now we're authenticated
                 username = c.form_result['username']
                 user = User.get_by_username(username, case_insensitive=True)
                 auth_user = AuthUser(user.user_id)
                 auth_user.set_authenticated()
-                session['rhodecode_user'] = auth_user
+                cs = auth_user.get_cookie_store()
+                session['rhodecode_user'] = cs
+                # If they want to be remembered, update the cookie
+                if c.form_result['remember'] is not False:
+                    session.cookie_expires = False
+                    session._set_cookie_values()
+                session._update_cookie_out()
                 session.save()
 
-                log.info('user %s is now authenticated and stored in session',
-                         username)
+                log.info('user %s is now authenticated and stored in '
+                         'session, session attrs %s' % (username, cs))
                 user.update_lastlogin()
+                Session.commit()
 
                 if c.came_from:
                     return redirect(c.came_from)
@@ -92,7 +100,6 @@ class LoginController(BaseController):
     @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.get_by_username('default').user_perms:
             if perm.permission.permission_name == 'hg.register.auto_activate':
@@ -105,9 +112,10 @@ class LoginController(BaseController):
             try:
                 form_result = register_form.to_python(dict(request.POST))
                 form_result['active'] = c.auto_active
-                user_model.create_registration(form_result)
+                UserModel().create_registration(form_result)
                 h.flash(_('You have successfully registered into rhodecode'),
                             category='success')
+                Session.commit()
                 return redirect(url('login_home'))
 
             except formencode.Invalid, errors:
@@ -121,13 +129,11 @@ class LoginController(BaseController):
         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_link(form_result)
+                UserModel().reset_password_link(form_result)
                 h.flash(_('Your password reset link was sent'),
                             category='success')
                 return redirect(url('login_home'))
@@ -143,13 +149,11 @@ class LoginController(BaseController):
         return render('/password_reset.html')
 
     def password_reset_confirmation(self):
-
         if request.GET and request.GET.get('key'):
             try:
-                user_model = UserModel()
                 user = User.get_by_api_key(request.GET.get('key'))
                 data = dict(email=user.email)
-                user_model.reset_password(data)
+                UserModel().reset_password(data)
                 h.flash(_('Your password reset was successful, '
                           'new password has been sent to your email'),
                             category='success')
@@ -160,7 +164,6 @@ class LoginController(BaseController):
         return redirect(url('login_home'))
 
     def logout(self):
-        del session['rhodecode_user']
-        session.save()
-        log.info('Logging out and setting user as Empty')
+        session.delete()
+        log.info('Logging out and deleting session for user')
         redirect(url('home'))
diff --git a/rhodecode/controllers/search.py b/rhodecode/controllers/search.py
--- a/rhodecode/controllers/search.py
+++ b/rhodecode/controllers/search.py
@@ -7,7 +7,7 @@
 
     :created_on: Aug 7, 2010
     :author: marcink
-    :copyright: (C) 2009-2011 Marcin Kuzminski 
+    :copyright: (C) 2010-2012 Marcin Kuzminski 
     :license: GPLv3, see COPYING for more details.
 """
 # This program is free software: you can redistribute it and/or modify
@@ -26,7 +26,7 @@ import logging
 import traceback
 
 from pylons.i18n.translation import _
-from pylons import request, config, session, tmpl_context as c
+from pylons import request, config, tmpl_context as c
 
 from rhodecode.lib.auth import LoginRequired
 from rhodecode.lib.base import BaseController, render
@@ -76,7 +76,7 @@ class SearchController(BaseController):
                     cur_query = u'repository:%s %s' % (c.repo_name, cur_query)
                 try:
                     query = qp.parse(unicode(cur_query))
-
+                    # extract words for highlight
                     if isinstance(query, Phrase):
                         highlight_items.update(query.words)
                     elif isinstance(query, Prefix):
@@ -92,18 +92,22 @@ class SearchController(BaseController):
                     log.debug(highlight_items)
                     results = searcher.search(query)
                     res_ln = len(results)
-                    c.runtime = '%s results (%.3f seconds)' \
-                        % (res_ln, results.runtime)
+                    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)
 
                     c.formated_results = Page(
-                                ResultWrapper(search_type, searcher, matcher,
-                                              highlight_items),
-                                page=p, item_count=res_ln,
-                                items_per_page=10, url=url_generator)
+                        ResultWrapper(search_type, searcher, matcher,
+                                      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.')
@@ -117,5 +121,6 @@ class SearchController(BaseController):
                 log.error(traceback.format_exc())
                 c.runtime = _('An error occurred during this search operation')
 
+
         # Return a rendered template
         return render('/search/search.html')
diff --git a/rhodecode/controllers/settings.py b/rhodecode/controllers/settings.py
--- a/rhodecode/controllers/settings.py
+++ b/rhodecode/controllers/settings.py
@@ -7,7 +7,7 @@
 
     :created_on: Jun 30, 2010
     :author: marcink
-    :copyright: (C) 2009-2011 Marcin Kuzminski 
+    :copyright: (C) 2010-2012 Marcin Kuzminski 
     :license: GPLv3, see COPYING for more details.
 """
 # This program is free software: you can redistribute it and/or modify
@@ -35,14 +35,14 @@ from pylons.i18n.translation import _
 
 import rhodecode.lib.helpers as h
 
-from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAllDecorator, \
-    HasRepoPermissionAnyDecorator, NotAnonymous
+from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAllDecorator
 from rhodecode.lib.base import BaseRepoController, render
 from rhodecode.lib.utils import invalidate_cache, action_logger
 
-from rhodecode.model.forms import RepoSettingsForm, RepoForkForm
+from rhodecode.model.forms import RepoSettingsForm
 from rhodecode.model.repo import RepoModel
-from rhodecode.model.db import Group
+from rhodecode.model.db import RepoGroup
+from rhodecode.model.meta import Session
 
 log = logging.getLogger(__name__)
 
@@ -52,15 +52,15 @@ class SettingsController(BaseRepoControl
     @LoginRequired()
     def __before__(self):
         super(SettingsController, self).__before__()
-    
+
     def __load_defaults(self):
-        c.repo_groups = Group.groups_choices()
+        c.repo_groups = RepoGroup.groups_choices()
         c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
-        
+
         repo_model = RepoModel()
         c.users_array = repo_model.get_users_js()
         c.users_groups_array = repo_model.get_users_groups_js()
-        
+
     @HasRepoPermissionAllDecorator('repository.admin')
     def index(self, repo_name):
         repo_model = RepoModel()
@@ -89,15 +89,15 @@ class SettingsController(BaseRepoControl
     def update(self, repo_name):
         repo_model = RepoModel()
         changed_name = repo_name
-        
+
         self.__load_defaults()
-        
+
         _form = RepoSettingsForm(edit=True,
                                  old_data={'repo_name': repo_name},
                                  repo_groups=c.repo_groups_choices)()
         try:
             form_result = _form.to_python(dict(request.POST))
-            
+
             repo_model.update(repo_name, form_result)
             invalidate_cache('get_repo_cached_%s' % repo_name)
             h.flash(_('Repository %s updated successfully' % repo_name),
@@ -105,6 +105,7 @@ class SettingsController(BaseRepoControl
             changed_name = form_result['repo_name_full']
             action_logger(self.rhodecode_user, 'user_updated_repo',
                           changed_name, '', self.sa)
+            Session.commit()
         except formencode.Invalid, errors:
             c.repo_info = repo_model.get_by_repo_name(repo_name)
             c.users_array = repo_model.get_users_js()
@@ -148,61 +149,10 @@ class SettingsController(BaseRepoControl
             repo_model.delete(repo)
             invalidate_cache('get_repo_cached_%s' % repo_name)
             h.flash(_('deleted repository %s') % repo_name, category='success')
+            Session.commit()
         except Exception:
             log.error(traceback.format_exc())
             h.flash(_('An error occurred during deletion of %s') % repo_name,
                     category='error')
 
         return redirect(url('home'))
-
-    @NotAnonymous()
-    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
-                                   'repository.admin')
-    def fork(self, repo_name):
-        repo_model = RepoModel()
-        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'
-                      ' it was created or renamed from the file system'
-                      ' please run the application again'
-                      ' in order to rescan repositories') % repo_name,
-                      category='error')
-
-            return redirect(url('home'))
-
-        return render('settings/repo_fork.html')
-
-    @NotAnonymous()
-    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
-                                   'repository.admin')
-    def fork_create(self, repo_name):
-        repo_model = RepoModel()
-        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, self.rhodecode_user)
-            h.flash(_('forked %s repository as %s') \
-                      % (repo_name, form_result['fork_name']),
-                    category='success')
-            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")
-        except Exception:
-            log.error(traceback.format_exc())
-            h.flash(_('An error occurred during repository forking %s') %
-                    repo_name, category='error')
-
-        return redirect(url('home'))
diff --git a/rhodecode/controllers/shortlog.py b/rhodecode/controllers/shortlog.py
--- a/rhodecode/controllers/shortlog.py
+++ b/rhodecode/controllers/shortlog.py
@@ -7,7 +7,7 @@
 
     :created_on: Apr 18, 2010
     :author: marcink
-    :copyright: (C) 2009-2011 Marcin Kuzminski 
+    :copyright: (C) 2010-2012 Marcin Kuzminski 
     :license: GPLv3, see COPYING for more details.
 """
 # This program is free software: you can redistribute it and/or modify
@@ -30,6 +30,7 @@ from pylons import tmpl_context as c, re
 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
 from rhodecode.lib.base import BaseRepoController, render
 from rhodecode.lib.helpers import RepoPage
+from pylons.controllers.util import redirect
 
 log = logging.getLogger(__name__)
 
@@ -50,8 +51,11 @@ class ShortlogController(BaseRepoControl
             return url('shortlog_home', repo_name=repo_name, size=size, **kw)
 
         c.repo_changesets = RepoPage(c.rhodecode_repo, page=p,
-                                                       items_per_page=size,
-                                                       url=url_generator)
+                                    items_per_page=size, url=url_generator)
+
+        if not c.repo_changesets:
+            return redirect(url('summary_home', repo_name=repo_name))
+
         c.shortlog_data = render('shortlog/shortlog_data.html')
         if request.environ.get('HTTP_X_PARTIAL_XHR'):
             return c.shortlog_data
diff --git a/rhodecode/controllers/summary.py b/rhodecode/controllers/summary.py
--- a/rhodecode/controllers/summary.py
+++ b/rhodecode/controllers/summary.py
@@ -7,7 +7,7 @@
 
     :created_on: Apr 18, 2010
     :author: marcink
-    :copyright: (C) 2009-2011 Marcin Kuzminski 
+    :copyright: (C) 2010-2012 Marcin Kuzminski 
     :license: GPLv3, see COPYING for more details.
 """
 # This program is free software: you can redistribute it and/or modify
@@ -23,23 +23,28 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see .
 
+import traceback
 import calendar
 import logging
 from time import mktime
-from datetime import datetime, timedelta, date
+from datetime import timedelta, date
+from itertools import product
+from urlparse import urlparse
 
-from vcs.exceptions import ChangesetError
+from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
+    NodeDoesNotExistError
 
-from pylons import tmpl_context as c, request, url
+from pylons import tmpl_context as c, request, url, config
 from pylons.i18n.translation import _
 
-from rhodecode.model.db import Statistics, Repository
-from rhodecode.model.repo import RepoModel
+from beaker.cache import cache_region, region_invalidate
 
+from rhodecode.model.db import Statistics, CacheInvalidation
+from rhodecode.lib import ALL_READMES, ALL_EXTS
 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
 from rhodecode.lib.base import BaseRepoController, render
 from rhodecode.lib.utils import EmptyChangeset
-
+from rhodecode.lib.markup_renderer import MarkupRenderer
 from rhodecode.lib.celerylib import run_task
 from rhodecode.lib.celerylib.tasks import get_commits_stats, \
     LANGUAGES_EXTENSIONS_MAP
@@ -48,6 +53,10 @@ from rhodecode.lib.compat import json, O
 
 log = logging.getLogger(__name__)
 
+README_FILES = [''.join([x[0][0], x[1][0]]) for x in
+                    sorted(list(product(ALL_READMES, ALL_EXTS)),
+                           key=lambda y:y[0][1] + y[1][1])]
+
 
 class SummaryController(BaseRepoController):
 
@@ -58,10 +67,7 @@ class SummaryController(BaseRepoControll
         super(SummaryController, self).__before__()
 
     def index(self, repo_name):
-
-        e = request.environ
         c.dbrepo = dbrepo = c.rhodecode_db_repo
-
         c.following = self.scm_model.is_following_repo(repo_name,
                                                 self.rhodecode_user.user_id)
 
@@ -72,26 +78,34 @@ class SummaryController(BaseRepoControll
                                      items_per_page=10, url=url_generator)
 
         if self.rhodecode_user.username == 'default':
-            #for default(anonymous) user we don't need to pass credentials
+            # for default(anonymous) user we don't need to pass credentials
             username = ''
             password = ''
         else:
             username = str(self.rhodecode_user.username)
             password = '@'
 
-        if e.get('wsgi.url_scheme') == 'https':
-            split_s = 'https://'
-        else:
-            split_s = 'http://'
+        parsed_url = urlparse(url.current(qualified=True))
+
+        default_clone_uri = '{scheme}://{user}{pass}{netloc}{path}'
+
+        uri_tmpl = config.get('clone_uri', default_clone_uri)
+        uri_tmpl = uri_tmpl.replace('{', '%(').replace('}', ')s')
 
-        qualified_uri = [split_s] + [url.current(qualified=True)\
-                                     .split(split_s)[-1]]
-        uri = u'%(proto)s%(user)s%(pass)s%(rest)s' \
-                % {'user': username,
-                     'pass': password,
-                     'proto': qualified_uri[0],
-                     'rest': qualified_uri[1]}
+        uri_dict = {
+           'user': username,
+           'pass': password,
+           'scheme': parsed_url.scheme,
+           'netloc': parsed_url.netloc,
+           'path': parsed_url.path
+        }
+        uri = uri_tmpl % uri_dict
+        # generate another clone url by id
+        uri_dict.update({'path': '/_%s' % c.dbrepo.repo_id})
+        uri_id = uri_tmpl % uri_dict
+
         c.clone_repo_url = uri
+        c.clone_repo_url_id = uri_id
         c.repo_tags = OrderedDict()
         for name, hash in c.rhodecode_repo.tags.items()[:10]:
             try:
@@ -161,8 +175,44 @@ class SummaryController(BaseRepoControll
         if c.enable_downloads:
             c.download_options = self._get_download_links(c.rhodecode_repo)
 
+        c.readme_data, c.readme_file = self.__get_readme_data(c.rhodecode_repo)
         return render('summary/summary.html')
 
+    def __get_readme_data(self, repo):
+
+        @cache_region('long_term')
+        def _get_readme_from_cache(key):
+            readme_data = None
+            readme_file = None
+            log.debug('Fetching readme file')
+            try:
+                cs = repo.get_changeset('tip')
+                renderer = MarkupRenderer()
+                for f in README_FILES:
+                    try:
+                        readme = cs.get_node(f)
+                        readme_file = f
+                        readme_data = renderer.render(readme.content, f)
+                        log.debug('Found readme %s' % readme_file)
+                        break
+                    except NodeDoesNotExistError:
+                        continue
+            except ChangesetError:
+                pass
+            except EmptyRepositoryError:
+                pass
+            except Exception:
+                log.error(traceback.format_exc())
+
+            return readme_data, readme_file
+
+        key = repo.name + '_README'
+        inv = CacheInvalidation.invalidate(key)
+        if inv is not None:
+            region_invalidate(_get_readme_from_cache, None, key)
+            CacheInvalidation.set_valid(inv.cache_key)
+        return _get_readme_from_cache(key)
+
     def _get_download_links(self, repo):
 
         download_l = []
diff --git a/rhodecode/controllers/tags.py b/rhodecode/controllers/tags.py
--- a/rhodecode/controllers/tags.py
+++ b/rhodecode/controllers/tags.py
@@ -7,7 +7,7 @@
 
     :created_on: Apr 21, 2010
     :author: marcink
-    :copyright: (C) 2009-2011 Marcin Kuzminski 
+    :copyright: (C) 2010-2012 Marcin Kuzminski 
     :license: GPLv3, see COPYING for more details.
 """
 # This program is free software: you can redistribute it and/or modify
diff --git a/rhodecode/lib/__init__.py b/rhodecode/lib/__init__.py
--- a/rhodecode/lib/__init__.py
+++ b/rhodecode/lib/__init__.py
@@ -24,6 +24,9 @@
 # along with this program.  If not, see .
 
 import os
+import re
+from rhodecode.lib.vcs.utils.lazy import LazyProperty
+
 
 def __get_lem():
     from pygments import lexers
@@ -66,6 +69,34 @@ ADDITIONAL_MAPPINGS = {'xaml': 'XAML'}
 
 LANGUAGES_EXTENSIONS_MAP.update(ADDITIONAL_MAPPINGS)
 
+# list of readme files to search in file tree and display in summary
+# attached weights defines the search  order lower is first
+ALL_READMES = [
+    ('readme', 0), ('README', 0), ('Readme', 0),
+    ('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1),
+    ('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2),
+    ('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2),
+    ('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2),
+]
+
+# extension together with weights to search lower is first
+RST_EXTS = [
+    ('', 0), ('.rst', 1), ('.rest', 1),
+    ('.RST', 2), ('.REST', 2),
+    ('.txt', 3), ('.TXT', 3)
+]
+
+MARKDOWN_EXTS = [
+    ('.md', 1), ('.MD', 1),
+    ('.mkdn', 2), ('.MKDN', 2),
+    ('.mdown', 3), ('.MDOWN', 3),
+    ('.markdown', 4), ('.MARKDOWN', 4)
+]
+
+PLAIN_EXTS = [('.text', 2), ('.TEXT', 2)]
+
+ALL_EXTS = MARKDOWN_EXTS + RST_EXTS + PLAIN_EXTS
+
 
 def str2bool(_str):
     """
@@ -107,7 +138,6 @@ def convert_line_endings(line, mode):
             line = replace(line, '\r\n', '\r')
             line = replace(line, '\n', '\r')
     elif mode == 2:
-            import re
             line = re.sub("\r(?!\n)|(?
+    :license: GPLv3, see COPYING for more details.
+"""
+
+from rhodecode.lib.vcs.exceptions import VCSError
+from rhodecode.lib.vcs.nodes import FileNode
+from pygments.formatters import HtmlFormatter
+from pygments import highlight
+
+import StringIO
+
+
+def annotate_highlight(filenode, annotate_from_changeset_func=None,
+        order=None, headers=None, **options):
+    """
+    Returns html portion containing annotated table with 3 columns: line
+    numbers, changeset information and pygmentized line of code.
+
+    :param filenode: FileNode object
+    :param annotate_from_changeset_func: function taking changeset and
+      returning single annotate cell; needs break line at the end
+    :param order: ordered sequence of ``ls`` (line numbers column),
+      ``annotate`` (annotate column), ``code`` (code column); Default is
+      ``['ls', 'annotate', 'code']``
+    :param headers: dictionary with headers (keys are whats in ``order``
+      parameter)
+    """
+    options['linenos'] = True
+    formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
+        headers=headers,
+        annotate_from_changeset_func=annotate_from_changeset_func, **options)
+    lexer = filenode.lexer
+    highlighted = highlight(filenode.content, lexer, formatter)
+    return highlighted
+
+
+class AnnotateHtmlFormatter(HtmlFormatter):
+
+    def __init__(self, filenode, annotate_from_changeset_func=None,
+            order=None, **options):
+        """
+        If ``annotate_from_changeset_func`` is passed it should be a function
+        which returns string from the given changeset. For example, we may pass
+        following function as ``annotate_from_changeset_func``::
+
+            def changeset_to_anchor(changeset):
+                return '%s\n' %\
+                       (changeset.id, changeset.id)
+
+        :param annotate_from_changeset_func: see above
+        :param order: (default: ``['ls', 'annotate', 'code']``); order of
+          columns;
+        :param options: standard pygment's HtmlFormatter options, there is
+          extra option tough, ``headers``. For instance we can pass::
+
+             formatter = AnnotateHtmlFormatter(filenode, headers={
+                'ls': '#',
+                'annotate': 'Annotate',
+                'code': 'Code',
+             })
+
+        """
+        super(AnnotateHtmlFormatter, self).__init__(**options)
+        self.annotate_from_changeset_func = annotate_from_changeset_func
+        self.order = order or ('ls', 'annotate', 'code')
+        headers = options.pop('headers', None)
+        if headers and not ('ls' in headers and 'annotate' in headers and
+            'code' in headers):
+            raise ValueError("If headers option dict is specified it must "
+                "all 'ls', 'annotate' and 'code' keys")
+        self.headers = headers
+        if isinstance(filenode, FileNode):
+            self.filenode = filenode
+        else:
+            raise VCSError("This formatter expect FileNode parameter, not %r"
+                % type(filenode))
+
+    def annotate_from_changeset(self, changeset):
+        """
+        Returns full html line for single changeset per annotated line.
+        """
+        if self.annotate_from_changeset_func:
+            return self.annotate_from_changeset_func(changeset)
+        else:
+            return ''.join((changeset.id, '\n'))
+
+    def _wrap_tablelinenos(self, inner):
+        dummyoutfile = StringIO.StringIO()
+        lncount = 0
+        for t, line in inner:
+            if t:
+                lncount += 1
+            dummyoutfile.write(line)
+
+        fl = self.linenostart
+        mw = len(str(lncount + fl - 1))
+        sp = self.linenospecial
+        st = self.linenostep
+        la = self.lineanchors
+        aln = self.anchorlinenos
+        if sp:
+            lines = []
+
+            for i in range(fl, fl + lncount):
+                if i % st == 0:
+                    if i % sp == 0:
+                        if aln:
+                            lines.append(''
+                                         '%*d' %
+                                         (la, i, mw, i))
+                        else:
+                            lines.append(''
+                                         '%*d' % (mw, i))
+                    else:
+                        if aln:
+                            lines.append(''
+                                         '%*d' % (la, i, mw, i))
+                        else:
+                            lines.append('%*d' % (mw, i))
+                else:
+                    lines.append('')
+            ls = '\n'.join(lines)
+        else:
+            lines = []
+            for i in range(fl, fl + lncount):
+                if i % st == 0:
+                    if aln:
+                        lines.append('%*d' \
+                                     % (la, i, mw, i))
+                    else:
+                        lines.append('%*d' % (mw, i))
+                else:
+                    lines.append('')
+            ls = '\n'.join(lines)
+
+        annotate_changesets = [tup[1] for tup in self.filenode.annotate]
+        # If pygments cropped last lines break we need do that too
+        ln_cs = len(annotate_changesets)
+        ln_ = len(ls.splitlines())
+        if  ln_cs > ln_:
+            annotate_changesets = annotate_changesets[:ln_ - ln_cs]
+        annotate = ''.join((self.annotate_from_changeset(changeset)
+            for changeset in annotate_changesets))
+        # in case you wonder about the seemingly redundant 
here: + # since the content in the other cell also is wrapped in a div, + # some browsers in some configurations seem to mess up the formatting. + ''' + yield 0, ('' % self.cssclass + + '' + + '
' +
+                  ls + '
') + yield 0, dummyoutfile.getvalue() + yield 0, '
' + + ''' + headers_row = [] + if self.headers: + headers_row = [''] + for key in self.order: + td = ''.join(('', self.headers[key], '')) + headers_row.append(td) + headers_row.append('') + + body_row_start = [''] + for key in self.order: + if key == 'ls': + body_row_start.append( + '
' +
+                    ls + '
') + elif key == 'annotate': + body_row_start.append( + '
' +
+                    annotate + '
') + elif key == 'code': + body_row_start.append('') + yield 0, ('' % self.cssclass + + ''.join(headers_row) + + ''.join(body_row_start) + ) + yield 0, dummyoutfile.getvalue() + yield 0, '
' diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -6,7 +6,8 @@ authentication and permission libraries :created_on: Apr 4, 2010 - :copyright: (C) 2009-2011 Marcin Kuzminski + :author: marcink + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -30,11 +31,12 @@ import hashlib from tempfile import _RandomNameSequence from decorator import decorator -from pylons import config, session, url, request +from pylons import config, url, request from pylons.controllers.util import abort, redirect from pylons.i18n.translation import _ from rhodecode import __platform__, PLATFORM_WIN, PLATFORM_OTHERS +from rhodecode.model.meta import Session if __platform__ in PLATFORM_WIN: from hashlib import sha256 @@ -43,20 +45,22 @@ if __platform__ in PLATFORM_OTHERS: from rhodecode.lib import str2bool, safe_unicode from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError -from rhodecode.lib.utils import get_repo_slug +from rhodecode.lib.utils import get_repo_slug, get_repos_group_slug from rhodecode.lib.auth_ldap import AuthLdap from rhodecode.model import meta from rhodecode.model.user import UserModel -from rhodecode.model.db import Permission, RhodeCodeSettings, User +from rhodecode.model.db import Permission, RhodeCodeSetting, User log = logging.getLogger(__name__) class PasswordGenerator(object): - """This is a simple class for generating password from - different sets of characters - usage: + """ + This is a simple class for generating password from different sets of + characters + usage:: + passwd_gen = PasswordGenerator() #print 8-letter password containing only big and small letters of alphabet @@ -128,15 +132,24 @@ def check_password(password, hashed): return RhodeCodeCrypto.hash_check(password, hashed) -def generate_api_key(username, salt=None): +def generate_api_key(str_, salt=None): + """ + Generates API KEY from given string + + :param str_: + :param salt: + """ + if salt is None: salt = _RandomNameSequence().next() - return hashlib.sha1(username + salt).hexdigest() + return hashlib.sha1(str_ + salt).hexdigest() def authfunc(environ, username, password): - """Dummy authentication function used in Mercurial/Git/ and access control, + """ + Dummy authentication wrapper function used in Mercurial and Git for + access control. :param environ: needed only for using in Basic auth """ @@ -144,7 +157,8 @@ def authfunc(environ, username, password def authenticate(username, password): - """Authentication function used for access control, + """ + 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 @@ -159,16 +173,16 @@ def authenticate(username, password): if user is not None and not user.ldap_dn: if user.active: if user.username == 'default' and user.active: - log.info('user %s authenticated correctly as anonymous user', + 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) + log.info('user %s authenticated correctly' % username) return True else: - log.warning('user %s is disabled', username) + log.warning('user %s tried auth but is disabled' % username) else: log.debug('Regular authentication failed') @@ -178,7 +192,7 @@ def authenticate(username, password): log.debug('this user already exists as non ldap') return False - ldap_settings = RhodeCodeSettings.get_ldap_settings() + ldap_settings = RhodeCodeSetting.get_ldap_settings() #====================================================================== # FALLBACK TO LDAP AUTH IF ENABLE #====================================================================== @@ -202,7 +216,7 @@ def authenticate(username, password): aldap = AuthLdap(**kwargs) (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password) - log.debug('Got ldap DN response %s', user_dn) + log.debug('Got ldap DN response %s' % user_dn) get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\ .get(k), [''])[0] @@ -222,6 +236,7 @@ def authenticate(username, password): user_attrs): log.info('created new ldap user %s' % username) + Session.commit() return True except (LdapUsernameError, LdapPasswordError,): pass @@ -231,6 +246,64 @@ def authenticate(username, password): return False +def login_container_auth(username): + user = User.get_by_username(username) + if user is None: + user_attrs = { + 'name': username, + 'lastname': None, + 'email': None, + } + user = UserModel().create_for_container_auth(username, user_attrs) + if not user: + return None + log.info('User %s was created by container authentication' % username) + + if not user.active: + return None + + user.update_lastlogin() + Session.commit() + + log.debug('User %s is now logged in by container authentication', + user.username) + return user + + +def get_container_username(environ, config): + username = None + + if str2bool(config.get('container_auth_enabled', False)): + from paste.httpheaders import REMOTE_USER + username = REMOTE_USER(environ) + + if not username and str2bool(config.get('proxypass_auth_enabled', False)): + username = environ.get('HTTP_X_FORWARDED_USER') + + if username: + # Removing realm and domain from username + username = username.partition('@')[0] + username = username.rpartition('\\')[2] + log.debug('Received username %s from container' % username) + + return username + + +class CookieStoreWrapper(object): + + def __init__(self, cookie_store): + self.cookie_store = cookie_store + + def __repr__(self): + return 'CookieStore<%s>' % (self.cookie_store) + + def get(self, key, other=None): + if isinstance(self.cookie_store, dict): + return self.cookie_store.get(key, other) + elif isinstance(self.cookie_store, AuthUser): + return self.cookie_store.__dict__.get(key, other) + + class AuthUser(object): """ A simple object that handles all attributes of user in RhodeCode @@ -241,12 +314,12 @@ class AuthUser(object): in """ - def __init__(self, user_id=None, api_key=None): + def __init__(self, user_id=None, api_key=None, username=None): self.user_id = user_id self.api_key = None + self.username = username - self.username = 'None' self.name = '' self.lastname = '' self.email = '' @@ -255,51 +328,85 @@ class AuthUser(object): self.permissions = {} self._api_key = api_key self.propagate_data() + self._instance = None def propagate_data(self): user_model = UserModel() - self.anonymous_user = User.get_by_username('default') + self.anonymous_user = User.get_by_username('default', cache=True) + is_user_loaded = False + + # try go get user by api key if self._api_key and self._api_key != self.anonymous_user.api_key: - #try go get user by api key - log.debug('Auth User lookup by API KEY %s', self._api_key) - user_model.fill_data(self, api_key=self._api_key) - else: - log.debug('Auth User lookup by USER ID %s', self.user_id) - if self.user_id is not None \ - and self.user_id != self.anonymous_user.user_id: - user_model.fill_data(self, user_id=self.user_id) + log.debug('Auth User lookup by API KEY %s' % self._api_key) + is_user_loaded = user_model.fill_data(self, api_key=self._api_key) + # lookup by userid + elif (self.user_id is not None and + self.user_id != self.anonymous_user.user_id): + log.debug('Auth User lookup by USER ID %s' % self.user_id) + is_user_loaded = user_model.fill_data(self, user_id=self.user_id) + # lookup by username + elif self.username and \ + str2bool(config.get('container_auth_enabled', False)): + + log.debug('Auth User lookup by USER NAME %s' % self.username) + dbuser = login_container_auth(self.username) + if dbuser is not None: + for k, v in dbuser.get_dict().items(): + setattr(self, k, v) + self.set_authenticated() + is_user_loaded = True + + if not is_user_loaded: + # if we cannot authenticate user try anonymous + if self.anonymous_user.active is True: + user_model.fill_data(self, user_id=self.anonymous_user.user_id) + # then we set this user is logged in + self.is_authenticated = True else: - if self.anonymous_user.active is True: - user_model.fill_data(self, - user_id=self.anonymous_user.user_id) - #then we set this user is logged in - self.is_authenticated = True - else: - self.is_authenticated = False + self.user_id = None + self.username = None + self.is_authenticated = False - log.debug('Auth User is now %s', self) + if not self.username: + self.username = 'None' + + log.debug('Auth User is now %s' % self) user_model.fill_perms(self) @property def is_admin(self): return self.admin - @property - def full_contact(self): - return '%s %s <%s>' % (self.name, self.lastname, self.email) - def __repr__(self): return "" % (self.user_id, self.username, self.is_authenticated) def set_authenticated(self, authenticated=True): - if self.user_id != self.anonymous_user.user_id: self.is_authenticated = authenticated + def get_cookie_store(self): + return {'username': self.username, + 'user_id': self.user_id, + 'is_authenticated': self.is_authenticated} + + @classmethod + def from_cookie_store(cls, cookie_store): + """ + Creates AuthUser from a cookie store + + :param cls: + :param cookie_store: + """ + user_id = cookie_store.get('user_id') + username = cookie_store.get('username') + api_key = cookie_store.get('api_key') + return AuthUser(user_id, api_key, username) + def set_available_permissions(config): - """This function will propagate pylons globals with all available defined + """ + This function will propagate pylons globals with all available defined permission given in db. We don't want to check each time from db for new permissions since adding a new permission also requires application restart ie. to decorate new views with the newly created permission @@ -309,9 +416,9 @@ def set_available_permissions(config): """ log.info('getting information about all available permissions') try: - sa = meta.Session() + sa = meta.Session all_perms = sa.query(Permission).all() - except: + except Exception: pass finally: meta.Session.remove() @@ -343,26 +450,31 @@ class LoginRequired(object): api_access_ok = False if self.api_access: - log.debug('Checking API KEY access for %s', cls) + log.debug('Checking API KEY access for %s' % cls) if user.api_key == request.GET.get('api_key'): api_access_ok = True else: log.debug("API KEY token not valid") - - log.debug('Checking if %s is authenticated @ %s', user.username, cls) + loc = "%s:%s" % (cls.__class__.__name__, func.__name__) + log.debug('Checking if %s is authenticated @ %s' % (user.username, loc)) if user.is_authenticated or api_access_ok: - log.debug('user %s is authenticated', user.username) + log.info('user %s is authenticated and granted access to %s' % ( + user.username, loc) + ) return func(*fargs, **fkwargs) else: - log.warn('user %s NOT authenticated', user.username) + log.warn('user %s NOT authenticated on func: %s' % ( + user, loc) + ) p = url.current() - 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 + """ + Must be logged in to execute this function else redirect to login page""" def __call__(self, func): @@ -372,7 +484,7 @@ class NotAnonymous(object): cls = fargs[0] self.user = cls.rhodecode_user - log.debug('Checking if user is not anonymous @%s', cls) + log.debug('Checking if user is not anonymous @%s' % cls) anonymous = self.user.username == 'default' @@ -411,13 +523,11 @@ class PermsDecorator(object): self.user) if self.check_permissions(): - log.debug('Permission granted for %s %s', cls, self.user) + log.debug('Permission granted for %s %s' % (cls, self.user)) return func(*fargs, **fkwargs) else: - log.warning('Permission denied for %s %s', cls, self.user) - - + log.debug('Permission denied for %s %s' % (cls, self.user)) anonymous = self.user.username == 'default' if anonymous: @@ -430,7 +540,7 @@ class PermsDecorator(object): return redirect(url('login_home', came_from=p)) else: - #redirect with forbidden ret code + # redirect with forbidden ret code return abort(403) def check_permissions(self): @@ -439,7 +549,8 @@ class PermsDecorator(object): class HasPermissionAllDecorator(PermsDecorator): - """Checks for access permission for all given predicates. All of them + """ + Checks for access permission for all given predicates. All of them have to be meet in order to fulfill the request """ @@ -450,7 +561,8 @@ class HasPermissionAllDecorator(PermsDec class HasPermissionAnyDecorator(PermsDecorator): - """Checks for access permission for any of given predicates. In order to + """ + Checks for access permission for any of given predicates. In order to fulfill the request any of predicates must be meet """ @@ -461,7 +573,8 @@ class HasPermissionAnyDecorator(PermsDec class HasRepoPermissionAllDecorator(PermsDecorator): - """Checks for access permission for all given predicates for specific + """ + Checks for access permission for all given predicates for specific repository. All of them have to be meet in order to fulfill the request """ @@ -477,7 +590,8 @@ class HasRepoPermissionAllDecorator(Perm class HasRepoPermissionAnyDecorator(PermsDecorator): - """Checks for access permission for any of given predicates for specific + """ + Checks for access permission for any of given predicates for specific repository. In order to fulfill the request any of predicates must be meet """ @@ -493,6 +607,41 @@ class HasRepoPermissionAnyDecorator(Perm return False +class HasReposGroupPermissionAllDecorator(PermsDecorator): + """ + 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): + group_name = get_repos_group_slug(request) + try: + user_perms = set([self.user_perms['repositories_groups'][group_name]]) + except KeyError: + return False + if self.required_perms.issubset(user_perms): + return True + return False + + +class HasReposGroupPermissionAnyDecorator(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): + group_name = get_repos_group_slug(request) + + try: + user_perms = set([self.user_perms['repositories_groups'][group_name]]) + except KeyError: + return False + if self.required_perms.intersection(user_perms): + return True + return False + + #============================================================================== # CHECK FUNCTIONS #============================================================================== @@ -511,7 +660,7 @@ class PermsFunction(object): self.repo_name = None def __call__(self, check_Location=''): - user = session.get('rhodecode_user', False) + user = request.user if not user: return False self.user_perms = user.permissions @@ -525,7 +674,7 @@ class PermsFunction(object): return True else: - log.warning('Permission denied for %s @ %s', self.granted_for, + log.debug('Permission denied for %s @ %s', self.granted_for, check_Location or 'unspecified location') return False @@ -559,8 +708,9 @@ class HasRepoPermissionAll(PermsFunction self.repo_name = get_repo_slug(request) try: - self.user_perms = set([self.user_perms['reposit' - 'ories'][self.repo_name]]) + self.user_perms = set( + [self.user_perms['repositories'][self.repo_name]] + ) except KeyError: return False self.granted_for = self.repo_name @@ -580,8 +730,9 @@ class HasRepoPermissionAny(PermsFunction self.repo_name = get_repo_slug(request) try: - self.user_perms = set([self.user_perms['reposi' - 'tories'][self.repo_name]]) + self.user_perms = set( + [self.user_perms['repositories'][self.repo_name]] + ) except KeyError: return False self.granted_for = self.repo_name @@ -590,6 +741,42 @@ class HasRepoPermissionAny(PermsFunction return False +class HasReposGroupPermissionAny(PermsFunction): + def __call__(self, group_name=None, check_Location=''): + self.group_name = group_name + return super(HasReposGroupPermissionAny, self).__call__(check_Location) + + def check_permissions(self): + try: + self.user_perms = set( + [self.user_perms['repositories_groups'][self.group_name]] + ) + except KeyError: + return False + self.granted_for = self.repo_name + if self.required_perms.intersection(self.user_perms): + return True + return False + + +class HasReposGroupPermissionAll(PermsFunction): + def __call__(self, group_name=None, check_Location=''): + self.group_name = group_name + return super(HasReposGroupPermissionAny, self).__call__(check_Location) + + def check_permissions(self): + try: + self.user_perms = set( + [self.user_perms['repositories_groups'][self.group_name]] + ) + except KeyError: + return False + self.granted_for = self.repo_name + if self.required_perms.issubset(self.user_perms): + return True + return False + + #============================================================================== # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH #============================================================================== diff --git a/rhodecode/lib/auth_ldap.py b/rhodecode/lib/auth_ldap.py --- a/rhodecode/lib/auth_ldap.py +++ b/rhodecode/lib/auth_ldap.py @@ -7,7 +7,7 @@ :created_on: Created on Nov 17, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -43,8 +43,7 @@ class AuthLdap(object): def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='', tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3, ldap_filter='(&(objectClass=user)(!(objectClass=computer)))', - search_scope='SUBTREE', - attr_login='uid'): + search_scope='SUBTREE', attr_login='uid'): self.ldap_version = ldap_version ldap_server_type = 'ldap' @@ -53,14 +52,14 @@ class AuthLdap(object): if self.TLS_KIND == 'LDAPS': port = port or 689 ldap_server_type = ldap_server_type + 's' - + OPT_X_TLS_DEMAND = 2 - self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, + self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND) self.LDAP_SERVER_ADDRESS = server self.LDAP_SERVER_PORT = port - #USE FOR READ ONLY BIND TO LDAP SERVER + # USE FOR READ ONLY BIND TO LDAP SERVER self.LDAP_BIND_DN = bind_dn self.LDAP_BIND_PASS = bind_pass @@ -74,7 +73,8 @@ class AuthLdap(object): self.attr_login = attr_login def authenticate_ldap(self, username, password): - """Authenticate a user via LDAP and return his/her LDAP properties. + """ + 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. @@ -87,11 +87,15 @@ class AuthLdap(object): uid = chop_at(username, "@%s" % self.LDAP_SERVER_ADDRESS) + if not password: + log.debug("Attempt to authenticate LDAP user " + "with blank password rejected.") + raise LdapPasswordError() if "," in username: raise LdapUsernameError("invalid character in username: ,") try: - if hasattr(ldap,'OPT_X_TLS_CACERTDIR'): - ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, + if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'): + ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, '/etc/openldap/cacerts') ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF) ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON) @@ -112,12 +116,12 @@ class AuthLdap(object): if self.LDAP_BIND_DN and self.LDAP_BIND_PASS: server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS) - filt = '(&%s(%s=%s))' % (self.LDAP_FILTER, self.attr_login, + filter_ = '(&%s(%s=%s))' % (self.LDAP_FILTER, self.attr_login, username) - log.debug("Authenticating %r filt %s at %s", self.BASE_DN, - filt, self.LDAP_SERVER) + log.debug("Authenticating %r filter %s at %s", self.BASE_DN, + filter_, self.LDAP_SERVER) lobjects = server.search_ext_s(self.BASE_DN, self.SEARCH_SCOPE, - filt) + filter_) if not lobjects: raise ldap.NO_SUCH_OBJECT() @@ -127,24 +131,28 @@ class AuthLdap(object): continue try: + log.debug('Trying simple bind with %s' % dn) server.simple_bind_s(dn, password) attrs = server.search_ext_s(dn, ldap.SCOPE_BASE, '(objectClass=*)')[0][1] break - except ldap.INVALID_CREDENTIALS, e: - log.debug("LDAP rejected password for user '%s' (%s): %s", - uid, username, dn) + except ldap.INVALID_CREDENTIALS: + log.debug( + "LDAP rejected password for user '%s' (%s): %s" % ( + uid, username, dn + ) + ) else: log.debug("No matching LDAP objects for authentication " "of '%s' (%s)", uid, username) raise LdapPasswordError() - except ldap.NO_SUCH_OBJECT, e: - log.debug("LDAP says no such user '%s' (%s)", uid, username) + except ldap.NO_SUCH_OBJECT: + log.debug("LDAP says no such user '%s' (%s)" % (uid, username)) raise LdapUsernameError() - except ldap.SERVER_DOWN, e: + except ldap.SERVER_DOWN: raise LdapConnectionError("LDAP can't access " "authentication server") diff --git a/rhodecode/lib/backup_manager.py b/rhodecode/lib/backup_manager.py --- a/rhodecode/lib/backup_manager.py +++ b/rhodecode/lib/backup_manager.py @@ -3,11 +3,12 @@ rhodecode.lib.backup_manager ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Mercurial repositories backup manager, it allows to backups all + Mercurial repositories backup manager, it allows to backups all repositories and send it to backup server using RSA key via ssh. :created_on: Feb 28, 2010 - :copyright: (C) 2009-2011 Marcin Kuzminski + :author: marcink + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -3,35 +3,125 @@ Provides the BaseController class for subclassing. """ import logging +import time +import traceback + +from paste.auth.basic import AuthBasicAuthenticator from pylons import config, tmpl_context as c, request, session, url from pylons.controllers import WSGIController from pylons.controllers.util import redirect from pylons.templating import render_mako as render -from rhodecode import __version__ -from rhodecode.lib import str2bool -from rhodecode.lib.auth import AuthUser -from rhodecode.lib.utils import get_repo_slug +from rhodecode import __version__, BACKENDS + +from rhodecode.lib import str2bool, safe_unicode +from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\ + HasPermissionAnyMiddleware, CookieStoreWrapper +from rhodecode.lib.utils import get_repo_slug, invalidate_cache from rhodecode.model import meta + +from rhodecode.model.db import Repository +from rhodecode.model.notification import NotificationModel from rhodecode.model.scm import ScmModel -from rhodecode import BACKENDS -from rhodecode.model.db import Repository log = logging.getLogger(__name__) + +class BaseVCSController(object): + + def __init__(self, application, config): + self.application = application + self.config = config + # base path of repo locations + self.basepath = self.config['base_path'] + #authenticate this mercurial request using authfunc + self.authenticate = AuthBasicAuthenticator('', authfunc) + self.ipaddr = '0.0.0.0' + + def _handle_request(self, environ, start_response): + raise NotImplementedError() + + def _get_by_id(self, repo_name): + """ + Get's a special pattern _ from clone url and tries to replace it + with a repository_name for support of _ non changable urls + + :param repo_name: + """ + try: + data = repo_name.split('/') + if len(data) >= 2: + by_id = data[1].split('_') + if len(by_id) == 2 and by_id[1].isdigit(): + _repo_name = Repository.get(by_id[1]).repo_name + data[1] = _repo_name + except: + log.debug('Failed to extract repo_name from id %s' % ( + traceback.format_exc() + ) + ) + + return '/'.join(data) + + def _invalidate_cache(self, repo_name): + """ + Set's cache for this repository for invalidation on next access + + :param repo_name: full repo name, also a cache key + """ + invalidate_cache('get_repo_cached_%s' % repo_name) + + def _check_permission(self, action, user, repo_name): + """ + Checks permissions using action (push/pull) user and repository + name + + :param action: push or pull action + :param user: user instance + :param repo_name: repository name + """ + if action == 'push': + if not HasPermissionAnyMiddleware('repository.write', + 'repository.admin')(user, + repo_name): + return False + + else: + #any other action need at least read permission + if not HasPermissionAnyMiddleware('repository.read', + 'repository.write', + 'repository.admin')(user, + repo_name): + return False + + return True + + def __call__(self, environ, start_response): + start = time.time() + try: + return self._handle_request(environ, start_response) + finally: + log = logging.getLogger('rhodecode.' + self.__class__.__name__) + log.debug('Request time: %.3fs' % (time.time() - start)) + meta.Session.remove() + + class BaseController(WSGIController): def __before__(self): c.rhodecode_version = __version__ + c.rhodecode_instanceid = config.get('instance_id') c.rhodecode_name = config.get('rhodecode_title') c.use_gravatar = str2bool(config.get('use_gravatar')) c.ga_code = config.get('rhodecode_ga_code') c.repo_name = get_repo_slug(request) c.backends = BACKENDS.keys() + c.unread_notifications = NotificationModel()\ + .get_unread_cnt_for_user(c.rhodecode_user.user_id) self.cut_off_limit = int(config.get('cut_off_limit')) - self.sa = meta.Session() + self.sa = meta.Session self.scm_model = ScmModel(self.sa) def __call__(self, environ, start_response): @@ -39,18 +129,30 @@ class BaseController(WSGIController): # WSGIController.__call__ dispatches to the Controller method # the request is routed to. This routing information is # available in environ['pylons.routes_dict'] + start = time.time() try: - # putting this here makes sure that we update permissions each time + # make sure that we update permissions each time we call controller api_key = request.GET.get('api_key') - user_id = getattr(session.get('rhodecode_user'), 'user_id', None) - self.rhodecode_user = c.rhodecode_user = AuthUser(user_id, api_key) - self.rhodecode_user.set_authenticated( - getattr(session.get('rhodecode_user'), - 'is_authenticated', False)) - session['rhodecode_user'] = self.rhodecode_user - session.save() + cookie_store = CookieStoreWrapper(session.get('rhodecode_user')) + user_id = cookie_store.get('user_id', None) + username = get_container_username(environ, config) + + auth_user = AuthUser(user_id, api_key, username) + request.user = auth_user + self.rhodecode_user = c.rhodecode_user = auth_user + if not self.rhodecode_user.is_authenticated and \ + self.rhodecode_user.user_id is not None: + self.rhodecode_user.set_authenticated( + cookie_store.get('is_authenticated') + ) + log.info('User: %s accessed %s' % ( + auth_user, safe_unicode(environ.get('PATH_INFO'))) + ) return WSGIController.__call__(self, environ, start_response) finally: + log.info('Request to %s time: %.3fs' % ( + safe_unicode(environ.get('PATH_INFO')), time.time() - start) + ) meta.Session.remove() @@ -80,4 +182,3 @@ class BaseRepoController(BaseController) c.repository_followers = self.scm_model.get_followers(c.repo_name) c.repository_forks = self.scm_model.get_forks(c.repo_name) - diff --git a/rhodecode/model/caching_query.py b/rhodecode/lib/caching_query.py rename from rhodecode/model/caching_query.py rename to rhodecode/lib/caching_query.py --- a/rhodecode/model/caching_query.py +++ b/rhodecode/lib/caching_query.py @@ -137,8 +137,13 @@ def _get_cache_parameters(query): if cache_key is None: # cache key - the value arguments from this query's parameters. - args = _params_from_query(query) - cache_key = " ".join([str(x) for x in args]) + args = [str(x) for x in _params_from_query(query)] + args.extend(filter(lambda k:k not in ['None', None, u'None'], + [str(query._limit), str(query._offset)])) + cache_key = " ".join(args) + + if cache_key is None: + raise Exception('Cache key cannot be None') # get cache #cache = query.cache_manager.get_cache_region(namespace, region) @@ -275,15 +280,20 @@ def _params_from_query(query): """ 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() + if bind.key in query._params: + value = query._params[bind.key] + elif bind.callable: + # lazyloader may dig a callable in here, intended + # to late-evaluate params after autoflush is called. + # convert to a scalar value. + value = bind.callable() + else: + value = bind.value v.append(value) if query._criterion is not None: visitors.traverse(query._criterion, {}, {'bindparam':visit_bindparam}) + for f in query._from_obj: + visitors.traverse(f, {}, {'bindparam':visit_bindparam}) return v diff --git a/rhodecode/lib/celerylib/__init__.py b/rhodecode/lib/celerylib/__init__.py --- a/rhodecode/lib/celerylib/__init__.py +++ b/rhodecode/lib/celerylib/__init__.py @@ -34,8 +34,8 @@ from pylons import config from hashlib import md5 from decorator import decorator -from vcs.utils.lazy import LazyProperty - +from rhodecode.lib.vcs.utils.lazy import LazyProperty +from rhodecode import CELERY_ON from rhodecode.lib import str2bool, safe_str from rhodecode.lib.pidlock import DaemonLock, LockHeld from rhodecode.model import init_model @@ -48,11 +48,6 @@ from celery.messaging import establish_c log = logging.getLogger(__name__) -try: - CELERY_ON = str2bool(config['app_conf'].get('use_celery')) -except KeyError: - CELERY_ON = False - class ResultWrapper(object): def __init__(self, task): @@ -67,7 +62,7 @@ def run_task(task, *args, **kwargs): if CELERY_ON: try: t = task.apply_async(args=args, kwargs=kwargs) - log.info('running task %s:%s', t.task_id, task) + log.info('running task %s:%s' % (t.task_id, task)) return t except socket.error, e: @@ -80,7 +75,7 @@ def run_task(task, *args, **kwargs): except Exception, e: log.error(traceback.format_exc()) - log.debug('executing task %s in sync mode', task) + log.debug('executing task %s in sync mode' % task) return ResultWrapper(task(*args, **kwargs)) @@ -100,7 +95,7 @@ def locked_task(func): lockkey = __get_lockkey(func, *fargs, **fkwargs) lockkey_path = config['here'] - log.info('running task with lockkey %s', lockkey) + log.info('running task with lockkey %s' % lockkey) try: l = DaemonLock(file_=jn(lockkey_path, lockkey)) ret = func(*fargs, **fkwargs) diff --git a/rhodecode/lib/celerylib/tasks.py b/rhodecode/lib/celerylib/tasks.py --- a/rhodecode/lib/celerylib/tasks.py +++ b/rhodecode/lib/celerylib/tasks.py @@ -28,7 +28,7 @@ from celery.decorators import task import os import traceback import logging -from os.path import dirname as dn, join as jn +from os.path import join as jn from time import mktime from operator import itemgetter @@ -37,69 +37,76 @@ from string import lower from pylons import config, url from pylons.i18n.translation import _ +from rhodecode.lib.vcs import get_backend + +from rhodecode import CELERY_ON from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, safe_str -from rhodecode.lib.celerylib import run_task, locked_task, str2bool, \ - __get_lockkey, LockHeld, DaemonLock, get_session, dbsession +from rhodecode.lib.celerylib import run_task, locked_task, dbsession, \ + str2bool, __get_lockkey, LockHeld, DaemonLock, get_session from rhodecode.lib.helpers import person -from rhodecode.lib.smtp_mailer import SmtpMailer -from rhodecode.lib.utils import add_cache +from rhodecode.lib.rcmail.smtp_mailer import SmtpMailer +from rhodecode.lib.utils import add_cache, action_logger from rhodecode.lib.compat import json, OrderedDict -from rhodecode.model.db import RhodeCodeUi, Statistics, Repository, User +from rhodecode.model.db import Statistics, Repository, User -from vcs.backends import get_repo -from vcs import get_backend add_cache(config) __all__ = ['whoosh_index', 'get_commits_stats', 'reset_user_password', 'send_email'] -CELERY_ON = str2bool(config['app_conf'].get('use_celery')) - -def get_repos_path(): - sa = get_session() - q = sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one() - return q.ui_value +def get_logger(cls): + if CELERY_ON: + try: + log = cls.get_logger() + except: + log = logging.getLogger(__name__) + else: + log = logging.getLogger(__name__) + + return log @task(ignore_result=True) @locked_task @dbsession def whoosh_index(repo_location, full_index): - #log = whoosh_index.get_logger() from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon + log = whoosh_index.get_logger(whoosh_index) + DBS = get_session() + index_location = config['index_dir'] WhooshIndexingDaemon(index_location=index_location, - repo_location=repo_location, sa=get_session())\ + repo_location=repo_location, sa=DBS)\ .run(full_index=full_index) @task(ignore_result=True) @dbsession def get_commits_stats(repo_name, ts_min_y, ts_max_y): - try: - log = get_commits_stats.get_logger() - except: - log = logging.getLogger(__name__) - + log = get_logger(get_commits_stats) + DBS = get_session() lockkey = __get_lockkey('get_commits_stats', repo_name, ts_min_y, ts_max_y) lockkey_path = config['here'] - log.info('running task with lockkey %s', lockkey) + log.info('running task with lockkey %s' % lockkey) + try: - sa = get_session() lock = l = DaemonLock(file_=jn(lockkey_path, lockkey)) - # for js data compatibilty cleans the key for person from ' + # for js data compatibility cleans the key for person from ' akc = lambda k: person(k).replace('"', "") co_day_auth_aggr = {} commits_by_day_aggregate = {} - repos_path = get_repos_path() - repo = get_repo(safe_str(os.path.join(repos_path, repo_name))) + repo = Repository.get_by_repo_name(repo_name) + if repo is None: + return True + + repo = repo.scm_instance repo_size = repo.count() # return if repo have no revisions if repo_size < 1: @@ -112,9 +119,9 @@ def get_commits_stats(repo_name, ts_min_ last_cs = None timegetter = itemgetter('time') - dbrepo = sa.query(Repository)\ + dbrepo = DBS.query(Repository)\ .filter(Repository.repo_name == repo_name).scalar() - cur_stats = sa.query(Statistics)\ + cur_stats = DBS.query(Statistics)\ .filter(Statistics.repository == dbrepo).scalar() if cur_stats is not None: @@ -132,7 +139,7 @@ def get_commits_stats(repo_name, ts_min_ cur_stats.commit_activity_combined)) co_day_auth_aggr = json.loads(cur_stats.commit_activity) - log.debug('starting parsing %s', parse_limit) + log.debug('starting parsing %s' % parse_limit) lmktime = mktime last_rev = last_rev + 1 if last_rev >= 0 else 0 @@ -207,9 +214,9 @@ def get_commits_stats(repo_name, ts_min_ stats.commit_activity = json.dumps(co_day_auth_aggr) stats.commit_activity_combined = json.dumps(overview_data) - log.debug('last revison %s', last_rev) + log.debug('last revison %s' % last_rev) leftovers = len(repo.revisions[last_rev:]) - log.debug('revisions to parse %s', leftovers) + log.debug('revisions to parse %s' % leftovers) if last_rev == 0 or leftovers < parse_limit: log.debug('getting code trending stats') @@ -218,18 +225,18 @@ def get_commits_stats(repo_name, ts_min_ try: stats.repository = dbrepo stats.stat_on_revision = last_cs.revision if last_cs else 0 - sa.add(stats) - sa.commit() + DBS.add(stats) + DBS.commit() except: log.error(traceback.format_exc()) - sa.rollback() + DBS.rollback() lock.release() return False - # final release + #final release lock.release() - # execute another task if celery is enabled + #execute another task if celery is enabled if len(repo.revisions) > 1 and CELERY_ON: run_task(get_commits_stats, repo_name, ts_min_y, ts_max_y) return True @@ -240,38 +247,28 @@ def get_commits_stats(repo_name, ts_min_ @task(ignore_result=True) @dbsession def send_password_link(user_email): - try: - log = reset_user_password.get_logger() - except: - log = logging.getLogger(__name__) + from rhodecode.model.notification import EmailNotificationModel - from rhodecode.lib import auth + log = get_logger(send_password_link) + DBS = get_session() try: - sa = get_session() - user = sa.query(User).filter(User.email == user_email).scalar() - + user = User.get_by_email(user_email) if user: + log.debug('password reset user found %s' % user) link = url('reset_password_confirmation', key=user.api_key, qualified=True) - tmpl = """ -Hello %s - -We received a request to create a new password for your account. - -You can generate it by clicking following URL: - -%s - -If you didn't request new password please ignore this email. - """ + reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET + body = EmailNotificationModel().get_email_tmpl(reg_type, + **{'user':user.short_contact, + 'reset_url':link}) + log.debug('sending email') run_task(send_email, user_email, - "RhodeCode password reset link", - tmpl % (user.short_contact, link)) - log.info('send new password mail to %s', user_email) - + _("password reset link"), body) + log.info('send new password mail to %s' % user_email) + else: + log.debug("password reset email %s not found" % user_email) except: - log.error('Failed to update user password') log.error(traceback.format_exc()) return False @@ -280,36 +277,32 @@ If you didn't request new password pleas @task(ignore_result=True) @dbsession def reset_user_password(user_email): - try: - log = reset_user_password.get_logger() - except: - log = logging.getLogger(__name__) + from rhodecode.lib import auth - from rhodecode.lib import auth + log = get_logger(reset_user_password) + DBS = get_session() try: try: - sa = get_session() - user = sa.query(User).filter(User.email == user_email).scalar() + user = User.get_by_email(user_email) new_passwd = auth.PasswordGenerator().gen_password(8, auth.PasswordGenerator.ALPHABETS_BIG_SMALL) if user: user.password = auth.get_crypt_password(new_passwd) user.api_key = auth.generate_api_key(user.username) - sa.add(user) - sa.commit() - log.info('change password for %s', user_email) + DBS.add(user) + DBS.commit() + 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() + DBS.rollback() run_task(send_email, user_email, - "Your new RhodeCode password", + 'Your new password', 'Your new RhodeCode password:%s' % (new_passwd)) - log.info('send new password mail to %s', user_email) + log.info('send new password mail to %s' % user_email) except: log.error('Failed to update user password') @@ -320,7 +313,7 @@ def reset_user_password(user_email): @task(ignore_result=True) @dbsession -def send_email(recipients, subject, body): +def send_email(recipients, subject, body, html_body=''): """ Sends an email with defined parameters from the .ini files. @@ -328,23 +321,20 @@ def send_email(recipients, subject, body address from field 'email_to' is used instead :param subject: subject of the mail :param body: body of the mail + :param html_body: html version of body """ - try: - log = send_email.get_logger() - except: - log = logging.getLogger(__name__) - - sa = get_session() + log = get_logger(send_email) + DBS = get_session() + email_config = config - + subject = "%s %s" % (email_config.get('email_prefix'), subject) if not recipients: # if recipients are not defined we send to email_config + all admins - admins = [ - u.email for u in sa.query(User).filter(User.admin==True).all() - ] + admins = [u.email for u in User.query() + .filter(User.admin == True).all()] recipients = [email_config.get('email_to')] + admins - mail_from = email_config.get('app_email_from') + mail_from = email_config.get('app_email_from', 'RhodeCode') user = email_config.get('smtp_username') passwd = email_config.get('smtp_password') mail_server = email_config.get('smtp_server') @@ -355,9 +345,9 @@ def send_email(recipients, subject, body smtp_auth = email_config.get('smtp_auth') try: - m = SmtpMailer(mail_from, user, passwd, mail_server,smtp_auth, + m = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth, mail_port, ssl, tls, debug=debug) - m.send(recipients, subject, body) + m.send(recipients, subject, body, html_body) except: log.error('Mail sending failed') log.error(traceback.format_exc()) @@ -368,29 +358,45 @@ def send_email(recipients, subject, body @task(ignore_result=True) @dbsession def create_repo_fork(form_data, cur_user): + """ + Creates a fork of repository using interval VCS methods + + :param form_data: + :param cur_user: + """ from rhodecode.model.repo import RepoModel - try: - log = create_repo_fork.get_logger() - except: - log = logging.getLogger(__name__) + log = get_logger(create_repo_fork) + DBS = get_session() + + base_path = Repository.base_path() + + RepoModel(DBS).create(form_data, cur_user, just_db=True, fork=True) + + alias = form_data['repo_type'] + org_repo_name = form_data['org_path'] + fork_name = form_data['repo_name_full'] + update_after_clone = form_data['update_after_clone'] + source_repo_path = os.path.join(base_path, org_repo_name) + destination_fork_path = os.path.join(base_path, fork_name) - 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']) - alias = form_data['repo_type'] + log.info('creating fork of %s as %s', source_repo_path, + destination_fork_path) + backend = get_backend(alias) + backend(safe_str(destination_fork_path), create=True, + src_url=safe_str(source_repo_path), + update_after_clone=update_after_clone) + action_logger(cur_user, 'user_forked_repo:%s' % fork_name, + org_repo_name, '', DBS) - 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)) - + action_logger(cur_user, 'user_created_fork:%s' % fork_name, + fork_name, '', DBS) + # finally commit at latest possible stage + DBS.commit() def __get_codes_stats(repo_name): - repos_path = get_repos_path() - repo = get_repo(safe_str(os.path.join(repos_path, repo_name))) + repo = Repository.get_by_repo_name(repo_name).scm_instance + tip = repo.get_changeset() code_stats = {} diff --git a/rhodecode/lib/celerypylons/commands.py b/rhodecode/lib/celerypylons/commands.py --- a/rhodecode/lib/celerypylons/commands.py +++ b/rhodecode/lib/celerypylons/commands.py @@ -1,7 +1,10 @@ +import rhodecode from rhodecode.lib.utils import BasePasterCommand, Command from celery.app import app_or_default from celery.bin import camqadm, celerybeat, celeryd, celeryev +from rhodecode.lib import str2bool + __all__ = ['CeleryDaemonCommand', 'CeleryBeatCommand', 'CAMQPAdminCommand', 'CeleryEventCommand'] @@ -26,6 +29,16 @@ class CeleryCommand(BasePasterCommand): self.parser.add_option(x) def command(self): + from pylons import config + try: + CELERY_ON = str2bool(config['app_conf'].get('use_celery')) + except KeyError: + CELERY_ON = False + + if CELERY_ON == False: + raise Exception('Please enable celery_on in .ini config ' + 'file before running celeryd') + rhodecode.CELERY_ON = CELERY_ON cmd = self.celery_command(app_or_default()) return cmd.run(**vars(self.options)) diff --git a/rhodecode/lib/compat.py b/rhodecode/lib/compat.py --- a/rhodecode/lib/compat.py +++ b/rhodecode/lib/compat.py @@ -4,11 +4,11 @@ ~~~~~~~~~~~~~~~~~~~~ Python backward compatibility functions and common libs - - + + :created_on: Oct 7, 2011 :author: marcink - :copyright: (C) 2009-2010 Marcin Kuzminski + :copyright: (C) 2010-2010 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -87,10 +87,11 @@ class _Nil(object): _nil = _Nil() + class _odict(object): """Ordered dict data structure, with O(1) complexity for dict operations that modify one element. - + Overwriting values doesn't change their original sequential order. """ @@ -146,7 +147,7 @@ class _odict(object): dict_impl = self._dict_impl() try: dict_impl.__getitem__(self, key)[1] = val - except KeyError, e: + except KeyError: new = [dict_impl.__getattribute__(self, 'lt'), val, _nil] dict_impl.__setitem__(self, key, new) if dict_impl.__getattribute__(self, 'lt') == _nil: @@ -158,7 +159,7 @@ class _odict(object): def __delitem__(self, key): dict_impl = self._dict_impl() - pred, _ , succ = self._dict_impl().__getitem__(self, key) + pred, _, succ = self._dict_impl().__getitem__(self, key) if pred == _nil: dict_impl.__setattr__(self, 'lh', succ) else: @@ -351,6 +352,7 @@ class _odict(object): dict_impl.__getattribute__(self, 'lt'), dict_impl.__repr__(self)) + class OrderedDict(_odict, dict): def _dict_impl(self): diff --git a/rhodecode/lib/db_manage.py b/rhodecode/lib/db_manage.py --- a/rhodecode/lib/db_manage.py +++ b/rhodecode/lib/db_manage.py @@ -8,7 +8,7 @@ :created_on: Apr 10, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -33,13 +33,15 @@ from os.path import dirname as dn, join from rhodecode import __dbversion__ from rhodecode.model import meta -from rhodecode.lib.auth import get_crypt_password, generate_api_key +from rhodecode.model.user import UserModel from rhodecode.lib.utils import ask_ok from rhodecode.model import init_model from rhodecode.model.db import User, Permission, RhodeCodeUi, \ - RhodeCodeSettings, UserToPerm, DbMigrateVersion + RhodeCodeSetting, UserToPerm, DbMigrateVersion, RepoGroup,\ + UserRepoGroupToPerm from sqlalchemy.engine import create_engine +from rhodecode.model.repos_group import ReposGroupModel log = logging.getLogger(__name__) @@ -57,10 +59,11 @@ class DbManage(object): def init_db(self): engine = create_engine(self.dburi, echo=self.log_sql) init_model(engine) - self.sa = meta.Session() + self.sa = meta.Session def create_tables(self, override=False): - """Create a auth database + """ + Create a auth database """ log.info("Any existing database is going to be destroyed") @@ -75,23 +78,19 @@ class DbManage(object): checkfirst = not override meta.Base.metadata.create_all(checkfirst=checkfirst) - log.info('Created tables for %s', self.dbname) + 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__) + ver = DbMigrateVersion() + ver.version = __dbversion__ + ver.repository_id = 'rhodecode_db_migrations' + ver.repository_path = 'versions' + self.sa.add(ver) + log.info('db version set to: %s' % __dbversion__) def upgrade(self): - """Upgrades given database schema to given revision following + """ + Upgrades given database schema to given revision following all needed steps, to perform the upgrade """ @@ -146,7 +145,7 @@ class DbManage(object): self.klass = klass def step_0(self): - #step 0 is the schema upgrade, and than follow proper upgrades + # 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__) @@ -170,16 +169,26 @@ class DbManage(object): self.klass.fix_settings() print ('Adding ldap defaults') self.klass.create_ldap_options(skip_existing=True) - + + def step_4(self): + print ('create permissions and fix groups') + self.klass.create_permissions() + self.klass.fixup_groups() + + def step_5(self): + pass + upgrade_steps = [0] + range(curr_version + 1, __dbversion__ + 1) - #CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE + # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE for step in upgrade_steps: print ('performing upgrade step %s' % step) getattr(UpgradeSteps(self), 'step_%s' % step)() + self.sa.commit() def fix_repo_paths(self): - """Fixes a old rhodecode version path into new one without a '*' + """ + Fixes a old rhodecode version path into new one without a '*' """ paths = self.sa.query(RhodeCodeUi)\ @@ -196,7 +205,8 @@ class DbManage(object): raise def fix_default_user(self): - """Fixes a old default user with some 'nicer' default values, + """ + Fixes a old default user with some 'nicer' default values, used mostly for anonymous access """ def_user = self.sa.query(User)\ @@ -215,10 +225,11 @@ class DbManage(object): raise def fix_settings(self): - """Fixes rhodecode settings adds ga_code key for google analytics + """ + Fixes rhodecode settings adds ga_code key for google analytics """ - hgsettings3 = RhodeCodeSettings('ga_code', '') + hgsettings3 = RhodeCodeSetting('ga_code', '') try: self.sa.add(hgsettings3) @@ -258,18 +269,27 @@ class DbManage(object): self.create_user(username, password, email, True) else: log.info('creating admin and regular test users') - 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) + from rhodecode.tests import TEST_USER_ADMIN_LOGIN,\ + TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL,\ + TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,\ + TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \ + TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL + + self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS, + TEST_USER_ADMIN_EMAIL, True) + + self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, + TEST_USER_REGULAR_EMAIL, False) + + self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS, + TEST_USER_REGULAR2_EMAIL, False) def create_ui_settings(self): - """Creates ui settings, fills out hooks + """ + Creates ui settings, fills out hooks and disables dotencode + """ - """ #HOOKS hooks1_key = RhodeCodeUi.HOOK_UPDATE hooks1_ = self.sa.query(RhodeCodeUi)\ @@ -300,7 +320,7 @@ class DbManage(object): hooks4.ui_key = RhodeCodeUi.HOOK_PULL hooks4.ui_value = 'python:rhodecode.lib.hooks.log_pull_action' - #For mercurial 1.7 set backward comapatibility with format + # For mercurial 1.7 set backward comapatibility with format dotencode_disable = RhodeCodeUi() dotencode_disable.ui_section = 'format' dotencode_disable.ui_key = 'dotencode' @@ -312,39 +332,43 @@ class DbManage(object): largefiles.ui_key = 'largefiles' largefiles.ui_value = '' - try: - self.sa.add(hooks1) - self.sa.add(hooks2) - self.sa.add(hooks3) - self.sa.add(hooks4) - self.sa.add(dotencode_disable) - self.sa.add(largefiles) - self.sa.commit() - except: - self.sa.rollback() - raise + self.sa.add(hooks1) + self.sa.add(hooks2) + self.sa.add(hooks3) + self.sa.add(hooks4) + self.sa.add(largefiles) - def create_ldap_options(self,skip_existing=False): + def create_ldap_options(self, skip_existing=False): """Creates ldap settings""" - try: - for k, v in [('ldap_active', 'false'), ('ldap_host', ''), - ('ldap_port', '389'), ('ldap_tls_kind', 'PLAIN'), - ('ldap_tls_reqcert', ''), ('ldap_dn_user', ''), - ('ldap_dn_pass', ''), ('ldap_base_dn', ''), - ('ldap_filter', ''), ('ldap_search_scope', ''), - ('ldap_attr_login', ''), ('ldap_attr_firstname', ''), - ('ldap_attr_lastname', ''), ('ldap_attr_email', '')]: + for k, v in [('ldap_active', 'false'), ('ldap_host', ''), + ('ldap_port', '389'), ('ldap_tls_kind', 'PLAIN'), + ('ldap_tls_reqcert', ''), ('ldap_dn_user', ''), + ('ldap_dn_pass', ''), ('ldap_base_dn', ''), + ('ldap_filter', ''), ('ldap_search_scope', ''), + ('ldap_attr_login', ''), ('ldap_attr_firstname', ''), + ('ldap_attr_lastname', ''), ('ldap_attr_email', '')]: + + if skip_existing and RhodeCodeSetting.get_by_name(k) != None: + log.debug('Skipping option %s' % k) + continue + setting = RhodeCodeSetting(k, v) + self.sa.add(setting) - if skip_existing and RhodeCodeSettings.get_by_name(k) != None: - log.debug('Skipping option %s' % k) - continue - setting = RhodeCodeSettings(k, v) - self.sa.add(setting) - self.sa.commit() - except: - self.sa.rollback() - raise + def fixup_groups(self): + def_usr = User.get_by_username('default') + for g in RepoGroup.query().all(): + g.group_name = g.get_new_name(g.name) + self.sa.add(g) + # get default perm + default = UserRepoGroupToPerm.query()\ + .filter(UserRepoGroupToPerm.group == g)\ + .filter(UserRepoGroupToPerm.user == def_usr)\ + .scalar() + + if default is None: + log.debug('missing default permission for group %s adding' % g) + ReposGroupModel()._create_default_perms(g) def config_prompt(self, test_repo_path='', retries=3): if retries == 3: @@ -359,16 +383,15 @@ class DbManage(object): path = test_repo_path path_ok = True - #check proper dir + # check proper dir if not os.path.isdir(path): path_ok = False - log.error('Given path %s is not a valid directory', path) + log.error('Given path %s is not a valid directory' % path) - #check write access + # check write access if not os.access(path, os.W_OK) and path_ok: path_ok = False - log.error('No write permission to given path %s', path) - + log.error('No write permission to given path %s' % path) if retries == 0: sys.exit('max retries reached') @@ -408,85 +431,68 @@ class DbManage(object): paths.ui_key = '/' paths.ui_value = path - hgsettings1 = RhodeCodeSettings('realm', 'RhodeCode authentication') - hgsettings2 = RhodeCodeSettings('title', 'RhodeCode') - hgsettings3 = RhodeCodeSettings('ga_code', '') + hgsettings1 = RhodeCodeSetting('realm', 'RhodeCode authentication') + hgsettings2 = RhodeCodeSetting('title', 'RhodeCode') + hgsettings3 = RhodeCodeSetting('ga_code', '') - try: - self.sa.add(web1) - self.sa.add(web2) - self.sa.add(web3) - self.sa.add(web4) - self.sa.add(paths) - self.sa.add(hgsettings1) - self.sa.add(hgsettings2) - self.sa.add(hgsettings3) - - self.sa.commit() - except: - self.sa.rollback() - raise + self.sa.add(web1) + self.sa.add(web2) + self.sa.add(web3) + self.sa.add(web4) + self.sa.add(paths) + self.sa.add(hgsettings1) + self.sa.add(hgsettings2) + self.sa.add(hgsettings3) 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) - - form_data = dict(username=username, - password=password, - active=True, - admin=admin, - name='RhodeCode', - lastname='Admin', - email=email) - User.create(form_data) - + log.info('creating user %s' % username) + UserModel().create_or_update(username, password, email, + name='RhodeCode', lastname='Admin', + active=True, admin=admin) def create_default_user(self): log.info('creating default user') - #create default user for handling default permissions. + # create default user for handling default permissions. + UserModel().create_or_update(username='default', + password=str(uuid.uuid1())[:8], + email='anonymous@rhodecode.org', + name='Anonymous', lastname='User') - form_data = dict(username='default', - password=str(uuid.uuid1())[:8], - active=False, - admin=False, - name='Anonymous', - lastname='User', - email='anonymous@rhodecode.org') - User.create(form_data) - def create_permissions(self): - #module.(access|create|change|delete)_[name] - #module.(read|write|owner) - perms = [('repository.none', 'Repository no access'), - ('repository.read', 'Repository read access'), - ('repository.write', 'Repository write access'), - ('repository.admin', 'Repository admin access'), - ('hg.admin', 'Hg Administrator'), - ('hg.create.repository', 'Repository create'), - ('hg.create.none', 'Repository creation disabled'), - ('hg.register.none', 'Register disabled'), - ('hg.register.manual_activate', 'Register new user with ' - 'RhodeCode without manual' - 'activation'), + # module.(access|create|change|delete)_[name] + # module.(none|read|write|admin) + perms = [ + ('repository.none', 'Repository no access'), + ('repository.read', 'Repository read access'), + ('repository.write', 'Repository write access'), + ('repository.admin', 'Repository admin access'), - ('hg.register.auto_activate', 'Register new user with ' - 'RhodeCode without auto ' - 'activation'), - ] + ('group.none', 'Repositories Group no access'), + ('group.read', 'Repositories Group read access'), + ('group.write', 'Repositories Group write access'), + ('group.admin', 'Repositories Group admin access'), + + ('hg.admin', 'Hg Administrator'), + ('hg.create.repository', 'Repository create'), + ('hg.create.none', 'Repository creation disabled'), + ('hg.register.none', 'Register disabled'), + ('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] - new_perm.permission_longname = p[1] - try: + if not Permission.get_by_key(p[0]): + new_perm = Permission() + new_perm.permission_name = p[0] + new_perm.permission_longname = p[1] self.sa.add(new_perm) - self.sa.commit() - except: - self.sa.rollback() - raise def populate_default_permissions(self): log.info('creating default user permissions') @@ -512,11 +518,6 @@ class DbManage(object): .filter(Permission.permission_name == 'repository.read')\ .scalar() - try: - self.sa.add(reg_perm) - self.sa.add(create_repo_perm) - self.sa.add(default_repo_perm) - self.sa.commit() - except: - self.sa.rollback() - raise + self.sa.add(reg_perm) + self.sa.add(create_repo_perm) + self.sa.add(default_repo_perm) diff --git a/rhodecode/lib/dbmigrate/__init__.py b/rhodecode/lib/dbmigrate/__init__.py --- a/rhodecode/lib/dbmigrate/__init__.py +++ b/rhodecode/lib/dbmigrate/__init__.py @@ -7,7 +7,7 @@ :created_on: Dec 11, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify diff --git a/rhodecode/lib/dbmigrate/migrate/__init__.py b/rhodecode/lib/dbmigrate/migrate/__init__.py --- a/rhodecode/lib/dbmigrate/migrate/__init__.py +++ b/rhodecode/lib/dbmigrate/migrate/__init__.py @@ -8,4 +8,4 @@ from rhodecode.lib.dbmigrate.migrate.versioning import * from rhodecode.lib.dbmigrate.migrate.changeset import * -__version__ = '0.7.2.dev' \ No newline at end of file +__version__ = '0.7.3.dev' diff --git a/rhodecode/lib/dbmigrate/migrate/changeset/__init__.py b/rhodecode/lib/dbmigrate/migrate/changeset/__init__.py --- a/rhodecode/lib/dbmigrate/migrate/changeset/__init__.py +++ b/rhodecode/lib/dbmigrate/migrate/changeset/__init__.py @@ -12,7 +12,7 @@ from sqlalchemy import __version__ as _s warnings.simplefilter('always', DeprecationWarning) -_sa_version = tuple(int(re.match("\d+", x).group(0)) +_sa_version = tuple(int(re.match("\d+", x).group(0)) for x in _sa_version.split(".")) SQLA_06 = _sa_version >= (0, 6) SQLA_07 = _sa_version >= (0, 7) diff --git a/rhodecode/lib/dbmigrate/migrate/changeset/ansisql.py b/rhodecode/lib/dbmigrate/migrate/changeset/ansisql.py --- a/rhodecode/lib/dbmigrate/migrate/changeset/ansisql.py +++ b/rhodecode/lib/dbmigrate/migrate/changeset/ansisql.py @@ -17,23 +17,19 @@ from sqlalchemy.schema import (ForeignKe Index) from rhodecode.lib.dbmigrate.migrate import exceptions -from rhodecode.lib.dbmigrate.migrate.changeset import constraint, SQLA_06 +from rhodecode.lib.dbmigrate.migrate.changeset import constraint -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 +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() + # 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.""" @@ -123,9 +119,8 @@ class ANSIColumnGenerator(AlterTableVisi name=column.primary_key_name) cons.create() - if SQLA_06: - def add_foreignkey(self, fk): - self.connection.execute(AddConstraint(fk)) + def add_foreignkey(self, fk): + self.connection.execute(AddConstraint(fk)) class ANSIColumnDropper(AlterTableVisitor, SchemaDropper): """Extends ANSI SQL dropper for column dropping (``ALTER TABLE @@ -232,10 +227,7 @@ class ANSISchemaChanger(AlterTableVisito 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() + type_text = str(type_.compile(dialect=self.dialect)) self.append("TYPE %s" % type_text) def _visit_column_name(self, table, column, delta): @@ -279,75 +271,17 @@ class ANSIConstraintCommon(AlterTableVis 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. - """ +class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator): + def _visit_constraint(self, constraint): + constraint.name = self.get_constraint_name(constraint) + self.append(self.process(AddConstraint(constraint))) + self.execute() - 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 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() class ANSIDialect(DefaultDialect): diff --git a/rhodecode/lib/dbmigrate/migrate/changeset/constraint.py b/rhodecode/lib/dbmigrate/migrate/changeset/constraint.py --- a/rhodecode/lib/dbmigrate/migrate/changeset/constraint.py +++ b/rhodecode/lib/dbmigrate/migrate/changeset/constraint.py @@ -4,7 +4,7 @@ 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.""" @@ -85,7 +85,6 @@ class PrimaryKeyConstraint(ConstraintCha 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 @@ -111,8 +110,9 @@ class ForeignKeyConstraint(ConstraintCha table = kwargs.pop('table', table) refcolnames, reftable = self._normalize_columns(refcolumns, table_name=True) - super(ForeignKeyConstraint, self).__init__(colnames, refcolnames, *args, - **kwargs) + super(ForeignKeyConstraint, self).__init__( + colnames, refcolnames, *args,**kwargs + ) if table is not None: self._set_parent(table) @@ -165,8 +165,6 @@ class CheckConstraint(ConstraintChangese 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 @@ -199,4 +197,4 @@ class UniqueConstraint(ConstraintChanges def autoname(self): """Mimic the database's automatic constraint names""" - return "%s_%s_key" % (self.table.name, self.colnames[0]) + return "%s_%s_key" % (self.table.name, '_'.join(self.colnames)) diff --git a/rhodecode/lib/dbmigrate/migrate/changeset/databases/firebird.py b/rhodecode/lib/dbmigrate/migrate/changeset/databases/firebird.py --- a/rhodecode/lib/dbmigrate/migrate/changeset/databases/firebird.py +++ b/rhodecode/lib/dbmigrate/migrate/changeset/databases/firebird.py @@ -4,13 +4,10 @@ from sqlalchemy.databases import firebird as sa_base from sqlalchemy.schema import PrimaryKeyConstraint from rhodecode.lib.dbmigrate.migrate import exceptions -from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06 +from rhodecode.lib.dbmigrate.migrate.changeset import ansisql -if SQLA_06: - FBSchemaGenerator = sa_base.FBDDLCompiler -else: - FBSchemaGenerator = sa_base.FBSchemaGenerator +FBSchemaGenerator = sa_base.FBDDLCompiler class FBColumnGenerator(FBSchemaGenerator, ansisql.ANSIColumnGenerator): """Firebird column generator implementation.""" @@ -41,10 +38,7 @@ class FBColumnDropper(ansisql.ANSIColumn # is deleted! continue - if SQLA_06: - should_drop = column.name in cons.columns - else: - should_drop = cons.contains_column(column) and cons.name + should_drop = column.name in cons.columns if should_drop: self.start_alter_table(column) self.append("DROP CONSTRAINT ") diff --git a/rhodecode/lib/dbmigrate/migrate/changeset/databases/mysql.py b/rhodecode/lib/dbmigrate/migrate/changeset/databases/mysql.py --- a/rhodecode/lib/dbmigrate/migrate/changeset/databases/mysql.py +++ b/rhodecode/lib/dbmigrate/migrate/changeset/databases/mysql.py @@ -6,13 +6,10 @@ from sqlalchemy.databases import mysql a from sqlalchemy import types as sqltypes from rhodecode.lib.dbmigrate.migrate import exceptions -from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06 +from rhodecode.lib.dbmigrate.migrate.changeset import ansisql -if not SQLA_06: - MySQLSchemaGenerator = sa_base.MySQLSchemaGenerator -else: - MySQLSchemaGenerator = sa_base.MySQLDDLCompiler +MySQLSchemaGenerator = sa_base.MySQLDDLCompiler class MySQLColumnGenerator(MySQLSchemaGenerator, ansisql.ANSIColumnGenerator): pass @@ -53,37 +50,11 @@ class MySQLSchemaChanger(MySQLSchemaGene 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 MySQLConstraintDropper(MySQLSchemaGenerator, ansisql.ANSIConstraintDropper): + def visit_migrate_check_constraint(self, *p, **k): + raise exceptions.NotSupportedError("MySQL does not support CHECK" + " constraints, use triggers instead.") class MySQLDialect(ansisql.ANSIDialect): diff --git a/rhodecode/lib/dbmigrate/migrate/changeset/databases/postgres.py b/rhodecode/lib/dbmigrate/migrate/changeset/databases/postgres.py --- a/rhodecode/lib/dbmigrate/migrate/changeset/databases/postgres.py +++ b/rhodecode/lib/dbmigrate/migrate/changeset/databases/postgres.py @@ -3,14 +3,11 @@ .. _`PostgreSQL`: http://www.postgresql.org/ """ -from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06 +from rhodecode.lib.dbmigrate.migrate.changeset import ansisql + -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 +from sqlalchemy.databases import postgresql as sa_base +PGSchemaGenerator = sa_base.PGDDLCompiler class PGColumnGenerator(PGSchemaGenerator, ansisql.ANSIColumnGenerator): diff --git a/rhodecode/lib/dbmigrate/migrate/changeset/databases/sqlite.py b/rhodecode/lib/dbmigrate/migrate/changeset/databases/sqlite.py --- a/rhodecode/lib/dbmigrate/migrate/changeset/databases/sqlite.py +++ b/rhodecode/lib/dbmigrate/migrate/changeset/databases/sqlite.py @@ -11,11 +11,8 @@ from sqlalchemy.databases import sqlite from rhodecode.lib.dbmigrate.migrate import exceptions from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06 +SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler -if not SQLA_06: - SQLiteSchemaGenerator = sa_base.SQLiteSchemaGenerator -else: - SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler class SQLiteCommon(object): @@ -39,7 +36,7 @@ class SQLiteHelper(SQLiteCommon): insertion_string = self._modify_table(table, column, delta) - table.create() + table.create(bind=self.connection) self.append(insertion_string % {'table_name': table_name}) self.execute() self.append('DROP TABLE migration_tmp') diff --git a/rhodecode/lib/dbmigrate/migrate/changeset/schema.py b/rhodecode/lib/dbmigrate/migrate/changeset/schema.py --- a/rhodecode/lib/dbmigrate/migrate/changeset/schema.py +++ b/rhodecode/lib/dbmigrate/migrate/changeset/schema.py @@ -349,10 +349,7 @@ class ColumnDelta(DictMixin, sqlalchemy. 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() + toinit = list() if column.server_default is not None: if isinstance(column.server_default, sqlalchemy.FetchedValue): @@ -368,9 +365,6 @@ class ColumnDelta(DictMixin, sqlalchemy. if toinit: column._init_items(*toinit) - if not SQLA_06: - column.args = [] - def _get_table(self): return getattr(self, '_table', None) @@ -469,14 +463,18 @@ class ChangesetTable(object): self._set_parent(self.metadata) def _meta_key(self): + """Get the meta key for this table.""" 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] + if SQLA_07: + self.metadata._remove_table(self.name, self.schema) + else: + key = self._meta_key() + meta = self.metadata + if key in meta.tables: + del meta.tables[key] class ChangesetColumn(object): diff --git a/rhodecode/lib/dbmigrate/migrate/exceptions.py b/rhodecode/lib/dbmigrate/migrate/exceptions.py --- a/rhodecode/lib/dbmigrate/migrate/exceptions.py +++ b/rhodecode/lib/dbmigrate/migrate/exceptions.py @@ -83,6 +83,5 @@ class NotSupportedError(Error): class InvalidConstraintError(Error): """Invalid constraint error""" - class MigrateDeprecationWarning(DeprecationWarning): """Warning for deprecated features in Migrate""" diff --git a/rhodecode/lib/dbmigrate/migrate/versioning/api.py b/rhodecode/lib/dbmigrate/migrate/versioning/api.py --- a/rhodecode/lib/dbmigrate/migrate/versioning/api.py +++ b/rhodecode/lib/dbmigrate/migrate/versioning/api.py @@ -119,7 +119,7 @@ def script_sql(database, description, re For instance, manage.py script_sql postgresql description creates: repository/versions/001_description_postgresql_upgrade.sql and - repository/versions/001_description_postgresql_postgres.sql + repository/versions/001_description_postgresql_downgrade.sql """ repo = Repository(repository) repo.create_script_sql(database, description, **opts) @@ -212,14 +212,15 @@ def test(url, repository, **opts): """ engine = opts.pop('engine') repos = Repository(repository) - script = repos.version(None).script() # Upgrade log.info("Upgrading...") + script = repos.version(None).script(engine.name, 'upgrade') script.run(engine, 1) log.info("done") log.info("Downgrading...") + script = repos.version(None).script(engine.name, 'downgrade') script.run(engine, -1) log.info("done") log.info("Success") diff --git a/rhodecode/lib/dbmigrate/migrate/versioning/genmodel.py b/rhodecode/lib/dbmigrate/migrate/versioning/genmodel.py --- a/rhodecode/lib/dbmigrate/migrate/versioning/genmodel.py +++ b/rhodecode/lib/dbmigrate/migrate/versioning/genmodel.py @@ -282,4 +282,3 @@ class ModelGenerator(object): except: trans.rollback() raise - diff --git a/rhodecode/lib/dbmigrate/migrate/versioning/repository.py b/rhodecode/lib/dbmigrate/migrate/versioning/repository.py --- a/rhodecode/lib/dbmigrate/migrate/versioning/repository.py +++ b/rhodecode/lib/dbmigrate/migrate/versioning/repository.py @@ -115,7 +115,7 @@ class Repository(pathed.Pathed): options.setdefault('version_table', 'migrate_version') options.setdefault('repository_id', name) options.setdefault('required_dbs', []) - options.setdefault('use_timestamp_numbering', '0') + options.setdefault('use_timestamp_numbering', False) tmpl = open(os.path.join(tmpl_dir, cls._config)).read() ret = TempitaTemplate(tmpl).substitute(options) @@ -153,7 +153,7 @@ class Repository(pathed.Pathed): def create_script(self, description, **k): """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`""" - + k['use_timestamp_numbering'] = self.use_timestamp_numbering self.versions.create_new_python_version(description, **k) @@ -180,9 +180,9 @@ class Repository(pathed.Pathed): @property def use_timestamp_numbering(self): """Returns use_timestamp_numbering specified in config""" - ts_numbering = self.config.get('db_settings', 'use_timestamp_numbering', raw=True) - - return ts_numbering + if self.config.has_option('db_settings', 'use_timestamp_numbering'): + return self.config.getboolean('db_settings', 'use_timestamp_numbering') + return False def version(self, *p, **k): """API to :attr:`migrate.versioning.version.Collection.version`""" diff --git a/rhodecode/lib/dbmigrate/migrate/versioning/schemadiff.py b/rhodecode/lib/dbmigrate/migrate/versioning/schemadiff.py --- a/rhodecode/lib/dbmigrate/migrate/versioning/schemadiff.py +++ b/rhodecode/lib/dbmigrate/migrate/versioning/schemadiff.py @@ -17,8 +17,16 @@ def getDiffOfModelAgainstDatabase(metada :return: object which will evaluate to :keyword:`True` if there \ are differences else :keyword:`False`. """ - return SchemaDiff(metadata, - sqlalchemy.MetaData(engine, reflect=True), + db_metadata = sqlalchemy.MetaData(engine, reflect=True) + + # sqlite will include a dynamically generated 'sqlite_sequence' table if + # there are autoincrement sequences in the database; this should not be + # compared. + if engine.dialect.name == 'sqlite': + if 'sqlite_sequence' in db_metadata.tables: + db_metadata.remove(db_metadata.tables['sqlite_sequence']) + + return SchemaDiff(metadata, db_metadata, labelA='model', labelB='database', excludeTables=excludeTables) @@ -31,7 +39,7 @@ def getDiffOfModelAgainstModel(metadataA :return: object which will evaluate to :keyword:`True` if there \ are differences else :keyword:`False`. """ - return SchemaDiff(metadataA, metadataB, excludeTables) + return SchemaDiff(metadataA, metadataB, excludeTables=excludeTables) class ColDiff(object): diff --git a/rhodecode/lib/dbmigrate/migrate/versioning/templates/manage/default.py_tmpl b/rhodecode/lib/dbmigrate/migrate/versioning/templates/manage/default.py_tmpl --- a/rhodecode/lib/dbmigrate/migrate/versioning/templates/manage/default.py_tmpl +++ b/rhodecode/lib/dbmigrate/migrate/versioning/templates/manage/default.py_tmpl @@ -7,4 +7,6 @@ del _vars['__template_name__'] _vars.pop('repository_name', None) defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()]) }} -main({{ defaults }}) + +if __name__ == '__main__': + main({{ defaults }}) diff --git a/rhodecode/lib/dbmigrate/migrate/versioning/templates/manage/pylons.py_tmpl b/rhodecode/lib/dbmigrate/migrate/versioning/templates/manage/pylons.py_tmpl --- a/rhodecode/lib/dbmigrate/migrate/versioning/templates/manage/pylons.py_tmpl +++ b/rhodecode/lib/dbmigrate/migrate/versioning/templates/manage/pylons.py_tmpl @@ -26,4 +26,5 @@ conf_dict = ConfigLoader(conf_path).pars # 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 }}) +if __name__ == '__main__': + main(url=engine_from_config(conf_dict), repository=migrations.__path__[0],{{ defaults }}) diff --git a/rhodecode/lib/dbmigrate/migrate/versioning/util/__init__.py b/rhodecode/lib/dbmigrate/migrate/versioning/util/__init__.py --- a/rhodecode/lib/dbmigrate/migrate/versioning/util/__init__.py +++ b/rhodecode/lib/dbmigrate/migrate/versioning/util/__init__.py @@ -158,7 +158,7 @@ def with_engine(f, *a, **kw): kw['engine'] = engine return f(*a, **kw) finally: - if isinstance(engine, Engine): + if isinstance(engine, Engine) and engine is not url: log.debug('Disposing SQLAlchemy engine %s', engine) engine.dispose() diff --git a/rhodecode/lib/dbmigrate/migrate/versioning/version.py b/rhodecode/lib/dbmigrate/migrate/versioning/version.py --- a/rhodecode/lib/dbmigrate/migrate/versioning/version.py +++ b/rhodecode/lib/dbmigrate/migrate/versioning/version.py @@ -60,7 +60,7 @@ class Collection(pathed.Pathed): 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: @@ -90,9 +90,7 @@ class Collection(pathed.Pathed): return max([VerNum(0)] + self.versions.keys()) def _next_ver_num(self, use_timestamp_numbering): - print use_timestamp_numbering if use_timestamp_numbering == True: - print "Creating new timestamp version!" return VerNum(int(datetime.utcnow().strftime('%Y%m%d%H%M%S'))) else: return self.latest + 1 @@ -113,7 +111,7 @@ class Collection(pathed.Pathed): script.PythonScript.create(filepath, **k) self.versions[ver] = Version(ver, self.path, [filename]) - + def create_new_sql_version(self, database, description, **k): """Create SQL files for new version""" ver = self._next_ver_num(k.pop('use_timestamp_numbering', False)) @@ -133,7 +131,7 @@ class Collection(pathed.Pathed): 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""" @@ -152,7 +150,7 @@ class Collection(pathed.Pathed): class Version(object): """A single version in a collection - :param vernum: Version Number + :param vernum: Version Number :param path: Path to script files :param filelist: List of scripts :type vernum: int, VerNum @@ -169,7 +167,7 @@ class Version(object): 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'): @@ -198,7 +196,7 @@ class Version(object): def _add_script_sql(self, path): basename = os.path.basename(path) match = self.SQL_FILENAME.match(basename) - + if match: basename = basename.replace('.sql', '') parts = basename.split('_') diff --git a/rhodecode/lib/dbmigrate/schema/__init__.py b/rhodecode/lib/dbmigrate/schema/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/schema/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.lib.dbmigrate.schema + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + Schemas for migrations + + + :created_on: Nov 1, 2011 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: , see LICENSE_FILE for more details. +""" diff --git a/rhodecode/lib/dbmigrate/schema/db_1_1_0.py b/rhodecode/lib/dbmigrate/schema/db_1_1_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/schema/db_1_1_0.py @@ -0,0 +1,94 @@ +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 + +class BaseModel(object): + """Base Model for all classess + + """ + + @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]) + + @classmethod + def query(cls): + return Session.query(cls) + + @classmethod + def get(cls, id_): + if id_: + return cls.query().get(id_) + + @classmethod + def getAll(cls): + return cls.query().all() + + @classmethod + def delete(cls, id_): + obj = cls.query().get(id_) + Session.delete(obj) + Session.commit() + + +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) diff --git a/rhodecode/lib/dbmigrate/schema/db_1_2_0.py b/rhodecode/lib/dbmigrate/schema/db_1_2_0.py new file mode 100755 --- /dev/null +++ b/rhodecode/lib/dbmigrate/schema/db_1_2_0.py @@ -0,0 +1,1098 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.model.db + ~~~~~~~~~~~~~~~~~~ + + Database Models for RhodeCode + + :created_on: Apr 08, 2010 + :author: marcink + :copyright: (C) 2010-2012 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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, see . + +import os +import logging +import datetime +import traceback +from datetime import date + +from sqlalchemy import * +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import relationship, joinedload, class_mapper, validates +from beaker.cache import cache_region, region_invalidate + +from rhodecode.lib.vcs import get_backend +from rhodecode.lib.vcs.utils.helpers import get_scm +from rhodecode.lib.vcs.exceptions import VCSError +from rhodecode.lib.vcs.utils.lazy import LazyProperty + +from rhodecode.lib import str2bool, safe_str, get_changeset_safe, \ + generate_api_key, safe_unicode +from rhodecode.lib.exceptions import UsersGroupsAssignedException +from rhodecode.lib.compat import json + +from rhodecode.model.meta import Base, Session +from rhodecode.lib.caching_query import FromCache + + +log = logging.getLogger(__name__) + +#============================================================================== +# BASE CLASSES +#============================================================================== + +class ModelSerializer(json.JSONEncoder): + """ + Simple Serializer for JSON, + + usage:: + + to make object customized for serialization implement a __json__ + method that will return a dict for serialization into json + + example:: + + class Task(object): + + def __init__(self, name, value): + self.name = name + self.value = value + + def __json__(self): + return dict(name=self.name, + value=self.value) + + """ + + def default(self, obj): + + if hasattr(obj, '__json__'): + return obj.__json__() + else: + return json.JSONEncoder.default(self, obj) + +class BaseModel(object): + """Base Model for all classess + + """ + + @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]) + + @classmethod + def query(cls): + return Session.query(cls) + + @classmethod + def get(cls, id_): + if id_: + return cls.query().get(id_) + + @classmethod + def getAll(cls): + return cls.query().all() + + @classmethod + def delete(cls, id_): + obj = cls.query().get(id_) + Session.delete(obj) + Session.commit() + + +class RhodeCodeSetting(Base, BaseModel): + __tablename__ = 'rhodecode_settings' + __table_args__ = (UniqueConstraint('app_settings_name'), {'extend_existing':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=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + _app_settings_value = Column("app_settings_value", String(length=255, 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 + + + @validates('_app_settings_value') + def validate_settings_value(self, key, val): + assert type(val) == unicode + return val + + @hybrid_property + def app_settings_value(self): + v = self._app_settings_value + if v == 'ldap_active': + v = str2bool(v) + return v + + @app_settings_value.setter + def app_settings_value(self, val): + """ + Setter that will always make sure we use unicode in app_settings_value + + :param val: + """ + self._app_settings_value = safe_unicode(val) + + def __repr__(self): + return "<%s('%s:%s')>" % (self.__class__.__name__, + self.app_settings_name, self.app_settings_value) + + + @classmethod + def get_by_name(cls, ldap_key): + return cls.query()\ + .filter(cls.app_settings_name == ldap_key).scalar() + + @classmethod + def get_app_settings(cls, cache=False): + + ret = cls.query() + + if cache: + ret = ret.options(FromCache("sql_cache_short", "get_hg_settings")) + + 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 + + @classmethod + def get_ldap_settings(cls, cache=False): + ret = cls.query()\ + .filter(cls.app_settings_name.startswith('ldap_')).all() + fd = {} + for row in ret: + fd.update({row.app_settings_name:row.app_settings_value}) + + return fd + + +class RhodeCodeUi(Base, BaseModel): + __tablename__ = 'rhodecode_ui' + __table_args__ = (UniqueConstraint('ui_key'), {'extend_existing':True}) + + HOOK_UPDATE = 'changegroup.update' + HOOK_REPO_SIZE = 'changegroup.repo_size' + HOOK_PUSH = 'pretxnchangegroup.push_logger' + HOOK_PULL = 'preoutgoing.pull_logger' + + ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True) + + + @classmethod + def get_by_key(cls, key): + return cls.query().filter(cls.ui_key == key) + + + @classmethod + def get_builtin_hooks(cls): + q = cls.query() + q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, + cls.HOOK_REPO_SIZE, + cls.HOOK_PUSH, cls.HOOK_PULL])) + return q.all() + + @classmethod + def get_custom_hooks(cls): + q = cls.query() + q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, + cls.HOOK_REPO_SIZE, + cls.HOOK_PUSH, cls.HOOK_PULL])) + q = q.filter(cls.ui_section == 'hooks') + return q.all() + + @classmethod + def create_or_update_hook(cls, key, val): + new_ui = cls.get_by_key(key).scalar() or cls() + new_ui.ui_section = 'hooks' + new_ui.ui_active = True + new_ui.ui_key = key + new_ui.ui_value = val + + Session.add(new_ui) + Session.commit() + + +class User(Base, BaseModel): + __tablename__ = 'users' + __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'extend_existing':True}) + user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + password = Column("password", String(length=255, 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=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + email = Column("email", String(length=255, 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) + ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + + user_log = relationship('UserLog', cascade='all') + user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all') + + repositories = relationship('Repository') + user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all') + repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all') + + group_member = relationship('UsersGroupMember', cascade='all') + + @property + def full_contact(self): + return '%s %s <%s>' % (self.name, self.lastname, self.email) + + @property + def short_contact(self): + return '%s %s' % (self.name, self.lastname) + + @property + def is_admin(self): + return self.admin + + def __repr__(self): + try: + return "<%s('id:%s:%s')>" % (self.__class__.__name__, + self.user_id, self.username) + except: + return self.__class__.__name__ + + @classmethod + def get_by_username(cls, username, case_insensitive=False): + if case_insensitive: + return Session.query(cls).filter(cls.username.ilike(username)).scalar() + else: + return Session.query(cls).filter(cls.username == username).scalar() + + @classmethod + def get_by_api_key(cls, api_key): + return cls.query().filter(cls.api_key == api_key).one() + + def update_lastlogin(self): + """Update user lastlogin""" + + self.last_login = datetime.datetime.now() + Session.add(self) + Session.commit() + log.debug('updated user %s lastlogin' % self.username) + + @classmethod + def create(cls, form_data): + from rhodecode.lib.auth import get_crypt_password + + try: + new_user = cls() + for k, v in form_data.items(): + if k == 'password': + v = get_crypt_password(v) + setattr(new_user, k, v) + + new_user.api_key = generate_api_key(form_data['username']) + Session.add(new_user) + Session.commit() + return new_user + except: + log.error(traceback.format_exc()) + Session.rollback() + raise + +class UserLog(Base, BaseModel): + __tablename__ = 'user_logs' + __table_args__ = {'extend_existing':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('users.user_id'), nullable=False, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) + repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + action = Column("action", UnicodeText(length=1200000, 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) + + @property + def action_as_day(self): + return date(*self.action_date.timetuple()[:3]) + + user = relationship('User') + repository = relationship('Repository') + + +class UsersGroup(Base, BaseModel): + __tablename__ = 'users_groups' + __table_args__ = {'extend_existing':True} + + users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None) + users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None) + + members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined") + + def __repr__(self): + return '' % (self.users_group_name) + + @classmethod + def get_by_group_name(cls, group_name, cache=False, case_insensitive=False): + if case_insensitive: + gr = cls.query()\ + .filter(cls.users_group_name.ilike(group_name)) + else: + gr = cls.query()\ + .filter(cls.users_group_name == group_name) + if cache: + gr = gr.options(FromCache("sql_cache_short", + "get_user_%s" % group_name)) + return gr.scalar() + + + @classmethod + def get(cls, users_group_id, cache=False): + users_group = cls.query() + if cache: + users_group = users_group.options(FromCache("sql_cache_short", + "get_users_group_%s" % users_group_id)) + return users_group.get(users_group_id) + + @classmethod + def create(cls, form_data): + try: + new_users_group = cls() + for k, v in form_data.items(): + setattr(new_users_group, k, v) + + Session.add(new_users_group) + Session.commit() + return new_users_group + except: + log.error(traceback.format_exc()) + Session.rollback() + raise + + @classmethod + def update(cls, users_group_id, form_data): + + try: + users_group = cls.get(users_group_id, cache=False) + + for k, v in form_data.items(): + if k == 'users_group_members': + users_group.members = [] + Session.flush() + members_list = [] + if v: + v = [v] if isinstance(v, basestring) else v + for u_id in set(v): + member = UsersGroupMember(users_group_id, u_id) + members_list.append(member) + setattr(users_group, 'members', members_list) + setattr(users_group, k, v) + + Session.add(users_group) + Session.commit() + except: + log.error(traceback.format_exc()) + Session.rollback() + raise + + @classmethod + def delete(cls, users_group_id): + try: + + # check if this group is not assigned to repo + assigned_groups = UsersGroupRepoToPerm.query()\ + .filter(UsersGroupRepoToPerm.users_group_id == + users_group_id).all() + + if assigned_groups: + raise UsersGroupsAssignedException('RepoGroup assigned to %s' % + assigned_groups) + + users_group = cls.get(users_group_id, cache=False) + Session.delete(users_group) + Session.commit() + except: + log.error(traceback.format_exc()) + Session.rollback() + raise + +class UsersGroupMember(Base, BaseModel): + __tablename__ = 'users_groups_members' + __table_args__ = {'extend_existing':True} + + users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + + user = relationship('User', lazy='joined') + users_group = relationship('UsersGroup') + + def __init__(self, gr_id='', u_id=''): + self.users_group_id = gr_id + self.user_id = u_id + + @staticmethod + def add_user_to_group(group, user): + ugm = UsersGroupMember() + ugm.users_group = group + ugm.user = user + Session.add(ugm) + Session.commit() + return ugm + +class Repository(Base, BaseModel): + __tablename__ = 'repositories' + __table_args__ = (UniqueConstraint('repo_name'), {'extend_existing':True},) + + repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None) + clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None) + repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg') + user_id = Column("user_id", Integer(), ForeignKey('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) + enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True) + description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) + + fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None) + group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None) + + + user = relationship('User') + fork = relationship('Repository', remote_side=repo_id) + group = relationship('RepoGroup') + repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id') + users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all') + stats = relationship('Statistics', cascade='all', uselist=False) + + followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all') + + logs = relationship('UserLog', cascade='all') + + def __repr__(self): + return "<%s('%s:%s')>" % (self.__class__.__name__, + self.repo_id, self.repo_name) + + @classmethod + def url_sep(cls): + return '/' + + @classmethod + def get_by_repo_name(cls, repo_name): + q = Session.query(cls).filter(cls.repo_name == repo_name) + q = q.options(joinedload(Repository.fork))\ + .options(joinedload(Repository.user))\ + .options(joinedload(Repository.group)) + return q.one() + + @classmethod + def get_repo_forks(cls, repo_id): + return cls.query().filter(Repository.fork_id == repo_id) + + @classmethod + def base_path(cls): + """ + Returns base path when all repos are stored + + :param cls: + """ + q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == + cls.url_sep()) + q.options(FromCache("sql_cache_short", "repository_repo_path")) + return q.one().ui_value + + @property + def just_name(self): + return self.repo_name.split(Repository.url_sep())[-1] + + @property + def groups_with_parents(self): + groups = [] + if self.group is None: + return groups + + cur_gr = self.group + groups.insert(0, cur_gr) + while 1: + gr = getattr(cur_gr, 'parent_group', None) + cur_gr = cur_gr.parent_group + if gr is None: + break + groups.insert(0, gr) + + return groups + + @property + def groups_and_repo(self): + return self.groups_with_parents, self.just_name + + @LazyProperty + def repo_path(self): + """ + Returns base full path for that repository means where it actually + exists on a filesystem + """ + q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == + Repository.url_sep()) + q.options(FromCache("sql_cache_short", "repository_repo_path")) + return q.one().ui_value + + @property + def repo_full_path(self): + p = [self.repo_path] + # we need to split the name by / since this is how we store the + # names in the database, but that eventually needs to be converted + # into a valid system path + p += self.repo_name.split(Repository.url_sep()) + return os.path.join(*p) + + def get_new_name(self, repo_name): + """ + returns new full repository name based on assigned group and new new + + :param group_name: + """ + path_prefix = self.group.full_path_splitted if self.group else [] + return Repository.url_sep().join(path_prefix + [repo_name]) + + @property + def _ui(self): + """ + Creates an db based ui object for this repository + """ + from mercurial import ui + from mercurial import config + baseui = ui.ui() + + #clean the baseui object + baseui._ocfg = config.config() + baseui._ucfg = config.config() + baseui._tcfg = config.config() + + + ret = RhodeCodeUi.query()\ + .options(FromCache("sql_cache_short", "repository_repo_ui")).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) + baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value) + + return baseui + + @classmethod + def is_valid(cls, repo_name): + """ + returns True if given repo name is a valid filesystem repository + + :param cls: + :param repo_name: + """ + from rhodecode.lib.utils import is_valid_repo + + return is_valid_repo(repo_name, cls.base_path()) + + + #========================================================================== + # SCM PROPERTIES + #========================================================================== + + def get_changeset(self, rev): + return get_changeset_safe(self.scm_instance, rev) + + @property + def tip(self): + return self.get_changeset('tip') + + @property + def author(self): + return self.tip.author + + @property + def last_change(self): + return self.scm_instance.last_change + + #========================================================================== + # SCM CACHE INSTANCE + #========================================================================== + + @property + def invalidate(self): + return CacheInvalidation.invalidate(self.repo_name) + + def set_invalidate(self): + """ + set a cache for invalidation for this instance + """ + CacheInvalidation.set_invalidate(self.repo_name) + + @LazyProperty + def scm_instance(self): + return self.__get_instance() + + @property + def scm_instance_cached(self): + @cache_region('long_term') + def _c(repo_name): + return self.__get_instance() + rn = self.repo_name + + inv = self.invalidate + if inv is not None: + region_invalidate(_c, None, rn) + # update our cache + CacheInvalidation.set_valid(inv.cache_key) + return _c(rn) + + def __get_instance(self): + + repo_full_path = self.repo_full_path + + try: + alias = get_scm(repo_full_path)[0] + log.debug('Creating instance of %s repository' % alias) + backend = get_backend(alias) + except VCSError: + log.error(traceback.format_exc()) + log.error('Perhaps this repository is in db and not in ' + 'filesystem run rescan repositories with ' + '"destroy old data " option from admin panel') + return + + if alias == 'hg': + + repo = backend(safe_str(repo_full_path), create=False, + baseui=self._ui) + # skip hidden web repository + if repo._get_hidden(): + return + else: + repo = backend(repo_full_path, create=False) + + return repo + + +class RepoGroup(Base, BaseModel): + __tablename__ = 'groups' + __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'), + CheckConstraint('group_id != group_parent_id'), {'extend_existing':True},) + __mapper_args__ = {'order_by':'group_name'} + + group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None) + group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None) + group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + + parent_group = relationship('RepoGroup', remote_side=group_id) + + + def __init__(self, group_name='', parent_group=None): + self.group_name = group_name + self.parent_group = parent_group + + def __repr__(self): + return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id, + self.group_name) + + @classmethod + def groups_choices(cls): + from webhelpers.html import literal as _literal + repo_groups = [('', '')] + sep = ' » ' + _name = lambda k: _literal(sep.join(k)) + + repo_groups.extend([(x.group_id, _name(x.full_path_splitted)) + for x in cls.query().all()]) + + repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0]) + return repo_groups + + @classmethod + def url_sep(cls): + return '/' + + @classmethod + def get_by_group_name(cls, group_name, cache=False, case_insensitive=False): + if case_insensitive: + gr = cls.query()\ + .filter(cls.group_name.ilike(group_name)) + else: + gr = cls.query()\ + .filter(cls.group_name == group_name) + if cache: + gr = gr.options(FromCache("sql_cache_short", + "get_group_%s" % group_name)) + return gr.scalar() + + @property + def parents(self): + parents_recursion_limit = 5 + groups = [] + if self.parent_group is None: + return groups + cur_gr = self.parent_group + groups.insert(0, cur_gr) + cnt = 0 + while 1: + cnt += 1 + gr = getattr(cur_gr, 'parent_group', None) + cur_gr = cur_gr.parent_group + if gr is None: + break + if cnt == parents_recursion_limit: + # this will prevent accidental infinit loops + log.error('group nested more than %s' % + parents_recursion_limit) + break + + groups.insert(0, gr) + return groups + + @property + def children(self): + return Group.query().filter(Group.parent_group == self) + + @property + def name(self): + return self.group_name.split(Group.url_sep())[-1] + + @property + def full_path(self): + return self.group_name + + @property + def full_path_splitted(self): + return self.group_name.split(Group.url_sep()) + + @property + def repositories(self): + return Repository.query().filter(Repository.group == self) + + @property + def repositories_recursive_count(self): + cnt = self.repositories.count() + + def children_count(group): + cnt = 0 + for child in group.children: + cnt += child.repositories.count() + cnt += children_count(child) + return cnt + + return cnt + children_count(self) + + + def get_new_name(self, group_name): + """ + returns new full group name based on parent and new name + + :param group_name: + """ + path_prefix = (self.parent_group.full_path_splitted if + self.parent_group else []) + return Group.url_sep().join(path_prefix + [group_name]) + + +class Permission(Base, BaseModel): + __tablename__ = 'permissions' + __table_args__ = {'extend_existing':True} + permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + + def __repr__(self): + return "<%s('%s:%s')>" % (self.__class__.__name__, + self.permission_id, self.permission_name) + + @classmethod + def get_by_key(cls, key): + return cls.query().filter(cls.permission_name == key).scalar() + +class UserRepoToPerm(Base, BaseModel): + __tablename__ = 'repo_to_perm' + __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'extend_existing':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('users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) + + user = relationship('User') + permission = relationship('Permission') + repository = relationship('Repository') + +class UserToPerm(Base, BaseModel): + __tablename__ = 'user_to_perm' + __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'extend_existing':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('users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + user = relationship('User') + permission = relationship('Permission') + + @classmethod + def has_perm(cls, user_id, perm): + if not isinstance(perm, Permission): + raise Exception('perm needs to be an instance of Permission class') + + return cls.query().filter(cls.user_id == user_id)\ + .filter(cls.permission == perm).scalar() is not None + + @classmethod + def grant_perm(cls, user_id, perm): + if not isinstance(perm, Permission): + raise Exception('perm needs to be an instance of Permission class') + + new = cls() + new.user_id = user_id + new.permission = perm + try: + Session.add(new) + Session.commit() + except: + Session.rollback() + + + @classmethod + def revoke_perm(cls, user_id, perm): + if not isinstance(perm, Permission): + raise Exception('perm needs to be an instance of Permission class') + + try: + cls.query().filter(cls.user_id == user_id)\ + .filter(cls.permission == perm).delete() + Session.commit() + except: + Session.rollback() + +class UsersGroupRepoToPerm(Base, BaseModel): + __tablename__ = 'users_group_repo_to_perm' + __table_args__ = (UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), {'extend_existing':True}) + users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) + + users_group = relationship('UsersGroup') + permission = relationship('Permission') + repository = relationship('Repository') + + def __repr__(self): + return ' %s >' % (self.users_group, self.repository) + +class UsersGroupToPerm(Base, BaseModel): + __tablename__ = 'users_group_to_perm' + __table_args__ = {'extend_existing':True} + users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + users_group = relationship('UsersGroup') + permission = relationship('Permission') + + + @classmethod + def has_perm(cls, users_group_id, perm): + if not isinstance(perm, Permission): + raise Exception('perm needs to be an instance of Permission class') + + return cls.query().filter(cls.users_group_id == + users_group_id)\ + .filter(cls.permission == perm)\ + .scalar() is not None + + @classmethod + def grant_perm(cls, users_group_id, perm): + if not isinstance(perm, Permission): + raise Exception('perm needs to be an instance of Permission class') + + new = cls() + new.users_group_id = users_group_id + new.permission = perm + try: + Session.add(new) + Session.commit() + except: + Session.rollback() + + + @classmethod + def revoke_perm(cls, users_group_id, perm): + if not isinstance(perm, Permission): + raise Exception('perm needs to be an instance of Permission class') + + try: + cls.query().filter(cls.users_group_id == users_group_id)\ + .filter(cls.permission == perm).delete() + Session.commit() + except: + Session.rollback() + + +class UserRepoGroupToPerm(Base, BaseModel): + __tablename__ = 'group_to_perm' + __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing':True}) + + group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None) + + user = relationship('User') + permission = relationship('Permission') + group = relationship('RepoGroup') + +class Statistics(Base, BaseModel): + __tablename__ = 'statistics' + __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing':True}) + stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + repository_id = Column("repository_id", Integer(), ForeignKey('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(1000000), nullable=False)#JSON data + commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data + languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data + + repository = relationship('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') + , {'extend_existing':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('users.user_id'), nullable=False, unique=None, default=None) + follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None) + follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) + + user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id') + + follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id') + follows_repository = relationship('Repository', order_by='Repository.repo_name') + + + @classmethod + def get_repo_followers(cls, repo_id): + return cls.query().filter(cls.follows_repo_id == repo_id) + +class CacheInvalidation(Base, BaseModel): + __tablename__ = 'cache_invalidation' + __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing':True}) + cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + cache_args = Column("cache_args", String(length=255, 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 "<%s('%s:%s')>" % (self.__class__.__name__, + self.cache_id, self.cache_key) + + @classmethod + def invalidate(cls, key): + """ + Returns Invalidation object if this given key should be invalidated + None otherwise. `cache_active = False` means that this cache + state is not valid and needs to be invalidated + + :param key: + """ + return cls.query()\ + .filter(CacheInvalidation.cache_key == key)\ + .filter(CacheInvalidation.cache_active == False)\ + .scalar() + + @classmethod + def set_invalidate(cls, key): + """ + Mark this Cache key for invalidation + + :param key: + """ + + log.debug('marking %s for invalidation' % key) + inv_obj = Session.query(cls)\ + .filter(cls.cache_key == key).scalar() + if inv_obj: + inv_obj.cache_active = False + else: + log.debug('cache key not found in invalidation db -> creating one') + inv_obj = CacheInvalidation(key) + + try: + Session.add(inv_obj) + Session.commit() + except Exception: + log.error(traceback.format_exc()) + Session.rollback() + + @classmethod + def set_valid(cls, key): + """ + Mark this cache key as active and currently cached + + :param key: + """ + inv_obj = Session.query(CacheInvalidation)\ + .filter(CacheInvalidation.cache_key == key).scalar() + inv_obj.cache_active = True + Session.add(inv_obj) + Session.commit() + +class DbMigrateVersion(Base, BaseModel): + __tablename__ = 'db_migrate_version' + __table_args__ = {'extend_existing':True} + repository_id = Column('repository_id', String(250), primary_key=True) + repository_path = Column('repository_path', Text) + version = Column('version', Integer) diff --git a/rhodecode/lib/dbmigrate/schema/db_1_3_0.py b/rhodecode/lib/dbmigrate/schema/db_1_3_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/schema/db_1_3_0.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.model.db + ~~~~~~~~~~~~~~~~~~ + + Database Models for RhodeCode + + :created_on: Apr 08, 2010 + :author: marcink + :copyright: (C) 2010-2012 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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, see . + +#TODO: when branch 1.3 is finished replacem with db.py content + +from rhodecode.model.db import * diff --git a/rhodecode/lib/dbmigrate/versions/001_initial_release.py b/rhodecode/lib/dbmigrate/versions/001_initial_release.py --- a/rhodecode/lib/dbmigrate/versions/001_initial_release.py +++ b/rhodecode/lib/dbmigrate/versions/001_initial_release.py @@ -14,7 +14,7 @@ from rhodecode.lib.dbmigrate.migrate imp log = logging.getLogger(__name__) -class RhodeCodeSettings(Base): +class RhodeCodeSetting(Base): __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) @@ -74,7 +74,7 @@ class User(Base): self.last_login = datetime.datetime.now() session.add(self) session.commit() - log.debug('updated user %s lastlogin', self.username) + log.debug('updated user %s lastlogin' % self.username) except (DatabaseError,): session.rollback() @@ -107,7 +107,7 @@ class Repository(Base): user = relation('User') fork = relation('Repository', remote_side=repo_id) - repo_to_perm = relation('RepoToPerm', cascade='all') + repo_to_perm = relation('UserRepoToPerm', cascade='all') stats = relation('Statistics', cascade='all', uselist=False) repo_followers = relation('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all') @@ -126,7 +126,7 @@ class Permission(Base): def __repr__(self): return "" % (self.permission_id, self.permission_name) -class RepoToPerm(Base): +class UserRepoToPerm(Base): __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) diff --git a/rhodecode/lib/dbmigrate/versions/002_version_1_1_0.py b/rhodecode/lib/dbmigrate/versions/002_version_1_1_0.py --- a/rhodecode/lib/dbmigrate/versions/002_version_1_1_0.py +++ b/rhodecode/lib/dbmigrate/versions/002_version_1_1_0.py @@ -12,6 +12,7 @@ from rhodecode.lib.dbmigrate.migrate.cha log = logging.getLogger(__name__) + def upgrade(migrate_engine): """ Upgrade operations go here. Don't create your own engine; bind migrate_engine to your metadata @@ -44,8 +45,6 @@ def upgrade(migrate_engine): nullable=True, unique=None, default=None) revision.create(tbl) - - #========================================================================== # Upgrade of `repositories` table #========================================================================== @@ -69,47 +68,18 @@ def upgrade(migrate_engine): #========================================================================== # Add table `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') - + from rhodecode.lib.dbmigrate.schema.db_1_1_0 import UserFollowing UserFollowing().__table__.create() #========================================================================== # Add table `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) + from rhodecode.lib.dbmigrate.schema.db_1_1_0 import CacheInvalidation CacheInvalidation().__table__.create() return + def downgrade(migrate_engine): meta = MetaData() meta.bind = migrate_engine diff --git a/rhodecode/lib/dbmigrate/versions/003_version_1_2_0.py b/rhodecode/lib/dbmigrate/versions/003_version_1_2_0.py --- a/rhodecode/lib/dbmigrate/versions/003_version_1_2_0.py +++ b/rhodecode/lib/dbmigrate/versions/003_version_1_2_0.py @@ -13,6 +13,7 @@ from rhodecode.model.meta import Base log = logging.getLogger(__name__) + def upgrade(migrate_engine): """ Upgrade operations go here. Don't create your own engine; bind migrate_engine to your metadata @@ -21,46 +22,46 @@ def upgrade(migrate_engine): #========================================================================== # Add table `groups`` #========================================================================== - from rhodecode.model.db import Group + from rhodecode.lib.dbmigrate.schema.db_1_2_0 import RepoGroup as Group Group().__table__.create() #========================================================================== # Add table `group_to_perm` #========================================================================== - from rhodecode.model.db import GroupToPerm - GroupToPerm().__table__.create() + from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UserRepoGroupToPerm + UserRepoGroupToPerm().__table__.create() #========================================================================== # Add table `users_groups` #========================================================================== - from rhodecode.model.db import UsersGroup + from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroup UsersGroup().__table__.create() #========================================================================== # Add table `users_groups_members` #========================================================================== - from rhodecode.model.db import UsersGroupMember + from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupMember UsersGroupMember().__table__.create() #========================================================================== # Add table `users_group_repo_to_perm` #========================================================================== - from rhodecode.model.db import UsersGroupRepoToPerm + from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupRepoToPerm UsersGroupRepoToPerm().__table__.create() #========================================================================== # Add table `users_group_to_perm` #========================================================================== - from rhodecode.model.db import UsersGroupToPerm + from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupToPerm UsersGroupToPerm().__table__.create() #========================================================================== # Upgrade of `users` table #========================================================================== - from rhodecode.model.db import User + from rhodecode.lib.dbmigrate.schema.db_1_2_0 import User #add column - ldap_dn = Column("ldap_dn", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) ldap_dn.create(User().__table__) api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) @@ -74,7 +75,7 @@ def upgrade(migrate_engine): #========================================================================== # Upgrade of `repositories` table #========================================================================== - from rhodecode.model.db import Repository + from rhodecode.lib.dbmigrate.schema.db_1_2_0 import Repository #ADD clone_uri column# @@ -83,7 +84,7 @@ def upgrade(migrate_engine): nullable=True, unique=False, default=None) clone_uri.create(Repository().__table__) - + #ADD downloads column# enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True) enable_downloads.create(Repository().__table__) @@ -104,10 +105,10 @@ def upgrade(migrate_engine): # Upgrade of `user_followings` table #========================================================================== - from rhodecode.model.db import UserFollowing + from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UserFollowing - follows_from = Column('follows_from', DateTime(timezone=False), - nullable=True, unique=None, + follows_from = Column('follows_from', DateTime(timezone=False), + nullable=True, unique=None, default=datetime.datetime.now) follows_from.create(UserFollowing().__table__) diff --git a/rhodecode/lib/dbmigrate/versions/004_version_1_3_0.py b/rhodecode/lib/dbmigrate/versions/004_version_1_3_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/004_version_1_3_0.py @@ -0,0 +1,74 @@ +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.lib.dbmigrate.migrate import * +from rhodecode.lib.dbmigrate.migrate.changeset import * + +from rhodecode.model.meta import Base + +log = logging.getLogger(__name__) + + +def upgrade(migrate_engine): + """ Upgrade operations go here. + Don't create your own engine; bind migrate_engine to your metadata + """ + #========================================================================== + # Add table `users_group_repo_group_to_perm` + #========================================================================== + from rhodecode.lib.dbmigrate.schema.db_1_3_0 import UsersGroupRepoGroupToPerm + UsersGroupRepoGroupToPerm().__table__.create() + + #========================================================================== + # Add table `changeset_comments` + #========================================================================== + from rhodecode.lib.dbmigrate.schema.db_1_3_0 import ChangesetComment + ChangesetComment().__table__.create() + + #========================================================================== + # Add table `notifications` + #========================================================================== + from rhodecode.lib.dbmigrate.schema.db_1_3_0 import Notification + Notification().__table__.create() + + #========================================================================== + # Add table `user_to_notification` + #========================================================================== + from rhodecode.lib.dbmigrate.schema.db_1_3_0 import UserNotification + UserNotification().__table__.create() + + #========================================================================== + # Add unique to table `users_group_to_perm` + #========================================================================== + from rhodecode.lib.dbmigrate.schema.db_1_3_0 import UsersGroupToPerm + tbl = UsersGroupToPerm().__table__ + cons = UniqueConstraint('users_group_id', 'permission_id', table=tbl) + cons.create() + + #========================================================================== + # Fix unique constrain on table `user_logs` + #========================================================================== + from rhodecode.lib.dbmigrate.schema.db_1_3_0 import UserLog + tbl = UserLog().__table__ + col = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), + nullable=False, unique=None, default=None) + col.alter(nullable=True, table=tbl) + + #========================================================================== + # Rename table `group_to_perm` to `user_repo_group_to_perm` + #========================================================================== + tbl = Table('group_to_perm', MetaData(bind=migrate_engine), autoload=True, + autoload_with=migrate_engine) + tbl.rename('user_repo_group_to_perm') + + return + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine diff --git a/rhodecode/lib/dbmigrate/versions/005_version_1_3_0.py b/rhodecode/lib/dbmigrate/versions/005_version_1_3_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/005_version_1_3_0.py @@ -0,0 +1,65 @@ +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.lib.dbmigrate.migrate import * +from rhodecode.lib.dbmigrate.migrate.changeset import * + +from rhodecode.model.meta import Base + +log = logging.getLogger(__name__) + + +def upgrade(migrate_engine): + """ Upgrade operations go here. + Don't create your own engine; bind migrate_engine to your metadata + """ + + #========================================================================== + # Change unique constraints of table `repo_to_perm` + #========================================================================== + from rhodecode.lib.dbmigrate.schema.db_1_3_0 import UserRepoToPerm + tbl = UserRepoToPerm().__table__ + new_cons = UniqueConstraint('user_id', 'repository_id', 'permission_id', table=tbl) + new_cons.create() + + if migrate_engine.name in ['mysql']: + old_cons = UniqueConstraint('user_id', 'repository_id', table=tbl, name="user_id") + old_cons.drop() + elif migrate_engine.name in ['postgresql']: + old_cons = UniqueConstraint('user_id', 'repository_id', table=tbl) + old_cons.drop() + else: + # sqlite doesn't support dropping constraints... + print """Please manually drop UniqueConstraint('user_id', 'repository_id')""" + + #========================================================================== + # fix uniques of table `user_repo_group_to_perm` + #========================================================================== + from rhodecode.lib.dbmigrate.schema.db_1_3_0 import UserRepoGroupToPerm + tbl = UserRepoGroupToPerm().__table__ + new_cons = UniqueConstraint('group_id', 'permission_id', 'user_id', table=tbl) + new_cons.create() + + # fix uniqueConstraints + if migrate_engine.name in ['mysql']: + #mysql is givinig troubles here... + old_cons = UniqueConstraint('group_id', 'permission_id', table=tbl, name="group_id") + old_cons.drop() + elif migrate_engine.name in ['postgresql']: + old_cons = UniqueConstraint('group_id', 'permission_id', table=tbl, name='group_to_perm_group_id_permission_id_key') + old_cons.drop() + else: + # sqlite doesn't support dropping constraints... + print """Please manually drop UniqueConstraint('group_id', 'permission_id')""" + + return + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine diff --git a/rhodecode/lib/dbmigrate/versions/__init__.py b/rhodecode/lib/dbmigrate/versions/__init__.py --- a/rhodecode/lib/dbmigrate/versions/__init__.py +++ b/rhodecode/lib/dbmigrate/versions/__init__.py @@ -7,7 +7,7 @@ :created_on: Dec 11, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify diff --git a/rhodecode/lib/diffs.py b/rhodecode/lib/diffs.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/diffs.py @@ -0,0 +1,517 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.lib.diffs + ~~~~~~~~~~~~~~~~~~~ + + Set of diffing helpers, previously part of vcs + + + :created_on: Dec 4, 2011 + :author: marcink + :copyright: (C) 2010-2012 Marcin Kuzminski + :original copyright: 2007-2008 by Armin Ronacher + :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, either version 3 of the License, or +# (at your option) any later version. +# +# 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, see . + +import re +import difflib +import markupsafe +from itertools import tee, imap + +from pylons.i18n.translation import _ + +from rhodecode.lib.vcs.exceptions import VCSError +from rhodecode.lib.vcs.nodes import FileNode + +from rhodecode.lib.utils import EmptyChangeset + + +def wrap_to_table(str_): + return ''' + + + + +
%s
''' % str_ + + +def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None, + ignore_whitespace=True, line_context=3, + enable_comments=False): + """ + returns a wrapped diff into a table, checks for cut_off_limit and presents + proper message + """ + + if filenode_old is None: + filenode_old = FileNode(filenode_new.path, '', EmptyChangeset()) + + if filenode_old.is_binary or filenode_new.is_binary: + diff = wrap_to_table(_('binary file')) + stats = (0, 0) + size = 0 + + elif cut_off_limit != -1 and (cut_off_limit is None or + (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)): + + f_gitdiff = get_gitdiff(filenode_old, filenode_new, + ignore_whitespace=ignore_whitespace, + context=line_context) + diff_processor = DiffProcessor(f_gitdiff, format='gitdiff') + + diff = diff_processor.as_html(enable_comments=enable_comments) + stats = diff_processor.stat() + size = len(diff or '') + else: + diff = wrap_to_table(_('Changeset was to big and was cut off, use ' + 'diff menu to display this diff')) + stats = (0, 0) + size = 0 + + if not diff: + diff = wrap_to_table(_('No changes detected')) + + cs1 = filenode_old.last_changeset.raw_id + cs2 = filenode_new.last_changeset.raw_id + + return size, cs1, cs2, diff, stats + + +def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3): + """ + Returns git style diff between given ``filenode_old`` and ``filenode_new``. + + :param ignore_whitespace: ignore whitespaces in diff + """ + # make sure we pass in default context + context = context or 3 + + for filenode in (filenode_old, filenode_new): + if not isinstance(filenode, FileNode): + raise VCSError("Given object should be FileNode object, not %s" + % filenode.__class__) + + repo = filenode_new.changeset.repository + old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET) + new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET) + + vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path, + ignore_whitespace, context) + + return vcs_gitdiff + + +class DiffProcessor(object): + """ + Give it a unified diff and it returns a list of the files that were + mentioned in the diff together with a dict of meta information that + can be used to render it in a HTML template. + """ + _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)') + + def __init__(self, diff, differ='diff', format='udiff'): + """ + :param diff: a text in diff format or generator + :param format: format of diff passed, `udiff` or `gitdiff` + """ + if isinstance(diff, basestring): + diff = [diff] + + self.__udiff = diff + self.__format = format + self.adds = 0 + self.removes = 0 + + if isinstance(self.__udiff, basestring): + self.lines = iter(self.__udiff.splitlines(1)) + + elif self.__format == 'gitdiff': + udiff_copy = self.copy_iterator() + self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy)) + else: + udiff_copy = self.copy_iterator() + self.lines = imap(self.escaper, udiff_copy) + + # Select a differ. + if differ == 'difflib': + self.differ = self._highlight_line_difflib + else: + self.differ = self._highlight_line_udiff + + def escaper(self, string): + return markupsafe.escape(string) + + def copy_iterator(self): + """ + make a fresh copy of generator, we should not iterate thru + an original as it's needed for repeating operations on + this instance of DiffProcessor + """ + self.__udiff, iterator_copy = tee(self.__udiff) + return iterator_copy + + def _extract_rev(self, line1, line2): + """ + Extract the filename and revision hint from a line. + """ + + try: + if line1.startswith('--- ') and line2.startswith('+++ '): + l1 = line1[4:].split(None, 1) + old_filename = (l1[0].replace('a/', '', 1) + if len(l1) >= 1 else None) + old_rev = l1[1] if len(l1) == 2 else 'old' + + l2 = line2[4:].split(None, 1) + new_filename = (l2[0].replace('b/', '', 1) + if len(l1) >= 1 else None) + new_rev = l2[1] if len(l2) == 2 else 'new' + + filename = (old_filename + if old_filename != '/dev/null' else new_filename) + + return filename, new_rev, old_rev + except (ValueError, IndexError): + pass + + return None, None, None + + def _parse_gitdiff(self, diffiterator): + def line_decoder(l): + if l.startswith('+') and not l.startswith('+++'): + self.adds += 1 + elif l.startswith('-') and not l.startswith('---'): + self.removes += 1 + return l.decode('utf8', 'replace') + + output = list(diffiterator) + size = len(output) + + if size == 2: + l = [] + l.extend([output[0]]) + l.extend(output[1].splitlines(1)) + return map(line_decoder, l) + elif size == 1: + return map(line_decoder, output[0].splitlines(1)) + elif size == 0: + return [] + + raise Exception('wrong size of diff %s' % size) + + def _highlight_line_difflib(self, line, next_): + """ + Highlight inline changes in both lines. + """ + + if line['action'] == 'del': + old, new = line, next_ + else: + old, new = next_, line + + oldwords = re.split(r'(\W)', old['line']) + newwords = re.split(r'(\W)', new['line']) + + sequence = difflib.SequenceMatcher(None, oldwords, newwords) + + oldfragments, newfragments = [], [] + for tag, i1, i2, j1, j2 in sequence.get_opcodes(): + oldfrag = ''.join(oldwords[i1:i2]) + newfrag = ''.join(newwords[j1:j2]) + if tag != 'equal': + if oldfrag: + oldfrag = '%s' % oldfrag + if newfrag: + newfrag = '%s' % newfrag + oldfragments.append(oldfrag) + newfragments.append(newfrag) + + old['line'] = "".join(oldfragments) + new['line'] = "".join(newfragments) + + def _highlight_line_udiff(self, line, next_): + """ + Highlight inline changes in both lines. + """ + start = 0 + limit = min(len(line['line']), len(next_['line'])) + while start < limit and line['line'][start] == next_['line'][start]: + start += 1 + end = -1 + limit -= start + while -end <= limit and line['line'][end] == next_['line'][end]: + end -= 1 + end += 1 + if start or end: + def do(l): + last = end + len(l['line']) + if l['action'] == 'add': + tag = 'ins' + else: + tag = 'del' + l['line'] = '%s<%s>%s%s' % ( + l['line'][:start], + tag, + l['line'][start:last], + tag, + l['line'][last:] + ) + do(line) + do(next_) + + def _parse_udiff(self): + """ + Parse the diff an return data for the template. + """ + lineiter = self.lines + files = [] + try: + line = lineiter.next() + # skip first context + skipfirst = True + while 1: + # continue until we found the old file + if not line.startswith('--- '): + line = lineiter.next() + continue + + chunks = [] + filename, old_rev, new_rev = \ + self._extract_rev(line, lineiter.next()) + files.append({ + 'filename': filename, + 'old_revision': old_rev, + 'new_revision': new_rev, + 'chunks': chunks + }) + + line = lineiter.next() + while line: + match = self._chunk_re.match(line) + if not match: + break + + lines = [] + chunks.append(lines) + + old_line, old_end, new_line, new_end = \ + [int(x or 1) for x in match.groups()[:-1]] + old_line -= 1 + new_line -= 1 + context = len(match.groups()) == 5 + old_end += old_line + new_end += new_line + + if context: + if not skipfirst: + lines.append({ + 'old_lineno': '...', + 'new_lineno': '...', + 'action': 'context', + 'line': line, + }) + else: + skipfirst = False + + line = lineiter.next() + while old_line < old_end or new_line < new_end: + if line: + command, line = line[0], line[1:] + else: + command = ' ' + affects_old = affects_new = False + + # ignore those if we don't expect them + if command in '#@': + continue + elif command == '+': + affects_new = True + action = 'add' + elif command == '-': + affects_old = True + action = 'del' + else: + affects_old = affects_new = True + action = 'unmod' + + old_line += affects_old + new_line += affects_new + lines.append({ + 'old_lineno': affects_old and old_line or '', + 'new_lineno': affects_new and new_line or '', + 'action': action, + 'line': line + }) + line = lineiter.next() + + except StopIteration: + pass + + # highlight inline changes + for _ in files: + for chunk in chunks: + lineiter = iter(chunk) + #first = True + try: + while 1: + line = lineiter.next() + if line['action'] != 'unmod': + nextline = lineiter.next() + if nextline['action'] == 'unmod' or \ + nextline['action'] == line['action']: + continue + self.differ(line, nextline) + except StopIteration: + pass + + return files + + def prepare(self): + """ + Prepare the passed udiff for HTML rendering. It'l return a list + of dicts + """ + return self._parse_udiff() + + def _safe_id(self, idstring): + """Make a string safe for including in an id attribute. + + The HTML spec says that id attributes 'must begin with + a letter ([A-Za-z]) and may be followed by any number + of letters, digits ([0-9]), hyphens ("-"), underscores + ("_"), colons (":"), and periods (".")'. These regexps + are slightly over-zealous, in that they remove colons + and periods unnecessarily. + + Whitespace is transformed into underscores, and then + anything which is not a hyphen or a character that + matches \w (alphanumerics and underscore) is removed. + + """ + # Transform all whitespace to underscore + idstring = re.sub(r'\s', "_", '%s' % idstring) + # Remove everything that is not a hyphen or a member of \w + idstring = re.sub(r'(?!-)\W', "", idstring).lower() + return idstring + + def raw_diff(self): + """ + Returns raw string as udiff + """ + udiff_copy = self.copy_iterator() + if self.__format == 'gitdiff': + udiff_copy = self._parse_gitdiff(udiff_copy) + return u''.join(udiff_copy) + + def as_html(self, table_class='code-difftable', line_class='line', + new_lineno_class='lineno old', old_lineno_class='lineno new', + code_class='code', enable_comments=False): + """ + Return udiff as html table with customized css classes + """ + def _link_to_if(condition, label, url): + """ + Generates a link if condition is meet or just the label if not. + """ + + if condition: + return '''%(label)s''' % { + 'url': url, + 'label': label + } + else: + return label + diff_lines = self.prepare() + _html_empty = True + _html = [] + _html.append('''\n''' % { + 'table_class': table_class + }) + for diff in diff_lines: + for line in diff['chunks']: + _html_empty = False + for change in line: + _html.append('''\n''' % { + 'lc': line_class, + 'action': change['action'] + }) + anchor_old_id = '' + anchor_new_id = '' + anchor_old = "%(filename)s_o%(oldline_no)s" % { + 'filename': self._safe_id(diff['filename']), + 'oldline_no': change['old_lineno'] + } + anchor_new = "%(filename)s_n%(oldline_no)s" % { + 'filename': self._safe_id(diff['filename']), + 'oldline_no': change['new_lineno'] + } + cond_old = (change['old_lineno'] != '...' and + change['old_lineno']) + cond_new = (change['new_lineno'] != '...' and + change['new_lineno']) + if cond_old: + anchor_old_id = 'id="%s"' % anchor_old + if cond_new: + anchor_new_id = 'id="%s"' % anchor_new + ########################################################### + # OLD LINE NUMBER + ########################################################### + _html.append('''\t\n''') + ########################################################### + # NEW LINE NUMBER + ########################################################### + + _html.append('''\t\n''') + ########################################################### + # CODE + ########################################################### + comments = '' if enable_comments else 'no-comment' + _html.append('''\t''') + _html.append('''\n\n''') + _html.append('''
''' % { + 'a_id': anchor_old_id, + 'olc': old_lineno_class + }) + + _html.append('''%(link)s''' % { + 'link': _link_to_if(True, change['old_lineno'], + '#%s' % anchor_old) + }) + _html.append('''''' % { + 'a_id': anchor_new_id, + 'nlc': new_lineno_class + }) + + _html.append('''%(link)s''' % { + 'link': _link_to_if(True, change['new_lineno'], + '#%s' % anchor_new) + }) + _html.append('''''' % { + 'cc': code_class, + 'inc': comments + }) + _html.append('''\n\t\t
%(code)s
\n''' % { + 'code': change['line'] + }) + _html.append('''\t
''') + if _html_empty: + return None + return ''.join(_html) + + def stat(self): + """ + Returns tuple of added, and removed lines for this instance + """ + return self.adds, self.removes diff --git a/rhodecode/lib/exceptions.py b/rhodecode/lib/exceptions.py --- a/rhodecode/lib/exceptions.py +++ b/rhodecode/lib/exceptions.py @@ -6,7 +6,8 @@ Set of custom exceptions used in RhodeCode :created_on: Nov 17, 2010 - :copyright: (C) 2009-2011 Marcin Kuzminski + :author: marcink + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -46,5 +47,6 @@ class DefaultUserException(Exception): class UserOwnsReposException(Exception): pass + class UsersGroupsAssignedException(Exception): pass diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -8,22 +8,24 @@ import hashlib import StringIO import urllib import math +import logging from datetime import datetime -from pygments.formatters import HtmlFormatter +from pygments.formatters.html import HtmlFormatter from pygments import highlight as code_highlight from pylons import url, request, config from pylons.i18n.translation import _, ungettext +from hashlib import md5 from webhelpers.html import literal, HTML, escape from webhelpers.html.tools import * from webhelpers.html.builder import make_tag from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \ - end_form, file, form, hidden, image, javascript_link, link_to, link_to_if, \ - link_to_unless, ol, required_legend, select, stylesheet_link, submit, text, \ - password, textarea, title, ul, xml_declaration, radio -from webhelpers.html.tools import auto_link, button_to, highlight, js_obfuscate, \ - mail_to, strip_links, strip_tags, tag_re + end_form, file, form, hidden, image, javascript_link, link_to, \ + link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \ + submit, text, password, textarea, title, ul, xml_declaration, radio +from webhelpers.html.tools import auto_link, button_to, highlight, \ + js_obfuscate, mail_to, strip_links, strip_tags, tag_re from webhelpers.number import format_byte_size, format_bit_size from webhelpers.pylonslib import Flash as _Flash from webhelpers.pylonslib.secure_form import secure_form @@ -33,11 +35,15 @@ from webhelpers.text import chop_at, col from webhelpers.date import time_ago_in_words from webhelpers.paginate import Page from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \ - convert_boolean_attrs, NotGiven + convert_boolean_attrs, NotGiven, _make_safe_id_component -from vcs.utils.annotate import annotate_highlight +from rhodecode.lib.annotate import annotate_highlight from rhodecode.lib.utils import repo_name_slug -from rhodecode.lib import str2bool, safe_unicode, safe_str,get_changeset_safe +from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe +from rhodecode.lib.markup_renderer import MarkupRenderer + +log = logging.getLogger(__name__) + def _reset(name, value=None, id=NotGiven, type="reset", **attrs): """ @@ -49,6 +55,19 @@ def _reset(name, value=None, id=NotGiven return HTML.input(**attrs) reset = _reset +safeid = _make_safe_id_component + + +def FID(raw_id, path): + """ + Creates a uniqe ID for filenode based on it's hash of path and revision + it's safe to use in urls + + :param raw_id: + :param path: + """ + + return 'C-%s-%s' % (short_id(raw_id), md5(path).hexdigest()[:12]) def get_token(): @@ -104,10 +123,14 @@ class _FilesBreadCrumbs(object): paths_l = paths.split('/') for cnt, p in enumerate(paths_l): if p != '': - url_l.append(link_to(p, url('files_home', - repo_name=repo_name, - revision=rev, - f_path='/'.join(paths_l[:cnt + 1])))) + url_l.append(link_to(p, + url('files_home', + repo_name=repo_name, + revision=rev, + f_path='/'.join(paths_l[:cnt + 1]) + ) + ) + ) return literal('/'.join(url_l)) @@ -198,13 +221,16 @@ def pygmentize(filenode, **kwargs): return literal(code_highlight(filenode.content, filenode.lexer, CodeHtmlFormatter(**kwargs))) + def pygmentize_annotation(repo_name, filenode, **kwargs): - """pygmentize function for annotation + """ + pygmentize function for annotation :param filenode: """ color_dict = {} + def gen_color(n=10000): """generator for getting n of evenly distributed colors using hsv color and golden ratio. It always return same order of colors @@ -213,19 +239,26 @@ def pygmentize_annotation(repo_name, fil """ def hsv_to_rgb(h, s, v): - if s == 0.0: return v, v, v - i = int(h * 6.0) # XXX assume int() truncates! + if s == 0.0: + return v, v, v + i = int(h * 6.0) # XXX assume int() truncates! f = (h * 6.0) - i p = v * (1.0 - s) q = v * (1.0 - s * f) t = v * (1.0 - s * (1.0 - f)) i = i % 6 - if i == 0: return v, t, p - if i == 1: return q, v, p - if i == 2: return p, v, t - if i == 3: return p, q, v - if i == 4: return t, p, v - if i == 5: return v, p, q + if i == 0: + return v, t, p + if i == 1: + return q, v, p + if i == 2: + return p, v, t + if i == 3: + return p, q, v + if i == 4: + return t, p, v + if i == 5: + return v, p, q golden_ratio = 0.618033988749895 h = 0.22717784590367374 @@ -235,12 +268,12 @@ def pygmentize_annotation(repo_name, fil h %= 1 HSV_tuple = [h, 0.95, 0.95] RGB_tuple = hsv_to_rgb(*HSV_tuple) - yield map(lambda x:str(int(x * 256)), RGB_tuple) + yield map(lambda x: str(int(x * 256)), RGB_tuple) cgenerator = gen_color() def get_color_string(cs): - if color_dict.has_key(cs): + if cs in color_dict: col = color_dict[cs] else: col = color_dict[cs] = cgenerator.next() @@ -275,6 +308,7 @@ def pygmentize_annotation(repo_name, fil return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs)) + def is_following_repo(repo_name, user_id): from rhodecode.model.scm import ScmModel return ScmModel().is_following_repo(repo_name, user_id) @@ -284,17 +318,75 @@ flash = _Flash() #============================================================================== # SCM FILTERS available via h. #============================================================================== -from vcs.utils import author_name, author_email +from rhodecode.lib.vcs.utils import author_name, author_email from rhodecode.lib import credentials_filter, age as _age +from rhodecode.model.db import User -age = lambda x:_age(x) +age = lambda x: _age(x) capitalize = lambda x: x.capitalize() email = author_email -email_or_none = lambda x: email(x) if email(x) != x else None -person = lambda x: author_name(x) short_id = lambda x: x[:12] hide_credentials = lambda x: ''.join(credentials_filter(x)) + +def is_git(repository): + if hasattr(repository, 'alias'): + _type = repository.alias + elif hasattr(repository, 'repo_type'): + _type = repository.repo_type + else: + _type = repository + return _type == 'git' + + +def is_hg(repository): + if hasattr(repository, 'alias'): + _type = repository.alias + elif hasattr(repository, 'repo_type'): + _type = repository.repo_type + else: + _type = repository + return _type == 'hg' + + +def email_or_none(author): + _email = email(author) + if _email != '': + return _email + + # See if it contains a username we can get an email from + user = User.get_by_username(author_name(author), case_insensitive=True, + cache=True) + if user is not None: + return user.email + + # No valid email, not a valid user in the system, none! + return None + + +def person(author): + # attr to return from fetched user + person_getter = lambda usr: usr.username + + # Valid email in the attribute passed, see if they're in the system + _email = email(author) + if _email != '': + user = User.get_by_email(_email, case_insensitive=True, cache=True) + if user is not None: + return person_getter(user) + return _email + + # Maybe it's a username? + _author = author_name(author) + user = User.get_by_username(_author, case_insensitive=True, + cache=True) + if user is not None: + return person_getter(user) + + # Still nothing? Just pass back the author name then + return _author + + def bool2icon(value): """Returns True/False values represented as small html image of true/false icons @@ -314,7 +406,8 @@ def bool2icon(value): def action_parser(user_log, feed=False): - """This helper will action_map the specified string action into translated + """ + This helper will action_map the specified string action into translated fancy names with icons and links :param user_log: user log instance @@ -330,52 +423,84 @@ def action_parser(user_log, feed=False): action, action_params = x def get_cs_links(): - revs_limit = 3 #display this amount always - revs_top_limit = 50 #show upto this amount of changesets hidden - revs = action_params.split(',') + revs_limit = 3 # display this amount always + revs_top_limit = 50 # show upto this amount of changesets hidden + revs_ids = action_params.split(',') + deleted = user_log.repository is None + if deleted: + return ','.join(revs_ids) + repo_name = user_log.repository.repo_name - from rhodecode.model.scm import ScmModel repo = user_log.repository.scm_instance - message = lambda rev: get_changeset_safe(repo, rev).message + message = lambda rev: rev.message + lnk = lambda rev, repo_name: ( + link_to('r%s:%s' % (rev.revision, rev.short_id), + url('changeset_home', repo_name=repo_name, + revision=rev.raw_id), + title=tooltip(message(rev)), class_='tooltip') + ) + # get only max revs_top_limit of changeset for performance/ui reasons + revs = [ + x for x in repo.get_changesets(revs_ids[0], + revs_ids[:revs_top_limit][-1]) + ] + cs_links = [] - cs_links.append(" " + ', '.join ([link_to(rev, - url('changeset_home', - repo_name=repo_name, - revision=rev), title=tooltip(message(rev)), - class_='tooltip') for rev in revs[:revs_limit] ])) + cs_links.append(" " + ', '.join( + [lnk(rev, repo_name) for rev in revs[:revs_limit]] + ) + ) - compare_view = ('
' - '%s ' - '
' % (_('Show all combined changesets %s->%s' \ - % (revs[0], revs[-1])), - url('changeset_home', repo_name=repo_name, - revision='%s...%s' % (revs[0], revs[-1]) - ), - _('compare view')) - ) + compare_view = ( + '
' + '%s
' % ( + _('Show all combined changesets %s->%s') % ( + revs_ids[0], revs_ids[-1] + ), + url('changeset_home', repo_name=repo_name, + revision='%s...%s' % (revs_ids[0], revs_ids[-1]) + ), + _('compare view') + ) + ) - if len(revs) > revs_limit: - uniq_id = revs[0] - html_tmpl = (' %s ' - '%s ' - '%s') + # if we have exactly one more than normally displayed + # just display it, takes less space than displaying + # "and 1 more revisions" + if len(revs_ids) == revs_limit + 1: + rev = revs[revs_limit] + cs_links.append(", " + lnk(rev, repo_name)) + + # hidden-by-default ones + if len(revs_ids) > revs_limit + 1: + uniq_id = revs_ids[0] + html_tmpl = ( + ' %s %s %s' + ) if not feed: - cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \ - % (len(revs) - revs_limit), - _('revisions'))) + cs_links.append(html_tmpl % ( + _('and'), + uniq_id, _('%s more') % (len(revs_ids) - revs_limit), + _('revisions') + ) + ) if not feed: - html_tmpl = '' + html_tmpl = '' else: html_tmpl = ' %s ' - cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev, - url('changeset_home', - repo_name=repo_name, revision=rev), - title=message(rev), class_='tooltip') - for rev in revs[revs_limit:revs_top_limit]]))) + morelinks = ', '.join( + [lnk(rev, repo_name) for rev in revs[revs_limit:]] + ) + + if len(revs_ids) > revs_top_limit: + morelinks += ', ...' + + cs_links.append(html_tmpl % (uniq_id, morelinks)) if len(revs) > 1: cs_links.append(compare_view) return ''.join(cs_links) @@ -385,36 +510,39 @@ def action_parser(user_log, feed=False): return _('fork name ') + str(link_to(action_params, url('summary_home', repo_name=repo_name,))) - action_map = {'user_deleted_repo':(_('[deleted] repository'), None), - 'user_created_repo':(_('[created] repository'), None), - 'user_forked_repo':(_('[forked] repository'), get_fork_name), - 'user_updated_repo':(_('[updated] repository'), None), - 'admin_deleted_repo':(_('[delete] repository'), None), - 'admin_created_repo':(_('[created] repository'), None), - 'admin_forked_repo':(_('[forked] repository'), None), - 'admin_updated_repo':(_('[updated] repository'), None), - 'push':(_('[pushed] into'), get_cs_links), - 'push_local':(_('[committed via RhodeCode] into'), get_cs_links), - 'push_remote':(_('[pulled from remote] into'), get_cs_links), - 'pull':(_('[pulled] from'), None), - 'started_following_repo':(_('[started following] repository'), None), - 'stopped_following_repo':(_('[stopped following] repository'), None), + action_map = {'user_deleted_repo': (_('[deleted] repository'), None), + 'user_created_repo': (_('[created] repository'), None), + 'user_created_fork': (_('[created] repository as fork'), None), + 'user_forked_repo': (_('[forked] repository'), get_fork_name), + 'user_updated_repo': (_('[updated] repository'), None), + 'admin_deleted_repo': (_('[delete] repository'), None), + 'admin_created_repo': (_('[created] repository'), None), + 'admin_forked_repo': (_('[forked] repository'), None), + 'admin_updated_repo': (_('[updated] repository'), None), + 'push': (_('[pushed] into'), get_cs_links), + 'push_local': (_('[committed via RhodeCode] into'), get_cs_links), + 'push_remote': (_('[pulled from remote] into'), get_cs_links), + 'pull': (_('[pulled] from'), None), + 'started_following_repo': (_('[started following] repository'), None), + 'stopped_following_repo': (_('[stopped following] repository'), None), } action_str = action_map.get(action, action) if feed: action = action_str[0].replace('[', '').replace(']', '') else: - action = action_str[0].replace('[', '')\ - .replace(']', '') + action = action_str[0]\ + .replace('[', '')\ + .replace(']', '') - action_params_func = lambda :"" + action_params_func = lambda: "" if callable(action_str[1]): action_params_func = action_str[1] return [literal(action), action_params_func] + def action_parser_icon(user_log): action = user_log.action action_params = None @@ -426,6 +554,7 @@ def action_parser_icon(user_log): tmpl = """%s""" map = {'user_deleted_repo':'database_delete.png', 'user_created_repo':'database_add.png', + 'user_created_fork':'arrow_divide.png', 'user_forked_repo':'arrow_divide.png', 'user_updated_repo':'database_edit.png', 'admin_deleted_repo':'database_delete.png', @@ -449,6 +578,7 @@ def action_parser_icon(user_log): from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \ HasRepoPermissionAny, HasRepoPermissionAll + #============================================================================== # GRAVATAR URL #============================================================================== @@ -456,7 +586,8 @@ HasRepoPermissionAny, HasRepoPermissionA def gravatar_url(email_address, size=30): if (not str2bool(config['app_conf'].get('use_gravatar')) or not email_address or email_address == 'anonymous@rhodecode.org'): - return url("/images/user%s.png" % size) + f = lambda a, l: min(l, key=lambda x: abs(x - a)) + return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30])) ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme') default = 'identicon' @@ -469,7 +600,7 @@ def gravatar_url(email_address, size=30) email_address = safe_str(email_address) # construct the url gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?" - gravatar_url += urllib.urlencode({'d':default, 's':str(size)}) + gravatar_url += urllib.urlencode({'d': default, 's': str(size)}) return gravatar_url @@ -480,7 +611,7 @@ def gravatar_url(email_address, size=30) class RepoPage(Page): def __init__(self, collection, page=1, items_per_page=20, - item_count=None, url=None, branch_name=None, **kwargs): + item_count=None, url=None, **kwargs): """Create a "RepoPage" instance. special pager for paging repository @@ -498,7 +629,7 @@ class RepoPage(Page): # The self.page is the number of the current page. # The first page has the number 1! try: - self.page = int(page) # make it int() if we get it as a string + self.page = int(page) # make it int() if we get it as a string except (ValueError, TypeError): self.page = 1 @@ -518,7 +649,8 @@ class RepoPage(Page): self.items_per_page)) self.last_page = self.first_page + self.page_count - 1 - # Make sure that the requested page number is the range of valid pages + # Make sure that the requested page number is the range of + # valid pages if self.page > self.last_page: self.page = self.last_page elif self.page < self.first_page: @@ -531,11 +663,7 @@ class RepoPage(Page): self.last_item = ((self.item_count - 1) - items_per_page * (self.page - 1)) - iterator = self.collection.get_changesets(start=self.first_item, - end=self.last_item, - reverse=True, - branch_name=branch_name) - self.items = list(iterator) + self.items = list(self.collection[self.first_item:self.last_item + 1]) # Links to previous and next page if self.page > self.first_page: @@ -560,14 +688,14 @@ class RepoPage(Page): self.items = [] # This is a subclass of the 'list' type. Initialise the list now. - list.__init__(self, self.items) + list.__init__(self, reversed(self.items)) def changed_tooltip(nodes): """ Generates a html string for changed nodes in changeset page. It limits the output to 30 entries - + :param nodes: LazyNodesGenerator """ if nodes: @@ -581,15 +709,14 @@ def changed_tooltip(nodes): return ': ' + _('No Files') - def repo_link(groups_and_repos): """ Makes a breadcrumbs link to repo within a group joins » on each group to create a fancy link - + ex:: group >> subgroup >> repo - + :param groups_and_repos: """ groups, repo_name = groups_and_repos @@ -603,11 +730,12 @@ def repo_link(groups_and_repos): return literal(' » '.join(map(make_link, groups)) + \ " » " + repo_name) + def fancy_file_stats(stats): """ Displays a fancy two colored bar for number of added/deleted lines of code on file - + :param stats: two element list of added/deleted lines of code """ @@ -630,13 +758,12 @@ def fancy_file_stats(stats): a_v = a if a > 0 else '' d_v = d if d > 0 else '' - def cgen(l_type): - mapping = {'tr':'top-right-rounded-corner', - 'tl':'top-left-rounded-corner', - 'br':'bottom-right-rounded-corner', - 'bl':'bottom-left-rounded-corner'} - map_getter = lambda x:mapping[x] + mapping = {'tr': 'top-right-rounded-corner', + 'tl': 'top-left-rounded-corner', + 'br': 'bottom-right-rounded-corner', + 'bl': 'bottom-left-rounded-corner'} + map_getter = lambda x: mapping[x] if l_type == 'a' and d_v: #case when added and deleted are present @@ -651,22 +778,137 @@ def fancy_file_stats(stats): if l_type == 'd' and not a_v: return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl'])) - - - d_a = '
%s
' % (cgen('a'), - a_p, a_v) - d_d = '
%s
' % (cgen('d'), - d_p, d_v) + d_a = '
%s
' % ( + cgen('a'), a_p, a_v + ) + d_d = '
%s
' % ( + cgen('d'), d_p, d_v + ) return literal('
%s%s
' % (width, d_a, d_d)) -def urlify_text(text): +def urlify_text(text_): import re - url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)') + url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]''' + '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''') def url_func(match_obj): url_full = match_obj.groups()[0] - return '%(url)s' % ({'url':url_full}) + return '%(url)s' % ({'url': url_full}) + + return literal(url_pat.sub(url_func, text_)) + + +def urlify_changesets(text_, repository): + import re + URL_PAT = re.compile(r'([0-9a-fA-F]{12,})') + + def url_func(match_obj): + rev = match_obj.groups()[0] + pref = '' + if match_obj.group().startswith(' '): + pref = ' ' + tmpl = ( + '%(pref)s' + '%(rev)s' + '' + ) + return tmpl % { + 'pref': pref, + 'cls': 'revision-link', + 'url': url('changeset_home', repo_name=repository, revision=rev), + 'rev': rev, + } + + newtext = URL_PAT.sub(url_func, text_) + + return newtext + + +def urlify_commit(text_, repository=None, link_=None): + """ + Parses given text message and makes proper links. + issues are linked to given issue-server, and rest is a changeset link + if link_ is given, in other case it's a plain text + + :param text_: + :param repository: + :param link_: changeset link + """ + import re + import traceback + + # urlify changesets + text_ = urlify_changesets(text_, repository) + + def linkify_others(t, l): + urls = re.compile(r'(\)',) + links = [] + for e in urls.split(t): + if not urls.match(e): + links.append('%s' % (l, e)) + else: + links.append(e) - return literal(url_pat.sub(url_func, text)) + return ''.join(links) + try: + conf = config['app_conf'] + + URL_PAT = re.compile(r'%s' % conf.get('issue_pat')) + + if URL_PAT: + ISSUE_SERVER_LNK = conf.get('issue_server_link') + ISSUE_PREFIX = conf.get('issue_prefix') + + def url_func(match_obj): + pref = '' + if match_obj.group().startswith(' '): + pref = ' ' + + issue_id = ''.join(match_obj.groups()) + tmpl = ( + '%(pref)s' + '%(issue-prefix)s%(id-repr)s' + '' + ) + url = ISSUE_SERVER_LNK.replace('{id}', issue_id) + if repository: + url = url.replace('{repo}', repository) + + return tmpl % { + 'pref': pref, + 'cls': 'issue-tracker-link', + 'url': url, + 'id-repr': issue_id, + 'issue-prefix': ISSUE_PREFIX, + 'serv': ISSUE_SERVER_LNK, + } + + newtext = URL_PAT.sub(url_func, text_) + + if link_: + # wrap not links into final link => link_ + newtext = linkify_others(newtext, link_) + + return literal(newtext) + except: + log.error(traceback.format_exc()) + pass + + return text_ + + +def rst(source): + return literal('
%s
' % + MarkupRenderer.rst(source)) + + +def rst_w_mentions(source): + """ + Wrapped rst renderer with @mention highlighting + + :param source: + """ + return literal('
%s
' % + MarkupRenderer.rst_with_mentions(source)) diff --git a/rhodecode/lib/hooks.py b/rhodecode/lib/hooks.py --- a/rhodecode/lib/hooks.py +++ b/rhodecode/lib/hooks.py @@ -7,7 +7,7 @@ :created_on: Aug 6, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -33,15 +33,14 @@ from rhodecode.lib.utils import action_l def repo_size(ui, repo, hooktype=None, **kwargs): - """Presents size of repository after push + """ + Presents size of repository after push :param ui: :param repo: :param hooktype: """ - if hooktype != 'changegroup': - return False size_hg, size_root = 0, 0 for path, dirs, files in os.walk(repo.root): if path.find('.hg') != -1: @@ -60,12 +59,20 @@ def repo_size(ui, repo, hooktype=None, * size_hg_f = h.format_byte_size(size_hg) size_root_f = h.format_byte_size(size_root) 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)) + + last_cs = repo[len(repo) - 1] + + msg = ('Repository size .hg:%s repo:%s total:%s\n' + 'Last revision is now r%s:%s\n') % ( + size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12] + ) + + sys.stdout.write(msg) def log_pull_action(ui, repo, **kwargs): - """Logs user last pull action + """ + Logs user last pull action :param ui: :param repo: @@ -76,13 +83,15 @@ def log_pull_action(ui, repo, **kwargs): repository = extra_params['repository'] action = 'pull' - action_logger(username, action, repository, extra_params['ip']) + action_logger(username, action, repository, extra_params['ip'], + commit=True) return 0 def log_push_action(ui, repo, **kwargs): - """Maps user last push action to new changeset id, from mercurial + """ + Maps user last push action to new changeset id, from mercurial :param ui: :param repo: @@ -110,6 +119,37 @@ def log_push_action(ui, repo, **kwargs): action = action % ','.join(revs) - action_logger(username, action, repository, extra_params['ip']) + action_logger(username, action, repository, extra_params['ip'], + commit=True) return 0 + + +def log_create_repository(repository_dict, created_by, **kwargs): + """ + Post create repository Hook. This is a dummy function for admins to re-use + if needed + + :param repository: dict dump of repository object + :param created_by: username who created repository + :param created_date: date of creation + + available keys of repository_dict: + + 'repo_type', + 'description', + 'private', + 'created_on', + 'enable_downloads', + 'repo_id', + 'user_id', + 'enable_statistics', + 'clone_uri', + 'fork_id', + 'group_id', + 'repo_name' + + """ + + + return 0 diff --git a/rhodecode/lib/indexers/__init__.py b/rhodecode/lib/indexers/__init__.py --- a/rhodecode/lib/indexers/__init__.py +++ b/rhodecode/lib/indexers/__init__.py @@ -7,7 +7,7 @@ :created_on: Aug 17, 2010 :author: marcink - :copyright: (C) 2009-2010 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -37,38 +37,39 @@ from whoosh.analysis import RegexTokeniz 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 +from whoosh.highlight import highlight, HtmlFormatter, ContextFragmenter from webhelpers.html.builder import escape from sqlalchemy import engine_from_config -from vcs.utils.lazy import LazyProperty from rhodecode.model import init_model from rhodecode.model.scm import ScmModel from rhodecode.model.repo import RepoModel from rhodecode.config.environment import load_environment -from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP +from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, LazyProperty from rhodecode.lib.utils import BasePasterCommand, Command, add_cache -#EXTENSIONS WE WANT TO INDEX CONTENT OFF +# EXTENSIONS WE WANT TO INDEX CONTENT OFF INDEX_EXTENSIONS = LANGUAGES_EXTENSIONS_MAP.keys() -#CUSTOM ANALYZER wordsplit + lowercase filter +# CUSTOM ANALYZER wordsplit + lowercase filter ANALYZER = RegexTokenizer(expression=r"\w+") | LowercaseFilter() #INDEX SCHEMA DEFINITION -SCHEMA = Schema(owner=TEXT(), - repository=TEXT(stored=True), - path=TEXT(stored=True), - content=FieldType(format=Characters(ANALYZER), - scorable=True, stored=True), - modtime=STORED(), extension=TEXT(stored=True)) - +SCHEMA = Schema( + owner=TEXT(), + repository=TEXT(stored=True), + path=TEXT(stored=True), + content=FieldType(format=Characters(), analyzer=ANALYZER, + scorable=True, stored=True), + modtime=STORED(), + extension=TEXT(stored=True) +) IDX_NAME = 'HG_INDEX' FORMATTER = HtmlFormatter('span', between='\n...\n') -FRAGMENTER = SimpleFragmenter(200) +FRAGMENTER = ContextFragmenter(200) class MakeIndex(BasePasterCommand): @@ -129,13 +130,14 @@ class MakeIndex(BasePasterCommand): " 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 self.searcher = searcher self.matcher = matcher self.highlight_items = highlight_items - self.fragment_size = 200 / 2 + self.fragment_size = 200 @LazyProperty def doc_ids(self): @@ -171,11 +173,10 @@ class ResultWrapper(object): """ i, j = key.start, key.stop - slice = [] + slices = [] for docid in self.doc_ids[i:j]: - slice.append(self.get_full_content(docid)) - return slice - + slices.append(self.get_full_content(docid)) + return slices def get_full_content(self, docid): res = self.searcher.stored_fields(docid[0]) @@ -183,9 +184,9 @@ class ResultWrapper(object): + 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}) + res.update({'content_short': content_short, + 'content_short_hl': self.highlight(content_short), + 'f_path': f_path}) return res @@ -198,7 +199,7 @@ class ResultWrapper(object): 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: """ @@ -217,10 +218,12 @@ class ResultWrapper(object): def highlight(self, content, top=5): if self.search_type != 'content': return '' - hl = highlight(escape(content), - self.highlight_items, - analyzer=ANALYZER, - fragmenter=FRAGMENTER, - formatter=FORMATTER, - top=top) + hl = highlight( + text=escape(content), + terms=self.highlight_items, + analyzer=ANALYZER, + fragmenter=FRAGMENTER, + formatter=FORMATTER, + top=top + ) return hl diff --git a/rhodecode/lib/indexers/daemon.py b/rhodecode/lib/indexers/daemon.py --- a/rhodecode/lib/indexers/daemon.py +++ b/rhodecode/lib/indexers/daemon.py @@ -7,7 +7,7 @@ :created_on: Jan 26, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -43,12 +43,12 @@ from rhodecode.model.scm import ScmModel from rhodecode.lib import safe_unicode from rhodecode.lib.indexers import INDEX_EXTENSIONS, SCHEMA, IDX_NAME -from vcs.exceptions import ChangesetError, RepositoryError +from rhodecode.lib.vcs.exceptions import ChangesetError, RepositoryError, \ + NodeDoesNotExistError from whoosh.index import create_in, open_dir - log = logging.getLogger('whooshIndexer') # create logger log.setLevel(logging.DEBUG) @@ -67,12 +67,13 @@ ch.setFormatter(formatter) # add ch to logger log.addHandler(ch) + class WhooshIndexingDaemon(object): """ Daemon for atomic jobs """ - def __init__(self, indexname='HG_INDEX', index_location=None, + def __init__(self, indexname=IDX_NAME, index_location=None, repo_location=None, sa=None, repo_list=None): self.indexname = indexname @@ -94,7 +95,6 @@ class WhooshIndexingDaemon(object): self.repo_paths = filtered_repo_paths - self.initial = False if not os.path.isdir(self.index_location): os.makedirs(self.index_location) @@ -154,7 +154,6 @@ class WhooshIndexingDaemon(object): modtime=self.get_node_mtime(node), extension=node.extension) - def build_index(self): if os.path.exists(self.index_location): log.debug('removing previous index') @@ -176,7 +175,6 @@ class WhooshIndexingDaemon(object): writer.commit(merge=True) log.debug('>>> FINISHED BUILDING INDEX <<<') - def update_index(self): log.debug('STARTING INCREMENTAL INDEXING UPDATE') @@ -198,7 +196,7 @@ class WhooshIndexingDaemon(object): try: node = self.get_node(repo, indexed_path) - except ChangesetError: + except (ChangesetError, NodeDoesNotExistError): # This file was deleted since it was indexed log.debug('removing from index %s' % indexed_path) writer.delete_by_term('path', indexed_path) diff --git a/rhodecode/lib/markup_renderer.py b/rhodecode/lib/markup_renderer.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/markup_renderer.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.lib.markup_renderer + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + Renderer for markup languages with ability to parse using rst or markdown + + :created_on: Oct 27, 2011 + :author: marcink + :copyright: (C) 2011-2012 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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, see . + +import re +import logging + +from rhodecode.lib import safe_unicode + +log = logging.getLogger(__name__) + + +class MarkupRenderer(object): + RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw'] + + MARKDOWN_PAT = re.compile(r'md|mkdn?|mdown|markdown', re.IGNORECASE) + RST_PAT = re.compile(r're?st', re.IGNORECASE) + PLAIN_PAT = re.compile(r'readme', re.IGNORECASE) + + def __detect_renderer(self, source, filename=None): + """ + runs detection of what renderer should be used for generating html + from a markup language + + filename can be also explicitly a renderer name + + :param source: + :param filename: + """ + + if MarkupRenderer.MARKDOWN_PAT.findall(filename): + detected_renderer = 'markdown' + elif MarkupRenderer.RST_PAT.findall(filename): + detected_renderer = 'rst' + elif MarkupRenderer.PLAIN_PAT.findall(filename): + detected_renderer = 'rst' + else: + detected_renderer = 'plain' + + return getattr(MarkupRenderer, detected_renderer) + + def render(self, source, filename=None): + """ + Renders a given filename using detected renderer + it detects renderers based on file extension or mimetype. + At last it will just do a simple html replacing new lines with
+ + :param file_name: + :param source: + """ + + renderer = self.__detect_renderer(source, filename) + readme_data = renderer(source) + return readme_data + + @classmethod + def plain(cls, source): + source = safe_unicode(source) + + def urlify_text(text): + url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]' + '|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)') + + def url_func(match_obj): + url_full = match_obj.groups()[0] + return '%(url)s' % ({'url': url_full}) + + return url_pat.sub(url_func, text) + + source = urlify_text(source) + return '
' + source.replace("\n", '
') + + @classmethod + def markdown(cls, source): + source = safe_unicode(source) + try: + import markdown as __markdown + return __markdown.markdown(source, ['codehilite']) + except ImportError: + log.warning('Install markdown to use this function') + return cls.plain(source) + + @classmethod + def rst(cls, source): + source = safe_unicode(source) + try: + from docutils.core import publish_parts + from docutils.parsers.rst import directives + docutils_settings = dict([(alias, None) for alias in + cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES]) + + docutils_settings.update({'input_encoding': 'unicode', + 'report_level': 4}) + + for k, v in docutils_settings.iteritems(): + directives.register_directive(k, v) + + parts = publish_parts(source=source, + writer_name="html4css1", + settings_overrides=docutils_settings) + + return parts['html_title'] + parts["fragment"] + except ImportError: + log.warning('Install docutils to use this function') + return cls.plain(source) + + @classmethod + def rst_with_mentions(cls, source): + mention_pat = re.compile(r'(?:^@|\s@)(\w+)') + + def wrapp(match_obj): + uname = match_obj.groups()[0] + return ' **@%(uname)s** ' % {'uname':uname} + mention_hl = mention_pat.sub(wrapp, source).strip() + return cls.rst(mention_hl) diff --git a/rhodecode/lib/middleware/https_fixup.py b/rhodecode/lib/middleware/https_fixup.py --- a/rhodecode/lib/middleware/https_fixup.py +++ b/rhodecode/lib/middleware/https_fixup.py @@ -7,7 +7,7 @@ :created_on: May 23, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify diff --git a/rhodecode/lib/middleware/simplegit.py b/rhodecode/lib/middleware/simplegit.py --- a/rhodecode/lib/middleware/simplegit.py +++ b/rhodecode/lib/middleware/simplegit.py @@ -8,7 +8,7 @@ :created_on: Apr 28, 2010 :author: marcink - :copyright: (C) 2009-2010 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -27,7 +27,6 @@ import os import logging import traceback -import time from dulwich import server as dulserver @@ -67,13 +66,12 @@ dulserver.DEFAULT_HANDLERS = { from dulwich.repo import Repo from dulwich.web import HTTPGitApplication -from paste.auth.basic import AuthBasicAuthenticator from paste.httpheaders import REMOTE_USER, AUTH_TYPE from rhodecode.lib import safe_str -from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware -from rhodecode.lib.utils import invalidate_cache, is_valid_repo -from rhodecode.model import meta +from rhodecode.lib.base import BaseVCSController +from rhodecode.lib.auth import get_container_username +from rhodecode.lib.utils import is_valid_repo from rhodecode.model.db import User from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError @@ -93,24 +91,7 @@ def is_git(environ): return False -class SimpleGit(object): - - def __init__(self, application, config): - self.application = application - self.config = config - # base path of repo locations - self.basepath = self.config['base_path'] - #authenticate this mercurial request using authfunc - self.authenticate = AuthBasicAuthenticator('', authfunc) - - def __call__(self, environ, start_response): - start = time.time() - try: - return self._handle_request(environ, start_response) - finally: - log = logging.getLogger(self.__class__.__name__) - log.debug('Request time: %.3fs' % (time.time() - start)) - meta.Session.remove() +class SimpleGit(BaseVCSController): def _handle_request(self, environ, start_response): if not is_git(environ): @@ -143,9 +124,8 @@ class SimpleGit(object): if action in ['pull', 'push']: anonymous_user = self.__get_user('default') username = anonymous_user.username - anonymous_perm = self.__check_permission(action, - anonymous_user, - repo_name) + anonymous_perm = self._check_permission(action, anonymous_user, + repo_name) if anonymous_perm is not True or anonymous_user.active is False: if anonymous_perm is not True: @@ -159,27 +139,29 @@ class SimpleGit(object): # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS #============================================================== - if not REMOTE_USER(environ): + # Attempting to retrieve username from the container + username = get_container_username(environ, self.config) + + # If not authenticated by the container, running basic auth + if not username: self.authenticate.realm = \ safe_str(self.config['rhodecode_realm']) result = self.authenticate(environ) if isinstance(result, str): AUTH_TYPE.update(environ, 'basic') REMOTE_USER.update(environ, result) + username = result else: return result.wsgi_application(environ, start_response) #============================================================== - # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM - # BASIC AUTH + # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME #============================================================== - if action in ['pull', 'push']: - username = REMOTE_USER(environ) try: user = self.__get_user(username) - if user is None: - return HTTPForbidden()(environ, start_response) + if user is None or not user.active: + return HTTPForbidden()(environ, start_response) username = user.username except: log.error(traceback.format_exc()) @@ -187,16 +169,11 @@ class SimpleGit(object): start_response) #check permissions for this repository - perm = self.__check_permission(action, user, + perm = self._check_permission(action, user, repo_name) if perm is not True: return HTTPForbidden()(environ, start_response) - extras = {'ip': ipaddr, - 'username': username, - 'action': action, - 'repository': repo_name} - #=================================================================== # GIT REQUEST HANDLING #=================================================================== @@ -211,8 +188,8 @@ class SimpleGit(object): try: #invalidate cache on push if action == 'push': - self.__invalidate_cache(repo_name) - + self._invalidate_cache(repo_name) + log.info('%s action on GIT repo "%s"' % (action, repo_name)) app = self.__make_app(repo_name, repo_path) return app(environ, start_response) except Exception: @@ -222,7 +199,7 @@ class SimpleGit(object): def __make_app(self, repo_name, repo_path): """ Make an wsgi application using dulserver - + :param repo_name: name of the repository :param repo_path: full path to the repository """ @@ -233,31 +210,6 @@ class SimpleGit(object): return gitserve - def __check_permission(self, action, user, repo_name): - """ - Checks permissions using action (push/pull) user and repository - name - - :param action: push or pull action - :param user: user instance - :param repo_name: repository name - """ - if action == 'push': - if not HasPermissionAnyMiddleware('repository.write', - 'repository.admin')(user, - repo_name): - return False - - else: - #any other action need at least read permission - if not HasPermissionAnyMiddleware('repository.read', - 'repository.write', - 'repository.admin')(user, - repo_name): - return False - - return True - def __get_repository(self, environ): """ Get's repository name out of PATH_INFO header @@ -265,6 +217,7 @@ class SimpleGit(object): :param environ: environ where PATH_INFO is stored """ try: + environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO']) repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:]) if repo_name.endswith('/'): repo_name = repo_name.rstrip('/') @@ -293,10 +246,3 @@ class SimpleGit(object): service_cmd if service_cmd else 'other') else: return 'other' - - 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('get_repo_cached_%s' % repo_name) - diff --git a/rhodecode/lib/middleware/simplehg.py b/rhodecode/lib/middleware/simplehg.py --- a/rhodecode/lib/middleware/simplehg.py +++ b/rhodecode/lib/middleware/simplehg.py @@ -8,7 +8,7 @@ :created_on: Apr 28, 2010 :author: marcink - :copyright: (C) 2009-2010 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -27,19 +27,16 @@ import os import logging import traceback -import time from mercurial.error import RepoError from mercurial.hgweb import hgweb_mod -from paste.auth.basic import AuthBasicAuthenticator from paste.httpheaders import REMOTE_USER, AUTH_TYPE from rhodecode.lib import safe_str -from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware -from rhodecode.lib.utils import make_ui, invalidate_cache, \ - is_valid_repo, ui_sections -from rhodecode.model import meta +from rhodecode.lib.base import BaseVCSController +from rhodecode.lib.auth import get_container_username +from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections from rhodecode.model.db import User from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError @@ -57,25 +54,7 @@ def is_mercurial(environ): return False -class SimpleHg(object): - - def __init__(self, application, config): - self.application = application - self.config = config - # base path of repo locations - self.basepath = self.config['base_path'] - #authenticate this mercurial request using authfunc - self.authenticate = AuthBasicAuthenticator('', authfunc) - self.ipaddr = '0.0.0.0' - - def __call__(self, environ, start_response): - start = time.time() - try: - return self._handle_request(environ, start_response) - finally: - log = logging.getLogger(self.__class__.__name__) - log.debug('Request time: %.3fs' % (time.time() - start)) - meta.Session.remove() +class SimpleHg(BaseVCSController): def _handle_request(self, environ, start_response): if not is_mercurial(environ): @@ -101,7 +80,6 @@ class SimpleHg(object): # GET ACTION PULL or PUSH #====================================================================== action = self.__get_action(environ) - #====================================================================== # CHECK ANONYMOUS PERMISSION #====================================================================== @@ -109,9 +87,8 @@ class SimpleHg(object): anonymous_user = self.__get_user('default') username = anonymous_user.username - anonymous_perm = self.__check_permission(action, - anonymous_user, - repo_name) + anonymous_perm = self._check_permission(action, anonymous_user, + repo_name) if anonymous_perm is not True or anonymous_user.active is False: if anonymous_perm is not True: @@ -125,26 +102,28 @@ class SimpleHg(object): # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS #============================================================== - if not REMOTE_USER(environ): + # Attempting to retrieve username from the container + username = get_container_username(environ, self.config) + + # If not authenticated by the container, running basic auth + if not username: self.authenticate.realm = \ safe_str(self.config['rhodecode_realm']) result = self.authenticate(environ) if isinstance(result, str): AUTH_TYPE.update(environ, 'basic') REMOTE_USER.update(environ, result) + username = result else: return result.wsgi_application(environ, start_response) #============================================================== - # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM - # BASIC AUTH + # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME #============================================================== - if action in ['pull', 'push']: - username = REMOTE_USER(environ) try: user = self.__get_user(username) - if user is None: + if user is None or not user.active: return HTTPForbidden()(environ, start_response) username = user.username except: @@ -153,7 +132,7 @@ class SimpleHg(object): start_response) #check permissions for this repository - perm = self.__check_permission(action, user, + perm = self._check_permission(action, user, repo_name) if perm is not True: return HTTPForbidden()(environ, start_response) @@ -173,16 +152,15 @@ class SimpleHg(object): baseui = make_ui('db') self.__inject_extras(repo_path, baseui, extras) - # quick check if that dir exists... if is_valid_repo(repo_name, self.basepath) is False: return HTTPNotFound()(environ, start_response) try: - #invalidate cache on push + # invalidate cache on push if action == 'push': - self.__invalidate_cache(repo_name) - + self._invalidate_cache(repo_name) + log.info('%s action on HG repo "%s"' % (action, repo_name)) app = self.__make_app(repo_path, baseui, extras) return app(environ, start_response) except RepoError, e: @@ -199,32 +177,6 @@ class SimpleHg(object): """ return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui) - - def __check_permission(self, action, user, repo_name): - """ - Checks permissions using action (push/pull) user and repository - name - - :param action: push or pull action - :param user: user instance - :param repo_name: repository name - """ - if action == 'push': - if not HasPermissionAnyMiddleware('repository.write', - 'repository.admin')(user, - repo_name): - return False - - else: - #any other action need at least read permission - if not HasPermissionAnyMiddleware('repository.read', - 'repository.write', - 'repository.admin')(user, - repo_name): - return False - - return True - def __get_repository(self, environ): """ Get's repository name out of PATH_INFO header @@ -232,6 +184,7 @@ class SimpleHg(object): :param environ: environ where PATH_INFO is stored """ try: + environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO']) repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:]) if repo_name.endswith('/'): repo_name = repo_name.rstrip('/') @@ -265,18 +218,12 @@ class SimpleHg(object): else: return 'pull' - 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('get_repo_cached_%s' % repo_name) - def __inject_extras(self, repo_path, baseui, extras={}): """ Injects some extra params into baseui instance - + also overwrites global settings with those takes from local hgrc file - + :param baseui: baseui instance :param extras: dict with extra params to put into baseui """ @@ -298,4 +245,3 @@ class SimpleHg(object): for section in ui_sections: for k, v in repoui.configitems(section): baseui.setconfig(section, k, v) - diff --git a/rhodecode/lib/rcmail/__init__.py b/rhodecode/lib/rcmail/__init__.py new file mode 100644 diff --git a/rhodecode/lib/rcmail/exceptions.py b/rhodecode/lib/rcmail/exceptions.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/rcmail/exceptions.py @@ -0,0 +1,13 @@ + + +class InvalidMessage(RuntimeError): + """ + Raised if message is missing vital headers, such + as recipients or sender address. + """ + + +class BadHeaders(RuntimeError): + """ + Raised if message contains newlines in headers. + """ diff --git a/rhodecode/lib/rcmail/message.py b/rhodecode/lib/rcmail/message.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/rcmail/message.py @@ -0,0 +1,182 @@ +from rhodecode.lib.rcmail.response import MailResponse + +from rhodecode.lib.rcmail.exceptions import BadHeaders +from rhodecode.lib.rcmail.exceptions import InvalidMessage + +class Attachment(object): + """ + Encapsulates file attachment information. + + :param filename: filename of attachment + :param content_type: file mimetype + :param data: the raw file data, either as string or file obj + :param disposition: content-disposition (if any) + """ + + def __init__(self, + filename=None, + content_type=None, + data=None, + disposition=None): + + self.filename = filename + self.content_type = content_type + self.disposition = disposition or 'attachment' + self._data = data + + @property + def data(self): + if isinstance(self._data, basestring): + return self._data + self._data = self._data.read() + return self._data + + +class Message(object): + """ + Encapsulates an email message. + + :param subject: email subject header + :param recipients: list of email addresses + :param body: plain text message + :param html: HTML message + :param sender: email sender address + :param cc: CC list + :param bcc: BCC list + :param extra_headers: dict of extra email headers + :param attachments: list of Attachment instances + :param recipients_separator: alternative separator for any of + 'From', 'To', 'Delivered-To', 'Cc', 'Bcc' fields + """ + + def __init__(self, + subject=None, + recipients=None, + body=None, + html=None, + sender=None, + cc=None, + bcc=None, + extra_headers=None, + attachments=None, + recipients_separator="; "): + + self.subject = subject or '' + self.sender = sender + self.body = body + self.html = html + + self.recipients = recipients or [] + self.attachments = attachments or [] + self.cc = cc or [] + self.bcc = bcc or [] + self.extra_headers = extra_headers or {} + + self.recipients_separator = recipients_separator + + @property + def send_to(self): + return set(self.recipients) | set(self.bcc or ()) | set(self.cc or ()) + + def to_message(self): + """ + Returns raw email.Message instance.Validates message first. + """ + + self.validate() + + return self.get_response().to_message() + + def get_response(self): + """ + Creates a Lamson MailResponse instance + """ + + response = MailResponse(Subject=self.subject, + To=self.recipients, + From=self.sender, + Body=self.body, + Html=self.html, + separator=self.recipients_separator) + + if self.cc: + response.base['Cc'] = self.cc + + for attachment in self.attachments: + + response.attach(attachment.filename, + attachment.content_type, + attachment.data, + attachment.disposition) + + response.update(self.extra_headers) + + return response + + def is_bad_headers(self): + """ + Checks for bad headers i.e. newlines in subject, sender or recipients. + """ + + headers = [self.subject, self.sender] + headers += list(self.send_to) + headers += self.extra_headers.values() + + for val in headers: + for c in '\r\n': + if c in val: + return True + return False + + def validate(self): + """ + Checks if message is valid and raises appropriate exception. + """ + + if not self.recipients: + raise InvalidMessage, "No recipients have been added" + + if not self.body and not self.html: + raise InvalidMessage, "No body has been set" + + if not self.sender: + raise InvalidMessage, "No sender address has been set" + + if self.is_bad_headers(): + raise BadHeaders + + def add_recipient(self, recipient): + """ + Adds another recipient to the message. + + :param recipient: email address of recipient. + """ + + self.recipients.append(recipient) + + def add_cc(self, recipient): + """ + Adds an email address to the CC list. + + :param recipient: email address of recipient. + """ + + self.cc.append(recipient) + + def add_bcc(self, recipient): + """ + Adds an email address to the BCC list. + + :param recipient: email address of recipient. + """ + + self.bcc.append(recipient) + + def attach(self, attachment): + """ + Adds an attachment to the message. + + :param attachment: an **Attachment** instance. + """ + + self.attachments.append(attachment) diff --git a/rhodecode/lib/rcmail/response.py b/rhodecode/lib/rcmail/response.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/rcmail/response.py @@ -0,0 +1,449 @@ +# The code in this module is entirely lifted from the Lamson project +# (http://lamsonproject.org/). Its copyright is: + +# Copyright (c) 2008, Zed A. Shaw +# All rights reserved. + +# It is provided under this license: + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: + +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. + +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# * Neither the name of the Zed A. Shaw nor the names of its contributors may +# be used to endorse or promote products derived from this software without +# specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import os +import mimetypes +import string +from email import encoders +from email.charset import Charset +from email.utils import parseaddr +from email.mime.base import MIMEBase + +ADDRESS_HEADERS_WHITELIST = ['From', 'To', 'Delivered-To', 'Cc'] +DEFAULT_ENCODING = "utf-8" +VALUE_IS_EMAIL_ADDRESS = lambda v: '@' in v + + +def normalize_header(header): + return string.capwords(header.lower(), '-') + + +class EncodingError(Exception): + """Thrown when there is an encoding error.""" + pass + + +class MailBase(object): + """MailBase is used as the basis of lamson.mail and contains the basics of + encoding an email. You actually can do all your email processing with this + class, but it's more raw. + """ + def __init__(self, items=()): + self.headers = dict(items) + self.parts = [] + self.body = None + self.content_encoding = {'Content-Type': (None, {}), + 'Content-Disposition': (None, {}), + 'Content-Transfer-Encoding': (None, {})} + + def __getitem__(self, key): + return self.headers.get(normalize_header(key), None) + + def __len__(self): + return len(self.headers) + + def __iter__(self): + return iter(self.headers) + + def __contains__(self, key): + return normalize_header(key) in self.headers + + def __setitem__(self, key, value): + self.headers[normalize_header(key)] = value + + def __delitem__(self, key): + del self.headers[normalize_header(key)] + + def __nonzero__(self): + return self.body != None or len(self.headers) > 0 or len(self.parts) > 0 + + def keys(self): + """Returns the sorted keys.""" + return sorted(self.headers.keys()) + + def attach_file(self, filename, data, ctype, disposition): + """ + A file attachment is a raw attachment with a disposition that + indicates the file name. + """ + assert filename, "You can't attach a file without a filename." + ctype = ctype.lower() + + part = MailBase() + part.body = data + part.content_encoding['Content-Type'] = (ctype, {'name': filename}) + part.content_encoding['Content-Disposition'] = (disposition, + {'filename': filename}) + self.parts.append(part) + + def attach_text(self, data, ctype): + """ + This attaches a simpler text encoded part, which doesn't have a + filename. + """ + ctype = ctype.lower() + + part = MailBase() + part.body = data + part.content_encoding['Content-Type'] = (ctype, {}) + self.parts.append(part) + + def walk(self): + for p in self.parts: + yield p + for x in p.walk(): + yield x + + +class MailResponse(object): + """ + You are given MailResponse objects from the lamson.view methods, and + whenever you want to generate an email to send to someone. It has the + same basic functionality as MailRequest, but it is designed to be written + to, rather than read from (although you can do both). + + You can easily set a Body or Html during creation or after by passing it + as __init__ parameters, or by setting those attributes. + + You can initially set the From, To, and Subject, but they are headers so + use the dict notation to change them: msg['From'] = 'joe@test.com'. + + The message is not fully crafted until right when you convert it with + MailResponse.to_message. This lets you change it and work with it, then + send it out when it's ready. + """ + def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None, + separator="; "): + self.Body = Body + self.Html = Html + self.base = MailBase([('To', To), ('From', From), ('Subject', Subject)]) + self.multipart = self.Body and self.Html + self.attachments = [] + self.separator = separator + + def __contains__(self, key): + return self.base.__contains__(key) + + def __getitem__(self, key): + return self.base.__getitem__(key) + + def __setitem__(self, key, val): + return self.base.__setitem__(key, val) + + def __delitem__(self, name): + del self.base[name] + + def attach(self, filename=None, content_type=None, data=None, + disposition=None): + """ + + Simplifies attaching files from disk or data as files. To attach + simple text simple give data and a content_type. To attach a file, + give the data/content_type/filename/disposition combination. + + For convenience, if you don't give data and only a filename, then it + will read that file's contents when you call to_message() later. If + you give data and filename then it will assume you've filled data + with what the file's contents are and filename is just the name to + use. + """ + + assert filename or data, ("You must give a filename or some data to " + "attach.") + assert data or os.path.exists(filename), ("File doesn't exist, and no " + "data given.") + + self.multipart = True + + if filename and not content_type: + content_type, encoding = mimetypes.guess_type(filename) + + assert content_type, ("No content type given, and couldn't guess " + "from the filename: %r" % filename) + + self.attachments.append({'filename': filename, + 'content_type': content_type, + 'data': data, + 'disposition': disposition,}) + + def attach_part(self, part): + """ + Attaches a raw MailBase part from a MailRequest (or anywhere) + so that you can copy it over. + """ + self.multipart = True + + self.attachments.append({'filename': None, + 'content_type': None, + 'data': None, + 'disposition': None, + 'part': part, + }) + + def attach_all_parts(self, mail_request): + """ + Used for copying the attachment parts of a mail.MailRequest + object for mailing lists that need to maintain attachments. + """ + for part in mail_request.all_parts(): + self.attach_part(part) + + self.base.content_encoding = mail_request.base.content_encoding.copy() + + def clear(self): + """ + Clears out the attachments so you can redo them. Use this to keep the + headers for a series of different messages with different attachments. + """ + del self.attachments[:] + del self.base.parts[:] + self.multipart = False + + def update(self, message): + """ + Used to easily set a bunch of heading from another dict + like object. + """ + for k in message.keys(): + self.base[k] = message[k] + + def __str__(self): + """ + Converts to a string. + """ + return self.to_message().as_string() + + def _encode_attachment(self, filename=None, content_type=None, data=None, + disposition=None, part=None): + """ + Used internally to take the attachments mentioned in self.attachments + and do the actual encoding in a lazy way when you call to_message. + """ + if part: + self.base.parts.append(part) + elif filename: + if not data: + data = open(filename).read() + + self.base.attach_file(filename, data, content_type, + disposition or 'attachment') + else: + self.base.attach_text(data, content_type) + + ctype = self.base.content_encoding['Content-Type'][0] + + if ctype and not ctype.startswith('multipart'): + self.base.content_encoding['Content-Type'] = ('multipart/mixed', {}) + + def to_message(self): + """ + Figures out all the required steps to finally craft the + message you need and return it. The resulting message + is also available as a self.base attribute. + + What is returned is a Python email API message you can + use with those APIs. The self.base attribute is the raw + lamson.encoding.MailBase. + """ + del self.base.parts[:] + + if self.Body and self.Html: + self.multipart = True + self.base.content_encoding['Content-Type'] = ( + 'multipart/alternative', {}) + + if self.multipart: + self.base.body = None + if self.Body: + self.base.attach_text(self.Body, 'text/plain') + + if self.Html: + self.base.attach_text(self.Html, 'text/html') + + for args in self.attachments: + self._encode_attachment(**args) + + elif self.Body: + self.base.body = self.Body + self.base.content_encoding['Content-Type'] = ('text/plain', {}) + + elif self.Html: + self.base.body = self.Html + self.base.content_encoding['Content-Type'] = ('text/html', {}) + + return to_message(self.base, separator=self.separator) + + def all_parts(self): + """ + Returns all the encoded parts. Only useful for debugging + or inspecting after calling to_message(). + """ + return self.base.parts + + def keys(self): + return self.base.keys() + + +def to_message(mail, separator="; "): + """ + Given a MailBase message, this will construct a MIMEPart + that is canonicalized for use with the Python email API. + """ + ctype, params = mail.content_encoding['Content-Type'] + + if not ctype: + if mail.parts: + ctype = 'multipart/mixed' + else: + ctype = 'text/plain' + else: + if mail.parts: + assert ctype.startswith(("multipart", "message")), \ + "Content type should be multipart or message, not %r" % ctype + + # adjust the content type according to what it should be now + mail.content_encoding['Content-Type'] = (ctype, params) + + try: + out = MIMEPart(ctype, **params) + except TypeError, exc: # pragma: no cover + raise EncodingError("Content-Type malformed, not allowed: %r; " + "%r (Python ERROR: %s" % + (ctype, params, exc.message)) + + for k in mail.keys(): + if k in ADDRESS_HEADERS_WHITELIST: + out[k.encode('ascii')] = header_to_mime_encoding( + mail[k], + not_email=False, + separator=separator + ) + else: + out[k.encode('ascii')] = header_to_mime_encoding( + mail[k], + not_email=True + ) + + out.extract_payload(mail) + + # go through the children + for part in mail.parts: + out.attach(to_message(part)) + + return out + +class MIMEPart(MIMEBase): + """ + A reimplementation of nearly everything in email.mime to be more useful + for actually attaching things. Rather than one class for every type of + thing you'd encode, there's just this one, and it figures out how to + encode what you ask it. + """ + def __init__(self, type, **params): + self.maintype, self.subtype = type.split('/') + MIMEBase.__init__(self, self.maintype, self.subtype, **params) + + def add_text(self, content): + # this is text, so encode it in canonical form + try: + encoded = content.encode('ascii') + charset = 'ascii' + except UnicodeError: + encoded = content.encode('utf-8') + charset = 'utf-8' + + self.set_payload(encoded, charset=charset) + + def extract_payload(self, mail): + if mail.body == None: return # only None, '' is still ok + + ctype, ctype_params = mail.content_encoding['Content-Type'] + cdisp, cdisp_params = mail.content_encoding['Content-Disposition'] + + assert ctype, ("Extract payload requires that mail.content_encoding " + "have a valid Content-Type.") + + if ctype.startswith("text/"): + self.add_text(mail.body) + else: + if cdisp: + # replicate the content-disposition settings + self.add_header('Content-Disposition', cdisp, **cdisp_params) + + self.set_payload(mail.body) + encoders.encode_base64(self) + + def __repr__(self): + return "" % ( + self.subtype, + self.maintype, + self['Content-Type'], + self['Content-Disposition'], + self.is_multipart()) + + +def header_to_mime_encoding(value, not_email=False, separator=", "): + if not value: return "" + + encoder = Charset(DEFAULT_ENCODING) + if type(value) == list: + return separator.join(properly_encode_header( + v, encoder, not_email) for v in value) + else: + return properly_encode_header(value, encoder, not_email) + +def properly_encode_header(value, encoder, not_email): + """ + The only thing special (weird) about this function is that it tries + to do a fast check to see if the header value has an email address in + it. Since random headers could have an email address, and email addresses + have weird special formatting rules, we have to check for it. + + Normally this works fine, but in Librelist, we need to "obfuscate" email + addresses by changing the '@' to '-AT-'. This is where + VALUE_IS_EMAIL_ADDRESS exists. It's a simple lambda returning True/False + to check if a header value has an email address. If you need to make this + check different, then change this. + """ + try: + return value.encode("ascii") + except UnicodeEncodeError: + if not_email is False and VALUE_IS_EMAIL_ADDRESS(value): + # this could have an email address, make sure we don't screw it up + name, address = parseaddr(value) + return '"%s" <%s>' % ( + encoder.header_encode(name.encode("utf-8")), address) + + return encoder.header_encode(value.encode("utf-8")) diff --git a/rhodecode/lib/smtp_mailer.py b/rhodecode/lib/rcmail/smtp_mailer.py rename from rhodecode/lib/smtp_mailer.py rename to rhodecode/lib/rcmail/smtp_mailer.py --- a/rhodecode/lib/smtp_mailer.py +++ b/rhodecode/lib/rcmail/smtp_mailer.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- """ - rhodecode.lib.smtp_mailer - ~~~~~~~~~~~~~~~~~~~~~~~~~ + rhodecode.lib.rcmail.smtp_mailer + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Simple smtp mailer used in RhodeCode :created_on: Sep 13, 2010 - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -24,16 +24,8 @@ import logging import smtplib -import mimetypes from socket import sslerror - -from email.mime.multipart import MIMEMultipart -from email.mime.image import MIMEImage -from email.mime.audio import MIMEAudio -from email.mime.base import MIMEBase -from email.mime.text import MIMEText -from email.utils import formatdate -from email import encoders +from rhodecode.lib.rcmail.message import Message class SmtpMailer(object): @@ -62,10 +54,15 @@ class SmtpMailer(object): self.debug = debug self.auth = smtp_auth - def send(self, recipients=[], subject='', body='', attachment_files=None): + def send(self, recipients=[], subject='', body='', html='', + attachment_files=None): if isinstance(recipients, basestring): recipients = [recipients] + msg = Message(subject, recipients, body, html, self.mail_from, + recipients_separator=", ") + raw_msg = msg.to_message() + if self.ssl: smtp_serv = smtplib.SMTP_SSL(self.mail_server, self.mail_port) else: @@ -87,26 +84,7 @@ class SmtpMailer(object): if self.user and self.passwd: smtp_serv.login(self.user, self.passwd) - date_ = formatdate(localtime=True) - msg = MIMEMultipart() - msg.set_type('multipart/alternative') - msg.preamble = 'You will not see this in a MIME-aware mail reader.\n' - - text_msg = MIMEText(body) - text_msg.set_type('text/plain') - text_msg.set_param('charset', 'UTF-8') - - msg['From'] = self.mail_from - msg['To'] = ','.join(recipients) - msg['Date'] = date_ - msg['Subject'] = subject - - msg.attach(text_msg) - - if attachment_files: - self.__atach_files(msg, attachment_files) - - smtp_serv.sendmail(self.mail_from, recipients, msg.as_string()) + smtp_serv.sendmail(msg.sender, msg.send_to, raw_msg.as_string()) logging.info('MAIL SEND TO: %s' % recipients) try: @@ -114,52 +92,3 @@ class SmtpMailer(object): except sslerror: # sslerror is raised in tls connections on closing sometimes pass - - def __atach_files(self, msg, attachment_files): - if isinstance(attachment_files, dict): - for f_name, msg_file in attachment_files.items(): - ctype, encoding = mimetypes.guess_type(f_name) - logging.info("guessing file %s type based on %s", ctype, - f_name) - if ctype is None or encoding is not None: - # No guess could be made, or the file is encoded - # (compressed), so use a generic bag-of-bits type. - ctype = 'application/octet-stream' - maintype, subtype = ctype.split('/', 1) - if maintype == 'text': - # Note: we should handle calculating the charset - file_part = MIMEText(self.get_content(msg_file), - _subtype=subtype) - elif maintype == 'image': - file_part = MIMEImage(self.get_content(msg_file), - _subtype=subtype) - elif maintype == 'audio': - file_part = MIMEAudio(self.get_content(msg_file), - _subtype=subtype) - else: - file_part = MIMEBase(maintype, subtype) - file_part.set_payload(self.get_content(msg_file)) - # Encode the payload using Base64 - encoders.encode_base64(msg) - # Set the filename parameter - 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"}') - - def get_content(self, msg_file): - """Get content based on type, if content is a string do open first - else just read because it's a probably open file object - - :param msg_file: - """ - if isinstance(msg_file, str): - return open(msg_file, "rb").read() - else: - # just for safe seek to 0 - msg_file.seek(0) - return msg_file.read() - diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -7,7 +7,7 @@ :created_on: Apr 18, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -29,6 +29,9 @@ import datetime import traceback import paste import beaker +import tarfile +import shutil +from os.path import abspath from os.path import dirname as dn, join as jn from paste.script.command import Command, BadCommand @@ -37,25 +40,27 @@ from mercurial import ui, config from webhelpers.text import collapse, remove_formatting, strip_tags -from vcs import get_backend -from vcs.backends.base import BaseChangeset -from vcs.utils.lazy import LazyProperty -from vcs.utils.helpers import get_scm -from vcs.exceptions import VCSError +from rhodecode.lib.vcs import get_backend +from rhodecode.lib.vcs.backends.base import BaseChangeset +from rhodecode.lib.vcs.utils.lazy import LazyProperty +from rhodecode.lib.vcs.utils.helpers import get_scm +from rhodecode.lib.vcs.exceptions import VCSError + +from rhodecode.lib.caching_query import FromCache from rhodecode.model import meta -from rhodecode.model.caching_query import FromCache -from rhodecode.model.db import Repository, User, RhodeCodeUi, UserLog, Group, \ - RhodeCodeSettings -from rhodecode.model.repo import RepoModel +from rhodecode.model.db import Repository, User, RhodeCodeUi, \ + UserLog, RepoGroup, RhodeCodeSetting, UserRepoGroupToPerm +from rhodecode.model.meta import Session +from rhodecode.model.repos_group import ReposGroupModel log = logging.getLogger(__name__) -def recursive_replace(str, replace=' '): +def recursive_replace(str_, replace=' '): """Recursive replace of given sign to just one instance - :param str: given string + :param str_: given string :param replace: char to find and replace multiple instances Examples:: @@ -63,11 +68,11 @@ def recursive_replace(str, replace=' '): 'Mighty-Mighty-Bo-sstones' """ - if str.find(replace * 2) == -1: - return str + if str_.find(replace * 2) == -1: + return str_ else: - str = str.replace(replace * 2, replace) - return recursive_replace(str, replace) + str_ = str_.replace(replace * 2, replace) + return recursive_replace(str_, replace) def repo_name_slug(value): @@ -90,7 +95,11 @@ def get_repo_slug(request): return request.environ['pylons.routes_dict'].get('repo_name') -def action_logger(user, action, repo, ipaddr='', sa=None): +def get_repos_group_slug(request): + return request.environ['pylons.routes_dict'].get('group_name') + + +def action_logger(user, action, repo, ipaddr='', sa=None, commit=False): """ Action logger for various actions made by users @@ -106,7 +115,7 @@ def action_logger(user, action, repo, ip """ if not sa: - sa = meta.Session() + sa = meta.Session try: if hasattr(user, 'user_id'): @@ -116,13 +125,12 @@ def action_logger(user, action, repo, ip else: raise Exception('You have to provide user object or username') - rm = RepoModel() if hasattr(repo, 'repo_id'): - repo_obj = rm.get(repo.repo_id, cache=False) + repo_obj = Repository.get(repo.repo_id) 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) + repo_obj = Repository.get_by_repo_name(repo_name) else: raise Exception('You have to provide repository to action logger') @@ -136,26 +144,25 @@ def action_logger(user, action, repo, ip 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_obj, action, repo) + log.info('Adding user %s, action %s on %s' % (user_obj, action, repo)) + if commit: + sa.commit() except: log.error(traceback.format_exc()) - sa.rollback() + raise def get_repos(path, recursive=False): """ Scans given path for repos and return (name,(type,path)) tuple - :param path: path to scann for repositories + :param path: path to scan for repositories :param recursive: recursive search and return names with subdirs in front """ - from vcs.utils.helpers import get_scm - from vcs.exceptions import VCSError # remove ending slash for better results - path = path.rstrip('/') + path = path.rstrip(os.sep) def _get_repos(p): if not os.access(p, os.W_OK): @@ -195,10 +202,11 @@ def is_valid_repo(repo_name, base_path): except VCSError: return False + def is_valid_repos_group(repos_group_name, base_path): """ Returns True if given path is a repos group False otherwise - + :param repo_name: :param base_path: """ @@ -214,6 +222,7 @@ def is_valid_repos_group(repos_group_nam return False + def ask_ok(prompt, retries=4, complaint='Yes or no, please!'): while True: ok = raw_input(prompt) @@ -250,28 +259,28 @@ def make_ui(read_from='file', path=None, baseui = ui.ui() - #clean the baseui object + # 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) + log.debug('hgrc file is not present at %s skipping...' % path) return False - log.debug('reading hgrc from %s', path) + log.debug('reading hgrc from %s' % path) cfg = config.config() 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) + log.debug('settings ui from file[%s]%s:%s' % (section, k, v)) baseui.setconfig(section, k, v) elif read_from == 'db': - sa = meta.Session() + sa = meta.Session ret = sa.query(RhodeCodeUi)\ - .options(FromCache("sql_cache_short", - "get_hg_ui_settings")).all() + .options(FromCache("sql_cache_short", "get_hg_ui_settings"))\ + .all() hg_ui = ret for ui_ in hg_ui: @@ -285,18 +294,20 @@ def make_ui(read_from='file', path=None, def set_rhodecode_config(config): - """Updates pylons config with new settings from database + """ + Updates pylons config with new settings from database :param config: """ - hgsettings = RhodeCodeSettings.get_app_settings() + hgsettings = RhodeCodeSetting.get_app_settings() for k, v in hgsettings.items(): config[k] = v def invalidate_cache(cache_key, *args): - """Puts cache invalidation task into db for + """ + Puts cache invalidation task into db for further global cache invalidation """ @@ -313,7 +324,8 @@ class EmptyChangeset(BaseChangeset): an EmptyChangeset """ - def __init__(self, cs='0' * 40, repo=None, requested_revision=None, alias=None): + def __init__(self, cs='0' * 40, repo=None, requested_revision=None, + alias=None): self._empty_cs = cs self.revision = -1 self.message = '' @@ -325,7 +337,8 @@ class EmptyChangeset(BaseChangeset): @LazyProperty def raw_id(self): - """Returns raw string identifying this changeset, useful for web + """ + Returns raw string identifying this changeset, useful for web representation. """ @@ -350,66 +363,74 @@ class EmptyChangeset(BaseChangeset): def map_groups(groups): - """Checks for groups existence, and creates groups structures. + """ + Checks for groups existence, and creates groups structures. It returns last group in structure :param groups: list of groups structure """ - sa = meta.Session() + sa = meta.Session parent = None group = None # last element is repo in nested groups structure groups = groups[:-1] - + rgm = ReposGroupModel(sa) for lvl, group_name in enumerate(groups): group_name = '/'.join(groups[:lvl] + [group_name]) - group = sa.query(Group).filter(Group.group_name == group_name).scalar() + group = RepoGroup.get_by_group_name(group_name) + desc = '%s group' % group_name + +# # WTF that doesn't work !? +# if group is None: +# group = rgm.create(group_name, desc, parent, just_db=True) +# sa.commit() if group is None: - group = Group(group_name, parent) + log.debug('creating group level: %s group_name: %s' % (lvl, group_name)) + group = RepoGroup(group_name, parent) + group.group_description = desc sa.add(group) + rgm._create_default_perms(group) sa.commit() parent = group return group def repo2db_mapper(initial_repo_list, remove_obsolete=False): - """maps all repos given in initial_repo_list, non existing repositories + """ + maps all repos given in initial_repo_list, non existing repositories are created, if remove_obsolete is True it also check for db entries that are not in initial_repo_list and removes them. :param initial_repo_list: list of repositories found by scanning methods :param remove_obsolete: check for obsolete entries in database """ - - sa = meta.Session() + from rhodecode.model.repo import RepoModel + sa = meta.Session rm = RepoModel() user = sa.query(User).filter(User.admin == True).first() + if user is None: + raise Exception('Missing administrative account !') added = [] - # fixup groups paths to new format on the fly - # TODO: remove this in future - for g in Group.query().all(): - g.group_name = g.get_new_name(g.name) - sa.add(g) + for name, repo in initial_repo_list.items(): group = map_groups(name.split(Repository.url_sep())) if not rm.get_by_repo_name(name, cache=False): - log.info('repository %s not found creating default', name) + log.info('repository %s not found creating default' % name) added.append(name) form_data = { - 'repo_name': name, - 'repo_name_full': name, - 'repo_type': repo.alias, - 'description': repo.description \ - if repo.description != 'unknown' else \ - '%s repository' % name, - 'private': False, - 'group_id': getattr(group, 'group_id', None) - } + 'repo_name': name, + 'repo_name_full': name, + 'repo_type': repo.alias, + 'description': repo.description \ + if repo.description != 'unknown' else '%s repository' % name, + 'private': False, + 'group_id': getattr(group, 'group_id', None) + } rm.create(form_data, user, just_db=True) - + sa.commit() removed = [] if remove_obsolete: #remove from database those repositories that are not in the filesystem @@ -421,7 +442,8 @@ def repo2db_mapper(initial_repo_list, re return added, removed -#set cache regions for beaker so celery can utilise it + +# set cache regions for beaker so celery can utilise it def add_cache(settings): cache_settings = {'regions': None} for key in settings.keys(): @@ -455,7 +477,7 @@ def add_cache(settings): def create_test_index(repo_location, config, full_index): """ Makes default test index - + :param config: test config :param full_index: """ @@ -480,19 +502,16 @@ def create_test_index(repo_location, con def create_test_env(repos_test_path, config): - """Makes a fresh database and + """ + Makes a fresh database and 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 abspath + from rhodecode.tests import HG_REPO, TESTS_TMP_PATH # PART ONE create db dbconf = config['sqlalchemy.db1.url'] - log.debug('making test db %s', dbconf) + log.debug('making test db %s' % dbconf) # create test dir if it doesn't exist if not os.path.isdir(repos_test_path): @@ -507,7 +526,7 @@ def create_test_env(repos_test_path, con dbmanage.admin_prompt() dbmanage.create_permissions() dbmanage.populate_default_permissions() - + Session.commit() # PART TWO make test repo log.debug('making test vcs repositories') @@ -595,4 +614,3 @@ class BasePasterCommand(Command): 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 --git a/rhodecode/lib/vcs/__init__.py b/rhodecode/lib/vcs/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/__init__.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" + vcs + ~~~ + + Various version Control System (vcs) management abstraction layer for + Python. + + :created_on: Apr 8, 2010 + :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak. +""" + +VERSION = (0, 2, 3, 'dev') + +__version__ = '.'.join((str(each) for each in VERSION[:4])) + +__all__ = [ + 'get_version', 'get_repo', 'get_backend', + 'VCSError', 'RepositoryError', 'ChangesetError'] + +import sys +from rhodecode.lib.vcs.backends import get_repo, get_backend +from rhodecode.lib.vcs.exceptions import VCSError, RepositoryError, ChangesetError + + +def get_version(): + """ + Returns shorter version (digit parts only) as string. + """ + return '.'.join((str(each) for each in VERSION[:3])) + +def main(argv=None): + if argv is None: + argv = sys.argv + from rhodecode.lib.vcs.cli import ExecutionManager + manager = ExecutionManager(argv) + manager.execute() + return 0 + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/rhodecode/lib/vcs/backends/__init__.py b/rhodecode/lib/vcs/backends/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/backends/__init__.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +""" + vcs.backends + ~~~~~~~~~~~~ + + Main package for scm backends + + :created_on: Apr 8, 2010 + :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak. +""" +import os +from pprint import pformat +from rhodecode.lib.vcs.conf import settings +from rhodecode.lib.vcs.exceptions import VCSError +from rhodecode.lib.vcs.utils.helpers import get_scm +from rhodecode.lib.vcs.utils.paths import abspath +from rhodecode.lib.vcs.utils.imports import import_class + + +def get_repo(path=None, alias=None, create=False): + """ + Returns ``Repository`` object of type linked with given ``alias`` at + the specified ``path``. If ``alias`` is not given it will try to guess it + using get_scm method + """ + if create: + if not (path or alias): + raise TypeError("If create is specified, we need path and scm type") + return get_backend(alias)(path, create=True) + if path is None: + path = abspath(os.path.curdir) + try: + scm, path = get_scm(path, search_recursively=True) + path = abspath(path) + alias = scm + except VCSError: + raise VCSError("No scm found at %s" % path) + if alias is None: + alias = get_scm(path)[0] + + backend = get_backend(alias) + repo = backend(path, create=create) + return repo + + +def get_backend(alias): + """ + Returns ``Repository`` class identified by the given alias or raises + VCSError if alias is not recognized or backend class cannot be imported. + """ + if alias not in settings.BACKENDS: + raise VCSError("Given alias '%s' is not recognized! Allowed aliases:\n" + "%s" % (alias, pformat(settings.BACKENDS.keys()))) + backend_path = settings.BACKENDS[alias] + klass = import_class(backend_path) + return klass + + +def get_supported_backends(): + """ + Returns list of aliases of supported backends. + """ + return settings.BACKENDS.keys() diff --git a/rhodecode/lib/vcs/backends/base.py b/rhodecode/lib/vcs/backends/base.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/backends/base.py @@ -0,0 +1,911 @@ +# -*- coding: utf-8 -*- +""" + vcs.backends.base + ~~~~~~~~~~~~~~~~~ + + Base for all available scm backends + + :created_on: Apr 8, 2010 + :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak. +""" + + +from itertools import chain +from rhodecode.lib.vcs.utils import author_name, author_email +from rhodecode.lib.vcs.utils.lazy import LazyProperty +from rhodecode.lib.vcs.utils.helpers import get_dict_for_attrs +from rhodecode.lib.vcs.conf import settings + +from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \ + NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, \ + NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, \ + RepositoryError + + +class BaseRepository(object): + """ + Base Repository for final backends + + **Attributes** + + ``DEFAULT_BRANCH_NAME`` + name of default branch (i.e. "trunk" for svn, "master" for git etc. + + ``scm`` + alias of scm, i.e. *git* or *hg* + + ``repo`` + object from external api + + ``revisions`` + list of all available revisions' ids, in ascending order + + ``changesets`` + storage dict caching returned changesets + + ``path`` + absolute path to the repository + + ``branches`` + branches as list of changesets + + ``tags`` + tags as list of changesets + """ + scm = None + DEFAULT_BRANCH_NAME = None + EMPTY_CHANGESET = '0' * 40 + + def __init__(self, repo_path, create=False, **kwargs): + """ + Initializes repository. Raises RepositoryError if repository could + not be find at the given ``repo_path`` or directory at ``repo_path`` + exists and ``create`` is set to True. + + :param repo_path: local path of the repository + :param create=False: if set to True, would try to craete repository. + :param src_url=None: if set, should be proper url from which repository + would be cloned; requires ``create`` parameter to be set to True - + raises RepositoryError if src_url is set and create evaluates to + False + """ + raise NotImplementedError + + def __str__(self): + return '<%s at %s>' % (self.__class__.__name__, self.path) + + def __repr__(self): + return self.__str__() + + def __len__(self): + return self.count() + + @LazyProperty + def alias(self): + for k, v in settings.BACKENDS.items(): + if v.split('.')[-1] == str(self.__class__.__name__): + return k + + @LazyProperty + def name(self): + raise NotImplementedError + + @LazyProperty + def owner(self): + raise NotImplementedError + + @LazyProperty + def description(self): + raise NotImplementedError + + @LazyProperty + def size(self): + """ + Returns combined size in bytes for all repository files + """ + + size = 0 + try: + tip = self.get_changeset() + for topnode, dirs, files in tip.walk('/'): + for f in files: + size += tip.get_file_size(f.path) + for dir in dirs: + for f in files: + size += tip.get_file_size(f.path) + + except RepositoryError, e: + pass + return size + + def is_valid(self): + """ + Validates repository. + """ + raise NotImplementedError + + def get_last_change(self): + self.get_changesets() + + #========================================================================== + # CHANGESETS + #========================================================================== + + def get_changeset(self, revision=None): + """ + Returns instance of ``Changeset`` class. If ``revision`` is None, most + recent changeset is returned. + + :raises ``EmptyRepositoryError``: if there are no revisions + """ + raise NotImplementedError + + def __iter__(self): + """ + Allows Repository objects to be iterated. + + *Requires* implementation of ``__getitem__`` method. + """ + for revision in self.revisions: + yield self.get_changeset(revision) + + def get_changesets(self, start=None, end=None, start_date=None, + end_date=None, branch_name=None, reverse=False): + """ + Returns iterator of ``MercurialChangeset`` objects from start to end + not inclusive This should behave just like a list, ie. end is not + inclusive + + :param start: None or str + :param end: None or str + :param start_date: + :param end_date: + :param branch_name: + :param reversed: + """ + raise NotImplementedError + + def __getslice__(self, i, j): + """ + Returns a iterator of sliced repository + """ + for rev in self.revisions[i:j]: + yield self.get_changeset(rev) + + def __getitem__(self, key): + return self.get_changeset(key) + + def count(self): + return len(self.revisions) + + def tag(self, name, user, revision=None, message=None, date=None, **opts): + """ + Creates and returns a tag for the given ``revision``. + + :param name: name for new tag + :param user: full username, i.e.: "Joe Doe " + :param revision: changeset id for which new tag would be created + :param message: message of the tag's commit + :param date: date of tag's commit + + :raises TagAlreadyExistError: if tag with same name already exists + """ + raise NotImplementedError + + def remove_tag(self, name, user, message=None, date=None): + """ + Removes tag with the given ``name``. + + :param name: name of the tag to be removed + :param user: full username, i.e.: "Joe Doe " + :param message: message of the tag's removal commit + :param date: date of tag's removal commit + + :raises TagDoesNotExistError: if tag with given name does not exists + """ + raise NotImplementedError + + def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False, + context=3): + """ + Returns (git like) *diff*, as plain text. Shows changes introduced by + ``rev2`` since ``rev1``. + + :param rev1: Entry point from which diff is shown. Can be + ``self.EMPTY_CHANGESET`` - in this case, patch showing all + the changes since empty state of the repository until ``rev2`` + :param rev2: Until which revision changes should be shown. + :param ignore_whitespace: If set to ``True``, would not show whitespace + changes. Defaults to ``False``. + :param context: How many lines before/after changed lines should be + shown. Defaults to ``3``. + """ + raise NotImplementedError + + # ========== # + # COMMIT API # + # ========== # + + @LazyProperty + def in_memory_changeset(self): + """ + Returns ``InMemoryChangeset`` object for this repository. + """ + raise NotImplementedError + + def add(self, filenode, **kwargs): + """ + Commit api function that will add given ``FileNode`` into this + repository. + + :raises ``NodeAlreadyExistsError``: if there is a file with same path + already in repository + :raises ``NodeAlreadyAddedError``: if given node is already marked as + *added* + """ + raise NotImplementedError + + def remove(self, filenode, **kwargs): + """ + Commit api function that will remove given ``FileNode`` into this + repository. + + :raises ``EmptyRepositoryError``: if there are no changesets yet + :raises ``NodeDoesNotExistError``: if there is no file with given path + """ + raise NotImplementedError + + def commit(self, message, **kwargs): + """ + Persists current changes made on this repository and returns newly + created changeset. + + :raises ``NothingChangedError``: if no changes has been made + """ + raise NotImplementedError + + def get_state(self): + """ + Returns dictionary with ``added``, ``changed`` and ``removed`` lists + containing ``FileNode`` objects. + """ + raise NotImplementedError + + def get_config_value(self, section, name, config_file=None): + """ + Returns configuration value for a given [``section``] and ``name``. + + :param section: Section we want to retrieve value from + :param name: Name of configuration we want to retrieve + :param config_file: A path to file which should be used to retrieve + configuration from (might also be a list of file paths) + """ + raise NotImplementedError + + def get_user_name(self, config_file=None): + """ + Returns user's name from global configuration file. + + :param config_file: A path to file which should be used to retrieve + configuration from (might also be a list of file paths) + """ + raise NotImplementedError + + def get_user_email(self, config_file=None): + """ + Returns user's email from global configuration file. + + :param config_file: A path to file which should be used to retrieve + configuration from (might also be a list of file paths) + """ + raise NotImplementedError + + # =========== # + # WORKDIR API # + # =========== # + + @LazyProperty + def workdir(self): + """ + Returns ``Workdir`` instance for this repository. + """ + raise NotImplementedError + + +class BaseChangeset(object): + """ + Each backend should implement it's changeset representation. + + **Attributes** + + ``repository`` + repository object within which changeset exists + + ``id`` + may be ``raw_id`` or i.e. for mercurial's tip just ``tip`` + + ``raw_id`` + raw changeset representation (i.e. full 40 length sha for git + backend) + + ``short_id`` + shortened (if apply) version of ``raw_id``; it would be simple + shortcut for ``raw_id[:12]`` for git/mercurial backends or same + as ``raw_id`` for subversion + + ``revision`` + revision number as integer + + ``files`` + list of ``FileNode`` (``Node`` with NodeKind.FILE) objects + + ``dirs`` + list of ``DirNode`` (``Node`` with NodeKind.DIR) objects + + ``nodes`` + combined list of ``Node`` objects + + ``author`` + author of the changeset, as unicode + + ``message`` + message of the changeset, as unicode + + ``parents`` + list of parent changesets + + ``last`` + ``True`` if this is last changeset in repository, ``False`` + otherwise; trying to access this attribute while there is no + changesets would raise ``EmptyRepositoryError`` + """ + def __str__(self): + return '<%s at %s:%s>' % (self.__class__.__name__, self.revision, + self.short_id) + + def __repr__(self): + return self.__str__() + + def __unicode__(self): + return u'%s:%s' % (self.revision, self.short_id) + + def __eq__(self, other): + return self.raw_id == other.raw_id + + @LazyProperty + def last(self): + if self.repository is None: + raise ChangesetError("Cannot check if it's most recent revision") + return self.raw_id == self.repository.revisions[-1] + + @LazyProperty + def parents(self): + """ + Returns list of parents changesets. + """ + raise NotImplementedError + + @LazyProperty + def id(self): + """ + Returns string identifying this changeset. + """ + raise NotImplementedError + + @LazyProperty + def raw_id(self): + """ + Returns raw string identifying this changeset. + """ + raise NotImplementedError + + @LazyProperty + def short_id(self): + """ + Returns shortened version of ``raw_id`` attribute, as string, + identifying this changeset, useful for web representation. + """ + raise NotImplementedError + + @LazyProperty + def revision(self): + """ + Returns integer identifying this changeset. + + """ + raise NotImplementedError + + @LazyProperty + def author(self): + """ + Returns Author for given commit + """ + + raise NotImplementedError + + @LazyProperty + def author_name(self): + """ + Returns Author name for given commit + """ + + return author_name(self.author) + + @LazyProperty + def author_email(self): + """ + Returns Author email address for given commit + """ + + return author_email(self.author) + + def get_file_mode(self, path): + """ + Returns stat mode of the file at the given ``path``. + """ + raise NotImplementedError + + def get_file_content(self, path): + """ + Returns content of the file at the given ``path``. + """ + raise NotImplementedError + + def get_file_size(self, path): + """ + Returns size of the file at the given ``path``. + """ + raise NotImplementedError + + def get_file_changeset(self, path): + """ + Returns last commit of the file at the given ``path``. + """ + raise NotImplementedError + + def get_file_history(self, path): + """ + Returns history of file as reversed list of ``Changeset`` objects for + which file at given ``path`` has been modified. + """ + raise NotImplementedError + + def get_nodes(self, path): + """ + Returns combined ``DirNode`` and ``FileNode`` objects list representing + state of changeset at the given ``path``. + + :raises ``ChangesetError``: if node at the given ``path`` is not + instance of ``DirNode`` + """ + raise NotImplementedError + + def get_node(self, path): + """ + Returns ``Node`` object from the given ``path``. + + :raises ``NodeDoesNotExistError``: if there is no node at the given + ``path`` + """ + raise NotImplementedError + + def fill_archive(self, stream=None, kind='tgz', prefix=None): + """ + Fills up given stream. + + :param stream: file like object. + :param kind: one of following: ``zip``, ``tar``, ``tgz`` + or ``tbz2``. Default: ``tgz``. + :param prefix: name of root directory in archive. + Default is repository name and changeset's raw_id joined with dash. + + repo-tip. + """ + + raise NotImplementedError + + def get_chunked_archive(self, **kwargs): + """ + Returns iterable archive. Tiny wrapper around ``fill_archive`` method. + + :param chunk_size: extra parameter which controls size of returned + chunks. Default:8k. + """ + + chunk_size = kwargs.pop('chunk_size', 8192) + stream = kwargs.get('stream') + self.fill_archive(**kwargs) + while True: + data = stream.read(chunk_size) + if not data: + break + yield data + + @LazyProperty + def root(self): + """ + Returns ``RootNode`` object for this changeset. + """ + return self.get_node('') + + def next(self, branch=None): + """ + Returns next changeset from current, if branch is gives it will return + next changeset belonging to this branch + + :param branch: show changesets within the given named branch + """ + raise NotImplementedError + + def prev(self, branch=None): + """ + Returns previous changeset from current, if branch is gives it will + return previous changeset belonging to this branch + + :param branch: show changesets within the given named branch + """ + raise NotImplementedError + + @LazyProperty + def added(self): + """ + Returns list of added ``FileNode`` objects. + """ + raise NotImplementedError + + @LazyProperty + def changed(self): + """ + Returns list of modified ``FileNode`` objects. + """ + raise NotImplementedError + + @LazyProperty + def removed(self): + """ + Returns list of removed ``FileNode`` objects. + """ + raise NotImplementedError + + @LazyProperty + def size(self): + """ + Returns total number of bytes from contents of all filenodes. + """ + return sum((node.size for node in self.get_filenodes_generator())) + + def walk(self, topurl=''): + """ + Similar to os.walk method. Insted of filesystem it walks through + changeset starting at given ``topurl``. Returns generator of tuples + (topnode, dirnodes, filenodes). + """ + topnode = self.get_node(topurl) + yield (topnode, topnode.dirs, topnode.files) + for dirnode in topnode.dirs: + for tup in self.walk(dirnode.path): + yield tup + + def get_filenodes_generator(self): + """ + Returns generator that yields *all* file nodes. + """ + for topnode, dirs, files in self.walk(): + for node in files: + yield node + + def as_dict(self): + """ + Returns dictionary with changeset's attributes and their values. + """ + data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id', + 'revision', 'date', 'message']) + data['author'] = {'name': self.author_name, 'email': self.author_email} + data['added'] = [node.path for node in self.added] + data['changed'] = [node.path for node in self.changed] + data['removed'] = [node.path for node in self.removed] + return data + + +class BaseWorkdir(object): + """ + Working directory representation of single repository. + + :attribute: repository: repository object of working directory + """ + + def __init__(self, repository): + self.repository = repository + + def get_branch(self): + """ + Returns name of current branch. + """ + raise NotImplementedError + + def get_changeset(self): + """ + Returns current changeset. + """ + raise NotImplementedError + + def get_added(self): + """ + Returns list of ``FileNode`` objects marked as *new* in working + directory. + """ + raise NotImplementedError + + def get_changed(self): + """ + Returns list of ``FileNode`` objects *changed* in working directory. + """ + raise NotImplementedError + + def get_removed(self): + """ + Returns list of ``RemovedFileNode`` objects marked as *removed* in + working directory. + """ + raise NotImplementedError + + def get_untracked(self): + """ + Returns list of ``FileNode`` objects which are present within working + directory however are not tracked by repository. + """ + raise NotImplementedError + + def get_status(self): + """ + Returns dict with ``added``, ``changed``, ``removed`` and ``untracked`` + lists. + """ + raise NotImplementedError + + def commit(self, message, **kwargs): + """ + Commits local (from working directory) changes and returns newly + created + ``Changeset``. Updates repository's ``revisions`` list. + + :raises ``CommitError``: if any error occurs while committing + """ + raise NotImplementedError + + def update(self, revision=None): + """ + Fetches content of the given revision and populates it within working + directory. + """ + raise NotImplementedError + + def checkout_branch(self, branch=None): + """ + Checks out ``branch`` or the backend's default branch. + + Raises ``BranchDoesNotExistError`` if the branch does not exist. + """ + raise NotImplementedError + + +class BaseInMemoryChangeset(object): + """ + Represents differences between repository's state (most recent head) and + changes made *in place*. + + **Attributes** + + ``repository`` + repository object for this in-memory-changeset + + ``added`` + list of ``FileNode`` objects marked as *added* + + ``changed`` + list of ``FileNode`` objects marked as *changed* + + ``removed`` + list of ``FileNode`` or ``RemovedFileNode`` objects marked to be + *removed* + + ``parents`` + list of ``Changeset`` representing parents of in-memory changeset. + Should always be 2-element sequence. + + """ + + def __init__(self, repository): + self.repository = repository + self.added = [] + self.changed = [] + self.removed = [] + self.parents = [] + + def add(self, *filenodes): + """ + Marks given ``FileNode`` objects as *to be committed*. + + :raises ``NodeAlreadyExistsError``: if node with same path exists at + latest changeset + :raises ``NodeAlreadyAddedError``: if node with same path is already + marked as *added* + """ + # Check if not already marked as *added* first + for node in filenodes: + if node.path in (n.path for n in self.added): + raise NodeAlreadyAddedError("Such FileNode %s is already " + "marked for addition" % node.path) + for node in filenodes: + self.added.append(node) + + def change(self, *filenodes): + """ + Marks given ``FileNode`` objects to be *changed* in next commit. + + :raises ``EmptyRepositoryError``: if there are no changesets yet + :raises ``NodeAlreadyExistsError``: if node with same path is already + marked to be *changed* + :raises ``NodeAlreadyRemovedError``: if node with same path is already + marked to be *removed* + :raises ``NodeDoesNotExistError``: if node doesn't exist in latest + changeset + :raises ``NodeNotChangedError``: if node hasn't really be changed + """ + for node in filenodes: + if node.path in (n.path for n in self.removed): + raise NodeAlreadyRemovedError("Node at %s is already marked " + "as removed" % node.path) + try: + self.repository.get_changeset() + except EmptyRepositoryError: + raise EmptyRepositoryError("Nothing to change - try to *add* new " + "nodes rather than changing them") + for node in filenodes: + if node.path in (n.path for n in self.changed): + raise NodeAlreadyChangedError("Node at '%s' is already " + "marked as changed" % node.path) + self.changed.append(node) + + def remove(self, *filenodes): + """ + Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be + *removed* in next commit. + + :raises ``NodeAlreadyRemovedError``: if node has been already marked to + be *removed* + :raises ``NodeAlreadyChangedError``: if node has been already marked to + be *changed* + """ + for node in filenodes: + if node.path in (n.path for n in self.removed): + raise NodeAlreadyRemovedError("Node is already marked to " + "for removal at %s" % node.path) + if node.path in (n.path for n in self.changed): + raise NodeAlreadyChangedError("Node is already marked to " + "be changed at %s" % node.path) + # We only mark node as *removed* - real removal is done by + # commit method + self.removed.append(node) + + def reset(self): + """ + Resets this instance to initial state (cleans ``added``, ``changed`` + and ``removed`` lists). + """ + self.added = [] + self.changed = [] + self.removed = [] + self.parents = [] + + def get_ipaths(self): + """ + Returns generator of paths from nodes marked as added, changed or + removed. + """ + for node in chain(self.added, self.changed, self.removed): + yield node.path + + def get_paths(self): + """ + Returns list of paths from nodes marked as added, changed or removed. + """ + return list(self.get_ipaths()) + + def check_integrity(self, parents=None): + """ + Checks in-memory changeset's integrity. Also, sets parents if not + already set. + + :raises CommitError: if any error occurs (i.e. + ``NodeDoesNotExistError``). + """ + if not self.parents: + parents = parents or [] + if len(parents) == 0: + try: + parents = [self.repository.get_changeset(), None] + except EmptyRepositoryError: + parents = [None, None] + elif len(parents) == 1: + parents += [None] + self.parents = parents + + # Local parents, only if not None + parents = [p for p in self.parents if p] + + # Check nodes marked as added + for p in parents: + for node in self.added: + try: + p.get_node(node.path) + except NodeDoesNotExistError: + pass + else: + raise NodeAlreadyExistsError("Node at %s already exists " + "at %s" % (node.path, p)) + + # Check nodes marked as changed + missing = set(self.changed) + not_changed = set(self.changed) + if self.changed and not parents: + raise NodeDoesNotExistError(str(self.changed[0].path)) + for p in parents: + for node in self.changed: + try: + old = p.get_node(node.path) + missing.remove(node) + if old.content != node.content: + not_changed.remove(node) + except NodeDoesNotExistError: + pass + if self.changed and missing: + raise NodeDoesNotExistError("Node at %s is missing " + "(parents: %s)" % (node.path, parents)) + + if self.changed and not_changed: + raise NodeNotChangedError("Node at %s wasn't actually changed " + "since parents' changesets: %s" % (not_changed.pop().path, + parents) + ) + + # Check nodes marked as removed + if self.removed and not parents: + raise NodeDoesNotExistError("Cannot remove node at %s as there " + "were no parents specified" % self.removed[0].path) + really_removed = set() + for p in parents: + for node in self.removed: + try: + p.get_node(node.path) + really_removed.add(node) + except ChangesetError: + pass + not_removed = set(self.removed) - really_removed + if not_removed: + raise NodeDoesNotExistError("Cannot remove node at %s from " + "following parents: %s" % (not_removed[0], parents)) + + def commit(self, message, author, parents=None, branch=None, date=None, + **kwargs): + """ + Performs in-memory commit (doesn't check workdir in any way) and + returns newly created ``Changeset``. Updates repository's + ``revisions``. + + .. note:: + While overriding this method each backend's should call + ``self.check_integrity(parents)`` in the first place. + + :param message: message of the commit + :param author: full username, i.e. "Joe Doe " + :param parents: single parent or sequence of parents from which commit + would be derieved + :param date: ``datetime.datetime`` instance. Defaults to + ``datetime.datetime.now()``. + :param branch: branch name, as string. If none given, default backend's + branch would be used. + + :raises ``CommitError``: if any error occurs while committing + """ + raise NotImplementedError diff --git a/rhodecode/lib/vcs/backends/git/__init__.py b/rhodecode/lib/vcs/backends/git/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/backends/git/__init__.py @@ -0,0 +1,9 @@ +from .repository import GitRepository +from .changeset import GitChangeset +from .inmemory import GitInMemoryChangeset +from .workdir import GitWorkdir + + +__all__ = [ + 'GitRepository', 'GitChangeset', 'GitInMemoryChangeset', 'GitWorkdir', +] diff --git a/rhodecode/lib/vcs/backends/git/changeset.py b/rhodecode/lib/vcs/backends/git/changeset.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/backends/git/changeset.py @@ -0,0 +1,450 @@ +import re +from itertools import chain +from dulwich import objects +from subprocess import Popen, PIPE +from rhodecode.lib.vcs.conf import settings +from rhodecode.lib.vcs.exceptions import RepositoryError +from rhodecode.lib.vcs.exceptions import ChangesetError +from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError +from rhodecode.lib.vcs.exceptions import VCSError +from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError +from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError +from rhodecode.lib.vcs.backends.base import BaseChangeset +from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, RemovedFileNode +from rhodecode.lib.vcs.utils import safe_unicode +from rhodecode.lib.vcs.utils import date_fromtimestamp +from rhodecode.lib.vcs.utils.lazy import LazyProperty + + +class GitChangeset(BaseChangeset): + """ + Represents state of the repository at single revision. + """ + + def __init__(self, repository, revision): + self._stat_modes = {} + self.repository = repository + self.raw_id = revision + self.revision = repository.revisions.index(revision) + + self.short_id = self.raw_id[:12] + self.id = self.raw_id + try: + commit = self.repository._repo.get_object(self.raw_id) + except KeyError: + raise RepositoryError("Cannot get object with id %s" % self.raw_id) + self._commit = commit + self._tree_id = commit.tree + + try: + self.message = safe_unicode(commit.message[:-1]) + # Always strip last eol + except UnicodeDecodeError: + self.message = commit.message[:-1].decode(commit.encoding + or 'utf-8') + #self.branch = None + self.tags = [] + #tree = self.repository.get_object(self._tree_id) + self.nodes = {} + self._paths = {} + + @LazyProperty + def author(self): + return safe_unicode(self._commit.committer) + + @LazyProperty + def date(self): + return date_fromtimestamp(self._commit.commit_time, + self._commit.commit_timezone) + + @LazyProperty + def status(self): + """ + Returns modified, added, removed, deleted files for current changeset + """ + return self.changed, self.added, self.removed + + @LazyProperty + def branch(self): + # TODO: Cache as we walk (id <-> branch name mapping) + refs = self.repository._repo.get_refs() + heads = [(key[len('refs/heads/'):], val) for key, val in refs.items() + if key.startswith('refs/heads/')] + + for name, id in heads: + walker = self.repository._repo.object_store.get_graph_walker([id]) + while True: + id = walker.next() + if not id: + break + if id == self.id: + return safe_unicode(name) + raise ChangesetError("This should not happen... Have you manually " + "change id of the changeset?") + + def _fix_path(self, path): + """ + Paths are stored without trailing slash so we need to get rid off it if + needed. + """ + if path.endswith('/'): + path = path.rstrip('/') + return path + + def _get_id_for_path(self, path): + # FIXME: Please, spare a couple of minutes and make those codes cleaner; + if not path in self._paths: + path = path.strip('/') + # set root tree + tree = self.repository._repo[self._commit.tree] + if path == '': + self._paths[''] = tree.id + return tree.id + splitted = path.split('/') + dirs, name = splitted[:-1], splitted[-1] + curdir = '' + for dir in dirs: + if curdir: + curdir = '/'.join((curdir, dir)) + else: + curdir = dir + #if curdir in self._paths: + ## This path have been already traversed + ## Update tree and continue + #tree = self.repository._repo[self._paths[curdir]] + #continue + dir_id = None + for item, stat, id in tree.iteritems(): + if curdir: + item_path = '/'.join((curdir, item)) + else: + item_path = item + self._paths[item_path] = id + self._stat_modes[item_path] = stat + if dir == item: + dir_id = id + if dir_id: + # Update tree + tree = self.repository._repo[dir_id] + if not isinstance(tree, objects.Tree): + raise ChangesetError('%s is not a directory' % curdir) + else: + raise ChangesetError('%s have not been found' % curdir) + for item, stat, id in tree.iteritems(): + if curdir: + name = '/'.join((curdir, item)) + else: + name = item + self._paths[name] = id + self._stat_modes[name] = stat + if not path in self._paths: + raise NodeDoesNotExistError("There is no file nor directory " + "at the given path %r at revision %r" + % (path, self.short_id)) + return self._paths[path] + + def _get_kind(self, path): + id = self._get_id_for_path(path) + obj = self.repository._repo[id] + if isinstance(obj, objects.Blob): + return NodeKind.FILE + elif isinstance(obj, objects.Tree): + return NodeKind.DIR + + def _get_file_nodes(self): + return chain(*(t[2] for t in self.walk())) + + @LazyProperty + def parents(self): + """ + Returns list of parents changesets. + """ + return [self.repository.get_changeset(parent) + for parent in self._commit.parents] + + def next(self, branch=None): + + if branch and self.branch != branch: + raise VCSError('Branch option used on changeset not belonging ' + 'to that branch') + + def _next(changeset, branch): + try: + next_ = changeset.revision + 1 + next_rev = changeset.repository.revisions[next_] + except IndexError: + raise ChangesetDoesNotExistError + cs = changeset.repository.get_changeset(next_rev) + + if branch and branch != cs.branch: + return _next(cs, branch) + + return cs + + return _next(self, branch) + + def prev(self, branch=None): + if branch and self.branch != branch: + raise VCSError('Branch option used on changeset not belonging ' + 'to that branch') + + def _prev(changeset, branch): + try: + prev_ = changeset.revision - 1 + if prev_ < 0: + raise IndexError + prev_rev = changeset.repository.revisions[prev_] + except IndexError: + raise ChangesetDoesNotExistError + + cs = changeset.repository.get_changeset(prev_rev) + + if branch and branch != cs.branch: + return _prev(cs, branch) + + return cs + + return _prev(self, branch) + + def get_file_mode(self, path): + """ + Returns stat mode of the file at the given ``path``. + """ + # ensure path is traversed + self._get_id_for_path(path) + return self._stat_modes[path] + + def get_file_content(self, path): + """ + Returns content of the file at given ``path``. + """ + id = self._get_id_for_path(path) + blob = self.repository._repo[id] + return blob.as_pretty_string() + + def get_file_size(self, path): + """ + Returns size of the file at given ``path``. + """ + id = self._get_id_for_path(path) + blob = self.repository._repo[id] + return blob.raw_length() + + def get_file_changeset(self, path): + """ + Returns last commit of the file at the given ``path``. + """ + node = self.get_node(path) + return node.history[0] + + def get_file_history(self, path): + """ + Returns history of file as reversed list of ``Changeset`` objects for + which file at given ``path`` has been modified. + + TODO: This function now uses os underlying 'git' and 'grep' commands + which is generally not good. Should be replaced with algorithm + iterating commits. + """ + cmd = 'log --name-status -p %s -- "%s" | grep "^commit"' \ + % (self.id, path) + so, se = self.repository.run_git_command(cmd) + ids = re.findall(r'\w{40}', so) + return [self.repository.get_changeset(id) for id in ids] + + def get_file_annotate(self, path): + """ + Returns a list of three element tuples with lineno,changeset and line + + TODO: This function now uses os underlying 'git' command which is + generally not good. Should be replaced with algorithm iterating + commits. + """ + cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path) + # -l ==> outputs long shas (and we need all 40 characters) + # --root ==> doesn't put '^' character for bounderies + # -r sha ==> blames for the given revision + so, se = self.repository.run_git_command(cmd) + annotate = [] + for i, blame_line in enumerate(so.split('\n')[:-1]): + ln_no = i + 1 + id, line = re.split(r' \(.+?\) ', blame_line, 1) + annotate.append((ln_no, self.repository.get_changeset(id), line)) + return annotate + + def fill_archive(self, stream=None, kind='tgz', prefix=None, + subrepos=False): + """ + Fills up given stream. + + :param stream: file like object. + :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``. + Default: ``tgz``. + :param prefix: name of root directory in archive. + Default is repository name and changeset's raw_id joined with dash + (``repo-tip.``). + :param subrepos: include subrepos in this archive. + + :raise ImproperArchiveTypeError: If given kind is wrong. + :raise VcsError: If given stream is None + + """ + allowed_kinds = settings.ARCHIVE_SPECS.keys() + if kind not in allowed_kinds: + raise ImproperArchiveTypeError('Archive kind not supported use one' + 'of %s', allowed_kinds) + + if prefix is None: + prefix = '%s-%s' % (self.repository.name, self.short_id) + elif prefix.startswith('/'): + raise VCSError("Prefix cannot start with leading slash") + elif prefix.strip() == '': + raise VCSError("Prefix cannot be empty") + + if kind == 'zip': + frmt = 'zip' + else: + frmt = 'tar' + cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix, + self.raw_id) + if kind == 'tgz': + cmd += ' | gzip -9' + elif kind == 'tbz2': + cmd += ' | bzip2 -9' + + if stream is None: + raise VCSError('You need to pass in a valid stream for filling' + ' with archival data') + popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True, + cwd=self.repository.path) + + buffer_size = 1024 * 8 + chunk = popen.stdout.read(buffer_size) + while chunk: + stream.write(chunk) + chunk = popen.stdout.read(buffer_size) + # Make sure all descriptors would be read + popen.communicate() + + def get_nodes(self, path): + if self._get_kind(path) != NodeKind.DIR: + raise ChangesetError("Directory does not exist for revision %r at " + " %r" % (self.revision, path)) + path = self._fix_path(path) + id = self._get_id_for_path(path) + tree = self.repository._repo[id] + dirnodes = [] + filenodes = [] + for name, stat, id in tree.iteritems(): + obj = self.repository._repo.get_object(id) + if path != '': + obj_path = '/'.join((path, name)) + else: + obj_path = name + if obj_path not in self._stat_modes: + self._stat_modes[obj_path] = stat + if isinstance(obj, objects.Tree): + dirnodes.append(DirNode(obj_path, changeset=self)) + elif isinstance(obj, objects.Blob): + filenodes.append(FileNode(obj_path, changeset=self, mode=stat)) + else: + raise ChangesetError("Requested object should be Tree " + "or Blob, is %r" % type(obj)) + nodes = dirnodes + filenodes + for node in nodes: + if not node.path in self.nodes: + self.nodes[node.path] = node + nodes.sort() + return nodes + + def get_node(self, path): + if isinstance(path, unicode): + path = path.encode('utf-8') + path = self._fix_path(path) + if not path in self.nodes: + try: + id = self._get_id_for_path(path) + except ChangesetError: + raise NodeDoesNotExistError("Cannot find one of parents' " + "directories for a given path: %s" % path) + obj = self.repository._repo.get_object(id) + if isinstance(obj, objects.Tree): + if path == '': + node = RootNode(changeset=self) + else: + node = DirNode(path, changeset=self) + node._tree = obj + elif isinstance(obj, objects.Blob): + node = FileNode(path, changeset=self) + node._blob = obj + else: + raise NodeDoesNotExistError("There is no file nor directory " + "at the given path %r at revision %r" + % (path, self.short_id)) + # cache node + self.nodes[path] = node + return self.nodes[path] + + @LazyProperty + def affected_files(self): + """ + Get's a fast accessible file changes for given changeset + """ + + return self.added + self.changed + + @LazyProperty + def _diff_name_status(self): + output = [] + for parent in self.parents: + cmd = 'diff --name-status %s %s' % (parent.raw_id, self.raw_id) + so, se = self.repository.run_git_command(cmd) + output.append(so.strip()) + return '\n'.join(output) + + def _get_paths_for_status(self, status): + """ + Returns sorted list of paths for given ``status``. + + :param status: one of: *added*, *modified* or *deleted* + """ + paths = set() + char = status[0].upper() + for line in self._diff_name_status.splitlines(): + if not line: + continue + if line.startswith(char): + splitted = line.split(char,1) + if not len(splitted) == 2: + raise VCSError("Couldn't parse diff result:\n%s\n\n and " + "particularly that line: %s" % (self._diff_name_status, + line)) + paths.add(splitted[1].strip()) + return sorted(paths) + + @LazyProperty + def added(self): + """ + Returns list of added ``FileNode`` objects. + """ + if not self.parents: + return list(self._get_file_nodes()) + return [self.get_node(path) for path in self._get_paths_for_status('added')] + + @LazyProperty + def changed(self): + """ + Returns list of modified ``FileNode`` objects. + """ + if not self.parents: + return [] + return [self.get_node(path) for path in self._get_paths_for_status('modified')] + + @LazyProperty + def removed(self): + """ + Returns list of removed ``FileNode`` objects. + """ + if not self.parents: + return [] + return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')] diff --git a/rhodecode/lib/vcs/backends/git/config.py b/rhodecode/lib/vcs/backends/git/config.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/backends/git/config.py @@ -0,0 +1,347 @@ +# config.py - Reading and writing Git config files +# Copyright (C) 2011 Jelmer Vernooij +# +# 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 option) a later version. +# +# 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. + +"""Reading and writing Git configuration files. + +TODO: + * preserve formatting when updating configuration files + * treat subsection names as case-insensitive for [branch.foo] style + subsections +""" + +# Taken from dulwich not yet released 0.8.3 version (until it is actually +# released) + +import errno +import os +import re + +from dulwich.file import GitFile + + +class Config(object): + """A Git configuration.""" + + def get(self, section, name): + """Retrieve the contents of a configuration setting. + + :param section: Tuple with section name and optional subsection namee + :param subsection: Subsection name + :return: Contents of the setting + :raise KeyError: if the value is not set + """ + raise NotImplementedError(self.get) + + def get_boolean(self, section, name, default=None): + """Retrieve a configuration setting as boolean. + + :param section: Tuple with section name and optional subsection namee + :param name: Name of the setting, including section and possible + subsection. + :return: Contents of the setting + :raise KeyError: if the value is not set + """ + try: + value = self.get(section, name) + except KeyError: + return default + if value.lower() == "true": + return True + elif value.lower() == "false": + return False + raise ValueError("not a valid boolean string: %r" % value) + + def set(self, section, name, value): + """Set a configuration value. + + :param name: Name of the configuration value, including section + and optional subsection + :param: Value of the setting + """ + raise NotImplementedError(self.set) + + +class ConfigDict(Config): + """Git configuration stored in a dictionary.""" + + def __init__(self, values=None): + """Create a new ConfigDict.""" + if values is None: + values = {} + self._values = values + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self._values) + + def __eq__(self, other): + return ( + isinstance(other, self.__class__) and + other._values == self._values) + + @classmethod + def _parse_setting(cls, name): + parts = name.split(".") + if len(parts) == 3: + return (parts[0], parts[1], parts[2]) + else: + return (parts[0], None, parts[1]) + + def get(self, section, name): + if isinstance(section, basestring): + section = (section, ) + if len(section) > 1: + try: + return self._values[section][name] + except KeyError: + pass + return self._values[(section[0],)][name] + + def set(self, section, name, value): + if isinstance(section, basestring): + section = (section, ) + self._values.setdefault(section, {})[name] = value + + +def _format_string(value): + if (value.startswith(" ") or + value.startswith("\t") or + value.endswith(" ") or + value.endswith("\t")): + return '"%s"' % _escape_value(value) + return _escape_value(value) + + +def _parse_string(value): + value = value.strip() + ret = [] + block = [] + in_quotes = False + for c in value: + if c == "\"": + in_quotes = (not in_quotes) + ret.append(_unescape_value("".join(block))) + block = [] + elif c in ("#", ";") and not in_quotes: + # the rest of the line is a comment + break + else: + block.append(c) + + if in_quotes: + raise ValueError("value starts with quote but lacks end quote") + + ret.append(_unescape_value("".join(block)).rstrip()) + + return "".join(ret) + + +def _unescape_value(value): + """Unescape a value.""" + def unescape(c): + return { + "\\\\": "\\", + "\\\"": "\"", + "\\n": "\n", + "\\t": "\t", + "\\b": "\b", + }[c.group(0)] + return re.sub(r"(\\.)", unescape, value) + + +def _escape_value(value): + """Escape a value.""" + return value.replace("\\", "\\\\").replace("\n", "\\n")\ + .replace("\t", "\\t").replace("\"", "\\\"") + + +def _check_variable_name(name): + for c in name: + if not c.isalnum() and c != '-': + return False + return True + + +def _check_section_name(name): + for c in name: + if not c.isalnum() and c not in ('-', '.'): + return False + return True + + +def _strip_comments(line): + line = line.split("#")[0] + line = line.split(";")[0] + return line + + +class ConfigFile(ConfigDict): + """A Git configuration file, like .git/config or ~/.gitconfig. + """ + + @classmethod + def from_file(cls, f): + """Read configuration from a file-like object.""" + ret = cls() + section = None + setting = None + for lineno, line in enumerate(f.readlines()): + line = line.lstrip() + if setting is None: + if _strip_comments(line).strip() == "": + continue + if line[0] == "[": + line = _strip_comments(line).rstrip() + if line[-1] != "]": + raise ValueError("expected trailing ]") + key = line.strip() + pts = key[1:-1].split(" ", 1) + pts[0] = pts[0].lower() + if len(pts) == 2: + if pts[1][0] != "\"" or pts[1][-1] != "\"": + raise ValueError( + "Invalid subsection " + pts[1]) + else: + pts[1] = pts[1][1:-1] + if not _check_section_name(pts[0]): + raise ValueError("invalid section name %s" % + pts[0]) + section = (pts[0], pts[1]) + else: + if not _check_section_name(pts[0]): + raise ValueError("invalid section name %s" % + pts[0]) + pts = pts[0].split(".", 1) + if len(pts) == 2: + section = (pts[0], pts[1]) + else: + section = (pts[0], ) + ret._values[section] = {} + else: + if section is None: + raise ValueError("setting %r without section" % line) + try: + setting, value = line.split("=", 1) + except ValueError: + setting = line + value = "true" + setting = setting.strip().lower() + if not _check_variable_name(setting): + raise ValueError("invalid variable name %s" % setting) + if value.endswith("\\\n"): + value = value[:-2] + continuation = True + else: + continuation = False + value = _parse_string(value) + ret._values[section][setting] = value + if not continuation: + setting = None + else: # continuation line + if line.endswith("\\\n"): + line = line[:-2] + continuation = True + else: + continuation = False + value = _parse_string(line) + ret._values[section][setting] += value + if not continuation: + setting = None + return ret + + @classmethod + def from_path(cls, path): + """Read configuration from a file on disk.""" + f = GitFile(path, 'rb') + try: + ret = cls.from_file(f) + ret.path = path + return ret + finally: + f.close() + + def write_to_path(self, path=None): + """Write configuration to a file on disk.""" + if path is None: + path = self.path + f = GitFile(path, 'wb') + try: + self.write_to_file(f) + finally: + f.close() + + def write_to_file(self, f): + """Write configuration to a file-like object.""" + for section, values in self._values.iteritems(): + try: + section_name, subsection_name = section + except ValueError: + (section_name, ) = section + subsection_name = None + if subsection_name is None: + f.write("[%s]\n" % section_name) + else: + f.write("[%s \"%s\"]\n" % (section_name, subsection_name)) + for key, value in values.iteritems(): + f.write("%s = %s\n" % (key, _escape_value(value))) + + +class StackedConfig(Config): + """Configuration which reads from multiple config files..""" + + def __init__(self, backends, writable=None): + self.backends = backends + self.writable = writable + + def __repr__(self): + return "<%s for %r>" % (self.__class__.__name__, self.backends) + + @classmethod + def default_backends(cls): + """Retrieve the default configuration. + + This will look in the repository configuration (if for_path is + specified), the users' home directory and the system + configuration. + """ + paths = [] + paths.append(os.path.expanduser("~/.gitconfig")) + paths.append("/etc/gitconfig") + backends = [] + for path in paths: + try: + cf = ConfigFile.from_path(path) + except (IOError, OSError), e: + if e.errno != errno.ENOENT: + raise + else: + continue + backends.append(cf) + return backends + + def get(self, section, name): + for backend in self.backends: + try: + return backend.get(section, name) + except KeyError: + pass + raise KeyError(name) + + def set(self, section, name, value): + if self.writable is None: + raise NotImplementedError(self.set) + return self.writable.set(section, name, value) diff --git a/rhodecode/lib/vcs/backends/git/inmemory.py b/rhodecode/lib/vcs/backends/git/inmemory.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/backends/git/inmemory.py @@ -0,0 +1,192 @@ +import time +import datetime +import posixpath +from dulwich import objects +from dulwich.repo import Repo +from rhodecode.lib.vcs.backends.base import BaseInMemoryChangeset +from rhodecode.lib.vcs.exceptions import RepositoryError + + +class GitInMemoryChangeset(BaseInMemoryChangeset): + + def commit(self, message, author, parents=None, branch=None, date=None, + **kwargs): + """ + Performs in-memory commit (doesn't check workdir in any way) and + returns newly created ``Changeset``. Updates repository's + ``revisions``. + + :param message: message of the commit + :param author: full username, i.e. "Joe Doe " + :param parents: single parent or sequence of parents from which commit + would be derieved + :param date: ``datetime.datetime`` instance. Defaults to + ``datetime.datetime.now()``. + :param branch: branch name, as string. If none given, default backend's + branch would be used. + + :raises ``CommitError``: if any error occurs while committing + """ + self.check_integrity(parents) + + from .repository import GitRepository + if branch is None: + branch = GitRepository.DEFAULT_BRANCH_NAME + + repo = self.repository._repo + object_store = repo.object_store + + ENCODING = "UTF-8" + DIRMOD = 040000 + + # Create tree and populates it with blobs + commit_tree = self.parents[0] and repo[self.parents[0]._commit.tree] or\ + objects.Tree() + for node in self.added + self.changed: + # Compute subdirs if needed + dirpath, nodename = posixpath.split(node.path) + dirnames = dirpath and dirpath.split('/') or [] + parent = commit_tree + ancestors = [('', parent)] + + # Tries to dig for the deepest existing tree + while dirnames: + curdir = dirnames.pop(0) + try: + dir_id = parent[curdir][1] + except KeyError: + # put curdir back into dirnames and stops + dirnames.insert(0, curdir) + break + else: + # If found, updates parent + parent = self.repository._repo[dir_id] + ancestors.append((curdir, parent)) + # Now parent is deepest exising tree and we need to create subtrees + # for dirnames (in reverse order) [this only applies for nodes from added] + new_trees = [] + blob = objects.Blob.from_string(node.content.encode(ENCODING)) + node_path = node.name.encode(ENCODING) + if dirnames: + # If there are trees which should be created we need to build + # them now (in reverse order) + reversed_dirnames = list(reversed(dirnames)) + curtree = objects.Tree() + curtree[node_path] = node.mode, blob.id + new_trees.append(curtree) + for dirname in reversed_dirnames[:-1]: + newtree = objects.Tree() + #newtree.add(DIRMOD, dirname, curtree.id) + newtree[dirname] = DIRMOD, curtree.id + new_trees.append(newtree) + curtree = newtree + parent[reversed_dirnames[-1]] = DIRMOD, curtree.id + else: + parent.add(node.mode, node_path, blob.id) + new_trees.append(parent) + # Update ancestors + for parent, tree, path in reversed([(a[1], b[1], b[0]) for a, b in + zip(ancestors, ancestors[1:])]): + parent[path] = DIRMOD, tree.id + object_store.add_object(tree) + + object_store.add_object(blob) + for tree in new_trees: + object_store.add_object(tree) + for node in self.removed: + paths = node.path.split('/') + tree = commit_tree + trees = [tree] + # Traverse deep into the forest... + for path in paths: + try: + obj = self.repository._repo[tree[path][1]] + if isinstance(obj, objects.Tree): + trees.append(obj) + tree = obj + except KeyError: + break + # Cut down the blob and all rotten trees on the way back... + for path, tree in reversed(zip(paths, trees)): + del tree[path] + if tree: + # This tree still has elements - don't remove it or any + # of it's parents + break + + object_store.add_object(commit_tree) + + # Create commit + commit = objects.Commit() + commit.tree = commit_tree.id + commit.parents = [p._commit.id for p in self.parents if p] + commit.author = commit.committer = author + commit.encoding = ENCODING + commit.message = message + ' ' + + # Compute date + if date is None: + date = time.time() + elif isinstance(date, datetime.datetime): + date = time.mktime(date.timetuple()) + + author_time = kwargs.pop('author_time', date) + commit.commit_time = int(date) + commit.author_time = int(author_time) + tz = time.timezone + author_tz = kwargs.pop('author_timezone', tz) + commit.commit_timezone = tz + commit.author_timezone = author_tz + + object_store.add_object(commit) + + ref = 'refs/heads/%s' % branch + repo.refs[ref] = commit.id + repo.refs.set_symbolic_ref('HEAD', ref) + + # Update vcs repository object & recreate dulwich repo + self.repository.revisions.append(commit.id) + self.repository._repo = Repo(self.repository.path) + tip = self.repository.get_changeset() + self.reset() + return tip + + def _get_missing_trees(self, path, root_tree): + """ + Creates missing ``Tree`` objects for the given path. + + :param path: path given as a string. It may be a path to a file node + (i.e. ``foo/bar/baz.txt``) or directory path - in that case it must + end with slash (i.e. ``foo/bar/``). + :param root_tree: ``dulwich.objects.Tree`` object from which we start + traversing (should be commit's root tree) + """ + dirpath = posixpath.split(path)[0] + dirs = dirpath.split('/') + if not dirs or dirs == ['']: + return [] + + def get_tree_for_dir(tree, dirname): + for name, mode, id in tree.iteritems(): + if name == dirname: + obj = self.repository._repo[id] + if isinstance(obj, objects.Tree): + return obj + else: + raise RepositoryError("Cannot create directory %s " + "at tree %s as path is occupied and is not a " + "Tree" % (dirname, tree)) + return None + + trees = [] + parent = root_tree + for dirname in dirs: + tree = get_tree_for_dir(parent, dirname) + if tree is None: + tree = objects.Tree() + dirmode = 040000 + parent.add(dirmode, dirname, tree.id) + parent = tree + # Always append tree + trees.append(tree) + return trees diff --git a/rhodecode/lib/vcs/backends/git/repository.py b/rhodecode/lib/vcs/backends/git/repository.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/backends/git/repository.py @@ -0,0 +1,508 @@ +# -*- coding: utf-8 -*- +""" + vcs.backends.git + ~~~~~~~~~~~~~~~~ + + Git backend implementation. + + :created_on: Apr 8, 2010 + :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak. +""" + +import os +import re +import time +import posixpath +from dulwich.repo import Repo, NotGitRepository +#from dulwich.config import ConfigFile +from string import Template +from subprocess import Popen, PIPE +from rhodecode.lib.vcs.backends.base import BaseRepository +from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError +from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError +from rhodecode.lib.vcs.exceptions import EmptyRepositoryError +from rhodecode.lib.vcs.exceptions import RepositoryError +from rhodecode.lib.vcs.exceptions import TagAlreadyExistError +from rhodecode.lib.vcs.exceptions import TagDoesNotExistError +from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp +from rhodecode.lib.vcs.utils.lazy import LazyProperty +from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict +from rhodecode.lib.vcs.utils.paths import abspath +from rhodecode.lib.vcs.utils.paths import get_user_home +from .workdir import GitWorkdir +from .changeset import GitChangeset +from .inmemory import GitInMemoryChangeset +from .config import ConfigFile + + +class GitRepository(BaseRepository): + """ + Git repository backend. + """ + DEFAULT_BRANCH_NAME = 'master' + scm = 'git' + + def __init__(self, repo_path, create=False, src_url=None, + update_after_clone=False, bare=False): + + self.path = abspath(repo_path) + self._repo = self._get_repo(create, src_url, update_after_clone, bare) + try: + self.head = self._repo.head() + except KeyError: + self.head = None + + self._config_files = [ + bare and abspath(self.path, 'config') or abspath(self.path, '.git', + 'config'), + abspath(get_user_home(), '.gitconfig'), + ] + + @LazyProperty + def revisions(self): + """ + Returns list of revisions' ids, in ascending order. Being lazy + attribute allows external tools to inject shas from cache. + """ + return self._get_all_revisions() + + def run_git_command(self, cmd): + """ + Runs given ``cmd`` as git command and returns tuple + (returncode, stdout, stderr). + + .. note:: + This method exists only until log/blame functionality is implemented + at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing + os command's output is road to hell... + + :param cmd: git command to be executed + """ + #cmd = '(cd %s && git %s)' % (self.path, cmd) + if isinstance(cmd, basestring): + cmd = 'git %s' % cmd + else: + cmd = ['git'] + cmd + try: + opts = dict( + shell=isinstance(cmd, basestring), + stdout=PIPE, + stderr=PIPE) + if os.path.isdir(self.path): + opts['cwd'] = self.path + p = Popen(cmd, **opts) + except OSError, err: + raise RepositoryError("Couldn't run git command (%s).\n" + "Original error was:%s" % (cmd, err)) + so, se = p.communicate() + if not se.startswith("fatal: bad default revision 'HEAD'") and \ + p.returncode != 0: + raise RepositoryError("Couldn't run git command (%s).\n" + "stderr:\n%s" % (cmd, se)) + return so, se + + def _check_url(self, url): + """ + Functon will check given url and try to verify if it's a valid + link. Sometimes it may happened that mercurial will issue basic + auth request that can cause whole API to hang when used from python + or other external calls. + + On failures it'll raise urllib2.HTTPError + """ + + #TODO: implement this + pass + + def _get_repo(self, create, src_url=None, update_after_clone=False, + bare=False): + if create and os.path.exists(self.path): + raise RepositoryError("Location already exist") + if src_url and not create: + raise RepositoryError("Create should be set to True if src_url is " + "given (clone operation creates repository)") + try: + if create and src_url: + self._check_url(src_url) + self.clone(src_url, update_after_clone, bare) + return Repo(self.path) + elif create: + os.mkdir(self.path) + if bare: + return Repo.init_bare(self.path) + else: + return Repo.init(self.path) + else: + return Repo(self.path) + except (NotGitRepository, OSError), err: + raise RepositoryError(err) + + def _get_all_revisions(self): + cmd = 'rev-list --all --date-order' + try: + so, se = self.run_git_command(cmd) + except RepositoryError: + # Can be raised for empty repositories + return [] + revisions = so.splitlines() + revisions.reverse() + return revisions + + def _get_revision(self, revision): + """ + For git backend we always return integer here. This way we ensure + that changset's revision attribute would become integer. + """ + pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$') + is_bstr = lambda o: isinstance(o, (str, unicode)) + is_null = lambda o: len(o) == revision.count('0') + + if len(self.revisions) == 0: + raise EmptyRepositoryError("There are no changesets yet") + + if revision in (None, '', 'tip', 'HEAD', 'head', -1): + revision = self.revisions[-1] + + if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12) + or isinstance(revision, int) or is_null(revision)): + try: + revision = self.revisions[int(revision)] + except: + raise ChangesetDoesNotExistError("Revision %r does not exist " + "for this repository %s" % (revision, self)) + + elif is_bstr(revision): + if not pattern.match(revision) or revision not in self.revisions: + raise ChangesetDoesNotExistError("Revision %r does not exist " + "for this repository %s" % (revision, self)) + + # Ensure we return full id + if not pattern.match(str(revision)): + raise ChangesetDoesNotExistError("Given revision %r not recognized" + % revision) + return revision + + def _get_archives(self, archive_name='tip'): + + for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]: + yield {"type": i[0], "extension": i[1], "node": archive_name} + + def _get_url(self, url): + """ + Returns normalized url. If schema is not given, would fall to + filesystem (``file:///``) schema. + """ + url = str(url) + if url != 'default' and not '://' in url: + url = ':///'.join(('file', url)) + return url + + @LazyProperty + def name(self): + return os.path.basename(self.path) + + @LazyProperty + def last_change(self): + """ + Returns last change made on this repository as datetime object + """ + return date_fromtimestamp(self._get_mtime(), makedate()[1]) + + def _get_mtime(self): + try: + return time.mktime(self.get_changeset().date.timetuple()) + except RepositoryError: + # fallback to filesystem + in_path = os.path.join(self.path, '.git', "index") + he_path = os.path.join(self.path, '.git', "HEAD") + if os.path.exists(in_path): + return os.stat(in_path).st_mtime + else: + return os.stat(he_path).st_mtime + + @LazyProperty + def description(self): + undefined_description = u'unknown' + description_path = os.path.join(self.path, '.git', 'description') + if os.path.isfile(description_path): + return safe_unicode(open(description_path).read()) + else: + return undefined_description + + @LazyProperty + def contact(self): + undefined_contact = u'Unknown' + return undefined_contact + + @property + def branches(self): + if not self.revisions: + return {} + refs = self._repo.refs.as_dict() + sortkey = lambda ctx: ctx[0] + _branches = [('/'.join(ref.split('/')[2:]), head) + for ref, head in refs.items() + if ref.startswith('refs/heads/') or + ref.startswith('refs/remotes/') and not ref.endswith('/HEAD')] + return OrderedDict(sorted(_branches, key=sortkey, reverse=False)) + + def _get_tags(self): + if not self.revisions: + return {} + sortkey = lambda ctx: ctx[0] + _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in + self._repo.get_refs().items() if ref.startswith('refs/tags/')] + return OrderedDict(sorted(_tags, key=sortkey, reverse=True)) + + @LazyProperty + def tags(self): + return self._get_tags() + + def tag(self, name, user, revision=None, message=None, date=None, + **kwargs): + """ + Creates and returns a tag for the given ``revision``. + + :param name: name for new tag + :param user: full username, i.e.: "Joe Doe " + :param revision: changeset id for which new tag would be created + :param message: message of the tag's commit + :param date: date of tag's commit + + :raises TagAlreadyExistError: if tag with same name already exists + """ + if name in self.tags: + raise TagAlreadyExistError("Tag %s already exists" % name) + changeset = self.get_changeset(revision) + message = message or "Added tag %s for commit %s" % (name, + changeset.raw_id) + self._repo.refs["refs/tags/%s" % name] = changeset._commit.id + + self.tags = self._get_tags() + return changeset + + def remove_tag(self, name, user, message=None, date=None): + """ + Removes tag with the given ``name``. + + :param name: name of the tag to be removed + :param user: full username, i.e.: "Joe Doe " + :param message: message of the tag's removal commit + :param date: date of tag's removal commit + + :raises TagDoesNotExistError: if tag with given name does not exists + """ + if name not in self.tags: + raise TagDoesNotExistError("Tag %s does not exist" % name) + tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name) + try: + os.remove(tagpath) + self.tags = self._get_tags() + except OSError, e: + raise RepositoryError(e.strerror) + + def get_changeset(self, revision=None): + """ + Returns ``GitChangeset`` object representing commit from git repository + at the given revision or head (most recent commit) if None given. + """ + if isinstance(revision, GitChangeset): + return revision + revision = self._get_revision(revision) + changeset = GitChangeset(repository=self, revision=revision) + return changeset + + def get_changesets(self, start=None, end=None, start_date=None, + end_date=None, branch_name=None, reverse=False): + """ + Returns iterator of ``GitChangeset`` objects from start to end (both + are inclusive), in ascending date order (unless ``reverse`` is set). + + :param start: changeset ID, as str; first returned changeset + :param end: changeset ID, as str; last returned changeset + :param start_date: if specified, changesets with commit date less than + ``start_date`` would be filtered out from returned set + :param end_date: if specified, changesets with commit date greater than + ``end_date`` would be filtered out from returned set + :param branch_name: if specified, changesets not reachable from given + branch would be filtered out from returned set + :param reverse: if ``True``, returned generator would be reversed + (meaning that returned changesets would have descending date order) + + :raise BranchDoesNotExistError: If given ``branch_name`` does not + exist. + :raise ChangesetDoesNotExistError: If changeset for given ``start`` or + ``end`` could not be found. + + """ + if branch_name and branch_name not in self.branches: + raise BranchDoesNotExistError("Branch '%s' not found" \ + % branch_name) + # %H at format means (full) commit hash, initial hashes are retrieved + # in ascending date order + cmd_template = 'log --date-order --reverse --pretty=format:"%H"' + cmd_params = {} + if start_date: + cmd_template += ' --since "$since"' + cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S') + if end_date: + cmd_template += ' --until "$until"' + cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S') + if branch_name: + cmd_template += ' $branch_name' + cmd_params['branch_name'] = branch_name + else: + cmd_template += ' --all' + + cmd = Template(cmd_template).safe_substitute(**cmd_params) + revs = self.run_git_command(cmd)[0].splitlines() + start_pos = 0 + end_pos = len(revs) + if start: + _start = self._get_revision(start) + try: + start_pos = revs.index(_start) + except ValueError: + pass + + if end is not None: + _end = self._get_revision(end) + try: + end_pos = revs.index(_end) + except ValueError: + pass + + if None not in [start, end] and start_pos > end_pos: + raise RepositoryError('start cannot be after end') + + if end_pos is not None: + end_pos += 1 + + revs = revs[start_pos:end_pos] + if reverse: + revs = reversed(revs) + for rev in revs: + yield self.get_changeset(rev) + + def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False, + context=3): + """ + Returns (git like) *diff*, as plain text. Shows changes introduced by + ``rev2`` since ``rev1``. + + :param rev1: Entry point from which diff is shown. Can be + ``self.EMPTY_CHANGESET`` - in this case, patch showing all + the changes since empty state of the repository until ``rev2`` + :param rev2: Until which revision changes should be shown. + :param ignore_whitespace: If set to ``True``, would not show whitespace + changes. Defaults to ``False``. + :param context: How many lines before/after changed lines should be + shown. Defaults to ``3``. + """ + flags = ['-U%s' % context] + if ignore_whitespace: + flags.append('-w') + + if rev1 == self.EMPTY_CHANGESET: + rev2 = self.get_changeset(rev2).raw_id + cmd = ' '.join(['show'] + flags + [rev2]) + else: + rev1 = self.get_changeset(rev1).raw_id + rev2 = self.get_changeset(rev2).raw_id + cmd = ' '.join(['diff'] + flags + [rev1, rev2]) + + if path: + cmd += ' -- "%s"' % path + stdout, stderr = self.run_git_command(cmd) + # If we used 'show' command, strip first few lines (until actual diff + # starts) + if rev1 == self.EMPTY_CHANGESET: + lines = stdout.splitlines() + x = 0 + for line in lines: + if line.startswith('diff'): + break + x += 1 + # Append new line just like 'diff' command do + stdout = '\n'.join(lines[x:]) + '\n' + return stdout + + @LazyProperty + def in_memory_changeset(self): + """ + Returns ``GitInMemoryChangeset`` object for this repository. + """ + return GitInMemoryChangeset(self) + + def clone(self, url, update_after_clone=True, bare=False): + """ + Tries to clone changes from external location. + + :param update_after_clone: If set to ``False``, git won't checkout + working directory + :param bare: If set to ``True``, repository would be cloned into + *bare* git repository (no working directory at all). + """ + url = self._get_url(url) + cmd = ['clone'] + if bare: + cmd.append('--bare') + elif not update_after_clone: + cmd.append('--no-checkout') + cmd += ['--', '"%s"' % url, '"%s"' % self.path] + cmd = ' '.join(cmd) + # If error occurs run_git_command raises RepositoryError already + self.run_git_command(cmd) + + @LazyProperty + def workdir(self): + """ + Returns ``Workdir`` instance for this repository. + """ + return GitWorkdir(self) + + def get_config_value(self, section, name, config_file=None): + """ + Returns configuration value for a given [``section``] and ``name``. + + :param section: Section we want to retrieve value from + :param name: Name of configuration we want to retrieve + :param config_file: A path to file which should be used to retrieve + configuration from (might also be a list of file paths) + """ + if config_file is None: + config_file = [] + elif isinstance(config_file, basestring): + config_file = [config_file] + + def gen_configs(): + for path in config_file + self._config_files: + try: + yield ConfigFile.from_path(path) + except (IOError, OSError, ValueError): + continue + + for config in gen_configs(): + try: + return config.get(section, name) + except KeyError: + continue + return None + + def get_user_name(self, config_file=None): + """ + Returns user's name from global configuration file. + + :param config_file: A path to file which should be used to retrieve + configuration from (might also be a list of file paths) + """ + return self.get_config_value('user', 'name', config_file) + + def get_user_email(self, config_file=None): + """ + Returns user's email from global configuration file. + + :param config_file: A path to file which should be used to retrieve + configuration from (might also be a list of file paths) + """ + return self.get_config_value('user', 'email', config_file) diff --git a/rhodecode/lib/vcs/backends/git/workdir.py b/rhodecode/lib/vcs/backends/git/workdir.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/backends/git/workdir.py @@ -0,0 +1,31 @@ +import re +from rhodecode.lib.vcs.backends.base import BaseWorkdir +from rhodecode.lib.vcs.exceptions import RepositoryError +from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError + + +class GitWorkdir(BaseWorkdir): + + def get_branch(self): + headpath = self.repository._repo.refs.refpath('HEAD') + try: + content = open(headpath).read() + match = re.match(r'^ref: refs/heads/(?P.+)\n$', content) + if match: + return match.groupdict()['branch'] + else: + raise RepositoryError("Couldn't compute workdir's branch") + except IOError: + # Try naive way... + raise RepositoryError("Couldn't compute workdir's branch") + + def get_changeset(self): + return self.repository.get_changeset( + self.repository._repo.refs.as_dict().get('HEAD')) + + def checkout_branch(self, branch=None): + if branch is None: + branch = self.repository.DEFAULT_BRANCH_NAME + if branch not in self.repository.branches: + raise BranchDoesNotExistError + self.repository.run_git_command(['checkout', branch]) diff --git a/rhodecode/lib/vcs/backends/hg/__init__.py b/rhodecode/lib/vcs/backends/hg/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/backends/hg/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +""" + vcs.backends.hg + ~~~~~~~~~~~~~~~~ + + Mercurial backend implementation. + + :created_on: Apr 8, 2010 + :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak. +""" + +from .repository import MercurialRepository +from .changeset import MercurialChangeset +from .inmemory import MercurialInMemoryChangeset +from .workdir import MercurialWorkdir + + +__all__ = [ + 'MercurialRepository', 'MercurialChangeset', + 'MercurialInMemoryChangeset', 'MercurialWorkdir', +] diff --git a/rhodecode/lib/vcs/backends/hg/changeset.py b/rhodecode/lib/vcs/backends/hg/changeset.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/backends/hg/changeset.py @@ -0,0 +1,338 @@ +import os +import posixpath + +from rhodecode.lib.vcs.backends.base import BaseChangeset +from rhodecode.lib.vcs.conf import settings +from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError, \ + ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError +from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, ChangedFileNodesGenerator, \ + DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode + +from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp +from rhodecode.lib.vcs.utils.lazy import LazyProperty +from rhodecode.lib.vcs.utils.paths import get_dirs_for_path + +from ...utils.hgcompat import archival, hex + + +class MercurialChangeset(BaseChangeset): + """ + Represents state of the repository at the single revision. + """ + + def __init__(self, repository, revision): + self.repository = repository + self.raw_id = revision + self._ctx = repository._repo[revision] + self.revision = self._ctx._rev + self.nodes = {} + + @LazyProperty + def tags(self): + return map(safe_unicode, self._ctx.tags()) + + @LazyProperty + def branch(self): + return safe_unicode(self._ctx.branch()) + + @LazyProperty + def message(self): + return safe_unicode(self._ctx.description()) + + @LazyProperty + def author(self): + return safe_unicode(self._ctx.user()) + + @LazyProperty + def date(self): + return date_fromtimestamp(*self._ctx.date()) + + @LazyProperty + def status(self): + """ + Returns modified, added, removed, deleted files for current changeset + """ + return self.repository._repo.status(self._ctx.p1().node(), + self._ctx.node()) + + @LazyProperty + def _file_paths(self): + return list(self._ctx) + + @LazyProperty + def _dir_paths(self): + p = list(set(get_dirs_for_path(*self._file_paths))) + p.insert(0, '') + return p + + @LazyProperty + def _paths(self): + return self._dir_paths + self._file_paths + + @LazyProperty + def id(self): + if self.last: + return u'tip' + return self.short_id + + @LazyProperty + def short_id(self): + return self.raw_id[:12] + + @LazyProperty + def parents(self): + """ + Returns list of parents changesets. + """ + return [self.repository.get_changeset(parent.rev()) + for parent in self._ctx.parents() if parent.rev() >= 0] + + def next(self, branch=None): + + if branch and self.branch != branch: + raise VCSError('Branch option used on changeset not belonging ' + 'to that branch') + + def _next(changeset, branch): + try: + next_ = changeset.revision + 1 + next_rev = changeset.repository.revisions[next_] + except IndexError: + raise ChangesetDoesNotExistError + cs = changeset.repository.get_changeset(next_rev) + + if branch and branch != cs.branch: + return _next(cs, branch) + + return cs + + return _next(self, branch) + + def prev(self, branch=None): + if branch and self.branch != branch: + raise VCSError('Branch option used on changeset not belonging ' + 'to that branch') + + def _prev(changeset, branch): + try: + prev_ = changeset.revision - 1 + if prev_ < 0: + raise IndexError + prev_rev = changeset.repository.revisions[prev_] + except IndexError: + raise ChangesetDoesNotExistError + + cs = changeset.repository.get_changeset(prev_rev) + + if branch and branch != cs.branch: + return _prev(cs, branch) + + return cs + + return _prev(self, branch) + + def _fix_path(self, path): + """ + Paths are stored without trailing slash so we need to get rid off it if + needed. Also mercurial keeps filenodes as str so we need to decode + from unicode to str + """ + if path.endswith('/'): + path = path.rstrip('/') + + return safe_str(path) + + def _get_kind(self, path): + path = self._fix_path(path) + if path in self._file_paths: + return NodeKind.FILE + elif path in self._dir_paths: + return NodeKind.DIR + else: + raise ChangesetError("Node does not exist at the given path %r" + % (path)) + + def _get_filectx(self, path): + path = self._fix_path(path) + if self._get_kind(path) != NodeKind.FILE: + raise ChangesetError("File does not exist for revision %r at " + " %r" % (self.revision, path)) + return self._ctx.filectx(path) + + def get_file_mode(self, path): + """ + Returns stat mode of the file at the given ``path``. + """ + fctx = self._get_filectx(path) + if 'x' in fctx.flags(): + return 0100755 + else: + return 0100644 + + def get_file_content(self, path): + """ + Returns content of the file at given ``path``. + """ + fctx = self._get_filectx(path) + return fctx.data() + + def get_file_size(self, path): + """ + Returns size of the file at given ``path``. + """ + fctx = self._get_filectx(path) + return fctx.size() + + def get_file_changeset(self, path): + """ + Returns last commit of the file at the given ``path``. + """ + fctx = self._get_filectx(path) + changeset = self.repository.get_changeset(fctx.linkrev()) + return changeset + + def get_file_history(self, path): + """ + Returns history of file as reversed list of ``Changeset`` objects for + which file at given ``path`` has been modified. + """ + fctx = self._get_filectx(path) + nodes = [fctx.filectx(x).node() for x in fctx.filelog()] + changesets = [self.repository.get_changeset(hex(node)) + for node in reversed(nodes)] + return changesets + + def get_file_annotate(self, path): + """ + Returns a list of three element tuples with lineno,changeset and line + """ + fctx = self._get_filectx(path) + annotate = [] + for i, annotate_data in enumerate(fctx.annotate()): + ln_no = i + 1 + annotate.append((ln_no, self.repository\ + .get_changeset(hex(annotate_data[0].node())), + annotate_data[1],)) + + return annotate + + def fill_archive(self, stream=None, kind='tgz', prefix=None, + subrepos=False): + """ + Fills up given stream. + + :param stream: file like object. + :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``. + Default: ``tgz``. + :param prefix: name of root directory in archive. + Default is repository name and changeset's raw_id joined with dash + (``repo-tip.``). + :param subrepos: include subrepos in this archive. + + :raise ImproperArchiveTypeError: If given kind is wrong. + :raise VcsError: If given stream is None + """ + + allowed_kinds = settings.ARCHIVE_SPECS.keys() + if kind not in allowed_kinds: + raise ImproperArchiveTypeError('Archive kind not supported use one' + 'of %s', allowed_kinds) + + if stream is None: + raise VCSError('You need to pass in a valid stream for filling' + ' with archival data') + + if prefix is None: + prefix = '%s-%s' % (self.repository.name, self.short_id) + elif prefix.startswith('/'): + raise VCSError("Prefix cannot start with leading slash") + elif prefix.strip() == '': + raise VCSError("Prefix cannot be empty") + + archival.archive(self.repository._repo, stream, self.raw_id, + kind, prefix=prefix, subrepos=subrepos) + + #stream.close() + + if stream.closed and hasattr(stream, 'name'): + stream = open(stream.name, 'rb') + elif hasattr(stream, 'mode') and 'r' not in stream.mode: + stream = open(stream.name, 'rb') + else: + stream.seek(0) + + def get_nodes(self, path): + """ + Returns combined ``DirNode`` and ``FileNode`` objects list representing + state of changeset at the given ``path``. If node at the given ``path`` + is not instance of ``DirNode``, ChangesetError would be raised. + """ + + if self._get_kind(path) != NodeKind.DIR: + raise ChangesetError("Directory does not exist for revision %r at " + " %r" % (self.revision, path)) + path = self._fix_path(path) + filenodes = [FileNode(f, changeset=self) for f in self._file_paths + if os.path.dirname(f) == path] + dirs = path == '' and '' or [d for d in self._dir_paths + if d and posixpath.dirname(d) == path] + dirnodes = [DirNode(d, changeset=self) for d in dirs + if os.path.dirname(d) == path] + nodes = dirnodes + filenodes + # cache nodes + for node in nodes: + self.nodes[node.path] = node + nodes.sort() + return nodes + + def get_node(self, path): + """ + Returns ``Node`` object from the given ``path``. If there is no node at + the given ``path``, ``ChangesetError`` would be raised. + """ + + path = self._fix_path(path) + + if not path in self.nodes: + if path in self._file_paths: + node = FileNode(path, changeset=self) + elif path in self._dir_paths or path in self._dir_paths: + if path == '': + node = RootNode(changeset=self) + else: + node = DirNode(path, changeset=self) + else: + raise NodeDoesNotExistError("There is no file nor directory " + "at the given path: %r at revision %r" + % (path, self.short_id)) + # cache node + self.nodes[path] = node + return self.nodes[path] + + @LazyProperty + def affected_files(self): + """ + Get's a fast accessible file changes for given changeset + """ + return self._ctx.files() + + @property + def added(self): + """ + Returns list of added ``FileNode`` objects. + """ + return AddedFileNodesGenerator([n for n in self.status[1]], self) + + @property + def changed(self): + """ + Returns list of modified ``FileNode`` objects. + """ + return ChangedFileNodesGenerator([n for n in self.status[0]], self) + + @property + def removed(self): + """ + Returns list of removed ``FileNode`` objects. + """ + return RemovedFileNodesGenerator([n for n in self.status[2]], self) diff --git a/rhodecode/lib/vcs/backends/hg/inmemory.py b/rhodecode/lib/vcs/backends/hg/inmemory.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/backends/hg/inmemory.py @@ -0,0 +1,110 @@ +import datetime +import errno + +from rhodecode.lib.vcs.backends.base import BaseInMemoryChangeset +from rhodecode.lib.vcs.exceptions import RepositoryError + +from ...utils.hgcompat import memfilectx, memctx, hex + + +class MercurialInMemoryChangeset(BaseInMemoryChangeset): + + def commit(self, message, author, parents=None, branch=None, date=None, + **kwargs): + """ + Performs in-memory commit (doesn't check workdir in any way) and + returns newly created ``Changeset``. Updates repository's + ``revisions``. + + :param message: message of the commit + :param author: full username, i.e. "Joe Doe " + :param parents: single parent or sequence of parents from which commit + would be derieved + :param date: ``datetime.datetime`` instance. Defaults to + ``datetime.datetime.now()``. + :param branch: branch name, as string. If none given, default backend's + branch would be used. + + :raises ``CommitError``: if any error occurs while committing + """ + self.check_integrity(parents) + + from .repository import MercurialRepository + if not isinstance(message, str) or not isinstance(author, str): + raise RepositoryError('Given message and author needs to be ' + 'an instance') + + if branch is None: + branch = MercurialRepository.DEFAULT_BRANCH_NAME + kwargs['branch'] = branch + + def filectxfn(_repo, memctx, path): + """ + Marks given path as added/changed/removed in a given _repo. This is + for internal mercurial commit function. + """ + + # check if this path is removed + if path in (node.path for node in self.removed): + # Raising exception is a way to mark node for removal + raise IOError(errno.ENOENT, '%s is deleted' % path) + + # check if this path is added + for node in self.added: + if node.path == path: + return memfilectx(path=node.path, + data=(node.content.encode('utf8') + if not node.is_binary else node.content), + islink=False, + isexec=node.is_executable, + copied=False) + + # or changed + for node in self.changed: + if node.path == path: + return memfilectx(path=node.path, + data=(node.content.encode('utf8') + if not node.is_binary else node.content), + islink=False, + isexec=node.is_executable, + copied=False) + + raise RepositoryError("Given path haven't been marked as added," + "changed or removed (%s)" % path) + + parents = [None, None] + for i, parent in enumerate(self.parents): + if parent is not None: + parents[i] = parent._ctx.node() + + if date and isinstance(date, datetime.datetime): + date = date.ctime() + + commit_ctx = memctx(repo=self.repository._repo, + parents=parents, + text='', + files=self.get_paths(), + filectxfn=filectxfn, + user=author, + date=date, + extra=kwargs) + + # injecting given _repo params + commit_ctx._text = message + commit_ctx._user = author + commit_ctx._date = date + + # TODO: Catch exceptions! + n = self.repository._repo.commitctx(commit_ctx) + # Returns mercurial node + self._commit_ctx = commit_ctx # For reference + # Update vcs repository object & recreate mercurial _repo + # new_ctx = self.repository._repo[node] + # new_tip = self.repository.get_changeset(new_ctx.hex()) + new_id = hex(n) + self.repository.revisions.append(new_id) + self._repo = self.repository._get_repo(create=False) + self.repository.branches = self.repository._get_branches() + tip = self.repository.get_changeset() + self.reset() + return tip diff --git a/rhodecode/lib/vcs/backends/hg/repository.py b/rhodecode/lib/vcs/backends/hg/repository.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/backends/hg/repository.py @@ -0,0 +1,521 @@ +import os +import time +import datetime +import urllib +import urllib2 + +from rhodecode.lib.vcs.backends.base import BaseRepository +from .workdir import MercurialWorkdir +from .changeset import MercurialChangeset +from .inmemory import MercurialInMemoryChangeset + +from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError, \ + ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, \ + VCSError, TagAlreadyExistError, TagDoesNotExistError +from rhodecode.lib.vcs.utils import author_email, author_name, date_fromtimestamp, \ + makedate, safe_unicode +from rhodecode.lib.vcs.utils.lazy import LazyProperty +from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict +from rhodecode.lib.vcs.utils.paths import abspath + +from ...utils.hgcompat import ui, nullid, match, patch, diffopts, clone, \ + get_contact, pull, localrepository, RepoLookupError, Abort, RepoError, hex + + +class MercurialRepository(BaseRepository): + """ + Mercurial repository backend + """ + DEFAULT_BRANCH_NAME = 'default' + scm = 'hg' + + def __init__(self, repo_path, create=False, baseui=None, src_url=None, + update_after_clone=False): + """ + Raises RepositoryError if repository could not be find at the given + ``repo_path``. + + :param repo_path: local path of the repository + :param create=False: if set to True, would try to create repository if + it does not exist rather than raising exception + :param baseui=None: user data + :param src_url=None: would try to clone repository from given location + :param update_after_clone=False: sets update of working copy after + making a clone + """ + + if not isinstance(repo_path, str): + raise VCSError('Mercurial backend requires repository path to ' + 'be instance of got %s instead' % + type(repo_path)) + + self.path = abspath(repo_path) + self.baseui = baseui or ui.ui() + # We've set path and ui, now we can set _repo itself + self._repo = self._get_repo(create, src_url, update_after_clone) + + @property + def _empty(self): + """ + Checks if repository is empty without any changesets + """ + # TODO: Following raises errors when using InMemoryChangeset... + # return len(self._repo.changelog) == 0 + return len(self.revisions) == 0 + + @LazyProperty + def revisions(self): + """ + Returns list of revisions' ids, in ascending order. Being lazy + attribute allows external tools to inject shas from cache. + """ + return self._get_all_revisions() + + @LazyProperty + def name(self): + return os.path.basename(self.path) + + @LazyProperty + def branches(self): + return self._get_branches() + + def _get_branches(self, closed=False): + """ + Get's branches for this repository + Returns only not closed branches by default + + :param closed: return also closed branches for mercurial + """ + + if self._empty: + return {} + + def _branchtags(localrepo): + """ + Patched version of mercurial branchtags to not return the closed + branches + + :param localrepo: locarepository instance + """ + + bt = {} + bt_closed = {} + for bn, heads in localrepo.branchmap().iteritems(): + tip = heads[-1] + if 'close' in localrepo.changelog.read(tip)[5]: + bt_closed[bn] = tip + else: + bt[bn] = tip + + if closed: + bt.update(bt_closed) + return bt + + sortkey = lambda ctx: ctx[0] # sort by name + _branches = [(safe_unicode(n), hex(h),) for n, h in + _branchtags(self._repo).items()] + + return OrderedDict(sorted(_branches, key=sortkey, reverse=False)) + + @LazyProperty + def tags(self): + """ + Get's tags for this repository + """ + return self._get_tags() + + def _get_tags(self): + if self._empty: + return {} + + sortkey = lambda ctx: ctx[0] # sort by name + _tags = [(safe_unicode(n), hex(h),) for n, h in + self._repo.tags().items()] + + return OrderedDict(sorted(_tags, key=sortkey, reverse=True)) + + def tag(self, name, user, revision=None, message=None, date=None, + **kwargs): + """ + Creates and returns a tag for the given ``revision``. + + :param name: name for new tag + :param user: full username, i.e.: "Joe Doe " + :param revision: changeset id for which new tag would be created + :param message: message of the tag's commit + :param date: date of tag's commit + + :raises TagAlreadyExistError: if tag with same name already exists + """ + if name in self.tags: + raise TagAlreadyExistError("Tag %s already exists" % name) + changeset = self.get_changeset(revision) + local = kwargs.setdefault('local', False) + + if message is None: + message = "Added tag %s for changeset %s" % (name, + changeset.short_id) + + if date is None: + date = datetime.datetime.now().ctime() + + try: + self._repo.tag(name, changeset._ctx.node(), message, local, user, + date) + except Abort, e: + raise RepositoryError(e.message) + + # Reinitialize tags + self.tags = self._get_tags() + tag_id = self.tags[name] + + return self.get_changeset(revision=tag_id) + + def remove_tag(self, name, user, message=None, date=None): + """ + Removes tag with the given ``name``. + + :param name: name of the tag to be removed + :param user: full username, i.e.: "Joe Doe " + :param message: message of the tag's removal commit + :param date: date of tag's removal commit + + :raises TagDoesNotExistError: if tag with given name does not exists + """ + if name not in self.tags: + raise TagDoesNotExistError("Tag %s does not exist" % name) + if message is None: + message = "Removed tag %s" % name + if date is None: + date = datetime.datetime.now().ctime() + local = False + + try: + self._repo.tag(name, nullid, message, local, user, date) + self.tags = self._get_tags() + except Abort, e: + raise RepositoryError(e.message) + + @LazyProperty + def bookmarks(self): + """ + Get's bookmarks for this repository + """ + return self._get_bookmarks() + + def _get_bookmarks(self): + if self._empty: + return {} + + sortkey = lambda ctx: ctx[0] # sort by name + _bookmarks = [(safe_unicode(n), hex(h),) for n, h in + self._repo._bookmarks.items()] + return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True)) + + def _get_all_revisions(self): + + return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1] + + def get_diff(self, rev1, rev2, path='', ignore_whitespace=False, + context=3): + """ + Returns (git like) *diff*, as plain text. Shows changes introduced by + ``rev2`` since ``rev1``. + + :param rev1: Entry point from which diff is shown. Can be + ``self.EMPTY_CHANGESET`` - in this case, patch showing all + the changes since empty state of the repository until ``rev2`` + :param rev2: Until which revision changes should be shown. + :param ignore_whitespace: If set to ``True``, would not show whitespace + changes. Defaults to ``False``. + :param context: How many lines before/after changed lines should be + shown. Defaults to ``3``. + """ + # Check if given revisions are present at repository (may raise + # ChangesetDoesNotExistError) + if rev1 != self.EMPTY_CHANGESET: + self.get_changeset(rev1) + self.get_changeset(rev2) + + file_filter = match(self.path, '', [path]) + return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter, + opts=diffopts(git=True, + ignorews=ignore_whitespace, + context=context))) + + def _check_url(self, url): + """ + Function will check given url and try to verify if it's a valid + link. Sometimes it may happened that mercurial will issue basic + auth request that can cause whole API to hang when used from python + or other external calls. + + On failures it'll raise urllib2.HTTPError, return code 200 if url + is valid or True if it's a local path + """ + + from mercurial.util import url as Url + + # those authnadlers are patched for python 2.6.5 bug an + # infinit looping when given invalid resources + from mercurial.url import httpbasicauthhandler, httpdigestauthhandler + + # check first if it's not an local url + if os.path.isdir(url) or url.startswith('file:'): + return True + + handlers = [] + test_uri, authinfo = Url(url).authinfo() + + if authinfo: + #create a password manager + passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm() + passmgr.add_password(*authinfo) + + handlers.extend((httpbasicauthhandler(passmgr), + httpdigestauthhandler(passmgr))) + + o = urllib2.build_opener(*handlers) + o.addheaders = [('Content-Type', 'application/mercurial-0.1'), + ('Accept', 'application/mercurial-0.1')] + + q = {"cmd": 'between'} + q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)}) + qs = '?%s' % urllib.urlencode(q) + cu = "%s%s" % (test_uri, qs) + req = urllib2.Request(cu, None, {}) + + try: + resp = o.open(req) + return resp.code == 200 + except Exception, e: + # means it cannot be cloned + raise urllib2.URLError(e) + + def _get_repo(self, create, src_url=None, update_after_clone=False): + """ + Function will check for mercurial repository in given path and return + a localrepo object. If there is no repository in that path it will + raise an exception unless ``create`` parameter is set to True - in + that case repository would be created and returned. + If ``src_url`` is given, would try to clone repository from the + location at given clone_point. Additionally it'll make update to + working copy accordingly to ``update_after_clone`` flag + """ + try: + if src_url: + url = str(self._get_url(src_url)) + opts = {} + if not update_after_clone: + opts.update({'noupdate': True}) + try: + self._check_url(url) + clone(self.baseui, url, self.path, **opts) +# except urllib2.URLError: +# raise Abort("Got HTTP 404 error") + except Exception: + raise + # Don't try to create if we've already cloned repo + create = False + return localrepository(self.baseui, self.path, create=create) + except (Abort, RepoError), err: + if create: + msg = "Cannot create repository at %s. Original error was %s"\ + % (self.path, err) + else: + msg = "Not valid repository at %s. Original error was %s"\ + % (self.path, err) + raise RepositoryError(msg) + + @LazyProperty + def in_memory_changeset(self): + return MercurialInMemoryChangeset(self) + + @LazyProperty + def description(self): + undefined_description = u'unknown' + return safe_unicode(self._repo.ui.config('web', 'description', + undefined_description, untrusted=True)) + + @LazyProperty + def contact(self): + undefined_contact = u'Unknown' + return safe_unicode(get_contact(self._repo.ui.config) + or undefined_contact) + + @LazyProperty + def last_change(self): + """ + Returns last change made on this repository as datetime object + """ + return date_fromtimestamp(self._get_mtime(), makedate()[1]) + + def _get_mtime(self): + try: + return time.mktime(self.get_changeset().date.timetuple()) + except RepositoryError: + #fallback to filesystem + cl_path = os.path.join(self.path, '.hg', "00changelog.i") + st_path = os.path.join(self.path, '.hg', "store") + if os.path.exists(cl_path): + return os.stat(cl_path).st_mtime + else: + return os.stat(st_path).st_mtime + + def _get_hidden(self): + return self._repo.ui.configbool("web", "hidden", untrusted=True) + + def _get_revision(self, revision): + """ + Get's an ID revision given as str. This will always return a fill + 40 char revision number + + :param revision: str or int or None + """ + + if self._empty: + raise EmptyRepositoryError("There are no changesets yet") + + if revision in [-1, 'tip', None]: + revision = 'tip' + + try: + revision = hex(self._repo.lookup(revision)) + except (IndexError, ValueError, RepoLookupError, TypeError): + raise ChangesetDoesNotExistError("Revision %r does not " + "exist for this repository %s" \ + % (revision, self)) + return revision + + def _get_archives(self, archive_name='tip'): + allowed = self.baseui.configlist("web", "allow_archive", + untrusted=True) + for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]: + if i[0] in allowed or self._repo.ui.configbool("web", + "allow" + i[0], + untrusted=True): + yield {"type": i[0], "extension": i[1], "node": archive_name} + + def _get_url(self, url): + """ + Returns normalized url. If schema is not given, would fall + to filesystem + (``file:///``) schema. + """ + url = str(url) + if url != 'default' and not '://' in url: + url = "file:" + urllib.pathname2url(url) + return url + + def get_changeset(self, revision=None): + """ + Returns ``MercurialChangeset`` object representing repository's + changeset at the given ``revision``. + """ + revision = self._get_revision(revision) + changeset = MercurialChangeset(repository=self, revision=revision) + return changeset + + def get_changesets(self, start=None, end=None, start_date=None, + end_date=None, branch_name=None, reverse=False): + """ + Returns iterator of ``MercurialChangeset`` objects from start to end + (both are inclusive) + + :param start: None, str, int or mercurial lookup format + :param end: None, str, int or mercurial lookup format + :param start_date: + :param end_date: + :param branch_name: + :param reversed: return changesets in reversed order + """ + + start_raw_id = self._get_revision(start) + start_pos = self.revisions.index(start_raw_id) if start else None + end_raw_id = self._get_revision(end) + end_pos = self.revisions.index(end_raw_id) if end else None + + if None not in [start, end] and start_pos > end_pos: + raise RepositoryError("start revision '%s' cannot be " + "after end revision '%s'" % (start, end)) + + if branch_name and branch_name not in self.branches.keys(): + raise BranchDoesNotExistError('Such branch %s does not exists for' + ' this repository' % branch_name) + if end_pos is not None: + end_pos += 1 + + slice_ = reversed(self.revisions[start_pos:end_pos]) if reverse else \ + self.revisions[start_pos:end_pos] + + for id_ in slice_: + cs = self.get_changeset(id_) + if branch_name and cs.branch != branch_name: + continue + if start_date and cs.date < start_date: + continue + if end_date and cs.date > end_date: + continue + + yield cs + + def pull(self, url): + """ + Tries to pull changes from external location. + """ + url = self._get_url(url) + try: + pull(self.baseui, self._repo, url) + except Abort, err: + # Propagate error but with vcs's type + raise RepositoryError(str(err)) + + @LazyProperty + def workdir(self): + """ + Returns ``Workdir`` instance for this repository. + """ + return MercurialWorkdir(self) + + def get_config_value(self, section, name, config_file=None): + """ + Returns configuration value for a given [``section``] and ``name``. + + :param section: Section we want to retrieve value from + :param name: Name of configuration we want to retrieve + :param config_file: A path to file which should be used to retrieve + configuration from (might also be a list of file paths) + """ + if config_file is None: + config_file = [] + elif isinstance(config_file, basestring): + config_file = [config_file] + + config = self._repo.ui + for path in config_file: + config.readconfig(path) + return config.config(section, name) + + def get_user_name(self, config_file=None): + """ + Returns user's name from global configuration file. + + :param config_file: A path to file which should be used to retrieve + configuration from (might also be a list of file paths) + """ + username = self.get_config_value('ui', 'username') + if username: + return author_name(username) + return None + + def get_user_email(self, config_file=None): + """ + Returns user's email from global configuration file. + + :param config_file: A path to file which should be used to retrieve + configuration from (might also be a list of file paths) + """ + username = self.get_config_value('ui', 'username') + if username: + return author_email(username) + return None diff --git a/rhodecode/lib/vcs/backends/hg/workdir.py b/rhodecode/lib/vcs/backends/hg/workdir.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/backends/hg/workdir.py @@ -0,0 +1,21 @@ +from rhodecode.lib.vcs.backends.base import BaseWorkdir +from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError + +from ...utils.hgcompat import hg_merge + + +class MercurialWorkdir(BaseWorkdir): + + def get_branch(self): + return self.repository._repo.dirstate.branch() + + def get_changeset(self): + return self.repository.get_changeset() + + def checkout_branch(self, branch=None): + if branch is None: + branch = self.repository.DEFAULT_BRANCH_NAME + if branch not in self.repository.branches: + raise BranchDoesNotExistError + + hg_merge.update(self.repository._repo, branch, False, False, None) diff --git a/rhodecode/lib/vcs/conf/__init__.py b/rhodecode/lib/vcs/conf/__init__.py new file mode 100644 diff --git a/rhodecode/lib/vcs/conf/settings.py b/rhodecode/lib/vcs/conf/settings.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/conf/settings.py @@ -0,0 +1,33 @@ +import os +import tempfile +from rhodecode.lib.vcs.utils.paths import get_user_home + +abspath = lambda * p: os.path.abspath(os.path.join(*p)) + +VCSRC_PATH = os.environ.get('VCSRC_PATH') + +if not VCSRC_PATH: + HOME_ = get_user_home() + if not HOME_: + HOME_ = tempfile.gettempdir() + +VCSRC_PATH = VCSRC_PATH or abspath(HOME_, '.vcsrc') +if os.path.isdir(VCSRC_PATH): + VCSRC_PATH = os.path.join(VCSRC_PATH, '__init__.py') + +BACKENDS = { + 'hg': 'vcs.backends.hg.MercurialRepository', + 'git': 'vcs.backends.git.GitRepository', +} + +ARCHIVE_SPECS = { + 'tar': ('application/x-tar', '.tar'), + 'tbz2': ('application/x-bzip2', '.tar.bz2'), + 'tgz': ('application/x-gzip', '.tar.gz'), + 'zip': ('application/zip', '.zip'), +} + +BACKENDS = { + 'hg': 'rhodecode.lib.vcs.backends.hg.MercurialRepository', + 'git': 'rhodecode.lib.vcs.backends.git.GitRepository', +} diff --git a/rhodecode/lib/vcs/exceptions.py b/rhodecode/lib/vcs/exceptions.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/exceptions.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +""" + vcs.exceptions + ~~~~~~~~~~~~~~ + + Custom exceptions module + + :created_on: Apr 8, 2010 + :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak. +""" + + +class VCSError(Exception): + pass + + +class RepositoryError(VCSError): + pass + + +class EmptyRepositoryError(RepositoryError): + pass + + +class TagAlreadyExistError(RepositoryError): + pass + + +class TagDoesNotExistError(RepositoryError): + pass + + +class BranchAlreadyExistError(RepositoryError): + pass + + +class BranchDoesNotExistError(RepositoryError): + pass + + +class ChangesetError(RepositoryError): + pass + + +class ChangesetDoesNotExistError(ChangesetError): + pass + + +class CommitError(RepositoryError): + pass + + +class NothingChangedError(CommitError): + pass + + +class NodeError(VCSError): + pass + + +class RemovedFileNodeError(NodeError): + pass + + +class NodeAlreadyExistsError(CommitError): + pass + + +class NodeAlreadyChangedError(CommitError): + pass + + +class NodeDoesNotExistError(CommitError): + pass + + +class NodeNotChangedError(CommitError): + pass + + +class NodeAlreadyAddedError(CommitError): + pass + + +class NodeAlreadyRemovedError(CommitError): + pass + + +class ImproperArchiveTypeError(VCSError): + pass + +class CommandError(VCSError): + pass diff --git a/rhodecode/lib/vcs/nodes.py b/rhodecode/lib/vcs/nodes.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/nodes.py @@ -0,0 +1,551 @@ +# -*- coding: utf-8 -*- +""" + vcs.nodes + ~~~~~~~~~ + + Module holding everything related to vcs nodes. + + :created_on: Apr 8, 2010 + :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak. +""" +import stat +import posixpath +import mimetypes + +from rhodecode.lib.vcs.utils.lazy import LazyProperty +from rhodecode.lib.vcs.utils import safe_unicode +from rhodecode.lib.vcs.exceptions import NodeError +from rhodecode.lib.vcs.exceptions import RemovedFileNodeError + +from pygments import lexers + + +class NodeKind: + DIR = 1 + FILE = 2 + + +class NodeState: + ADDED = u'added' + CHANGED = u'changed' + NOT_CHANGED = u'not changed' + REMOVED = u'removed' + + +class NodeGeneratorBase(object): + """ + Base class for removed added and changed filenodes, it's a lazy generator + class that will create filenodes only on iteration or call + + The len method doesn't need to create filenodes at all + """ + + def __init__(self, current_paths, cs): + self.cs = cs + self.current_paths = current_paths + + def __call__(self): + return [n for n in self] + + def __getslice__(self, i, j): + for p in self.current_paths[i:j]: + yield self.cs.get_node(p) + + def __len__(self): + return len(self.current_paths) + + def __iter__(self): + for p in self.current_paths: + yield self.cs.get_node(p) + + +class AddedFileNodesGenerator(NodeGeneratorBase): + """ + Class holding Added files for current changeset + """ + pass + + +class ChangedFileNodesGenerator(NodeGeneratorBase): + """ + Class holding Changed files for current changeset + """ + pass + + +class RemovedFileNodesGenerator(NodeGeneratorBase): + """ + Class holding removed files for current changeset + """ + def __iter__(self): + for p in self.current_paths: + yield RemovedFileNode(path=p) + + def __getslice__(self, i, j): + for p in self.current_paths[i:j]: + yield RemovedFileNode(path=p) + + +class Node(object): + """ + Simplest class representing file or directory on repository. SCM backends + should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node`` + directly. + + Node's ``path`` cannot start with slash as we operate on *relative* paths + only. Moreover, every single node is identified by the ``path`` attribute, + so it cannot end with slash, too. Otherwise, path could lead to mistakes. + """ + + def __init__(self, path, kind): + if path.startswith('/'): + raise NodeError("Cannot initialize Node objects with slash at " + "the beginning as only relative paths are supported") + self.path = path.rstrip('/') + if path == '' and kind != NodeKind.DIR: + raise NodeError("Only DirNode and its subclasses may be " + "initialized with empty path") + self.kind = kind + #self.dirs, self.files = [], [] + if self.is_root() and not self.is_dir(): + raise NodeError("Root node cannot be FILE kind") + + @LazyProperty + def parent(self): + parent_path = self.get_parent_path() + if parent_path: + if self.changeset: + return self.changeset.get_node(parent_path) + return DirNode(parent_path) + return None + + @LazyProperty + def name(self): + """ + Returns name of the node so if its path + then only last part is returned. + """ + return safe_unicode(self.path.rstrip('/').split('/')[-1]) + + def _get_kind(self): + return self._kind + + def _set_kind(self, kind): + if hasattr(self, '_kind'): + raise NodeError("Cannot change node's kind") + else: + self._kind = kind + # Post setter check (path's trailing slash) + if self.path.endswith('/'): + raise NodeError("Node's path cannot end with slash") + + kind = property(_get_kind, _set_kind) + + def __cmp__(self, other): + """ + Comparator using name of the node, needed for quick list sorting. + """ + kind_cmp = cmp(self.kind, other.kind) + if kind_cmp: + return kind_cmp + return cmp(self.name, other.name) + + def __eq__(self, other): + for attr in ['name', 'path', 'kind']: + if getattr(self, attr) != getattr(other, attr): + return False + if self.is_file(): + if self.content != other.content: + return False + else: + # For DirNode's check without entering each dir + self_nodes_paths = list(sorted(n.path for n in self.nodes)) + other_nodes_paths = list(sorted(n.path for n in self.nodes)) + if self_nodes_paths != other_nodes_paths: + return False + return True + + def __nq__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return '<%s %r>' % (self.__class__.__name__, self.path) + + def __str__(self): + return self.__repr__() + + def __unicode__(self): + return self.name + + def get_parent_path(self): + """ + Returns node's parent path or empty string if node is root. + """ + if self.is_root(): + return '' + return posixpath.dirname(self.path.rstrip('/')) + '/' + + def is_file(self): + """ + Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False`` + otherwise. + """ + return self.kind == NodeKind.FILE + + def is_dir(self): + """ + Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False`` + otherwise. + """ + return self.kind == NodeKind.DIR + + def is_root(self): + """ + Returns ``True`` if node is a root node and ``False`` otherwise. + """ + return self.kind == NodeKind.DIR and self.path == '' + + @LazyProperty + def added(self): + return self.state is NodeState.ADDED + + @LazyProperty + def changed(self): + return self.state is NodeState.CHANGED + + @LazyProperty + def not_changed(self): + return self.state is NodeState.NOT_CHANGED + + @LazyProperty + def removed(self): + return self.state is NodeState.REMOVED + + +class FileNode(Node): + """ + Class representing file nodes. + + :attribute: path: path to the node, relative to repostiory's root + :attribute: content: if given arbitrary sets content of the file + :attribute: changeset: if given, first time content is accessed, callback + :attribute: mode: octal stat mode for a node. Default is 0100644. + """ + + def __init__(self, path, content=None, changeset=None, mode=None): + """ + Only one of ``content`` and ``changeset`` may be given. Passing both + would raise ``NodeError`` exception. + + :param path: relative path to the node + :param content: content may be passed to constructor + :param changeset: if given, will use it to lazily fetch content + :param mode: octal representation of ST_MODE (i.e. 0100644) + """ + + if content and changeset: + raise NodeError("Cannot use both content and changeset") + super(FileNode, self).__init__(path, kind=NodeKind.FILE) + self.changeset = changeset + self._content = content + self._mode = mode or 0100644 + + @LazyProperty + def mode(self): + """ + Returns lazily mode of the FileNode. If ``changeset`` is not set, would + use value given at initialization or 0100644 (default). + """ + if self.changeset: + mode = self.changeset.get_file_mode(self.path) + else: + mode = self._mode + return mode + + @property + def content(self): + """ + Returns lazily content of the FileNode. If possible, would try to + decode content from UTF-8. + """ + if self.changeset: + content = self.changeset.get_file_content(self.path) + else: + content = self._content + + if bool(content and '\0' in content): + return content + return safe_unicode(content) + + @LazyProperty + def size(self): + if self.changeset: + return self.changeset.get_file_size(self.path) + raise NodeError("Cannot retrieve size of the file without related " + "changeset attribute") + + @LazyProperty + def message(self): + if self.changeset: + return self.last_changeset.message + raise NodeError("Cannot retrieve message of the file without related " + "changeset attribute") + + @LazyProperty + def last_changeset(self): + if self.changeset: + return self.changeset.get_file_changeset(self.path) + raise NodeError("Cannot retrieve last changeset of the file without " + "related changeset attribute") + + def get_mimetype(self): + """ + Mimetype is calculated based on the file's content. If ``_mimetype`` + attribute is available, it will be returned (backends which store + mimetypes or can easily recognize them, should set this private + attribute to indicate that type should *NOT* be calculated). + """ + if hasattr(self, '_mimetype'): + if (isinstance(self._mimetype,(tuple,list,)) and + len(self._mimetype) == 2): + return self._mimetype + else: + raise NodeError('given _mimetype attribute must be an 2 ' + 'element list or tuple') + + mtype,encoding = mimetypes.guess_type(self.name) + + if mtype is None: + if self.is_binary: + mtype = 'application/octet-stream' + encoding = None + else: + mtype = 'text/plain' + encoding = None + return mtype,encoding + + @LazyProperty + def mimetype(self): + """ + Wrapper around full mimetype info. It returns only type of fetched + mimetype without the encoding part. use get_mimetype function to fetch + full set of (type,encoding) + """ + return self.get_mimetype()[0] + + @LazyProperty + def mimetype_main(self): + return self.mimetype.split('/')[0] + + @LazyProperty + def lexer(self): + """ + Returns pygment's lexer class. Would try to guess lexer taking file's + content, name and mimetype. + """ + try: + lexer = lexers.guess_lexer_for_filename(self.name, self.content) + except lexers.ClassNotFound: + lexer = lexers.TextLexer() + # returns first alias + return lexer + + @LazyProperty + def lexer_alias(self): + """ + Returns first alias of the lexer guessed for this file. + """ + return self.lexer.aliases[0] + + @LazyProperty + def history(self): + """ + Returns a list of changeset for this file in which the file was changed + """ + if self.changeset is None: + raise NodeError('Unable to get changeset for this FileNode') + return self.changeset.get_file_history(self.path) + + @LazyProperty + def annotate(self): + """ + Returns a list of three element tuples with lineno,changeset and line + """ + if self.changeset is None: + raise NodeError('Unable to get changeset for this FileNode') + return self.changeset.get_file_annotate(self.path) + + @LazyProperty + def state(self): + if not self.changeset: + raise NodeError("Cannot check state of the node if it's not " + "linked with changeset") + elif self.path in (node.path for node in self.changeset.added): + return NodeState.ADDED + elif self.path in (node.path for node in self.changeset.changed): + return NodeState.CHANGED + else: + return NodeState.NOT_CHANGED + + @property + def is_binary(self): + """ + Returns True if file has binary content. + """ + bin = '\0' in self.content + return bin + + @LazyProperty + def extension(self): + """Returns filenode extension""" + return self.name.split('.')[-1] + + def is_executable(self): + """ + Returns ``True`` if file has executable flag turned on. + """ + return bool(self.mode & stat.S_IXUSR) + + +class RemovedFileNode(FileNode): + """ + Dummy FileNode class - trying to access any public attribute except path, + name, kind or state (or methods/attributes checking those two) would raise + RemovedFileNodeError. + """ + ALLOWED_ATTRIBUTES = ['name', 'path', 'state', 'is_root', 'is_file', + 'is_dir', 'kind', 'added', 'changed', 'not_changed', 'removed'] + + def __init__(self, path): + """ + :param path: relative path to the node + """ + super(RemovedFileNode, self).__init__(path=path) + + def __getattribute__(self, attr): + if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES: + return super(RemovedFileNode, self).__getattribute__(attr) + raise RemovedFileNodeError("Cannot access attribute %s on " + "RemovedFileNode" % attr) + + @LazyProperty + def state(self): + return NodeState.REMOVED + + +class DirNode(Node): + """ + DirNode stores list of files and directories within this node. + Nodes may be used standalone but within repository context they + lazily fetch data within same repositorty's changeset. + """ + + def __init__(self, path, nodes=(), changeset=None): + """ + Only one of ``nodes`` and ``changeset`` may be given. Passing both + would raise ``NodeError`` exception. + + :param path: relative path to the node + :param nodes: content may be passed to constructor + :param changeset: if given, will use it to lazily fetch content + :param size: always 0 for ``DirNode`` + """ + if nodes and changeset: + raise NodeError("Cannot use both nodes and changeset") + super(DirNode, self).__init__(path, NodeKind.DIR) + self.changeset = changeset + self._nodes = nodes + + @LazyProperty + def content(self): + raise NodeError("%s represents a dir and has no ``content`` attribute" + % self) + + @LazyProperty + def nodes(self): + if self.changeset: + nodes = self.changeset.get_nodes(self.path) + else: + nodes = self._nodes + self._nodes_dict = dict((node.path, node) for node in nodes) + return sorted(nodes) + + @LazyProperty + def files(self): + return sorted((node for node in self.nodes if node.is_file())) + + @LazyProperty + def dirs(self): + return sorted((node for node in self.nodes if node.is_dir())) + + def __iter__(self): + for node in self.nodes: + yield node + + def get_node(self, path): + """ + Returns node from within this particular ``DirNode``, so it is now + allowed to fetch, i.e. node located at 'docs/api/index.rst' from node + 'docs'. In order to access deeper nodes one must fetch nodes between + them first - this would work:: + + docs = root.get_node('docs') + docs.get_node('api').get_node('index.rst') + + :param: path - relative to the current node + + .. note:: + To access lazily (as in example above) node have to be initialized + with related changeset object - without it node is out of + context and may know nothing about anything else than nearest + (located at same level) nodes. + """ + try: + path = path.rstrip('/') + if path == '': + raise NodeError("Cannot retrieve node without path") + self.nodes # access nodes first in order to set _nodes_dict + paths = path.split('/') + if len(paths) == 1: + if not self.is_root(): + path = '/'.join((self.path, paths[0])) + else: + path = paths[0] + return self._nodes_dict[path] + elif len(paths) > 1: + if self.changeset is None: + raise NodeError("Cannot access deeper " + "nodes without changeset") + else: + path1, path2 = paths[0], '/'.join(paths[1:]) + return self.get_node(path1).get_node(path2) + else: + raise KeyError + except KeyError: + raise NodeError("Node does not exist at %s" % path) + + @LazyProperty + def state(self): + raise NodeError("Cannot access state of DirNode") + + @LazyProperty + def size(self): + size = 0 + for root, dirs, files in self.changeset.walk(self.path): + for f in files: + size += f.size + + return size + + +class RootNode(DirNode): + """ + DirNode being the root node of the repository. + """ + + def __init__(self, nodes=(), changeset=None): + super(RootNode, self).__init__(path='', nodes=nodes, + changeset=changeset) + + def __repr__(self): + return '<%s>' % self.__class__.__name__ diff --git a/rhodecode/lib/vcs/utils/__init__.py b/rhodecode/lib/vcs/utils/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/__init__.py @@ -0,0 +1,139 @@ +""" +This module provides some useful tools for ``vcs`` like annotate/diff html +output. It also includes some internal helpers. +""" +import sys +import time +import datetime + + +def makedate(): + lt = time.localtime() + if lt[8] == 1 and time.daylight: + tz = time.altzone + else: + tz = time.timezone + return time.mktime(lt), tz + + +def date_fromtimestamp(unixts, tzoffset=0): + """ + Makes a local datetime object out of unix timestamp + + :param unixts: + :param tzoffset: + """ + + return datetime.datetime.fromtimestamp(float(unixts)) + + +def safe_unicode(str_, from_encoding=None): + """ + safe unicode function. Does few trick to turn str_ into unicode + + In case of UnicodeDecode error we try to return it with encoding detected + by chardet library if it fails fallback to unicode with errors replaced + + :param str_: string to decode + :rtype: unicode + :returns: unicode object + """ + if isinstance(str_, unicode): + return str_ + if not from_encoding: + import rhodecode + DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding', 'utf8') + from_encoding = DEFAULT_ENCODING + try: + return unicode(str_) + except UnicodeDecodeError: + pass + + try: + return unicode(str_, from_encoding) + except UnicodeDecodeError: + pass + + try: + import chardet + encoding = chardet.detect(str_)['encoding'] + if encoding is None: + raise Exception() + return str_.decode(encoding) + except (ImportError, UnicodeDecodeError, Exception): + return unicode(str_, from_encoding, 'replace') + + +def safe_str(unicode_, to_encoding=None): + """ + safe str function. Does few trick to turn unicode_ into string + + In case of UnicodeEncodeError we try to return it with encoding detected + by chardet library if it fails fallback to string with errors replaced + + :param unicode_: unicode to encode + :rtype: str + :returns: str object + """ + + if isinstance(unicode_, str): + return unicode_ + if not to_encoding: + import rhodecode + DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding', 'utf8') + to_encoding = DEFAULT_ENCODING + try: + return unicode_.encode(to_encoding) + except UnicodeEncodeError: + pass + + try: + import chardet + encoding = chardet.detect(unicode_)['encoding'] + print encoding + if encoding is None: + raise UnicodeEncodeError() + + return unicode_.encode(encoding) + except (ImportError, UnicodeEncodeError): + return unicode_.encode(to_encoding, 'replace') + + return safe_str + + +def author_email(author): + """ + returns email address of given author. + If any of <,> sign are found, it fallbacks to regex findall() + and returns first found result or empty string + + Regex taken from http://www.regular-expressions.info/email.html + """ + import re + r = author.find('>') + l = author.find('<') + + if l == -1 or r == -1: + # fallback to regex match of email out of a string + email_re = re.compile(r"""[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!""" + r"""#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z""" + r"""0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]""" + r"""*[a-z0-9])?""", re.IGNORECASE) + m = re.findall(email_re, author) + return m[0] if m else '' + + return author[l + 1:r].strip() + + +def author_name(author): + """ + get name of author, or else username. + It'll try to find an email in the author string and just cut it off + to get the username + """ + + if not '@' in author: + return author + else: + return author.replace(author_email(author), '').replace('<', '')\ + .replace('>', '').strip() diff --git a/rhodecode/lib/vcs/utils/annotate.py b/rhodecode/lib/vcs/utils/annotate.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/annotate.py @@ -0,0 +1,177 @@ +from rhodecode.lib.vcs.exceptions import VCSError +from rhodecode.lib.vcs.nodes import FileNode +from pygments.formatters import HtmlFormatter +from pygments import highlight + +import StringIO + + +def annotate_highlight(filenode, annotate_from_changeset_func=None, + order=None, headers=None, **options): + """ + Returns html portion containing annotated table with 3 columns: line + numbers, changeset information and pygmentized line of code. + + :param filenode: FileNode object + :param annotate_from_changeset_func: function taking changeset and + returning single annotate cell; needs break line at the end + :param order: ordered sequence of ``ls`` (line numbers column), + ``annotate`` (annotate column), ``code`` (code column); Default is + ``['ls', 'annotate', 'code']`` + :param headers: dictionary with headers (keys are whats in ``order`` + parameter) + """ + options['linenos'] = True + formatter = AnnotateHtmlFormatter(filenode=filenode, order=order, + headers=headers, + annotate_from_changeset_func=annotate_from_changeset_func, **options) + lexer = filenode.lexer + highlighted = highlight(filenode.content, lexer, formatter) + return highlighted + + +class AnnotateHtmlFormatter(HtmlFormatter): + + def __init__(self, filenode, annotate_from_changeset_func=None, + order=None, **options): + """ + If ``annotate_from_changeset_func`` is passed it should be a function + which returns string from the given changeset. For example, we may pass + following function as ``annotate_from_changeset_func``:: + + def changeset_to_anchor(changeset): + return '%s\n' %\ + (changeset.id, changeset.id) + + :param annotate_from_changeset_func: see above + :param order: (default: ``['ls', 'annotate', 'code']``); order of + columns; + :param options: standard pygment's HtmlFormatter options, there is + extra option tough, ``headers``. For instance we can pass:: + + formatter = AnnotateHtmlFormatter(filenode, headers={ + 'ls': '#', + 'annotate': 'Annotate', + 'code': 'Code', + }) + + """ + super(AnnotateHtmlFormatter, self).__init__(**options) + self.annotate_from_changeset_func = annotate_from_changeset_func + self.order = order or ('ls', 'annotate', 'code') + headers = options.pop('headers', None) + if headers and not ('ls' in headers and 'annotate' in headers and + 'code' in headers): + raise ValueError("If headers option dict is specified it must " + "all 'ls', 'annotate' and 'code' keys") + self.headers = headers + if isinstance(filenode, FileNode): + self.filenode = filenode + else: + raise VCSError("This formatter expect FileNode parameter, not %r" + % type(filenode)) + + def annotate_from_changeset(self, changeset): + """ + Returns full html line for single changeset per annotated line. + """ + if self.annotate_from_changeset_func: + return self.annotate_from_changeset_func(changeset) + else: + return ''.join((changeset.id, '\n')) + + def _wrap_tablelinenos(self, inner): + dummyoutfile = StringIO.StringIO() + lncount = 0 + for t, line in inner: + if t: + lncount += 1 + dummyoutfile.write(line) + + fl = self.linenostart + mw = len(str(lncount + fl - 1)) + sp = self.linenospecial + st = self.linenostep + la = self.lineanchors + aln = self.anchorlinenos + if sp: + lines = [] + + for i in range(fl, fl + lncount): + if i % st == 0: + if i % sp == 0: + if aln: + lines.append('' + '%*d' % + (la, i, mw, i)) + else: + lines.append('' + '%*d' % (mw, i)) + else: + if aln: + lines.append('' + '%*d' % (la, i, mw, i)) + else: + lines.append('%*d' % (mw, i)) + else: + lines.append('') + ls = '\n'.join(lines) + else: + lines = [] + for i in range(fl, fl + lncount): + if i % st == 0: + if aln: + lines.append('%*d' \ + % (la, i, mw, i)) + else: + lines.append('%*d' % (mw, i)) + else: + lines.append('') + ls = '\n'.join(lines) + + annotate_changesets = [tup[1] for tup in self.filenode.annotate] + # If pygments cropped last lines break we need do that too + ln_cs = len(annotate_changesets) + ln_ = len(ls.splitlines()) + if ln_cs > ln_: + annotate_changesets = annotate_changesets[:ln_ - ln_cs] + annotate = ''.join((self.annotate_from_changeset(changeset) + for changeset in annotate_changesets)) + # in case you wonder about the seemingly redundant
here: + # since the content in the other cell also is wrapped in a div, + # some browsers in some configurations seem to mess up the formatting. + ''' + yield 0, ('' % self.cssclass + + '' + + '
' +
+                  ls + '
') + yield 0, dummyoutfile.getvalue() + yield 0, '
' + + ''' + headers_row = [] + if self.headers: + headers_row = [''] + for key in self.order: + td = ''.join(('', self.headers[key], '')) + headers_row.append(td) + headers_row.append('') + + body_row_start = [''] + for key in self.order: + if key == 'ls': + body_row_start.append( + '
' +
+                    ls + '
') + elif key == 'annotate': + body_row_start.append( + '
' +
+                    annotate + '
') + elif key == 'code': + body_row_start.append('') + yield 0, ('' % self.cssclass + + ''.join(headers_row) + + ''.join(body_row_start) + ) + yield 0, dummyoutfile.getvalue() + yield 0, '
' diff --git a/rhodecode/lib/vcs/utils/archivers.py b/rhodecode/lib/vcs/utils/archivers.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/archivers.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +""" + vcs.utils.archivers + ~~~~~~~~~~~~~~~~~~~ + + set of archiver functions for creating archives from repository content + + :created_on: Jan 21, 2011 + :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak. +""" + + +class BaseArchiver(object): + + def __init__(self): + self.archive_file = self._get_archive_file() + + def addfile(self): + """ + Adds a file to archive container + """ + pass + + def close(self): + """ + Closes and finalizes operation of archive container object + """ + self.archive_file.close() + + def _get_archive_file(self): + """ + Returns container for specific archive + """ + raise NotImplementedError() + + +class TarArchiver(BaseArchiver): + pass + + +class Tbz2Archiver(BaseArchiver): + pass + + +class TgzArchiver(BaseArchiver): + pass + + +class ZipArchiver(BaseArchiver): + pass + + +def get_archiver(self, kind): + """ + Returns instance of archiver class specific to given kind + + :param kind: archive kind + """ + + archivers = { + 'tar': TarArchiver, + 'tbz2': Tbz2Archiver, + 'tgz': TgzArchiver, + 'zip': ZipArchiver, + } + + return archivers[kind]() diff --git a/rhodecode/lib/vcs/utils/baseui_config.py b/rhodecode/lib/vcs/utils/baseui_config.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/baseui_config.py @@ -0,0 +1,47 @@ +from mercurial import ui, config + + +def make_ui(self, path='hgwebdir.config'): + """ + A funcion that will read python rc files and make an ui from read options + + :param path: path to mercurial config file + """ + #propagated from mercurial documentation + sections = [ + 'alias', + 'auth', + 'decode/encode', + 'defaults', + 'diff', + 'email', + 'extensions', + 'format', + 'merge-patterns', + 'merge-tools', + 'hooks', + 'http_proxy', + 'smtp', + 'patch', + 'paths', + 'profiling', + 'server', + 'trusted', + 'ui', + 'web', + ] + + repos = path + baseui = ui.ui() + cfg = config.config() + cfg.read(repos) + self.paths = cfg.items('paths') + self.base_path = self.paths[0][1].replace('*', '') + self.check_repo_dir(self.paths) + self.set_statics(cfg) + + for section in sections: + for k, v in cfg.items(section): + baseui.setconfig(section, k, v) + + return baseui diff --git a/rhodecode/lib/vcs/utils/compat.py b/rhodecode/lib/vcs/utils/compat.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/compat.py @@ -0,0 +1,13 @@ +""" +Various utilities to work with Python < 2.7. + +Those utilities may be deleted once ``vcs`` stops support for older Python +versions. +""" +import sys + + +if sys.version_info >= (2, 7): + unittest = __import__('unittest') +else: + unittest = __import__('unittest2') diff --git a/rhodecode/lib/vcs/utils/diffs.py b/rhodecode/lib/vcs/utils/diffs.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/diffs.py @@ -0,0 +1,460 @@ +# -*- coding: utf-8 -*- +# original copyright: 2007-2008 by Armin Ronacher +# licensed under the BSD license. + +import re +import difflib +import logging + +from difflib import unified_diff +from itertools import tee, imap + +from mercurial.match import match + +from rhodecode.lib.vcs.exceptions import VCSError +from rhodecode.lib.vcs.nodes import FileNode, NodeError + + +def get_udiff(filenode_old, filenode_new,show_whitespace=True): + """ + Returns unified diff between given ``filenode_old`` and ``filenode_new``. + """ + try: + filenode_old_date = filenode_old.last_changeset.date + except NodeError: + filenode_old_date = None + + try: + filenode_new_date = filenode_new.last_changeset.date + except NodeError: + filenode_new_date = None + + for filenode in (filenode_old, filenode_new): + if not isinstance(filenode, FileNode): + raise VCSError("Given object should be FileNode object, not %s" + % filenode.__class__) + + if filenode_old_date and filenode_new_date: + if not filenode_old_date < filenode_new_date: + logging.debug("Generating udiff for filenodes with not increasing " + "dates") + + vcs_udiff = unified_diff(filenode_old.content.splitlines(True), + filenode_new.content.splitlines(True), + filenode_old.name, + filenode_new.name, + filenode_old_date, + filenode_old_date) + return vcs_udiff + + +def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True): + """ + Returns git style diff between given ``filenode_old`` and ``filenode_new``. + + :param ignore_whitespace: ignore whitespaces in diff + """ + + for filenode in (filenode_old, filenode_new): + if not isinstance(filenode, FileNode): + raise VCSError("Given object should be FileNode object, not %s" + % filenode.__class__) + + old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40) + new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40) + + repo = filenode_new.changeset.repository + vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path, + ignore_whitespace) + + return vcs_gitdiff + + +class DiffProcessor(object): + """ + Give it a unified diff and it returns a list of the files that were + mentioned in the diff together with a dict of meta information that + can be used to render it in a HTML template. + """ + _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)') + + def __init__(self, diff, differ='diff', format='udiff'): + """ + :param diff: a text in diff format or generator + :param format: format of diff passed, `udiff` or `gitdiff` + """ + if isinstance(diff, basestring): + diff = [diff] + + self.__udiff = diff + self.__format = format + self.adds = 0 + self.removes = 0 + + if isinstance(self.__udiff, basestring): + self.lines = iter(self.__udiff.splitlines(1)) + + elif self.__format == 'gitdiff': + udiff_copy = self.copy_iterator() + self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy)) + else: + udiff_copy = self.copy_iterator() + self.lines = imap(self.escaper, udiff_copy) + + # Select a differ. + if differ == 'difflib': + self.differ = self._highlight_line_difflib + else: + self.differ = self._highlight_line_udiff + + def escaper(self, string): + return string.replace('<', '<').replace('>', '>') + + def copy_iterator(self): + """ + make a fresh copy of generator, we should not iterate thru + an original as it's needed for repeating operations on + this instance of DiffProcessor + """ + self.__udiff, iterator_copy = tee(self.__udiff) + return iterator_copy + + def _extract_rev(self, line1, line2): + """ + Extract the filename and revision hint from a line. + """ + + try: + if line1.startswith('--- ') and line2.startswith('+++ '): + l1 = line1[4:].split(None, 1) + old_filename = l1[0].lstrip('a/') if len(l1) >= 1 else None + old_rev = l1[1] if len(l1) == 2 else 'old' + + l2 = line2[4:].split(None, 1) + new_filename = l2[0].lstrip('b/') if len(l1) >= 1 else None + new_rev = l2[1] if len(l2) == 2 else 'new' + + filename = old_filename if (old_filename != + 'dev/null') else new_filename + + return filename, new_rev, old_rev + except (ValueError, IndexError): + pass + + return None, None, None + + def _parse_gitdiff(self, diffiterator): + def line_decoder(l): + if l.startswith('+') and not l.startswith('+++'): + self.adds += 1 + elif l.startswith('-') and not l.startswith('---'): + self.removes += 1 + return l.decode('utf8', 'replace') + + output = list(diffiterator) + size = len(output) + + if size == 2: + l = [] + l.extend([output[0]]) + l.extend(output[1].splitlines(1)) + return map(line_decoder, l) + elif size == 1: + return map(line_decoder, output[0].splitlines(1)) + elif size == 0: + return [] + + raise Exception('wrong size of diff %s' % size) + + def _highlight_line_difflib(self, line, next): + """ + Highlight inline changes in both lines. + """ + + if line['action'] == 'del': + old, new = line, next + else: + old, new = next, line + + oldwords = re.split(r'(\W)', old['line']) + newwords = re.split(r'(\W)', new['line']) + + sequence = difflib.SequenceMatcher(None, oldwords, newwords) + + oldfragments, newfragments = [], [] + for tag, i1, i2, j1, j2 in sequence.get_opcodes(): + oldfrag = ''.join(oldwords[i1:i2]) + newfrag = ''.join(newwords[j1:j2]) + if tag != 'equal': + if oldfrag: + oldfrag = '%s' % oldfrag + if newfrag: + newfrag = '%s' % newfrag + oldfragments.append(oldfrag) + newfragments.append(newfrag) + + old['line'] = "".join(oldfragments) + new['line'] = "".join(newfragments) + + def _highlight_line_udiff(self, line, next): + """ + Highlight inline changes in both lines. + """ + start = 0 + limit = min(len(line['line']), len(next['line'])) + while start < limit and line['line'][start] == next['line'][start]: + start += 1 + end = -1 + limit -= start + while -end <= limit and line['line'][end] == next['line'][end]: + end -= 1 + end += 1 + if start or end: + def do(l): + last = end + len(l['line']) + if l['action'] == 'add': + tag = 'ins' + else: + tag = 'del' + l['line'] = '%s<%s>%s%s' % ( + l['line'][:start], + tag, + l['line'][start:last], + tag, + l['line'][last:] + ) + do(line) + do(next) + + def _parse_udiff(self): + """ + Parse the diff an return data for the template. + """ + lineiter = self.lines + files = [] + try: + line = lineiter.next() + # skip first context + skipfirst = True + while 1: + # continue until we found the old file + if not line.startswith('--- '): + line = lineiter.next() + continue + + chunks = [] + filename, old_rev, new_rev = \ + self._extract_rev(line, lineiter.next()) + files.append({ + 'filename': filename, + 'old_revision': old_rev, + 'new_revision': new_rev, + 'chunks': chunks + }) + + line = lineiter.next() + while line: + match = self._chunk_re.match(line) + if not match: + break + + lines = [] + chunks.append(lines) + + old_line, old_end, new_line, new_end = \ + [int(x or 1) for x in match.groups()[:-1]] + old_line -= 1 + new_line -= 1 + context = len(match.groups()) == 5 + old_end += old_line + new_end += new_line + + if context: + if not skipfirst: + lines.append({ + 'old_lineno': '...', + 'new_lineno': '...', + 'action': 'context', + 'line': line, + }) + else: + skipfirst = False + + line = lineiter.next() + while old_line < old_end or new_line < new_end: + if line: + command, line = line[0], line[1:] + else: + command = ' ' + affects_old = affects_new = False + + # ignore those if we don't expect them + if command in '#@': + continue + elif command == '+': + affects_new = True + action = 'add' + elif command == '-': + affects_old = True + action = 'del' + else: + affects_old = affects_new = True + action = 'unmod' + + old_line += affects_old + new_line += affects_new + lines.append({ + 'old_lineno': affects_old and old_line or '', + 'new_lineno': affects_new and new_line or '', + 'action': action, + 'line': line + }) + line = lineiter.next() + + except StopIteration: + pass + + # highlight inline changes + for file in files: + for chunk in chunks: + lineiter = iter(chunk) + #first = True + try: + while 1: + line = lineiter.next() + if line['action'] != 'unmod': + nextline = lineiter.next() + if nextline['action'] == 'unmod' or \ + nextline['action'] == line['action']: + continue + self.differ(line, nextline) + except StopIteration: + pass + + return files + + def prepare(self): + """ + Prepare the passed udiff for HTML rendering. It'l return a list + of dicts + """ + return self._parse_udiff() + + def _safe_id(self, idstring): + """Make a string safe for including in an id attribute. + + The HTML spec says that id attributes 'must begin with + a letter ([A-Za-z]) and may be followed by any number + of letters, digits ([0-9]), hyphens ("-"), underscores + ("_"), colons (":"), and periods (".")'. These regexps + are slightly over-zealous, in that they remove colons + and periods unnecessarily. + + Whitespace is transformed into underscores, and then + anything which is not a hyphen or a character that + matches \w (alphanumerics and underscore) is removed. + + """ + # Transform all whitespace to underscore + idstring = re.sub(r'\s', "_", '%s' % idstring) + # Remove everything that is not a hyphen or a member of \w + idstring = re.sub(r'(?!-)\W', "", idstring).lower() + return idstring + + def raw_diff(self): + """ + Returns raw string as udiff + """ + udiff_copy = self.copy_iterator() + if self.__format == 'gitdiff': + udiff_copy = self._parse_gitdiff(udiff_copy) + return u''.join(udiff_copy) + + def as_html(self, table_class='code-difftable', line_class='line', + new_lineno_class='lineno old', old_lineno_class='lineno new', + code_class='code'): + """ + Return udiff as html table with customized css classes + """ + def _link_to_if(condition, label, url): + """ + Generates a link if condition is meet or just the label if not. + """ + + if condition: + return '''%(label)s''' % {'url': url, + 'label': label} + else: + return label + diff_lines = self.prepare() + _html_empty = True + _html = [] + _html.append('''\n''' \ + % {'table_class': table_class}) + for diff in diff_lines: + for line in diff['chunks']: + _html_empty = False + for change in line: + _html.append('''\n''' \ + % {'line_class': line_class, + 'action': change['action']}) + anchor_old_id = '' + anchor_new_id = '' + anchor_old = "%(filename)s_o%(oldline_no)s" % \ + {'filename': self._safe_id(diff['filename']), + 'oldline_no': change['old_lineno']} + anchor_new = "%(filename)s_n%(oldline_no)s" % \ + {'filename': self._safe_id(diff['filename']), + 'oldline_no': change['new_lineno']} + cond_old = change['old_lineno'] != '...' and \ + change['old_lineno'] + cond_new = change['new_lineno'] != '...' and \ + change['new_lineno'] + if cond_old: + anchor_old_id = 'id="%s"' % anchor_old + if cond_new: + anchor_new_id = 'id="%s"' % anchor_new + ########################################################### + # OLD LINE NUMBER + ########################################################### + _html.append('''\t\n''') + ########################################################### + # NEW LINE NUMBER + ########################################################### + + _html.append('''\t\n''') + ########################################################### + # CODE + ########################################################### + _html.append('''\t''') + _html.append('''\n\n''') + _html.append('''
''' \ + % {'a_id': anchor_old_id, + 'old_lineno_cls': old_lineno_class}) + + _html.append('''
%(link)s
''' \ + % {'link': + _link_to_if(cond_old, change['old_lineno'], '#%s' \ + % anchor_old)}) + _html.append('''
''' \ + % {'a_id': anchor_new_id, + 'new_lineno_cls': new_lineno_class}) + + _html.append('''
%(link)s
''' \ + % {'link': + _link_to_if(cond_new, change['new_lineno'], '#%s' \ + % anchor_new)}) + _html.append('''
''' \ + % {'code_class': code_class}) + _html.append('''\n\t\t
%(code)s
\n''' \ + % {'code': change['line']}) + _html.append('''\t
''') + if _html_empty: + return None + return ''.join(_html) + + def stat(self): + """ + Returns tuple of adde,and removed lines for this instance + """ + return self.adds, self.removes diff --git a/rhodecode/lib/vcs/utils/fakemod.py b/rhodecode/lib/vcs/utils/fakemod.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/fakemod.py @@ -0,0 +1,13 @@ +import imp + + +def create_module(name, path): + """ + Returns module created *on the fly*. Returned module would have name same + as given ``name`` and would contain code read from file at the given + ``path`` (it may also be a zip or package containing *__main__* module). + """ + module = imp.new_module(name) + module.__file__ = path + execfile(path, module.__dict__) + return module diff --git a/rhodecode/lib/vcs/utils/filesize.py b/rhodecode/lib/vcs/utils/filesize.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/filesize.py @@ -0,0 +1,28 @@ +def filesizeformat(bytes, sep=' '): + """ + Formats the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, + 102 B, 2.3 GB etc). + + Grabbed from Django (http://www.djangoproject.com), slightly modified. + + :param bytes: size in bytes (as integer) + :param sep: string separator between number and abbreviation + """ + try: + bytes = float(bytes) + except (TypeError, ValueError, UnicodeDecodeError): + return '0%sB' % sep + + if bytes < 1024: + size = bytes + template = '%.0f%sB' + elif bytes < 1024 * 1024: + size = bytes / 1024 + template = '%.0f%sKB' + elif bytes < 1024 * 1024 * 1024: + size = bytes / 1024 / 1024 + template = '%.1f%sMB' + else: + size = bytes / 1024 / 1024 / 1024 + template = '%.2f%sGB' + return template % (size, sep) diff --git a/rhodecode/lib/vcs/utils/helpers.py b/rhodecode/lib/vcs/utils/helpers.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/helpers.py @@ -0,0 +1,252 @@ +""" +Utitlites aimed to help achieve mostly basic tasks. +""" +from __future__ import division + +import re +import time +import datetime +import os.path +from subprocess import Popen, PIPE +from rhodecode.lib.vcs.exceptions import VCSError +from rhodecode.lib.vcs.exceptions import RepositoryError +from rhodecode.lib.vcs.utils.paths import abspath + +ALIASES = ['hg', 'git'] + + +def get_scm(path, search_recursively=False, explicit_alias=None): + """ + Returns one of alias from ``ALIASES`` (in order of precedence same as + shortcuts given in ``ALIASES``) and top working dir path for the given + argument. If no scm-specific directory is found or more than one scm is + found at that directory, ``VCSError`` is raised. + + :param search_recursively: if set to ``True``, this function would try to + move up to parent directory every time no scm is recognized for the + currently checked path. Default: ``False``. + :param explicit_alias: can be one of available backend aliases, when given + it will return given explicit alias in repositories under more than one + version control, if explicit_alias is different than found it will raise + VCSError + """ + if not os.path.isdir(path): + raise VCSError("Given path %s is not a directory" % path) + + def get_scms(path): + return [(scm, path) for scm in get_scms_for_path(path)] + + found_scms = get_scms(path) + while not found_scms and search_recursively: + newpath = abspath(path, '..') + if newpath == path: + break + path = newpath + found_scms = get_scms(path) + + if len(found_scms) > 1: + for scm in found_scms: + if scm[0] == explicit_alias: + return scm + raise VCSError('More than one [%s] scm found at given path %s' + % (','.join((x[0] for x in found_scms)), path)) + + if len(found_scms) is 0: + raise VCSError('No scm found at given path %s' % path) + + return found_scms[0] + + +def get_scms_for_path(path): + """ + Returns all scm's found at the given path. If no scm is recognized + - empty list is returned. + + :param path: path to directory which should be checked. May be callable. + + :raises VCSError: if given ``path`` is not a directory + """ + from rhodecode.lib.vcs.backends import get_backend + if hasattr(path, '__call__'): + path = path() + if not os.path.isdir(path): + raise VCSError("Given path %r is not a directory" % path) + + result = [] + for key in ALIASES: + dirname = os.path.join(path, '.' + key) + if os.path.isdir(dirname): + result.append(key) + continue + # We still need to check if it's not bare repository as + # bare repos don't have working directories + try: + get_backend(key)(path) + result.append(key) + continue + except RepositoryError: + # Wrong backend + pass + except VCSError: + # No backend at all + pass + return result + + +def get_repo_paths(path): + """ + Returns path's subdirectories which seems to be a repository. + """ + repo_paths = [] + dirnames = (os.path.abspath(dirname) for dirname in os.listdir(path)) + for dirname in dirnames: + try: + get_scm(dirname) + repo_paths.append(dirname) + except VCSError: + pass + return repo_paths + + +def run_command(cmd, *args): + """ + Runs command on the system with given ``args``. + """ + command = ' '.join((cmd, args)) + p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE) + stdout, stderr = p.communicate() + return p.retcode, stdout, stderr + + +def get_highlighted_code(name, code, type='terminal'): + """ + If pygments are available on the system + then returned output is colored. Otherwise + unchanged content is returned. + """ + import logging + try: + import pygments + pygments + except ImportError: + return code + from pygments import highlight + from pygments.lexers import guess_lexer_for_filename, ClassNotFound + from pygments.formatters import TerminalFormatter + + try: + lexer = guess_lexer_for_filename(name, code) + formatter = TerminalFormatter() + content = highlight(code, lexer, formatter) + except ClassNotFound: + logging.debug("Couldn't guess Lexer, will not use pygments.") + content = code + return content + +def parse_changesets(text): + """ + Returns dictionary with *start*, *main* and *end* ids. + + Examples:: + + >>> parse_changesets('aaabbb') + {'start': None, 'main': 'aaabbb', 'end': None} + >>> parse_changesets('aaabbb..cccddd') + {'start': 'aaabbb', 'main': None, 'end': 'cccddd'} + + """ + text = text.strip() + CID_RE = r'[a-zA-Z0-9]+' + if not '..' in text: + m = re.match(r'^(?P%s)$' % CID_RE, text) + if m: + return { + 'start': None, + 'main': text, + 'end': None, + } + else: + RE = r'^(?P%s)?\.{2,3}(?P%s)?$' % (CID_RE, CID_RE) + m = re.match(RE, text) + if m: + result = m.groupdict() + result['main'] = None + return result + raise ValueError("IDs not recognized") + +def parse_datetime(text): + """ + Parses given text and returns ``datetime.datetime`` instance or raises + ``ValueError``. + + :param text: string of desired date/datetime or something more verbose, + like *yesterday*, *2weeks 3days*, etc. + """ + + text = text.strip().lower() + + INPUT_FORMATS = ( + '%Y-%m-%d %H:%M:%S', + '%Y-%m-%d %H:%M', + '%Y-%m-%d', + '%m/%d/%Y %H:%M:%S', + '%m/%d/%Y %H:%M', + '%m/%d/%Y', + '%m/%d/%y %H:%M:%S', + '%m/%d/%y %H:%M', + '%m/%d/%y', + ) + for format in INPUT_FORMATS: + try: + return datetime.datetime(*time.strptime(text, format)[:6]) + except ValueError: + pass + + # Try descriptive texts + if text == 'tomorrow': + future = datetime.datetime.now() + datetime.timedelta(days=1) + args = future.timetuple()[:3] + (23, 59, 59) + return datetime.datetime(*args) + elif text == 'today': + return datetime.datetime(*datetime.datetime.today().timetuple()[:3]) + elif text == 'now': + return datetime.datetime.now() + elif text == 'yesterday': + past = datetime.datetime.now() - datetime.timedelta(days=1) + return datetime.datetime(*past.timetuple()[:3]) + else: + days = 0 + matched = re.match( + r'^((?P\d+) ?w(eeks?)?)? ?((?P\d+) ?d(ays?)?)?$', text) + if matched: + groupdict = matched.groupdict() + if groupdict['days']: + days += int(matched.groupdict()['days']) + if groupdict['weeks']: + days += int(matched.groupdict()['weeks']) * 7 + past = datetime.datetime.now() - datetime.timedelta(days=days) + return datetime.datetime(*past.timetuple()[:3]) + + raise ValueError('Wrong date: "%s"' % text) + + +def get_dict_for_attrs(obj, attrs): + """ + Returns dictionary for each attribute from given ``obj``. + """ + data = {} + for attr in attrs: + data[attr] = getattr(obj, attr) + return data + + +def get_total_seconds(timedelta): + """ + Backported for Python 2.5. + + See http://docs.python.org/library/datetime.html. + """ + return ((timedelta.microseconds + ( + timedelta.seconds + + timedelta.days * 24 * 60 * 60 + ) * 10**6) / 10**6) diff --git a/rhodecode/lib/vcs/utils/hgcompat.py b/rhodecode/lib/vcs/utils/hgcompat.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/hgcompat.py @@ -0,0 +1,12 @@ +"""Mercurial libs compatibility + +""" +from mercurial import archival, merge as hg_merge, patch, ui +from mercurial.commands import clone, nullid, pull +from mercurial.context import memctx, memfilectx +from mercurial.error import RepoError, RepoLookupError, Abort +from mercurial.hgweb.common import get_contact +from mercurial.localrepo import localrepository +from mercurial.match import match +from mercurial.mdiff import diffopts +from mercurial.node import hex diff --git a/rhodecode/lib/vcs/utils/imports.py b/rhodecode/lib/vcs/utils/imports.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/imports.py @@ -0,0 +1,27 @@ +from rhodecode.lib.vcs.exceptions import VCSError + + +def import_class(class_path): + """ + Returns class from the given path. + + For example, in order to get class located at + ``vcs.backends.hg.MercurialRepository``: + + try: + hgrepo = import_class('vcs.backends.hg.MercurialRepository') + except VCSError: + # hadle error + """ + splitted = class_path.split('.') + mod_path = '.'.join(splitted[:-1]) + class_name = splitted[-1] + try: + class_mod = __import__(mod_path, {}, {}, [class_name]) + except ImportError, err: + msg = "There was problem while trying to import backend class. "\ + "Original error was:\n%s" % err + raise VCSError(msg) + cls = getattr(class_mod, class_name) + + return cls diff --git a/rhodecode/lib/vcs/utils/lazy.py b/rhodecode/lib/vcs/utils/lazy.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/lazy.py @@ -0,0 +1,27 @@ +class LazyProperty(object): + """ + Decorator for easier creation of ``property`` from potentially expensive to + calculate attribute of the class. + + Usage:: + + class Foo(object): + @LazyProperty + def bar(self): + print 'Calculating self._bar' + return 42 + + Taken from http://blog.pythonisito.com/2008/08/lazy-descriptors.html and + used widely. + """ + + def __init__(self, func): + self._func = func + self.__name__ = func.__name__ + self.__doc__ = func.__doc__ + + def __get__(self, obj, klass=None): + if obj is None: + return None + result = obj.__dict__[self.__name__] = self._func(obj) + return result diff --git a/rhodecode/lib/vcs/utils/lockfiles.py b/rhodecode/lib/vcs/utils/lockfiles.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/lockfiles.py @@ -0,0 +1,72 @@ +import os + + +class LockFile(object): + """Provides methods to obtain, check for, and release a file based lock which + should be used to handle concurrent access to the same file. + + As we are a utility class to be derived from, we only use protected methods. + + Locks will automatically be released on destruction""" + __slots__ = ("_file_path", "_owns_lock") + + def __init__(self, file_path): + self._file_path = file_path + self._owns_lock = False + + def __del__(self): + self._release_lock() + + def _lock_file_path(self): + """:return: Path to lockfile""" + return "%s.lock" % (self._file_path) + + def _has_lock(self): + """:return: True if we have a lock and if the lockfile still exists + :raise AssertionError: if our lock-file does not exist""" + if not self._owns_lock: + return False + + return True + + def _obtain_lock_or_raise(self): + """Create a lock file as flag for other instances, mark our instance as lock-holder + + :raise IOError: if a lock was already present or a lock file could not be written""" + if self._has_lock(): + return + lock_file = self._lock_file_path() + if os.path.isfile(lock_file): + raise IOError("Lock for file %r did already exist, delete %r in case the lock is illegal" % (self._file_path, lock_file)) + + try: + fd = os.open(lock_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0) + os.close(fd) + except OSError,e: + raise IOError(str(e)) + + self._owns_lock = True + + def _obtain_lock(self): + """The default implementation will raise if a lock cannot be obtained. + Subclasses may override this method to provide a different implementation""" + return self._obtain_lock_or_raise() + + def _release_lock(self): + """Release our lock if we have one""" + if not self._has_lock(): + return + + # if someone removed our file beforhand, lets just flag this issue + # instead of failing, to make it more usable. + lfp = self._lock_file_path() + try: + # on bloody windows, the file needs write permissions to be removable. + # Why ... + if os.name == 'nt': + os.chmod(lfp, 0777) + # END handle win32 + os.remove(lfp) + except OSError: + pass + self._owns_lock = False diff --git a/rhodecode/lib/vcs/utils/ordered_dict.py b/rhodecode/lib/vcs/utils/ordered_dict.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/ordered_dict.py @@ -0,0 +1,102 @@ +"""Ordered dict implementation""" +from UserDict import DictMixin + + +class OrderedDict(dict, DictMixin): + + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next = self.__map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.__end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + return len(self) == len(other) and self.items() == other.items() + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other diff --git a/rhodecode/lib/vcs/utils/paths.py b/rhodecode/lib/vcs/utils/paths.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/paths.py @@ -0,0 +1,36 @@ +import os + +abspath = lambda * p: os.path.abspath(os.path.join(*p)) + + +def get_dirs_for_path(*paths): + """ + Returns list of directories, including intermediate. + """ + for path in paths: + head = path + while head: + head, tail = os.path.split(head) + if head: + yield head + else: + # We don't need to yield empty path + break + + +def get_dir_size(path): + root_path = path + size = 0 + for path, dirs, files in os.walk(root_path): + for f in files: + try: + size += os.path.getsize(os.path.join(path, f)) + except OSError: + pass + return size + +def get_user_home(): + """ + Returns home path of the user. + """ + return os.getenv('HOME', os.getenv('USERPROFILE')) diff --git a/rhodecode/lib/vcs/utils/progressbar.py b/rhodecode/lib/vcs/utils/progressbar.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/progressbar.py @@ -0,0 +1,419 @@ +# encoding: UTF-8 +import sys +import datetime +from string import Template +from rhodecode.lib.vcs.utils.filesize import filesizeformat +from rhodecode.lib.vcs.utils.helpers import get_total_seconds + + +class ProgressBarError(Exception): + pass + +class AlreadyFinishedError(ProgressBarError): + pass + + +class ProgressBar(object): + + default_elements = ['percentage', 'bar', 'steps'] + + def __init__(self, steps=100, stream=None, elements=None): + self.step = 0 + self.steps = steps + self.stream = stream or sys.stderr + self.bar_char = '=' + self.width = 50 + self.separator = ' | ' + self.elements = elements or self.default_elements + self.started = None + self.finished = False + self.steps_label = 'Step' + self.time_label = 'Time' + self.eta_label = 'ETA' + self.speed_label = 'Speed' + self.transfer_label = 'Transfer' + + def __str__(self): + return self.get_line() + + def __iter__(self): + start = self.step + end = self.steps + 1 + for x in xrange(start, end): + self.render(x) + yield x + + def get_separator(self): + return self.separator + + def get_bar_char(self): + return self.bar_char + + def get_bar(self): + char = self.get_bar_char() + perc = self.get_percentage() + length = int(self.width * perc / 100) + bar = char * length + bar = bar.ljust(self.width) + return bar + + def get_elements(self): + return self.elements + + def get_template(self): + separator = self.get_separator() + elements = self.get_elements() + return Template(separator.join((('$%s' % e) for e in elements))) + + def get_total_time(self, current_time=None): + if current_time is None: + current_time = datetime.datetime.now() + if not self.started: + return datetime.timedelta() + return current_time - self.started + + def get_rendered_total_time(self): + delta = self.get_total_time() + if not delta: + ttime = '-' + else: + ttime = str(delta) + return '%s %s' % (self.time_label, ttime) + + def get_eta(self, current_time=None): + if current_time is None: + current_time = datetime.datetime.now() + if self.step == 0: + return datetime.timedelta() + total_seconds = get_total_seconds(self.get_total_time()) + eta_seconds = total_seconds * self.steps / self.step - total_seconds + return datetime.timedelta(seconds=int(eta_seconds)) + + def get_rendered_eta(self): + eta = self.get_eta() + if not eta: + eta = '--:--:--' + else: + eta = str(eta).rjust(8) + return '%s: %s' % (self.eta_label, eta) + + def get_percentage(self): + return float(self.step) / self.steps * 100 + + def get_rendered_percentage(self): + perc = self.get_percentage() + return ('%s%%' % (int(perc))).rjust(5) + + def get_rendered_steps(self): + return '%s: %s/%s' % (self.steps_label, self.step, self.steps) + + def get_rendered_speed(self, step=None, total_seconds=None): + if step is None: + step = self.step + if total_seconds is None: + total_seconds = get_total_seconds(self.get_total_time()) + if step <= 0 or total_seconds <= 0: + speed = '-' + else: + speed = filesizeformat(float(step) / total_seconds) + return '%s: %s/s' % (self.speed_label, speed) + + def get_rendered_transfer(self, step=None, steps=None): + if step is None: + step = self.step + if steps is None: + steps = self.steps + + if steps <= 0: + return '%s: -' % self.transfer_label + total = filesizeformat(float(steps)) + if step <= 0: + transferred = '-' + else: + transferred = filesizeformat(float(step)) + return '%s: %s / %s' % (self.transfer_label, transferred, total) + + def get_context(self): + return { + 'percentage': self.get_rendered_percentage(), + 'bar': self.get_bar(), + 'steps': self.get_rendered_steps(), + 'time': self.get_rendered_total_time(), + 'eta': self.get_rendered_eta(), + 'speed': self.get_rendered_speed(), + 'transfer': self.get_rendered_transfer(), + } + + def get_line(self): + template = self.get_template() + context = self.get_context() + return template.safe_substitute(**context) + + def write(self, data): + self.stream.write(data) + + def render(self, step): + if not self.started: + self.started = datetime.datetime.now() + if self.finished: + raise AlreadyFinishedError + self.step = step + self.write('\r%s' % self) + if step == self.steps: + self.finished = True + if step == self.steps: + self.write('\n') + + +""" +termcolors.py + +Grabbed from Django (http://www.djangoproject.com) +""" + +color_names = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white') +foreground = dict([(color_names[x], '3%s' % x) for x in range(8)]) +background = dict([(color_names[x], '4%s' % x) for x in range(8)]) + +RESET = '0' +opt_dict = {'bold': '1', 'underscore': '4', 'blink': '5', 'reverse': '7', 'conceal': '8'} + +def colorize(text='', opts=(), **kwargs): + """ + Returns your text, enclosed in ANSI graphics codes. + + Depends on the keyword arguments 'fg' and 'bg', and the contents of + the opts tuple/list. + + Returns the RESET code if no parameters are given. + + Valid colors: + 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' + + Valid options: + 'bold' + 'underscore' + 'blink' + 'reverse' + 'conceal' + 'noreset' - string will not be auto-terminated with the RESET code + + Examples: + colorize('hello', fg='red', bg='blue', opts=('blink',)) + colorize() + colorize('goodbye', opts=('underscore',)) + print colorize('first line', fg='red', opts=('noreset',)) + print 'this should be red too' + print colorize('and so should this') + print 'this should not be red' + """ + code_list = [] + if text == '' and len(opts) == 1 and opts[0] == 'reset': + return '\x1b[%sm' % RESET + for k, v in kwargs.iteritems(): + if k == 'fg': + code_list.append(foreground[v]) + elif k == 'bg': + code_list.append(background[v]) + for o in opts: + if o in opt_dict: + code_list.append(opt_dict[o]) + if 'noreset' not in opts: + text = text + '\x1b[%sm' % RESET + return ('\x1b[%sm' % ';'.join(code_list)) + text + +def make_style(opts=(), **kwargs): + """ + Returns a function with default parameters for colorize() + + Example: + bold_red = make_style(opts=('bold',), fg='red') + print bold_red('hello') + KEYWORD = make_style(fg='yellow') + COMMENT = make_style(fg='blue', opts=('bold',)) + """ + return lambda text: colorize(text, opts, **kwargs) + +NOCOLOR_PALETTE = 'nocolor' +DARK_PALETTE = 'dark' +LIGHT_PALETTE = 'light' + +PALETTES = { + NOCOLOR_PALETTE: { + 'ERROR': {}, + 'NOTICE': {}, + 'SQL_FIELD': {}, + 'SQL_COLTYPE': {}, + 'SQL_KEYWORD': {}, + 'SQL_TABLE': {}, + 'HTTP_INFO': {}, + 'HTTP_SUCCESS': {}, + 'HTTP_REDIRECT': {}, + 'HTTP_NOT_MODIFIED': {}, + 'HTTP_BAD_REQUEST': {}, + 'HTTP_NOT_FOUND': {}, + 'HTTP_SERVER_ERROR': {}, + }, + DARK_PALETTE: { + 'ERROR': { 'fg': 'red', 'opts': ('bold',) }, + 'NOTICE': { 'fg': 'red' }, + 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) }, + 'SQL_COLTYPE': { 'fg': 'green' }, + 'SQL_KEYWORD': { 'fg': 'yellow' }, + 'SQL_TABLE': { 'opts': ('bold',) }, + 'HTTP_INFO': { 'opts': ('bold',) }, + 'HTTP_SUCCESS': { }, + 'HTTP_REDIRECT': { 'fg': 'green' }, + 'HTTP_NOT_MODIFIED': { 'fg': 'cyan' }, + 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) }, + 'HTTP_NOT_FOUND': { 'fg': 'yellow' }, + 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) }, + }, + LIGHT_PALETTE: { + 'ERROR': { 'fg': 'red', 'opts': ('bold',) }, + 'NOTICE': { 'fg': 'red' }, + 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) }, + 'SQL_COLTYPE': { 'fg': 'green' }, + 'SQL_KEYWORD': { 'fg': 'blue' }, + 'SQL_TABLE': { 'opts': ('bold',) }, + 'HTTP_INFO': { 'opts': ('bold',) }, + 'HTTP_SUCCESS': { }, + 'HTTP_REDIRECT': { 'fg': 'green', 'opts': ('bold',) }, + 'HTTP_NOT_MODIFIED': { 'fg': 'green' }, + 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) }, + 'HTTP_NOT_FOUND': { 'fg': 'red' }, + 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) }, + } +} +DEFAULT_PALETTE = DARK_PALETTE + +# ---------------------------- # +# --- End of termcolors.py --- # +# ---------------------------- # + + +class ColoredProgressBar(ProgressBar): + + BAR_COLORS = ( + (10, 'red'), + (30, 'magenta'), + (50, 'yellow'), + (99, 'green'), + (100, 'blue'), + ) + + def get_line(self): + line = super(ColoredProgressBar, self).get_line() + perc = self.get_percentage() + if perc > 100: + color = 'blue' + for max_perc, color in self.BAR_COLORS: + if perc <= max_perc: + break + return colorize(line, fg=color) + + +class AnimatedProgressBar(ProgressBar): + + def get_bar_char(self): + chars = '-/|\\' + if self.step >= self.steps: + return '=' + return chars[self.step % len(chars)] + + +class BarOnlyProgressBar(ProgressBar): + + default_elements = ['bar', 'steps'] + + def get_bar(self): + bar = super(BarOnlyProgressBar, self).get_bar() + perc = self.get_percentage() + perc_text = '%s%%' % int(perc) + text = (' %s%% ' % (perc_text)).center(self.width, '=') + L = text.find(' ') + R = text.rfind(' ') + bar = ' '.join((bar[:L], perc_text, bar[R:])) + return bar + + +class AnimatedColoredProgressBar(AnimatedProgressBar, + ColoredProgressBar): + pass + + +class BarOnlyColoredProgressBar(ColoredProgressBar, + BarOnlyProgressBar): + pass + + + +def main(): + import time + + print "Standard progress bar..." + bar = ProgressBar(30) + for x in xrange(1, 31): + bar.render(x) + time.sleep(0.02) + bar.stream.write('\n') + print + + print "Empty bar..." + bar = ProgressBar(50) + bar.render(0) + print + print + + print "Colored bar..." + bar = ColoredProgressBar(20) + for x in bar: + time.sleep(0.01) + print + + print "Animated char bar..." + bar = AnimatedProgressBar(20) + for x in bar: + time.sleep(0.01) + print + + print "Animated + colored char bar..." + bar = AnimatedColoredProgressBar(20) + for x in bar: + time.sleep(0.01) + print + + print "Bar only ..." + bar = BarOnlyProgressBar(20) + for x in bar: + time.sleep(0.01) + print + + print "Colored, longer bar-only, eta, total time ..." + bar = BarOnlyColoredProgressBar(40) + bar.width = 60 + bar.elements += ['time', 'eta'] + for x in bar: + time.sleep(0.01) + print + print + + print "File transfer bar, breaks after 2 seconds ..." + total_bytes = 1024 * 1024 * 2 + bar = ProgressBar(total_bytes) + bar.width = 50 + bar.elements.remove('steps') + bar.elements += ['transfer', 'time', 'eta', 'speed'] + for x in xrange(0, bar.steps, 1024): + bar.render(x) + time.sleep(0.01) + now = datetime.datetime.now() + if now - bar.started >= datetime.timedelta(seconds=2): + break + print + print + + + +if __name__ == '__main__': + main() diff --git a/rhodecode/lib/vcs/utils/termcolors.py b/rhodecode/lib/vcs/utils/termcolors.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/utils/termcolors.py @@ -0,0 +1,200 @@ +""" +termcolors.py + +Grabbed from Django (http://www.djangoproject.com) +""" + +color_names = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white') +foreground = dict([(color_names[x], '3%s' % x) for x in range(8)]) +background = dict([(color_names[x], '4%s' % x) for x in range(8)]) + +RESET = '0' +opt_dict = {'bold': '1', 'underscore': '4', 'blink': '5', 'reverse': '7', 'conceal': '8'} + +def colorize(text='', opts=(), **kwargs): + """ + Returns your text, enclosed in ANSI graphics codes. + + Depends on the keyword arguments 'fg' and 'bg', and the contents of + the opts tuple/list. + + Returns the RESET code if no parameters are given. + + Valid colors: + 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' + + Valid options: + 'bold' + 'underscore' + 'blink' + 'reverse' + 'conceal' + 'noreset' - string will not be auto-terminated with the RESET code + + Examples: + colorize('hello', fg='red', bg='blue', opts=('blink',)) + colorize() + colorize('goodbye', opts=('underscore',)) + print colorize('first line', fg='red', opts=('noreset',)) + print 'this should be red too' + print colorize('and so should this') + print 'this should not be red' + """ + code_list = [] + if text == '' and len(opts) == 1 and opts[0] == 'reset': + return '\x1b[%sm' % RESET + for k, v in kwargs.iteritems(): + if k == 'fg': + code_list.append(foreground[v]) + elif k == 'bg': + code_list.append(background[v]) + for o in opts: + if o in opt_dict: + code_list.append(opt_dict[o]) + if 'noreset' not in opts: + text = text + '\x1b[%sm' % RESET + return ('\x1b[%sm' % ';'.join(code_list)) + text + +def make_style(opts=(), **kwargs): + """ + Returns a function with default parameters for colorize() + + Example: + bold_red = make_style(opts=('bold',), fg='red') + print bold_red('hello') + KEYWORD = make_style(fg='yellow') + COMMENT = make_style(fg='blue', opts=('bold',)) + """ + return lambda text: colorize(text, opts, **kwargs) + +NOCOLOR_PALETTE = 'nocolor' +DARK_PALETTE = 'dark' +LIGHT_PALETTE = 'light' + +PALETTES = { + NOCOLOR_PALETTE: { + 'ERROR': {}, + 'NOTICE': {}, + 'SQL_FIELD': {}, + 'SQL_COLTYPE': {}, + 'SQL_KEYWORD': {}, + 'SQL_TABLE': {}, + 'HTTP_INFO': {}, + 'HTTP_SUCCESS': {}, + 'HTTP_REDIRECT': {}, + 'HTTP_NOT_MODIFIED': {}, + 'HTTP_BAD_REQUEST': {}, + 'HTTP_NOT_FOUND': {}, + 'HTTP_SERVER_ERROR': {}, + }, + DARK_PALETTE: { + 'ERROR': { 'fg': 'red', 'opts': ('bold',) }, + 'NOTICE': { 'fg': 'red' }, + 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) }, + 'SQL_COLTYPE': { 'fg': 'green' }, + 'SQL_KEYWORD': { 'fg': 'yellow' }, + 'SQL_TABLE': { 'opts': ('bold',) }, + 'HTTP_INFO': { 'opts': ('bold',) }, + 'HTTP_SUCCESS': { }, + 'HTTP_REDIRECT': { 'fg': 'green' }, + 'HTTP_NOT_MODIFIED': { 'fg': 'cyan' }, + 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) }, + 'HTTP_NOT_FOUND': { 'fg': 'yellow' }, + 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) }, + }, + LIGHT_PALETTE: { + 'ERROR': { 'fg': 'red', 'opts': ('bold',) }, + 'NOTICE': { 'fg': 'red' }, + 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) }, + 'SQL_COLTYPE': { 'fg': 'green' }, + 'SQL_KEYWORD': { 'fg': 'blue' }, + 'SQL_TABLE': { 'opts': ('bold',) }, + 'HTTP_INFO': { 'opts': ('bold',) }, + 'HTTP_SUCCESS': { }, + 'HTTP_REDIRECT': { 'fg': 'green', 'opts': ('bold',) }, + 'HTTP_NOT_MODIFIED': { 'fg': 'green' }, + 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) }, + 'HTTP_NOT_FOUND': { 'fg': 'red' }, + 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) }, + } +} +DEFAULT_PALETTE = DARK_PALETTE + +def parse_color_setting(config_string): + """Parse a DJANGO_COLORS environment variable to produce the system palette + + The general form of a pallete definition is: + + "palette;role=fg;role=fg/bg;role=fg,option,option;role=fg/bg,option,option" + + where: + palette is a named palette; one of 'light', 'dark', or 'nocolor'. + role is a named style used by Django + fg is a background color. + bg is a background color. + option is a display options. + + Specifying a named palette is the same as manually specifying the individual + definitions for each role. Any individual definitions following the pallete + definition will augment the base palette definition. + + Valid roles: + 'error', 'notice', 'sql_field', 'sql_coltype', 'sql_keyword', 'sql_table', + 'http_info', 'http_success', 'http_redirect', 'http_bad_request', + 'http_not_found', 'http_server_error' + + Valid colors: + 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' + + Valid options: + 'bold', 'underscore', 'blink', 'reverse', 'conceal' + + """ + if not config_string: + return PALETTES[DEFAULT_PALETTE] + + # Split the color configuration into parts + parts = config_string.lower().split(';') + palette = PALETTES[NOCOLOR_PALETTE].copy() + for part in parts: + if part in PALETTES: + # A default palette has been specified + palette.update(PALETTES[part]) + elif '=' in part: + # Process a palette defining string + definition = {} + + # Break the definition into the role, + # plus the list of specific instructions. + # The role must be in upper case + role, instructions = part.split('=') + role = role.upper() + + styles = instructions.split(',') + styles.reverse() + + # The first instruction can contain a slash + # to break apart fg/bg. + colors = styles.pop().split('/') + colors.reverse() + fg = colors.pop() + if fg in color_names: + definition['fg'] = fg + if colors and colors[-1] in color_names: + definition['bg'] = colors[-1] + + # All remaining instructions are options + opts = tuple(s for s in styles if s in opt_dict.keys()) + if opts: + definition['opts'] = opts + + # The nocolor palette has all available roles. + # Use that palette as the basis for determining + # if the role is valid. + if role in PALETTES[NOCOLOR_PALETTE] and definition: + palette[role] = definition + + # If there are no colors specified, return the empty palette. + if palette == PALETTES[NOCOLOR_PALETTE]: + return None + return palette diff --git a/rhodecode/model/__init__.py b/rhodecode/model/__init__.py --- a/rhodecode/model/__init__.py +++ b/rhodecode/model/__init__.py @@ -7,7 +7,7 @@ :created_on: Nov 25, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. @@ -56,12 +56,13 @@ def init_model(engine): :param engine: engine to bind to """ - log.info("initializing db for %s", engine) + log.info("initializing db for %s" % engine) meta.Base.metadata.bind = engine class BaseModel(object): - """Base Model for all RhodeCode models, it adds sql alchemy session + """ + 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 @@ -72,3 +73,26 @@ class BaseModel(object): self.sa = sa else: self.sa = meta.Session + + def _get_instance(self, cls, instance, callback=None): + """ + Get's instance of given cls using some simple lookup mechanism. + + :param cls: class to fetch + :param instance: int or Instance + :param callback: callback to call if all lookups failed + """ + + if isinstance(instance, cls): + return instance + elif isinstance(instance, int) or str(instance).isdigit(): + return cls.get(instance) + else: + if instance: + if callback is None: + raise Exception( + 'given object must be int or Instance of %s got %s, ' + 'no callback provided' % (cls, type(instance)) + ) + else: + return callback(instance) diff --git a/rhodecode/model/comment.py b/rhodecode/model/comment.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/comment.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.model.comment + ~~~~~~~~~~~~~~~~~~~~~~~ + + comments model for RhodeCode + + :created_on: Nov 11, 2011 + :author: marcink + :copyright: (C) 2011-2012 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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, see . + +import logging +import traceback + +from pylons.i18n.translation import _ +from sqlalchemy.util.compat import defaultdict + +from rhodecode.lib import extract_mentioned_users +from rhodecode.lib import helpers as h +from rhodecode.model import BaseModel +from rhodecode.model.db import ChangesetComment, User, Repository, Notification +from rhodecode.model.notification import NotificationModel + +log = logging.getLogger(__name__) + + +class ChangesetCommentsModel(BaseModel): + + def __get_changeset_comment(self, changeset_comment): + return self._get_instance(ChangesetComment, changeset_comment) + + def _extract_mentions(self, s): + user_objects = [] + for username in extract_mentioned_users(s): + user_obj = User.get_by_username(username, case_insensitive=True) + if user_obj: + user_objects.append(user_obj) + return user_objects + + def create(self, text, repo_id, user_id, revision, f_path=None, + line_no=None): + """ + Creates new comment for changeset + + :param text: + :param repo_id: + :param user_id: + :param revision: + :param f_path: + :param line_no: + """ + if text: + repo = Repository.get(repo_id) + cs = repo.scm_instance.get_changeset(revision) + desc = cs.message + author = cs.author_email + comment = ChangesetComment() + comment.repo = repo + comment.user_id = user_id + comment.revision = revision + comment.text = text + comment.f_path = f_path + comment.line_no = line_no + + self.sa.add(comment) + self.sa.flush() + + # make notification + line = '' + if line_no: + line = _('on line %s') % line_no + subj = h.link_to('Re commit: %(commit_desc)s %(line)s' % \ + {'commit_desc': desc, 'line': line}, + h.url('changeset_home', repo_name=repo.repo_name, + revision=revision, + anchor='comment-%s' % comment.comment_id, + qualified=True, + ) + ) + body = text + recipients = ChangesetComment.get_users(revision=revision) + # add changeset author + recipients += [User.get_by_email(author)] + + NotificationModel().create(created_by=user_id, subject=subj, + body=body, recipients=recipients, + type_=Notification.TYPE_CHANGESET_COMMENT) + + mention_recipients = set(self._extract_mentions(body))\ + .difference(recipients) + if mention_recipients: + subj = _('[Mention]') + ' ' + subj + NotificationModel().create(created_by=user_id, subject=subj, + body=body, + recipients=mention_recipients, + type_=Notification.TYPE_CHANGESET_COMMENT) + + return comment + + def delete(self, comment): + """ + Deletes given comment + + :param comment_id: + """ + comment = self.__get_changeset_comment(comment) + self.sa.delete(comment) + + return comment + + def get_comments(self, repo_id, revision): + return ChangesetComment.query()\ + .filter(ChangesetComment.repo_id == repo_id)\ + .filter(ChangesetComment.revision == revision)\ + .filter(ChangesetComment.line_no == None)\ + .filter(ChangesetComment.f_path == None).all() + + def get_inline_comments(self, repo_id, revision): + comments = self.sa.query(ChangesetComment)\ + .filter(ChangesetComment.repo_id == repo_id)\ + .filter(ChangesetComment.revision == revision)\ + .filter(ChangesetComment.line_no != None)\ + .filter(ChangesetComment.f_path != None).all() + + paths = defaultdict(lambda: defaultdict(list)) + + for co in comments: + paths[co.f_path][co.line_no].append(co) + return paths.items() diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -7,7 +7,7 @@ :created_on: Apr 08, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -27,25 +27,23 @@ import os import logging import datetime import traceback -from datetime import date +from collections import defaultdict from sqlalchemy import * from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import relationship, joinedload, class_mapper, validates from beaker.cache import cache_region, region_invalidate -from vcs import get_backend -from vcs.utils.helpers import get_scm -from vcs.exceptions import VCSError -from vcs.utils.lazy import LazyProperty +from rhodecode.lib.vcs import get_backend +from rhodecode.lib.vcs.utils.helpers import get_scm +from rhodecode.lib.vcs.exceptions import VCSError +from rhodecode.lib.vcs.utils.lazy import LazyProperty -from rhodecode.lib import str2bool, safe_str, get_changeset_safe, \ - generate_api_key, safe_unicode -from rhodecode.lib.exceptions import UsersGroupsAssignedException +from rhodecode.lib import str2bool, safe_str, get_changeset_safe, safe_unicode from rhodecode.lib.compat import json +from rhodecode.lib.caching_query import FromCache from rhodecode.model.meta import Base, Session -from rhodecode.model.caching_query import FromCache log = logging.getLogger(__name__) @@ -87,8 +85,8 @@ class ModelSerializer(json.JSONEncoder): class BaseModel(object): - """Base Model for all classess - + """ + Base Model for all classess """ @classmethod @@ -97,7 +95,8 @@ class BaseModel(object): return class_mapper(cls).c.keys() def get_dict(self): - """return dict with keys and values corresponding + """ + return dict with keys and values corresponding to this model data """ d = {} @@ -142,12 +141,14 @@ class BaseModel(object): def delete(cls, id_): obj = cls.query().get(id_) Session.delete(obj) - Session.commit() -class RhodeCodeSettings(Base, BaseModel): +class RhodeCodeSetting(Base, BaseModel): __tablename__ = 'rhodecode_settings' - __table_args__ = (UniqueConstraint('app_settings_name'), {'extend_existing':True}) + __table_args__ = ( + UniqueConstraint('app_settings_name'), + {'extend_existing': 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=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) @@ -156,7 +157,6 @@ class RhodeCodeSettings(Base, BaseModel) self.app_settings_name = k self.app_settings_value = v - @validates('_app_settings_value') def validate_settings_value(self, key, val): assert type(val) == unicode @@ -165,7 +165,7 @@ class RhodeCodeSettings(Base, BaseModel) @hybrid_property def app_settings_value(self): v = self._app_settings_value - if v == 'ldap_active': + if self.app_settings_name == 'ldap_active': v = str2bool(v) return v @@ -179,9 +179,10 @@ class RhodeCodeSettings(Base, BaseModel) self._app_settings_value = safe_unicode(val) def __repr__(self): - return "<%s('%s:%s')>" % (self.__class__.__name__, - self.app_settings_name, self.app_settings_value) - + return "<%s('%s:%s')>" % ( + self.__class__.__name__, + self.app_settings_name, self.app_settings_value + ) @classmethod def get_by_name(cls, ldap_key): @@ -218,7 +219,10 @@ class RhodeCodeSettings(Base, BaseModel) class RhodeCodeUi(Base, BaseModel): __tablename__ = 'rhodecode_ui' - __table_args__ = (UniqueConstraint('ui_key'), {'extend_existing':True}) + __table_args__ = ( + UniqueConstraint('ui_key'), + {'extend_existing': True} + ) HOOK_UPDATE = 'changegroup.update' HOOK_REPO_SIZE = 'changegroup.repo_size' @@ -231,12 +235,10 @@ class RhodeCodeUi(Base, BaseModel): ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True) - @classmethod def get_by_key(cls, key): return cls.query().filter(cls.ui_key == key) - @classmethod def get_builtin_hooks(cls): q = cls.query() @@ -263,12 +265,14 @@ class RhodeCodeUi(Base, BaseModel): new_ui.ui_value = val Session.add(new_ui) - Session.commit() class User(Base, BaseModel): __tablename__ = 'users' - __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'extend_existing':True}) + __table_args__ = ( + UniqueConstraint('username'), UniqueConstraint('email'), + {'extend_existing': True} + ) user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) @@ -286,10 +290,12 @@ class User(Base, BaseModel): repositories = relationship('Repository') user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all') - repo_to_perm = relationship('RepoToPerm', primaryjoin='RepoToPerm.user_id==User.user_id', cascade='all') + repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all') group_member = relationship('UsersGroupMember', cascade='all') + notifications = relationship('UserNotification',) + @hybrid_property def email(self): return self._email @@ -303,6 +309,11 @@ class User(Base, BaseModel): return '%s %s' % (self.name, self.lastname) @property + def full_name_or_username(self): + return ('%s %s' % (self.name, self.lastname) + if (self.name and self.lastname) else self.username) + + @property def full_contact(self): return '%s %s <%s>' % (self.name, self.lastname, self.email) @@ -315,60 +326,64 @@ class User(Base, BaseModel): return self.admin def __repr__(self): - try: - return "<%s('id:%s:%s')>" % (self.__class__.__name__, - self.user_id, self.username) - except: - return self.__class__.__name__ + return "<%s('id:%s:%s')>" % (self.__class__.__name__, + self.user_id, self.username) - def __json__(self): - return {'email': self.email} + @classmethod + def get_by_username(cls, username, case_insensitive=False, cache=False): + if case_insensitive: + q = cls.query().filter(cls.username.ilike(username)) + else: + q = cls.query().filter(cls.username == username) + + if cache: + q = q.options(FromCache("sql_cache_short", + "get_user_%s" % username)) + return q.scalar() @classmethod - def get_by_username(cls, username, case_insensitive=False): - if case_insensitive: - return Session.query(cls).filter(cls.username.ilike(username)).scalar() - else: - return Session.query(cls).filter(cls.username == username).scalar() + def get_by_api_key(cls, api_key, cache=False): + q = cls.query().filter(cls.api_key == api_key) + + if cache: + q = q.options(FromCache("sql_cache_short", + "get_api_key_%s" % api_key)) + return q.scalar() @classmethod - def get_by_api_key(cls, api_key): - return cls.query().filter(cls.api_key == api_key).one() + def get_by_email(cls, email, case_insensitive=False, cache=False): + if case_insensitive: + q = cls.query().filter(cls.email.ilike(email)) + else: + q = cls.query().filter(cls.email == email) + + if cache: + q = q.options(FromCache("sql_cache_short", + "get_api_key_%s" % email)) + return q.scalar() def update_lastlogin(self): """Update user lastlogin""" - self.last_login = datetime.datetime.now() Session.add(self) - Session.commit() - log.debug('updated user %s lastlogin', self.username) - - @classmethod - def create(cls, form_data): - from rhodecode.lib.auth import get_crypt_password + log.debug('updated user %s lastlogin' % self.username) - try: - new_user = cls() - for k, v in form_data.items(): - if k == 'password': - v = get_crypt_password(v) - setattr(new_user, k, v) + def __json__(self): + return dict( + email=self.email, + full_name=self.full_name, + full_name_or_username=self.full_name_or_username, + short_contact=self.short_contact, + full_contact=self.full_contact + ) - new_user.api_key = generate_api_key(form_data['username']) - Session.add(new_user) - Session.commit() - return new_user - except: - log.error(traceback.format_exc()) - Session.rollback() - raise class UserLog(Base, BaseModel): __tablename__ = 'user_logs' - __table_args__ = {'extend_existing':True} + __table_args__ = {'extend_existing': 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('users.user_id'), nullable=False, unique=None, default=None) - repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True) repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) @@ -376,15 +391,15 @@ class UserLog(Base, BaseModel): @property def action_as_day(self): - return date(*self.action_date.timetuple()[:3]) + return datetime.date(*self.action_date.timetuple()[:3]) user = relationship('User') - repository = relationship('Repository') + repository = relationship('Repository',cascade='') class UsersGroup(Base, BaseModel): __tablename__ = 'users_groups' - __table_args__ = {'extend_existing':True} + __table_args__ = {'extend_existing': True} users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None) @@ -396,18 +411,16 @@ class UsersGroup(Base, BaseModel): return '' % (self.users_group_name) @classmethod - def get_by_group_name(cls, group_name, cache=False, case_insensitive=False): + def get_by_group_name(cls, group_name, cache=False, + case_insensitive=False): if case_insensitive: - gr = cls.query()\ - .filter(cls.users_group_name.ilike(group_name)) + q = cls.query().filter(cls.users_group_name.ilike(group_name)) else: - gr = cls.query()\ - .filter(cls.users_group_name == group_name) + q = cls.query().filter(cls.users_group_name == group_name) if cache: - gr = gr.options(FromCache("sql_cache_short", - "get_user_%s" % group_name)) - return gr.scalar() - + q = q.options(FromCache("sql_cache_short", + "get_user_%s" % group_name)) + return q.scalar() @classmethod def get(cls, users_group_id, cache=False): @@ -417,71 +430,10 @@ class UsersGroup(Base, BaseModel): "get_users_group_%s" % users_group_id)) return users_group.get(users_group_id) - @classmethod - def create(cls, form_data): - try: - new_users_group = cls() - for k, v in form_data.items(): - setattr(new_users_group, k, v) - - Session.add(new_users_group) - Session.commit() - return new_users_group - except: - log.error(traceback.format_exc()) - Session.rollback() - raise - - @classmethod - def update(cls, users_group_id, form_data): - - try: - users_group = cls.get(users_group_id, cache=False) - - for k, v in form_data.items(): - if k == 'users_group_members': - users_group.members = [] - Session.flush() - members_list = [] - if v: - v = [v] if isinstance(v, basestring) else v - for u_id in set(v): - member = UsersGroupMember(users_group_id, u_id) - members_list.append(member) - setattr(users_group, 'members', members_list) - setattr(users_group, k, v) - - Session.add(users_group) - Session.commit() - except: - log.error(traceback.format_exc()) - Session.rollback() - raise - - @classmethod - def delete(cls, users_group_id): - try: - - # check if this group is not assigned to repo - assigned_groups = UsersGroupRepoToPerm.query()\ - .filter(UsersGroupRepoToPerm.users_group_id == - users_group_id).all() - - if assigned_groups: - raise UsersGroupsAssignedException('Group assigned to %s' % - assigned_groups) - - users_group = cls.get(users_group_id, cache=False) - Session.delete(users_group) - Session.commit() - except: - log.error(traceback.format_exc()) - Session.rollback() - raise class UsersGroupMember(Base, BaseModel): __tablename__ = 'users_groups_members' - __table_args__ = {'extend_existing':True} + __table_args__ = {'extend_existing': True} users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) @@ -494,18 +446,13 @@ class UsersGroupMember(Base, BaseModel): self.users_group_id = gr_id self.user_id = u_id - @staticmethod - def add_user_to_group(group, user): - ugm = UsersGroupMember() - ugm.users_group = group - ugm.user = user - Session.add(ugm) - Session.commit() - return ugm class Repository(Base, BaseModel): __tablename__ = 'repositories' - __table_args__ = (UniqueConstraint('repo_name'), {'extend_existing':True},) + __table_args__ = ( + UniqueConstraint('repo_name'), + {'extend_existing': True}, + ) repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None) @@ -521,17 +468,16 @@ class Repository(Base, BaseModel): fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None) group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None) - user = relationship('User') fork = relationship('Repository', remote_side=repo_id) - group = relationship('Group') - repo_to_perm = relationship('RepoToPerm', cascade='all', order_by='RepoToPerm.repo_to_perm_id') + group = relationship('RepoGroup') + repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id') users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all') stats = relationship('Statistics', cascade='all', uselist=False) followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all') - logs = relationship('UserLog', cascade='all') + logs = relationship('UserLog') def __repr__(self): return "<%s('%s:%s')>" % (self.__class__.__name__, @@ -547,7 +493,7 @@ class Repository(Base, BaseModel): q = q.options(joinedload(Repository.fork))\ .options(joinedload(Repository.user))\ .options(joinedload(Repository.group)) - return q.one() + return q.scalar() @classmethod def get_repo_forks(cls, repo_id): @@ -560,9 +506,9 @@ class Repository(Base, BaseModel): :param cls: """ - q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == - cls.url_sep()) - q.options(FromCache("sql_cache_short", "repository_repo_path")) + q = Session.query(RhodeCodeUi)\ + .filter(RhodeCodeUi.ui_key == cls.url_sep()) + q = q.options(FromCache("sql_cache_short", "repository_repo_path")) return q.one().ui_value @property @@ -598,7 +544,7 @@ class Repository(Base, BaseModel): """ q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == Repository.url_sep()) - q.options(FromCache("sql_cache_short", "repository_repo_path")) + q = q.options(FromCache("sql_cache_short", "repository_repo_path")) return q.one().ui_value @property @@ -633,7 +579,6 @@ class Repository(Base, BaseModel): baseui._ucfg = config.config() baseui._tcfg = config.config() - ret = RhodeCodeUi.query()\ .options(FromCache("sql_cache_short", "repository_repo_ui")).all() @@ -651,14 +596,13 @@ class Repository(Base, BaseModel): """ returns True if given repo name is a valid filesystem repository - @param cls: - @param repo_name: + :param cls: + :param repo_name: """ from rhodecode.lib.utils import is_valid_repo return is_valid_repo(repo_name, cls.base_path()) - #========================================================================== # SCM PROPERTIES #========================================================================== @@ -678,35 +622,34 @@ class Repository(Base, BaseModel): def last_change(self): return self.scm_instance.last_change + def comments(self, revisions=None): + """ + Returns comments for this repository grouped by revisions + + :param revisions: filter query by revisions only + """ + cmts = ChangesetComment.query()\ + .filter(ChangesetComment.repo == self) + if revisions: + cmts = cmts.filter(ChangesetComment.revision.in_(revisions)) + grouped = defaultdict(list) + for cmt in cmts.all(): + grouped[cmt.revision].append(cmt) + return grouped + #========================================================================== # SCM CACHE INSTANCE #========================================================================== @property def invalidate(self): - """ - Returns Invalidation object if this repo should be invalidated - None otherwise. `cache_active = False` means that this cache - state is not valid and needs to be invalidated - """ - return CacheInvalidation.query()\ - .filter(CacheInvalidation.cache_key == self.repo_name)\ - .filter(CacheInvalidation.cache_active == False)\ - .scalar() + return CacheInvalidation.invalidate(self.repo_name) def set_invalidate(self): """ set a cache for invalidation for this instance """ - inv = CacheInvalidation.query()\ - .filter(CacheInvalidation.cache_key == self.repo_name)\ - .scalar() - - if inv is None: - inv = CacheInvalidation(self.repo_name) - inv.cache_active = True - Session.add(inv) - Session.commit() + CacheInvalidation.set_invalidate(self.repo_name) @LazyProperty def scm_instance(self): @@ -717,28 +660,20 @@ class Repository(Base, BaseModel): @cache_region('long_term') def _c(repo_name): return self.__get_instance() - - # TODO: remove this trick when beaker 1.6 is released - # and have fixed this issue with not supporting unicode keys - rn = safe_str(self.repo_name) - + rn = self.repo_name + log.debug('Getting cached instance of repo') inv = self.invalidate if inv is not None: region_invalidate(_c, None, rn) # update our cache - inv.cache_active = True - Session.add(inv) - Session.commit() - + CacheInvalidation.set_valid(inv.cache_key) return _c(rn) def __get_instance(self): - repo_full_path = self.repo_full_path - try: alias = get_scm(repo_full_path)[0] - log.debug('Creating instance of %s repository', alias) + log.debug('Creating instance of %s repository' % alias) backend = get_backend(alias) except VCSError: log.error(traceback.format_exc()) @@ -751,7 +686,7 @@ class Repository(Base, BaseModel): repo = backend(safe_str(repo_full_path), create=False, baseui=self._ui) - #skip hidden web repository + # skip hidden web repository if repo._get_hidden(): return else: @@ -760,19 +695,24 @@ class Repository(Base, BaseModel): return repo -class Group(Base, BaseModel): +class RepoGroup(Base, BaseModel): __tablename__ = 'groups' - __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'), - CheckConstraint('group_id != group_parent_id'), {'extend_existing':True},) - __mapper_args__ = {'order_by':'group_name'} + __table_args__ = ( + UniqueConstraint('group_name', 'group_parent_id'), + CheckConstraint('group_id != group_parent_id'), + {'extend_existing': True}, + ) + __mapper_args__ = {'order_by': 'group_name'} group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None) group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None) group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - parent_group = relationship('Group', remote_side=group_id) + repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id') + users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all') + parent_group = relationship('RepoGroup', remote_side=group_id) def __init__(self, group_name='', parent_group=None): self.group_name = group_name @@ -838,11 +778,11 @@ class Group(Base, BaseModel): @property def children(self): - return Group.query().filter(Group.parent_group == self) + return RepoGroup.query().filter(RepoGroup.parent_group == self) @property def name(self): - return self.group_name.split(Group.url_sep())[-1] + return self.group_name.split(RepoGroup.url_sep())[-1] @property def full_path(self): @@ -850,7 +790,7 @@ class Group(Base, BaseModel): @property def full_path_splitted(self): - return self.group_name.split(Group.url_sep()) + return self.group_name.split(RepoGroup.url_sep()) @property def repositories(self): @@ -869,93 +809,100 @@ class Group(Base, BaseModel): return cnt + children_count(self) - def get_new_name(self, group_name): """ returns new full group name based on parent and new name :param group_name: """ - path_prefix = (self.parent_group.full_path_splitted if + path_prefix = (self.parent_group.full_path_splitted if self.parent_group else []) - return Group.url_sep().join(path_prefix + [group_name]) + return RepoGroup.url_sep().join(path_prefix + [group_name]) class Permission(Base, BaseModel): __tablename__ = 'permissions' - __table_args__ = {'extend_existing':True} + __table_args__ = {'extend_existing': True} permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) def __repr__(self): - return "<%s('%s:%s')>" % (self.__class__.__name__, - self.permission_id, self.permission_name) + return "<%s('%s:%s')>" % ( + self.__class__.__name__, self.permission_id, self.permission_name + ) @classmethod def get_by_key(cls, key): return cls.query().filter(cls.permission_name == key).scalar() -class RepoToPerm(Base, BaseModel): + @classmethod + def get_default_perms(cls, default_user_id): + q = Session.query(UserRepoToPerm, Repository, cls)\ + .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\ + .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\ + .filter(UserRepoToPerm.user_id == default_user_id) + + return q.all() + + @classmethod + def get_default_group_perms(cls, default_user_id): + q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\ + .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\ + .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\ + .filter(UserRepoGroupToPerm.user_id == default_user_id) + + return q.all() + + +class UserRepoToPerm(Base, BaseModel): __tablename__ = 'repo_to_perm' - __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'extend_existing':True}) + __table_args__ = ( + UniqueConstraint('user_id', 'repository_id', 'permission_id'), + {'extend_existing': 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('users.user_id'), nullable=False, unique=None, default=None) permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) user = relationship('User') + repository = relationship('Repository') permission = relationship('Permission') - repository = relationship('Repository') + + @classmethod + def create(cls, user, repository, permission): + n = cls() + n.user = user + n.repository = repository + n.permission = permission + Session.add(n) + return n + + def __repr__(self): + return ' %s >' % (self.user, self.repository) + class UserToPerm(Base, BaseModel): __tablename__ = 'user_to_perm' - __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'extend_existing':True}) + __table_args__ = ( + UniqueConstraint('user_id', 'permission_id'), + {'extend_existing': 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('users.user_id'), nullable=False, unique=None, default=None) permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) user = relationship('User') - permission = relationship('Permission') - - @classmethod - def has_perm(cls, user_id, perm): - if not isinstance(perm, Permission): - raise Exception('perm needs to be an instance of Permission class') - - return cls.query().filter(cls.user_id == user_id)\ - .filter(cls.permission == perm).scalar() is not None - - @classmethod - def grant_perm(cls, user_id, perm): - if not isinstance(perm, Permission): - raise Exception('perm needs to be an instance of Permission class') + permission = relationship('Permission', lazy='joined') - new = cls() - new.user_id = user_id - new.permission = perm - try: - Session.add(new) - Session.commit() - except: - Session.rollback() - - - @classmethod - def revoke_perm(cls, user_id, perm): - if not isinstance(perm, Permission): - raise Exception('perm needs to be an instance of Permission class') - - try: - cls.query().filter(cls.user_id == user_id)\ - .filter(cls.permission == perm).delete() - Session.commit() - except: - Session.rollback() class UsersGroupRepoToPerm(Base, BaseModel): __tablename__ = 'users_group_repo_to_perm' - __table_args__ = (UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), {'extend_existing':True}) + __table_args__ = ( + UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), + {'extend_existing': True} + ) users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) @@ -965,11 +912,25 @@ class UsersGroupRepoToPerm(Base, BaseMod permission = relationship('Permission') repository = relationship('Repository') + @classmethod + def create(cls, users_group, repository, permission): + n = cls() + n.users_group = users_group + n.repository = repository + n.permission = permission + Session.add(n) + return n + def __repr__(self): return ' %s >' % (self.users_group, self.repository) + class UsersGroupToPerm(Base, BaseModel): __tablename__ = 'users_group_to_perm' + __table_args__ = ( + UniqueConstraint('users_group_id', 'permission_id',), + {'extend_existing': True} + ) users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) @@ -978,60 +939,43 @@ class UsersGroupToPerm(Base, BaseModel): permission = relationship('Permission') - @classmethod - def has_perm(cls, users_group_id, perm): - if not isinstance(perm, Permission): - raise Exception('perm needs to be an instance of Permission class') - - return cls.query().filter(cls.users_group_id == - users_group_id)\ - .filter(cls.permission == perm)\ - .scalar() is not None - - @classmethod - def grant_perm(cls, users_group_id, perm): - if not isinstance(perm, Permission): - raise Exception('perm needs to be an instance of Permission class') - - new = cls() - new.users_group_id = users_group_id - new.permission = perm - try: - Session.add(new) - Session.commit() - except: - Session.rollback() - - - @classmethod - def revoke_perm(cls, users_group_id, perm): - if not isinstance(perm, Permission): - raise Exception('perm needs to be an instance of Permission class') - - try: - cls.query().filter(cls.users_group_id == users_group_id)\ - .filter(cls.permission == perm).delete() - Session.commit() - except: - Session.rollback() - - -class GroupToPerm(Base, BaseModel): - __tablename__ = 'group_to_perm' - __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing':True}) +class UserRepoGroupToPerm(Base, BaseModel): + __tablename__ = 'user_repo_group_to_perm' + __table_args__ = ( + UniqueConstraint('user_id', 'group_id', 'permission_id'), + {'extend_existing': True} + ) group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None) permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) - group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None) user = relationship('User') + group = relationship('RepoGroup') permission = relationship('Permission') - group = relationship('Group') + + +class UsersGroupRepoGroupToPerm(Base, BaseModel): + __tablename__ = 'users_group_repo_group_to_perm' + __table_args__ = ( + UniqueConstraint('users_group_id', 'group_id'), + {'extend_existing': True} + ) + + users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + users_group = relationship('UsersGroup') + permission = relationship('Permission') + group = relationship('RepoGroup') + class Statistics(Base, BaseModel): __tablename__ = 'statistics' - __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing':True}) + __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing': True}) stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None) stat_on_revision = Column("stat_on_revision", Integer(), nullable=False) @@ -1041,11 +985,14 @@ class Statistics(Base, BaseModel): repository = relationship('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') - , {'extend_existing':True}) + __table_args__ = ( + UniqueConstraint('user_id', 'follows_repository_id'), + UniqueConstraint('user_id', 'follows_user_id'), + {'extend_existing': 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('users.user_id'), nullable=False, unique=None, default=None) @@ -1058,20 +1005,19 @@ class UserFollowing(Base, BaseModel): follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id') follows_repository = relationship('Repository', order_by='Repository.repo_name') - @classmethod def get_repo_followers(cls, repo_id): return cls.query().filter(cls.follows_repo_id == repo_id) + class CacheInvalidation(Base, BaseModel): __tablename__ = 'cache_invalidation' - __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing':True}) + __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing': True}) cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) cache_args = Column("cache_args", String(length=255, 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 @@ -1081,10 +1027,177 @@ class CacheInvalidation(Base, BaseModel) return "<%s('%s:%s')>" % (self.__class__.__name__, self.cache_id, self.cache_key) + @classmethod + def _get_key(cls, key): + """ + Wrapper for generating a key + + :param key: + """ + import rhodecode + prefix = '' + iid = rhodecode.CONFIG.get('instance_id') + if iid: + prefix = iid + return "%s%s" % (prefix, key) + + @classmethod + def get_by_key(cls, key): + return cls.query().filter(cls.cache_key == key).scalar() + + @classmethod + def invalidate(cls, key): + """ + Returns Invalidation object if this given key should be invalidated + None otherwise. `cache_active = False` means that this cache + state is not valid and needs to be invalidated + + :param key: + """ + return cls.query()\ + .filter(CacheInvalidation.cache_key == key)\ + .filter(CacheInvalidation.cache_active == False)\ + .scalar() + + @classmethod + def set_invalidate(cls, key): + """ + Mark this Cache key for invalidation + + :param key: + """ + + log.debug('marking %s for invalidation' % key) + inv_obj = Session.query(cls)\ + .filter(cls.cache_key == key).scalar() + if inv_obj: + inv_obj.cache_active = False + else: + log.debug('cache key not found in invalidation db -> creating one') + inv_obj = CacheInvalidation(key) + + try: + Session.add(inv_obj) + Session.commit() + except Exception: + log.error(traceback.format_exc()) + Session.rollback() + + @classmethod + def set_valid(cls, key): + """ + Mark this cache key as active and currently cached + + :param key: + """ + inv_obj = cls.get_by_key(key) + inv_obj.cache_active = True + Session.add(inv_obj) + Session.commit() + + +class ChangesetComment(Base, BaseModel): + __tablename__ = 'changeset_comments' + __table_args__ = ({'extend_existing': True},) + comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True) + repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False) + revision = Column('revision', String(40), nullable=False) + line_no = Column('line_no', Unicode(10), nullable=True) + f_path = Column('f_path', Unicode(1000), nullable=True) + user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False) + text = Column('text', Unicode(25000), nullable=False) + modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now) + + author = relationship('User', lazy='joined') + repo = relationship('Repository') + + @classmethod + def get_users(cls, revision): + """ + Returns user associated with this changesetComment. ie those + who actually commented + + :param cls: + :param revision: + """ + return Session.query(User)\ + .filter(cls.revision == revision)\ + .join(ChangesetComment.author).all() + + +class Notification(Base, BaseModel): + __tablename__ = 'notifications' + __table_args__ = ({'extend_existing': True},) + + TYPE_CHANGESET_COMMENT = u'cs_comment' + TYPE_MESSAGE = u'message' + TYPE_MENTION = u'mention' + TYPE_REGISTRATION = u'registration' + + notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True) + subject = Column('subject', Unicode(512), nullable=True) + body = Column('body', Unicode(50000), nullable=True) + created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + type_ = Column('type', Unicode(256)) + + created_by_user = relationship('User') + notifications_to_users = relationship('UserNotification', lazy='joined', + cascade="all, delete, delete-orphan") + + @property + def recipients(self): + return [x.user for x in UserNotification.query()\ + .filter(UserNotification.notification == self).all()] + + @classmethod + def create(cls, created_by, subject, body, recipients, type_=None): + if type_ is None: + type_ = Notification.TYPE_MESSAGE + + notification = cls() + notification.created_by_user = created_by + notification.subject = subject + notification.body = body + notification.type_ = type_ + notification.created_on = datetime.datetime.now() + + for u in recipients: + assoc = UserNotification() + assoc.notification = notification + u.notifications.append(assoc) + Session.add(notification) + return notification + + @property + def description(self): + from rhodecode.model.notification import NotificationModel + return NotificationModel().make_description(self) + + +class UserNotification(Base, BaseModel): + __tablename__ = 'user_to_notification' + __table_args__ = ( + UniqueConstraint('user_id', 'notification_id'), + {'extend_existing': True} + ) + user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True) + notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True) + read = Column('read', Boolean, default=False) + sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None) + + user = relationship('User', lazy="joined") + notification = relationship('Notification', lazy="joined", + order_by=lambda: Notification.created_on.desc(),) + + def mark_as_read(self): + self.read = True + Session.add(self) + + class DbMigrateVersion(Base, BaseModel): __tablename__ = 'db_migrate_version' - __table_args__ = {'extend_existing':True} + __table_args__ = {'extend_existing': True} repository_id = Column('repository_id', String(250), primary_key=True) repository_path = Column('repository_path', Text) version = Column('version', Integer) - diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -36,28 +36,33 @@ from rhodecode.config.routing import ADM from rhodecode.lib.utils import repo_name_slug from rhodecode.lib.auth import authenticate, get_crypt_password from rhodecode.lib.exceptions import LdapImportError -from rhodecode.model.user import UserModel -from rhodecode.model.repo import RepoModel -from rhodecode.model.db import User, UsersGroup, Group, Repository +from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository from rhodecode import BACKENDS log = logging.getLogger(__name__) + #this is needed to translate the messages using _() in validators class State_obj(object): _ = staticmethod(_) + #============================================================================== # VALIDATORS #============================================================================== class ValidAuthToken(formencode.validators.FancyValidator): - messages = {'invalid_token':_('Token mismatch')} + messages = {'invalid_token': _('Token mismatch')} def validate_python(self, value, state): if value != authentication_token(): - raise formencode.Invalid(self.message('invalid_token', state, - search_number=value), value, state) + raise formencode.Invalid( + self.message('invalid_token', + state, search_number=value), + value, + state + ) + def ValidUsername(edit, old_data): class _ValidUsername(formencode.validators.FancyValidator): @@ -68,7 +73,7 @@ def ValidUsername(edit, old_data): #check if user is unique old_un = None if edit: - old_un = UserModel().get(old_data.get('user_id')).username + old_un = User.get(old_data.get('user_id')).username if old_un != value or not edit: if User.get_by_username(value, case_insensitive=True): @@ -76,11 +81,13 @@ def ValidUsername(edit, old_data): 'exists') , value, state) 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, periods or dashes ' - 'and must begin with alphanumeric ' - 'character'), value, state) + raise formencode.Invalid( + _('Username may only contain alphanumeric characters ' + 'underscores, periods or dashes and must begin with ' + 'alphanumeric character'), + value, + state + ) return _ValidUsername @@ -102,16 +109,17 @@ def ValidUsersGroup(edit, old_data): if UsersGroup.get_by_group_name(value, cache=False, case_insensitive=True): raise formencode.Invalid(_('This users group ' - 'already exists') , value, + 'already exists'), value, state) - if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None: - raise formencode.Invalid(_('Group name may only contain ' - 'alphanumeric characters ' - 'underscores, periods or dashes ' - 'and must begin with alphanumeric ' - 'character'), value, state) + raise formencode.Invalid( + _('RepoGroup name may only contain alphanumeric characters ' + 'underscores, periods or dashes and must begin with ' + 'alphanumeric character'), + value, + state + ) return _ValidUsersGroup @@ -141,15 +149,14 @@ def ValidReposGroup(edit, old_data): old_gname = None if edit: - old_gname = Group.get( - old_data.get('group_id')).group_name + old_gname = RepoGroup.get(old_data.get('group_id')).group_name if old_gname != group_name or not edit: # check group - gr = Group.query()\ - .filter(Group.group_name == slug)\ - .filter(Group.group_parent_id == group_parent_id)\ + gr = RepoGroup.query()\ + .filter(RepoGroup.group_name == slug)\ + .filter(RepoGroup.group_parent_id == group_parent_id)\ .scalar() if gr: @@ -173,58 +180,64 @@ def ValidReposGroup(edit, old_data): return _ValidReposGroup + class ValidPassword(formencode.validators.FancyValidator): def to_python(self, value, state): - if value: + if not value: + return - 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'): + 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('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) + 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 + return value + class ValidPasswordsMatch(formencode.validators.FancyValidator): def validate_python(self, value, state): - + pass_val = value.get('password') or value.get('new_password') if pass_val != value['password_confirmation']: e_dict = {'password_confirmation': _('Passwords 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 account is disabled') } - + # error mapping - e_dict = {'username':messages['invalid_login'], - 'password':messages['invalid_password']} - e_dict_disable = {'username':messages['disabled_account']} + 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'] @@ -235,16 +248,21 @@ class ValidAuth(formencode.validators.Fa 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) + 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) + log.warning('user %s failed to authenticate' % username) + raise formencode.Invalid( + self.message('invalid_password', + state=State_obj), value, state, + error_dict=self.e_dict + ) + class ValidRepoUser(formencode.validators.FancyValidator): @@ -257,6 +275,7 @@ class ValidRepoUser(formencode.validator value, state) return value + def ValidRepoName(edit, old_data): class _ValidRepoName(formencode.validators.FancyValidator): def to_python(self, value, state): @@ -268,41 +287,41 @@ def ValidRepoName(edit, old_data): e_dict = {'repo_name': _('This repository name is disallowed')} raise formencode.Invalid('', value, state, error_dict=e_dict) - if value.get('repo_group'): - gr = Group.get(value.get('repo_group')) + gr = RepoGroup.get(value.get('repo_group')) group_path = gr.full_path # value needs to be aware of group name in order to check # db key This is an actual just the name to store in the # database - repo_name_full = group_path + Group.url_sep() + repo_name - + repo_name_full = group_path + RepoGroup.url_sep() + repo_name + else: group_path = '' repo_name_full = repo_name - value['repo_name_full'] = repo_name_full rename = old_data.get('repo_name') != repo_name_full create = not edit if rename or create: if group_path != '': - if RepoModel().get_by_repo_name(repo_name_full,): - e_dict = {'repo_name':_('This repository already ' - 'exists in a group "%s"') % - gr.group_name} + if Repository.get_by_repo_name(repo_name_full): + e_dict = { + 'repo_name': _('This repository already exists in ' + 'a group "%s"') % gr.group_name + } raise formencode.Invalid('', value, state, error_dict=e_dict) - elif Group.get_by_group_name(repo_name_full): - e_dict = {'repo_name':_('There is a group with this' - ' name already "%s"') % - repo_name_full} + elif RepoGroup.get_by_group_name(repo_name_full): + e_dict = { + 'repo_name': _('There is a group with this name ' + 'already "%s"') % repo_name_full + } raise formencode.Invalid('', value, state, error_dict=e_dict) - elif RepoModel().get_by_repo_name(repo_name_full): - e_dict = {'repo_name':_('This repository ' + elif Repository.get_by_repo_name(repo_name_full): + e_dict = {'repo_name': _('This repository ' 'already exists')} raise formencode.Invalid('', value, state, error_dict=e_dict) @@ -311,24 +330,9 @@ def ValidRepoName(edit, old_data): return _ValidRepoName -def ValidForkName(): - class _ValidForkName(formencode.validators.FancyValidator): - def to_python(self, value, state): - repo_name = value.get('fork_name') - - slug = repo_name_slug(repo_name) - if slug in [ADMIN_PREFIX, '']: - e_dict = {'repo_name': _('This repository name is disallowed')} - raise formencode.Invalid('', value, state, error_dict=e_dict) - - if RepoModel().get_by_repo_name(repo_name): - e_dict = {'fork_name':_('This repository ' - 'already exists')} - raise formencode.Invalid('', value, state, - error_dict=e_dict) - return value - return _ValidForkName +def ValidForkName(*args, **kwargs): + return ValidRepoName(*args, **kwargs) def SlugifyName(): @@ -339,6 +343,7 @@ def SlugifyName(): return _SlugifyName + def ValidCloneUri(): from mercurial.httprepo import httprepository, httpsrepository from rhodecode.lib.utils import make_ui @@ -351,14 +356,14 @@ def ValidCloneUri(): elif value.startswith('https'): try: httpsrepository(make_ui('db'), value).capabilities - except Exception, e: + except Exception: log.error(traceback.format_exc()) raise formencode.Invalid(_('invalid clone url'), value, state) elif value.startswith('http'): try: httprepository(make_ui('db'), value).capabilities - except Exception, e: + except Exception: log.error(traceback.format_exc()) raise formencode.Invalid(_('invalid clone url'), value, state) @@ -370,6 +375,7 @@ def ValidCloneUri(): return _ValidCloneUri + def ValidForkType(old_data): class _ValidForkType(formencode.validators.FancyValidator): @@ -381,64 +387,77 @@ def ValidForkType(old_data): return value return _ValidForkType -class ValidPerms(formencode.validators.FancyValidator): - messages = {'perm_new_member_name':_('This username or users group name' - ' is not valid')} + +def ValidPerms(type_='repo'): + if type_ == 'group': + EMPTY_PERM = 'group.none' + elif type_ == 'repo': + EMPTY_PERM = 'repository.none' - def to_python(self, value, state): - perms_update = [] - perms_new = [] - #build a list of permission to update and new permission to create - for k, v in value.items(): - #means new added member to permissions - if k.startswith('perm_new_member'): - new_perm = value.get('perm_new_member', False) - new_member = value.get('perm_new_member_name', False) - new_type = value.get('perm_new_member_type') + class _ValidPerms(formencode.validators.FancyValidator): + messages = { + 'perm_new_member_name': + _('This username or users group name is not valid') + } + + def to_python(self, value, state): + perms_update = [] + perms_new = [] + # build a list of permission to update and new permission to create + for k, v in value.items(): + # means new added member to permissions + if k.startswith('perm_new_member'): + new_perm = value.get('perm_new_member', False) + new_member = value.get('perm_new_member_name', False) + new_type = value.get('perm_new_member_type') - if new_member and new_perm: - if (new_member, new_perm, new_type) not in perms_new: - perms_new.append((new_member, new_perm, new_type)) - elif k.startswith('u_perm_') or k.startswith('g_perm_'): - member = k[7:] - t = {'u':'user', - 'g':'users_group'}[k[0]] - if member == 'default': - if value['private']: - #set none for default when updating to private repo - v = 'repository.none' - perms_update.append((member, v, t)) + if new_member and new_perm: + if (new_member, new_perm, new_type) not in perms_new: + perms_new.append((new_member, new_perm, new_type)) + elif k.startswith('u_perm_') or k.startswith('g_perm_'): + member = k[7:] + t = {'u': 'user', + 'g': 'users_group' + }[k[0]] + if member == 'default': + if value.get('private'): + # set none for default when updating to private repo + v = EMPTY_PERM + perms_update.append((member, v, t)) - value['perms_updates'] = perms_update - value['perms_new'] = perms_new + value['perms_updates'] = perms_update + value['perms_new'] = perms_new - #update permissions - for k, v, t in perms_new: - try: - if t is 'user': - self.user_db = User.query()\ - .filter(User.active == True)\ - .filter(User.username == k).one() - if t is 'users_group': - self.user_db = UsersGroup.query()\ - .filter(UsersGroup.users_group_active == True)\ - .filter(UsersGroup.users_group_name == k).one() + # update permissions + for k, v, t in perms_new: + try: + if t is 'user': + self.user_db = User.query()\ + .filter(User.active == True)\ + .filter(User.username == k).one() + if t is 'users_group': + self.user_db = UsersGroup.query()\ + .filter(UsersGroup.users_group_active == True)\ + .filter(UsersGroup.users_group_name == k).one() - except Exception: - msg = self.message('perm_new_member_name', - state=State_obj) - raise formencode.Invalid(msg, value, state, - error_dict={'perm_new_member_name':msg}) - return value + except Exception: + msg = self.message('perm_new_member_name', + state=State_obj) + raise formencode.Invalid( + msg, value, state, error_dict={'perm_new_member_name': msg} + ) + return value + return _ValidPerms + class ValidSettings(formencode.validators.FancyValidator): def to_python(self, value, state): - #settings form can't edit user - if value.has_key('user'): + # settings form can't edit user + if 'user' in value: del['value']['user'] + return value - return value class ValidPath(formencode.validators.FancyValidator): def to_python(self, value, state): @@ -446,33 +465,37 @@ class ValidPath(formencode.validators.Fa 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}) + 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: - user = User.query().filter(User.email == value).scalar() + if old_data.get('email', '').lower() != value: + user = User.get_by_email(value, case_insensitive=True) if user: raise formencode.Invalid( - _("This e-mail address is already taken"), - value, state) + _("This e-mail address is already taken"), value, state + ) return value return _UniqSystemEmail + class ValidSystemEmail(formencode.validators.FancyValidator): def to_python(self, value, state): value = value.lower() - user = User.query().filter(User.email == value).scalar() + user = User.get_by_email(value, case_insensitive=True) if user is None: - raise formencode.Invalid(_("This e-mail address doesn't exist.") , - value, state) + raise formencode.Invalid( + _("This e-mail address doesn't exist."), value, state + ) return value + class LdapLibValidator(formencode.validators.FancyValidator): def to_python(self, value, state): @@ -483,45 +506,50 @@ class LdapLibValidator(formencode.valida raise LdapImportError return value + class AttrLoginValidator(formencode.validators.FancyValidator): def to_python(self, value, state): if not value or not isinstance(value, (str, unicode)): - raise formencode.Invalid(_("The LDAP Login attribute of the CN " - "must be specified - this is the name " - "of the attribute that is equivalent " - "to 'username'"), - value, state) + raise formencode.Invalid( + _("The LDAP Login attribute of the CN must be specified - " + "this is the name of the attribute that is equivalent " + "to 'username'"), value, state + ) return value -#=============================================================================== + +#============================================================================== # FORMS -#=============================================================================== +#============================================================================== class LoginForm(formencode.Schema): allow_extra_fields = True filter_extra_fields = True username = UnicodeString( - strip=True, - min=1, - not_empty=True, - messages={ - 'empty':_('Please enter a login'), - 'tooShort':_('Enter a value %(min)i characters long or more')} - ) + strip=True, + min=1, + not_empty=True, + messages={ + 'empty': _('Please enter a login'), + 'tooShort': _('Enter a value %(min)i characters long or more')} + ) password = UnicodeString( - strip=True, - min=3, - not_empty=True, - messages={ - 'empty':_('Please enter a password'), - 'tooShort':_('Enter %(min)i characters or more')} - ) + strip=True, + min=3, + not_empty=True, + messages={ + 'empty': _('Please enter a password'), + 'tooShort': _('Enter %(min)i characters or more')} + ) + + remember = StringBoolean(if_missing=False) chained_validators = [ValidAuth] + def UserForm(edit=False, old_data={}): class _UserForm(formencode.Schema): allow_extra_fields = True @@ -530,15 +558,17 @@ def UserForm(edit=False, old_data={}): ValidUsername(edit, old_data)) if edit: new_password = All(UnicodeString(strip=True, min=6, not_empty=False)) - password_confirmation = All(UnicodeString(strip=True, min=6, not_empty=False)) + password_confirmation = 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)) - password_confirmation = All(UnicodeString(strip=True, min=6, not_empty=False)) - + password_confirmation = All(UnicodeString(strip=True, min=6, + not_empty=False)) + active = StringBoolean(if_missing=False) - name = UnicodeString(strip=True, min=1, not_empty=True) - lastname = UnicodeString(strip=True, min=1, not_empty=True) + name = UnicodeString(strip=True, min=1, not_empty=False) + lastname = UnicodeString(strip=True, min=1, not_empty=False) email = All(Email(not_empty=True), UniqSystemEmail(old_data)) chained_validators = [ValidPasswordsMatch, ValidPassword] @@ -563,10 +593,11 @@ def UsersGroupForm(edit=False, old_data= return _UsersGroupForm + def ReposGroupForm(edit=False, old_data={}, available_groups=[]): class _ReposGroupForm(formencode.Schema): allow_extra_fields = True - filter_extra_fields = True + filter_extra_fields = False group_name = All(UnicodeString(strip=True, min=1, not_empty=True), SlugifyName()) @@ -576,10 +607,11 @@ def ReposGroupForm(edit=False, old_data= testValueList=True, if_missing=None, not_empty=False) - chained_validators = [ValidReposGroup(edit, old_data)] + chained_validators = [ValidReposGroup(edit, old_data), ValidPerms('group')] return _ReposGroupForm + def RegisterForm(edit=False, old_data={}): class _RegisterForm(formencode.Schema): allow_extra_fields = True @@ -589,14 +621,15 @@ def RegisterForm(edit=False, old_data={} 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) + name = UnicodeString(strip=True, min=1, not_empty=False) + lastname = UnicodeString(strip=True, min=1, not_empty=False) 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 @@ -604,6 +637,7 @@ def PasswordResetForm(): email = All(ValidSystemEmail(), Email(not_empty=True)) return _PasswordResetForm + def RepoForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(), repo_groups=[]): class _RepoForm(formencode.Schema): @@ -624,23 +658,29 @@ def RepoForm(edit=False, old_data={}, su #this is repo owner user = All(UnicodeString(not_empty=True), ValidRepoUser) - chained_validators = [ValidRepoName(edit, old_data), ValidPerms] + chained_validators = [ValidRepoName(edit, old_data), ValidPerms()] return _RepoForm -def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()): + +def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(), + repo_groups=[]): class _RepoForkForm(formencode.Schema): allow_extra_fields = True filter_extra_fields = False - fork_name = All(UnicodeString(strip=True, min=1, not_empty=True), + repo_name = All(UnicodeString(strip=True, min=1, not_empty=True), SlugifyName()) + repo_group = OneOf(repo_groups, hideList=True) + repo_type = All(ValidForkType(old_data), OneOf(supported_backends)) description = UnicodeString(strip=True, min=1, not_empty=True) private = StringBoolean(if_missing=False) - repo_type = All(ValidForkType(old_data), OneOf(supported_backends)) - - chained_validators = [ValidForkName()] + copy_permissions = StringBoolean(if_missing=False) + update_after_clone = StringBoolean(if_missing=False) + fork_parent_id = UnicodeString() + chained_validators = [ValidForkName(edit, old_data)] return _RepoForkForm + def RepoSettingsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(), repo_groups=[]): class _RepoForm(formencode.Schema): @@ -652,7 +692,7 @@ def RepoSettingsForm(edit=False, old_dat repo_group = OneOf(repo_groups, hideList=True) private = StringBoolean(if_missing=False) - chained_validators = [ValidRepoName(edit, old_data), ValidPerms, + chained_validators = [ValidRepoName(edit, old_data), ValidPerms(), ValidSettings] return _RepoForm @@ -667,6 +707,7 @@ def ApplicationSettingsForm(): return _ApplicationSettingsForm + def ApplicationUiSettingsForm(): class _ApplicationUiSettingsForm(formencode.Schema): allow_extra_fields = True @@ -680,6 +721,7 @@ def ApplicationUiSettingsForm(): return _ApplicationUiSettingsForm + def DefaultPermissionsForm(perms_choices, register_choices, create_choices): class _DefaultPermissionsForm(formencode.Schema): allow_extra_fields = True diff --git a/rhodecode/model/meta.py b/rhodecode/model/meta.py --- a/rhodecode/model/meta.py +++ b/rhodecode/model/meta.py @@ -3,7 +3,7 @@ from sqlalchemy.ext.declarative import d from sqlalchemy.orm import scoped_session, sessionmaker from beaker import cache -from rhodecode.model import caching_query +from rhodecode.lib import caching_query # Beaker CacheManager. A home base for cache configurations. @@ -15,7 +15,8 @@ cache_manager = cache.CacheManager() # Session = scoped_session( sessionmaker( - query_cls=caching_query.query_callable(cache_manager) + query_cls=caching_query.query_callable(cache_manager), + expire_on_commit=True, ) ) diff --git a/rhodecode/model/notification.py b/rhodecode/model/notification.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/notification.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.model.notification + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Model for notifications + + + :created_on: Nov 20, 2011 + :author: marcink + :copyright: (C) 2010-2012 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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, see . + +import os +import logging +import traceback +import datetime + +from pylons.i18n.translation import _ + +import rhodecode +from rhodecode.lib import helpers as h +from rhodecode.model import BaseModel +from rhodecode.model.db import Notification, User, UserNotification + +log = logging.getLogger(__name__) + + +class NotificationModel(BaseModel): + + def __get_user(self, user): + return self._get_instance(User, user, callback=User.get_by_username) + + def __get_notification(self, notification): + if isinstance(notification, Notification): + return notification + elif isinstance(notification, int): + return Notification.get(notification) + else: + if notification: + raise Exception('notification must be int or Instance' + ' of Notification got %s' % type(notification)) + + def create(self, created_by, subject, body, recipients=None, + type_=Notification.TYPE_MESSAGE, with_email=True, + email_kwargs={}): + """ + + Creates notification of given type + + :param created_by: int, str or User instance. User who created this + notification + :param subject: + :param body: + :param recipients: list of int, str or User objects, when None + is given send to all admins + :param type_: type of notification + :param with_email: send email with this notification + :param email_kwargs: additional dict to pass as args to email template + """ + from rhodecode.lib.celerylib import tasks, run_task + + if recipients and not getattr(recipients, '__iter__', False): + raise Exception('recipients must be a list of iterable') + + created_by_obj = self.__get_user(created_by) + + if recipients: + recipients_objs = [] + for u in recipients: + obj = self.__get_user(u) + if obj: + recipients_objs.append(obj) + recipients_objs = set(recipients_objs) + else: + # empty recipients means to all admins + recipients_objs = User.query().filter(User.admin == True).all() + + notif = Notification.create(created_by=created_by_obj, subject=subject, + body=body, recipients=recipients_objs, + type_=type_) + + if with_email is False: + return notif + + # send email with notification + for rec in recipients_objs: + email_subject = NotificationModel().make_description(notif, False) + type_ = type_ + email_body = body + kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)} + kwargs.update(email_kwargs) + email_body_html = EmailNotificationModel()\ + .get_email_tmpl(type_, **kwargs) + run_task(tasks.send_email, rec.email, email_subject, email_body, + email_body_html) + + return notif + + def delete(self, user, notification): + # we don't want to remove actual notification just the assignment + try: + notification = self.__get_notification(notification) + user = self.__get_user(user) + if notification and user: + obj = UserNotification.query()\ + .filter(UserNotification.user == user)\ + .filter(UserNotification.notification + == notification)\ + .one() + self.sa.delete(obj) + return True + except Exception: + log.error(traceback.format_exc()) + raise + + def get_for_user(self, user): + user = self.__get_user(user) + return user.notifications + + def mark_all_read_for_user(self, user): + user = self.__get_user(user) + UserNotification.query()\ + .filter(UserNotification.read==False)\ + .update({'read': True}) + + def get_unread_cnt_for_user(self, user): + user = self.__get_user(user) + return UserNotification.query()\ + .filter(UserNotification.read == False)\ + .filter(UserNotification.user == user).count() + + def get_unread_for_user(self, user): + user = self.__get_user(user) + return [x.notification for x in UserNotification.query()\ + .filter(UserNotification.read == False)\ + .filter(UserNotification.user == user).all()] + + def get_user_notification(self, user, notification): + user = self.__get_user(user) + notification = self.__get_notification(notification) + + return UserNotification.query()\ + .filter(UserNotification.notification == notification)\ + .filter(UserNotification.user == user).scalar() + + def make_description(self, notification, show_age=True): + """ + Creates a human readable description based on properties + of notification object + """ + + _map = {notification.TYPE_CHANGESET_COMMENT:_('commented on commit'), + notification.TYPE_MESSAGE:_('sent message'), + notification.TYPE_MENTION:_('mentioned you'), + notification.TYPE_REGISTRATION:_('registered in RhodeCode')} + + DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" + + tmpl = "%(user)s %(action)s %(when)s" + if show_age: + when = h.age(notification.created_on) + else: + DTF = lambda d: datetime.datetime.strftime(d, DATETIME_FORMAT) + when = DTF(notification.created_on) + data = dict(user=notification.created_by_user.username, + action=_map[notification.type_], + when=when) + return tmpl % data + + +class EmailNotificationModel(BaseModel): + + TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT + TYPE_PASSWORD_RESET = 'passoword_link' + TYPE_REGISTRATION = Notification.TYPE_REGISTRATION + TYPE_DEFAULT = 'default' + + def __init__(self): + self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0] + self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup + + self.email_types = { + self.TYPE_CHANGESET_COMMENT:'email_templates/changeset_comment.html', + self.TYPE_PASSWORD_RESET:'email_templates/password_reset.html', + self.TYPE_REGISTRATION:'email_templates/registration.html', + self.TYPE_DEFAULT:'email_templates/default.html' + } + + def get_email_tmpl(self, type_, **kwargs): + """ + return generated template for email based on given type + + :param type_: + """ + + base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT]) + email_template = self._tmpl_lookup.get_template(base) + # translator inject + _kwargs = {'_':_} + _kwargs.update(kwargs) + log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs)) + return email_template.render(**_kwargs) diff --git a/rhodecode/model/permission.py b/rhodecode/model/permission.py --- a/rhodecode/model/permission.py +++ b/rhodecode/model/permission.py @@ -7,7 +7,7 @@ :created_on: Aug 20, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -28,19 +28,22 @@ import traceback from sqlalchemy.exc import DatabaseError +from rhodecode.lib.caching_query import FromCache + from rhodecode.model import BaseModel -from rhodecode.model.db import User, Permission, UserToPerm, RepoToPerm -from rhodecode.model.caching_query import FromCache +from rhodecode.model.db import User, Permission, UserToPerm, UserRepoToPerm log = logging.getLogger(__name__) class PermissionModel(BaseModel): - """Permissions model for RhodeCode + """ + Permissions model for RhodeCode """ def get_permission(self, permission_id, cache=False): - """Get's permissions by id + """ + Get's permissions by id :param permission_id: id of permission to get from database :param cache: use Cache for this query @@ -52,7 +55,8 @@ class PermissionModel(BaseModel): return perm.get(permission_id) def get_permission_by_name(self, name, cache=False): - """Get's permissions by given name + """ + Get's permissions by given name :param name: name to fetch :param cache: Use cache for this query @@ -66,8 +70,8 @@ class PermissionModel(BaseModel): def update(self, form_result): perm_user = self.sa.query(User)\ - .filter(User.username == - form_result['perm_user_name']).scalar() + .filter(User.username == + form_result['perm_user_name']).scalar() u2p = self.sa.query(UserToPerm).filter(UserToPerm.user == perm_user).all() if len(u2p) != 3: @@ -76,7 +80,7 @@ class PermissionModel(BaseModel): ' your database' % len(u2p)) try: - #stage 1 change defaults + # stage 1 change defaults for p in u2p: if p.permission.permission_name.startswith('repository.'): p.permission = self.get_permission_by_name( @@ -95,19 +99,17 @@ class PermissionModel(BaseModel): #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(): + for r2p in self.sa.query(UserRepoToPerm)\ + .filter(UserRepoToPerm.user == perm_user).all(): r2p.permission = self.get_permission_by_name( form_result['default_perm']) self.sa.add(r2p) - #stage 3 set anonymous access + # 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 --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py old mode 100755 new mode 100644 --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -7,7 +7,7 @@ :created_on: Jun 5, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -28,24 +28,46 @@ import logging import traceback from datetime import datetime -from vcs.utils.lazy import LazyProperty -from vcs.backends import get_backend +from rhodecode.lib.vcs.backends import get_backend +from rhodecode.lib import LazyProperty from rhodecode.lib import safe_str, safe_unicode +from rhodecode.lib.caching_query import FromCache +from rhodecode.lib.hooks import log_create_repository from rhodecode.model import BaseModel -from rhodecode.model.caching_query import FromCache -from rhodecode.model.db import Repository, RepoToPerm, User, Permission, \ - Statistics, UsersGroup, UsersGroupRepoToPerm, RhodeCodeUi, Group +from rhodecode.model.db import Repository, UserRepoToPerm, User, Permission, \ + Statistics, UsersGroup, UsersGroupRepoToPerm, RhodeCodeUi, RepoGroup + log = logging.getLogger(__name__) class RepoModel(BaseModel): + def __get_user(self, user): + return self._get_instance(User, user, callback=User.get_by_username) + + def __get_users_group(self, users_group): + return self._get_instance(UsersGroup, users_group, + callback=UsersGroup.get_by_group_name) + + def __get_repos_group(self, repos_group): + return self._get_instance(RepoGroup, repos_group, + callback=RepoGroup.get_by_group_name) + + def __get_repo(self, repository): + return self._get_instance(Repository, repository, + callback=Repository.get_by_repo_name) + + def __get_perm(self, permission): + return self._get_instance(Permission, permission, + callback=Permission.get_by_key) + @LazyProperty def repos_path(self): - """Get's the repositories root path from database + """ + Get's the repositories root path from database """ q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one() @@ -60,6 +82,9 @@ class RepoModel(BaseModel): "get_repo_%s" % repo_id)) return repo.scalar() + def get_repo(self, repository): + return self.__get_repo(repository) + def get_by_repo_name(self, repo_name, cache=False): repo = self.sa.query(Repository)\ .filter(Repository.repo_name == repo_name) @@ -69,7 +94,6 @@ class RepoModel(BaseModel): "get_repo_%s" % repo_name)) return repo.scalar() - def get_users_js(self): users = self.sa.query(User).filter(User.active == True).all() @@ -93,9 +117,9 @@ class RepoModel(BaseModel): def _get_defaults(self, repo_name): """ - Get's information about repository, and returns a dict for + Get's information about repository, and returns a dict for usage in forms - + :param repo_name: """ @@ -130,7 +154,6 @@ class RepoModel(BaseModel): return defaults - def update(self, repo_name, form_data): try: cur_repo = self.get_by_repo_name(repo_name, cache=False) @@ -138,48 +161,24 @@ class RepoModel(BaseModel): # update permissions for member, perm, member_type in form_data['perms_updates']: if member_type == 'user': - r2p = self.sa.query(RepoToPerm)\ - .filter(RepoToPerm.user == User.get_by_username(member))\ - .filter(RepoToPerm.repository == cur_repo)\ - .one() - - r2p.permission = self.sa.query(Permission)\ - .filter(Permission.permission_name == - perm).scalar() - self.sa.add(r2p) + # this updates existing one + RepoModel().grant_user_permission( + repo=cur_repo, user=member, perm=perm + ) else: - g2p = self.sa.query(UsersGroupRepoToPerm)\ - .filter(UsersGroupRepoToPerm.users_group == - UsersGroup.get_by_group_name(member))\ - .filter(UsersGroupRepoToPerm.repository == - cur_repo).one() - - g2p.permission = self.sa.query(Permission)\ - .filter(Permission.permission_name == - perm).scalar() - self.sa.add(g2p) - + RepoModel().grant_users_group_permission( + repo=cur_repo, group_name=member, perm=perm + ) # set new permissions for member, perm, member_type in form_data['perms_new']: if member_type == 'user': - r2p = RepoToPerm() - r2p.repository = cur_repo - r2p.user = User.get_by_username(member) - - r2p.permission = self.sa.query(Permission)\ - .filter(Permission. - permission_name == perm)\ - .scalar() - self.sa.add(r2p) + RepoModel().grant_user_permission( + repo=cur_repo, user=member, perm=perm + ) else: - g2p = UsersGroupRepoToPerm() - g2p.repository = cur_repo - g2p.users_group = UsersGroup.get_by_group_name(member) - g2p.permission = self.sa.query(Permission)\ - .filter(Permission. - permission_name == perm)\ - .scalar() - self.sa.add(g2p) + RepoModel().grant_users_group_permission( + repo=cur_repo, group_name=member, perm=perm + ) # update current repo for k, v in form_data.items(): @@ -188,7 +187,7 @@ class RepoModel(BaseModel): elif k == 'repo_name': pass elif k == 'repo_group': - cur_repo.group = Group.get(v) + cur_repo.group = RepoGroup.get(v) else: setattr(cur_repo, k, v) @@ -202,133 +201,220 @@ class RepoModel(BaseModel): # rename repository self.__rename_repo(old=repo_name, new=new_name) - self.sa.commit() return cur_repo except: log.error(traceback.format_exc()) - self.sa.rollback() raise def create(self, form_data, cur_user, just_db=False, fork=False): + from rhodecode.model.scm import ScmModel try: if fork: - repo_name = form_data['fork_name'] - org_name = form_data['repo_name'] - org_full_name = org_name + fork_parent_id = form_data['fork_parent_id'] - else: - org_name = repo_name = form_data['repo_name'] - repo_name_full = form_data['repo_name_full'] + # repo name is just a name of repository + # while repo_name_full is a full qualified name that is combined + # with name and path of group + repo_name = form_data['repo_name'] + repo_name_full = form_data['repo_name_full'] new_repo = Repository() new_repo.enable_statistics = False + for k, v in form_data.items(): if k == 'repo_name': - if fork: - v = repo_name - else: - v = repo_name_full + v = repo_name_full if k == 'repo_group': k = 'group_id' - if k == 'description': - v = safe_unicode(v) or repo_name + v = v or repo_name setattr(new_repo, k, v) if fork: - parent_repo = self.sa.query(Repository)\ - .filter(Repository.repo_name == org_full_name).one() + parent_repo = Repository.get(fork_parent_id) 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 User.get_by_username('default').user_perms: - if p.permission.permission_name.startswith('repository.'): - default = p.permission.permission_name - break + def _create_default_perms(): + # create default permission + repo_to_perm = UserRepoToPerm() + default = 'repository.read' + for p in User.get_by_username('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 = new_repo + repo_to_perm.user_id = User.get_by_username('default').user_id + + self.sa.add(repo_to_perm) - default_perm = 'repository.none' if form_data['private'] else default + if fork: + if form_data.get('copy_permissions'): + repo = Repository.get(fork_parent_id) + user_perms = UserRepoToPerm.query()\ + .filter(UserRepoToPerm.repository == repo).all() + group_perms = UsersGroupRepoToPerm.query()\ + .filter(UsersGroupRepoToPerm.repository == repo).all() - repo_to_perm.permission_id = self.sa.query(Permission)\ - .filter(Permission.permission_name == default_perm)\ - .one().permission_id + for perm in user_perms: + UserRepoToPerm.create(perm.user, new_repo, + perm.permission) - repo_to_perm.repository = new_repo - repo_to_perm.user_id = User.get_by_username('default').user_id - - self.sa.add(repo_to_perm) + for perm in group_perms: + UsersGroupRepoToPerm.create(perm.users_group, new_repo, + perm.permission) + else: + _create_default_perms() + else: + _create_default_perms() if not just_db: self.__create_repo(repo_name, form_data['repo_type'], form_data['repo_group'], form_data['clone_uri']) - self.sa.commit() - - #now automatically start following this repository as owner - from rhodecode.model.scm import ScmModel + # now automatically start following this repository as owner ScmModel(self.sa).toggle_following_repo(new_repo.repo_id, - cur_user.user_id) + cur_user.user_id) + log_create_repository(new_repo.get_dict(), + created_by=cur_user.username) return new_repo except: log.error(traceback.format_exc()) - self.sa.rollback() raise def create_fork(self, form_data, cur_user): + """ + Simple wrapper into executing celery task for fork creation + + :param form_data: + :param cur_user: + """ from rhodecode.lib.celerylib import tasks, run_task run_task(tasks.create_repo_fork, form_data, cur_user) def delete(self, repo): + repo = self.__get_repo(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_perm_users_group(self, form_data, repo_name): + def grant_user_permission(self, repo, user, perm): + """ + Grant permission for user on given repository, or update existing one + if found + + :param repo: Instance of Repository, repository_id, or repository name + :param user: Instance of User, user_id or username + :param perm: Instance of Permission, or permission_name + """ + user = self.__get_user(user) + repo = self.__get_repo(repo) + permission = self.__get_perm(perm) + + # check if we have that permission already + obj = self.sa.query(UserRepoToPerm)\ + .filter(UserRepoToPerm.user == user)\ + .filter(UserRepoToPerm.repository == repo)\ + .scalar() + if obj is None: + # create new ! + obj = UserRepoToPerm() + obj.repository = repo + obj.user = user + obj.permission = permission + self.sa.add(obj) + + def revoke_user_permission(self, repo, user): + """ + Revoke permission for user on given repository + + :param repo: Instance of Repository, repository_id, or repository name + :param user: Instance of User, user_id or username + """ + user = self.__get_user(user) + repo = self.__get_repo(repo) + + obj = self.sa.query(UserRepoToPerm)\ + .filter(UserRepoToPerm.repository == repo)\ + .filter(UserRepoToPerm.user == user)\ + .one() + self.sa.delete(obj) + + def grant_users_group_permission(self, repo, group_name, perm): + """ + Grant permission for users group on given repository, or update + existing one if found + + :param repo: Instance of Repository, repository_id, or repository name + :param group_name: Instance of UserGroup, users_group_id, + or users group name + :param perm: Instance of Permission, or permission_name + """ + repo = self.__get_repo(repo) + group_name = self.__get_users_group(group_name) + permission = self.__get_perm(perm) + + # check if we have that permission already + obj = self.sa.query(UsersGroupRepoToPerm)\ + .filter(UsersGroupRepoToPerm.users_group == group_name)\ + .filter(UsersGroupRepoToPerm.repository == repo)\ + .scalar() + + if obj is None: + # create new + obj = UsersGroupRepoToPerm() + + obj.repository = repo + obj.users_group = group_name + obj.permission = permission + self.sa.add(obj) + + def revoke_users_group_permission(self, repo, group_name): + """ + Revoke permission for users group on given repository + + :param repo: Instance of Repository, repository_id, or repository name + :param group_name: Instance of UserGroup, users_group_id, + or users group name + """ + repo = self.__get_repo(repo) + group_name = self.__get_users_group(group_name) + + obj = self.sa.query(UsersGroupRepoToPerm)\ + .filter(UsersGroupRepoToPerm.repository == repo)\ + .filter(UsersGroupRepoToPerm.users_group == group_name)\ + .one() + self.sa.delete(obj) + + def delete_stats(self, repo_name): + """ + removes stats for given repo + + :param repo_name: + """ try: - self.sa.query(UsersGroupRepoToPerm)\ - .filter(UsersGroupRepoToPerm.repository \ - == self.get_by_repo_name(repo_name))\ - .filter(UsersGroupRepoToPerm.users_group_id \ - == form_data['users_group_id']).delete() - self.sa.commit() + obj = self.sa.query(Statistics)\ + .filter(Statistics.repository == + self.get_by_repo_name(repo_name))\ + .one() + self.sa.delete(obj) 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, new_parent_id, clone_uri=False): @@ -345,15 +431,16 @@ class RepoModel(BaseModel): from rhodecode.lib.utils import is_valid_repo, is_valid_repos_group if new_parent_id: - paths = Group.get(new_parent_id).full_path.split(Group.url_sep()) + paths = RepoGroup.get(new_parent_id)\ + .full_path.split(RepoGroup.url_sep()) new_parent_path = os.sep.join(paths) else: new_parent_path = '' - repo_path = os.path.join(*map(lambda x:safe_str(x), + # we need to make it str for mercurial + repo_path = os.path.join(*map(lambda x: safe_str(x), [self.repos_path, new_parent_path, repo_name])) - # check if this path is not a repository if is_valid_repo(repo_path, self.repos_path): raise Exception('This path %s is a valid repository' % repo_path) @@ -362,13 +449,14 @@ class RepoModel(BaseModel): if is_valid_repos_group(repo_path, self.repos_path): raise Exception('This path %s is a valid group' % repo_path) - log.info('creating repo %s in %s @ %s', repo_name, repo_path, - clone_uri) + log.info('creating repo %s in %s @ %s' % ( + repo_name, safe_unicode(repo_path), clone_uri + ) + ) backend = get_backend(alias) backend(repo_path, create=True, src_url=clone_uri) - def __rename_repo(self, old, new): """ renames repository on filesystem @@ -376,13 +464,14 @@ class RepoModel(BaseModel): :param old: old name :param new: new name """ - log.info('renaming repo from %s to %s', old, new) + log.info('renaming repo from %s to %s' % (old, new)) old_path = os.path.join(self.repos_path, old) new_path = os.path.join(self.repos_path, new) if os.path.isdir(new_path): - raise Exception('Was trying to rename to already existing dir %s' \ - % 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): @@ -395,14 +484,12 @@ class RepoModel(BaseModel): :param repo: repo object """ rm_path = os.path.join(self.repos_path, repo.repo_name) - log.info("Removing %s", rm_path) - #disable hg/git + 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(self.repos_path, 'rm__%s__%s' \ - % (datetime.today()\ - .strftime('%Y%m%d_%H%M%S_%f'), - repo.repo_name))) - + # disable repo + _d = 'rm__%s__%s' % (datetime.now().strftime('%Y%m%d_%H%M%S_%f'), + repo.repo_name) + shutil.move(rm_path, os.path.join(self.repos_path, _d)) diff --git a/rhodecode/model/repo_permission.py b/rhodecode/model/repo_permission.py --- a/rhodecode/model/repo_permission.py +++ b/rhodecode/model/repo_permission.py @@ -8,7 +8,7 @@ :created_on: Oct 1, 2011 :author: nvinot, marcink :copyright: (C) 2011-2011 Nicolas Vinot - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2011-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -25,18 +25,33 @@ # along with this program. If not, see . import logging -from rhodecode.model.db import BaseModel, RepoToPerm, Permission,\ - UsersGroupRepoToPerm -from rhodecode.model.meta import Session +from rhodecode.model import BaseModel +from rhodecode.model.db import UserRepoToPerm, UsersGroupRepoToPerm, Permission,\ + User, Repository log = logging.getLogger(__name__) class RepositoryPermissionModel(BaseModel): + + def __get_user(self, user): + return self._get_instance(User, user, callback=User.get_by_username) + + def __get_repo(self, repository): + return self._get_instance(Repository, repository, + callback=Repository.get_by_repo_name) + + def __get_perm(self, permission): + return self._get_instance(Permission, permission, + callback=Permission.get_by_key) + def get_user_permission(self, repository, user): - return RepoToPerm.query() \ - .filter(RepoToPerm.user == user) \ - .filter(RepoToPerm.repository == repository) \ + repository = self.__get_repo(repository) + user = self.__get_user(user) + + return UserRepoToPerm.query() \ + .filter(UserRepoToPerm.user == user) \ + .filter(UserRepoToPerm.repository == repository) \ .scalar() def update_user_permission(self, repository, user, permission): @@ -46,18 +61,16 @@ class RepositoryPermissionModel(BaseMode if not current.permission is permission: current.permission = permission else: - p = RepoToPerm() + p = UserRepoToPerm() p.user = user p.repository = repository p.permission = permission - Session.add(p) - Session.commit() + self.sa.add(p) def delete_user_permission(self, repository, user): current = self.get_user_permission(repository, user) if current: - Session.delete(current) - Session.commit() + self.sa.delete(current) def get_users_group_permission(self, repository, users_group): return UsersGroupRepoToPerm.query() \ @@ -78,13 +91,11 @@ class RepositoryPermissionModel(BaseMode p.repository = repository p.permission = permission self.sa.add(p) - Session.commit() def delete_users_group_permission(self, repository, users_group): current = self.get_users_group_permission(repository, users_group) if current: self.sa.delete(current) - Session.commit() def update_or_delete_user_permission(self, repository, user, permission): if permission: diff --git a/rhodecode/model/repos_group.py b/rhodecode/model/repos_group.py --- a/rhodecode/model/repos_group.py +++ b/rhodecode/model/repos_group.py @@ -7,7 +7,7 @@ :created_on: Jan 25, 2011 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2011-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -28,19 +28,32 @@ import logging import traceback import shutil -from pylons.i18n.translation import _ - -from vcs.utils.lazy import LazyProperty +from rhodecode.lib import LazyProperty from rhodecode.model import BaseModel -from rhodecode.model.caching_query import FromCache -from rhodecode.model.db import Group, RhodeCodeUi +from rhodecode.model.db import RepoGroup, RhodeCodeUi, UserRepoGroupToPerm, \ + User, Permission, UsersGroupRepoGroupToPerm, UsersGroup log = logging.getLogger(__name__) class ReposGroupModel(BaseModel): + def __get_user(self, user): + return self._get_instance(User, user, callback=User.get_by_username) + + def __get_users_group(self, users_group): + return self._get_instance(UsersGroup, users_group, + callback=UsersGroup.get_by_group_name) + + def __get_repos_group(self, repos_group): + return self._get_instance(RepoGroup, repos_group, + callback=RepoGroup.get_by_group_name) + + def __get_perm(self, permission): + return self._get_instance(Permission, permission, + callback=Permission.get_by_key) + @LazyProperty def repos_path(self): """ @@ -50,6 +63,24 @@ class ReposGroupModel(BaseModel): q = RhodeCodeUi.get_by_key('/').one() return q.ui_value + def _create_default_perms(self, new_group): + # create default permission + repo_group_to_perm = UserRepoGroupToPerm() + default_perm = 'group.read' + for p in User.get_by_username('default').user_perms: + if p.permission.permission_name.startswith('group.'): + default_perm = p.permission.permission_name + break + + repo_group_to_perm.permission_id = self.sa.query(Permission)\ + .filter(Permission.permission_name == default_perm)\ + .one().permission_id + + repo_group_to_perm.group = new_group + repo_group_to_perm.user_id = User.get_by_username('default').user_id + + self.sa.add(repo_group_to_perm) + def __create_group(self, group_name): """ makes repositories group on filesystem @@ -59,7 +90,7 @@ class ReposGroupModel(BaseModel): """ create_path = os.path.join(self.repos_path, group_name) - log.debug('creating new group in %s', create_path) + log.debug('creating new group in %s' % create_path) if os.path.isdir(create_path): raise Exception('That directory already exists !') @@ -69,7 +100,7 @@ class ReposGroupModel(BaseModel): def __rename_group(self, old, new): """ Renames a group on filesystem - + :param group_name: """ @@ -77,13 +108,12 @@ class ReposGroupModel(BaseModel): log.debug('skipping group rename') return - log.debug('renaming repos group from %s to %s', old, new) - + log.debug('renaming repos group from %s to %s' % (old, new)) old_path = os.path.join(self.repos_path, old) new_path = os.path.join(self.repos_path, new) - log.debug('renaming repos paths from %s to %s', old_path, new_path) + log.debug('renaming repos paths from %s to %s' % (old_path, new_path)) if os.path.isdir(new_path): raise Exception('Was trying to rename to already ' @@ -93,10 +123,10 @@ class ReposGroupModel(BaseModel): def __delete_group(self, group): """ Deletes a group from a filesystem - + :param group: instance of group from database """ - paths = group.full_path.split(Group.url_sep()) + paths = group.full_path.split(RepoGroup.url_sep()) paths = os.sep.join(paths) rm_path = os.path.join(self.repos_path, paths) @@ -104,33 +134,59 @@ class ReposGroupModel(BaseModel): # delete only if that path really exists os.rmdir(rm_path) - def create(self, form_data): + def create(self, group_name, group_description, parent, just_db=False): try: - new_repos_group = Group() - new_repos_group.group_description = form_data['group_description'] - new_repos_group.parent_group = Group.get(form_data['group_parent_id']) - new_repos_group.group_name = new_repos_group.get_new_name(form_data['group_name']) + new_repos_group = RepoGroup() + new_repos_group.group_description = group_description + new_repos_group.parent_group = self.__get_repos_group(parent) + new_repos_group.group_name = new_repos_group.get_new_name(group_name) self.sa.add(new_repos_group) + self._create_default_perms(new_repos_group) - self.__create_group(new_repos_group.group_name) + if not just_db: + # we need to flush here, in order to check if database won't + # throw any exceptions, create filesystem dirs at the very end + self.sa.flush() + self.__create_group(new_repos_group.group_name) - self.sa.commit() return new_repos_group except: log.error(traceback.format_exc()) - self.sa.rollback() raise def update(self, repos_group_id, form_data): try: - repos_group = Group.get(repos_group_id) + repos_group = RepoGroup.get(repos_group_id) + + # update permissions + for member, perm, member_type in form_data['perms_updates']: + if member_type == 'user': + # this updates also current one if found + ReposGroupModel().grant_user_permission( + repos_group=repos_group, user=member, perm=perm + ) + else: + ReposGroupModel().grant_users_group_permission( + repos_group=repos_group, group_name=member, perm=perm + ) + # set new permissions + for member, perm, member_type in form_data['perms_new']: + if member_type == 'user': + ReposGroupModel().grant_user_permission( + repos_group=repos_group, user=member, perm=perm + ) + else: + ReposGroupModel().grant_users_group_permission( + repos_group=repos_group, group_name=member, perm=perm + ) + old_path = repos_group.full_path - + # change properties repos_group.group_description = form_data['group_description'] - repos_group.parent_group = Group.get(form_data['group_parent_id']) + repos_group.parent_group = RepoGroup.get(form_data['group_parent_id']) repos_group.group_name = repos_group.get_new_name(form_data['group_name']) new_path = repos_group.full_path @@ -139,26 +195,116 @@ class ReposGroupModel(BaseModel): self.__rename_group(old_path, new_path) - # we need to get all repositories from this new group and + # we need to get all repositories from this new group and # rename them accordingly to new group path for r in repos_group.repositories: r.repo_name = r.get_new_name(r.just_name) self.sa.add(r) - self.sa.commit() return repos_group except: log.error(traceback.format_exc()) - self.sa.rollback() raise def delete(self, users_group_id): try: - users_group = Group.get(users_group_id) + users_group = RepoGroup.get(users_group_id) self.sa.delete(users_group) self.__delete_group(users_group) - self.sa.commit() except: log.error(traceback.format_exc()) - self.sa.rollback() raise + + def grant_user_permission(self, repos_group, user, perm): + """ + Grant permission for user on given repositories group, or update + existing one if found + + :param repos_group: Instance of ReposGroup, repositories_group_id, + or repositories_group name + :param user: Instance of User, user_id or username + :param perm: Instance of Permission, or permission_name + """ + + repos_group = self.__get_repos_group(repos_group) + user = self.__get_user(user) + permission = self.__get_perm(perm) + + # check if we have that permission already + obj = self.sa.query(UserRepoGroupToPerm)\ + .filter(UserRepoGroupToPerm.user == user)\ + .filter(UserRepoGroupToPerm.group == repos_group)\ + .scalar() + if obj is None: + # create new ! + obj = UserRepoGroupToPerm() + obj.group = repos_group + obj.user = user + obj.permission = permission + self.sa.add(obj) + + def revoke_user_permission(self, repos_group, user): + """ + Revoke permission for user on given repositories group + + :param repos_group: Instance of ReposGroup, repositories_group_id, + or repositories_group name + :param user: Instance of User, user_id or username + """ + + repos_group = self.__get_repos_group(repos_group) + user = self.__get_user(user) + + obj = self.sa.query(UserRepoGroupToPerm)\ + .filter(UserRepoGroupToPerm.user == user)\ + .filter(UserRepoGroupToPerm.group == repos_group)\ + .one() + self.sa.delete(obj) + + def grant_users_group_permission(self, repos_group, group_name, perm): + """ + Grant permission for users group on given repositories group, or update + existing one if found + + :param repos_group: Instance of ReposGroup, repositories_group_id, + or repositories_group name + :param group_name: Instance of UserGroup, users_group_id, + or users group name + :param perm: Instance of Permission, or permission_name + """ + repos_group = self.__get_repos_group(repos_group) + group_name = self.__get_users_group(group_name) + permission = self.__get_perm(perm) + + # check if we have that permission already + obj = self.sa.query(UsersGroupRepoGroupToPerm)\ + .filter(UsersGroupRepoGroupToPerm.group == repos_group)\ + .filter(UsersGroupRepoGroupToPerm.users_group == group_name)\ + .scalar() + + if obj is None: + # create new + obj = UsersGroupRepoGroupToPerm() + + obj.group = repos_group + obj.users_group = group_name + obj.permission = permission + self.sa.add(obj) + + def revoke_users_group_permission(self, repos_group, group_name): + """ + Revoke permission for users group on given repositories group + + :param repos_group: Instance of ReposGroup, repositories_group_id, + or repositories_group name + :param group_name: Instance of UserGroup, users_group_id, + or users group name + """ + repos_group = self.__get_repos_group(repos_group) + group_name = self.__get_users_group(group_name) + + obj = self.sa.query(UsersGroupRepoGroupToPerm)\ + .filter(UsersGroupRepoGroupToPerm.group == repos_group)\ + .filter(UsersGroupRepoGroupToPerm.users_group == group_name)\ + .one() + self.sa.delete(obj) diff --git a/rhodecode/model/scm.py b/rhodecode/model/scm.py --- a/rhodecode/model/scm.py +++ b/rhodecode/model/scm.py @@ -28,22 +28,20 @@ import traceback import logging import cStringIO -from sqlalchemy.exc import DatabaseError - -from vcs import get_backend -from vcs.exceptions import RepositoryError -from vcs.utils.lazy import LazyProperty -from vcs.nodes import FileNode +from rhodecode.lib.vcs import get_backend +from rhodecode.lib.vcs.exceptions import RepositoryError +from rhodecode.lib.vcs.utils.lazy import LazyProperty +from rhodecode.lib.vcs.nodes import FileNode from rhodecode import BACKENDS from rhodecode.lib import helpers as h from rhodecode.lib import safe_str -from rhodecode.lib.auth import HasRepoPermissionAny +from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny from rhodecode.lib.utils import get_repos as get_filesystem_repos, make_ui, \ action_logger, EmptyChangeset from rhodecode.model import BaseModel from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \ - UserFollowing, UserLog, User + UserFollowing, UserLog, User, RepoGroup log = logging.getLogger(__name__) @@ -63,6 +61,7 @@ class RepoTemp(object): def __repr__(self): return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id) + class CachedRepoList(object): def __init__(self, db_repo_list, repos_path, order_by=None): @@ -79,19 +78,18 @@ class CachedRepoList(object): def __iter__(self): for dbr in self.db_repo_list: - scmr = dbr.scm_instance_cached - # check permission at this level - if not HasRepoPermissionAny('repository.read', 'repository.write', - 'repository.admin')(dbr.repo_name, - 'get repo check'): + if not HasRepoPermissionAny( + 'repository.read', 'repository.write', 'repository.admin' + )(dbr.repo_name, 'get repo check'): continue if scmr is None: - log.error('%s this repository is present in database but it ' - 'cannot be created as an scm instance', - dbr.repo_name) + log.error( + '%s this repository is present in database but it ' + 'cannot be created as an scm instance' % dbr.repo_name + ) continue last_change = scmr.last_change @@ -103,8 +101,7 @@ class CachedRepoList(object): tmp_d['description'] = dbr.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['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 @@ -115,17 +112,53 @@ class CachedRepoList(object): tmp_d['last_msg'] = tip.message tmp_d['author'] = tip.author tmp_d['dbrepo'] = dbr.get_dict() - tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork \ - else {} + tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {} yield tmp_d + +class GroupList(object): + + def __init__(self, db_repo_group_list): + self.db_repo_group_list = db_repo_group_list + + def __len__(self): + return len(self.db_repo_group_list) + + def __repr__(self): + return '<%s (%s)>' % (self.__class__.__name__, self.__len__()) + + def __iter__(self): + for dbgr in self.db_repo_group_list: + # check permission at this level + if not HasReposGroupPermissionAny( + 'group.read', 'group.write', 'group.admin' + )(dbgr.group_name, 'get group repo check'): + continue + + yield dbgr + + class ScmModel(BaseModel): - """Generic Scm Model + """ + Generic Scm Model """ + def __get_repo(self, instance): + cls = Repository + if isinstance(instance, cls): + return instance + elif isinstance(instance, int) or str(instance).isdigit(): + return cls.get(instance) + elif isinstance(instance, basestring): + return cls.get_by_repo_name(instance) + elif instance: + raise Exception('given object must be int, basestr or Instance' + ' of %s got %s' % (type(cls), type(instance))) + @LazyProperty def repos_path(self): - """Get's the repositories root path from database + """ + Get's the repositories root path from database """ q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one() @@ -133,7 +166,8 @@ class ScmModel(BaseModel): return q.ui_value def repo_scan(self, repos_path=None): - """Listing of repositories in given path. This path should not be a + """ + 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 @@ -142,19 +176,19 @@ class ScmModel(BaseModel): if repos_path is None: repos_path = self.repos_path - log.info('scanning for repositories in %s', repos_path) + log.info('scanning for repositories in %s' % repos_path) baseui = make_ui('db') - repos_list = {} + repos = {} for name, path in get_filesystem_repos(repos_path, recursive=True): - + # name need to be decomposed and put back together using the / # since this is internal storage separator for rhodecode name = Repository.url_sep().join(name.split(os.sep)) - + try: - if name in repos_list: + if name in repos: raise RepositoryError('Duplicate repository name %s ' 'found in %s' % (name, path)) else: @@ -162,17 +196,14 @@ class ScmModel(BaseModel): klass = get_backend(path[0]) if path[0] == 'hg' and path[0] in BACKENDS.keys(): - - # for mercurial we need to have an str path - repos_list[name] = klass(safe_str(path[1]), - baseui=baseui) + repos[name] = klass(safe_str(path[1]), baseui=baseui) if path[0] == 'git' and path[0] in BACKENDS.keys(): - repos_list[name] = klass(path[1]) + repos[name] = klass(path[1]) except OSError: continue - return repos_list + return repos def get_repos(self, all_repos=None, sort_key=None): """ @@ -192,30 +223,22 @@ class ScmModel(BaseModel): return repo_iter + def get_repos_groups(self, all_groups=None): + if all_groups is None: + all_groups = RepoGroup.query()\ + .filter(RepoGroup.group_parent_id == None).all() + group_iter = GroupList(all_groups) + + return group_iter + 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() + CacheInvalidation.set_invalidate(repo_name) + CacheInvalidation.set_invalidate(repo_name + "_README") def toggle_following_repo(self, follow_repo_id, user_id): @@ -224,17 +247,14 @@ class ScmModel(BaseModel): .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: @@ -242,13 +262,12 @@ class ScmModel(BaseModel): 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): @@ -259,11 +278,9 @@ class ScmModel(BaseModel): 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: @@ -271,10 +288,8 @@ class ScmModel(BaseModel): 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, cache=False): @@ -310,6 +325,13 @@ class ScmModel(BaseModel): return self.sa.query(Repository)\ .filter(Repository.fork_id == repo_id).count() + def mark_as_fork(self, repo, fork, user): + repo = self.__get_repo(repo) + fork = self.__get_repo(fork) + repo.fork = fork + self.sa.add(repo) + return repo + def pull_changes(self, repo_name, username): dbrepo = Repository.get_by_repo_name(repo_name) clone_uri = dbrepo.clone_uri @@ -333,13 +355,13 @@ class ScmModel(BaseModel): log.error(traceback.format_exc()) raise - def commit_change(self, repo, repo_name, cs, user, author, message, content, - f_path): + def commit_change(self, repo, repo_name, cs, user, author, message, + content, f_path): if repo.alias == 'hg': - from vcs.backends.hg import MercurialInMemoryChangeset as IMC + from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC elif repo.alias == 'git': - from vcs.backends.git import GitInMemoryChangeset as IMC + from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC # decoding here will force that we have proper encoded values # in any other case this will throw exceptions and deny commit @@ -363,9 +385,9 @@ class ScmModel(BaseModel): def create_node(self, repo, repo_name, cs, user, author, message, content, f_path): if repo.alias == 'hg': - from vcs.backends.hg import MercurialInMemoryChangeset as IMC + from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC elif repo.alias == 'git': - from vcs.backends.git import GitInMemoryChangeset as IMC + from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC # decoding here will force that we have proper encoded values # in any other case this will throw exceptions and deny commit @@ -400,20 +422,35 @@ class ScmModel(BaseModel): self.mark_for_invalidation(repo_name) + def get_nodes(self, repo_name, revision, root_path='/', flat=True): + """ + recursive walk in root dir and return a set of all path in that dir + based on repository walk function + + :param repo_name: name of repository + :param revision: revision for which to list nodes + :param root_path: root path to list + :param flat: return as a list, if False returns a dict with decription + + """ + _files = list() + _dirs = list() + try: + _repo = self.__get_repo(repo_name) + changeset = _repo.scm_instance.get_changeset(revision) + root_path = root_path.lstrip('/') + for topnode, dirs, files in changeset.walk(root_path): + for f in files: + _files.append(f.path if flat else {"name": f.path, + "type": "file"}) + for d in dirs: + _dirs.append(d.path if flat else {"name": d.path, + "type": "dir"}) + except RepositoryError: + log.debug(traceback.format_exc()) + raise + + return _dirs, _files 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)\ - .filter(CacheInvalidation.cache_key == repo_name)\ - .filter(CacheInvalidation.cache_active == False)\ - .scalar() - - return ret - diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -7,7 +7,7 @@ :created_on: Apr 9, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -26,13 +26,16 @@ import logging import traceback +from pylons import url from pylons.i18n.translation import _ from rhodecode.lib import safe_unicode +from rhodecode.lib.caching_query import FromCache + from rhodecode.model import BaseModel -from rhodecode.model.caching_query import FromCache -from rhodecode.model.db import User, RepoToPerm, Repository, Permission, \ - UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember +from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \ + UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember, \ + Notification, RepoGroup, UserRepoGroupToPerm, UsersGroup from rhodecode.lib.exceptions import DefaultUserException, \ UserOwnsReposException @@ -42,13 +45,28 @@ from sqlalchemy.orm import joinedload log = logging.getLogger(__name__) -PERM_WEIGHTS = {'repository.none': 0, - 'repository.read': 1, - 'repository.write': 3, - 'repository.admin': 3} + +PERM_WEIGHTS = { + 'repository.none': 0, + 'repository.read': 1, + 'repository.write': 3, + 'repository.admin': 4, + 'group.none': 0, + 'group.read': 1, + 'group.write': 3, + 'group.admin': 4, +} class UserModel(BaseModel): + + def __get_user(self, user): + return self._get_instance(User, user, callback=User.get_by_username) + + def __get_perm(self, permission): + return self._get_instance(Permission, permission, + callback=Permission.get_by_key) + def get(self, user_id, cache=False): user = self.sa.query(User) if cache: @@ -56,6 +74,9 @@ class UserModel(BaseModel): "get_user_%s" % user_id)) return user.get(user_id) + def get_user(self, user): + return self.__get_user(user) + def get_by_username(self, username, cache=False, case_insensitive=False): if case_insensitive: @@ -69,13 +90,7 @@ class UserModel(BaseModel): return user.scalar() def get_by_api_key(self, api_key, cache=False): - - user = self.sa.query(User)\ - .filter(User.api_key == api_key) - if cache: - user = user.options(FromCache("sql_cache_short", - "get_user_%s" % api_key)) - return user.scalar() + return User.get_by_api_key(api_key, cache) def create(self, form_data): try: @@ -85,18 +100,91 @@ class UserModel(BaseModel): new_user.api_key = generate_api_key(form_data['username']) self.sa.add(new_user) - self.sa.commit() return new_user except: log.error(traceback.format_exc()) - self.sa.rollback() raise + def create_or_update(self, username, password, email, name, lastname, + active=True, admin=False, ldap_dn=None): + """ + Creates a new instance if not found, or updates current one + + :param username: + :param password: + :param email: + :param active: + :param name: + :param lastname: + :param active: + :param admin: + :param ldap_dn: + """ + + from rhodecode.lib.auth import get_crypt_password + + log.debug('Checking for %s account in RhodeCode database' % username) + user = User.get_by_username(username, case_insensitive=True) + if user is None: + log.debug('creating new user %s' % username) + new_user = User() + else: + log.debug('updating user %s' % username) + new_user = user + + try: + new_user.username = username + new_user.admin = admin + new_user.password = get_crypt_password(password) + new_user.api_key = generate_api_key(username) + new_user.email = email + new_user.active = active + new_user.ldap_dn = safe_unicode(ldap_dn) if ldap_dn else None + new_user.name = name + new_user.lastname = lastname + self.sa.add(new_user) + return new_user + except (DatabaseError,): + log.error(traceback.format_exc()) + raise + + def create_for_container_auth(self, username, attrs): + """ + Creates the given user if it's not already in the database + + :param username: + :param attrs: + """ + if self.get_by_username(username, case_insensitive=True) is None: + + # autogenerate email for container account without one + generate_email = lambda usr: '%s@container_auth.account' % usr + + try: + new_user = User() + new_user.username = username + new_user.password = None + new_user.api_key = generate_api_key(username) + new_user.email = attrs['email'] + new_user.active = attrs.get('active', True) + new_user.name = attrs['name'] or generate_email(username) + new_user.lastname = attrs['lastname'] + + self.sa.add(new_user) + return new_user + except (DatabaseError,): + log.error(traceback.format_exc()) + self.sa.rollback() + raise + log.debug('User %s already exists. Skipping creation of account' + ' for container auth.', username) + return None + def create_ldap(self, username, password, user_dn, attrs): """ Checks if user is in database, if not creates this user marked as ldap user - + :param username: :param password: :param user_dn: @@ -105,31 +193,36 @@ class UserModel(BaseModel): 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: + + # autogenerate email for ldap account without one + generate_email = lambda usr: '%s@ldap.account' % usr + try: new_user = User() + username = username.lower() # add ldap account always lowercase - new_user.username = username.lower() + new_user.username = username new_user.password = get_crypt_password(password) new_user.api_key = generate_api_key(username) - new_user.email = attrs['email'] - new_user.active = True + new_user.email = attrs['email'] or generate_email(username) + new_user.active = attrs.get('active', True) new_user.ldap_dn = safe_unicode(user_dn) new_user.name = attrs['name'] new_user.lastname = attrs['lastname'] self.sa.add(new_user) - self.sa.commit() - return True + return new_user 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 + return None def create_registration(self, form_data): - from rhodecode.lib.celerylib import tasks, run_task + from rhodecode.model.notification import NotificationModel + try: new_user = User() for k, v in form_data.items(): @@ -137,18 +230,26 @@ class UserModel(BaseModel): setattr(new_user, k, v) self.sa.add(new_user) - self.sa.commit() + self.sa.flush() + + # notification to admins + subject = _('new user registration') body = ('New user registration\n' - 'username: %s\n' - 'email: %s\n') - body = body % (form_data['username'], form_data['email']) + '---------------------\n' + '- Username: %s\n' + '- Full Name: %s\n' + '- Email: %s\n') + body = body % (new_user.username, new_user.full_name, + new_user.email) + edit_url = url('edit_user', id=new_user.user_id, qualified=True) + kw = {'registered_user_url': edit_url} + NotificationModel().create(created_by=new_user, subject=subject, + body=body, recipients=None, + type_=Notification.TYPE_REGISTRATION, + email_kwargs=kw) - 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): @@ -167,10 +268,8 @@ class UserModel(BaseModel): setattr(user, k, v) self.sa.add(user) - self.sa.commit() except: log.error(traceback.format_exc()) - self.sa.rollback() raise def update_my_account(self, user_id, form_data): @@ -189,15 +288,14 @@ class UserModel(BaseModel): setattr(user, k, v) self.sa.add(user) - self.sa.commit() except: log.error(traceback.format_exc()) - self.sa.rollback() raise - def delete(self, user_id): + def delete(self, user): + user = self.__get_user(user) + try: - user = self.get(user_id, cache=False) if user.username == 'default': raise DefaultUserException( _("You can't remove this user since it's" @@ -209,10 +307,8 @@ class UserModel(BaseModel): '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_link(self, data): @@ -243,16 +339,19 @@ class UserModel(BaseModel): else: dbuser = self.get(user_id) - if dbuser is not None: - log.debug('filling %s data', dbuser) + if dbuser is not None and dbuser.active: + log.debug('filling %s data' % dbuser) for k, v in dbuser.get_dict().items(): setattr(auth_user, k, v) + else: + return False except: log.error(traceback.format_exc()) auth_user.is_authenticated = False + return False - return auth_user + return True def fill_perms(self, user): """ @@ -262,98 +361,109 @@ class UserModel(BaseModel): :param user: user instance to fill his perms """ - - user.permissions['repositories'] = {} - user.permissions['global'] = set() + RK = 'repositories' + GK = 'repositories_groups' + GLOBAL = 'global' + user.permissions[RK] = {} + user.permissions[GK] = {} + user.permissions[GLOBAL] = set() #====================================================================== # fetch default permissions #====================================================================== - default_user = self.get_by_username('default', cache=True) + default_user = User.get_by_username('default', cache=True) + default_user_id = default_user.user_id - default_perms = self.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() + default_repo_perms = Permission.get_default_perms(default_user_id) + default_repo_groups_perms = Permission.get_default_group_perms(default_user_id) if user.is_admin: #================================================================== - # #admin have all default rights set to admin + # admin user have all default rights for repositories + # and groups set to admin #================================================================== - user.permissions['global'].add('hg.admin') + user.permissions[GLOBAL].add('hg.admin') - for perm in default_perms: + # repositories + for perm in default_repo_perms: + r_k = perm.UserRepoToPerm.repository.repo_name p = 'repository.admin' - user.permissions['repositories'][perm.RepoToPerm. - repository.repo_name] = p + user.permissions[RK][r_k] = p + + # repositories groups + for perm in default_repo_groups_perms: + rg_k = perm.UserRepoGroupToPerm.group.group_name + p = 'group.admin' + user.permissions[GK][rg_k] = p else: #================================================================== - # set default permissions + # set default permissions first for repositories and groups #================================================================== uid = user.user_id - #default global + # default global permissions default_global_perms = self.sa.query(UserToPerm)\ - .filter(UserToPerm.user == default_user) + .filter(UserToPerm.user_id == default_user_id) for perm in default_global_perms: - user.permissions['global'].add(perm.permission.permission_name) + user.permissions[GLOBAL].add(perm.permission.permission_name) - #default for repositories - for perm in default_perms: - if perm.Repository.private and not (perm.Repository.user_id == - uid): - #diself.sable defaults for private repos, + # default for repositories + for perm in default_repo_perms: + r_k = perm.UserRepoToPerm.repository.repo_name + if perm.Repository.private and not (perm.Repository.user_id == uid): + # disable defaults for private repos, p = 'repository.none' elif perm.Repository.user_id == uid: - #set admin if owner + # set admin if owner p = 'repository.admin' else: p = perm.Permission.permission_name - user.permissions['repositories'][perm.RepoToPerm. - repository.repo_name] = p + user.permissions[RK][r_k] = p + + # default for repositories groups + for perm in default_repo_groups_perms: + rg_k = perm.UserRepoGroupToPerm.group.group_name + p = perm.Permission.permission_name + user.permissions[GK][rg_k] = p #================================================================== # overwrite default with user permissions if any #================================================================== - #user global + # user global user_perms = self.sa.query(UserToPerm)\ .options(joinedload(UserToPerm.permission))\ .filter(UserToPerm.user_id == uid).all() for perm in user_perms: - user.permissions['global'].add(perm.permission. - permission_name) + user.permissions[GLOBAL].add(perm.permission.permission_name) - #user repositories - user_repo_perms = self.sa.query(RepoToPerm, Permission, - Repository)\ - .join((Repository, RepoToPerm.repository_id == - Repository.repo_id))\ - .join((Permission, RepoToPerm.permission_id == - Permission.permission_id))\ - .filter(RepoToPerm.user_id == uid).all() + # user repositories + user_repo_perms = \ + self.sa.query(UserRepoToPerm, Permission, Repository)\ + .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\ + .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\ + .filter(UserRepoToPerm.user_id == uid)\ + .all() for perm in user_repo_perms: # set admin if owner + r_k = perm.UserRepoToPerm.repository.repo_name if perm.Repository.user_id == uid: p = 'repository.admin' else: p = perm.Permission.permission_name - user.permissions['repositories'][perm.RepoToPerm. - repository.repo_name] = p + user.permissions[RK][r_k] = p #================================================================== # check if user is part of groups for this repository and fill in # (or replace with higher) permissions #================================================================== - #users group global + # users group global user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\ .options(joinedload(UsersGroupToPerm.permission))\ .join((UsersGroupMember, UsersGroupToPerm.users_group_id == @@ -361,30 +471,82 @@ class UserModel(BaseModel): .filter(UsersGroupMember.user_id == uid).all() for perm in user_perms_from_users_groups: - user.permissions['global'].add(perm.permission.permission_name) + user.permissions[GLOBAL].add(perm.permission.permission_name) - #users group repositories - user_repo_perms_from_users_groups = self.sa.query( - UsersGroupRepoToPerm, - Permission, Repository,)\ - .join((Repository, UsersGroupRepoToPerm.repository_id == - Repository.repo_id))\ - .join((Permission, UsersGroupRepoToPerm.permission_id == - Permission.permission_id))\ - .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id == - UsersGroupMember.users_group_id))\ - .filter(UsersGroupMember.user_id == uid).all() + # users group repositories + user_repo_perms_from_users_groups = \ + self.sa.query(UsersGroupRepoToPerm, Permission, Repository,)\ + .join((Repository, UsersGroupRepoToPerm.repository_id == Repository.repo_id))\ + .join((Permission, UsersGroupRepoToPerm.permission_id == Permission.permission_id))\ + .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id == UsersGroupMember.users_group_id))\ + .filter(UsersGroupMember.user_id == uid)\ + .all() for perm in user_repo_perms_from_users_groups: + r_k = perm.UsersGroupRepoToPerm.repository.repo_name p = perm.Permission.permission_name - cur_perm = user.permissions['repositories'][perm. - UsersGroupRepoToPerm. - repository.repo_name] - #overwrite permission only if it's greater than permission + cur_perm = user.permissions[RK][r_k] + # overwrite permission only if it's greater than permission # given from other sources if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]: - user.permissions['repositories'][perm.UsersGroupRepoToPerm. - repository.repo_name] = p + user.permissions[RK][r_k] = p + + #================================================================== + # get access for this user for repos group and override defaults + #================================================================== + + # user repositories groups + user_repo_groups_perms = \ + self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\ + .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\ + .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\ + .filter(UserRepoToPerm.user_id == uid)\ + .all() + + for perm in user_repo_groups_perms: + rg_k = perm.UserRepoGroupToPerm.group.group_name + p = perm.Permission.permission_name + cur_perm = user.permissions[GK][rg_k] + if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]: + user.permissions[GK][rg_k] = p return user + def has_perm(self, user, perm): + if not isinstance(perm, Permission): + raise Exception('perm needs to be an instance of Permission class ' + 'got %s instead' % type(perm)) + + user = self.__get_user(user) + + return UserToPerm.query().filter(UserToPerm.user == user)\ + .filter(UserToPerm.permission == perm).scalar() is not None + + def grant_perm(self, user, perm): + """ + Grant user global permissions + + :param user: + :param perm: + """ + user = self.__get_user(user) + perm = self.__get_perm(perm) + new = UserToPerm() + new.user = user + new.permission = perm + self.sa.add(new) + + def revoke_perm(self, user, perm): + """ + Revoke users global permissions + + :param user: + :param perm: + """ + user = self.__get_user(user) + perm = self.__get_perm(perm) + + obj = UserToPerm.query().filter(UserToPerm.user == user)\ + .filter(UserToPerm.permission == perm).scalar() + if obj: + self.sa.delete(obj) diff --git a/rhodecode/model/users_group.py b/rhodecode/model/users_group.py --- a/rhodecode/model/users_group.py +++ b/rhodecode/model/users_group.py @@ -8,6 +8,7 @@ :created_on: Oct 1, 2011 :author: nvinot :copyright: (C) 2011-2011 Nicolas Vinot + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -27,50 +28,99 @@ import logging import traceback from rhodecode.model import BaseModel -from rhodecode.model.caching_query import FromCache -from rhodecode.model.db import UsersGroupMember, UsersGroup +from rhodecode.model.db import UsersGroupMember, UsersGroup,\ + UsersGroupRepoToPerm, Permission, UsersGroupToPerm, User +from rhodecode.lib.exceptions import UsersGroupsAssignedException log = logging.getLogger(__name__) + class UsersGroupModel(BaseModel): - def get(self, users_group_id, cache = False): - users_group = UsersGroup.query() - if cache: - users_group = users_group.options(FromCache("sql_cache_short", - "get_users_group_%s" % users_group_id)) - return users_group.get(users_group_id) + def __get_user(self, user): + return self._get_instance(User, user, callback=User.get_by_username) + + def __get_users_group(self, users_group): + return self._get_instance(UsersGroup, users_group, + callback=UsersGroup.get_by_group_name) + + def __get_perm(self, permission): + return self._get_instance(Permission, permission, + callback=Permission.get_by_key) - def get_by_name(self, name, cache = False, case_insensitive = False): - users_group = UsersGroup.query() - if case_insensitive: - users_group = users_group.filter(UsersGroup.users_group_name.ilike(name)) - else: - users_group = users_group.filter(UsersGroup.users_group_name == name) - if cache: - users_group = users_group.options(FromCache("sql_cache_short", - "get_users_group_%s" % name)) - return users_group.scalar() + def get(self, users_group_id, cache=False): + return UsersGroup.get(users_group_id) + + def get_by_name(self, name, cache=False, case_insensitive=False): + return UsersGroup.get_by_group_name(name, cache, case_insensitive) - def create(self, form_data): + def create(self, name, active=True): try: - new_users_group = UsersGroup() - for k, v in form_data.items(): - setattr(new_users_group, k, v) - - self.sa.add(new_users_group) - self.sa.commit() - return new_users_group + new = UsersGroup() + new.users_group_name = name + new.users_group_active = active + self.sa.add(new) + return new except: log.error(traceback.format_exc()) - self.sa.rollback() + raise + + def update(self, users_group, form_data): + + try: + users_group = self.__get_users_group(users_group) + + for k, v in form_data.items(): + if k == 'users_group_members': + users_group.members = [] + self.sa.flush() + members_list = [] + if v: + v = [v] if isinstance(v, basestring) else v + for u_id in set(v): + member = UsersGroupMember(users_group.users_group_id, u_id) + members_list.append(member) + setattr(users_group, 'members', members_list) + setattr(users_group, k, v) + + self.sa.add(users_group) + except: + log.error(traceback.format_exc()) + raise + + def delete(self, users_group, force=False): + """ + Deletes repos group, unless force flag is used + raises exception if there are members in that group, else deletes + group and users + + :param users_group: + :param force: + """ + try: + users_group = self.__get_users_group(users_group) + + # check if this group is not assigned to repo + assigned_groups = UsersGroupRepoToPerm.query()\ + .filter(UsersGroupRepoToPerm.users_group == users_group).all() + + if assigned_groups and force is False: + raise UsersGroupsAssignedException('RepoGroup assigned to %s' % + assigned_groups) + + self.sa.delete(users_group) + except: + log.error(traceback.format_exc()) raise def add_user_to_group(self, users_group, user): + users_group = self.__get_users_group(users_group) + user = self.__get_user(user) + for m in users_group.members: u = m.user if u.user_id == user.user_id: - return m + return True try: users_group_member = UsersGroupMember() @@ -81,9 +131,58 @@ class UsersGroupModel(BaseModel): user.group_member.append(users_group_member) self.sa.add(users_group_member) - self.sa.commit() return users_group_member except: log.error(traceback.format_exc()) - self.sa.rollback() raise + + def remove_user_from_group(self, users_group, user): + users_group = self.__get_users_group(users_group) + user = self.__get_user(user) + + users_group_member = None + for m in users_group.members: + if m.user.user_id == user.user_id: + # Found this user's membership row + users_group_member = m + break + + if users_group_member: + try: + self.sa.delete(users_group_member) + return True + except: + log.error(traceback.format_exc()) + raise + else: + # User isn't in that group + return False + + def has_perm(self, users_group, perm): + users_group = self.__get_users_group(users_group) + perm = self.__get_perm(perm) + + return UsersGroupToPerm.query()\ + .filter(UsersGroupToPerm.users_group == users_group)\ + .filter(UsersGroupToPerm.permission == perm).scalar() is not None + + def grant_perm(self, users_group, perm): + if not isinstance(perm, Permission): + raise Exception('perm needs to be an instance of Permission class') + + users_group = self.__get_users_group(users_group) + + new = UsersGroupToPerm() + new.users_group = users_group + new.permission = perm + self.sa.add(new) + + def revoke_perm(self, users_group, perm): + users_group = self.__get_users_group(users_group) + perm = self.__get_perm(perm) + + obj = UsersGroupToPerm.query()\ + .filter(UsersGroupToPerm.users_group == users_group)\ + .filter(UsersGroupToPerm.permission == perm).scalar() + if obj: + self.sa.delete(obj) diff --git a/rhodecode/public/css/diff.css b/rhodecode/public/css/diff.css deleted file mode 100644 --- a/rhodecode/public/css/diff.css +++ /dev/null @@ -1,119 +0,0 @@ -div.diffblock { - overflow: auto; - padding: 0px; - border: 1px solid #ccc; - background: #f8f8f8; - font-size: 100%; - line-height: 100%; - /* new */ - line-height: 125%; -} -div.diffblock .code-header{ - border-bottom: 1px solid #CCCCCC; - background: #EEEEEE; - padding:10px 0 10px 0; -} -div.diffblock .code-header div{ - margin-left:25px; - font-weight: bold; -} -div.diffblock .code-body{ - background: #FFFFFF; -} -div.diffblock pre.raw{ - background: #FFFFFF; - color:#000000; -} - -table.code-difftable{ - border-collapse: collapse; - width: 99%; -} -table.code-difftable td:target *{ - background: repeat scroll 0 0 #FFFFBE !important; - text-decoration: underline; -} - -table.code-difftable td { - padding: 0 !important; - background: none !important; - border:0 !important; -} - - -.code-difftable .context{ - background:none repeat scroll 0 0 #DDE7EF; -} -.code-difftable .add{ - background:none repeat scroll 0 0 #DDFFDD; -} -.code-difftable .add ins{ - background:none repeat scroll 0 0 #AAFFAA; - text-decoration:none; -} - -.code-difftable .del{ - background:none repeat scroll 0 0 #FFDDDD; -} -.code-difftable .del del{ - background:none repeat scroll 0 0 #FFAAAA; - text-decoration:none; -} - -.code-difftable .lineno{ - background:none repeat scroll 0 0 #EEEEEE !important; - padding-left:2px; - padding-right:2px; - text-align:right; - width:30px; - -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; - letter-spacing:-1px; - text-align:right; - width:20px; -} -.code-difftable .lineno a{ -font-weight: 700; -cursor: pointer; -} -.code-difftable .code td{ - margin:0; - padding: 0; -} -.code-difftable .code pre{ - margin:0; - padding:0; -} - -.code { - display: block; - width: 100%; -} -.code-diff { - padding: 0px; - margin-top: 5px; - margin-bottom: 5px; - border-left: 2px solid #ccc; -} -.code-diff pre, .line pre { - padding: 3px; - margin: 0; -} -.lineno a { - text-decoration: none; -} - -.line{ - padding:0; - margin:0; -} \ No newline at end of file diff --git a/rhodecode/public/css/pygments.css b/rhodecode/public/css/pygments.css --- a/rhodecode/public/css/pygments.css +++ b/rhodecode/public/css/pygments.css @@ -7,15 +7,53 @@ div.codeblock { line-height: 100%; /* new */ line-height: 125%; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; } div.codeblock .code-header{ border-bottom: 1px solid #CCCCCC; background: #EEEEEE; padding:10px 0 10px 0; } -div.codeblock .code-header .revision{ + +div.codeblock .code-header .stats{ + clear: both; + padding: 6px 8px 6px 10px; + border-bottom: 1px solid rgb(204, 204, 204); + height: 23px; + margin-bottom: 6px; +} + +div.codeblock .code-header .stats .left{ + float:left; +} +div.codeblock .code-header .stats .left.img{ + margin-top:-2px; +} +div.codeblock .code-header .stats .left.item{ + float:left; + padding: 0 9px 0 9px; + border-right:1px solid #ccc; +} +div.codeblock .code-header .stats .left.item pre{ + +} +div.codeblock .code-header .stats .left.item.last{ + border-right:none; +} +div.codeblock .code-header .stats .buttons{ + float:right; + padding-right:4px; +} + +div.codeblock .code-header .author{ margin-left:25px; font-weight: bold; + height: 25px; +} +div.codeblock .code-header .author .user{ + padding-top:3px; } div.codeblock .code-header .commit{ margin-left:25px; @@ -33,10 +71,20 @@ div.codeblock .code-body table td { div.code-body { background-color: #FFFFFF; } -div.code-body pre .match{ + +div.codeblock .code-header .search-path { + padding: 0px 0px 0px 10px; +} + +div.search-code-body { + background-color: #FFFFFF; + padding: 5px 0px 5px 10px; +} + +div.search-code-body pre .match{ background-color: #FAFFA6; } -div.code-body pre .break{ +div.search-code-body pre .break{ background-color: #DDE7EF; width: 100%; color: #747474; @@ -64,64 +112,64 @@ div.annotatediv{ .linenos a { text-decoration: none; } .code { display: block; } -.code-highlight .hll { background-color: #ffffcc } -.code-highlight .c { color: #408080; font-style: italic } /* Comment */ -.code-highlight .err { border: 1px solid #FF0000 } /* Error */ -.code-highlight .k { color: #008000; font-weight: bold } /* Keyword */ -.code-highlight .o { color: #666666 } /* Operator */ -.code-highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */ -.code-highlight .cp { color: #BC7A00 } /* Comment.Preproc */ -.code-highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */ -.code-highlight .cs { color: #408080; font-style: italic } /* Comment.Special */ -.code-highlight .gd { color: #A00000 } /* Generic.Deleted */ -.code-highlight .ge { font-style: italic } /* Generic.Emph */ -.code-highlight .gr { color: #FF0000 } /* Generic.Error */ -.code-highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.code-highlight .gi { color: #00A000 } /* Generic.Inserted */ -.code-highlight .go { color: #808080 } /* Generic.Output */ -.code-highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -.code-highlight .gs { font-weight: bold } /* Generic.Strong */ -.code-highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.code-highlight .gt { color: #0040D0 } /* Generic.Traceback */ -.code-highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ -.code-highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ -.code-highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ -.code-highlight .kp { color: #008000 } /* Keyword.Pseudo */ -.code-highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ -.code-highlight .kt { color: #B00040 } /* Keyword.Type */ -.code-highlight .m { color: #666666 } /* Literal.Number */ -.code-highlight .s { color: #BA2121 } /* Literal.String */ -.code-highlight .na { color: #7D9029 } /* Name.Attribute */ -.code-highlight .nb { color: #008000 } /* Name.Builtin */ -.code-highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ -.code-highlight .no { color: #880000 } /* Name.Constant */ -.code-highlight .nd { color: #AA22FF } /* Name.Decorator */ -.code-highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ -.code-highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ -.code-highlight .nf { color: #0000FF } /* Name.Function */ -.code-highlight .nl { color: #A0A000 } /* Name.Label */ -.code-highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ -.code-highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ -.code-highlight .nv { color: #19177C } /* Name.Variable */ -.code-highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ -.code-highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.code-highlight .mf { color: #666666 } /* Literal.Number.Float */ -.code-highlight .mh { color: #666666 } /* Literal.Number.Hex */ -.code-highlight .mi { color: #666666 } /* Literal.Number.Integer */ -.code-highlight .mo { color: #666666 } /* Literal.Number.Oct */ -.code-highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ -.code-highlight .sc { color: #BA2121 } /* Literal.String.Char */ -.code-highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ -.code-highlight .s2 { color: #BA2121 } /* Literal.String.Double */ -.code-highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ -.code-highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ -.code-highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ -.code-highlight .sx { color: #008000 } /* Literal.String.Other */ -.code-highlight .sr { color: #BB6688 } /* Literal.String.Regex */ -.code-highlight .s1 { color: #BA2121 } /* Literal.String.Single */ -.code-highlight .ss { color: #19177C } /* Literal.String.Symbol */ -.code-highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ -.code-highlight .vc { color: #19177C } /* Name.Variable.Class */ -.code-highlight .vg { color: #19177C } /* Name.Variable.Global */ -.code-highlight .vi { color: #19177C } /* Name.Variable.Instance */ -.code-highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ +.code-highlight .hll, .codehilite .hll { background-color: #ffffcc } +.code-highlight .c, .codehilite .c { color: #408080; font-style: italic } /* Comment */ +.code-highlight .err, .codehilite .err { border: 1px solid #FF0000 } /* Error */ +.code-highlight .k, .codehilite .k { color: #008000; font-weight: bold } /* Keyword */ +.code-highlight .o, .codehilite .o { color: #666666 } /* Operator */ +.code-highlight .cm, .codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */ +.code-highlight .cp, .codehilite .cp { color: #BC7A00 } /* Comment.Preproc */ +.code-highlight .c1, .codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */ +.code-highlight .cs, .codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */ +.code-highlight .gd, .codehilite .gd { color: #A00000 } /* Generic.Deleted */ +.code-highlight .ge, .codehilite .ge { font-style: italic } /* Generic.Emph */ +.code-highlight .gr, .codehilite .gr { color: #FF0000 } /* Generic.Error */ +.code-highlight .gh, .codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.code-highlight .gi, .codehilite .gi { color: #00A000 } /* Generic.Inserted */ +.code-highlight .go, .codehilite .go { color: #808080 } /* Generic.Output */ +.code-highlight .gp, .codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.code-highlight .gs, .codehilite .gs { font-weight: bold } /* Generic.Strong */ +.code-highlight .gu, .codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.code-highlight .gt, .codehilite .gt { color: #0040D0 } /* Generic.Traceback */ +.code-highlight .kc, .codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.code-highlight .kd, .codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.code-highlight .kn, .codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.code-highlight .kp, .codehilite .kp { color: #008000 } /* Keyword.Pseudo */ +.code-highlight .kr, .codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.code-highlight .kt, .codehilite .kt { color: #B00040 } /* Keyword.Type */ +.code-highlight .m, .codehilite .m { color: #666666 } /* Literal.Number */ +.code-highlight .s, .codehilite .s { color: #BA2121 } /* Literal.String */ +.code-highlight .na, .codehilite .na { color: #7D9029 } /* Name.Attribute */ +.code-highlight .nb, .codehilite .nb { color: #008000 } /* Name.Builtin */ +.code-highlight .nc, .codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.code-highlight .no, .codehilite .no { color: #880000 } /* Name.Constant */ +.code-highlight .nd, .codehilite .nd { color: #AA22FF } /* Name.Decorator */ +.code-highlight .ni, .codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */ +.code-highlight .ne, .codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.code-highlight .nf, .codehilite .nf { color: #0000FF } /* Name.Function */ +.code-highlight .nl, .codehilite .nl { color: #A0A000 } /* Name.Label */ +.code-highlight .nn, .codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.code-highlight .nt, .codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.code-highlight .nv, .codehilite .nv { color: #19177C } /* Name.Variable */ +.code-highlight .ow, .codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.code-highlight .w, .codehilite .w { color: #bbbbbb } /* Text.Whitespace */ +.code-highlight .mf, .codehilite .mf { color: #666666 } /* Literal.Number.Float */ +.code-highlight .mh, .codehilite .mh { color: #666666 } /* Literal.Number.Hex */ +.code-highlight .mi, .codehilite .mi { color: #666666 } /* Literal.Number.Integer */ +.code-highlight .mo, .codehilite .mo { color: #666666 } /* Literal.Number.Oct */ +.code-highlight .sb, .codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */ +.code-highlight .sc, .codehilite .sc { color: #BA2121 } /* Literal.String.Char */ +.code-highlight .sd, .codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.code-highlight .s2, .codehilite .s2 { color: #BA2121 } /* Literal.String.Double */ +.code-highlight .se, .codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.code-highlight .sh, .codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.code-highlight .si, .codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.code-highlight .sx, .codehilite .sx { color: #008000 } /* Literal.String.Other */ +.code-highlight .sr, .codehilite .sr { color: #BB6688 } /* Literal.String.Regex */ +.code-highlight .s1, .codehilite .s1 { color: #BA2121 } /* Literal.String.Single */ +.code-highlight .ss, .codehilite .ss { color: #19177C } /* Literal.String.Symbol */ +.code-highlight .bp, .codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.code-highlight .vc, .codehilite .vc { color: #19177C } /* Name.Variable.Class */ +.code-highlight .vg, .codehilite .vg { color: #19177C } /* Name.Variable.Global */ +.code-highlight .vi, .codehilite .vi { color: #19177C } /* Name.Variable.Instance */ +.code-highlight .il, .codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */ diff --git a/rhodecode/public/css/style.css b/rhodecode/public/css/style.css --- a/rhodecode/public/css/style.css +++ b/rhodecode/public/css/style.css @@ -1,2759 +1,4272 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td { -border:0; -outline:0; -font-size:100%; -vertical-align:baseline; -background:transparent; -margin:0; -padding:0; +html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td + { + border: 0; + outline: 0; + font-size: 100%; + vertical-align: baseline; + background: transparent; + margin: 0; + padding: 0; } body { -line-height:1; -height:100%; -background:url("../images/background.png") repeat scroll 0 0 #B0B0B0; -font-family:Lucida Grande, Verdana, Lucida Sans Regular, Lucida Sans Unicode, Arial, sans-serif; -font-size:12px; -color:#000; -margin:0; -padding:0; + line-height: 1; + height: 100%; + background: url("../images/background.png") repeat scroll 0 0 #B0B0B0; + font-family: Lucida Grande, Verdana, Lucida Sans Regular, + Lucida Sans Unicode, Arial, sans-serif; font-size : 12px; + color: #000; + margin: 0; + padding: 0; + font-size: 12px; } ol,ul { -list-style:none; + list-style: none; } blockquote,q { -quotes:none; + quotes: none; } blockquote:before,blockquote:after,q:before,q:after { -content:none; + content: none; } :focus { -outline:0; + outline: 0; } del { -text-decoration:line-through; + text-decoration: line-through; } table { -border-collapse:collapse; -border-spacing:0; + border-collapse: collapse; + border-spacing: 0; } html { -height:100%; + height: 100%; } a { -color:#003367; -text-decoration:none; -cursor:pointer; + color: #003367; + text-decoration: none; + cursor: pointer; } a:hover { -color:#316293; -text-decoration:underline; + color: #316293; + text-decoration: underline; } h1,h2,h3,h4,h5,h6 { -color:#292929; -font-weight:700; + color: #292929; + font-weight: 700; } h1 { -font-size:22px; + font-size: 22px; } h2 { -font-size:20px; + font-size: 20px; } h3 { -font-size:18px; + font-size: 18px; } h4 { -font-size:16px; + font-size: 16px; } h5 { -font-size:14px; + font-size: 14px; } h6 { -font-size:11px; + font-size: 11px; } ul.circle { -list-style-type:circle; + list-style-type: circle; } ul.disc { -list-style-type:disc; + list-style-type: disc; } ul.square { -list-style-type:square; + list-style-type: square; } ol.lower-roman { -list-style-type:lower-roman; + list-style-type: lower-roman; } ol.upper-roman { -list-style-type:upper-roman; + list-style-type: upper-roman; } ol.lower-alpha { -list-style-type:lower-alpha; + list-style-type: lower-alpha; } ol.upper-alpha { -list-style-type:upper-alpha; + list-style-type: upper-alpha; } ol.decimal { -list-style-type:decimal; + list-style-type: decimal; } div.color { -clear:both; -overflow:hidden; -position:absolute; -background:#FFF; -margin:7px 0 0 60px; -padding:1px 1px 1px 0; + clear: both; + overflow: hidden; + position: absolute; + background: #FFF; + margin: 7px 0 0 60px; + padding: 1px 1px 1px 0; } div.color a { -width:15px; -height:15px; -display:block; -float:left; -margin:0 0 0 1px; -padding:0; + width: 15px; + height: 15px; + display: block; + float: left; + margin: 0 0 0 1px; + padding: 0; } div.options { -clear:both; -overflow:hidden; -position:absolute; -background:#FFF; -margin:7px 0 0 162px; -padding:0; + clear: both; + overflow: hidden; + position: absolute; + background: #FFF; + margin: 7px 0 0 162px; + padding: 0; } div.options a { -height:1%; -display:block; -text-decoration:none; -margin:0; -padding:3px 8px; + height: 1%; + display: block; + text-decoration: none; + margin: 0; + padding: 3px 8px; } .top-left-rounded-corner { --webkit-border-top-left-radius: 8px; --khtml-border-radius-topleft: 8px; --moz-border-radius-topleft: 8px; -border-top-left-radius: 8px; + -webkit-border-top-left-radius: 8px; + -khtml-border-radius-topleft: 8px; + -moz-border-radius-topleft: 8px; + border-top-left-radius: 8px; } .top-right-rounded-corner { --webkit-border-top-right-radius: 8px; --khtml-border-radius-topright: 8px; --moz-border-radius-topright: 8px; -border-top-right-radius: 8px; + -webkit-border-top-right-radius: 8px; + -khtml-border-radius-topright: 8px; + -moz-border-radius-topright: 8px; + border-top-right-radius: 8px; } .bottom-left-rounded-corner { --webkit-border-bottom-left-radius: 8px; --khtml-border-radius-bottomleft: 8px; --moz-border-radius-bottomleft: 8px; -border-bottom-left-radius: 8px; + -webkit-border-bottom-left-radius: 8px; + -khtml-border-radius-bottomleft: 8px; + -moz-border-radius-bottomleft: 8px; + border-bottom-left-radius: 8px; } .bottom-right-rounded-corner { --webkit-border-bottom-right-radius: 8px; --khtml-border-radius-bottomright: 8px; --moz-border-radius-bottomright: 8px; -border-bottom-right-radius: 8px; -} - + -webkit-border-bottom-right-radius: 8px; + -khtml-border-radius-bottomright: 8px; + -moz-border-radius-bottomright: 8px; + border-bottom-right-radius: 8px; +} #header { -margin:0; -padding:0 10px; -} - - -#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; -box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); + margin: 0; + padding: 0 10px; +} + +#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-color: #eedc94; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), + to(#eedc94) ); + background-image: -moz-linear-gradient(top, #003b76, #00376e); + background-image: -ms-linear-gradient(top, #003b76, #00376e); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) ); + background-image: -webkit-linear-gradient(top, #003b76, #00376e); + background-image: -o-linear-gradient(top, #003b76, #00376e); + background-image: linear-gradient(top, #003b76, #00376e); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', + endColorstr='#00376e', GradientType=0 ); + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); } #header ul#logged-user li { -list-style:none; -float:left; -margin:8px 0 0; -padding:4px 12px; -border-left: 1px solid #316293; + list-style: none; + float: left; + margin: 8px 0 0; + padding: 4px 12px; + border-left: 1px solid #316293; } #header ul#logged-user li.first { -border-left:none; -margin:4px; + border-left: none; + margin: 4px; } #header ul#logged-user li.first div.gravatar { -margin-top:-2px; + margin-top: -2px; } #header ul#logged-user li.first div.account { -padding-top:4px; -float:left; + padding-top: 4px; + float: left; } #header ul#logged-user li.last { -border-right:none; + border-right: none; } #header ul#logged-user li a { -color:#fff; -font-weight:700; -text-decoration:none; + color: #fff; + font-weight: 700; + text-decoration: none; } #header ul#logged-user li a:hover { -text-decoration:underline; + text-decoration: underline; } #header ul#logged-user li.highlight a { -color:#fff; + color: #fff; } #header ul#logged-user li.highlight a:hover { -color:#FFF; + color: #FFF; } #header #header-inner { -min-height:40px; -clear:both; -position:relative; -background:#003367 url("../images/header_inner.png") repeat-x; -margin:0; -padding:0; -display:block; -box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); --webkit-border-radius: 4px 4px 4px 4px; --khtml-border-radius: 4px 4px 4px 4px; --moz-border-radius: 4px 4px 4px 4px; -border-radius: 4px 4px 4px 4px; -} - + min-height: 44px; + clear: both; + position: relative; + background-color: #eedc94; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1),to(#eedc94) ); + background-image: -moz-linear-gradient(top, #003b76, #00376e); + background-image: -ms-linear-gradient(top, #003b76, #00376e); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76),color-stop(100%, #00376e) ); + background-image: -webkit-linear-gradient(top, #003b76, #00376e); + background-image: -o-linear-gradient(top, #003b76, #00376e); + background-image: linear-gradient(top, #003b76, #00376e); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',endColorstr='#00376e', GradientType=0 ); + margin: 0; + padding: 0; + display: block; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); + -webkit-border-radius: 4px 4px 4px 4px; + -khtml-border-radius: 4px 4px 4px 4px; + -moz-border-radius: 4px 4px 4px 4px; + border-radius: 4px 4px 4px 4px; +} +#header #header-inner.hover{ + position: fixed !important; + width: 100% !important; + margin-left: -10px !important; + z-index: 10000; + -webkit-border-radius: 0px 0px 0px 0px; + -khtml-border-radius: 0px 0px 0px 0px; + -moz-border-radius: 0px 0px 0px 0px; + border-radius: 0px 0px 0px 0px; +} #header #header-inner #home a { -height:40px; -width:46px; -display:block; -background:url("../images/button_home.png"); -background-position:0 0; -margin:0; -padding:0; + height: 40px; + width: 46px; + display: block; + background: url("../images/button_home.png"); + background-position: 0 0; + margin: 0; + padding: 0; } #header #header-inner #home a:hover { -background-position:0 -40px; -} + background-position: 0 -40px; +} + #header #header-inner #logo { - float: left; - position: absolute; -} + float: left; + position: absolute; +} + #header #header-inner #logo h1 { -color:#FFF; -font-size:18px; -margin:10px 0 0 13px; -padding:0; + color: #FFF; + font-size: 20px; + margin: 12px 0 0 13px; + padding: 0; } #header #header-inner #logo a { -color:#fff; -text-decoration:none; + color: #fff; + text-decoration: none; } #header #header-inner #logo a:hover { -color:#bfe3ff; + color: #bfe3ff; } #header #header-inner #quick,#header #header-inner #quick ul { -position:relative; -float:right; -list-style-type:none; -list-style-position:outside; -margin:6px 5px 0 0; -padding:0; + position: relative; + float: right; + list-style-type: none; + list-style-position: outside; + margin: 8px 8px 0 0; + padding: 0; } #header #header-inner #quick li { -position:relative; -float:left; -margin:0 5px 0 0; -padding:0; -} - -#header #header-inner #quick li a { -top:0; -left:0; -height:1%; -display:block; -clear:both; -overflow:hidden; -color:#FFF; -font-weight:700; -text-decoration:none; -background:#369; -padding:0; --webkit-border-radius: 4px 4px 4px 4px; --khtml-border-radius: 4px 4px 4px 4px; --moz-border-radius: 4px 4px 4px 4px; -border-radius: 4px 4px 4px 4px; + position: relative; + float: left; + margin: 0 5px 0 0; + padding: 0; +} + +#header #header-inner #quick li a.menu_link { + top: 0; + left: 0; + height: 1%; + display: block; + clear: both; + overflow: hidden; + color: #FFF; + font-weight: 700; + text-decoration: none; + background: #369; + padding: 0; + -webkit-border-radius: 4px 4px 4px 4px; + -khtml-border-radius: 4px 4px 4px 4px; + -moz-border-radius: 4px 4px 4px 4px; + border-radius: 4px 4px 4px 4px; } #header #header-inner #quick li span.short { -padding:9px 6px 8px 6px; + padding: 9px 6px 8px 6px; } #header #header-inner #quick li span { -top:0; -right:0; -height:1%; -display:block; -float:left; -border-left:1px solid #3f6f9f; -margin:0; -padding:10px 12px 8px 10px; + top: 0; + right: 0; + height: 1%; + display: block; + float: left; + border-left: 1px solid #3f6f9f; + margin: 0; + padding: 10px 12px 8px 10px; } #header #header-inner #quick li span.normal { -border:none; -padding:10px 12px 8px; + border: none; + padding: 10px 12px 8px; } #header #header-inner #quick li span.icon { -top:0; -left:0; -border-left:none; -border-right:1px solid #2e5c89; -padding:8px 6px 4px; + top: 0; + left: 0; + border-left: none; + border-right: 1px solid #2e5c89; + padding: 8px 6px 4px; } #header #header-inner #quick li span.icon_short { -top:0; -left:0; -border-left:none; -border-right:1px solid #2e5c89; -padding:8px 6px 4px; -} -#header #header-inner #quick li span.icon img, #header #header-inner #quick li span.icon_short img { + top: 0; + left: 0; + border-left: none; + border-right: 1px solid #2e5c89; + padding: 8px 6px 4px; +} + +#header #header-inner #quick li span.icon img,#header #header-inner #quick li span.icon_short img + { margin: 0px -2px 0px 0px; } #header #header-inner #quick li a:hover { -background:#4e4e4e no-repeat top left; + background: #4e4e4e no-repeat top left; } #header #header-inner #quick li a:hover span { -border-left:1px solid #545454; -} - -#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; + border-left: 1px solid #545454; +} + +#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; } #header #header-inner #quick ul { -top:29px; -right:0; -min-width:200px; -display:none; -position:absolute; -background:#FFF; -border:1px solid #666; -border-top:1px solid #003367; -z-index:100; -margin:0; -padding:0; + top: 29px; + right: 0; + min-width: 200px; + display: none; + position: absolute; + background: #FFF; + border: 1px solid #666; + border-top: 1px solid #003367; + z-index: 100; + margin: 0px 0px 0px 0px; + padding: 0; } #header #header-inner #quick ul.repo_switcher { -max-height:275px; -overflow-x:hidden; -overflow-y:auto; -} + max-height: 275px; + overflow-x: hidden; + overflow-y: auto; +} + #header #header-inner #quick ul.repo_switcher li.qfilter_rs { -float:none; -margin:0; -border-bottom:2px solid #003367; -} - - -#header #header-inner #quick .repo_switcher_type{ -position:absolute; -left:0; -top:9px; - -} + float: none; + margin: 0; + border-bottom: 2px solid #003367; +} + +#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; + border-bottom: 1px solid #ddd; } #header #header-inner #quick li ul li a { -width:182px; -height:auto; -display:block; -float:left; -background:#FFF; -color:#003367; -font-weight:400; -margin:0; -padding:7px 9px; + width: 182px; + height: auto; + display: block; + float: left; + background: #FFF; + color: #003367; + font-weight: 400; + margin: 0; + padding: 7px 9px; } #header #header-inner #quick li ul li a:hover { -color:#000; -background:#FFF; + color: #000; + background: #FFF; } #header #header-inner #quick ul ul { -top:auto; + top: auto; } #header #header-inner #quick li ul ul { -right:200px; -max-height:275px; -overflow:auto; -overflow-x:hidden; -white-space:normal; -} - -#header #header-inner #quick li ul li a.journal,#header #header-inner #quick li ul li a.journal:hover { -background:url("../images/icons/book.png") no-repeat scroll 4px 9px #FFF; -width:167px; -margin:0; -padding:12px 9px 7px 24px; -} - -#header #header-inner #quick li ul li a.private_repo,#header #header-inner #quick li ul li a.private_repo:hover { -background:url("../images/icons/lock.png") no-repeat scroll 4px 9px #FFF; -min-width:167px; -margin:0; -padding:12px 9px 7px 24px; -} - -#header #header-inner #quick li ul li a.public_repo,#header #header-inner #quick li ul li a.public_repo:hover { -background:url("../images/icons/lock_open.png") no-repeat scroll 4px 9px #FFF; -min-width:167px; -margin:0; -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; -margin:0; -padding:12px 9px 7px 24px; -} - -#header #header-inner #quick li ul li a.repos_groups,#header #header-inner #quick li ul li a.repos_groups:hover { -background:url("../images/icons/database_link.png") no-repeat scroll 4px 9px #FFF; -width:167px; -margin:0; -padding:12px 9px 7px 24px; -} - -#header #header-inner #quick li ul li a.users,#header #header-inner #quick li ul li a.users:hover { -background:#FFF url("../images/icons/user_edit.png") no-repeat 4px 9px; -width:167px; -margin:0; -padding:12px 9px 7px 24px; -} - -#header #header-inner #quick li ul li a.groups,#header #header-inner #quick li ul li a.groups:hover { -background:#FFF url("../images/icons/group_edit.png") no-repeat 4px 9px; -width:167px; -margin:0; -padding:12px 9px 7px 24px; -} - -#header #header-inner #quick li ul li a.settings,#header #header-inner #quick li ul li a.settings:hover { -background:#FFF url("../images/icons/cog.png") no-repeat 4px 9px; -width:167px; -margin:0; -padding:12px 9px 7px 24px; -} - -#header #header-inner #quick li ul li a.permissions,#header #header-inner #quick li ul li a.permissions:hover { -background:#FFF url("../images/icons/key.png") no-repeat 4px 9px; -width:167px; -margin:0; -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; -margin:0; -padding:12px 9px 7px 24px; -} - -#header #header-inner #quick li ul li a.search,#header #header-inner #quick li ul li a.search:hover { -background:#FFF url("../images/icons/search_16.png") no-repeat 4px 9px; -width:167px; -margin:0; -padding:12px 9px 7px 24px; -} - -#header #header-inner #quick li ul li a.delete,#header #header-inner #quick li ul li a.delete:hover { -background:#FFF url("../images/icons/delete.png") no-repeat 4px 9px; -width:167px; -margin:0; -padding:12px 9px 7px 24px; -} - -#header #header-inner #quick li ul li a.branches,#header #header-inner #quick li ul li a.branches:hover { -background:#FFF url("../images/icons/arrow_branch.png") no-repeat 4px 9px; -width:167px; -margin:0; -padding:12px 9px 7px 24px; -} - -#header #header-inner #quick li ul li a.tags,#header #header-inner #quick li ul li a.tags:hover { -background:#FFF url("../images/icons/tag_blue.png") no-repeat 4px 9px; -width:167px; -margin:0; -padding:12px 9px 7px 24px; -} - -#header #header-inner #quick li ul li a.admin,#header #header-inner #quick li ul li a.admin:hover { -background:#FFF url("../images/icons/cog_edit.png") no-repeat 4px 9px; -width:167px; -margin:0; -padding:12px 9px 7px 24px; -} - + right: 200px; + max-height: 275px; + overflow: auto; + overflow-x: hidden; + white-space: normal; +} + +#header #header-inner #quick li ul li a.journal,#header #header-inner #quick li ul li a.journal:hover + { + background: url("../images/icons/book.png") no-repeat scroll 4px 9px + #FFF; + width: 167px; + margin: 0; + padding: 12px 9px 7px 24px; +} + +#header #header-inner #quick li ul li a.private_repo,#header #header-inner #quick li ul li a.private_repo:hover + { + background: url("../images/icons/lock.png") no-repeat scroll 4px 9px + #FFF; + min-width: 167px; + margin: 0; + padding: 12px 9px 7px 24px; +} + +#header #header-inner #quick li ul li a.public_repo,#header #header-inner #quick li ul li a.public_repo:hover + { + background: url("../images/icons/lock_open.png") no-repeat scroll 4px + 9px #FFF; + min-width: 167px; + margin: 0; + 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; + margin: 0; + padding: 12px 9px 7px 24px; +} + +#header #header-inner #quick li ul li a.repos_groups,#header #header-inner #quick li ul li a.repos_groups:hover + { + background: url("../images/icons/database_link.png") no-repeat scroll + 4px 9px #FFF; + width: 167px; + margin: 0; + padding: 12px 9px 7px 24px; +} + +#header #header-inner #quick li ul li a.users,#header #header-inner #quick li ul li a.users:hover + { + background: #FFF url("../images/icons/user_edit.png") no-repeat 4px 9px; + width: 167px; + margin: 0; + padding: 12px 9px 7px 24px; +} + +#header #header-inner #quick li ul li a.groups,#header #header-inner #quick li ul li a.groups:hover + { + background: #FFF url("../images/icons/group_edit.png") no-repeat 4px 9px; + width: 167px; + margin: 0; + padding: 12px 9px 7px 24px; +} + +#header #header-inner #quick li ul li a.settings,#header #header-inner #quick li ul li a.settings:hover + { + background: #FFF url("../images/icons/cog.png") no-repeat 4px 9px; + width: 167px; + margin: 0; + padding: 12px 9px 7px 24px; +} + +#header #header-inner #quick li ul li a.permissions,#header #header-inner #quick li ul li a.permissions:hover + { + background: #FFF url("../images/icons/key.png") no-repeat 4px 9px; + width: 167px; + margin: 0; + 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; + margin: 0; + padding: 12px 9px 7px 24px; +} + +#header #header-inner #quick li ul li a.search,#header #header-inner #quick li ul li a.search:hover + { + background: #FFF url("../images/icons/search_16.png") no-repeat 4px 9px; + width: 167px; + margin: 0; + padding: 12px 9px 7px 24px; +} + +#header #header-inner #quick li ul li a.delete,#header #header-inner #quick li ul li a.delete:hover + { + background: #FFF url("../images/icons/delete.png") no-repeat 4px 9px; + width: 167px; + margin: 0; + padding: 12px 9px 7px 24px; +} + +#header #header-inner #quick li ul li a.branches,#header #header-inner #quick li ul li a.branches:hover + { + background: #FFF url("../images/icons/arrow_branch.png") no-repeat 4px + 9px; + width: 167px; + margin: 0; + padding: 12px 9px 7px 24px; +} + +#header #header-inner #quick li ul li a.tags, +#header #header-inner #quick li ul li a.tags:hover{ + background: #FFF url("../images/icons/tag_blue.png") no-repeat 4px 9px; + width: 167px; + margin: 0; + padding: 12px 9px 7px 24px; +} + +#header #header-inner #quick li ul li a.bookmarks, +#header #header-inner #quick li ul li a.bookmarks:hover{ + background: #FFF url("../images/icons/tag_green.png") no-repeat 4px 9px; + width: 167px; + margin: 0; + padding: 12px 9px 7px 24px; +} + +#header #header-inner #quick li ul li a.admin, +#header #header-inner #quick li ul li a.admin:hover{ + background: #FFF url("../images/icons/cog_edit.png") no-repeat 4px 9px; + width: 167px; + margin: 0; + padding: 12px 9px 7px 24px; +} .groups_breadcrumbs a { color: #fff; } + .groups_breadcrumbs a:hover { - color: #bfe3ff; - text-decoration: none; -} - -.quick_repo_menu{ + color: #bfe3ff; + text-decoration: none; +} + +td.quick_repo_menu { background: #FFF url("../images/vertical-indicator.png") 8px 50% no-repeat !important; cursor: pointer; width: 8px; -} -.quick_repo_menu.active{ - background: #FFF url("../images/horizontal-indicator.png") 4px 50% no-repeat !important; + border: 1px solid transparent; +} + +td.quick_repo_menu.active { + background: url("../images/dt-arrow-dn.png") no-repeat scroll 5px 50% #FFFFFF !important; + border: 1px solid #003367; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); cursor: pointer; } -.quick_repo_menu .menu_items{ - margin-top:6px; - width:150px; + +td.quick_repo_menu .menu_items { + margin-top: 10px; + margin-left:-6px; + width: 150px; position: absolute; - background-color:#FFF; - background: none repeat scroll 0 0 #FFFFFF; - border-color: #003367 #666666 #666666; - border-right: 1px solid #666666; - border-style: solid; - border-width: 1px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); -} -.quick_repo_menu .menu_items li{ - padding:0 !important; -} -.quick_repo_menu .menu_items a{ + background-color: #FFF; + background: none repeat scroll 0 0 #FFFFFF; + border-color: #003367 #666666 #666666; + border-right: 1px solid #666666; + border-style: solid; + border-width: 1px; + box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2); + border-top-style: none; +} + +td.quick_repo_menu .menu_items li { + padding: 0 !important; +} + +td.quick_repo_menu .menu_items a { display: block; padding: 4px 12px 4px 8px; } -.quick_repo_menu .menu_items a:hover{ - background-color: #EEE; + +td.quick_repo_menu .menu_items a:hover { + background-color: #EEE; + text-decoration: none; +} + +td.quick_repo_menu .menu_items .icon img { + margin-bottom: -2px; +} + +td.quick_repo_menu .menu_items.hidden { + display: none; +} + +.yui-dt-first th { + text-align: left; +} + +/* +Copyright (c) 2011, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.com/yui/license.html +version: 2.9.0 +*/ +.yui-skin-sam .yui-dt-mask { + position: absolute; + z-index: 9500; +} +.yui-dt-tmp { + position: absolute; + left: -9000px; +} +.yui-dt-scrollable .yui-dt-bd { overflow: auto } +.yui-dt-scrollable .yui-dt-hd { + overflow: hidden; + position: relative; +} +.yui-dt-scrollable .yui-dt-bd thead tr, +.yui-dt-scrollable .yui-dt-bd thead th { + position: absolute; + left: -1500px; +} +.yui-dt-scrollable tbody { -moz-outline: 0 } +.yui-skin-sam thead .yui-dt-sortable { cursor: pointer } +.yui-skin-sam thead .yui-dt-draggable { cursor: move } +.yui-dt-coltarget { + position: absolute; + z-index: 999; +} +.yui-dt-hd { zoom: 1 } +th.yui-dt-resizeable .yui-dt-resizerliner { position: relative } +.yui-dt-resizer { + position: absolute; + right: 0; + bottom: 0; + height: 100%; + cursor: e-resize; + cursor: col-resize; + background-color: #CCC; + opacity: 0; + filter: alpha(opacity=0); +} +.yui-dt-resizerproxy { + visibility: hidden; + position: absolute; + z-index: 9000; + background-color: #CCC; + opacity: 0; + filter: alpha(opacity=0); +} +th.yui-dt-hidden .yui-dt-liner, +td.yui-dt-hidden .yui-dt-liner, +th.yui-dt-hidden .yui-dt-resizer { display: none } +.yui-dt-editor, +.yui-dt-editor-shim { + position: absolute; + z-index: 9000; +} +.yui-skin-sam .yui-dt table { + margin: 0; + padding: 0; + font-family: arial; + font-size: inherit; + border-collapse: separate; + *border-collapse: collapse; + border-spacing: 0; + border: 1px solid #7f7f7f; +} +.yui-skin-sam .yui-dt thead { border-spacing: 0 } +.yui-skin-sam .yui-dt caption { + color: #000; + font-size: 85%; + font-weight: normal; + font-style: italic; + line-height: 1; + padding: 1em 0; + text-align: center; +} +.yui-skin-sam .yui-dt th { background: #d8d8da url(../images/sprite.png) repeat-x 0 0 } +.yui-skin-sam .yui-dt th, +.yui-skin-sam .yui-dt th a { + font-weight: normal; text-decoration: none; - -} -.quick_repo_menu .menu_items .icon img{ - margin-bottom:-2px; -} -.quick_repo_menu .menu_items.hidden{ - display: none; + color: #000; + vertical-align: bottom; +} +.yui-skin-sam .yui-dt th { + margin: 0; + padding: 0; + border: 0; + border-right: 1px solid #cbcbcb; +} +.yui-skin-sam .yui-dt tr.yui-dt-first td { border-top: 1px solid #7f7f7f } +.yui-skin-sam .yui-dt th .yui-dt-liner { white-space: nowrap } +.yui-skin-sam .yui-dt-liner { + margin: 0; + padding: 0; +} +.yui-skin-sam .yui-dt-coltarget { + width: 5px; + background-color: red; +} +.yui-skin-sam .yui-dt td { + margin: 0; + padding: 0; + border: 0; + border-right: 1px solid #cbcbcb; + text-align: left; +} +.yui-skin-sam .yui-dt-list td { border-right: 0 } +.yui-skin-sam .yui-dt-resizer { width: 6px } +.yui-skin-sam .yui-dt-mask { + background-color: #000; + opacity: .25; + filter: alpha(opacity=25); +} +.yui-skin-sam .yui-dt-message { background-color: #FFF } +.yui-skin-sam .yui-dt-scrollable table { border: 0 } +.yui-skin-sam .yui-dt-scrollable .yui-dt-hd { + border-left: 1px solid #7f7f7f; + border-top: 1px solid #7f7f7f; + border-right: 1px solid #7f7f7f; +} +.yui-skin-sam .yui-dt-scrollable .yui-dt-bd { + border-left: 1px solid #7f7f7f; + border-bottom: 1px solid #7f7f7f; + border-right: 1px solid #7f7f7f; + background-color: #FFF; +} +.yui-skin-sam .yui-dt-scrollable .yui-dt-data tr.yui-dt-last td { border-bottom: 1px solid #7f7f7f } +.yui-skin-sam th.yui-dt-asc, +.yui-skin-sam th.yui-dt-desc { background: url(../images/sprite.png) repeat-x 0 -100px } +.yui-skin-sam th.yui-dt-sortable .yui-dt-label { margin-right: 10px } +.yui-skin-sam th.yui-dt-asc .yui-dt-liner { background: url(../images/dt-arrow-up.png) no-repeat right } +.yui-skin-sam th.yui-dt-desc .yui-dt-liner { background: url(../images/dt-arrow-dn.png) no-repeat right } +tbody .yui-dt-editable { cursor: pointer } +.yui-dt-editor { + text-align: left; + background-color: #f2f2f2; + border: 1px solid #808080; + padding: 6px; +} +.yui-dt-editor label { + padding-left: 4px; + padding-right: 6px; +} +.yui-dt-editor .yui-dt-button { + padding-top: 6px; + text-align: right; +} +.yui-dt-editor .yui-dt-button button { + background: url(../images/sprite.png) repeat-x 0 0; + border: 1px solid #999; + width: 4em; + height: 1.8em; + margin-left: 6px; +} +.yui-dt-editor .yui-dt-button button.yui-dt-default { + background: url(../images/sprite.png) repeat-x 0 -1400px; + background-color: #5584e0; + border: 1px solid #304369; + color: #FFF; +} +.yui-dt-editor .yui-dt-button button:hover { + background: url(../images/sprite.png) repeat-x 0 -1300px; + color: #000; +} +.yui-dt-editor .yui-dt-button button:active { + background: url(../images/sprite.png) repeat-x 0 -1700px; + color: #000; +} +.yui-skin-sam tr.yui-dt-even { background-color: #FFF } +.yui-skin-sam tr.yui-dt-odd { background-color: #edf5ff } +.yui-skin-sam tr.yui-dt-even td.yui-dt-asc, +.yui-skin-sam tr.yui-dt-even td.yui-dt-desc { background-color: #edf5ff } +.yui-skin-sam tr.yui-dt-odd td.yui-dt-asc, +.yui-skin-sam tr.yui-dt-odd td.yui-dt-desc { background-color: #dbeaff } +.yui-skin-sam .yui-dt-list tr.yui-dt-even { background-color: #FFF } +.yui-skin-sam .yui-dt-list tr.yui-dt-odd { background-color: #FFF } +.yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-asc, +.yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-desc { background-color: #edf5ff } +.yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-asc, +.yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-desc { background-color: #edf5ff } +.yui-skin-sam th.yui-dt-highlighted, +.yui-skin-sam th.yui-dt-highlighted a { background-color: #b2d2ff } +.yui-skin-sam tr.yui-dt-highlighted, +.yui-skin-sam tr.yui-dt-highlighted td.yui-dt-asc, +.yui-skin-sam tr.yui-dt-highlighted td.yui-dt-desc, +.yui-skin-sam tr.yui-dt-even td.yui-dt-highlighted, +.yui-skin-sam tr.yui-dt-odd td.yui-dt-highlighted { + cursor: pointer; + background-color: #b2d2ff; +} +.yui-skin-sam .yui-dt-list th.yui-dt-highlighted, +.yui-skin-sam .yui-dt-list th.yui-dt-highlighted a { background-color: #b2d2ff } +.yui-skin-sam .yui-dt-list tr.yui-dt-highlighted, +.yui-skin-sam .yui-dt-list tr.yui-dt-highlighted td.yui-dt-asc, +.yui-skin-sam .yui-dt-list tr.yui-dt-highlighted td.yui-dt-desc, +.yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-highlighted, +.yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-highlighted { + cursor: pointer; + background-color: #b2d2ff; +} +.yui-skin-sam th.yui-dt-selected, +.yui-skin-sam th.yui-dt-selected a { background-color: #446cd7 } +.yui-skin-sam tr.yui-dt-selected td, +.yui-skin-sam tr.yui-dt-selected td.yui-dt-asc, +.yui-skin-sam tr.yui-dt-selected td.yui-dt-desc { + background-color: #426fd9; + color: #FFF; +} +.yui-skin-sam tr.yui-dt-even td.yui-dt-selected, +.yui-skin-sam tr.yui-dt-odd td.yui-dt-selected { + background-color: #446cd7; + color: #FFF; +} +.yui-skin-sam .yui-dt-list th.yui-dt-selected, +.yui-skin-sam .yui-dt-list th.yui-dt-selected a { background-color: #446cd7 } +.yui-skin-sam .yui-dt-list tr.yui-dt-selected td, +.yui-skin-sam .yui-dt-list tr.yui-dt-selected td.yui-dt-asc, +.yui-skin-sam .yui-dt-list tr.yui-dt-selected td.yui-dt-desc { + background-color: #426fd9; + color: #FFF; +} +.yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-selected, +.yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-selected { + background-color: #446cd7; + color: #FFF; +} +.yui-skin-sam .yui-dt-paginator { + display: block; + margin: 6px 0; + white-space: nowrap; +} +.yui-skin-sam .yui-dt-paginator .yui-dt-first, +.yui-skin-sam .yui-dt-paginator .yui-dt-last, +.yui-skin-sam .yui-dt-paginator .yui-dt-selected { padding: 2px 6px } +.yui-skin-sam .yui-dt-paginator a.yui-dt-first, +.yui-skin-sam .yui-dt-paginator a.yui-dt-last { text-decoration: none } +.yui-skin-sam .yui-dt-paginator .yui-dt-previous, +.yui-skin-sam .yui-dt-paginator .yui-dt-next { display: none } +.yui-skin-sam a.yui-dt-page { + border: 1px solid #cbcbcb; + padding: 2px 6px; + text-decoration: none; + background-color: #fff; +} +.yui-skin-sam .yui-dt-selected { + border: 1px solid #fff; + background-color: #fff; } #content #left { -left:0; -width:280px; -position:absolute; + left: 0; + width: 280px; + position: absolute; } #content #right { -margin:0 60px 10px 290px; + margin: 0 60px 10px 290px; } #content div.box { -clear:both; -overflow:hidden; -background:#fff; -margin:0 0 10px; -padding:0 0 10px; --webkit-border-radius: 4px 4px 4px 4px; --khtml-border-radius: 4px 4px 4px 4px; --moz-border-radius: 4px 4px 4px 4px; -border-radius: 4px 4px 4px 4px; -box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); - + clear: both; + overflow: hidden; + background: #fff; + margin: 0 0 10px; + padding: 0 0 10px; + -webkit-border-radius: 4px 4px 4px 4px; + -khtml-border-radius: 4px 4px 4px 4px; + -moz-border-radius: 4px 4px 4px 4px; + border-radius: 4px 4px 4px 4px; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); } #content div.box-left { -width:49%; -clear:none; -float:left; -margin:0 0 10px; + width: 49%; + clear: none; + float: left; + margin: 0 0 10px; } #content div.box-right { -width:49%; -clear:none; -float:right; -margin:0 0 10px; + width: 49%; + clear: none; + float: right; + margin: 0 0 10px; } #content div.box div.title { -clear:both; -overflow:hidden; -background:#369 url("../images/header_inner.png") repeat-x; -margin:0 0 20px; -padding:0; + clear: both; + overflow: hidden; + background-color: #eedc94; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94) ); + background-image: -moz-linear-gradient(top, #003b76, #00376e); + background-image: -ms-linear-gradient(top, #003b76, #00376e); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) ); + background-image: -webkit-linear-gradient(top, #003b76, #00376e); + background-image: -o-linear-gradient(top, #003b76, #00376e); + background-image: linear-gradient(top, #003b76, #00376e); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', endColorstr='#00376e', GradientType=0 ); + margin: 0 0 20px; + padding: 0; } #content div.box div.title h5 { -float:left; -border:none; -color:#fff; -text-transform:uppercase; -margin:0; -padding:11px 0 11px 10px; + float: left; + border: none; + color: #fff; + text-transform: uppercase; + margin: 0; + padding: 11px 0 11px 10px; +} + +#content div.box div.title .link-white{ + color: #FFFFFF; } #content div.box div.title ul.links li { -list-style:none; -float:left; -margin:0; -padding:0; + list-style: none; + float: left; + margin: 0; + padding: 0; } #content div.box div.title ul.links li a { - border-left: 1px solid #316293; - color: #FFFFFF; - display: block; - float: left; - font-size: 13px; - font-weight: 700; - height: 1%; - margin: 0; - padding: 11px 22px 12px; - text-decoration: none; -} - -#content div.box h1,#content div.box h2,#content div.box h3,#content div.box h4,#content div.box h5,#content div.box h6 { -clear:both; -overflow:hidden; -border-bottom:1px solid #DDD; -margin:10px 20px; -padding:0 0 15px; + border-left: 1px solid #316293; + color: #FFFFFF; + display: block; + float: left; + font-size: 13px; + font-weight: 700; + height: 1%; + margin: 0; + padding: 11px 22px 12px; + text-decoration: none; +} + +#content div.box h1,#content div.box h2,#content div.box h3,#content div.box h4,#content div.box h5,#content div.box h6 + { + clear: both; + overflow: hidden; + border-bottom: 1px solid #DDD; + margin: 10px 20px; + padding: 0 0 15px; } #content div.box p { -color:#5f5f5f; -font-size:12px; -line-height:150%; -margin:0 24px 10px; -padding:0; + color: #5f5f5f; + font-size: 12px; + line-height: 150%; + margin: 0 24px 10px; + padding: 0; } #content div.box blockquote { -border-left:4px solid #DDD; -color:#5f5f5f; -font-size:11px; -line-height:150%; -margin:0 34px; -padding:0 0 0 14px; + border-left: 4px solid #DDD; + color: #5f5f5f; + font-size: 11px; + line-height: 150%; + margin: 0 34px; + padding: 0 0 0 14px; } #content div.box blockquote p { -margin:10px 0; -padding:0; + margin: 10px 0; + padding: 0; } #content div.box dl { -margin:10px 24px; + margin: 10px 0px; } #content div.box dt { -font-size:12px; -margin:0; + font-size: 12px; + margin: 0; } #content div.box dd { -font-size:12px; -margin:0; -padding:8px 0 8px 15px; + font-size: 12px; + margin: 0; + padding: 8px 0 8px 15px; } #content div.box li { -font-size:12px; -padding:4px 0; + font-size: 12px; + padding: 4px 0; } #content div.box ul.disc,#content div.box ul.circle { -margin:10px 24px 10px 38px; + margin: 10px 24px 10px 38px; } #content div.box ul.square { -margin:10px 24px 10px 40px; + margin: 10px 24px 10px 40px; } #content div.box img.left { -border:none; -float:left; -margin:10px 10px 10px 0; + border: none; + float: left; + margin: 10px 10px 10px 0; } #content div.box img.right { -border:none; -float:right; -margin:10px 0 10px 10px; + border: none; + float: right; + margin: 10px 0 10px 10px; } #content div.box div.messages { -clear:both; -overflow:hidden; -margin:0 20px; -padding:0; + clear: both; + overflow: hidden; + margin: 0 20px; + padding: 0; } #content div.box div.message { -clear:both; -overflow:hidden; -margin:0; -padding:10px 0; + clear: both; + overflow: hidden; + margin: 0; + padding: 5px 0; + white-space: pre-wrap; +} +#content div.box div.expand { + width: 110%; + height:14px; + font-size:10px; + text-align:center; + cursor: pointer; + color:#666; + + background:-webkit-gradient(linear,0% 50%,100% 50%,color-stop(0%,rgba(255,255,255,0)),color-stop(100%,rgba(64,96,128,0.1))); + background:-webkit-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1)); + background:-moz-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1)); + background:-o-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1)); + background:-ms-linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1)); + background:linear-gradient(top,rgba(255,255,255,0),rgba(64,96,128,0.1)); + + display: none; +} +#content div.box div.expand .expandtext { + background-color: #ffffff; + padding: 2px; + border-radius: 2px; } #content div.box div.message a { -font-weight:400 !important; + font-weight: 400 !important; } #content div.box div.message div.image { -float:left; -margin:9px 0 0 5px; -padding:6px; + float: left; + margin: 9px 0 0 5px; + padding: 6px; } #content div.box div.message div.image img { -vertical-align:middle; -margin:0; + vertical-align: middle; + margin: 0; } #content div.box div.message div.text { -float:left; -margin:0; -padding:9px 6px; + float: left; + margin: 0; + padding: 9px 6px; } #content div.box div.message div.dismiss a { -height:16px; -width:16px; -display:block; -background:url("../images/icons/cross.png") no-repeat; -margin:15px 14px 0 0; -padding:0; -} - -#content div.box div.message div.text h1,#content div.box div.message div.text h2,#content div.box div.message div.text h3,#content div.box div.message div.text h4,#content div.box div.message div.text h5,#content div.box div.message div.text h6 { -border:none; -margin:0; -padding:0; + height: 16px; + width: 16px; + display: block; + background: url("../images/icons/cross.png") no-repeat; + margin: 15px 14px 0 0; + padding: 0; +} + +#content div.box div.message div.text h1,#content div.box div.message div.text h2,#content div.box div.message div.text h3,#content div.box div.message div.text h4,#content div.box div.message div.text h5,#content div.box div.message div.text h6 + { + border: none; + margin: 0; + padding: 0; } #content div.box div.message div.text span { -height:1%; -display:block; -margin:0; -padding:5px 0 0; + height: 1%; + display: block; + margin: 0; + padding: 5px 0 0; } #content div.box div.message-error { -height:1%; -clear:both; -overflow:hidden; -background:#FBE3E4; -border:1px solid #FBC2C4; -color:#860006; + height: 1%; + clear: both; + overflow: hidden; + background: #FBE3E4; + border: 1px solid #FBC2C4; + color: #860006; } #content div.box div.message-error h6 { -color:#860006; + color: #860006; } #content div.box div.message-warning { -height:1%; -clear:both; -overflow:hidden; -background:#FFF6BF; -border:1px solid #FFD324; -color:#5f5200; + height: 1%; + clear: both; + overflow: hidden; + background: #FFF6BF; + border: 1px solid #FFD324; + color: #5f5200; } #content div.box div.message-warning h6 { -color:#5f5200; + color: #5f5200; } #content div.box div.message-notice { -height:1%; -clear:both; -overflow:hidden; -background:#8FBDE0; -border:1px solid #6BACDE; -color:#003863; + height: 1%; + clear: both; + overflow: hidden; + background: #8FBDE0; + border: 1px solid #6BACDE; + color: #003863; } #content div.box div.message-notice h6 { -color:#003863; + color: #003863; } #content div.box div.message-success { -height:1%; -clear:both; -overflow:hidden; -background:#E6EFC2; -border:1px solid #C6D880; -color:#4e6100; + height: 1%; + clear: both; + overflow: hidden; + background: #E6EFC2; + border: 1px solid #C6D880; + color: #4e6100; } #content div.box div.message-success h6 { -color:#4e6100; + color: #4e6100; } #content div.box div.form div.fields div.field { -height:1%; -border-bottom:1px solid #DDD; -clear:both; -margin:0; -padding:10px 0; + height: 1%; + border-bottom: 1px solid #DDD; + clear: both; + margin: 0; + padding: 10px 0; } #content div.box div.form div.fields div.field-first { -padding:0 0 10px; + padding: 0 0 10px; } #content div.box div.form div.fields div.field-noborder { -border-bottom:0 !important; + border-bottom: 0 !important; } #content div.box div.form div.fields div.field span.error-message { -height:1%; -display:inline-block; -color:red; -margin:8px 0 0 4px; -padding:0; + height: 1%; + display: inline-block; + color: red; + margin: 8px 0 0 4px; + padding: 0; } #content div.box div.form div.fields div.field span.success { -height:1%; -display:block; -color:#316309; -margin:8px 0 0; -padding:0; + height: 1%; + display: block; + color: #316309; + margin: 8px 0 0; + padding: 0; } #content div.box div.form div.fields div.field div.label { -left:70px; -width:155px; -position:absolute; -margin:0; -padding:8px 0 0 5px; -} - -#content div.box-left div.form div.fields div.field div.label,#content div.box-right div.form div.fields div.field div.label { -clear:both; -overflow:hidden; -left:0; -width:auto; -position:relative; -margin:0; -padding:0 0 8px; + left: 70px; + width: 155px; + position: absolute; + margin: 0; + padding: 5px 0 0 0px; +} + +#content div.box div.form div.fields div.field div.label-summary { + left: 30px; + width: 155px; + position: absolute; + margin: 0; + padding: 0px 0 0 0px; +} + +#content div.box-left div.form div.fields div.field div.label, +#content div.box-right div.form div.fields div.field div.label, +#content div.box-left div.form div.fields div.field div.label, +#content div.box-left div.form div.fields div.field div.label-summary, +#content div.box-right div.form div.fields div.field div.label-summary, +#content div.box-left div.form div.fields div.field div.label-summary + { + clear: both; + overflow: hidden; + left: 0; + width: auto; + position: relative; + margin: 0; + padding: 0 0 8px; } #content div.box div.form div.fields div.field div.label-select { -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 { -padding:0 0 8px; -} - -#content div.box-left div.form div.fields div.field div.label-textarea,#content div.box-right div.form div.fields div.field div.label-textarea { -padding:0 0 8px !important; -} - -#content div.box div.form div.fields div.field div.label label, div.label label{ -color:#393939; -font-weight:700; -} - + 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 + { + padding: 0 0 8px; +} + +#content div.box-left div.form div.fields div.field div.label-textarea, +#content div.box-right div.form div.fields div.field div.label-textarea + { + padding: 0 0 8px !important; +} + +#content div.box div.form div.fields div.field div.label label,div.label label + { + color: #393939; + font-weight: 700; +} +#content div.box div.form div.fields div.field div.label label,div.label-summary label + { + color: #393939; + font-weight: 700; +} #content div.box div.form div.fields div.field div.input { -margin:0 0 0 200px; + margin: 0 0 0 200px; +} + +#content div.box div.form div.fields div.field div.input.summary { + margin: 0 0 0 110px; +} +#content div.box div.form div.fields div.field div.input.summary-short { + margin: 0 0 0 110px; } #content div.box div.form div.fields div.field div.file { -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 { -margin:0 0 0 0px; + 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 + { + margin: 0 0 0 0px; } #content div.box div.form div.fields div.field div.input input { -background:#FFF; -border-top:1px solid #b3b3b3; -border-left:1px solid #b3b3b3; -border-right:1px solid #eaeaea; -border-bottom:1px solid #eaeaea; -color:#000; -font-family:Lucida Grande, Verdana, Lucida Sans Regular, Lucida Sans Unicode, Arial, sans-serif; -font-size:11px; -margin:0; -padding:7px 7px 6px; + background: #FFF; + border-top: 1px solid #b3b3b3; + border-left: 1px solid #b3b3b3; + border-right: 1px solid #eaeaea; + border-bottom: 1px solid #eaeaea; + color: #000; + font-size: 11px; + margin: 0; + padding: 7px 7px 6px; +} + +#content div.box div.form div.fields div.field div.input input#clone_url, +#content div.box div.form div.fields div.field div.input input#clone_url_id +{ + font-size: 16px; + padding: 2px; } #content div.box div.form div.fields div.field div.file input { - background: none repeat scroll 0 0 #FFFFFF; - border-color: #B3B3B3 #EAEAEA #EAEAEA #B3B3B3; - border-style: solid; - border-width: 1px; - color: #000000; - font-family: Lucida Grande,Verdana,Lucida Sans Regular,Lucida Sans Unicode,Arial,sans-serif; - font-size: 11px; - margin: 0; - padding: 7px 7px 6px; + background: none repeat scroll 0 0 #FFFFFF; + border-color: #B3B3B3 #EAEAEA #EAEAEA #B3B3B3; + border-style: solid; + border-width: 1px; + color: #000000; + font-size: 11px; + margin: 0; + padding: 7px 7px 6px; } input.disabled { background-color: #F5F5F5 !important; } - #content div.box div.form div.fields div.field div.input input.small { -width:30%; + width: 30%; } #content div.box div.form div.fields div.field div.input input.medium { -width:55%; + width: 55%; } #content div.box div.form div.fields div.field div.input input.large { -width:85%; + width: 85%; } #content div.box div.form div.fields div.field div.input input.date { -width:177px; + width: 177px; } #content div.box div.form div.fields div.field div.input input.button { -background:#D4D0C8; -border-top:1px solid #FFF; -border-left:1px solid #FFF; -border-right:1px solid #404040; -border-bottom:1px solid #404040; -color:#000; -margin:0; -padding:4px 8px; + background: #D4D0C8; + border-top: 1px solid #FFF; + border-left: 1px solid #FFF; + border-right: 1px solid #404040; + border-bottom: 1px solid #404040; + color: #000; + margin: 0; + padding: 4px 8px; } #content div.box div.form div.fields div.field div.textarea { -border-top:1px solid #b3b3b3; -border-left:1px solid #b3b3b3; -border-right:1px solid #eaeaea; -border-bottom:1px solid #eaeaea; -margin:0 0 0 200px; -padding:10px; + border-top: 1px solid #b3b3b3; + border-left: 1px solid #b3b3b3; + border-right: 1px solid #eaeaea; + border-bottom: 1px solid #eaeaea; + margin: 0 0 0 200px; + padding: 10px; } #content div.box div.form div.fields div.field div.textarea-editor { -border:1px solid #ddd; -padding:0; + border: 1px solid #ddd; + padding: 0; } #content div.box div.form div.fields div.field div.textarea textarea { -width:100%; -height:220px; -overflow:hidden; -background:#FFF; -color:#000; -font-family:Lucida Grande, Verdana, Lucida Sans Regular, Lucida Sans Unicode, Arial, sans-serif; -font-size:11px; -outline:none; -border-width:0; -margin:0; -padding:0; -} - -#content div.box-left div.form div.fields div.field div.textarea textarea,#content div.box-right div.form div.fields div.field div.textarea textarea { -width:100%; -height:100px; + width: 100%; + height: 220px; + overflow: hidden; + background: #FFF; + color: #000; + font-size: 11px; + outline: none; + border-width: 0; + margin: 0; + padding: 0; +} + +#content div.box-left div.form div.fields div.field div.textarea textarea,#content div.box-right div.form div.fields div.field div.textarea textarea + { + width: 100%; + height: 100px; } #content div.box div.form div.fields div.field div.textarea table { -width:100%; -border:none; -margin:0; -padding:0; + width: 100%; + border: none; + margin: 0; + padding: 0; } #content div.box div.form div.fields div.field div.textarea table td { -background:#DDD; -border:none; -padding:0; -} - -#content div.box div.form div.fields div.field div.textarea table td table { -width:auto; -border:none; -margin:0; -padding:0; -} - -#content div.box div.form div.fields div.field div.textarea table td table td { -font-family:Lucida Grande, Verdana, Lucida Sans Regular, Lucida Sans Unicode, Arial, sans-serif; -font-size:11px; -padding:5px 5px 5px 0; -} - -#content div.box div.form div.fields div.field input[type=text]:focus,#content div.box div.form div.fields div.field input[type=password]:focus,#content div.box div.form div.fields div.field input[type=file]:focus,#content div.box div.form div.fields div.field textarea:focus,#content div.box div.form div.fields div.field select:focus { -background:#f6f6f6; -border-color:#666; + background: #DDD; + border: none; + padding: 0; +} + +#content div.box div.form div.fields div.field div.textarea table td table + { + width: auto; + border: none; + margin: 0; + padding: 0; +} + +#content div.box div.form div.fields div.field div.textarea table td table td + { + font-size: 11px; + padding: 5px 5px 5px 0; +} + +#content div.box div.form div.fields div.field input[type=text]:focus,#content div.box div.form div.fields div.field input[type=password]:focus,#content div.box div.form div.fields div.field input[type=file]:focus,#content div.box div.form div.fields div.field textarea:focus,#content div.box div.form div.fields div.field select:focus + { + background: #f6f6f6; + border-color: #666; } div.form div.fields div.field div.button { -margin:0; -padding:0 0 0 8px; -} - + margin: 0; + padding: 0 0 0 8px; +} +#content div.box table.noborder { + border: 1px solid transparent; +} #content div.box table { -width:100%; -border-collapse:collapse; -margin:0; -padding:0; -border: 1px solid #eee; + width: 100%; + border-collapse: separate; + margin: 0; + padding: 0; + border: 1px solid #eee; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; } #content div.box table th { -background:#eee; -border-bottom:1px solid #ddd; -padding:5px 0px 5px 5px; + background: #eee; + border-bottom: 1px solid #ddd; + padding: 5px 0px 5px 5px; } #content div.box table th.left { -text-align:left; + text-align: left; } #content div.box table th.right { -text-align:right; + text-align: right; } #content div.box table th.center { -text-align:center; + text-align: center; } #content div.box table th.selected { -vertical-align:middle; -padding:0; + vertical-align: middle; + padding: 0; } #content div.box table td { -background:#fff; -border-bottom:1px solid #cdcdcd; -vertical-align:middle; -padding:5px; + background: #fff; + border-bottom: 1px solid #cdcdcd; + vertical-align: middle; + padding: 5px; } #content div.box table tr.selected td { -background:#FFC; + background: #FFC; } #content div.box table td.selected { -width:3%; -text-align:center; -vertical-align:middle; -padding:0; + width: 3%; + text-align: center; + vertical-align: middle; + padding: 0; } #content div.box table td.action { -width:45%; -text-align:left; + width: 45%; + text-align: left; } #content div.box table td.date { -width:33%; -text-align:center; + width: 33%; + text-align: center; } #content div.box div.action { -float:right; -background:#FFF; -text-align:right; -margin:10px 0 0; -padding:0; + float: right; + background: #FFF; + text-align: right; + margin: 10px 0 0; + padding: 0; } #content div.box div.action select { -font-family:Lucida Grande, Verdana, Lucida Sans Regular, Lucida Sans Unicode, Arial, sans-serif; -font-size:11px; -margin:0; + font-size: 11px; + margin: 0; } #content div.box div.action .ui-selectmenu { -margin:0; -padding:0; + margin: 0; + padding: 0; } #content div.box div.pagination { -height:1%; -clear:both; -overflow:hidden; -margin:10px 0 0; -padding:0; + height: 1%; + clear: both; + overflow: hidden; + margin: 10px 0 0; + padding: 0; } #content div.box div.pagination ul.pager { -float:right; -text-align:right; -margin:0; -padding:0; + float: right; + text-align: right; + margin: 0; + padding: 0; } #content div.box div.pagination ul.pager li { -height:1%; -float:left; -list-style:none; -background:#ebebeb url("../images/pager.png") repeat-x; -border-top:1px solid #dedede; -border-left:1px solid #cfcfcf; -border-right:1px solid #c4c4c4; -border-bottom:1px solid #c4c4c4; -color:#4A4A4A; -font-weight:700; -margin:0 0 0 4px; -padding:0; + height: 1%; + float: left; + list-style: none; + background: #ebebeb url("../images/pager.png") repeat-x; + border-top: 1px solid #dedede; + border-left: 1px solid #cfcfcf; + border-right: 1px solid #c4c4c4; + border-bottom: 1px solid #c4c4c4; + color: #4A4A4A; + font-weight: 700; + margin: 0 0 0 4px; + padding: 0; } #content div.box div.pagination ul.pager li.separator { -padding:6px; + padding: 6px; } #content div.box div.pagination ul.pager li.current { -background:#b4b4b4 url("../images/pager_selected.png") repeat-x; -border-top:1px solid #ccc; -border-left:1px solid #bebebe; -border-right:1px solid #b1b1b1; -border-bottom:1px solid #afafaf; -color:#515151; -padding:6px; + background: #b4b4b4 url("../images/pager_selected.png") repeat-x; + border-top: 1px solid #ccc; + border-left: 1px solid #bebebe; + border-right: 1px solid #b1b1b1; + border-bottom: 1px solid #afafaf; + color: #515151; + padding: 6px; } #content div.box div.pagination ul.pager li a { -height:1%; -display:block; -float:left; -color:#515151; -text-decoration:none; -margin:0; -padding:6px; -} - -#content div.box div.pagination ul.pager li a:hover,#content div.box div.pagination ul.pager li a:active { -background:#b4b4b4 url("../images/pager_selected.png") repeat-x; -border-top:1px solid #ccc; -border-left:1px solid #bebebe; -border-right:1px solid #b1b1b1; -border-bottom:1px solid #afafaf; -margin:-1px; + height: 1%; + display: block; + float: left; + color: #515151; + text-decoration: none; + margin: 0; + padding: 6px; +} + +#content div.box div.pagination ul.pager li a:hover,#content div.box div.pagination ul.pager li a:active + { + background: #b4b4b4 url("../images/pager_selected.png") repeat-x; + border-top: 1px solid #ccc; + border-left: 1px solid #bebebe; + border-right: 1px solid #b1b1b1; + border-bottom: 1px solid #afafaf; + margin: -1px; } #content div.box div.pagination-wh { -height:1%; -clear:both; -overflow:hidden; -text-align:right; -margin:10px 0 0; -padding:0; + height: 1%; + clear: both; + overflow: hidden; + text-align: right; + margin: 10px 0 0; + padding: 0; } #content div.box div.pagination-right { -float:right; -} - -#content div.box div.pagination-wh a,#content div.box div.pagination-wh span.pager_dotdot { -height:1%; -float:left; -background:#ebebeb url("../images/pager.png") repeat-x; -border-top:1px solid #dedede; -border-left:1px solid #cfcfcf; -border-right:1px solid #c4c4c4; -border-bottom:1px solid #c4c4c4; -color:#4A4A4A; -font-weight:700; -margin:0 0 0 4px; -padding:6px; + float: right; +} + +#content div.box div.pagination-wh a,#content div.box div.pagination-wh span.pager_dotdot + { + height: 1%; + float: left; + background: #ebebeb url("../images/pager.png") repeat-x; + border-top: 1px solid #dedede; + border-left: 1px solid #cfcfcf; + border-right: 1px solid #c4c4c4; + border-bottom: 1px solid #c4c4c4; + color: #4A4A4A; + font-weight: 700; + margin: 0 0 0 4px; + padding: 6px; } #content div.box div.pagination-wh span.pager_curpage { -height:1%; -float:left; -background:#b4b4b4 url("../images/pager_selected.png") repeat-x; -border-top:1px solid #ccc; -border-left:1px solid #bebebe; -border-right:1px solid #b1b1b1; -border-bottom:1px solid #afafaf; -color:#515151; -font-weight:700; -margin:0 0 0 4px; -padding:6px; -} - -#content div.box div.pagination-wh a:hover,#content div.box div.pagination-wh a:active { -background:#b4b4b4 url("../images/pager_selected.png") repeat-x; -border-top:1px solid #ccc; -border-left:1px solid #bebebe; -border-right:1px solid #b1b1b1; -border-bottom:1px solid #afafaf; -text-decoration:none; + height: 1%; + float: left; + background: #b4b4b4 url("../images/pager_selected.png") repeat-x; + border-top: 1px solid #ccc; + border-left: 1px solid #bebebe; + border-right: 1px solid #b1b1b1; + border-bottom: 1px solid #afafaf; + color: #515151; + font-weight: 700; + margin: 0 0 0 4px; + padding: 6px; +} + +#content div.box div.pagination-wh a:hover,#content div.box div.pagination-wh a:active + { + background: #b4b4b4 url("../images/pager_selected.png") repeat-x; + border-top: 1px solid #ccc; + border-left: 1px solid #bebebe; + border-right: 1px solid #b1b1b1; + border-bottom: 1px solid #afafaf; + text-decoration: none; } #content div.box div.traffic div.legend { -clear:both; -overflow:hidden; -border-bottom:1px solid #ddd; -margin:0 0 10px; -padding:0 0 10px; + clear: both; + overflow: hidden; + border-bottom: 1px solid #ddd; + margin: 0 0 10px; + padding: 0 0 10px; } #content div.box div.traffic div.legend h6 { -float:left; -border:none; -margin:0; -padding:0; + float: left; + border: none; + margin: 0; + padding: 0; } #content div.box div.traffic div.legend li { -list-style:none; -float:left; -font-size:11px; -margin:0; -padding:0 8px 0 4px; + list-style: none; + float: left; + font-size: 11px; + margin: 0; + padding: 0 8px 0 4px; } #content div.box div.traffic div.legend li.visits { -border-left:12px solid #edc240; + border-left: 12px solid #edc240; } #content div.box div.traffic div.legend li.pageviews { -border-left:12px solid #afd8f8; + border-left: 12px solid #afd8f8; } #content div.box div.traffic table { -width:auto; + width: auto; } #content div.box div.traffic table td { -background:transparent; -border:none; -padding:2px 3px 3px; + background: transparent; + border: none; + padding: 2px 3px 3px; } #content div.box div.traffic table td.legendLabel { -padding:0 3px 2px; -} - -#summary{ - -} - -#summary .desc{ -white-space: pre; -width: 100%; -} - -#summary .repo_name{ -font-size: 1.6em; -font-weight: bold; -vertical-align: baseline; -clear:right -} - + padding: 0 3px 2px; +} + +#summary { + +} + +#summary .desc { + white-space: pre; + width: 100%; +} + +#summary .repo_name { + font-size: 1.6em; + font-weight: bold; + vertical-align: baseline; + clear: right +} #footer { -clear:both; -overflow:hidden; -text-align:right; -margin:0; -padding:0 10px 4px; -margin:-10px 0 0; + clear: both; + overflow: hidden; + text-align: right; + margin: 0; + padding: 0 10px 4px; + margin: -10px 0 0; } #footer div#footer-inner { -background:url("../images/header_inner.png") repeat-x scroll 0 0 #003367; -box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); --webkit-border-radius: 4px 4px 4px 4px; --khtml-border-radius: 4px 4px 4px 4px; --moz-border-radius: 4px 4px 4px 4px; -border-radius: 4px 4px 4px 4px; + background-color: #eedc94; background-repeat : repeat-x; + background-image : -khtml-gradient( linear, left top, left bottom, + from( #fceec1), to( #eedc94)); background-image : -moz-linear-gradient( + top, #003b76, #00376e); background-image : -ms-linear-gradient( top, + #003b76, #00376e); background-image : -webkit-gradient( linear, left + top, left bottom, color-stop( 0%, #003b76), color-stop( 100%, #00376e)); + background-image : -webkit-linear-gradient( top, #003b76, #00376e)); + background-image : -o-linear-gradient( top, #003b76, #00376e)); + background-image : linear-gradient( top, #003b76, #00376e); filter : + progid : DXImageTransform.Microsoft.gradient ( startColorstr = + '#003b76', endColorstr = '#00376e', GradientType = 0); + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); + -webkit-border-radius: 4px 4px 4px 4px; + -khtml-border-radius: 4px 4px 4px 4px; + -moz-border-radius: 4px 4px 4px 4px; + border-radius: 4px 4px 4px 4px; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), + to(#eedc94) ); + background-image: -moz-linear-gradient(top, #003b76, #00376e); + background-image: -ms-linear-gradient(top, #003b76, #00376e); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) ); + background-image: -webkit-linear-gradient(top, #003b76, #00376e); + background-image: -o-linear-gradient(top, #003b76, #00376e); + background-image: linear-gradient(top, #003b76, #00376e); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', + endColorstr='#00376e', GradientType=0 ); } #footer div#footer-inner p { -padding:15px 25px 15px 0; -color:#FFF; -font-weight:700; -} + padding: 15px 25px 15px 0; + color: #FFF; + font-weight: 700; +} + #footer div#footer-inner .footer-link { -float:left; -padding-left:10px; -} -#footer div#footer-inner .footer-link a,#footer div#footer-inner .footer-link-right a { -color:#FFF; + float: left; + padding-left: 10px; +} + +#footer div#footer-inner .footer-link a,#footer div#footer-inner .footer-link-right a + { + color: #FFF; } #login div.title { -width:420px; -clear:both; -overflow:hidden; -position:relative; -background:#003367 url("../images/header_inner.png") repeat-x; -margin:0 auto; -padding:0; + width: 420px; + clear: both; + overflow: hidden; + position: relative; + background-color: #eedc94; background-repeat : repeat-x; + background-image : -khtml-gradient( linear, left top, left bottom, + from( #fceec1), to( #eedc94)); background-image : -moz-linear-gradient( + top, #003b76, #00376e); background-image : -ms-linear-gradient( top, + #003b76, #00376e); background-image : -webkit-gradient( linear, left + top, left bottom, color-stop( 0%, #003b76), color-stop( 100%, #00376e)); + background-image : -webkit-linear-gradient( top, #003b76, #00376e)); + background-image : -o-linear-gradient( top, #003b76, #00376e)); + background-image : linear-gradient( top, #003b76, #00376e); filter : + progid : DXImageTransform.Microsoft.gradient ( startColorstr = + '#003b76', endColorstr = '#00376e', GradientType = 0); + margin: 0 auto; + padding: 0; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), + to(#eedc94) ); + background-image: -moz-linear-gradient(top, #003b76, #00376e); + background-image: -ms-linear-gradient(top, #003b76, #00376e); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) ); + background-image: -webkit-linear-gradient(top, #003b76, #00376e); + background-image: -o-linear-gradient(top, #003b76, #00376e); + background-image: linear-gradient(top, #003b76, #00376e); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', + endColorstr='#00376e', GradientType=0 ); } #login div.inner { -width:380px; -background:#FFF url("../images/login.png") no-repeat top left; -border-top:none; -border-bottom:none; -margin:0 auto; -padding:20px; + width: 380px; + background: #FFF url("../images/login.png") no-repeat top left; + border-top: none; + border-bottom: none; + margin: 0 auto; + padding: 20px; } #login div.form div.fields div.field div.label { -width:173px; -float:left; -text-align:right; -margin:2px 10px 0 0; -padding:5px 0 0 5px; + width: 173px; + float: left; + text-align: right; + margin: 2px 10px 0 0; + padding: 5px 0 0 5px; } #login div.form div.fields div.field div.input input { -width:176px; -background:#FFF; -border-top:1px solid #b3b3b3; -border-left:1px solid #b3b3b3; -border-right:1px solid #eaeaea; -border-bottom:1px solid #eaeaea; -color:#000; -font-family:Lucida Grande, Verdana, Lucida Sans Regular, Lucida Sans Unicode, Arial, sans-serif; -font-size:11px; -margin:0; -padding:7px 7px 6px; + width: 176px; + background: #FFF; + border-top: 1px solid #b3b3b3; + border-left: 1px solid #b3b3b3; + border-right: 1px solid #eaeaea; + border-bottom: 1px solid #eaeaea; + color: #000; + font-size: 11px; + margin: 0; + padding: 7px 7px 6px; } #login div.form div.fields div.buttons { -clear:both; -overflow:hidden; -border-top:1px solid #DDD; -text-align:right; -margin:0; -padding:10px 0 0; + clear: both; + overflow: hidden; + border-top: 1px solid #DDD; + text-align: right; + margin: 0; + padding: 10px 0 0; } #login div.form div.links { -clear:both; -overflow:hidden; -margin:10px 0 0; -padding:0 0 2px; -} - + clear: both; + overflow: hidden; + margin: 10px 0 0; + padding: 0 0 2px; +} + +.user-menu{ + margin: 0px !important; + float: left; +} + +.user-menu .container{ + padding:0px 4px 0px 4px; + margin: 0px 0px 0px 0px; +} + +.user-menu .gravatar{ + margin: 0px 0px 0px 0px; + cursor: pointer; +} +.user-menu .gravatar.enabled{ + background-color: #FDF784 !important; +} +.user-menu .gravatar:hover{ + background-color: #FDF784 !important; +} #quick_login{ -top: 31px; -background-color: rgb(0, 51, 103); -z-index: 999; -height: 150px; -position: absolute; -margin-left: -16px; -width: 281px; --webkit-border-radius: 0px 0px 4px 4px; --khtml-border-radius: 0px 0px 4px 4px; --moz-border-radius: 0px 0px 4px 4px; -border-radius: 0px 0px 4px 4px; - -box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); -} - -#quick_login .password_forgoten{ -padding-right:10px; -padding-top:0px; -float:left; -} -#quick_login .password_forgoten a{ - font-size: 10px -} - -#quick_login .register{ -padding-right:10px; -padding-top:5px; -float:left; -} - -#quick_login .register a{ - font-size: 10px -} -#quick_login div.form div.fields{ -padding-top: 2px; -padding-left:10px; -} - -#quick_login div.form div.fields div.field{ - padding: 5px; -} - -#quick_login div.form div.fields div.field div.label label{ -color:#fff; -padding-bottom: 3px; + min-height: 80px; + margin: 37px 0 0 -251px; + padding: 4px; + position: absolute; + width: 278px; + + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), + to(#eedc94) ); + background-image: -moz-linear-gradient(top, #003b76, #00376e); + background-image: -ms-linear-gradient(top, #003b76, #00376e); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) ); + background-image: -webkit-linear-gradient(top, #003b76, #00376e); + background-image: -o-linear-gradient(top, #003b76, #00376e); + background-image: linear-gradient(top, #003b76, #00376e); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', + endColorstr='#00376e', GradientType=0 ); + + z-index: 999; + -webkit-border-radius: 0px 0px 4px 4px; + -khtml-border-radius: 0px 0px 4px 4px; + -moz-border-radius: 0px 0px 4px 4px; + border-radius: 0px 0px 4px 4px; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); +} +#quick_login h4{ + color: #fff; + padding: 5px 0px 5px 14px; +} + +#quick_login .password_forgoten { + padding-right: 10px; + padding-top: 0px; + text-align: left; +} + +#quick_login .password_forgoten a { + font-size: 10px; + color: #fff; +} + +#quick_login .register { + padding-right: 10px; + padding-top: 5px; + text-align: left; +} + +#quick_login .register a { + font-size: 10px; + color: #fff; +} + +#quick_login .submit { + margin: -20px 0 0 0px; + position: absolute; + right: 15px; +} + +#quick_login .links_left{ + float: left; +} +#quick_login .links_right{ + float: right; +} +#quick_login .full_name{ + color: #FFFFFF; + font-weight: bold; + padding: 3px; +} +#quick_login .big_gravatar{ + padding:4px 0px 0px 6px; +} +#quick_login .inbox{ + padding:4px 0px 0px 6px; + color: #FFFFFF; + font-weight: bold; +} +#quick_login .inbox a{ + color: #FFFFFF; +} +#quick_login .email,#quick_login .email a{ + color: #FFFFFF; + padding: 3px; + +} +#quick_login .links .logout{ + +} + +#quick_login div.form div.fields { + padding-top: 2px; + padding-left: 10px; +} + +#quick_login div.form div.fields div.field { + padding: 5px; +} + +#quick_login div.form div.fields div.field div.label label { + color: #fff; + padding-bottom: 3px; } #quick_login div.form div.fields div.field div.input input { -width:236px; -background:#FFF; -border-top:1px solid #b3b3b3; -border-left:1px solid #b3b3b3; -border-right:1px solid #eaeaea; -border-bottom:1px solid #eaeaea; -color:#000; -font-family:Lucida Grande, Verdana, Lucida Sans Regular, Lucida Sans Unicode, Arial, sans-serif; -font-size:11px; -margin:0; -padding:5px 7px 4px; + width: 236px; + background: #FFF; + border-top: 1px solid #b3b3b3; + border-left: 1px solid #b3b3b3; + border-right: 1px solid #eaeaea; + border-bottom: 1px solid #eaeaea; + color: #000; + font-size: 11px; + margin: 0; + padding: 5px 7px 4px; } #quick_login div.form div.fields div.buttons { -clear:both; -overflow:hidden; -text-align:right; -margin:0; -padding:10px 14px 0px 5px; + clear: both; + overflow: hidden; + text-align: right; + margin: 0; + padding: 5px 14px 0px 5px; } #quick_login div.form div.links { -clear:both; -overflow:hidden; -margin:10px 0 0; -padding:0 0 2px; + clear: both; + overflow: hidden; + margin: 10px 0 0; + padding: 0 0 2px; +} + +#quick_login ol.links{ + display: block; + font-weight: bold; + list-style: none outside none; + text-align: right; +} +#quick_login ol.links li{ + line-height: 27px; + margin: 0; + padding: 0; + color: #fff; + display: block; + float:none !important; +} + +#quick_login ol.links li a{ + color: #fff; + display: block; + padding: 2px; +} +#quick_login ol.links li a:HOVER{ + background-color: inherit !important; } #register div.title { -clear:both; -overflow:hidden; -position:relative; -background:#003367 url("../images/header_inner.png") repeat-x; -margin:0 auto; -padding:0; + clear: both; + overflow: hidden; + position: relative; + background-color: #eedc94; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), + to(#eedc94) ); + background-image: -moz-linear-gradient(top, #003b76, #00376e); + background-image: -ms-linear-gradient(top, #003b76, #00376e); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) ); + background-image: -webkit-linear-gradient(top, #003b76, #00376e); + background-image: -o-linear-gradient(top, #003b76, #00376e); + background-image: linear-gradient(top, #003b76, #00376e); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', + endColorstr='#00376e', GradientType=0 ); + margin: 0 auto; + padding: 0; } #register div.inner { -background:#FFF; -border-top:none; -border-bottom:none; -margin:0 auto; -padding:20px; + background: #FFF; + border-top: none; + border-bottom: none; + margin: 0 auto; + padding: 20px; } #register div.form div.fields div.field div.label { -width:135px; -float:left; -text-align:right; -margin:2px 10px 0 0; -padding:5px 0 0 5px; + width: 135px; + float: left; + text-align: right; + margin: 2px 10px 0 0; + padding: 5px 0 0 5px; } #register div.form div.fields div.field div.input input { -width:300px; -background:#FFF; -border-top:1px solid #b3b3b3; -border-left:1px solid #b3b3b3; -border-right:1px solid #eaeaea; -border-bottom:1px solid #eaeaea; -color:#000; -font-family:Lucida Grande, Verdana, Lucida Sans Regular, Lucida Sans Unicode, Arial, sans-serif; -font-size:11px; -margin:0; -padding:7px 7px 6px; + width: 300px; + background: #FFF; + border-top: 1px solid #b3b3b3; + border-left: 1px solid #b3b3b3; + border-right: 1px solid #eaeaea; + border-bottom: 1px solid #eaeaea; + color: #000; + font-size: 11px; + margin: 0; + padding: 7px 7px 6px; } #register div.form div.fields div.buttons { -clear:both; -overflow:hidden; -border-top:1px solid #DDD; -text-align:left; -margin:0; -padding:10px 0 0 150px; -} - + clear: both; + overflow: hidden; + border-top: 1px solid #DDD; + text-align: left; + margin: 0; + padding: 10px 0 0 150px; +} #register div.form div.activation_msg { -padding-top:4px; -padding-bottom:4px; -} - -#journal .journal_day{ -font-size:20px; -padding:10px 0px; -border-bottom:2px solid #DDD; -margin-left:10px; -margin-right:10px; -} - -#journal .journal_container{ -padding:5px; -clear:both; -margin:0px 5px 0px 10px; -} - -#journal .journal_action_container{ -padding-left:38px; -} - -#journal .journal_user{ -color: #747474; -font-size: 14px; -font-weight: bold; -height: 30px; -} -#journal .journal_icon{ -clear: both; -float: left; -padding-right: 4px; -padding-top: 3px; -} -#journal .journal_action{ -padding-top:4px; -min-height:2px; -float:left -} -#journal .journal_action_params{ -clear: left; -padding-left: 22px; -} -#journal .journal_repo{ -float: left; -margin-left: 6px; -padding-top: 3px; -} -#journal .date{ -clear: both; -color: #777777; -font-size: 11px; -padding-left: 22px; -} -#journal .journal_repo .journal_repo_name{ -font-weight: bold; -font-size: 1.1em; -} -#journal .compare_view{ -padding: 5px 0px 5px 0px; -width: 95px; -} -.journal_highlight{ -font-weight: bold; -padding: 0 2px; -vertical-align: bottom; -} + padding-top: 4px; + padding-bottom: 4px; +} + +#journal .journal_day { + font-size: 20px; + padding: 10px 0px; + border-bottom: 2px solid #DDD; + margin-left: 10px; + margin-right: 10px; +} + +#journal .journal_container { + padding: 5px; + clear: both; + margin: 0px 5px 0px 10px; +} + +#journal .journal_action_container { + padding-left: 38px; +} + +#journal .journal_user { + color: #747474; + font-size: 14px; + font-weight: bold; + height: 30px; +} + +#journal .journal_icon { + clear: both; + float: left; + padding-right: 4px; + padding-top: 3px; +} + +#journal .journal_action { + padding-top: 4px; + min-height: 2px; + float: left +} + +#journal .journal_action_params { + clear: left; + padding-left: 22px; +} + +#journal .journal_repo { + float: left; + margin-left: 6px; + padding-top: 3px; +} + +#journal .date { + clear: both; + color: #777777; + font-size: 11px; + padding-left: 22px; +} + +#journal .journal_repo .journal_repo_name { + font-weight: bold; + font-size: 1.1em; +} + +#journal .compare_view { + padding: 5px 0px 5px 0px; + width: 95px; +} + +.journal_highlight { + font-weight: bold; + padding: 0 2px; + vertical-align: bottom; +} + .trending_language_tbl,.trending_language_tbl td { -border:0 !important; -margin:0 !important; -padding:0 !important; + border: 0 !important; + margin: 0 !important; + padding: 0 !important; +} + +.trending_language_tbl,.trending_language_tbl tr { + border-spacing: 1px; } .trending_language { -background-color:#003367; -color:#FFF; -display:block; -min-width:20px; -text-decoration:none; -height:12px; -margin-bottom:4px; -margin-left:5px; -white-space:pre; -padding:3px; + background-color: #003367; + color: #FFF; + display: block; + min-width: 20px; + text-decoration: none; + height: 12px; + margin-bottom: 0px; + margin-left: 5px; + white-space: pre; + padding: 3px; } h3.files_location { -font-size:1.8em; -font-weight:700; -border-bottom:none !important; -margin:10px 0 !important; + font-size: 1.8em; + font-weight: 700; + border-bottom: none !important; + margin: 10px 0 !important; } #files_data dl dt { -float:left; -width:115px; -margin:0 !important; -padding:5px; + float: left; + width: 60px; + margin: 0 !important; + padding: 5px; } #files_data dl dd { -margin:0 !important; -padding:5px !important; + margin: 0 !important; + padding: 5px !important; +} + +.tablerow0 { + background-color: #F8F8F8; +} + +.tablerow1 { + background-color: #FFFFFF; +} + +.changeset_id { + font-family: monospace; + color: #666666; +} + +.changeset_hash { + color: #000000; } #changeset_content { -border:1px solid #CCC; -padding:5px; -} -#changeset_compare_view_content{ -border:1px solid #CCC; -padding:5px; + border-left: 1px solid #CCC; + border-right: 1px solid #CCC; + border-bottom: 1px solid #CCC; + padding: 5px; +} + +#changeset_compare_view_content { + border: 1px solid #CCC; + padding: 5px; } #changeset_content .container { -min-height:120px; -font-size:1.2em; -overflow:hidden; -} - -#changeset_compare_view_content .compare_view_commits{ -width: auto !important; -} - -#changeset_compare_view_content .compare_view_commits td{ -padding:0px 0px 0px 12px !important; + min-height: 100px; + font-size: 1.2em; + overflow: hidden; +} + +#changeset_compare_view_content .compare_view_commits { + width: auto !important; +} + +#changeset_compare_view_content .compare_view_commits td { + padding: 0px 0px 0px 12px !important; } #changeset_content .container .right { -float:right; -width:25%; -text-align:right; + float: right; + width: 20%; + text-align: right; } #changeset_content .container .left .message { -font-style:italic; -color:#556CB5; -white-space:pre-wrap; -} - -.cs_files .cur_cs{ -margin:10px 2px; -font-weight: bold; -} - -.cs_files .node{ -float: left; -} -.cs_files .changes{ -float: right; -} -.cs_files .changes .added{ -background-color: #BBFFBB; -float: left; -text-align: center; -font-size: 90%; -} -.cs_files .changes .deleted{ -background-color: #FF8888; -float: left; -text-align: center; -font-size: 90%; -} + white-space: pre-wrap; +} +#changeset_content .container .left .message a:hover { + text-decoration: none; +} +.cs_files .cur_cs { + margin: 10px 2px; + font-weight: bold; +} + +.cs_files .node { + float: left; +} + +.cs_files .changes { + float: right; + color:#003367; + +} + +.cs_files .changes .added { + background-color: #BBFFBB; + float: left; + text-align: center; + font-size: 9px; + padding: 2px 0px 2px 0px; +} + +.cs_files .changes .deleted { + background-color: #FF8888; + float: left; + text-align: center; + font-size: 9px; + padding: 2px 0px 2px 0px; +} + .cs_files .cs_added { -background:url("../images/icons/page_white_add.png") no-repeat scroll 3px; -height:16px; -padding-left:20px; -margin-top:7px; -text-align:left; + background: url("../images/icons/page_white_add.png") no-repeat scroll + 3px; + height: 16px; + padding-left: 20px; + margin-top: 7px; + text-align: left; } .cs_files .cs_changed { -background:url("../images/icons/page_white_edit.png") no-repeat scroll 3px; -height:16px; -padding-left:20px; -margin-top:7px; -text-align:left; + background: url("../images/icons/page_white_edit.png") no-repeat scroll + 3px; + height: 16px; + padding-left: 20px; + margin-top: 7px; + text-align: left; } .cs_files .cs_removed { -background:url("../images/icons/page_white_delete.png") no-repeat scroll 3px; -height:16px; -padding-left:20px; -margin-top:7px; -text-align:left; + background: url("../images/icons/page_white_delete.png") no-repeat + scroll 3px; + height: 16px; + padding-left: 20px; + margin-top: 7px; + text-align: left; } #graph { -overflow:hidden; + overflow: hidden; } #graph_nodes { -float: left; -margin-right: -6px; -margin-top: -4px; + float: left; + margin-right: -6px; + margin-top: 0px; } #graph_content { -width:800px; -float:left; - + width: 80%; + float: left; } #graph_content .container_header { -border:1px solid #CCC; -padding:10px; -} -#graph_content #rev_range_container{ -padding:10px 0px; -} + border-bottom: 1px solid #DDD; + padding: 10px; + height: 25px; +} + +#graph_content #rev_range_container { + padding: 7px 20px; + float: left; +} + #graph_content .container { -border-bottom:1px solid #CCC; -border-left:1px solid #CCC; -border-right:1px solid #CCC; -min-height:70px; -overflow:hidden; -font-size:1.2em; + border-bottom: 1px solid #DDD; + height: 56px; + overflow: hidden; } #graph_content .container .right { -float:right; -width:28%; -text-align:right; -padding-bottom:5px; -} + float: right; + width: 23%; + text-align: right; +} + +#graph_content .container .left { + float: left; + width: 25%; + padding-left: 5px; +} + +#graph_content .container .mid { + float: left; + width: 49%; +} + #graph_content .container .left .date { -font-weight:700; -padding-bottom:5px; -} -#graph_content .container .left .date span{ -vertical-align: text-top; -} - -#graph_content .container .left .author{ - height: 22px; -} -#graph_content .container .left .author .user{ -color: #444444; -float: left; -font-size: 12px; -margin-left: -4px; -margin-top: 4px; -} - -#graph_content .container .left .message { -font-size:100%; -padding-top:3px; -white-space:pre-wrap; -} - -.right div { -clear:both; -} - -.right .changes .changed_total{ -border:1px solid #DDD; -display:block; -float:right; -text-align:center; -min-width:45px; -cursor: pointer; -background:#FD8; -font-weight: bold; -} + color: #666; + padding-left: 22px; + font-size: 10px; +} + +#graph_content .container .left .author { + height: 22px; +} + +#graph_content .container .left .author .user { + color: #444444; + float: left; + margin-left: -4px; + margin-top: 4px; +} + +#graph_content .container .mid .message { + white-space: pre-wrap; +} + +#graph_content .container .mid .message a:hover{ + text-decoration: none; +} +#content #graph_content .message .revision-link, +#changeset_content .container .message .revision-link + { + color:#3F6F9F; + font-weight: bold !important; +} + +#content #graph_content .message .issue-tracker-link, +#changeset_content .container .message .issue-tracker-link{ + color:#3F6F9F; + font-weight: bold !important; +} + +.right .comments-container{ + padding-right: 5px; + margin-top:1px; + float:right; + height:14px; +} + +.right .comments-cnt{ + float: left; + color: rgb(136, 136, 136); + padding-right: 2px; +} + +.right .changes{ + clear: both; +} + +.right .changes .changed_total { + display: block; + float: right; + text-align: center; + min-width: 45px; + cursor: pointer; + color: #444444; + background: #FEA; + -webkit-border-radius: 0px 0px 0px 6px; + -moz-border-radius: 0px 0px 0px 6px; + border-radius: 0px 0px 0px 6px; + padding: 1px; +} + .right .changes .added,.changed,.removed { -border:1px solid #DDD; -display:block; -float:right; -text-align:center; -min-width:15px; -cursor: help; -} -.right .changes .large { -border:1px solid #DDD; -display:block; -float:right; -text-align:center; -min-width:45px; -cursor: help; -background: #54A9F7; + display: block; + padding: 1px; + color: #444444; + float: right; + text-align: center; + min-width: 15px; } .right .changes .added { -background:#BFB; + background: #CFC; } .right .changes .changed { -background:#FD8; + background: #FEA; } .right .changes .removed { -background:#F88; + background: #FAA; } .right .merge { -vertical-align:top; -font-size:0.75em; -font-weight:700; + padding: 1px 3px 1px 3px; + background-color: #fca062; + font-size: 10px; + font-weight: bold; + color: #ffffff; + text-transform: uppercase; + white-space: nowrap; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + margin-right: 2px; } .right .parent { -font-size:90%; -font-family:monospace; -} - -.right .logtags .branchtag { -background:#FFF url("../images/icons/arrow_branch.png") no-repeat right 6px; -display:block; -font-size:0.8em; -padding:11px 16px 0 0; -} - -.right .logtags .tagtag { -background:#FFF url("../images/icons/tag_blue.png") no-repeat right 6px; -display:block; -font-size:0.8em; -padding:11px 16px 0 0; -} - + color: #666666; + clear:both; +} +.right .logtags{ + padding: 2px 2px 2px 2px; +} +.right .logtags .branchtag,.logtags .branchtag { + padding: 1px 3px 1px 3px; + background-color: #bfbfbf; + font-size: 10px; + font-weight: bold; + color: #ffffff; + text-transform: uppercase; + white-space: nowrap; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.right .logtags .branchtag a:hover,.logtags .branchtag a{ + color: #ffffff; +} +.right .logtags .branchtag a:hover,.logtags .branchtag a:hover{ + text-decoration: none; + color: #ffffff; +} +.right .logtags .tagtag,.logtags .tagtag { + padding: 1px 3px 1px 3px; + background-color: #62cffc; + font-size: 10px; + font-weight: bold; + color: #ffffff; + text-transform: uppercase; + white-space: nowrap; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.right .logtags .tagtag a:hover,.logtags .tagtag a{ + color: #ffffff; +} +.right .logtags .tagtag a:hover,.logtags .tagtag a:hover{ + text-decoration: none; + color: #ffffff; +} +.right .logbooks .bookbook,.logbooks .bookbook { + padding: 1px 3px 2px; + background-color: #46A546; + font-size: 9.75px; + font-weight: bold; + color: #ffffff; + text-transform: uppercase; + white-space: nowrap; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.right .logbooks .bookbook,.logbooks .bookbook a{ + color: #ffffff; +} +.right .logbooks .bookbook,.logbooks .bookbook a:hover{ + text-decoration: none; + color: #ffffff; +} div.browserblock { -overflow:hidden; -border:1px solid #ccc; -background:#f8f8f8; -font-size:100%; -line-height:125%; -padding:0; + overflow: hidden; + border: 1px solid #ccc; + background: #f8f8f8; + font-size: 100%; + line-height: 125%; + padding: 0; + -webkit-border-radius: 6px 6px 0px 0px; + -moz-border-radius: 6px 6px 0px 0px; + border-radius: 6px 6px 0px 0px; } div.browserblock .browser-header { -background:#FFF; -padding:10px 0px 15px 0px; -width: 100%; -} + background: #FFF; + padding: 10px 0px 15px 0px; + width: 100%; +} + div.browserblock .browser-nav { -float:left + float: left } div.browserblock .browser-branch { -float:left; + float: left; } div.browserblock .browser-branch label { -color:#4A4A4A; -vertical-align:text-top; + color: #4A4A4A; + vertical-align: text-top; } div.browserblock .browser-header span { -margin-left:5px; -font-weight:700; -} - -div.browserblock .browser-search{ - clear:both; - padding:8px 8px 0px 5px; + margin-left: 5px; + font-weight: 700; +} + +div.browserblock .browser-search { + clear: both; + padding: 8px 8px 0px 5px; height: 20px; } + div.browserblock #node_filter_box { -} - -div.browserblock .search_activate{ - float: left -} - -div.browserblock .add_node{ - float: left; - padding-left: 5px; -} - -div.browserblock .search_activate a:hover,div.browserblock .add_node a:hover{ - text-decoration: none !important; + +} + +div.browserblock .search_activate { + float: left +} + +div.browserblock .add_node { + float: left; + padding-left: 5px; +} + +div.browserblock .search_activate a:hover,div.browserblock .add_node a:hover + { + text-decoration: none !important; } div.browserblock .browser-body { -background:#EEE; -border-top:1px solid #CCC; + background: #EEE; + border-top: 1px solid #CCC; } table.code-browser { -border-collapse:collapse; -width:100%; + border-collapse: collapse; + width: 100%; } table.code-browser tr { -margin:3px; + margin: 3px; } table.code-browser thead th { -background-color:#EEE; -height:20px; -font-size:1.1em; -font-weight:700; -text-align:left; -padding-left:10px; + background-color: #EEE; + height: 20px; + font-size: 1.1em; + font-weight: 700; + text-align: left; + padding-left: 10px; } table.code-browser tbody td { -padding-left:10px; -height:20px; + padding-left: 10px; + height: 20px; } table.code-browser .browser-file { -background:url("../images/icons/document_16.png") no-repeat scroll 3px; -height:16px; -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; -} - + background: url("../images/icons/document_16.png") no-repeat scroll 3px; + height: 16px; + padding-left: 20px; + text-align: left; +} +.diffblock .changeset_header { + height: 16px; +} +.diffblock .changeset_file { + background: url("../images/icons/file.png") no-repeat scroll 3px; + text-align: left; + float: left; + padding: 2px 0px 2px 22px; +} +.diffblock .diff-menu-wrapper{ + float: left; +} + +.diffblock .diff-menu{ + position: absolute; + background: none repeat scroll 0 0 #FFFFFF; + border-color: #003367 #666666 #666666; + border-right: 1px solid #666666; + border-style: solid solid solid; + border-width: 1px; + box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2); + margin-top:5px; + margin-left:1px; + +} +.diffblock .diff-actions { + padding: 2px 0px 0px 2px; + float: left; +} +.diffblock .diff-menu ul li { + padding: 0px 0px 0px 0px !important; +} +.diffblock .diff-menu ul li a{ + display: block; + padding: 3px 8px 3px 8px !important; +} +.diffblock .diff-menu ul li a:hover{ + text-decoration: none; + background-color: #EEEEEE; +} table.code-browser .browser-dir { -background:url("../images/icons/folder_16.png") no-repeat scroll 3px; -height:16px; -padding-left:20px; -text-align:left; + background: url("../images/icons/folder_16.png") no-repeat scroll 3px; + height: 16px; + padding-left: 20px; + text-align: left; } .box .search { + clear: both; + overflow: hidden; + margin: 0; + padding: 0 20px 10px; +} + +.box .search div.search_path { + background: none repeat scroll 0 0 #EEE; + border: 1px solid #CCC; + color: blue; + margin-bottom: 10px; + padding: 10px 0; +} + +.box .search div.search_path div.link { + font-weight: 700; + margin-left: 25px; +} + +.box .search div.search_path div.link a { + color: #003367; + cursor: pointer; + text-decoration: none; +} + +#path_unlock { + color: red; + font-size: 1.2em; + padding-left: 4px; +} + +.info_box span { + margin-left: 3px; + margin-right: 3px; +} + +.info_box .rev { + color: #003367; + font-size: 1.6em; + font-weight: bold; + vertical-align: sub; +} + +.info_box input#at_rev,.info_box input#size { + background: #FFF; + border-top: 1px solid #b3b3b3; + border-left: 1px solid #b3b3b3; + border-right: 1px solid #eaeaea; + border-bottom: 1px solid #eaeaea; + color: #000; + font-size: 12px; + margin: 0; + padding: 1px 5px 1px; +} + +.info_box input#view { + text-align: center; + padding: 4px 3px 2px 2px; +} + +.yui-overlay,.yui-panel-container { + visibility: hidden; + position: absolute; + z-index: 2; +} + +.yui-tt { + visibility: hidden; + position: absolute; + color: #666; + background-color: #FFF; + border: 2px solid #003367; + font: 100% sans-serif; + width: auto; + opacity: 1px; + padding: 8px; + white-space: pre-wrap; + -webkit-border-radius: 8px 8px 8px 8px; + -khtml-border-radius: 8px 8px 8px 8px; + -moz-border-radius: 8px 8px 8px 8px; + border-radius: 8px 8px 8px 8px; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); +} + +.ac { + vertical-align: top; +} + +.ac .yui-ac { + position: relative; + font-size: 100%; +} + +.ac .perm_ac { + width: 15em; +} + +.ac .yui-ac-input { + width: 100%; +} + +.ac .yui-ac-container { + position: absolute; + top: 1.6em; + width: 100%; +} + +.ac .yui-ac-content { + position: absolute; + width: 100%; + border: 1px solid gray; + background: #fff; + overflow: hidden; + z-index: 9050; +} + +.ac .yui-ac-shadow { + position: absolute; + width: 100%; + background: #000; + -moz-opacity: 0.1px; + opacity: .10; + filter: alpha(opacity = 10); + z-index: 9049; + margin: .3em; +} + +.ac .yui-ac-content ul { + width: 100%; + margin: 0; + padding: 0; +} + +.ac .yui-ac-content li { + cursor: default; + white-space: nowrap; + margin: 0; + padding: 2px 5px; +} + +.ac .yui-ac-content li.yui-ac-prehighlight { + background: #B3D4FF; +} + +.ac .yui-ac-content li.yui-ac-highlight { + background: #556CB5; + 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; +} + +.add_icon { + background: url("../images/icons/add.png") no-repeat scroll 3px; + padding-left: 20px; + padding-top: 0px; + text-align: left; +} + +.edit_icon { + background: url("../images/icons/folder_edit.png") no-repeat scroll 3px; + padding-left: 20px; + padding-top: 0px; + text-align: left; +} + +.delete_icon { + background: url("../images/icons/delete.png") no-repeat scroll 3px; + padding-left: 20px; + padding-top: 0px; + text-align: left; +} + +.refresh_icon { + background: url("../images/icons/arrow_refresh.png") no-repeat scroll + 3px; + padding-left: 20px; + padding-top: 0px; + text-align: left; +} + +.pull_icon { + background: url("../images/icons/connect.png") no-repeat scroll 3px; + padding-left: 20px; + padding-top: 0px; + text-align: left; +} + +.rss_icon { + background: url("../images/icons/rss_16.png") no-repeat scroll 3px; + padding-left: 20px; + padding-top: 4px; + text-align: left; + font-size: 8px +} + +.atom_icon { + background: url("../images/icons/atom.png") no-repeat scroll 3px; + padding-left: 20px; + padding-top: 4px; + text-align: left; + font-size: 8px +} + +.archive_icon { + background: url("../images/icons/compress.png") no-repeat scroll 3px; + padding-left: 20px; + text-align: left; + padding-top: 1px; +} + +.start_following_icon { + background: url("../images/icons/heart_add.png") no-repeat scroll 3px; + padding-left: 20px; + text-align: left; + padding-top: 0px; +} + +.stop_following_icon { + background: url("../images/icons/heart_delete.png") no-repeat scroll 3px; + padding-left: 20px; + text-align: left; + padding-top: 0px; +} + +.action_button { + border: 0; + display: inline; +} + +.action_button:hover { + border: 0; + text-decoration: underline; + cursor: pointer; +} + +#switch_repos { + position: absolute; + height: 25px; + z-index: 1; +} + +#switch_repos select { + min-width: 150px; + max-height: 250px; + z-index: 1; +} + +.breadcrumbs { + border: medium none; + color: #FFF; + float: left; + text-transform: uppercase; + font-weight: 700; + font-size: 14px; + margin: 0; + padding: 11px 0 11px 10px; +} + +.breadcrumbs .hash { + text-transform: none; + color: #fff; +} + +.breadcrumbs a { + color: #FFF; +} + +.flash_msg { + +} + +.flash_msg ul { + +} + +.error_msg { + background-color: #c43c35; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), + to(#c43c35) ); + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), + color-stop(100%, #c43c35) ); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(top, #ee5f5b, #c43c35); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', + endColorstr='#c43c35', GradientType=0 ); + border-color: #c43c35 #c43c35 #882a25; +} + +.warning_msg { + color: #404040 !important; + background-color: #eedc94; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), + to(#eedc94) ); + background-image: -moz-linear-gradient(top, #fceec1, #eedc94); + background-image: -ms-linear-gradient(top, #fceec1, #eedc94); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), + color-stop(100%, #eedc94) ); + background-image: -webkit-linear-gradient(top, #fceec1, #eedc94); + background-image: -o-linear-gradient(top, #fceec1, #eedc94); + background-image: linear-gradient(top, #fceec1, #eedc94); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', + endColorstr='#eedc94', GradientType=0 ); + border-color: #eedc94 #eedc94 #e4c652; +} + +.success_msg { + background-color: #57a957; + background-repeat: repeat-x !important; + background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), + to(#57a957) ); + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -ms-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), + color-stop(100%, #57a957) ); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(top, #62c462, #57a957); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', + endColorstr='#57a957', GradientType=0 ); + border-color: #57a957 #57a957 #3d773d; +} + +.notice_msg { + background-color: #339bb9; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), + to(#339bb9) ); + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -ms-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), + color-stop(100%, #339bb9) ); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(top, #5bc0de, #339bb9); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', + endColorstr='#339bb9', GradientType=0 ); + border-color: #339bb9 #339bb9 #22697d; +} + +.success_msg,.error_msg,.notice_msg,.warning_msg { + font-size: 12px; + font-weight: 700; + min-height: 14px; + line-height: 14px; + margin-bottom: 10px; + margin-top: 0; + display: block; + overflow: auto; + padding: 6px 10px 6px 10px; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + position: relative; + color: #FFF; + border-width: 1px; + border-style: solid; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); +} + +#msg_close { + background: transparent url("../icons/cross_grey_small.png") no-repeat + scroll 0 0; + cursor: pointer; + height: 16px; + position: absolute; + right: 5px; + top: 5px; + width: 16px; +} + +div#legend_container table,div#legend_choices table { + width: auto !important; +} + +table#permissions_manage { + width: 0 !important; +} + +table#permissions_manage span.private_repo_msg { + font-size: 0.8em; + opacity: 0.6px; +} + +table#permissions_manage td.private_repo_msg { + font-size: 0.8em; +} + +table#permissions_manage tr#add_perm_input td { + vertical-align: middle; +} + +div.gravatar { + background-color: #FFF; + float: left; + margin-right: 0.7em; + padding: 1px 1px 1px 1px; + line-height:0; + -webkit-border-radius: 3px; + -khtml-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +div.gravatar img { + -webkit-border-radius: 2px; + -khtml-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; +} + +#header,#content,#footer { + min-width: 978px; +} + +#content { + clear: both; + overflow: hidden; + padding: 54px 10px 14px 10px; +} + +#content div.box div.title div.search { + + border-left: 1px solid #316293; +} + +#content div.box div.title div.search div.input input { + border: 1px solid #316293; +} + +.ui-btn{ + color: #515151; + background-color: #DADADA; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#F4F4F4),to(#DADADA) ); + background-image: -moz-linear-gradient(top, #F4F4F4, #DADADA); + background-image: -ms-linear-gradient(top, #F4F4F4, #DADADA); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #F4F4F4),color-stop(100%, #DADADA) ); + background-image: -webkit-linear-gradient(top, #F4F4F4, #DADADA) ); + background-image: -o-linear-gradient(top, #F4F4F4, #DADADA) ); + background-image: linear-gradient(top, #F4F4F4, #DADADA); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#F4F4F4', endColorstr='#DADADA', GradientType=0); + + border-top: 1px solid #DDD; + border-left: 1px solid #c6c6c6; + border-right: 1px solid #DDD; + border-bottom: 1px solid #c6c6c6; + color: #515151; + outline: none; + margin: 0px 3px 3px 0px; + -webkit-border-radius: 4px 4px 4px 4px !important; + -khtml-border-radius: 4px 4px 4px 4px !important; + -moz-border-radius: 4px 4px 4px 4px !important; + border-radius: 4px 4px 4px 4px !important; + cursor: pointer !important; + padding: 3px 3px 3px 3px; + background-position: 0 -15px; + +} +.ui-btn.xsmall{ + padding: 1px 2px 1px 1px; +} +.ui-btn.clone{ + padding: 5px 2px 6px 1px; + margin: 0px -4px 3px 0px; + -webkit-border-radius: 4px 0px 0px 4px !important; + -khtml-border-radius: 4px 0px 0px 4px !important; + -moz-border-radius: 4px 0px 0px 4px !important; + border-radius: 4px 0px 0px 4px !important; + width: 100px; + text-align: center; + float: left; + position: absolute; +} +.ui-btn:focus { + outline: none; +} +.ui-btn:hover{ + background-position: 0 0px; + text-decoration: none; + color: #515151; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25), 0 0 3px #FFFFFF !important; +} + +.ui-btn.red{ + color:#fff; + background-color: #c43c35; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35)); + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(top, #ee5f5b, #c43c35); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0); + border-color: #c43c35 #c43c35 #882a25; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); +} + + +.ui-btn.blue{ + background-color: #339bb9; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9)); + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -ms-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(top, #5bc0de, #339bb9); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0); + border-color: #339bb9 #339bb9 #22697d; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); +} + +.ui-btn.green{ + background-color: #57a957; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957)); + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -ms-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(top, #62c462, #57a957); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0); + border-color: #57a957 #57a957 #3d773d; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); +} + +ins,div.options a:hover { + text-decoration: none; +} + +img, +#header #header-inner #quick li a:hover span.normal, +#header #header-inner #quick li ul li.last, +#content div.box div.form div.fields div.field div.textarea table td table td a, +#clone_url, +#clone_url_id +{ + border: none; +} + +img.icon,.right .merge img { + vertical-align: bottom; +} + +#header ul#logged-user,#content div.box div.title ul.links, +#content div.box div.message div.dismiss, +#content div.box div.traffic div.legend ul + { + float: right; + margin: 0; + padding: 0; +} + +#header #header-inner #home,#header #header-inner #logo, +#content div.box ul.left,#content div.box ol.left, +#content div.box div.pagination-left,div#commit_history, +div#legend_data,div#legend_container,div#legend_choices + { + float: left; +} + +#header #header-inner #quick li:hover ul ul, +#header #header-inner #quick li:hover ul ul ul, +#header #header-inner #quick li:hover ul ul ul ul, +#content #left #menu ul.closed,#content #left #menu li ul.collapsed,.yui-tt-shadow + { + display: none; +} + +#header #header-inner #quick li:hover ul,#header #header-inner #quick li li:hover ul,#header #header-inner #quick li li li:hover ul,#header #header-inner #quick li li li li:hover ul,#content #left #menu ul.opened,#content #left #menu li ul.expanded + { + display: block; +} + +#content div.graph { + padding: 0 10px 10px; +} + +#content div.box div.title ul.links li a:hover,#content div.box div.title ul.links li.ui-tabs-selected a + { + color: #bfe3ff; +} + +#content div.box ol.lower-roman,#content div.box ol.upper-roman,#content div.box ol.lower-alpha,#content div.box ol.upper-alpha,#content div.box ol.decimal + { + margin: 10px 24px 10px 44px; +} + +#content div.box div.form,#content div.box div.table,#content div.box div.traffic + { + clear: both; + overflow: hidden; + margin: 0; + padding: 0 20px 10px; +} + +#content div.box div.form div.fields,#login div.form,#login div.form div.fields,#register div.form,#register div.form div.fields + { + clear: both; + overflow: hidden; + margin: 0; + padding: 0; +} + +#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; + color: #363636; + margin: 0; + padding: 2px 0 0; +} + +#content div.box div.form div.fields div.field div.input input.error,#login div.form div.fields div.field div.input input.error,#register div.form div.fields div.field div.input input.error + { + background: #FBE3E4; + border-top: 1px solid #e1b2b3; + border-left: 1px solid #e1b2b3; + border-right: 1px solid #FBC2C4; + border-bottom: 1px solid #FBC2C4; +} + +#content div.box div.form div.fields div.field div.input input.success,#login div.form div.fields div.field div.input input.success,#register div.form div.fields div.field div.input input.success + { + background: #E6EFC2; + border-top: 1px solid #cebb98; + border-left: 1px solid #cebb98; + border-right: 1px solid #c6d880; + border-bottom: 1px solid #c6d880; +} + +#content div.box-left div.form div.fields div.field div.textarea,#content div.box-right div.form div.fields div.field div.textarea,#content div.box div.form div.fields div.field div.select select,#content div.box table th.selected input,#content div.box table td.selected input + { + 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; +} + +#content div.box div.form div.fields div.field div.select a.ui-selectmenu-focus,#content div.box div.action a.ui-selectmenu-focus + { + border: 1px solid #666; +} + +#content div.box div.form div.fields div.field div.checkboxes div.checkbox,#content div.box div.form div.fields div.field div.radios div.radio + { + clear: both; + overflow: hidden; + margin: 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 + { + float: left; + margin: 0; +} + +#content div.box div.form div.fields div.field div.checkboxes div.checkbox label,#content div.box div.form div.fields div.field div.radios div.radio label + { + height: 1%; + display: block; + float: left; + 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 + { + color: #000; + font-size: 11px; + font-weight: 700; + margin: 0; +} + +input.ui-button { + background: #e5e3e3 url("../images/button.png") repeat-x; + border-top: 1px solid #DDD; + border-left: 1px solid #c6c6c6; + border-right: 1px solid #DDD; + border-bottom: 1px solid #c6c6c6; + color: #515151 !important; + outline: none; + margin: 0; + padding: 6px 12px; + -webkit-border-radius: 4px 4px 4px 4px; + -khtml-border-radius: 4px 4px 4px 4px; + -moz-border-radius: 4px 4px 4px 4px; + border-radius: 4px 4px 4px 4px; + box-shadow: 0 1px 0 #ececec; + cursor: pointer; +} + +input.ui-button:hover { + background: #b4b4b4 url("../images/button_selected.png") repeat-x; + border-top: 1px solid #ccc; + border-left: 1px solid #bebebe; + border-right: 1px solid #b1b1b1; + border-bottom: 1px solid #afafaf; +} + +div.form div.fields div.field div.highlight,#content div.box div.form div.fields div.buttons div.highlight + { + display: inline; +} + +#content div.box div.form div.fields div.buttons,div.form div.fields div.buttons + { + margin: 10px 0 0 200px; + padding: 0; +} + +#content div.box-left div.form div.fields div.buttons,#content div.box-right div.form div.fields div.buttons,div.box-left div.form div.fields div.buttons,div.box-right div.form div.fields div.buttons + { + margin: 10px 0 0; +} + +#content div.box table td.user,#content div.box table td.address { + width: 10%; + text-align: center; +} + +#content div.box div.action div.button,#login div.form div.fields div.field div.input div.link,#register div.form div.fields div.field div.input div.link + { + text-align: right; + margin: 6px 0 0; + padding: 0; +} + +#content div.box div.action div.button input.ui-state-hover,#login div.form div.fields div.buttons input.ui-state-hover,#register div.form div.fields div.buttons input.ui-state-hover + { + background: #b4b4b4 url("../images/button_selected.png") repeat-x; + border-top: 1px solid #ccc; + border-left: 1px solid #bebebe; + border-right: 1px solid #b1b1b1; + border-bottom: 1px solid #afafaf; + color: #515151; + margin: 0; + padding: 6px 12px; +} + +#content div.box div.pagination div.results,#content div.box div.pagination-wh div.results + { + text-align: left; + float: left; + margin: 0; + padding: 0; +} + +#content div.box div.pagination div.results span,#content div.box div.pagination-wh div.results span + { + height: 1%; + display: block; + float: left; + background: #ebebeb url("../images/pager.png") repeat-x; + border-top: 1px solid #dedede; + border-left: 1px solid #cfcfcf; + border-right: 1px solid #c4c4c4; + border-bottom: 1px solid #c4c4c4; + color: #4A4A4A; + font-weight: 700; + margin: 0; + padding: 6px 8px; +} + +#content div.box div.pagination ul.pager li.disabled,#content div.box div.pagination-wh a.disabled + { + color: #B4B4B4; + padding: 6px; +} + +#login,#register { + width: 520px; + margin: 10% auto 0; + padding: 0; +} + +#login div.color,#register div.color { + clear: both; + overflow: hidden; + background: #FFF; + margin: 10px auto 0; + padding: 3px 3px 3px 0; +} + +#login div.color a,#register div.color a { + width: 20px; + height: 20px; + display: block; + float: left; + margin: 0 0 0 3px; + padding: 0; +} + +#login div.title h5,#register div.title h5 { + color: #fff; + margin: 10px; + padding: 0; +} + +#login div.form div.fields div.field,#register div.form div.fields div.field + { + clear: both; + overflow: hidden; + margin: 0; + padding: 0 0 10px; +} + +#login div.form div.fields div.field span.error-message,#register div.form div.fields div.field span.error-message + { + height: 1%; + display: block; + 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 + { + color: #000; + font-weight: 700; +} + +#login div.form div.fields div.field div.input,#register div.form div.fields div.field div.input + { + float: left; + margin: 0; + padding: 0; +} + +#login div.form div.fields div.field div.checkbox,#register div.form div.fields div.field div.checkbox + { + margin: 0 0 0 184px; + padding: 0; +} + +#login div.form div.fields div.field div.checkbox label,#register div.form div.fields div.field div.checkbox label + { + color: #565656; + font-weight: 700; +} + +#login div.form div.fields div.buttons input,#register div.form div.fields div.buttons input + { + color: #000; + font-size: 1em; + font-weight: 700; + margin: 0; +} + +#changeset_content .container .wrapper,#graph_content .container .wrapper + { + width: 600px; +} + +#changeset_content .container .left { + float: left; + width: 75%; + padding-left: 5px; +} + +#changeset_content .container .left .date,.ac .match { + font-weight: 700; + padding-top: 5px; + padding-bottom: 5px; +} + +div#legend_container table td,div#legend_choices table td { + border: none !important; + height: 20px !important; + padding: 0 !important; +} + +.q_filter_box { + -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + border: 0 none; + color: #AAAAAA; + margin-bottom: -4px; + margin-top: -4px; + padding-left: 3px; +} + +#node_filter { + border: 0px solid #545454; + color: #AAAAAA; + padding-left: 3px; +} + + +.group_members_wrap{ + +} + +.group_members .group_member{ + height: 30px; + padding:0px 0px 0px 10px; +} + +/*README STYLE*/ + +div.readme { + padding:0px; +} + +div.readme h2 { + font-weight: normal; +} + +div.readme .readme_box { + background-color: #fafafa; +} + +div.readme .readme_box { clear:both; overflow:hidden; margin:0; padding:0 20px 10px; } -.box .search div.search_path { -background:none repeat scroll 0 0 #EEE; -border:1px solid #CCC; -color:blue; -margin-bottom:10px; -padding:10px 0; -} - -.box .search div.search_path div.link { -font-weight:700; -margin-left:25px; -} - -.box .search div.search_path div.link a { -color:#003367; -cursor:pointer; -text-decoration:none; -} - -#path_unlock { -color:red; -font-size:1.2em; -padding-left:4px; -} - -.info_box span { -margin-left:3px; -margin-right:3px; -} - -.info_box .rev { -color: #003367; -font-size: 1.6em; -font-weight: bold; -vertical-align: sub; -} - - -.info_box input#at_rev,.info_box input#size { -background:#FFF; -border-top:1px solid #b3b3b3; -border-left:1px solid #b3b3b3; -border-right:1px solid #eaeaea; -border-bottom:1px solid #eaeaea; -color:#000; -font-family:Lucida Grande, Verdana, Lucida Sans Regular, Lucida Sans Unicode, Arial, sans-serif; -font-size:12px; -margin:0; -padding:1px 5px 1px; -} - -.info_box input#view { -text-align:center; -padding:4px 3px 2px 2px; -} - -.yui-overlay,.yui-panel-container { -visibility:hidden; -position:absolute; -z-index:2; -} - -.yui-tt { -visibility:hidden; -position:absolute; -color:#666; -background-color:#FFF; -font-family:arial, helvetica, verdana, sans-serif; -border:2px solid #003367; -font:100% sans-serif; -width:auto; -opacity:1px; -padding:8px; -white-space: pre-wrap; --webkit-border-radius: 8px 8px 8px 8px; --khtml-border-radius: 8px 8px 8px 8px; --moz-border-radius: 8px 8px 8px 8px; -border-radius: 8px 8px 8px 8px; -box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); -} - -.ac { -vertical-align:top; -} - -.ac .yui-ac { -position:relative; -font-family:arial; -font-size:100%; -} - -.ac .perm_ac { -width:15em; -} - -.ac .yui-ac-input { -width:100%; -} - -.ac .yui-ac-container { -position:absolute; -top:1.6em; -width:100%; -} - -.ac .yui-ac-content { -position:absolute; -width:100%; -border:1px solid gray; -background:#fff; -overflow:hidden; -z-index:9050; -} - -.ac .yui-ac-shadow { -position:absolute; -width:100%; -background:#000; --moz-opacity:0.1px; -opacity:.10; -filter:alpha(opacity = 10); -z-index:9049; -margin:.3em; -} - -.ac .yui-ac-content ul { -width:100%; -margin:0; -padding:0; -} - -.ac .yui-ac-content li { -cursor:default; -white-space:nowrap; -margin:0; -padding:2px 5px; -} - -.ac .yui-ac-content li.yui-ac-prehighlight { -background:#B3D4FF; -} - -.ac .yui-ac-content li.yui-ac-highlight { -background:#556CB5; -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; -} - -.add_icon { -background:url("../images/icons/add.png") no-repeat scroll 3px; -padding-left:20px; -padding-top:0px; -text-align:left; -} - -.edit_icon { -background:url("../images/icons/folder_edit.png") no-repeat scroll 3px; -padding-left:20px; -padding-top:0px; -text-align:left; -} - -.delete_icon { -background:url("../images/icons/delete.png") no-repeat scroll 3px; -padding-left:20px; -padding-top:0px; -text-align:left; -} - -.refresh_icon { -background:url("../images/icons/arrow_refresh.png") no-repeat scroll 3px; -padding-left:20px; -padding-top:0px; -text-align:left; -} - -.pull_icon { -background:url("../images/icons/connect.png") no-repeat scroll 3px; -padding-left:20px; -padding-top:0px; -text-align:left; -} - -.rss_icon { -background:url("../images/icons/rss_16.png") no-repeat scroll 3px; -padding-left:20px; -padding-top:0px; -text-align:left; -} - -.atom_icon { -background:url("../images/icons/atom.png") no-repeat scroll 3px; -padding-left:20px; -padding-top:0px; -text-align:left; -} - -.archive_icon { -background:url("../images/icons/compress.png") no-repeat scroll 3px; -padding-left:20px; -text-align:left; -padding-top:1px; -} - -.start_following_icon { -background:url("../images/icons/heart_add.png") no-repeat scroll 3px; -padding-left:20px; -text-align:left; -padding-top:0px; -} - -.stop_following_icon { -background:url("../images/icons/heart_delete.png") no-repeat scroll 3px; -padding-left:20px; -text-align:left; -padding-top:0px; -} - -.action_button { -border:0; -display:inline; -} - -.action_button:hover { -border:0; -text-decoration:underline; -cursor:pointer; -} - -#switch_repos { -position:absolute; -height:25px; -z-index:1; -} - -#switch_repos select { -min-width:150px; -max-height:250px; -z-index:1; -} - -.breadcrumbs { -border:medium none; -color:#FFF; -float:left; -text-transform:uppercase; -font-weight:700; -font-size:14px; -margin:0; -padding:11px 0 11px 10px; -} - -.breadcrumbs a { -color:#FFF; -} - -.flash_msg ul { -margin:0; -padding:0 0 10px; -} - -.error_msg { -background-color:#FFCFCF; -background-image:url("../images/icons/error_msg.png"); -border:1px solid #FF9595; -color:#C30; -} - -.warning_msg { -background-color:#FFFBCC; -background-image:url("../images/icons/warning_msg.png"); -border:1px solid #FFF35E; -color:#C69E00; -} - -.success_msg { -background-color:#D5FFCF; -background-image:url("../images/icons/success_msg.png"); -border:1px solid #97FF88; -color:#090; -} - -.notice_msg { -background-color:#DCE3FF; -background-image:url("../images/icons/notice_msg.png"); -border:1px solid #93A8FF; -color:#556CB5; -} - -.success_msg,.error_msg,.notice_msg,.warning_msg { -background-position:10px center; -background-repeat:no-repeat; -font-size:12px; -font-weight:700; -min-height:14px; -line-height:14px; -margin-bottom:0; -margin-top:0; -display:block; -overflow:auto; -padding:6px 10px 6px 40px; -} - -#msg_close { -background:transparent url("../icons/cross_grey_small.png") no-repeat scroll 0 0; -cursor:pointer; -height:16px; -position:absolute; -right:5px; -top:5px; -width:16px; -} - -div#legend_container table,div#legend_choices table { -width:auto !important; -} - -table#permissions_manage { -width:0 !important; -} - -table#permissions_manage span.private_repo_msg { -font-size:0.8em; -opacity:0.6px; -} - -table#permissions_manage td.private_repo_msg { -font-size:0.8em; -} - -table#permissions_manage tr#add_perm_input td { -vertical-align:middle; -} - -div.gravatar { -background-color:#FFF; -border:1px solid #D0D0D0; -float:left; -margin-right:0.7em; -padding:2px 2px 0; - --webkit-border-radius: 6px; --khtml-border-radius: 6px; --moz-border-radius: 6px; -border-radius: 6px; - -} - -div.gravatar img { --webkit-border-radius: 4px; --khtml-border-radius: 4px; --moz-border-radius: 4px; -border-radius: 4px; -} - -#header,#content,#footer { -min-width:978px; -} - -#content { -clear:both; -overflow:hidden; -padding:14px 10px; -} - -#content div.box div.title div.search { -background:url("../images/title_link.png") no-repeat top left; -border-left:1px solid #316293; -} - -#content div.box div.title div.search div.input input { -border:1px solid #316293; -} - -.ui-button-small a:hover { - -} -input.ui-button-small,.ui-button-small { -background:#e5e3e3 url("../images/button.png") repeat-x !important; -border-top:1px solid #DDD !important; -border-left:1px solid #c6c6c6 !important; -border-right:1px solid #DDD !important; -border-bottom:1px solid #c6c6c6 !important; -color:#515151 !important; -outline:none !important; -margin:0 !important; --webkit-border-radius: 4px 4px 4px 4px !important; --khtml-border-radius: 4px 4px 4px 4px !important; --moz-border-radius: 4px 4px 4px 4px !important; -border-radius: 4px 4px 4px 4px !important; -box-shadow: 0 1px 0 #ececec !important; -cursor: pointer !important; -padding:0px 2px 1px 2px; -} - -input.ui-button-small:hover,.ui-button-small:hover { -background:#b4b4b4 url("../images/button_selected.png") repeat-x !important; -border-top:1px solid #ccc !important; -border-left:1px solid #bebebe !important; -border-right:1px solid #b1b1b1 !important; -border-bottom:1px solid #afafaf !important; -text-decoration: none; -} - -input.ui-button-small-blue,.ui-button-small-blue { -background:#4e85bb url("../images/button_highlight.png") repeat-x; -border-top:1px solid #5c91a4; -border-left:1px solid #2a6f89; -border-right:1px solid #2b7089; -border-bottom:1px solid #1a6480; -color:#fff; --webkit-border-radius: 4px 4px 4px 4px; --khtml-border-radius: 4px 4px 4px 4px; --moz-border-radius: 4px 4px 4px 4px; -border-radius: 4px 4px 4px 4px; -box-shadow: 0 1px 0 #ececec; -cursor: pointer; -padding:0px 2px 1px 2px; -} - -input.ui-button-small-blue:hover { - -} - - -ins,div.options a:hover { -text-decoration:none; -} - -img,#header #header-inner #quick li a:hover span.normal,#header #header-inner #quick li ul li.last,#content div.box div.form div.fields div.field div.textarea table td table td a,#clone_url { -border:none; -} - -img.icon,.right .merge img { -vertical-align:bottom; -} - -#header ul#logged-user,#content div.box div.title ul.links,#content div.box div.message div.dismiss,#content div.box div.traffic div.legend ul { -float:right; -margin:0; -padding:0; -} - - -#header #header-inner #home,#header #header-inner #logo,#content div.box ul.left,#content div.box ol.left,#content div.box div.pagination-left,div#commit_history,div#legend_data,div#legend_container,div#legend_choices { -float:left; -} - -#header #header-inner #quick li:hover ul ul,#header #header-inner #quick li:hover ul ul ul,#header #header-inner #quick li:hover ul ul ul ul,#content #left #menu ul.closed,#content #left #menu li ul.collapsed,.yui-tt-shadow { -display:none; -} - -#header #header-inner #quick li:hover ul,#header #header-inner #quick li li:hover ul,#header #header-inner #quick li li li:hover ul,#header #header-inner #quick li li li li:hover ul,#content #left #menu ul.opened,#content #left #menu li ul.expanded { -display:block; -} - -#content div.graph{ -padding:0 10px 10px; -} - -#content div.box div.title ul.links li a:hover,#content div.box div.title ul.links li.ui-tabs-selected a { -color:#bfe3ff; -} - -#content div.box ol.lower-roman,#content div.box ol.upper-roman,#content div.box ol.lower-alpha,#content div.box ol.upper-alpha,#content div.box ol.decimal { -margin:10px 24px 10px 44px; -} - -#content div.box div.form,#content div.box div.table,#content div.box div.traffic { +div.readme .readme_box h1, div.readme .readme_box h2, div.readme .readme_box h3, div.readme .readme_box h4, div.readme .readme_box h5, div.readme .readme_box h6 { +border-bottom: 0 !important; +margin: 0 !important; +padding: 0 !important; +line-height: 1.5em !important; +} + + +div.readme .readme_box h1:first-child { +padding-top: .25em !important; +} + +div.readme .readme_box h2, div.readme .readme_box h3 { +margin: 1em 0 !important; +} + +div.readme .readme_box h2 { +margin-top: 1.5em !important; +border-top: 4px solid #e0e0e0 !important; +padding-top: .5em !important; +} + +div.readme .readme_box p { +color: black !important; +margin: 1em 0 !important; +line-height: 1.5em !important; +} + +div.readme .readme_box ul { +list-style: disc !important; +margin: 1em 0 1em 2em !important; +} + +div.readme .readme_box ol { +list-style: decimal; +margin: 1em 0 1em 2em !important; +} + +div.readme .readme_box pre, code { +font: 12px "Bitstream Vera Sans Mono","Courier",monospace; +} + +div.readme .readme_box code { + font-size: 12px !important; + background-color: ghostWhite !important; + color: #444 !important; + padding: 0 .2em !important; + border: 1px solid #dedede !important; +} + +div.readme .readme_box pre code { + padding: 0 !important; + font-size: 12px !important; + background-color: #eee !important; + border: none !important; +} + +div.readme .readme_box pre { + margin: 1em 0; + font-size: 12px; + background-color: #eee; + border: 1px solid #ddd; + padding: 5px; + color: #444; + overflow: auto; + -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + + +/** RST STYLE **/ + + +div.rst-block { + padding:0px; +} + +div.rst-block h2 { + font-weight: normal; +} + +div.rst-block { + background-color: #fafafa; +} + +div.rst-block { clear:both; overflow:hidden; margin:0; padding:0 20px 10px; } -#content div.box div.form div.fields,#login div.form,#login div.form div.fields,#register div.form,#register div.form div.fields { -clear:both; -overflow:hidden; -margin:0; -padding:0; -} - -#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; -color:#363636; -margin:0; -padding:2px 0 0; -} - -#content div.box div.form div.fields div.field div.input input.error,#login div.form div.fields div.field div.input input.error,#register div.form div.fields div.field div.input input.error { -background:#FBE3E4; -border-top:1px solid #e1b2b3; -border-left:1px solid #e1b2b3; -border-right:1px solid #FBC2C4; -border-bottom:1px solid #FBC2C4; -} - -#content div.box div.form div.fields div.field div.input input.success,#login div.form div.fields div.field div.input input.success,#register div.form div.fields div.field div.input input.success { -background:#E6EFC2; -border-top:1px solid #cebb98; -border-left:1px solid #cebb98; -border-right:1px solid #c6d880; -border-bottom:1px solid #c6d880; -} - -#content div.box-left div.form div.fields div.field div.textarea,#content div.box-right div.form div.fields div.field div.textarea,#content div.box div.form div.fields div.field div.select select,#content div.box table th.selected input,#content div.box table td.selected input { -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; -} - -#content div.box div.form div.fields div.field div.select a.ui-selectmenu-focus,#content div.box div.action a.ui-selectmenu-focus { -border:1px solid #666; -} - -#content div.box div.form div.fields div.field div.checkboxes div.checkbox,#content div.box div.form div.fields div.field div.radios div.radio { -clear:both; -overflow:hidden; -margin: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 { -float:left; -margin:0; -} - -#content div.box div.form div.fields div.field div.checkboxes div.checkbox label,#content div.box div.form div.fields div.field div.radios div.radio label { -height:1%; -display:block; -float:left; -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 { -color:#000; -font-family:Lucida Grande, Verdana, Lucida Sans Regular, Lucida Sans Unicode, Arial, sans-serif; -font-size:11px; -font-weight:700; -margin:0; -} - -input.ui-button { -background:#e5e3e3 url("../images/button.png") repeat-x; -border-top:1px solid #DDD; -border-left:1px solid #c6c6c6; -border-right:1px solid #DDD; -border-bottom:1px solid #c6c6c6; -color:#515151 !important; -outline:none; -margin:0; -padding:6px 12px; --webkit-border-radius: 4px 4px 4px 4px; --khtml-border-radius: 4px 4px 4px 4px; --moz-border-radius: 4px 4px 4px 4px; -border-radius: 4px 4px 4px 4px; -box-shadow: 0 1px 0 #ececec; -cursor: pointer; -} - -input.ui-button:hover { -background:#b4b4b4 url("../images/button_selected.png") repeat-x; -border-top:1px solid #ccc; -border-left:1px solid #bebebe; -border-right:1px solid #b1b1b1; -border-bottom:1px solid #afafaf; -} - -div.form div.fields div.field div.highlight,#content div.box div.form div.fields div.buttons div.highlight { -display:inline; -} - -#content div.box div.form div.fields div.buttons,div.form div.fields div.buttons { -margin:10px 0 0 200px; -padding:0; -} - -#content div.box-left div.form div.fields div.buttons,#content div.box-right div.form div.fields div.buttons,div.box-left div.form div.fields div.buttons,div.box-right div.form div.fields div.buttons { -margin:10px 0 0; -} - -#content div.box table td.user,#content div.box table td.address { -width:10%; -text-align:center; -} - -#content div.box div.action div.button,#login div.form div.fields div.field div.input div.link,#register div.form div.fields div.field div.input div.link { -text-align:right; -margin:6px 0 0; -padding:0; -} - - -#content div.box div.action div.button input.ui-state-hover,#login div.form div.fields div.buttons input.ui-state-hover,#register div.form div.fields div.buttons input.ui-state-hover { -background:#b4b4b4 url("../images/button_selected.png") repeat-x; -border-top:1px solid #ccc; -border-left:1px solid #bebebe; -border-right:1px solid #b1b1b1; -border-bottom:1px solid #afafaf; -color:#515151; -margin:0; -padding:6px 12px; -} - -#content div.box div.pagination div.results,#content div.box div.pagination-wh div.results { -text-align:left; -float:left; -margin:0; -padding:0; -} - -#content div.box div.pagination div.results span,#content div.box div.pagination-wh div.results span { -height:1%; -display:block; -float:left; -background:#ebebeb url("../images/pager.png") repeat-x; -border-top:1px solid #dedede; -border-left:1px solid #cfcfcf; -border-right:1px solid #c4c4c4; -border-bottom:1px solid #c4c4c4; -color:#4A4A4A; -font-weight:700; -margin:0; -padding:6px 8px; -} - -#content div.box div.pagination ul.pager li.disabled,#content div.box div.pagination-wh a.disabled { -color:#B4B4B4; -padding:6px; -} - -#login,#register { -width:520px; -margin:10% auto 0; -padding:0; -} - -#login div.color,#register div.color { -clear:both; -overflow:hidden; -background:#FFF; -margin:10px auto 0; -padding:3px 3px 3px 0; -} - -#login div.color a,#register div.color a { -width:20px; -height:20px; -display:block; -float:left; -margin:0 0 0 3px; -padding:0; -} - -#login div.title h5,#register div.title h5 { -color:#fff; -margin:10px; -padding:0; -} - -#login div.form div.fields div.field,#register div.form div.fields div.field { -clear:both; -overflow:hidden; -margin:0; -padding:0 0 10px; -} - -#login div.form div.fields div.field span.error-message,#register div.form div.fields div.field span.error-message { -height:1%; -display:block; -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 { -color:#000; -font-weight:700; -} - -#login div.form div.fields div.field div.input,#register div.form div.fields div.field div.input { -float:left; -margin:0; -padding:0; -} - -#login div.form div.fields div.field div.checkbox,#register div.form div.fields div.field div.checkbox { -margin:0 0 0 184px; -padding:0; -} - -#login div.form div.fields div.field div.checkbox label,#register div.form div.fields div.field div.checkbox label { -color:#565656; -font-weight:700; -} - -#login div.form div.fields div.buttons input,#register div.form div.fields div.buttons input { -color:#000; -font-size:1em; -font-weight:700; -font-family:Verdana, Helvetica, Sans-Serif; -margin:0; -} - -#changeset_content .container .wrapper,#graph_content .container .wrapper { -width:600px; -} - -#changeset_content .container .left,#graph_content .container .left { -float:left; -width:70%; -padding-left:5px; -} - -#changeset_content .container .left .date,.ac .match { -font-weight:700; -padding-top: 5px; -padding-bottom:5px; -} - -div#legend_container table td,div#legend_choices table td { -border:none !important; -height:20px !important; -padding:0 !important; -} - -#q_filter{ -border:0 none; -color:#AAAAAA; -margin-bottom:-4px; -margin-top:-4px; -padding-left:3px; -} - -#node_filter{ -border:0px solid #545454; -color:#AAAAAA; -padding-left:3px; -} +div.rst-block h1, div.rst-block h2, div.rst-block h3, div.rst-block h4, div.rst-block h5, div.rst-block h6 { +border-bottom: 0 !important; +margin: 0 !important; +padding: 0 !important; +line-height: 1.5em !important; +} + + +div.rst-block h1:first-child { +padding-top: .25em !important; +} + +div.rst-block h2, div.rst-block h3 { +margin: 1em 0 !important; +} + +div.rst-block h2 { +margin-top: 1.5em !important; +border-top: 4px solid #e0e0e0 !important; +padding-top: .5em !important; +} + +div.rst-block p { +color: black !important; +margin: 1em 0 !important; +line-height: 1.5em !important; +} + +div.rst-block ul { +list-style: disc !important; +margin: 1em 0 1em 2em !important; +} + +div.rst-block ol { +list-style: decimal; +margin: 1em 0 1em 2em !important; +} + +div.rst-block pre, code { +font: 12px "Bitstream Vera Sans Mono","Courier",monospace; +} + +div.rst-block code { + font-size: 12px !important; + background-color: ghostWhite !important; + color: #444 !important; + padding: 0 .2em !important; + border: 1px solid #dedede !important; +} + +div.rst-block pre code { + padding: 0 !important; + font-size: 12px !important; + background-color: #eee !important; + border: none !important; +} + +div.rst-block pre { + margin: 1em 0; + font-size: 12px; + background-color: #eee; + border: 1px solid #ddd; + padding: 5px; + color: #444; + overflow: auto; + -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + + +/** comment main **/ +.comments { + padding:10px 20px; +} + +.comments .comment { + border: 1px solid #ddd; + margin-top: 10px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.comments .comment .meta { + background: #f8f8f8; + padding: 4px; + border-bottom: 1px solid #ddd; +} + +.comments .comment .meta img { + vertical-align: middle; +} + +.comments .comment .meta .user { + font-weight: bold; +} + +.comments .comment .meta .date { +} + +.comments .comment .text { + background-color: #FAFAFA; +} +.comment .text div.rst-block p { + margin: 0.5em 0px !important; +} + +.comments .comments-number{ + padding:0px 0px 10px 0px; + font-weight: bold; + color: #666; + font-size: 16px; +} + +/** comment form **/ + +.comment-form .clearfix{ + background: #EEE; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + padding: 10px; +} + +div.comment-form { + margin-top: 20px; +} + +.comment-form strong { + display: block; + margin-bottom: 15px; +} + +.comment-form textarea { + width: 100%; + height: 100px; + font-family: 'Monaco', 'Courier', 'Courier New', monospace; +} + +form.comment-form { + margin-top: 10px; + margin-left: 10px; +} + +.comment-form-submit { + margin-top: 5px; + margin-left: 525px; +} + +.file-comments { + display: none; +} + +.comment-form .comment { + margin-left: 10px; +} + +.comment-form .comment-help{ + padding: 0px 0px 5px 0px; + color: #666; +} + +.comment-form .comment-button{ + padding-top:5px; +} + +.add-another-button { + margin-left: 10px; + margin-top: 10px; + margin-bottom: 10px; +} + +.comment .buttons { + float: right; +} + + +.show-inline-comments{ + position: relative; + top:1px +} + +/** comment inline form **/ + +.comment-inline-form .clearfix{ + background: #EEE; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + padding: 5px; +} + +div.comment-inline-form { + margin-top: 5px; + padding:2px 6px 8px 6px; +} + +.comment-inline-form strong { + display: block; + margin-bottom: 15px; +} + +.comment-inline-form textarea { + width: 100%; + height: 100px; + font-family: 'Monaco', 'Courier', 'Courier New', monospace; +} + +form.comment-inline-form { + margin-top: 10px; + margin-left: 10px; +} + +.comment-inline-form-submit { + margin-top: 5px; + margin-left: 525px; +} + +.file-comments { + display: none; +} + +.comment-inline-form .comment { + margin-left: 10px; +} + +.comment-inline-form .comment-help{ + padding: 0px 0px 2px 0px; + color: #666666; + font-size: 10px; +} + +.comment-inline-form .comment-button{ + padding-top:5px; +} + +/** comment inline **/ +.inline-comments { + padding:10px 20px; +} + +.inline-comments div.rst-block { + clear:both; + overflow:hidden; + margin:0; + padding:0 20px 0px; +} +.inline-comments .comment { + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + margin: 3px 3px 5px 5px; + background-color: #FAFAFA; +} +.inline-comments .comment-wrapp{ + padding:1px; +} +.inline-comments .comment .meta { + background: #f8f8f8; + padding: 4px; + border-bottom: 1px solid #ddd; +} + +.inline-comments .comment .meta img { + vertical-align: middle; +} + +.inline-comments .comment .meta .user { + font-weight: bold; +} + +.inline-comments .comment .meta .date { +} + +.inline-comments .comment .text { + background-color: #FAFAFA; +} + +.inline-comments .comments-number{ + padding:0px 0px 10px 0px; + font-weight: bold; + color: #666; + font-size: 16px; +} +.inline-comments-button .add-comment{ + margin:10px 5px !important; +} +.notifications{ + border-radius: 4px 4px 4px 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + float: right; + margin: 20px 0px 0px 0px; + position: absolute; + text-align: center; + width: 26px; + z-index: 1000; +} +.notifications a{ + color:#888 !important; + display: block; + font-size: 10px; + background-color: #DEDEDE !important; + border-radius: 2px !important; + -webkit-border-radius: 2px !important; + -moz-border-radius: 2px !important; +} +.notifications a:hover{ + text-decoration: none !important; + background-color: #EEEFFF !important; +} +.notification-header{ + padding-top:6px; +} +.notification-header .desc{ + font-size: 16px; + height: 24px; + float: left +} +.notification-list .container.unread{ + +} +.notification-header .gravatar{ + +} +.notification-header .desc.unread{ + font-weight: bold; + font-size: 17px; +} + +.notification-header .delete-notifications{ + float: right; + padding-top: 8px; + cursor: pointer; +} +.notification-subject{ + clear:both; + border-bottom: 1px solid #eee; + padding:5px 0px 5px 38px; +} + + +/***************************************************************************** + DIFFS CSS +******************************************************************************/ + +div.diffblock { + overflow: auto; + padding: 0px; + border: 1px solid #ccc; + background: #f8f8f8; + font-size: 100%; + line-height: 100%; + /* new */ + line-height: 125%; + -webkit-border-radius: 6px 6px 0px 0px; + -moz-border-radius: 6px 6px 0px 0px; + border-radius: 6px 6px 0px 0px; +} +div.diffblock.margined{ + margin: 0px 20px 0px 20px; +} +div.diffblock .code-header{ + border-bottom: 1px solid #CCCCCC; + background: #EEEEEE; + padding:10px 0 10px 0; + height: 14px; +} +div.diffblock .code-header.cv{ + height: 34px; +} +div.diffblock .code-header-title{ + padding: 0px 0px 10px 5px !important; + margin: 0 !important; +} +div.diffblock .code-header .hash{ + float: left; + padding: 2px 0 0 2px; +} +div.diffblock .code-header .date{ + float:left; + text-transform: uppercase; + padding: 2px 0px 0px 2px; +} +div.diffblock .code-header div{ + margin-left:4px; + font-weight: bold; + font-size: 14px; +} +div.diffblock .code-body{ + background: #FFFFFF; +} +div.diffblock pre.raw{ + background: #FFFFFF; + color:#000000; +} +table.code-difftable{ + border-collapse: collapse; + width: 99%; +} +table.code-difftable td { + padding: 0 !important; + background: none !important; + border:0 !important; + vertical-align: none !important; +} +table.code-difftable .context{ + background:none repeat scroll 0 0 #DDE7EF; +} +table.code-difftable .add{ + background:none repeat scroll 0 0 #DDFFDD; +} +table.code-difftable .add ins{ + background:none repeat scroll 0 0 #AAFFAA; + text-decoration:none; +} +table.code-difftable .del{ + background:none repeat scroll 0 0 #FFDDDD; +} +table.code-difftable .del del{ + background:none repeat scroll 0 0 #FFAAAA; + text-decoration:none; +} + +/** LINE NUMBERS **/ +table.code-difftable .lineno{ + + padding-left:2px; + padding-right:2px; + text-align:right; + width:32px; + -moz-user-select:none; + -webkit-user-select: none; + border-right: 1px solid #CCC !important; + border-left: 0px solid #CCC !important; + border-top: 0px solid #CCC !important; + border-bottom: none !important; + vertical-align: middle !important; + +} +table.code-difftable .lineno.new { +} +table.code-difftable .lineno.old { +} +table.code-difftable .lineno a{ + color:#747474 !important; + font:11px "Bitstream Vera Sans Mono",Monaco,"Courier New",Courier,monospace !important; + letter-spacing:-1px; + text-align:right; + padding-right: 2px; + cursor: pointer; + display: block; + width: 32px; +} + +table.code-difftable .lineno-inline{ + background:none repeat scroll 0 0 #FFF !important; + padding-left:2px; + padding-right:2px; + text-align:right; + width:30px; + -moz-user-select:none; + -webkit-user-select: none; +} + +/** CODE **/ +table.code-difftable .code { + display: block; + width: 100%; +} +table.code-difftable .code td{ + margin:0; + padding:0; +} +table.code-difftable .code pre{ + margin:0; + padding:0; + height: 17px; + line-height: 17px; +} + + +.diffblock.margined.comm .line .code:hover{ + background-color:#FFFFCC !important; + cursor: pointer !important; + background-image:url("../images/icons/comment_add.png") !important; + background-repeat:no-repeat !important; + background-position: right !important; + background-position: 0% 50% !important; +} +.diffblock.margined.comm .line .code.no-comment:hover{ + background-image: none !important; + cursor: auto !important; + background-color: inherit !important; + +} diff --git a/rhodecode/public/images/button_highlight_selected.png b/rhodecode/public/images/button_highlight_selected.png deleted file mode 100644 index ffd3a38e7d2741563661f91ef0ae0084e3f0f2ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@;M1%fy~Ir2;r~IJAo|5 zk|4ie28U-i(tsREPZ!6Kid)Guq>ddput17Y#X`7&C779+nL(zFDdZjVkv^az22WQ% Jmvv4FO#tWt9i9LH diff --git a/rhodecode/public/images/dt-arrow-up.png b/rhodecode/public/images/dt-arrow-up.png new file mode 100644 index 0000000000000000000000000000000000000000..1c674316aed41943dae79b01583956db63c8be08 GIT binary patch literal 116 zc%17D@N?(olHy`uVBq!ia0vp@K+M9#3?wzWV%32ZXMj(L>;M1%fy}VT__niwAVJ2G zAirP+hi5m^fE-Cr7srr_TgeGo2?+@gd>DjQ2|6TYNiZ`ri2h~@(drDS1}b9kboFyt I=akR{0Kz`Cro6_s8FWh@xnjC}wWaZYHIbTbY*Iq>foGSyNapWn!kFmW`U4woqn!xs_H^ zo9iKO#kd6kR6hwgUhN;N^bIi9Rq$p-`5WmvK1U($do6;-XkAURYR=N~L3C6I>ooAQU=Y{KlD; zGg&M?pU-BGibNubM8aZ^ip3Izujy$%dvuIDE1#VeO$g;usmvjQE}EO0=JPv8<;)p5 zZ)%D)BWLjCqu=Gd9Jx%kU~z)f&Xo6Y@{Y*ROYv$>}_W3-*{$J|hq;EG%+G z^1(^@2$MCz;mBmN#Kc6F!sXc5`t|EMT%P&py_&w+?Kn{m{_ysZ%VG z>^;X?>Q;pHW01IB?;sP&g}8FuQ`n>K6Amrk;h0#fb@yL?Z2?^++r& z*|GXLIXN>o^=Ci(Z`t|ab*G$tMXS8LTt*`C3T+?%+5Cw%$$hnNioA8IVee%9MtbFz zc{R1@=;-OD-7;P^ufitcR*~4nc)P68ILyhm|EdCV zb90+Gr}Q#fW5?cXai=?{!jR1s%m<$0-FM_XM{*N%IUG*kP7r61GFX7vuq`tzENu3x zbJtb4@VTY!!7On`hQ-06*~;{m$2xWO^x3}9IRas!=lP-Im3sPNi+z1&rWYivqYMwF z1@5QuTN?B<<#~GJwP!WP8?G%TCZ0_AGn$F|#q&_~9u&}BJu5;9U)^RYc z+&wj&B5g?;4Md15d=e6iipyKP&(?p>*jk*n-e^Otm_@WZQoVF3Hn*U6;*-a#!lA_} zCMRYG`@5XkLHH>co%Z#cxoz^Jz*hKpd%vCWx3=4oD_7X~)v@>o0aG6>9cPG7iYqkf6fh&J*F}PC4;}ediC1Iqhe-<`H_olAt51~ zTwZWE+#$yB$3gk)w}k@(gN$J&PbB{bqT?a0g+|scm%ao(KV)@2t7_6!7grBy1Ap+4j>HR+Q^V76*DB1B^BT(0LGaC575eramE4T`1IaGF6vCZKqKE zDAYvCDy@I`zZJyeGh zS^z^^P*}R^Bo+09V31uLVI%6QaB3?i_5Wbs<6ciqHF+A8r}9)gIMR5MADDZ%p;lCd z>2@VRPrh@`)!6pH#;osaeTHwvTvhZ`SCw}Ger=Cll~bHv1G+&Hc4DqR_xC%!k9!^B z1nioXqeh$Z3Bw%7< z!VW3~+Rp2+tfK*1(n_J>%2(6?&XQ2mNU0t8P?Mc$oMyuAH$8@09 z++d`kpdovL2PrqQ*2D(YerOu0z|J?^w6I|4Su~GSVh6?&OOJeiE(`^q!0`K9Hr2$( zwEVoKL*msvD+$Opl#Dp(T)pi7`H{P3e0TEdZgI{db_6`WGx^K<`+X0QlDp7u*cpfM zG&{9M^&8NasCh%$X}F`Kqc3pt7zp+32zG$d*qKv~&fij22=L*+5S(1Cq?o?(%$bSV z3qmI0RIz@b0VGh3B-;<`Rj$7U9)cT>3cKM+EAMETefsbnZ*$;i;R8rV-@5=_arIqB zU;Uofmfw;)UfY}_YD1j60*VZew@p12+n|bkr^}yz&{HJYB#=SQGBq_-1H~O=ehg$T=Hw{+s2;)WHnS79in z5NcOy2#-9iP z>rqnWuJz_hWu-txg+EBR`T9x*W?$H9zi^5VmG)NCV)TX?yHZofRKGZa0;*zTjBwN8 zPj=opk#~Pi@=#Teh z`Nez^KhK7coT)>$E#uTUt|DSs9zBQG&x6)W7cj zGB|m9T7@2?@v}fsO~x5!B>o84JYX|-qus(B(_Jtg1FJ2)csQ@!0zY4tocmXLe)CA9 z$;MnOS*q&K>4Ik3$9Kvu`vTx@KEs#mbNE0+wNGbfnW*X&Pcv zIK9bfc(}PaxOK*5w~%(fqT<@MQAxt^E@kOsZB6wmDf89ojCel{^&&fXk&sz*tWNcs z**2A>IwY|Fn|FoQh1qk7iJcF6BKki!nd`1df?bn5Meo=72u##Y)a9+Hb&X&m2|^?p z5=lBlk}{E`K_tP5Bm*Kzk4OR$NeCioEs>;3BteNwFm;oE3A7rYG&Es9qdZAc*xH7% zTCzjQwxeWwP?t?n>k<&es|LiY5aLx3@hX%^TCJ{O4|U#;a86KiPEc`PhW>wzTqnY3 z+>0b)0r!*SXfhF$4OUM7r_3U;)9Xi zsi_a*)RR_(0&7Q{p&X16fYEtM7P$vw>;W_rf)*&DpIpoAq_D1Y)G<*TQJ3LFjCwOh z!d4vOCA3sIg_#LqDj}G@2+21}#NG<}az1 zCwfDc?y1a|ubCC9BOL#ssRMku-o^E4+?Pew-REPGsAq8JMs?>MP&5;k(%nk&Yt7Si zP5?XaP(uHjVzS|1vJdrE84}#S$PehHpZ)Py8_8weTw7u2W-A6Gx~6+C z=`;mNzZjKda6;hohI>O*%2;jiC!v^{>G8p@E5~*(cku1c%)7;=$JTf1=?zB3v*~$-VOELb2tZk#@n#)d#L`+OWmJttpNFVAERsL}1UOuyGTwx?{Qk zF2`VQtuy?qUag`@Fq?$Z3+TJqHSf(*rIE|t3YDq=yS{=OcVDO2eMp&BBcMEYmNg4e z+D7L}8%pBdO8h}9N>%~3I?EGX`3(yN4AMM8(dZYKt32Iq+6B3gz3ymTrWwk5U;epy z^hM4?Ue>+8y9g80r1BJAFLM8>W+ZU(D9Z*AP=IEGa!%w}+-=1_ O5O8zyJXGR{P5uvhO#S}= diff --git a/rhodecode/public/images/title.png b/rhodecode/public/images/title.png deleted file mode 100644 index 94d4b242cf3f362a91e593a282427e5ebf9823cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@' + // Set to the height of the text, causes scrolling - '
' + // To measure line/char size
-           'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
' + - '
' + // Moved around its parent to cover visible view - '
' + - '
' + // Wraps and hides input textarea - '
' + - // Provides positioning relative to (visible) text origin - '
' + - '
 
' + // Absolutely positioned blinky cursor - '
'; // This DIV contains the actual code + '
' + // Wraps and hides input textarea + '
' + + '
' + + '
' + // Set to the height of the text, causes scrolling + '
' + + '
' + // Moved around its parent to cover visible view + '
' + + // Provides positioning relative to (visible) text origin + '
' + + '
 
' + // Absolutely positioned blinky cursor + '
' + // This DIV contains the actual code + '
'; if (place.appendChild) place.appendChild(wrapper); else place(wrapper); // I've never seen more elegant code in my life. - var code = wrapper.firstChild, measure = code.firstChild, mover = measure.nextSibling, + var inputDiv = wrapper.firstChild, input = inputDiv.firstChild, + scroller = wrapper.lastChild, code = scroller.firstChild, + measure = code.firstChild, mover = measure.nextSibling, gutter = mover.firstChild, gutterText = gutter.firstChild, - inputDiv = gutter.nextSibling, input = inputDiv.firstChild, - lineSpace = inputDiv.nextSibling.firstChild, cursor = lineSpace.firstChild, lineDiv = cursor.nextSibling; + lineSpace = gutter.nextSibling.firstChild, + cursor = lineSpace.firstChild, lineDiv = cursor.nextSibling; if (options.tabindex != null) input.tabindex = options.tabindex; if (!options.gutter && !options.lineNumbers) gutter.style.display = "none"; + // Check for problem with IE innerHTML not working when we have a + // P (or similar) parent node. + try { stringWidth("x"); } + catch (e) { + if (e.message.match(/unknown runtime/i)) + e = new Error("A CodeMirror inside a P-style element does not work in Internet Explorer. (innerHTML bug)"); + throw e; + } + // Delayed object wrap timeouts, making sure only one is active. blinker holds an interval. var poll = new Delayed(), highlight = new Delayed(), blinker; @@ -46,7 +59,7 @@ var CodeMirror = (function() { // (see Line constructor), work an array of lines that should be // parsed, and history the undo history (instance of History // constructor). - var mode, lines = [new Line("")], work, history = new History(), focused; + var mode, lines = [new Line("")], work, focused; loadMode(); // The selection. These are always maintained to point at valid // positions. Inverted is used to remember that the user is @@ -56,10 +69,10 @@ var CodeMirror = (function() { // whether the user is holding shift. reducedSelection is a hack // to get around the fact that we can't create inverted // selections. See below. - var shiftSelecting, reducedSelection; + var shiftSelecting, reducedSelection, lastClick, lastDoubleClick, draggingText; // Variables used by startOperation/endOperation to track what // happened during the operation. - var updateInput, changes, textChanged, selectionChanged, leaveInputAlone; + var updateInput, changes, textChanged, selectionChanged, leaveInputAlone, gutterDirty; // Current visible range (may be bigger than the view window). var showingFrom = 0, showingTo = 0, lastHeight = 0, curKeyId = null; // editing will hold an object describing the things we put in the @@ -67,35 +80,46 @@ var CodeMirror = (function() { // bracketHighlighted is used to remember that a backet has been // marked. var editing, bracketHighlighted; + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + var maxLine = "", maxWidth; - // Initialize the content. Somewhat hacky (delayed prepareInput) - // to work around browser issues. + // Initialize the content. operation(function(){setValue(options.value || ""); updateInput = false;})(); - setTimeout(prepareInput, 20); + var history = new History(); // Register our event handlers. - connect(wrapper, "mousedown", operation(onMouseDown)); + connect(scroller, "mousedown", operation(onMouseDown)); + connect(scroller, "dblclick", operation(onDoubleClick)); + connect(lineSpace, "dragstart", onDragStart); // Gecko browsers fire contextmenu *after* opening the menu, at // which point we can't mess with it anymore. Context menu is // handled in onMouseDown for Gecko. - if (!gecko) connect(wrapper, "contextmenu", operation(onContextMenu)); - connect(code, "dblclick", operation(onDblClick)); - connect(wrapper, "scroll", function() {updateDisplay([]); if (options.onScroll) options.onScroll(instance);}); + if (!gecko) connect(scroller, "contextmenu", onContextMenu); + connect(scroller, "scroll", function() { + updateDisplay([]); + if (options.fixedGutter) gutter.style.left = scroller.scrollLeft + "px"; + if (options.onScroll) options.onScroll(instance); + }); connect(window, "resize", function() {updateDisplay(true);}); connect(input, "keyup", operation(onKeyUp)); + connect(input, "input", function() {fastPoll(curKeyId);}); connect(input, "keydown", operation(onKeyDown)); connect(input, "keypress", operation(onKeyPress)); connect(input, "focus", onFocus); connect(input, "blur", onBlur); - connect(wrapper, "dragenter", function(e){e.stop();}); - connect(wrapper, "dragover", function(e){e.stop();}); - connect(wrapper, "drop", operation(onDrop)); - connect(wrapper, "paste", function(){input.focus(); fastPoll();}); + connect(scroller, "dragenter", e_stop); + connect(scroller, "dragover", e_stop); + connect(scroller, "drop", operation(onDrop)); + connect(scroller, "paste", function(){focusInput(); fastPoll();}); connect(input, "paste", function(){fastPoll();}); connect(input, "cut", function(){fastPoll();}); - if (document.activeElement == input) onFocus(); + // IE throws unspecified error in certain cases, when + // trying to access activeElement before onload + var hasFocus; try { hasFocus = (targetDocument.activeElement == input); } catch(e) { } + if (hasFocus) setTimeout(onFocus, 20); else onBlur(); function isLine(l) {return l >= 0 && l < lines.length;} @@ -104,27 +128,37 @@ var CodeMirror = (function() { // range checking and/or clipping. operation is used to wrap the // call so that changes it makes are tracked, and the display is // updated afterwards. - var instance = { + var instance = wrapper.CodeMirror = { getValue: getValue, setValue: operation(setValue), getSelection: getSelection, replaceSelection: operation(replaceSelection), - focus: function(){input.focus(); onFocus(); fastPoll();}, + focus: function(){focusInput(); onFocus(); fastPoll();}, setOption: function(option, value) { options[option] = value; - if (option == "lineNumbers" || option == "gutter") gutterChanged(); + if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber") + operation(gutterChanged)(); else if (option == "mode" || option == "indentUnit") loadMode(); + else if (option == "readOnly" && value == "nocursor") input.blur(); + else if (option == "theme") scroller.className = scroller.className.replace(/cm-s-\w+/, "cm-s-" + value); }, getOption: function(option) {return options[option];}, undo: operation(undo), redo: operation(redo), - indentLine: operation(function(n) {if (isLine(n)) indentLine(n, "smart");}), + indentLine: operation(function(n, dir) { + if (isLine(n)) indentLine(n, dir == null ? "smart" : dir ? "add" : "subtract"); + }), historySize: function() {return {undo: history.done.length, redo: history.undone.length};}, + clearHistory: function() {history = new History();}, matchBrackets: operation(function(){matchBrackets(true);}), getTokenAt: function(pos) { pos = clipPos(pos); return lines[pos.line].getTokenAt(mode, getStateBefore(pos.line), pos.ch); }, + getStateAfter: function(line) { + line = clipLine(line == null ? lines.length - 1: line); + return getStateBefore(line + 1); + }, cursorCoords: function(start){ if (start == null) start = sel.inverted; return pageCoords(start ? sel.from : sel.to); @@ -132,22 +166,41 @@ var CodeMirror = (function() { charCoords: function(pos){return pageCoords(clipPos(pos));}, coordsChar: function(coords) { var off = eltOffset(lineSpace); - var line = Math.min(showingTo - 1, showingFrom + Math.floor(coords.y / lineHeight())); - return clipPos({line: line, ch: charFromX(clipLine(line), coords.x)}); + var line = clipLine(Math.min(lines.length - 1, showingFrom + Math.floor((coords.y - off.top) / lineHeight()))); + return clipPos({line: line, ch: charFromX(clipLine(line), coords.x - off.left)}); }, getSearchCursor: function(query, pos, caseFold) {return new SearchCursor(query, pos, caseFold);}, - markText: operation(function(a, b, c){return operation(markText(a, b, c));}), - setMarker: addGutterMarker, - clearMarker: removeGutterMarker, + markText: operation(markText), + setMarker: operation(addGutterMarker), + clearMarker: operation(removeGutterMarker), setLineClass: operation(setLineClass), lineInfo: lineInfo, - addWidget: function(pos, node, scroll) { - var pos = localCoords(clipPos(pos), true); - node.style.top = (showingFrom * lineHeight() + pos.yBot + paddingTop()) + "px"; - node.style.left = (pos.x + paddingLeft()) + "px"; + addWidget: function(pos, node, scroll, vert, horiz) { + pos = localCoords(clipPos(pos)); + var top = pos.yBot, left = pos.x; + node.style.position = "absolute"; code.appendChild(node); + if (vert == "over") top = pos.y; + else if (vert == "near") { + var vspace = Math.max(scroller.offsetHeight, lines.length * lineHeight()), + hspace = Math.max(code.clientWidth, lineSpace.clientWidth) - paddingLeft(); + if (pos.yBot + node.offsetHeight > vspace && pos.y > node.offsetHeight) + top = pos.y - node.offsetHeight; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; + } + node.style.top = (top + paddingTop()) + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = code.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (code.clientWidth - node.offsetWidth) / 2; + node.style.left = (left + paddingLeft()) + "px"; + } if (scroll) - scrollIntoView(pos.x, pos.yBot, pos.x + node.offsetWidth, pos.yBot + node.offsetHeight); + scrollIntoView(left, top, left + node.offsetWidth, top + node.offsetHeight); }, lineCount: function() {return lines.length;}, @@ -171,18 +224,30 @@ var CodeMirror = (function() { replaceRange: operation(replaceRange), getRange: function(from, to) {return getRange(clipPos(from), clipPos(to));}, + coordsFromIndex: function(index) { + var total = lines.length, pos = 0, line, ch, len; + + for (line = 0; line < total; line++) { + len = lines[line].text.length + 1; + if (pos + len > index) { ch = index - pos; break; } + pos += len; + } + return clipPos({line: line, ch: ch}); + }, + operation: function(f){return operation(f)();}, refresh: function(){updateDisplay(true);}, getInputField: function(){return input;}, - getWrapperElement: function(){return wrapper;} + getWrapperElement: function(){return wrapper;}, + getScrollerElement: function(){return scroller;}, + getGutterElement: function(){return gutter;} }; function setValue(code) { - history = null; var top = {line: 0, ch: 0}; updateLines(top, {line: lines.length - 1, ch: lines[lines.length-1].text.length}, splitLines(code), top, top); - history = new History(); + updateInput = true; } function getValue(code) { var text = []; @@ -192,38 +257,70 @@ var CodeMirror = (function() { } function onMouseDown(e) { + // Check whether this is a click in a widget + for (var n = e_target(e); n != wrapper; n = n.parentNode) + if (n.parentNode == code && n != mover) return; + // First, see if this is a click in the gutter - for (var n = e.target(); n != wrapper; n = n.parentNode) + for (var n = e_target(e); n != wrapper; n = n.parentNode) if (n.parentNode == gutterText) { if (options.onGutterClick) - options.onGutterClick(instance, indexOf(gutterText.childNodes, n) + showingFrom); - return e.stop(); + options.onGutterClick(instance, indexOf(gutterText.childNodes, n) + showingFrom, e); + return e_preventDefault(e); } - if (gecko && e.button() == 3) onContextMenu(e); - if (e.button() != 1) return; + var start = posFromMouse(e); + + switch (e_button(e)) { + case 3: + if (gecko && !mac) onContextMenu(e); + return; + case 2: + if (start) setCursor(start.line, start.ch, true); + return; + } // For button 1, if it was clicked inside the editor // (posFromMouse returning non-null), we have to adjust the // selection. - var start = posFromMouse(e), last = start, going; - if (!start) {if (e.target() == wrapper) e.stop(); return;} - setCursor(start.line, start.ch, false); + if (!start) {if (e_target(e) == scroller) e_preventDefault(e); return;} if (!focused) onFocus(); - e.stop(); - // And then we have to see if it's a drag event, in which case - // the dragged-over text must be selected. - function end() { - input.focus(); - updateInput = true; - move(); up(); + + var now = +new Date; + if (lastDoubleClick > now - 400) { + e_preventDefault(e); + return selectLine(start.line); + } else if (lastClick > now - 400) { + lastDoubleClick = now; + e_preventDefault(e); + return selectWordAt(start); + } else { lastClick = now; } + + var last = start, going; + if (dragAndDrop && !posEq(sel.from, sel.to) && + !posLess(start, sel.from) && !posLess(sel.to, start)) { + // Let the drag handler handle this. + var up = connect(targetDocument, "mouseup", operation(function(e2) { + draggingText = false; + up(); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + setCursor(start.line, start.ch, true); + focusInput(); + } + }), true); + draggingText = true; + return; } + e_preventDefault(e); + setCursor(start.line, start.ch, true); + function extend(e) { var cur = posFromMouse(e, true); if (cur && !posEq(cur, last)) { if (!focused) onFocus(); last = cur; - setSelection(start, cur); + setSelectionUser(start, cur); updateInput = false; var visible = visibleLines(); if (cur.line >= visible.to || cur.line < visible.from) @@ -231,68 +328,95 @@ var CodeMirror = (function() { } } - var move = connect(document, "mousemove", operation(function(e) { + var move = connect(targetDocument, "mousemove", operation(function(e) { clearTimeout(going); - e.stop(); + e_preventDefault(e); extend(e); }), true); - var up = connect(document, "mouseup", operation(function(e) { + var up = connect(targetDocument, "mouseup", operation(function(e) { clearTimeout(going); var cur = posFromMouse(e); - if (cur) setSelection(start, cur); - e.stop(); - end(); + if (cur) setSelectionUser(start, cur); + e_preventDefault(e); + focusInput(); + updateInput = true; + move(); up(); }), true); } - function onDblClick(e) { - var pos = posFromMouse(e); - if (!pos) return; - selectWordAt(pos); - e.stop(); + function onDoubleClick(e) { + var start = posFromMouse(e); + if (!start) return; + lastDoubleClick = +new Date; + e_preventDefault(e); + selectWordAt(start); } function onDrop(e) { - var pos = posFromMouse(e, true), files = e.e.dataTransfer.files; + e.preventDefault(); + var pos = posFromMouse(e, true), files = e.dataTransfer.files; if (!pos || options.readOnly) return; if (files && files.length && window.FileReader && window.File) { - var n = files.length, text = Array(n), read = 0; - for (var i = 0; i < n; ++i) loadFile(files[i], i); function loadFile(file, i) { var reader = new FileReader; reader.onload = function() { text[i] = reader.result; - if (++read == n) replaceRange(text.join(""), clipPos(pos), clipPos(pos)); + if (++read == n) { + pos = clipPos(pos); + var end = replaceRange(text.join(""), pos, pos); + setSelectionUser(pos, end); + } }; reader.readAsText(file); } + var n = files.length, text = Array(n), read = 0; + for (var i = 0; i < n; ++i) loadFile(files[i], i); } else { try { - var text = e.e.dataTransfer.getData("Text"); - if (text) replaceRange(text, pos, pos); + var text = e.dataTransfer.getData("Text"); + if (text) { + var end = replaceRange(text, pos, pos); + var curFrom = sel.from, curTo = sel.to; + setSelectionUser(pos, end); + if (draggingText) replaceRange("", curFrom, curTo); + focusInput(); + } } catch(e){} } } + function onDragStart(e) { + var txt = getSelection(); + // This will reset escapeElement + htmlEscape(txt); + e.dataTransfer.setDragImage(escapeElement, 0, 0); + e.dataTransfer.setData("Text", txt); + } function onKeyDown(e) { if (!focused) onFocus(); - var code = e.e.keyCode; + var code = e.keyCode; + // IE does strange things with escape. + if (ie && code == 27) { e.returnValue = false; } // Tries to detect ctrl on non-mac, cmd on mac. - var mod = (mac ? e.e.metaKey : e.e.ctrlKey) && !e.e.altKey, anyMod = e.e.ctrlKey || e.e.altKey || e.e.metaKey; - if (code == 16 || e.e.shiftKey) shiftSelecting = shiftSelecting || (sel.inverted ? sel.to : sel.from); + var mod = (mac ? e.metaKey : e.ctrlKey) && !e.altKey, anyMod = e.ctrlKey || e.altKey || e.metaKey; + if (code == 16 || e.shiftKey) shiftSelecting = shiftSelecting || (sel.inverted ? sel.to : sel.from); else shiftSelecting = null; // First give onKeyEvent option a chance to handle this. - if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e.e))) return; + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; - if (code == 33 || code == 34) {scrollPage(code == 34); return e.stop();} // page up/down - if (mod && (code == 36 || code == 35)) {scrollEnd(code == 36); return e.stop();} // ctrl-home/end - if (mod && code == 65) {selectAll(); return e.stop();} // ctrl-a + if (code == 33 || code == 34) {scrollPage(code == 34); return e_preventDefault(e);} // page up/down + if (mod && ((code == 36 || code == 35) || // ctrl-home/end + mac && (code == 38 || code == 40))) { // cmd-up/down + scrollEnd(code == 36 || code == 38); return e_preventDefault(e); + } + if (mod && code == 65) {selectAll(); return e_preventDefault(e);} // ctrl-a if (!options.readOnly) { if (!anyMod && code == 13) {return;} // enter - if (!anyMod && code == 9 && handleTab(e.e.shiftKey)) return e.stop(); // tab - if (mod && code == 90) {undo(); return e.stop();} // ctrl-z - if (mod && ((e.e.shiftKey && code == 90) || code == 89)) {redo(); return e.stop();} // ctrl-shift-z, ctrl-y + if (!anyMod && code == 9 && handleTab(e.shiftKey)) return e_preventDefault(e); // tab + if (mod && code == 90) {undo(); return e_preventDefault(e);} // ctrl-z + if (mod && ((e.shiftKey && code == 90) || code == 89)) {redo(); return e_preventDefault(e);} // ctrl-shift-z, ctrl-y } + if (code == 36) { if (options.smartHome) { smartHome(); return e_preventDefault(e); } } // Key id to use in the movementKeys map. We also pass it to // fastPoll in order to 'self learn'. We need this because @@ -300,51 +424,60 @@ var CodeMirror = (function() { // its start when it is inverted and a movement key is pressed // (and later restore it again), shouldn't be used for // non-movement keys. - curKeyId = (mod ? "c" : "") + code; - if (sel.inverted && movementKeys.hasOwnProperty(curKeyId)) { + curKeyId = (mod ? "c" : "") + (e.altKey ? "a" : "") + code; + if (sel.inverted && movementKeys[curKeyId] === true) { var range = selRange(input); if (range) { reducedSelection = {anchor: range.start}; setSelRange(input, range.start, range.start); } } + // Don't save the key as a movementkey unless it had a modifier + if (!mod && !e.altKey) curKeyId = null; fastPoll(curKeyId); } function onKeyUp(e) { + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; if (reducedSelection) { reducedSelection = null; updateInput = true; } - if (e.e.keyCode == 16) shiftSelecting = null; + if (e.keyCode == 16) shiftSelecting = null; } function onKeyPress(e) { - if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e.e))) return; + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; if (options.electricChars && mode.electricChars) { - var ch = String.fromCharCode(e.e.charCode == null ? e.e.keyCode : e.e.charCode); + var ch = String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode); if (mode.electricChars.indexOf(ch) > -1) setTimeout(operation(function() {indentLine(sel.to.line, "smart");}), 50); } - var code = e.e.keyCode; + var code = e.keyCode; // Re-stop tab and enter. Necessary on some browsers. - if (code == 13) {handleEnter(); e.stop();} - else if (code == 9 && options.tabMode != "default") e.stop(); + if (code == 13) {if (!options.readOnly) handleEnter(); e_preventDefault(e);} + else if (!e.ctrlKey && !e.altKey && !e.metaKey && code == 9 && options.tabMode != "default") e_preventDefault(e); else fastPoll(curKeyId); } function onFocus() { - if (!focused && options.onFocus) options.onFocus(instance); - focused = true; + if (options.readOnly == "nocursor") return; + if (!focused) { + if (options.onFocus) options.onFocus(instance); + focused = true; + if (wrapper.className.search(/\bCodeMirror-focused\b/) == -1) + wrapper.className += " CodeMirror-focused"; + if (!leaveInputAlone) prepareInput(); + } slowPoll(); - if (wrapper.className.search(/\bCodeMirror-focused\b/) == -1) - wrapper.className += " CodeMirror-focused"; restartBlink(); } function onBlur() { - if (focused && options.onBlur) options.onBlur(instance); + if (focused) { + if (options.onBlur) options.onBlur(instance); + focused = false; + wrapper.className = wrapper.className.replace(" CodeMirror-focused", ""); + } clearInterval(blinker); - shiftSelecting = null; - focused = false; - wrapper.className = wrapper.className.replace(" CodeMirror-focused", ""); + setTimeout(function() {if (!focused) shiftSelecting = null;}, 150); } // Replace the range from from to to by the strings in newText. @@ -367,12 +500,18 @@ var CodeMirror = (function() { var pos = clipPos({line: change.start + change.old.length - 1, ch: editEnd(replaced[replaced.length-1], change.old[change.old.length-1])}); updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: lines[end-1].text.length}, change.old, pos, pos); + updateInput = true; } } function undo() {unredoHelper(history.done, history.undone);} function redo() {unredoHelper(history.undone, history.done);} function updateLinesNoUndo(from, to, newText, selFrom, selTo) { + var recomputeMaxLength = false, maxLineLength = maxLine.length; + for (var i = from.line; i <= to.line; ++i) { + if (lines[i].text.length == maxLineLength) {recomputeMaxLength = true; break;} + } + var nlines = to.line - from.line, firstLine = lines[from.line], lastLine = lines[to.line]; // First adjust the line structure, taking some care to leave highlighting intact. if (firstLine == lastLine) { @@ -381,24 +520,46 @@ var CodeMirror = (function() { else { lastLine = firstLine.split(to.ch, newText[newText.length-1]); var spliceargs = [from.line + 1, nlines]; - firstLine.replace(from.ch, firstLine.text.length, newText[0]); - for (var i = 1, e = newText.length - 1; i < e; ++i) spliceargs.push(new Line(newText[i])); + firstLine.replace(from.ch, null, newText[0]); + for (var i = 1, e = newText.length - 1; i < e; ++i) + spliceargs.push(Line.inheritMarks(newText[i], firstLine)); spliceargs.push(lastLine); lines.splice.apply(lines, spliceargs); } } else if (newText.length == 1) { - firstLine.replace(from.ch, firstLine.text.length, newText[0] + lastLine.text.slice(to.ch)); + firstLine.replace(from.ch, null, newText[0]); + lastLine.replace(null, to.ch, ""); + firstLine.append(lastLine); lines.splice(from.line + 1, nlines); } else { var spliceargs = [from.line + 1, nlines - 1]; - firstLine.replace(from.ch, firstLine.text.length, newText[0]); - lastLine.replace(0, to.ch, newText[newText.length-1]); - for (var i = 1, e = newText.length - 1; i < e; ++i) spliceargs.push(new Line(newText[i])); + firstLine.replace(from.ch, null, newText[0]); + lastLine.replace(null, to.ch, newText[newText.length-1]); + for (var i = 1, e = newText.length - 1; i < e; ++i) + spliceargs.push(Line.inheritMarks(newText[i], firstLine)); lines.splice.apply(lines, spliceargs); } + + for (var i = from.line, e = i + newText.length; i < e; ++i) { + var l = lines[i].text; + if (l.length > maxLineLength) { + maxLine = l; maxLineLength = l.length; maxWidth = null; + recomputeMaxLength = false; + } + } + if (recomputeMaxLength) { + maxLineLength = 0; maxLine = ""; maxWidth = null; + for (var i = 0, e = lines.length; i < e; ++i) { + var l = lines[i].text; + if (l.length > maxLineLength) { + maxLineLength = l.length; maxLine = l; + } + } + } + // Add these lines to the work array, so that they will be // highlighted. Adjust work lines if lines were added/removed. var newWork = [], lendiff = newText.length - nlines - 1; @@ -407,12 +568,17 @@ var CodeMirror = (function() { if (task < from.line) newWork.push(task); else if (task > to.line) newWork.push(task + lendiff); } - if (newText.length) newWork.push(from.line); + if (newText.length < 5) { + highlightLines(from.line, from.line + newText.length); + newWork.push(from.line + newText.length); + } else { + newWork.push(from.line); + } work = newWork; startWorker(100); // Remember that these lines changed, for updating the display changes.push({from: from.line, to: to.line + 1, diff: lendiff}); - textChanged = true; + textChanged = {from: from, to: to, text: newText}; // Update the selection function updateLine(n) {return n <= Math.min(to.line, to.line + lendiff) ? n : n + lendiff;} @@ -483,7 +649,10 @@ var CodeMirror = (function() { function p() { startOperation(); var changed = readInput(); - if (changed == "moved" && keyId) movementKeys[keyId] = true; + if (changed && keyId) { + if (changed == "moved" && movementKeys[keyId] == null) movementKeys[keyId] = true; + if (changed == "changed") movementKeys[keyId] = false; + } if (!changed && !missed) {missed = true; poll.set(80, p);} else {pollingFast = false; slowPoll();} endOperation(); @@ -495,13 +664,12 @@ var CodeMirror = (function() { // to the data in the editing variable, and updates the editor // content or cursor if something changed. function readInput() { + if (leaveInputAlone || !focused) return; var changed = false, text = input.value, sr = selRange(input); if (!sr) return false; var changed = editing.text != text, rs = reducedSelection; var moved = changed || sr.start != editing.start || sr.end != (rs ? editing.start : editing.end); - if (reducedSelection && !moved && sel.from.line == 0 && sel.from.ch == 0) - reducedSelection = null; - else if (!moved) return false; + if (!moved && !rs) return false; if (changed) { shiftSelecting = reducedSelection = null; if (options.readOnly) {updateInput = true; return "changed";} @@ -524,13 +692,10 @@ var CodeMirror = (function() { // so that you can, for example, press shift-up at the start of // your selection and have the right thing happen. if (rs) { - from = sr.start == rs.anchor ? to : from; - to = shiftSelecting ? sel.to : sr.start == rs.anchor ? from : to; - if (!posLess(from, to)) { - reducedSelection = null; - sel.inverted = false; - var tmp = from; from = to; to = tmp; - } + var head = sr.start == rs.anchor ? to : from; + var tail = shiftSelecting ? sel.to : sr.start == rs.anchor ? from : to; + if (sel.inverted = posLess(head, tail)) { from = head; to = tail; } + else { reducedSelection = null; from = tail; to = head; } } // In some cases (cursor on same line as before), we don't have @@ -550,8 +715,8 @@ var CodeMirror = (function() { var ch = nl > -1 ? start - nl : start, endline = editing.to - 1, edend = editing.text.length; for (;;) { c = editing.text.charAt(edend); + if (text.charAt(end) != c) {++end; ++edend; break;} if (c == "\n") endline--; - if (text.charAt(end) != c) {++end; ++edend; break;} if (edend <= start || end <= start) break; --end; --edend; } @@ -580,22 +745,36 @@ var CodeMirror = (function() { editing = {text: text, from: from, to: to, start: startch, end: endch}; setSelRange(input, startch, reducedSelection ? startch : endch); } + function focusInput() { + if (options.readOnly != "nocursor") input.focus(); + } + function scrollEditorIntoView() { + if (!cursor.getBoundingClientRect) return; + var rect = cursor.getBoundingClientRect(); + var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight); + if (rect.top < 0 || rect.bottom > winH) cursor.scrollIntoView(); + } function scrollCursorIntoView() { var cursor = localCoords(sel.inverted ? sel.from : sel.to); return scrollIntoView(cursor.x, cursor.y, cursor.x, cursor.yBot); } function scrollIntoView(x1, y1, x2, y2) { - var pl = paddingLeft(), pt = paddingTop(); + var pl = paddingLeft(), pt = paddingTop(), lh = lineHeight(); y1 += pt; y2 += pt; x1 += pl; x2 += pl; - var screen = wrapper.clientHeight, screentop = wrapper.scrollTop, scrolled = false, result = true; - if (y1 < screentop) {wrapper.scrollTop = Math.max(0, y1 - 10); scrolled = true;} - else if (y2 > screentop + screen) {wrapper.scrollTop = y2 + 10 - screen; scrolled = true;} + var screen = scroller.clientHeight, screentop = scroller.scrollTop, scrolled = false, result = true; + if (y1 < screentop) {scroller.scrollTop = Math.max(0, y1 - 2*lh); scrolled = true;} + else if (y2 > screentop + screen) {scroller.scrollTop = y2 + lh - screen; scrolled = true;} - var screenw = wrapper.clientWidth, screenleft = wrapper.scrollLeft; - if (x1 < screenleft) {wrapper.scrollLeft = Math.max(0, x1 - 10); scrolled = true;} + var screenw = scroller.clientWidth, screenleft = scroller.scrollLeft; + var gutterw = options.fixedGutter ? gutter.clientWidth : 0; + if (x1 < screenleft + gutterw) { + if (x1 < 50) x1 = 0; + scroller.scrollLeft = Math.max(0, x1 - 10 - gutterw); + scrolled = true; + } else if (x2 > screenw + screenleft) { - wrapper.scrollLeft = x2 + 10 - screenw; + scroller.scrollLeft = x2 + 10 - screenw; scrolled = true; if (x2 > code.clientWidth) result = false; } @@ -604,15 +783,15 @@ var CodeMirror = (function() { } function visibleLines() { - var lh = lineHeight(), top = wrapper.scrollTop - paddingTop(); + var lh = lineHeight(), top = scroller.scrollTop - paddingTop(); return {from: Math.min(lines.length, Math.max(0, Math.floor(top / lh))), - to: Math.min(lines.length, Math.ceil((top + wrapper.clientHeight) / lh))}; + to: Math.min(lines.length, Math.ceil((top + scroller.clientHeight) / lh))}; } // Uses a set of changes plus the current scroll position to // determine which DOM updates have to be made, and makes the // updates. function updateDisplay(changes) { - if (!wrapper.clientWidth) { + if (!scroller.clientWidth) { showingFrom = showingTo = 0; return; } @@ -629,7 +808,7 @@ var CodeMirror = (function() { intact2.push(range); else { if (change.from > range.from) - intact2.push({from: range.from, to: change.from, domStart: range.domStart}) + intact2.push({from: range.from, to: change.from, domStart: range.domStart}); if (change.to < range.to) intact2.push({from: change.to + diff, to: range.to + diff, domStart: range.domStart + (change.to - range.from)}); @@ -659,6 +838,7 @@ var CodeMirror = (function() { if (domPos != domEnd || pos != to) { changedLines += Math.abs(to - pos); updates.push({from: pos, to: to, domSize: domEnd - domPos, domStart: domPos}); + if (to - pos != domEnd - domPos) gutterDirty = true; } if (!updates.length) return; @@ -674,13 +854,23 @@ var CodeMirror = (function() { // Position the mover div to align with the lines it's supposed // to be showing (which will cover the visible display) - var different = from != showingFrom || to != showingTo || lastHeight != wrapper.clientHeight; + var different = from != showingFrom || to != showingTo || lastHeight != scroller.clientHeight; showingFrom = from; showingTo = to; mover.style.top = (from * lineHeight()) + "px"; if (different) { - lastHeight = wrapper.clientHeight; + lastHeight = scroller.clientHeight; code.style.height = (lines.length * lineHeight() + 2 * paddingTop()) + "px"; - updateGutter(); + } + if (different || gutterDirty) updateGutter(); + + if (maxWidth == null) maxWidth = stringWidth(maxLine); + if (maxWidth > scroller.clientWidth) { + lineSpace.style.width = maxWidth + "px"; + // Needed to prevent odd wrapping/hiding of widgets placed in here. + code.style.width = ""; + code.style.width = scroller.scrollWidth + "px"; + } else { + lineSpace.style.width = code.style.width = ""; } // Since this is all rather error prone, it is honoured with the @@ -712,7 +902,7 @@ var CodeMirror = (function() { // there .innerHTML on PRE nodes is dumb, and discards // whitespace. var sfrom = sel.from.line, sto = sel.to.line, off = 0, - scratch = badInnerHTML && document.createElement("div"); + scratch = badInnerHTML && targetDocument.createElement("div"); for (var i = 0, e = updates.length; i < e; ++i) { var rec = updates[i]; var extra = (rec.to - rec.from) - rec.domSize; @@ -722,7 +912,7 @@ var CodeMirror = (function() { lineDiv.removeChild(nodeAfter ? nodeAfter.previousSibling : lineDiv.lastChild); else if (extra) { for (var j = Math.max(0, extra); j > 0; --j) - lineDiv.insertBefore(document.createElement("pre"), nodeAfter); + lineDiv.insertBefore(targetDocument.createElement("pre"), nodeAfter); for (var j = Math.max(0, -extra); j > 0; --j) lineDiv.removeChild(nodeAfter ? nodeAfter.previousSibling : lineDiv.lastChild); } @@ -753,10 +943,10 @@ var CodeMirror = (function() { function updateGutter() { if (!options.gutter && !options.lineNumbers) return; - var hText = mover.offsetHeight, hEditor = wrapper.clientHeight; + var hText = mover.offsetHeight, hEditor = scroller.clientHeight; gutter.style.height = (hText - hEditor < 2 ? hEditor : hText) + "px"; var html = []; - for (var i = showingFrom; i < showingTo; ++i) { + for (var i = showingFrom; i < Math.max(showingTo, showingFrom + 1); ++i) { var marker = lines[i].gutterMarker; var text = options.lineNumbers ? i + options.firstLineNumber : null; if (marker && marker.text) @@ -769,37 +959,43 @@ var CodeMirror = (function() { gutterText.innerHTML = html.join(""); var minwidth = String(lines.length).length, firstNode = gutterText.firstChild, val = eltText(firstNode), pad = ""; while (val.length + pad.length < minwidth) pad += "\u00a0"; - if (pad) firstNode.insertBefore(document.createTextNode(pad), firstNode.firstChild); + if (pad) firstNode.insertBefore(targetDocument.createTextNode(pad), firstNode.firstChild); gutter.style.display = ""; lineSpace.style.marginLeft = gutter.offsetWidth + "px"; + gutterDirty = false; } function updateCursor() { - var head = sel.inverted ? sel.from : sel.to; - var x = charX(head.line, head.ch) + "px", y = (head.line - showingFrom) * lineHeight() + "px"; - inputDiv.style.top = y; inputDiv.style.left = x; + var head = sel.inverted ? sel.from : sel.to, lh = lineHeight(); + var x = charX(head.line, head.ch); + var top = head.line * lh - scroller.scrollTop; + inputDiv.style.top = Math.max(Math.min(top, scroller.offsetHeight), 0) + "px"; + inputDiv.style.left = (x - scroller.scrollLeft) + "px"; if (posEq(sel.from, sel.to)) { - cursor.style.top = y; cursor.style.left = x; + cursor.style.top = (head.line - showingFrom) * lh + "px"; + cursor.style.left = x + "px"; cursor.style.display = ""; } else cursor.style.display = "none"; } + function setSelectionUser(from, to) { + var sh = shiftSelecting && clipPos(shiftSelecting); + if (sh) { + if (posLess(sh, from)) from = sh; + else if (posLess(to, sh)) to = sh; + } + setSelection(from, to); + } // Update the selection. Last two args are only used by // updateLines, since they have to be expressed in the line // numbers before the update. function setSelection(from, to, oldFrom, oldTo) { if (posEq(sel.from, from) && posEq(sel.to, to)) return; - var sh = shiftSelecting && clipPos(shiftSelecting); if (posLess(to, from)) {var tmp = to; to = from; from = tmp;} - if (sh) { - if (posLess(sh, from)) from = sh; - else if (posLess(to, sh)) to = sh; - } - var startEq = posEq(sel.to, to), endEq = posEq(sel.from, from); if (posEq(from, to)) sel.inverted = false; - else if (startEq && !endEq) sel.inverted = true; - else if (endEq && !startEq) sel.inverted = false; + else if (posEq(from, sel.to)) sel.inverted = false; + else if (posEq(to, sel.from)) sel.inverted = true; // Some ugly logic used to only mark the lines that actually did // see a change in selection as changed, rather than the whole @@ -829,9 +1025,9 @@ var CodeMirror = (function() { sel.from = from; sel.to = to; selectionChanged = true; } - function setCursor(line, ch) { + function setCursor(line, ch, user) { var pos = clipPos({line: line, ch: ch || 0}); - setSelection(pos, pos); + (user ? setSelectionUser : setSelection)(pos, pos); } function clipLine(n) {return Math.max(0, Math.min(n, lines.length-1));} @@ -845,11 +1041,12 @@ var CodeMirror = (function() { } function scrollPage(down) { - var linesPerPage = Math.floor(wrapper.clientHeight / lineHeight()), head = sel.inverted ? sel.from : sel.to; - setCursor(head.line + (Math.max(linesPerPage - 1, 1) * (down ? 1 : -1)), head.ch); + var linesPerPage = Math.floor(scroller.clientHeight / lineHeight()), head = sel.inverted ? sel.from : sel.to; + setCursor(head.line + (Math.max(linesPerPage - 1, 1) * (down ? 1 : -1)), head.ch, true); } function scrollEnd(top) { - setCursor(top ? 0 : lines.length - 1); + var pos = top ? {line: 0, ch: 0} : {line: lines.length - 1, ch: lines[lines.length-1].text.length}; + setSelectionUser(pos, pos); } function selectAll() { var endLine = lines.length - 1; @@ -859,8 +1056,11 @@ var CodeMirror = (function() { var line = lines[pos.line].text; var start = pos.ch, end = pos.ch; while (start > 0 && /\w/.test(line.charAt(start - 1))) --start; - while (end < line.length - 1 && /\w/.test(line.charAt(end))) ++end; - setSelection({line: pos.line, ch: start}, {line: pos.line, ch: end}); + while (end < line.length && /\w/.test(line.charAt(end))) ++end; + setSelectionUser({line: pos.line, ch: start}, {line: pos.line, ch: end}); + } + function selectLine(line) { + setSelectionUser({line: line, ch: 0}, {line: line, ch: lines[line].text.length}); } function handleEnter() { replaceSelection("\n", "end"); @@ -868,12 +1068,17 @@ var CodeMirror = (function() { indentLine(sel.from.line, options.enterMode == "keep" ? "prev" : "smart"); } function handleTab(shift) { + function indentSelected(mode) { + if (posEq(sel.from, sel.to)) return indentLine(sel.from.line, mode); + var e = sel.to.line - (sel.to.ch ? 0 : 1); + for (var i = sel.from.line; i <= e; ++i) indentLine(i, mode); + } shiftSelecting = null; switch (options.tabMode) { case "default": return false; case "indent": - for (var i = sel.from.line, e = sel.to.line; i <= e; ++i) indentLine(i, "smart"); + indentSelected("smart"); break; case "classic": if (posEq(sel.from, sel.to)) { @@ -882,11 +1087,15 @@ var CodeMirror = (function() { break; } case "shift": - for (var i = sel.from.line, e = sel.to.line; i <= e; ++i) indentLine(i, shift ? "subtract" : "add"); + indentSelected(shift ? "subtract" : "add"); break; } return true; } + function smartHome() { + var firstNonWS = Math.max(0, lines[sel.from.line].text.search(/\S/)); + setCursor(sel.from.line, sel.from.ch <= firstNonWS && sel.from.ch ? 0 : firstNonWS, true); + } function indentLine(n, how) { if (how == "smart") { @@ -924,21 +1133,20 @@ var CodeMirror = (function() { for (var i = 0, l = lines.length; i < l; ++i) lines[i].stateAfter = null; work = [0]; + startWorker(); } function gutterChanged() { var visible = options.gutter || options.lineNumbers; gutter.style.display = visible ? "" : "none"; - if (visible) updateGutter(); + if (visible) gutterDirty = true; else lineDiv.parentNode.style.marginLeft = 0; } function markText(from, to, className) { from = clipPos(from); to = clipPos(to); - var accum = []; + var set = []; function add(line, from, to, className) { - var line = lines[line], mark = line.addMark(from, to, className); - mark.line = line; - accum.push(mark); + mark = lines[line].addMark(from, to, className, set); } if (from.line == to.line) add(from.line, from.ch, to.ch, className); else { @@ -948,30 +1156,51 @@ var CodeMirror = (function() { add(to.line, 0, to.ch, className); } changes.push({from: from.line, to: to.line + 1}); - return function() { - var start, end; - for (var i = 0; i < accum.length; ++i) { - var mark = accum[i], found = indexOf(lines, mark.line); - mark.line.removeMark(mark); - if (found > -1) { - if (start == null) start = found; - end = found; + return new TextMarker(set); + } + + function TextMarker(set) { this.set = set; } + TextMarker.prototype.clear = operation(function() { + for (var i = 0, e = this.set.length; i < e; ++i) { + var mk = this.set[i].marked; + for (var j = 0; j < mk.length; ++j) { + if (mk[j].set == this.set) mk.splice(j--, 1); + } + } + // We don't know the exact lines that changed. Refreshing is + // cheaper than finding them. + changes.push({from: 0, to: lines.length}); + }); + TextMarker.prototype.find = function() { + var from, to; + for (var i = 0, e = this.set.length; i < e; ++i) { + var line = this.set[i], mk = line.marked; + for (var j = 0; j < mk.length; ++j) { + var mark = mk[j]; + if (mark.set == this.set) { + if (mark.from != null || mark.to != null) { + var found = indexOf(lines, line); + if (found > -1) { + if (mark.from != null) from = {line: found, ch: mark.from}; + if (mark.to != null) to = {line: found, ch: mark.to}; + } + } } } - if (start != null) changes.push({from: start, to: end + 1}); - }; - } + } + return {from: from, to: to}; + }; function addGutterMarker(line, text, className) { if (typeof line == "number") line = lines[clipLine(line)]; line.gutterMarker = {text: text, style: className}; - updateGutter(); + gutterDirty = true; return line; } function removeGutterMarker(line) { if (typeof line == "number") line = lines[clipLine(line)]; line.gutterMarker = null; - updateGutter(); + gutterDirty = true; } function setLineClass(line, className) { if (typeof line == "number") { @@ -982,8 +1211,10 @@ var CodeMirror = (function() { var no = indexOf(lines, line); if (no == -1) return null; } - line.className = className; - changes.push({from: no, to: no + 1}); + if (line.className != className) { + line.className = className; + changes.push({from: no, to: no + 1}); + } return line; } @@ -1001,35 +1232,44 @@ var CodeMirror = (function() { return {line: n, text: line.text, markerText: marker && marker.text, markerClass: marker && marker.style}; } + function stringWidth(str) { + measure.innerHTML = "
x
"; + measure.firstChild.firstChild.firstChild.nodeValue = str; + return measure.firstChild.firstChild.offsetWidth || 10; + } // These are used to go from pixel positions to character - // positions, taking tabs into account. + // positions, taking varying character widths into account. function charX(line, pos) { - var text = lines[line].text, span = measure.firstChild; - if (text.lastIndexOf("\t", pos) == -1) return pos * charWidth(); - var old = span.firstChild.nodeValue; - try { - span.firstChild.nodeValue = text.slice(0, pos); - return span.offsetWidth; - } finally {span.firstChild.nodeValue = old;} + if (pos == 0) return 0; + measure.innerHTML = "
" + lines[line].getHTML(null, null, false, pos) + "
"; + return measure.firstChild.firstChild.offsetWidth; } function charFromX(line, x) { - var text = lines[line].text, cw = charWidth(); if (x <= 0) return 0; - if (text.indexOf("\t") == -1) return Math.min(text.length, Math.round(x / cw)); - var mspan = measure.firstChild, mtext = mspan.firstChild, old = mtext.nodeValue; - try { - mtext.nodeValue = text; - var from = 0, fromX = 0, to = text.length, toX = mspan.offsetWidth; - if (x > toX) return to; - for (;;) { - if (to - from <= 1) return (toX - x > x - fromX) ? from : to; - var middle = Math.ceil((from + to) / 2); - mtext.nodeValue = text.slice(0, middle); - var curX = mspan.offsetWidth; - if (curX > x) {to = middle; toX = curX;} - else {from = middle; fromX = curX;} - } - } finally {mtext.nodeValue = old;} + var lineObj = lines[line], text = lineObj.text; + function getX(len) { + measure.innerHTML = "
" + lineObj.getHTML(null, null, false, len) + "
"; + return measure.firstChild.firstChild.offsetWidth; + } + var from = 0, fromX = 0, to = text.length, toX; + // Guess a suitable upper bound for our search. + var estimated = Math.min(to, Math.ceil(x / stringWidth("x"))); + for (;;) { + var estX = getX(estimated); + if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2)); + else {toX = estX; to = estimated; break;} + } + if (x > toX) return to; + // Try to guess a suitable lower bound as well. + estimated = Math.floor(to * 0.8); estX = getX(estimated); + if (estX < x) {from = estimated; fromX = estX;} + // Do a binary search between these bounds. + for (;;) { + if (to - from <= 1) return (toX - x > x - fromX) ? from : to; + var middle = Math.ceil((from + to) / 2), middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX;} + else {from = middle; fromX = middleX;} + } } function localCoords(pos, inLineWrap) { @@ -1043,45 +1283,61 @@ var CodeMirror = (function() { function lineHeight() { var nlines = lineDiv.childNodes.length; - if (nlines) return lineDiv.offsetHeight / nlines; - else return measure.firstChild.offsetHeight || 1; + if (nlines) return (lineDiv.offsetHeight / nlines) || 1; + measure.innerHTML = "
x
"; + return measure.firstChild.offsetHeight || 1; } - function charWidth() {return (measure.firstChild.offsetWidth || 320) / 40;} function paddingTop() {return lineSpace.offsetTop;} function paddingLeft() {return lineSpace.offsetLeft;} function posFromMouse(e, liberal) { - var off = eltOffset(lineSpace), - x = e.pageX() - off.left, - y = e.pageY() - off.top; - if (!liberal && e.target() != lineSpace.parentNode && !(e.target() == wrapper && y > (lines.length * lineHeight()))) - for (var n = e.target(); n != lineDiv && n != cursor; n = n.parentNode) - if (!n || n == wrapper) return null; - var line = showingFrom + Math.floor(y / lineHeight()); - return clipPos({line: line, ch: charFromX(clipLine(line), x)}); + var offW = eltOffset(scroller, true), x, y; + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX; y = e.clientY; } catch (e) { return null; } + // This is a mess of a heuristic to try and determine whether a + // scroll-bar was clicked or not, and to return null if one was + // (and !liberal). + if (!liberal && (x - offW.left > scroller.clientWidth || y - offW.top > scroller.clientHeight)) + return null; + var offL = eltOffset(lineSpace, true); + var line = showingFrom + Math.floor((y - offL.top) / lineHeight()); + return clipPos({line: line, ch: charFromX(clipLine(line), x - offL.left)}); } function onContextMenu(e) { var pos = posFromMouse(e); if (!pos || window.opera) return; // Opera is difficult. if (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to)) - setCursor(pos.line, pos.ch); + operation(setCursor)(pos.line, pos.ch); var oldCSS = input.style.cssText; - input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.pageY() - 1) + - "px; left: " + (e.pageX() - 1) + "px; z-index: 1000; background: white; " + - "border-width: 0; outline: none; overflow: hidden;"; + inputDiv.style.position = "absolute"; + input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; " + + "border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + leaveInputAlone = true; var val = input.value = getSelection(); - input.focus(); - setSelRange(input, 0, val.length); - if (gecko) e.stop(); - leaveInputAlone = true; - setTimeout(function() { - if (input.value != val) operation(replaceSelection)(input.value, "end"); + focusInput(); + setSelRange(input, 0, input.value.length); + function rehide() { + var newVal = splitLines(input.value).join("\n"); + if (newVal != val) operation(replaceSelection)(newVal, "end"); + inputDiv.style.position = "relative"; input.style.cssText = oldCSS; leaveInputAlone = false; prepareInput(); slowPoll(); - }, 50); + } + + if (gecko) { + e_stop(e); + var mouseup = connect(window, "mouseup", function() { + mouseup(); + setTimeout(rehide, 20); + }, true); + } + else { + setTimeout(rehide, 50); + } } // Cursor-blinking @@ -1120,19 +1376,18 @@ var CodeMirror = (function() { } } } - for (var i = head.line, e = forward ? Math.min(i + 50, lines.length) : Math.max(0, i - 50); i != e; i+=d) { + for (var i = head.line, e = forward ? Math.min(i + 100, lines.length) : Math.max(-1, i - 100); i != e; i+=d) { var line = lines[i], first = i == head.line; var found = scan(line, first && forward ? pos + 1 : 0, first && !forward ? pos : line.text.length); - if (found) { - var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; - var one = markText({line: head.line, ch: pos}, {line: head.line, ch: pos+1}, style), - two = markText({line: i, ch: found.pos}, {line: i, ch: found.pos + 1}, style); - var clear = operation(function(){one(); two();}); - if (autoclear) setTimeout(clear, 800); - else bracketHighlighted = clear; - break; - } + if (found) break; } + if (!found) found = {pos: null, match: false}; + var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; + var one = markText({line: head.line, ch: pos}, {line: head.line, ch: pos+1}, style), + two = found.pos != null && markText({line: i, ch: found.pos}, {line: i, ch: found.pos + 1}, style); + var clear = operation(function(){one.clear(); two && two.clear();}); + if (autoclear) setTimeout(clear, 800); + else bracketHighlighted = clear; } // Finds the line to start with when starting a parse. Tries to @@ -1148,7 +1403,7 @@ var CodeMirror = (function() { if (line.stateAfter) return search; var indented = line.indentation(); if (minline == null || minindent > indented) { - minline = search; + minline = search - 1; minindent = indented; } } @@ -1163,11 +1418,21 @@ var CodeMirror = (function() { line.highlight(mode, state); line.stateAfter = copyState(mode, state); } - if (!lines[n].stateAfter) work.push(n); + changes.push({from: start, to: n}); + if (n < lines.length && !lines[n].stateAfter) work.push(n); return state; } + function highlightLines(start, end) { + var state = getStateBefore(start); + for (var i = start; i < end; ++i) { + var line = lines[i]; + line.highlight(mode, state); + line.stateAfter = copyState(mode, state); + } + } function highlightWorker() { var end = +new Date + options.workTime; + var foundWork = work.length; while (work.length) { if (!lines[showingFrom].stateAfter) var task = showingFrom; else var task = work.pop(); @@ -1176,20 +1441,29 @@ var CodeMirror = (function() { if (state) state = copyState(mode, state); else state = startState(mode); + var unchanged = 0, compare = mode.compareStates, realChange = false; for (var i = start, l = lines.length; i < l; ++i) { var line = lines[i], hadState = line.stateAfter; if (+new Date > end) { work.push(i); startWorker(options.workDelay); - changes.push({from: task, to: i}); + if (realChange) changes.push({from: task, to: i + 1}); return; } var changed = line.highlight(mode, state); + if (changed) realChange = true; line.stateAfter = copyState(mode, state); - if (hadState && !changed && line.text) break; + if (compare) { + if (hadState && compare(hadState, state)) break; + } else { + if (changed !== false || !hadState) unchanged = 0; + else if (++unchanged > 3) break; + } } - changes.push({from: task, to: i}); + if (realChange) changes.push({from: task, to: i + 1}); } + if (foundWork && options.onHighlightComplete) + options.onHighlightComplete(instance); } function startWorker(time) { if (!work.length) return; @@ -1207,24 +1481,29 @@ var CodeMirror = (function() { var reScroll = false; if (selectionChanged) reScroll = !scrollCursorIntoView(); if (changes.length) updateDisplay(changes); - else if (selectionChanged) updateCursor(); + else { + if (selectionChanged) updateCursor(); + if (gutterDirty) updateGutter(); + } if (reScroll) scrollCursorIntoView(); - if (selectionChanged) restartBlink(); + if (selectionChanged) {scrollEditorIntoView(); restartBlink();} // updateInput can be set to a boolean value to force/prevent an // update. - if (!leaveInputAlone && (updateInput === true || (updateInput !== false && selectionChanged))) + if (focused && !leaveInputAlone && + (updateInput === true || (updateInput !== false && selectionChanged))) prepareInput(); - if (selectionChanged && options.onCursorActivity) - options.onCursorActivity(instance); - if (textChanged && options.onChange) - options.onChange(instance); if (selectionChanged && options.matchBrackets) setTimeout(operation(function() { if (bracketHighlighted) {bracketHighlighted(); bracketHighlighted = null;} matchBrackets(false); }), 20); + var tc = textChanged; // textChanged can be reset by cursoractivity callback + if (selectionChanged && options.onCursorActivity) + options.onCursorActivity(instance); + if (tc && options.onChange && instance) + options.onChange(instance, tc); } var nestedOperation = 0; function operation(f) { @@ -1259,6 +1538,7 @@ var CodeMirror = (function() { var newmatch = line.match(query); if (newmatch) match = newmatch; else break; + start++; } } else { @@ -1338,9 +1618,21 @@ var CodeMirror = (function() { }, from: function() {if (this.atOccurrence) return copyPos(this.pos.from);}, - to: function() {if (this.atOccurrence) return copyPos(this.pos.to);} + to: function() {if (this.atOccurrence) return copyPos(this.pos.to);}, + + replace: function(newText) { + var self = this; + if (this.atOccurrence) + operation(function() { + self.pos.to = replaceRange(newText, self.pos.from, self.pos.to); + })(); + } }; + for (var ext in extensions) + if (extensions.propertyIsEnumerable(ext) && + !instance.propertyIsEnumerable(ext)) + instance[ext] = extensions[ext]; return instance; } // (end of function CodeMirror) @@ -1348,6 +1640,7 @@ var CodeMirror = (function() { CodeMirror.defaults = { value: "", mode: null, + theme: "default", indentUnit: 2, indentWithTabs: false, tabMode: "classic", @@ -1356,17 +1649,21 @@ var CodeMirror = (function() { onKeyEvent: null, lineNumbers: false, gutter: false, + fixedGutter: false, firstLineNumber: 1, readOnly: false, + smartHome: true, onChange: null, onCursorActivity: null, onGutterClick: null, + onHighlightComplete: null, onFocus: null, onBlur: null, onScroll: null, matchBrackets: false, workTime: 100, workDelay: 200, undoDepth: 40, - tabindex: null + tabindex: null, + document: window.document }; // Known modes, by name and by MIME @@ -1383,15 +1680,15 @@ var CodeMirror = (function() { spec = mimeModes[spec]; if (typeof spec == "string") var mname = spec, config = {}; - else + else if (spec != null) var mname = spec.name, config = spec; var mfactory = modes[mname]; if (!mfactory) { if (window.console) console.warn("No mode " + mname + " found, falling back to plain text."); return CodeMirror.getMode(options, "text/plain"); } - return mfactory(options, config); - } + return mfactory(options, config || {}); + }; CodeMirror.listModes = function() { var list = []; for (var m in modes) @@ -1401,10 +1698,15 @@ var CodeMirror = (function() { CodeMirror.listMIMEs = function() { var list = []; for (var m in mimeModes) - if (mimeModes.propertyIsEnumerable(m)) list.push(m); + if (mimeModes.propertyIsEnumerable(m)) list.push({mime: m, mode: mimeModes[m]}); return list; }; + var extensions = {}; + CodeMirror.defineExtension = function(name, func) { + extensions[name] = func; + }; + CodeMirror.fromTextArea = function(textarea, options) { if (!options) options = {}; options.value = textarea.value; @@ -1484,7 +1786,7 @@ var CodeMirror = (function() { if (ok) {++this.pos; return ch;} }, eatWhile: function(match) { - var start = this.start; + var start = this.pos; while (this.eat(match)){} return this.pos > start; }, @@ -1517,6 +1819,7 @@ var CodeMirror = (function() { }, current: function(){return this.string.slice(this.start, this.pos);} }; + CodeMirror.StringStream = StringStream; // Line objects. These hold state related to a line, including // highlighting info (the styles array). @@ -1526,10 +1829,23 @@ var CodeMirror = (function() { this.text = text; this.marked = this.gutterMarker = this.className = null; } + Line.inheritMarks = function(text, orig) { + var ln = new Line(text), mk = orig.marked; + if (mk) { + for (var i = 0; i < mk.length; ++i) { + if (mk[i].to == null) { + var newmk = ln.marked || (ln.marked = []), mark = mk[i]; + newmk.push({from: null, to: null, style: mark.style, set: mark.set}); + mark.set.push(ln); + } + } + } + return ln; + } Line.prototype = { // Replace a piece of a line, keeping the styles around it intact. - replace: function(from, to, text) { - var st = [], mk = this.marked; + replace: function(from, to_, text) { + var st = [], mk = this.marked, to = to_ == null ? this.text.length : to_; copyStyles(0, from, this.styles, st); if (text) st.push(text, null); copyStyles(to, this.text.length, this.styles, st); @@ -1538,39 +1854,86 @@ var CodeMirror = (function() { this.stateAfter = null; if (mk) { var diff = text.length - (to - from), end = this.text.length; - function fix(n) {return n <= Math.min(to, to + diff) ? n : n + diff;} + var changeStart = Math.min(from, from + diff); for (var i = 0; i < mk.length; ++i) { var mark = mk[i], del = false; - if (mark.from >= end) del = true; - else {mark.from = fix(mark.from); if (mark.to != null) mark.to = fix(mark.to);} - if (del || mark.from >= mark.to) {mk.splice(i, 1); i--;} + if (mark.from != null && mark.from >= end) del = true; + else { + if (mark.from != null && mark.from >= from) { + mark.from += diff; + if (mark.from <= 0) mark.from = from == null ? null : 0; + } + else if (to_ == null) mark.to = null; + if (mark.to != null && mark.to > from) { + mark.to += diff; + if (mark.to < 0) del = true; + } + } + if (del || (mark.from != null && mark.to != null && mark.from >= mark.to)) mk.splice(i--, 1); } } }, - // Split a line in two, again keeping styles intact. + // Split a part off a line, keeping styles and markers intact. split: function(pos, textBefore) { - var st = [textBefore, null]; + var st = [textBefore, null], mk = this.marked; copyStyles(pos, this.text.length, this.styles, st); - return new Line(textBefore + this.text.slice(pos), st); + var taken = new Line(textBefore + this.text.slice(pos), st); + if (mk) { + for (var i = 0; i < mk.length; ++i) { + var mark = mk[i]; + if (mark.to > pos || mark.to == null) { + if (!taken.marked) taken.marked = []; + taken.marked.push({ + from: mark.from < pos || mark.from == null ? null : mark.from - pos + textBefore.length, + to: mark.to == null ? null : mark.to - pos + textBefore.length, + style: mark.style, set: mark.set + }); + mark.set.push(taken); + } + } + } + return taken; }, - addMark: function(from, to, style) { - var mk = this.marked, mark = {from: from, to: to, style: style}; + append: function(line) { + if (!line.text.length) return; + var mylen = this.text.length, mk = line.marked; + this.text += line.text; + copyStyles(0, line.text.length, line.styles, this.styles); + if (mk && mk.length) { + var mymk = this.marked || (this.marked = []); + for (var i = 0; i < mymk.length; ++i) + if (mymk[i].to == null) mymk[i].to = mylen; + outer: for (var i = 0; i < mk.length; ++i) { + var mark = mk[i]; + if (!mark.from) { + for (var j = 0; j < mymk.length; ++j) { + var mymark = mymk[j]; + if (mymark.to == mylen && mymark.set == mark.set) { + mymark.to = mark.to == null ? null : mark.to + mylen; + continue outer; + } + } + } + mymk.push(mark); + mark.set.push(this); + mark.from += mylen; + if (mark.to != null) mark.to += mylen; + } + } + }, + addMark: function(from, to, style, set) { + set.push(this); if (this.marked == null) this.marked = []; - this.marked.push(mark); - this.marked.sort(function(a, b){return a.from - b.from;}); - return mark; - }, - removeMark: function(mark) { - var mk = this.marked; - if (!mk) return; - for (var i = 0; i < mk.length; ++i) - if (mk[i] == mark) {mk.splice(i, 1); break;} + this.marked.push({from: from, to: to, style: style, set: set}); + this.marked.sort(function(a, b){return (a.from || 0) - (b.from || 0);}); }, // Run the given mode's parser over a line, update the styles // array, which contains alternating fragments of text and CSS // classes. highlight: function(mode, state) { - var stream = new StringStream(this.text), st = this.styles, pos = 0, changed = false; + var stream = new StringStream(this.text), st = this.styles, pos = 0; + var changed = false, curWord = st[0], prevWord; + if (this.text == "" && mode.blankLine) mode.blankLine(state); while (!stream.eol()) { var style = mode.token(stream, state); var substr = this.text.slice(stream.start, stream.pos); @@ -1578,8 +1941,9 @@ var CodeMirror = (function() { if (pos && st[pos-1] == style) st[pos-2] += substr; else if (substr) { - if (!changed && st[pos] != substr || st[pos+1] != style) changed = true; + if (!changed && (st[pos+1] != style || (pos && st[pos-2] != prevWord))) changed = true; st[pos++] = substr; st[pos++] = style; + prevWord = curWord; curWord = st[pos]; } // Give up when line is ridiculously long if (stream.pos > 5000) { @@ -1588,7 +1952,11 @@ var CodeMirror = (function() { } } if (st.length != pos) {st.length = pos; changed = true;} - return changed; + if (pos && st[pos-2] != prevWord) changed = true; + // Short lines with simple highlights return null, and are + // counted as changed by the driver because they are likely to + // highlight the same way in various contexts. + return changed || (st.length < 5 && this.text.length < 10 ? null : false); }, // Fetch the parser token for a given character. Useful for hacks // that want to inspect the mode state (say, for completion). @@ -1607,7 +1975,7 @@ var CodeMirror = (function() { indentation: function() {return countColumn(this.text);}, // Produces an HTML fragment for the line, taking selection, // marking, and highlighting into account. - getHTML: function(sfrom, sto, includePre) { + getHTML: function(sfrom, sto, includePre, endAt) { var html = []; if (includePre) html.push(this.className ? '
': "
");
@@ -1618,11 +1986,18 @@ var CodeMirror = (function() {
       }
       var st = this.styles, allText = this.text, marked = this.marked;
       if (sfrom == sto) sfrom = null;
+      var len = allText.length;
+      if (endAt != null) len = Math.min(endAt, len);
 
-      if (!allText)
+      if (!allText && endAt == null)
         span(" ", sfrom != null && sto == null ? "CodeMirror-selected" : null);
       else if (!marked && sfrom == null)
-        for (var i = 0, e = st.length; i < e; i+=2) span(st[i], st[i+1]);
+        for (var i = 0, ch = 0; ch < len; i+=2) {
+          var str = st[i], style = st[i+1], l = str.length;
+          if (ch + l > len) str = str.slice(0, len - ch);
+          ch += l;
+          span(str, style && "cm-" + style);
+        }
       else {
         var pos = 0, i = 0, text = "", style, sg = 0;
         var markpos = -1, mark = null;
@@ -1632,9 +2007,9 @@ var CodeMirror = (function() {
             mark = (markpos < marked.length) ? marked[markpos] : null;
           }
         }
-        nextMark();        
-        while (pos < allText.length) {
-          var upto = allText.length;
+        nextMark();
+        while (pos < len) {
+          var upto = len;
           var extraStyle = "";
           if (sfrom != null) {
             if (sfrom > pos) upto = sfrom;
@@ -1653,12 +2028,12 @@ var CodeMirror = (function() {
           }
           for (;;) {
             var end = pos + text.length;
-            var apliedStyle = style;
-            if (extraStyle) apliedStyle = style ? style + extraStyle : extraStyle;
-            span(end > upto ? text.slice(0, upto - pos) : text, apliedStyle);
+            var appliedStyle = style;
+            if (extraStyle) appliedStyle = style ? style + extraStyle : extraStyle;
+            span(end > upto ? text.slice(0, upto - pos) : text, appliedStyle);
             if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
             pos = end;
-            text = st[i++]; style = st[i++];
+            text = st[i++]; style = "cm-" + st[i++];
           }
         }
         if (sfrom != null && sto == null) span(" ", "CodeMirror-selected");
@@ -1716,42 +2091,34 @@ var CodeMirror = (function() {
     }
   };
 
-  // Event stopping compatibility wrapper.
-  function stopEvent() {
-    if (this.preventDefault) {this.preventDefault(); this.stopPropagation();}
-    else {this.returnValue = false; this.cancelBubble = true;}
-  }
+  function stopMethod() {e_stop(this);}
   // Ensure an event has a stop method.
   function addStop(event) {
-    if (!event.stop) event.stop = stopEvent;
+    if (!event.stop) event.stop = stopMethod;
     return event;
   }
 
-  // Event wrapper, exposing the few operations we need.
-  function Event(orig) {this.e = orig;}
-  Event.prototype = {
-    stop: function() {stopEvent.call(this.e);},
-    target: function() {return this.e.target || this.e.srcElement;},
-    button: function() {
-      if (this.e.which) return this.e.which;
-      else if (this.e.button & 1) return 1;
-      else if (this.e.button & 2) return 3;
-      else if (this.e.button & 4) return 2;
-    },
-    pageX: function() {
-      if (this.e.pageX != null) return this.e.pageX;
-      else return this.e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
-    },
-    pageY: function() {
-      if (this.e.pageY != null) return this.e.pageY;
-      else return this.e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
-    }
-  };
+  function e_preventDefault(e) {
+    if (e.preventDefault) e.preventDefault();
+    else e.returnValue = false;
+  }
+  function e_stopPropagation(e) {
+    if (e.stopPropagation) e.stopPropagation();
+    else e.cancelBubble = true;
+  }
+  function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);}
+  function e_target(e) {return e.target || e.srcElement;}
+  function e_button(e) {
+    if (e.which) return e.which;
+    else if (e.button & 1) return 1;
+    else if (e.button & 2) return 3;
+    else if (e.button & 4) return 2;
+  }
 
   // Event handler registration. If disconnect is true, it'll return a
   // function that unregisters the handler.
   function connect(node, type, handler, disconnect) {
-    function wrapHandler(event) {handler(new Event(event || window.event));}
+    function wrapHandler(event) {handler(event || window.event);}
     if (typeof node.addEventListener == "function") {
       node.addEventListener(type, wrapHandler, false);
       if (disconnect) return function() {node.removeEventListener(type, wrapHandler, false);};
@@ -1772,7 +2139,18 @@ var CodeMirror = (function() {
     pre.innerHTML = " "; return !pre.innerHTML;
   })();
 
+  // Detect drag-and-drop
+  var dragAndDrop = (function() {
+    // IE8 has ondragstart and ondrop properties, but doesn't seem to
+    // actually support ondragstart the way it's supposed to work.
+    if (/MSIE [1-8]\b/.test(navigator.userAgent)) return false;
+    var div = document.createElement('div');
+    return "ondragstart" in div && "ondrop" in div;
+  })();
+
   var gecko = /gecko\/\d{7}/i.test(navigator.userAgent);
+  var ie = /MSIE \d/.test(navigator.userAgent);
+  var safari = /Apple Computer/.test(navigator.vendor);
 
   var lineSep = "\n";
   // Feature-detect whether newlines in textareas are converted to \r\n
@@ -1802,11 +2180,23 @@ var CodeMirror = (function() {
     return n;
   }
 
+  function computedStyle(elt) {
+    if (elt.currentStyle) return elt.currentStyle;
+    return window.getComputedStyle(elt, null);
+  }
   // Find the position of an element by following the offsetParent chain.
-  function eltOffset(node) {
-    var x = 0, y = 0, n2 = node;
-    for (var n = node; n; n = n.offsetParent) {x += n.offsetLeft; y += n.offsetTop;}
-    for (var n = node; n != document.body; n = n.parentNode) {x -= n.scrollLeft; y -= n.scrollTop;}
+  // If screen==true, it returns screen (rather than page) coordinates.
+  function eltOffset(node, screen) {
+    var doc = node.ownerDocument.body;
+    var x = 0, y = 0, skipDoc = false;
+    for (var n = node; n; n = n.offsetParent) {
+      x += n.offsetLeft; y += n.offsetTop;
+      if (screen && computedStyle(n).position == "fixed")
+        skipDoc = true;
+    }
+    var e = screen && !skipDoc ? null : doc;
+    for (var n = node.parentNode; n != e; n = n.parentNode)
+      if (n.scrollLeft != null) { x -= n.scrollLeft; y -= n.scrollTop;}
     return {left: x, top: y};
   }
   // Get a node's text content.
@@ -1819,9 +2209,18 @@ var CodeMirror = (function() {
   function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);}
   function copyPos(x) {return {line: x.line, ch: x.ch};}
 
+  var escapeElement = document.createElement("pre");
   function htmlEscape(str) {
-    return str.replace(/[<&]/g, function(str) {return str == "&" ? "&" : "<";});
+    if (badTextContent) {
+      escapeElement.innerHTML = "";
+      escapeElement.appendChild(document.createTextNode(str));
+    } else {
+      escapeElement.textContent = str;
+    }
+    return escapeElement.innerHTML;
   }
+  var badTextContent = htmlEscape("\t") != "\t";
+  CodeMirror.htmlEscape = htmlEscape;
 
   // Used to position the cursor after an undo/redo by finding the
   // last edited character.
@@ -1842,8 +2241,9 @@ var CodeMirror = (function() {
 
   // See if "".split is the broken IE version, if so, provide an
   // alternative way to split lines.
+  var splitLines, selRange, setSelRange;
   if ("\n\nb".split(/\n/).length != 3)
-    var splitLines = function(string) {
+    splitLines = function(string) {
       var pos = 0, nl, result = [];
       while ((nl = string.indexOf("\n", pos)) > -1) {
         result.push(string.slice(pos, string.charAt(nl-1) == "\r" ? nl - 1 : nl));
@@ -1853,23 +2253,40 @@ var CodeMirror = (function() {
       return result;
     };
   else
-    var splitLines = function(string){return string.split(/\r?\n/);};
+    splitLines = function(string){return string.split(/\r?\n/);};
+  CodeMirror.splitLines = splitLines;
 
   // Sane model of finding and setting the selection in a textarea
   if (window.getSelection) {
-    var selRange = function(te) {
+    selRange = function(te) {
       try {return {start: te.selectionStart, end: te.selectionEnd};}
       catch(e) {return null;}
     };
-    var setSelRange = function(te, start, end) {
-      try {te.setSelectionRange(start, end);}
-      catch(e) {} // Fails on Firefox when textarea isn't part of the document
-    };
+    if (safari)
+      // On Safari, selection set with setSelectionRange are in a sort
+      // of limbo wrt their anchor. If you press shift-left in them,
+      // the anchor is put at the end, and the selection expanded to
+      // the left. If you press shift-right, the anchor ends up at the
+      // front. This is not what CodeMirror wants, so it does a
+      // spurious modify() call to get out of limbo.
+      setSelRange = function(te, start, end) {
+        if (start == end)
+          te.setSelectionRange(start, end);
+        else {
+          te.setSelectionRange(start, end - 1);
+          window.getSelection().modify("extend", "forward", "character");
+        }
+      };
+    else
+      setSelRange = function(te, start, end) {
+        try {te.setSelectionRange(start, end);}
+        catch(e) {} // Fails on Firefox when textarea isn't part of the document
+      };
   }
   // IE model. Don't ask.
   else {
-    var selRange = function(te) {
-      try {var range = document.selection.createRange();}
+    selRange = function(te) {
+      try {var range = te.ownerDocument.selection.createRange();}
       catch(e) {return null;}
       if (!range || range.parentElement() != te) return null;
       var val = te.value, len = val.length, localRange = te.createTextRange();
@@ -1890,7 +2307,7 @@ var CodeMirror = (function() {
       for (var i = val.indexOf("\r"); i > -1 && i < end; i = val.indexOf("\r", i+1), end++) {}
       return {start: start, end: end};
     };
-    var setSelRange = function(te, start, end) {
+    setSelRange = function(te, start, end) {
       var range = te.createTextRange();
       range.collapse(true);
       var endrange = range.duplicate();
diff --git a/rhodecode/public/js/css_browser_selector.js b/rhodecode/public/js/css_browser_selector.js
deleted file mode 100644
--- a/rhodecode/public/js/css_browser_selector.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-CSS Browser Selector v0.3.5 (Feb 05, 2010)
-Rafael Lima (http://rafael.adm.br)
-http://rafael.adm.br/css_browser_selector
-License: http://creativecommons.org/licenses/by/2.5/
-Contributors: http://rafael.adm.br/css_browser_selector#contributors
-2. Set CSS attributes with the code of each browser/os you want to hack
-
-Examples:
-
-    * html.gecko div#header { margin: 1em; }
-    * .opera #header { margin: 1.2em; }
-    * .ie .mylink { font-weight: bold; }
-    * .mac.ie .mylink { font-weight: bold; }
-    * .[os].[browser] .mylink { font-weight: bold; } -> without space between .[os] and .[browser]
-
-Available OS Codes [os]:
-
-    * win - Microsoft Windows
-    * linux - Linux (x11 and linux)
-    * mac - Mac OS
-    * freebsd - FreeBSD
-    * ipod - iPod Touch
-    * iphone - iPhone
-    * webtv - WebTV
-    * mobile - J2ME Devices (ex: Opera mini)
-
-Available Browser Codes [browser]:
-
-    * ie - Internet Explorer (All versions)
-    * ie8 - Internet Explorer 8.x
-    * ie7 - Internet Explorer 7.x
-    * ie6 - Internet Explorer 6.x
-    * ie5 - Internet Explorer 5.x
-    * gecko - Mozilla, Firefox (all versions), Camino
-    * ff2 - Firefox 2
-    * ff3 - Firefox 3
-    * ff3_5 - Firefox 3.5 new
-    * opera - Opera (All versions)
-    * opera8 - Opera 8.x
-    * opera9 - Opera 9.x
-    * opera10 - Opera 10.x
-    * konqueror - Konqueror
-    * webkit or safari - Safari, NetNewsWire, OmniWeb, Shiira, Google Chrome
-    * safari3 - Safari 3.x
-    * chrome - Google Chrome
-    * iron - SRWare Iron new
-
-*/
-function css_browser_selector(u){var ua = u.toLowerCase(),is=function(t){return ua.indexOf(t)>-1;},g='gecko',w='webkit',s='safari',o='opera',h=document.documentElement,b=[(!(/opera|webtv/i.test(ua))&&/msie\s(\d)/.test(ua))?('ie ie'+RegExp.$1):is('firefox/2')?g+' ff2':is('firefox/3.5')?g+' ff3 ff3_5':is('firefox/3')?g+' ff3':is('gecko/')?g:is('opera')?o+(/version\/(\d+)/.test(ua)?' '+o+RegExp.$1:(/opera(\s|\/)(\d+)/.test(ua)?' '+o+RegExp.$2:'')):is('konqueror')?'konqueror':is('chrome')?w+' chrome':is('iron')?w+' iron':is('applewebkit/')?w+' '+s+(/version\/(\d+)/.test(ua)?' '+s+RegExp.$1:''):is('mozilla/')?g:'',is('j2me')?'mobile':is('iphone')?'iphone':is('ipod')?'ipod':is('mac')?'mac':is('darwin')?'mac':is('webtv')?'webtv':is('win')?'win':is('freebsd')?'freebsd':(is('x11')||is('linux'))?'linux':'','js']; c = b.join(' '); h.className += ' '+c; return c;}; css_browser_selector(navigator.userAgent);
diff --git a/rhodecode/public/js/graph.js b/rhodecode/public/js/graph.js
--- a/rhodecode/public/js/graph.js
+++ b/rhodecode/public/js/graph.js
@@ -63,18 +63,16 @@ function BranchRenderer() {
 		var rela = document.getElementById('graph');
 		var pad = pad;
 		var scale = 22;
-		
+
 		for (var i in data) {
 			this.scale(scale);
+
 			var row = document.getElementById("chg_"+idx);
-			var	next = document.getElementById("chg_"+idx+1);
+			if (row == null)
+				continue;
+			var	next = document.getElementById("chg_"+(idx+1));
 			var extra = 0;
 			
-			//skip this since i don't have DATE in my app
-			//if (next.is('.changesets-date')) {
-			//	extra = next.outerHeight();
-			//}
-						
 			this.cell[1] += row.clientWidth;
 			this.bg[1] += this.bg_height;
 			
@@ -82,7 +80,10 @@ function BranchRenderer() {
 			nodeid = cur[0];
 			node = cur[1];
 			in_l = cur[2];
-			
+
+			var rowY = row.offsetTop + row.offsetHeight/2 - rela.offsetTop;
+			var nextY = (next == null) ? rowY + row.offsetHeight/2 : next.offsetTop + next.offsetHeight/2 - rela.offsetTop;
+
 			for (var j in in_l) {
 				
 				line = in_l[j];
@@ -99,17 +100,26 @@ function BranchRenderer() {
 				}
 				
 				this.setColor(color, 0.0, 0.65);
+
 				
-				y = row.offsetTop-rela.offsetTop+4;
 				x = pad-((this.cell[0] + this.box_size * start - 1) + this.bg_height-2);
 				
 				this.ctx.lineWidth=this.line_width;
 				this.ctx.beginPath();
-				this.ctx.moveTo(x, y);
+				this.ctx.moveTo(x, rowY);
 
-				y += row.offsetHeight;
-				x = pad-((1 + this.box_size * end) + this.bg_height-2);
-				this.ctx.lineTo(x,y+extra,3);
+				
+				if (start == end)
+				{
+					x = pad-((1 + this.box_size * end) + this.bg_height-2);
+					this.ctx.lineTo(x,nextY+extra,3);
+				}
+				else
+				{
+					var x2 = pad-((1 + this.box_size * end) + this.bg_height-2);
+					var ymid = (rowY+nextY) / 2;
+					this.ctx.bezierCurveTo (x,ymid,x2,ymid,x2,nextY);
+				}
 				this.ctx.stroke();
 			}
 			
@@ -117,12 +127,12 @@ function BranchRenderer() {
 			color = node[1]
 			
 			radius = this.dot_radius;
-			y = row.offsetTop-rela.offsetTop+4;
+
 			x = pad-(Math.round(this.cell[0] * scale/2 * column + radius) + 15 - (column*4));
 		
 			this.ctx.beginPath();
 			this.setColor(color, 0.25, 0.75);
-			this.ctx.arc(x, y, radius, 0, Math.PI * 2, true);
+			this.ctx.arc(x, rowY, radius, 0, Math.PI * 2, true);
 			this.ctx.fill();
 			
 			idx++;
diff --git a/rhodecode/public/js/rhodecode.js b/rhodecode/public/js/rhodecode.js
--- a/rhodecode/public/js/rhodecode.js
+++ b/rhodecode/public/js/rhodecode.js
@@ -7,10 +7,10 @@ if (typeof console == "undefined" || typ
 }
 
 
-function str_repeat(i, m) {
+var str_repeat = function(i, m) {
 	for (var o = []; m > 0; o[--m] = i);
 	return o.join('');
-}
+};
 
 /**
  * INJECT .format function into String
@@ -55,7 +55,7 @@ String.prototype.format = function() {
  * 
  * @returns {ColorGenerator}
  */
-function ColorGenerator(){
+var ColorGenerator = function(){
 	this.GOLDEN_RATIO = 0.618033988749895;
 	this.CURRENT_RATIO = 0.22717784590367374 // this can be random
 	this.HSV_1 = 0.75;//saturation
@@ -129,7 +129,21 @@ var push_state_enabled = Boolean(
 				/* disable for the mercury iOS browser, or at least older versions of the webkit engine */
 				|| (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent)
 		)
-)
+);
+
+var _run_callbacks = function(callbacks){
+	if (callbacks !== undefined){
+		var _l = callbacks.length;
+	    for (var i=0;i<_l;i++){
+	    	var func = callbacks[i];
+	    	if(typeof(func)=='function'){
+	            try{
+	          	    func();
+	            }catch (err){};            		
+	    	}
+	    }
+	}		
+}
 
 /**
  * Partial Ajax Implementation
@@ -172,11 +186,14 @@ function ypjax(url,container,s_call,f_ca
 	YUC.asyncRequest(method,url,{
 		success:s_wrapper,
 		failure:function(o){
-			console.log(o)
+			console.log(o);
+			YUD.get(container).innerHTML='ERROR';
+			YUD.setStyle(container,'opacity','1.0');
+			YUD.setStyle(container,'color','red');
 		}
 	},args);
 	
-}
+};
 
 /**
  * tooltip activate
@@ -203,7 +220,7 @@ var tooltip_activate = function(){
         hidedelay:5,
         showdelay:20,
     });
-}
+};
 
 /**
  * show more
@@ -214,5 +231,492 @@ var show_more_event = function(){
         YUD.setStyle(YUD.get(el.id.substring(1)),'display','');
         YUD.setStyle(el.parentNode,'display','none');
     });
+};
+
+
+/**
+ * Quick filter widget
+ * 
+ * @param target: filter input target
+ * @param nodes: list of nodes in html we want to filter.
+ * @param display_element function that takes current node from nodes and
+ *    does hide or show based on the node
+ * 
+ */
+var q_filter = function(target,nodes,display_element){
+	
+	var nodes = nodes;
+	var q_filter_field = YUD.get(target);
+	var F = YAHOO.namespace(target);
+
+	YUE.on(q_filter_field,'click',function(){
+	   q_filter_field.value = '';
+	});
+
+	YUE.on(q_filter_field,'keyup',function(e){
+	    clearTimeout(F.filterTimeout); 
+	    F.filterTimeout = setTimeout(F.updateFilter,600); 
+	});
+
+	F.filterTimeout = null;
+
+	var show_node = function(node){
+		YUD.setStyle(node,'display','')
+	}
+	var hide_node = function(node){
+		YUD.setStyle(node,'display','none');
+	}
+	
+	F.updateFilter  = function() { 
+	   // Reset timeout 
+	   F.filterTimeout = null;
+	   
+	   var obsolete = [];
+	   
+	   var req = q_filter_field.value.toLowerCase();
+	   
+	   var l = nodes.length;
+	   var i;
+	   var showing = 0;
+	   
+       for (i=0;i'+ 
+                     '{0}'.format(body);
+	return form;
+};
+
+var createInlineForm = function(parent_tr, f_path, line) {
+	var tmpl = YUD.get('comment-inline-form-template').innerHTML;
+	tmpl = tmpl.format(f_path, line);
+	var form = tableTr('comment-form-inline',tmpl)
+	
+	// create event for hide button
+	form = new YAHOO.util.Element(form);
+	var form_hide_button = new YAHOO.util.Element(form.getElementsByClassName('hide-inline-form')[0]);
+	form_hide_button.on('click', function(e) {
+		var newtr = e.currentTarget.parentNode.parentNode.parentNode.parentNode.parentNode;
+		removeInlineForm(newtr);
+		YUD.removeClass(parent_tr, 'form-open');
+	});
+	return form
+};
+var injectInlineForm = function(tr){
+	  if(YUD.hasClass(tr,'form-open') || YUD.hasClass(tr,'context') || YUD.hasClass(tr,'no-comment')){
+		  return
+	  }	
+	  YUD.addClass(tr,'form-open');
+	  var node = tr.parentNode.parentNode.parentNode.getElementsByClassName('full_f_path')[0];
+	  var f_path = YUD.getAttribute(node,'path');
+	  var lineno = getLineNo(tr);
+	  var form = createInlineForm(tr, f_path, lineno);
+	  var target_tr = tr;
+	  if(YUD.hasClass(YUD.getNextSibling(tr),'inline-comments')){
+		  target_tr = YUD.getNextSibling(tr);
+	  }
+	  YUD.insertAfter(form,target_tr);
+	  YUD.get('text_'+lineno).focus();
+	  tooltip_activate();
+};
+
+var createInlineAddButton = function(tr,label){
+	var html = '
{0}
'.format(label); + + var add = new YAHOO.util.Element(tableTr('inline-comments-button',html)); + add.on('click', function(e) { + injectInlineForm(tr); + }); + return add; +}; + +var getLineNo = function(tr) { + var line; + var o = tr.children[0].id.split('_'); + var n = tr.children[1].id.split('_'); + + if (n.length >= 2) { + line = n[n.length-1]; + } else if (o.length >= 2) { + line = o[o.length-1]; + } + + return line +}; + + +var fileBrowserListeners = function(current_url, node_list_url, url_base, + truncated_lbl, nomatch_lbl){ + var current_url_branch = +"?branch=__BRANCH__"; + var url = url_base; + var node_url = node_list_url; + + YUE.on('stay_at_branch','click',function(e){ + if(e.target.checked){ + var uri = current_url_branch; + uri = uri.replace('__BRANCH__',e.target.value); + window.location = uri; + } + else{ + window.location = current_url; + } + }) + + var n_filter = YUD.get('node_filter'); + var F = YAHOO.namespace('node_filter'); + + F.filterTimeout = null; + var nodes = null; + + F.initFilter = function(){ + YUD.setStyle('node_filter_box_loading','display',''); + YUD.setStyle('search_activate_id','display','none'); + YUD.setStyle('add_node_id','display','none'); + YUC.initHeader('X-PARTIAL-XHR',true); + YUC.asyncRequest('GET',url,{ + success:function(o){ + nodes = JSON.parse(o.responseText); + YUD.setStyle('node_filter_box_loading','display','none'); + YUD.setStyle('node_filter_box','display',''); + n_filter.focus(); + if(YUD.hasClass(n_filter,'init')){ + n_filter.value = ''; + YUD.removeClass(n_filter,'init'); + } + }, + failure:function(o){ + console.log('failed to load'); + } + },null); + } + + F.updateFilter = function(e) { + + return function(){ + // Reset timeout + F.filterTimeout = null; + var query = e.target.value.toLowerCase(); + var match = []; + var matches = 0; + var matches_max = 20; + if (query != ""){ + for(var i=0;i matches_max){ + break; + } + + var n = nodes[i].name; + var t = nodes[i].type; + var n_hl = n.substring(0,pos) + +"{0}".format(n.substring(pos,pos+query.length)) + +n.substring(pos+query.length) + match.push('{2}'.format(t,node_url.replace('__FPATH__',n),n_hl)); + } + if(match.length >= matches_max){ + match.push('{0}'.format(truncated_lbl)); + } + + } + } + if(query != ""){ + YUD.setStyle('tbody','display','none'); + YUD.setStyle('tbody_filtered','display',''); + + if (match.length==0){ + match.push('{0}'.format(nomatch_lbl)); + } + + YUD.get('tbody_filtered').innerHTML = match.join(""); + } + else{ + YUD.setStyle('tbody','display',''); + YUD.setStyle('tbody_filtered','display','none'); + } + + } + }; + + YUE.on(YUD.get('filter_activate'),'click',function(){ + F.initFilter(); + }) + YUE.on(n_filter,'click',function(){ + if(YUD.hasClass(n_filter,'init')){ + n_filter.value = ''; + YUD.removeClass(n_filter,'init'); + } + }); + YUE.on(n_filter,'keyup',function(e){ + clearTimeout(F.filterTimeout); + F.filterTimeout = setTimeout(F.updateFilter(e),600); + }); +}; + + +var initCodeMirror = function(textAreadId,resetUrl){ + var myCodeMirror = CodeMirror.fromTextArea(YUD.get(textAreadId),{ + mode: "null", + lineNumbers:true + }); + YUE.on('reset','click',function(e){ + window.location=resetUrl + }); + + YUE.on('file_enable','click',function(){ + YUD.setStyle('editor_container','display',''); + YUD.setStyle('upload_file_container','display','none'); + YUD.setStyle('filename_container','display',''); + }); + + YUE.on('upload_file_enable','click',function(){ + YUD.setStyle('editor_container','display','none'); + YUD.setStyle('upload_file_container','display',''); + YUD.setStyle('filename_container','display','none'); + }); +}; + + + +var getIdentNode = function(n){ + //iterate thru nodes untill matched interesting node ! + + if (typeof n == 'undefined'){ + return -1 + } + + if(typeof n.id != "undefined" && n.id.match('L[0-9]+')){ + return n + } + else{ + return getIdentNode(n.parentNode); + } +}; + +var getSelectionLink = function(selection_link_label) { + return function(){ + //get selection from start/to nodes + if (typeof window.getSelection != "undefined") { + s = window.getSelection(); + + from = getIdentNode(s.anchorNode); + till = getIdentNode(s.focusNode); + + f_int = parseInt(from.id.replace('L','')); + t_int = parseInt(till.id.replace('L','')); + + if (f_int > t_int){ + //highlight from bottom + offset = -35; + ranges = [t_int,f_int]; + + } + else{ + //highligth from top + offset = 35; + ranges = [f_int,t_int]; + } + + if (ranges[0] != ranges[1]){ + if(YUD.get('linktt') == null){ + hl_div = document.createElement('div'); + hl_div.id = 'linktt'; + } + anchor = '#L'+ranges[0]+'-'+ranges[1]; + hl_div.innerHTML = ''; + l = document.createElement('a'); + l.href = location.href.substring(0,location.href.indexOf('#'))+anchor; + l.innerHTML = selection_link_label; + hl_div.appendChild(l); + + YUD.get('body').appendChild(hl_div); + + xy = YUD.getXY(till.id); + + YUD.addClass('linktt','yui-tt'); + YUD.setStyle('linktt','top',xy[1]+offset+'px'); + YUD.setStyle('linktt','left',xy[0]+'px'); + YUD.setStyle('linktt','visibility','visible'); + } + else{ + YUD.setStyle('linktt','visibility','hidden'); + } + } + } +}; + +var deleteNotification = function(url, notification_id,callbacks){ + var callback = { + success:function(o){ + var obj = YUD.get(String("notification_"+notification_id)); + if(obj.parentNode !== undefined){ + obj.parentNode.removeChild(obj); + } + _run_callbacks(callbacks); + }, + failure:function(o){ + alert("error"); + }, + }; + var postData = '_method=delete'; + var sUrl = url.replace('__NOTIFICATION_ID__',notification_id); + var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, + callback, postData); +}; + + +/** + * QUICK REPO MENU + */ +var quick_repo_menu = function(){ + YUE.on(YUQ('.quick_repo_menu'),'click',function(e){ + var menu = e.currentTarget.firstElementChild.firstElementChild; + if(YUD.hasClass(menu,'hidden')){ + YUD.addClass(e.currentTarget,'active'); + YUD.removeClass(menu,'hidden'); + }else{ + YUD.removeClass(e.currentTarget,'active'); + YUD.addClass(menu,'hidden'); + } + }) +}; + + +/** + * TABLE SORTING + */ + +// returns a node from given html; +var fromHTML = function(html){ + var _html = document.createElement('element'); + _html.innerHTML = html; + return _html; +} +var get_rev = function(node){ + var n = node.firstElementChild.firstElementChild; + + if (n===null){ + return -1 + } + else{ + out = n.firstElementChild.innerHTML.split(':')[0].replace('r',''); + return parseInt(out); + } } +var get_name = function(node){ + var name = node.firstElementChild.children[2].innerHTML; + return name +} +var get_group_name = function(node){ + var name = node.firstElementChild.children[1].innerHTML; + return name +} +var get_date = function(node){ + var date_ = node.firstElementChild.innerHTML; + return date_ +} + +var revisionSort = function(a, b, desc, field) { + + var a_ = fromHTML(a.getData(field)); + var b_ = fromHTML(b.getData(field)); + + // extract revisions from string nodes + a_ = get_rev(a_) + b_ = get_rev(b_) + + var comp = YAHOO.util.Sort.compare; + var compState = comp(a_, b_, desc); + return compState; +}; +var ageSort = function(a, b, desc, field) { + var a_ = a.getData(field); + var b_ = b.getData(field); + + var comp = YAHOO.util.Sort.compare; + var compState = comp(a_, b_, desc); + return compState; +}; + +var nameSort = function(a, b, desc, field) { + var a_ = fromHTML(a.getData(field)); + var b_ = fromHTML(b.getData(field)); + + // extract name from table + a_ = get_name(a_) + b_ = get_name(b_) + + var comp = YAHOO.util.Sort.compare; + var compState = comp(a_, b_, desc); + return compState; +}; + +var groupNameSort = function(a, b, desc, field) { + var a_ = fromHTML(a.getData(field)); + var b_ = fromHTML(b.getData(field)); + + // extract name from table + a_ = get_group_name(a_) + b_ = get_group_name(b_) + + var comp = YAHOO.util.Sort.compare; + var compState = comp(a_, b_, desc); + return compState; +}; +var dateSort = function(a, b, desc, field) { + var a_ = fromHTML(a.getData(field)); + var b_ = fromHTML(b.getData(field)); + + // extract name from table + a_ = get_date(a_) + b_ = get_date(b_) + + var comp = YAHOO.util.Sort.compare; + var compState = comp(a_, b_, desc); + return compState; +}; \ No newline at end of file diff --git a/rhodecode/public/js/yui.2.9.js b/rhodecode/public/js/yui.2.9.js --- a/rhodecode/public/js/yui.2.9.js +++ b/rhodecode/public/js/yui.2.9.js @@ -101,4 +101,50 @@ Code licensed under the BSD License: http://developer.yahoo.com/yui/license.html version: 2.9.0 */ -(function(){var b=YAHOO.util.Event,g=YAHOO.lang,e=b.addListener,f=b.removeListener,c=b.getListeners,d=[],h={mouseenter:"mouseover",mouseleave:"mouseout"},a=function(n,m,l){var j=b._getCacheIndex(d,n,m,l),i,k;if(j>=0){i=d[j];}if(n&&i){k=f.call(b,i[0],m,i[3]);if(k){delete d[j][2];delete d[j][3];d.splice(j,1);}}return k;};g.augmentObject(b._specialTypes,h);g.augmentObject(b,{_createMouseDelegate:function(i,j,k){return function(q,m){var p=this,l=b.getRelatedTarget(q),o,n;if(p!=l&&!YAHOO.util.Dom.isAncestor(p,l)){o=p;if(k){if(k===true){o=j;}else{o=k;}}n=[q,j];if(m){n.splice(1,0,p,m);}return i.apply(o,n);}};},addListener:function(m,l,k,n,o){var i,j;if(h[l]){i=b._createMouseDelegate(k,n,o);i.mouseDelegate=true;d.push([m,l,k,i]);j=e.call(b,m,l,i);}else{j=e.apply(b,arguments);}return j;},removeListener:function(l,k,j){var i;if(h[k]){i=a.apply(b,arguments);}else{i=f.apply(b,arguments);}return i;},getListeners:function(p,o){var n=[],r,m=(o==="mouseover"||o==="mouseout"),q,k,j;if(o&&(m||h[o])){r=c.call(b,p,this._getType(o));if(r){for(k=r.length-1;k>-1;k--){j=r[k];q=j.fn.mouseDelegate;if((h[o]&&q)||(m&&!q)){n.push(j);}}}}else{n=c.apply(b,arguments);}return(n&&n.length)?n:null;}},true);b.on=b.addListener;}());YAHOO.register("event-mouseenter",YAHOO.util.Event,{version:"2.9.0",build:"2800"}); \ No newline at end of file +(function(){var b=YAHOO.util.Event,g=YAHOO.lang,e=b.addListener,f=b.removeListener,c=b.getListeners,d=[],h={mouseenter:"mouseover",mouseleave:"mouseout"},a=function(n,m,l){var j=b._getCacheIndex(d,n,m,l),i,k;if(j>=0){i=d[j];}if(n&&i){k=f.call(b,i[0],m,i[3]);if(k){delete d[j][2];delete d[j][3];d.splice(j,1);}}return k;};g.augmentObject(b._specialTypes,h);g.augmentObject(b,{_createMouseDelegate:function(i,j,k){return function(q,m){var p=this,l=b.getRelatedTarget(q),o,n;if(p!=l&&!YAHOO.util.Dom.isAncestor(p,l)){o=p;if(k){if(k===true){o=j;}else{o=k;}}n=[q,j];if(m){n.splice(1,0,p,m);}return i.apply(o,n);}};},addListener:function(m,l,k,n,o){var i,j;if(h[l]){i=b._createMouseDelegate(k,n,o);i.mouseDelegate=true;d.push([m,l,k,i]);j=e.call(b,m,l,i);}else{j=e.apply(b,arguments);}return j;},removeListener:function(l,k,j){var i;if(h[k]){i=a.apply(b,arguments);}else{i=f.apply(b,arguments);}return i;},getListeners:function(p,o){var n=[],r,m=(o==="mouseover"||o==="mouseout"),q,k,j;if(o&&(m||h[o])){r=c.call(b,p,this._getType(o));if(r){for(k=r.length-1;k>-1;k--){j=r[k];q=j.fn.mouseDelegate;if((h[o]&&q)||(m&&!q)){n.push(j);}}}}else{n=c.apply(b,arguments);}return(n&&n.length)?n:null;}},true);b.on=b.addListener;}());YAHOO.register("event-mouseenter",YAHOO.util.Event,{version:"2.9.0",build:"2800"}); +/* +Copyright (c) 2011, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.com/yui/license.html +version: 2.9.0 +*/ +(function(){var lang=YAHOO.lang,util=YAHOO.util,Ev=util.Event;util.DataSourceBase=function(oLiveData,oConfigs){if(oLiveData===null||oLiveData===undefined){return;}this.liveData=oLiveData;this._oQueue={interval:null,conn:null,requests:[]};this.responseSchema={};if(oConfigs&&(oConfigs.constructor==Object)){for(var sConfig in oConfigs){if(sConfig){this[sConfig]=oConfigs[sConfig];}}}var maxCacheEntries=this.maxCacheEntries;if(!lang.isNumber(maxCacheEntries)||(maxCacheEntries<0)){maxCacheEntries=0;}this._aIntervals=[];this.createEvent("cacheRequestEvent");this.createEvent("cacheResponseEvent");this.createEvent("requestEvent");this.createEvent("responseEvent");this.createEvent("responseParseEvent");this.createEvent("responseCacheEvent");this.createEvent("dataErrorEvent");this.createEvent("cacheFlushEvent");var DS=util.DataSourceBase;this._sName="DataSource instance"+DS._nIndex;DS._nIndex++;};var DS=util.DataSourceBase;lang.augmentObject(DS,{TYPE_UNKNOWN:-1,TYPE_JSARRAY:0,TYPE_JSFUNCTION:1,TYPE_XHR:2,TYPE_JSON:3,TYPE_XML:4,TYPE_TEXT:5,TYPE_HTMLTABLE:6,TYPE_SCRIPTNODE:7,TYPE_LOCAL:8,ERROR_DATAINVALID:"Invalid data",ERROR_DATANULL:"Null data",_nIndex:0,_nTransactionId:0,_cloneObject:function(o){if(!lang.isValue(o)){return o;}var copy={};if(Object.prototype.toString.apply(o)==="[object RegExp]"){copy=o;}else{if(lang.isFunction(o)){copy=o;}else{if(lang.isArray(o)){var array=[];for(var i=0,len=o.length;i0){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();}oResponse=(this.cloneBeforeCaching)?DS._cloneObject(oResponse):oResponse;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.call(this,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,oCallback):this.liveData(oRequest,oCallback);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){return new util.LocalDataSource(oLiveData,oConfigs);}else{if(dataType==DS.TYPE_XHR){return new util.XHRDataSource(oLiveData,oConfigs);}else{if(dataType==DS.TYPE_SCRIPTNODE){return new util.ScriptNodeDataSource(oLiveData,oConfigs);}else{if(dataType==DS.TYPE_JSFUNCTION){return new util.FunctionDataSource(oLiveData,oConfigs);}}}}}if(YAHOO.lang.isString(oLiveData)){return new util.XHRDataSource(oLiveData,oConfigs);}else{if(YAHOO.lang.isFunction(oLiveData)){return new util.FunctionDataSource(oLiveData,oConfigs);}else{return new util.LocalDataSource(oLiveData,oConfigs);}}};lang.augmentObject(util.DataSource,DS);})();YAHOO.util.Number={format:function(e,k){if(e===""||e===null||!isFinite(e)){return"";}e=+e;k=YAHOO.lang.merge(YAHOO.util.Number.format.defaults,(k||{}));var j=e+"",l=Math.abs(e),b=k.decimalPlaces||0,r=k.thousandsSeparator,f=k.negativeFormat||("-"+k.format),q,p,g,h;if(f.indexOf("#")>-1){f=f.replace(/#/,k.format);}if(b<0){q=l-(l%1)+"";g=q.length+b;if(g>0){q=Number("."+q).toFixed(g).slice(2)+new Array(q.length-g+1).join("0");}else{q="0";}}else{var a=l+"";if(b>0||a.indexOf(".")>0){var d=Math.pow(10,b);q=Math.round(l*d)/d+"";var c=q.indexOf("."),m,o;if(c<0){m=b;o=(Math.pow(10,m)+"").substring(1);if(b>0){q=q+"."+o;}}else{m=b-(q.length-c-1);o=(Math.pow(10,m)+"").substring(1);q=q+o;}}else{q=l.toFixed(b)+"";}}p=q.split(/\D/);if(l>=1000){g=p[0].length%3||3;p[0]=p[0].slice(0,g)+p[0].slice(g).replace(/(\d{3})/g,r+"$1");}return YAHOO.util.Number.format._applyFormat((e<0?f:k.format),p.join(k.decimalSeparator),k);}};YAHOO.util.Number.format.defaults={format:"{prefix}{number}{suffix}",negativeFormat:null,decimalSeparator:".",decimalPlaces:null,thousandsSeparator:""};YAHOO.util.Number.format._applyFormat=function(a,b,c){return a.replace(/\{(\w+)\}/g,function(d,e){return e==="number"?b:e in c?c[e]:"";});};(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(e,c){return c.a[e.getDay()];},A:function(e,c){return c.A[e.getDay()];},b:function(e,c){return c.b[e.getMonth()];},B:function(e,c){return c.B[e.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(f){var g=f.getFullYear();var e=parseInt(b.formats.V(f),10);var c=parseInt(b.formats.W(f),10);if(c>e){g++;}else{if(c===0&&e>=52){g--;}}return g;},H:["getHours","0"],I:function(e){var c=e.getHours()%12;return a(c===0?12:c,0);},j:function(h){var g=new Date(""+h.getFullYear()+"/1/1 GMT");var e=new Date(""+h.getFullYear()+"/"+(h.getMonth()+1)+"/"+h.getDate()+" GMT");var c=e-g;var f=parseInt(c/60000/60/24,10)+1;return a(f,0,100);},k:["getHours"," "],l:function(e){var c=e.getHours()%12;return a(c===0?12:c," ");},m:function(c){return a(c.getMonth()+1,0);},M:["getMinutes","0"],p:function(e,c){return c.p[e.getHours()>=12?1:0];},P:function(e,c){return c.P[e.getHours()>=12?1:0];},s:function(e,c){return parseInt(e.getTime()/1000,10);},S:["getSeconds","0"],u:function(c){var e=c.getDay();return e===0?7:e;},U:function(g){var c=parseInt(b.formats.j(g),10);var f=6-g.getDay();var e=parseInt((c+f)/7,10);return a(e,0);},V:function(g){var f=parseInt(b.formats.W(g),10);var c=(new Date(""+g.getFullYear()+"/1/1")).getDay();var e=f+(c>4||c<=1?0:1);if(e===53&&(new Date(""+g.getFullYear()+"/12/31")).getDay()<4){e=1;}else{if(e===0){e=b.formats.V(new Date(""+(g.getFullYear()-1)+"/12/31"));}}return a(e,0);},w:"getDay",W:function(g){var c=parseInt(b.formats.j(g),10);var f=7-b.formats.u(g);var e=parseInt((c+f)/7,10); +return a(e,0,10);},y:function(c){return a(c.getFullYear()%100,0);},Y:"getFullYear",z:function(f){var e=f.getTimezoneOffset();var c=a(parseInt(Math.abs(e/60),10),0);var g=a(Math.abs(e%60),0);return(e>0?"-":"+")+c+g;},Z:function(c){var e=c.toString().replace(/^.*:\d\d( GMT[+-]\d+)? \(?([A-Za-z ]+)\)?\d*$/,"$2").replace(/[a-z ]/g,"");if(e.length>4){e=b.formats.z(c);}return e;},"%":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.9.0",build:"2800"});/* +Copyright (c) 2011, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.com/yui/license.html +version: 2.9.0 +*/ +YAHOO.util.Chain=function(){this.q=[].slice.call(arguments);this.createEvent("end");};YAHOO.util.Chain.prototype={id:0,run:function(){var g=this.q[0],d;if(!g){this.fireEvent("end");return this;}else{if(this.id){return this;}}d=g.method||g;if(typeof d==="function"){var f=g.scope||{},b=g.argument||[],a=g.timeout||0,e=this;if(!(b instanceof Array)){b=[b];}if(a<0){this.id=a;if(g.until){for(;!g.until();){d.apply(f,b);}}else{if(g.iterations){for(;g.iterations-->0;){d.apply(f,b);}}else{d.apply(f,b);}}this.q.shift();this.id=0;return this.run();}else{if(g.until){if(g.until()){this.q.shift();return this.run();}}else{if(!g.iterations||!--g.iterations){this.q.shift();}}this.id=setTimeout(function(){d.apply(f,b);if(e.id){e.id=0;e.run();}},a);}}return this;},add:function(a){this.q.push(a);return this;},pause:function(){if(this.id>0){clearTimeout(this.id);}this.id=0;return this;},stop:function(){this.pause();this.q=[];return this;}};YAHOO.lang.augmentProto(YAHOO.util.Chain,YAHOO.util.EventProvider);(function(){var a=YAHOO.util.Event,c=YAHOO.lang,b=[],d=function(h,e,f){var g;if(!h||h===f){g=false;}else{g=YAHOO.util.Selector.test(h,e)?h:d(h.parentNode,e,f);}return g;};c.augmentObject(a,{_createDelegate:function(f,e,g,h){return function(i){var j=this,n=a.getTarget(i),l=e,p=(j.nodeType===9),q,k,o,m;if(c.isFunction(e)){q=e(n);}else{if(c.isString(e)){if(!p){o=j.id;if(!o){o=a.generateId(j);}m=("#"+o+" ");l=(m+e).replace(/,/gi,(","+m));}if(YAHOO.util.Selector.test(n,l)){q=n;}else{if(YAHOO.util.Selector.test(n,((l.replace(/,/gi," *,"))+" *"))){q=d(n,l,j);}}}}if(q){k=q;if(h){if(h===true){k=g;}else{k=h;}}return f.call(k,i,q,j,g);}};},delegate:function(f,j,l,g,h,i){var e=j,k,m;if(c.isString(g)&&!YAHOO.util.Selector){return false;}if(j=="mouseenter"||j=="mouseleave"){if(!a._createMouseDelegate){return false;}e=a._getType(j);k=a._createMouseDelegate(l,h,i);m=a._createDelegate(function(p,o,n){return k.call(o,p,n);},g,h,i);}else{m=a._createDelegate(l,g,h,i);}b.push([f,e,l,m]);return a.on(f,e,m);},removeDelegate:function(f,j,i){var k=j,h=false,g,e;if(j=="mouseenter"||j=="mouseleave"){k=a._getType(j);}g=a._getCacheIndex(b,f,k,i);if(g>=0){e=b[g];}if(f&&e){h=a.removeListener(e[0],e[1],e[3]);if(h){delete b[g][2];delete b[g][3];b.splice(g,1);}}return h;}});}());(function(){var b=YAHOO.util.Event,g=YAHOO.lang,e=b.addListener,f=b.removeListener,c=b.getListeners,d=[],h={mouseenter:"mouseover",mouseleave:"mouseout"},a=function(n,m,l){var j=b._getCacheIndex(d,n,m,l),i,k;if(j>=0){i=d[j];}if(n&&i){k=f.call(b,i[0],m,i[3]);if(k){delete d[j][2];delete d[j][3];d.splice(j,1);}}return k;};g.augmentObject(b._specialTypes,h);g.augmentObject(b,{_createMouseDelegate:function(i,j,k){return function(q,m){var p=this,l=b.getRelatedTarget(q),o,n;if(p!=l&&!YAHOO.util.Dom.isAncestor(p,l)){o=p;if(k){if(k===true){o=j;}else{o=k;}}n=[q,j];if(m){n.splice(1,0,p,m);}return i.apply(o,n);}};},addListener:function(m,l,k,n,o){var i,j;if(h[l]){i=b._createMouseDelegate(k,n,o);i.mouseDelegate=true;d.push([m,l,k,i]);j=e.call(b,m,l,i);}else{j=e.apply(b,arguments);}return j;},removeListener:function(l,k,j){var i;if(h[k]){i=a.apply(b,arguments);}else{i=f.apply(b,arguments);}return i;},getListeners:function(p,o){var n=[],r,m=(o==="mouseover"||o==="mouseout"),q,k,j;if(o&&(m||h[o])){r=c.call(b,p,this._getType(o));if(r){for(k=r.length-1;k>-1;k--){j=r[k];q=j.fn.mouseDelegate;if((h[o]&&q)||(m&&!q)){n.push(j);}}}}else{n=c.apply(b,arguments);}return(n&&n.length)?n:null;}},true);b.on=b.addListener;}());YAHOO.register("event-mouseenter",YAHOO.util.Event,{version:"2.9.0",build:"2800"});var Y=YAHOO,Y_DOM=YAHOO.util.Dom,EMPTY_ARRAY=[],Y_UA=Y.env.ua,Y_Lang=Y.lang,Y_DOC=document,Y_DOCUMENT_ELEMENT=Y_DOC.documentElement,Y_DOM_inDoc=Y_DOM.inDocument,Y_mix=Y_Lang.augmentObject,Y_guid=Y_DOM.generateId,Y_getDoc=function(a){var b=Y_DOC;if(a){b=(a.nodeType===9)?a:a.ownerDocument||a.document||Y_DOC;}return b;},Y_Array=function(g,d){var c,b,h=d||0;try{return Array.prototype.slice.call(g,h);}catch(f){b=[];c=g.length;for(;hc){return 1;}}return -1;}:(Y_DOCUMENT_ELEMENT[COMPARE_DOCUMENT_POSITION]?function(b,a){if(b[COMPARE_DOCUMENT_POSITION](a)&4){return -1;}else{return 1;}}:function(e,d){var c,a,b;if(e&&d){c=e[OWNER_DOCUMENT].createRange();c.setStart(e,0);a=d[OWNER_DOCUMENT].createRange();a.setStart(d,0);b=c.compareBoundaryPoints(1,a);}return b;}),_sort:function(a){if(a){a=Y_Array(a,0,true);if(a.sort){a.sort(Selector._compare);}}return a;},_deDupe:function(a){var b=[],c,d;for(c=0;(d=a[c++]);){if(!d._found){b[b.length]=d;d._found=true;}}for(c=0;(d=b[c++]);){d._found=null;d.removeAttribute("_found");}return b;},query:function(b,j,k,a){if(typeof j=="string"){j=Y_DOM.get(j);if(!j){return(k)?null:[];}}else{j=j||Y_DOC;}var f=[],c=(Selector.useNative&&Y_DOC.querySelector&&!a),e=[[b,j]],g,l,d,h=(c)?Selector._nativeQuery:Selector._bruteQuery;if(b&&h){if(!a&&(!c||j.tagName)){e=Selector._splitQueries(b,j);}for(d=0;(g=e[d++]);){l=h(g[0],g[1],k);if(!k){l=Y_Array(l,0,true);}if(l){f=f.concat(l);}}if(e.length>1){f=Selector._sort(Selector._deDupe(f));}}Y.log("query: "+b+" returning: "+f.length,"info","Selector");return(k)?(f[0]||null):f;},_splitQueries:function(c,f){var b=c.split(","),d=[],g="",e,a;if(f){if(f.tagName){f.id=f.id||Y_guid();g='[id="'+f.id+'"] ';}for(e=0,a=b.length;e-1&&(Selector.pseudos&&Selector.pseudos.checked)){return Selector.query(a,b,c,true); +}try{return b["querySelector"+(c?"":"All")](a);}catch(d){return Selector.query(a,b,c,true);}},filter:function(b,a){var c=[],d,e;if(b&&a){for(d=0;(e=b[d++]);){if(Selector.test(e,a)){c[c.length]=e;}}}else{Y.log("invalid filter input (nodes: "+b+", selector: "+a+")","warn","Selector");}return c;},test:function(c,d,k){var g=false,b=d.split(","),a=false,l,o,h,n,f,e,m;if(c&&c.tagName){if(!k&&!Y_DOM_inDoc(c)){l=c.parentNode;if(l){k=l;}else{n=c[OWNER_DOCUMENT].createDocumentFragment();n.appendChild(c);k=n;a=true;}}k=k||c[OWNER_DOCUMENT];if(!c.id){c.id=Y_guid();}for(f=0;(m=b[f++]);){m+='[id="'+c.id+'"]';h=Selector.query(m,k);for(e=0;o=h[e++];){if(o===c){g=true;break;}}if(g){break;}}if(a){n.removeChild(c);}}return g;}};YAHOO.util.Selector=Selector;var PARENT_NODE="parentNode",TAG_NAME="tagName",ATTRIBUTES="attributes",COMBINATOR="combinator",PSEUDOS="pseudos",SelectorCSS2={_reRegExpTokens:/([\^\$\?\[\]\*\+\-\.\(\)\|\\])/,SORT_RESULTS:true,_children:function(e,a){var b=e.children,d,c=[],f,g;if(e.children&&a&&e.children.tags){c=e.children.tags(a);}else{if((!b&&e[TAG_NAME])||(b&&a)){f=b||e.childNodes;b=[];for(d=0;(g=f[d++]);){if(g.tagName){if(!a||a===g.tagName){b.push(g);}}}}}return b||[];},_re:{attr:/(\[[^\]]*\])/g,esc:/\\[:\[\]\(\)#\.\'\>+~"]/gi,pseudos:/(\([^\)]*\))/g},shorthand:{"\\#(-?[_a-z]+[-\\w\\uE000]*)":"[id=$1]","\\.(-?[_a-z]+[-\\w\\uE000]*)":"[className~=$1]"},operators:{"":function(b,a){return !!b.getAttribute(a);},"~=":"(?:^|\\s+){val}(?:\\s+|$)","|=":"^{val}(?:-|$)"},pseudos:{"first-child":function(a){return Selector._children(a[PARENT_NODE])[0]===a;}},_bruteQuery:function(f,j,l){var g=[],a=[],i=Selector._tokenize(f),e=i[i.length-1],k=Y_getDoc(j),c,b,h,d;if(e){b=e.id;h=e.className;d=e.tagName||"*";if(j.getElementsByTagName){if(b&&(j.all||(j.nodeType===9||Y_DOM_inDoc(j)))){a=Y_DOM_allById(b,j);}else{if(h){a=j.getElementsByClassName(h);}else{a=j.getElementsByTagName(d);}}}else{c=j.firstChild;while(c){if(c.tagName){a.push(c);}c=c.nextSilbing||c.firstChild;}}if(a.length){g=Selector._filterNodes(a,i,l);}}return g;},_filterNodes:function(l,f,h){var r=0,q,s=f.length,k=s-1,e=[],o=l[0],v=o,t=Selector.getters,d,p,c,g,a,m,b,u;for(r=0;(v=o=l[r++]);){k=s-1;g=null;testLoop:while(v&&v.tagName){c=f[k];b=c.tests;q=b.length;if(q&&!a){while((u=b[--q])){d=u[1];if(t[u[0]]){m=t[u[0]](v,u[0]);}else{m=v[u[0]];if(m===undefined&&v.getAttribute){m=v.getAttribute(u[0]);}}if((d==="="&&m!==u[2])||(typeof d!=="string"&&d.test&&!d.test(m))||(!d.test&&typeof d==="function"&&!d(v,u[0],u[2]))){if((v=v[g])){while(v&&(!v.tagName||(c.tagName&&c.tagName!==v.tagName))){v=v[g];}}continue testLoop;}}}k--;if(!a&&(p=c.combinator)){g=p.axis;v=v[g];while(v&&!v.tagName){v=v[g];}if(p.direct){g=null;}}else{e.push(o);if(h){return e;}break;}}}o=v=null;return e;},combinators:{" ":{axis:"parentNode"},">":{axis:"parentNode",direct:true},"+":{axis:"previousSibling",direct:true}},_parsers:[{name:ATTRIBUTES,re:/^\uE003(-?[a-z]+[\w\-]*)+([~\|\^\$\*!=]=?)?['"]?([^\uE004'"]*)['"]?\uE004/i,fn:function(d,e){var c=d[2]||"",a=Selector.operators,b=(d[3])?d[3].replace(/\\/g,""):"",f;if((d[1]==="id"&&c==="=")||(d[1]==="className"&&Y_DOCUMENT_ELEMENT.getElementsByClassName&&(c==="~="||c==="="))){e.prefilter=d[1];d[3]=b;e[d[1]]=(d[1]==="id")?d[3]:b;}if(c in a){f=a[c];if(typeof f==="string"){d[3]=b.replace(Selector._reRegExpTokens,"\\$1");f=new RegExp(f.replace("{val}",d[3]));}d[2]=f;}if(!e.last||e.prefilter!==d[1]){return d.slice(1);}}},{name:TAG_NAME,re:/^((?:-?[_a-z]+[\w-]*)|\*)/i,fn:function(b,c){var a=b[1].toUpperCase();c.tagName=a;if(a!=="*"&&(!c.last||c.prefilter)){return[TAG_NAME,"=",a];}if(!c.prefilter){c.prefilter="tagName";}}},{name:COMBINATOR,re:/^\s*([>+~]|\s)\s*/,fn:function(a,b){}},{name:PSEUDOS,re:/^:([\-\w]+)(?:\uE005['"]?([^\uE005]*)['"]?\uE006)*/i,fn:function(a,b){var c=Selector[PSEUDOS][a[1]];if(c){if(a[2]){a[2]=a[2].replace(/\\/g,"");}return[a[2],c];}else{return false;}}}],_getToken:function(a){return{tagName:null,id:null,className:null,attributes:{},combinator:null,tests:[]};},_tokenize:function(c){c=c||"";c=Selector._replaceShorthand(Y_Lang.trim(c));var b=Selector._getToken(),h=c,g=[],j=false,e,f,d,a;outer:do{j=false;for(d=0;(a=Selector._parsers[d++]);){if((e=a.re.exec(c))){if(a.name!==COMBINATOR){b.selector=c;}c=c.replace(e[0],"");if(!c.length){b.last=true;}if(Selector._attrFilters[e[1]]){e[1]=Selector._attrFilters[e[1]];}f=a.fn(e,b);if(f===false){j=false;break outer;}else{if(f){b.tests.push(f);}}if(!c.length||a.name===COMBINATOR){g.push(b);b=Selector._getToken(b);if(a.name===COMBINATOR){b.combinator=Selector.combinators[e[1]];}}j=true;}}}while(j&&c.length);if(!j||c.length){Y.log("query: "+h+" contains unsupported token in: "+c,"warn","Selector");g=[];}return g;},_replaceShorthand:function(b){var d=Selector.shorthand,c=b.match(Selector._re.esc),e,h,g,f,a;if(c){b=b.replace(Selector._re.esc,"\uE000");}e=b.match(Selector._re.attr);h=b.match(Selector._re.pseudos);if(e){b=b.replace(Selector._re.attr,"\uE001");}if(h){b=b.replace(Selector._re.pseudos,"\uE002");}for(g in d){if(d.hasOwnProperty(g)){b=b.replace(new RegExp(g,"gi"),d[g]);}}if(e){for(f=0,a=e.length;f=0&&l[e]===d){return true;}}}else{for(var e=l.length-k,g=l.length;e>=0;e-=m){if(e-1;},"checked":function(a){return(a.checked===true||a.selected===true);},enabled:function(a){return(a.disabled!==undefined&&!a.disabled);},disabled:function(a){return(a.disabled);}});Y_mix(Selector.operators,{"^=":"^{val}","!=":function(b,a,c){return b[a]!==c;},"$=":"{val}$","*=":"{val}"});Selector.combinators["~"]={axis:"previousSibling"};YAHOO.register("selector",YAHOO.util.Selector,{version:"2.9.0",build:"2800"});var Dom=YAHOO.util.Dom;YAHOO.widget.ColumnSet=function(a){this._sId=Dom.generateId(null,"yui-cs");a=YAHOO.widget.DataTable._cloneObject(a);this._init(a);YAHOO.widget.ColumnSet._nCount++;};YAHOO.widget.ColumnSet._nCount=0;YAHOO.widget.ColumnSet.prototype={_sId:null,_aDefinitions:null,tree:null,flat:null,keys:null,headers:null,_init:function(j){var k=[];var a=[];var g=[];var e=[];var c=-1;var b=function(m,s){c++;if(!k[c]){k[c]=[];}for(var o=0;on){n=p;}}}};for(var i=0;i-1;b--){if(a[b]._sId===c){return a[b];}}}return null;},getColumn:function(c){if(YAHOO.lang.isNumber(c)&&this.keys[c]){return this.keys[c];}else{if(YAHOO.lang.isString(c)){var a=this.flat;var d=[];for(var b=0;b1){return d;}}}}return null;},getDescendants:function(d){var b=this;var c=[];var a;var e=function(f){c.push(f);if(f.children){for(a=0;ac){return(e)?-1:1;}else{return 0;}}}};YAHOO.widget.ColumnDD=function(d,a,c,b){if(d&&a&&c&&b){this.datatable=d;this.table=d.getTableEl();this.column=a;this.headCell=c;this.pointer=b;this.newIndex=null;this.init(c);this.initFrame();this.invalidHandleTypes={};this.setPadding(10,0,(this.datatable.getTheadEl().offsetHeight+10),0);YAHOO.util.Event.on(window,"resize",function(){this.initConstraints();},this,true);}else{}};if(YAHOO.util.DDProxy){YAHOO.extend(YAHOO.widget.ColumnDD,YAHOO.util.DDProxy,{initConstraints:function(){var g=YAHOO.util.Dom.getRegion(this.table),d=this.getEl(),f=YAHOO.util.Dom.getXY(d),c=parseInt(YAHOO.util.Dom.getStyle(d,"width"),10),a=parseInt(YAHOO.util.Dom.getStyle(d,"height"),10),e=((f[0]-g.left)+15),b=((g.right-f[0]-c)+15);this.setXConstraint(e,b);this.setYConstraint(10,10);},_resizeProxy:function(){YAHOO.widget.ColumnDD.superclass._resizeProxy.apply(this,arguments);var a=this.getDragEl(),b=this.getEl();YAHOO.util.Dom.setStyle(this.pointer,"height",(this.table.parentNode.offsetHeight+10)+"px");YAHOO.util.Dom.setStyle(this.pointer,"display","block");var c=YAHOO.util.Dom.getXY(b);YAHOO.util.Dom.setXY(this.pointer,[c[0],(c[1]-5)]);YAHOO.util.Dom.setStyle(a,"height",this.datatable.getContainerEl().offsetHeight+"px");YAHOO.util.Dom.setStyle(a,"width",(parseInt(YAHOO.util.Dom.getStyle(a,"width"),10)+4)+"px");YAHOO.util.Dom.setXY(this.dragEl,c);},onMouseDown:function(){this.initConstraints();this.resetConstraints();},clickValidator:function(b){if(!this.column.hidden){var a=YAHOO.util.Event.getTarget(b);return(this.isValidHandleChild(a)&&(this.id==this.handleElId||this.DDM.handleWasClicked(a,this.id)));}},onDragOver:function(h,a){var f=this.datatable.getColumn(a);if(f){var c=f.getTreeIndex();while((c===null)&&f.getParent()){f=f.getParent();c=f.getTreeIndex();}if(c!==null){var b=f.getThEl();var k=c;var d=YAHOO.util.Event.getPageX(h),i=YAHOO.util.Dom.getX(b),j=i+((YAHOO.util.Dom.get(b).offsetWidth)/2),e=this.column.getTreeIndex();if(de){k--;}if(k<0){k=0;}else{if(k>this.datatable.getColumnSet().tree[0].length){k=this.datatable.getColumnSet().tree[0].length;}}this.newIndex=k;}}},onDragDrop:function(){this.datatable.reorderColumn(this.column,this.newIndex);},endDrag:function(){this.newIndex=null;YAHOO.util.Dom.setStyle(this.pointer,"display","none");}});}YAHOO.util.ColumnResizer=function(e,c,d,a,b){if(e&&c&&d&&a){this.datatable=e;this.column=c;this.headCell=d;this.headCellLiner=c.getThLinerEl();this.resizerLiner=d.firstChild;this.init(a,a,{dragOnly:true,dragElId:b.id});this.initFrame();this.resetResizerEl();this.setPadding(0,1,0,0);}else{}};if(YAHOO.util.DD){YAHOO.extend(YAHOO.util.ColumnResizer,YAHOO.util.DDProxy,{resetResizerEl:function(){var a=YAHOO.util.Dom.get(this.handleElId).style;a.left="auto";a.right=0;a.top="auto";a.bottom=0;a.height=this.headCell.offsetHeight+"px";},onMouseUp:function(h){var f=this.datatable.getColumnSet().keys,b;for(var c=0,a=f.length;cYAHOO.util.Dom.getX(this.headCellLiner)){var a=d-this.startX;var b=this.startWidth+a-this.nLinerPadding;if(b>0){this.datatable.setColumnWidth(this.column,b);}}}});}(function(){var g=YAHOO.lang,a=YAHOO.util,e=YAHOO.widget,c=a.Dom,f=a.Event,d=e.DataTable;YAHOO.widget.RecordSet=function(h){this._init(h);};var b=e.RecordSet;b._nCount=0;b.prototype={_sId:null,_init:function(h){this._sId=c.generateId(null,"yui-rs");e.RecordSet._nCount++;this._records=[];this._initEvents();if(h){if(g.isArray(h)){this.addRecords(h);}else{if(g.isObject(h)){this.addRecord(h);}}}},_initEvents:function(){this.createEvent("recordAddEvent");this.createEvent("recordsAddEvent");this.createEvent("recordSetEvent");this.createEvent("recordsSetEvent");this.createEvent("recordUpdateEvent");this.createEvent("recordDeleteEvent");this.createEvent("recordsDeleteEvent");this.createEvent("resetEvent");this.createEvent("recordValueUpdateEvent");},_addRecord:function(j,h){var i=new YAHOO.widget.Record(j);if(YAHOO.lang.isNumber(h)&&(h>-1)){this._records.splice(h,0,i);}else{this._records[this._records.length]=i;}return i;},_setRecord:function(i,h){if(!g.isNumber(h)||h<0){h=this._records.length;}return(this._records[h]=new e.Record(i));},_deleteRecord:function(i,h){if(!g.isNumber(h)||(h<0)){h=1;}this._records.splice(i,h);},getId:function(){return this._sId;},toString:function(){return"RecordSet instance "+this._sId;},getLength:function(){return this._records.length;},getRecord:function(h){var j;if(h instanceof e.Record){for(j=0;j-1)&&(h-1;h--){if(this._records[h]&&j.getId()===this._records[h].getId()){return h;}}}return null;},addRecord:function(j,h){if(g.isObject(j)){var i=this._addRecord(j,h);this.fireEvent("recordAddEvent",{record:i,data:j});return i;}else{return null;}},addRecords:function(m,l){if(g.isArray(m)){var p=[],j,n,h;l=g.isNumber(l)?l:this._records.length;j=l;for(n=0,h=m.length;n-1)&&(h-1)&&(k'+l+"";},formatCheckbox:function(i,j,k,n,m){var l=n;l=(l)?' checked="checked"':"";i.innerHTML='';},formatCurrency:function(j,k,l,n,m){var i=m||this;j.innerHTML=a.Number.format(n,l.currencyOptions||i.get("currencyOptions"));},formatDate:function(j,l,m,o,n){var i=n||this,k=m.dateOptions||i.get("dateOptions");j.innerHTML=a.Date.format(o,k,k.locale);},formatDropdown:function(l,u,q,j,t){var s=t||this,r=(h.isValue(j))?j:u.getData(q.field),v=(h.isArray(q.dropdownOptions))?q.dropdownOptions:null,k,p=l.getElementsByTagName("select");if(p.length===0){k=document.createElement("select");k.className=d.CLASS_DROPDOWN;k=l.appendChild(k);g.addListener(k,"change",s._onDropdownChange,s);}k=p[0];if(k){k.innerHTML="";if(v){for(var n=0;n'+r+"";}}else{l.innerHTML=h.isValue(j)?j:"";}},formatEmail:function(i,j,k,m,l){if(h.isString(m)){m=h.escapeHTML(m);i.innerHTML=''+m+"";}else{i.innerHTML=h.isValue(m)?h.escapeHTML(m.toString()):"";}},formatLink:function(i,j,k,m,l){if(h.isString(m)){m=h.escapeHTML(m);i.innerHTML=''+m+"";}else{i.innerHTML=h.isValue(m)?h.escapeHTML(m.toString()):"";}},formatNumber:function(j,k,l,n,m){var i=m||this;j.innerHTML=a.Number.format(n,l.numberOptions||i.get("numberOptions"));},formatRadio:function(j,k,l,o,n){var i=n||this,m=o;m=(m)?' checked="checked"':"";j.innerHTML='';},formatText:function(i,j,l,n,m){var k=(h.isValue(n))?n:"";i.innerHTML=h.escapeHTML(k.toString());},formatTextarea:function(j,k,m,o,n){var l=(h.isValue(o))?h.escapeHTML(o.toString()):"",i="";j.innerHTML=i;},formatTextbox:function(j,k,m,o,n){var l=(h.isValue(o))?h.escapeHTML(o.toString()):"",i='';j.innerHTML=i;},formatDefault:function(i,j,k,m,l){i.innerHTML=(h.isValue(m)&&m!=="")?m.toString():" ";},validateNumber:function(j){var i=j*1;if(h.isNumber(i)){return i;}else{return undefined;}}});d.Formatter={button:d.formatButton,checkbox:d.formatCheckbox,currency:d.formatCurrency,"date":d.formatDate,dropdown:d.formatDropdown,email:d.formatEmail,link:d.formatLink,"number":d.formatNumber,radio:d.formatRadio,text:d.formatText,textarea:d.formatTextarea,textbox:d.formatTextbox,defaultFormatter:d.formatDefault};h.extend(d,a.Element,{initAttributes:function(i){i=i||{};d.superclass.initAttributes.call(this,i);this.setAttributeConfig("summary",{value:"",validator:h.isString,method:function(j){if(this._elTable){this._elTable.summary=j;}}});this.setAttributeConfig("selectionMode",{value:"standard",validator:h.isString});this.setAttributeConfig("sortedBy",{value:null,validator:function(j){if(j){return(h.isObject(j)&&j.key); +}else{return(j===null);}},method:function(k){var r=this.get("sortedBy");this._configs.sortedBy.value=k;var j,o,m,q;if(this._elThead){if(r&&r.key&&r.dir){j=this._oColumnSet.getColumn(r.key);o=j.getKeyIndex();var u=j.getThEl();c.removeClass(u,r.dir);this.formatTheadCell(j.getThLinerEl().firstChild,j,k);}if(k){m=(k.column)?k.column:this._oColumnSet.getColumn(k.key);q=m.getKeyIndex();var v=m.getThEl();if(k.dir&&((k.dir=="asc")||(k.dir=="desc"))){var p=(k.dir=="desc")?d.CLASS_DESC:d.CLASS_ASC;c.addClass(v,p);}else{var l=k.dir||d.CLASS_ASC;c.addClass(v,l);}this.formatTheadCell(m.getThLinerEl().firstChild,m,k);}}if(this._elTbody){this._elTbody.style.display="none";var s=this._elTbody.rows,t;for(var n=s.length-1;n>-1;n--){t=s[n].childNodes;if(t[o]){c.removeClass(t[o],r.dir);}if(t[q]){c.addClass(t[q],k.dir);}}this._elTbody.style.display="";}this._clearTrTemplateEl();}});this.setAttributeConfig("paginator",{value:null,validator:function(j){return j===null||j instanceof e.Paginator;},method:function(){this._updatePaginator.apply(this,arguments);}});this.setAttributeConfig("caption",{value:null,validator:h.isString,method:function(j){this._initCaptionEl(j);}});this.setAttributeConfig("draggableColumns",{value:false,validator:h.isBoolean,method:function(j){if(this._elThead){if(j){this._initDraggableColumns();}else{this._destroyDraggableColumns();}}}});this.setAttributeConfig("renderLoopSize",{value:0,validator:h.isNumber});this.setAttributeConfig("sortFunction",{value:function(k,j,o,n){var m=YAHOO.util.Sort.compare,l=m(k.getData(n),j.getData(n),o);if(l===0){return m(k.getCount(),j.getCount(),o);}else{return l;}}});this.setAttributeConfig("formatRow",{value:null,validator:h.isFunction});this.setAttributeConfig("generateRequest",{value:function(k,n){k=k||{pagination:null,sortedBy:null};var m=encodeURIComponent((k.sortedBy)?k.sortedBy.key:n.getColumnSet().keys[0].getKey());var j=(k.sortedBy&&k.sortedBy.dir===YAHOO.widget.DataTable.CLASS_DESC)?"desc":"asc";var o=(k.pagination)?k.pagination.recordOffset:0;var l=(k.pagination)?k.pagination.rowsPerPage:null;return"sort="+m+"&dir="+j+"&startIndex="+o+((l!==null)?"&results="+l:"");},validator:h.isFunction});this.setAttributeConfig("initialRequest",{value:null});this.setAttributeConfig("initialLoad",{value:true});this.setAttributeConfig("dynamicData",{value:false,validator:h.isBoolean});this.setAttributeConfig("MSG_EMPTY",{value:"No records found.",validator:h.isString});this.setAttributeConfig("MSG_LOADING",{value:"Loading...",validator:h.isString});this.setAttributeConfig("MSG_ERROR",{value:"Data error.",validator:h.isString});this.setAttributeConfig("MSG_SORTASC",{value:"Click to sort ascending",validator:h.isString,method:function(k){if(this._elThead){for(var l=0,m=this.getColumnSet().keys,j=m.length;l=0;--j){if(k[j].editor){g.purgeElement(k[j].editor._elContainer);}}m.innerHTML="";this._elContainer=null;this._elColgroup=null;this._elThead=null;this._elTbody=null;},_initContainerEl:function(j){j=c.get(j);if(j&&j.nodeName&&(j.nodeName.toLowerCase()=="div")){this._destroyContainerEl(j);c.addClass(j,d.CLASS_DATATABLE);g.addListener(j,"focus",this._onTableFocus,this);g.addListener(j,"dblclick",this._onTableDblclick,this);this._elContainer=j;var i=document.createElement("div");i.className=d.CLASS_MASK;i.style.display="none";this._elMask=j.appendChild(i);}},_destroyTableEl:function(){var i=this._elTable;if(i){g.purgeElement(i,true);i.parentNode.removeChild(i);this._elCaption=null;this._elColgroup=null;this._elThead=null;this._elTbody=null;}},_initCaptionEl:function(i){if(this._elTable&&i){if(!this._elCaption){this._elCaption=this._elTable.createCaption();}this._elCaption.innerHTML=i;}else{if(this._elCaption){this._elCaption.parentNode.removeChild(this._elCaption);}}},_initTableEl:function(i){if(i){this._destroyTableEl();this._elTable=i.appendChild(document.createElement("table"));this._elTable.summary=this.get("summary");if(this.get("caption")){this._initCaptionEl(this.get("caption"));}g.delegate(this._elTable,"mouseenter",this._onTableMouseover,"thead ."+d.CLASS_LABEL,this);g.delegate(this._elTable,"mouseleave",this._onTableMouseout,"thead ."+d.CLASS_LABEL,this);g.delegate(this._elTable,"mouseenter",this._onTableMouseover,"tbody.yui-dt-data>tr>td",this);g.delegate(this._elTable,"mouseleave",this._onTableMouseout,"tbody.yui-dt-data>tr>td",this);g.delegate(this._elTable,"mouseenter",this._onTableMouseover,"tbody.yui-dt-message>tr>td",this);g.delegate(this._elTable,"mouseleave",this._onTableMouseout,"tbody.yui-dt-message>tr>td",this);}},_destroyColgroupEl:function(){var i=this._elColgroup;if(i){var j=i.parentNode;g.purgeElement(i,true);j.removeChild(i);this._elColgroup=null;}},_initColgroupEl:function(s){if(s){this._destroyColgroupEl();var l=this._aColIds||[],r=this._oColumnSet.keys,m=0,p=l.length,j,o,q=document.createDocumentFragment(),n=document.createElement("col");for(m=0,p=r.length;ml[l.length-1])){var j,n=[];for(j=l.length-1;j>-1;j--){n.push(this._elColgroup.removeChild(this._elColgroup.childNodes[l[j]]));}var m=this._elColgroup.childNodes[k]||null;for(j=n.length-1;j>-1;j--){this._elColgroup.insertBefore(n[j],m);}}},_destroyTheadEl:function(){var j=this._elThead;if(j){var i=j.parentNode;g.purgeElement(j,true);this._destroyColumnHelpers();i.removeChild(j);this._elThead=null;}},_initTheadEl:function(v){v=v||this._elTable;if(v){this._destroyTheadEl();var q=(this._elColgroup)?v.insertBefore(document.createElement("thead"),this._elColgroup.nextSibling):v.appendChild(document.createElement("thead"));g.addListener(q,"focus",this._onTheadFocus,this);g.addListener(q,"keydown",this._onTheadKeydown,this);g.addListener(q,"mousedown",this._onTableMousedown,this);g.addListener(q,"mouseup",this._onTableMouseup,this);g.addListener(q,"click",this._onTheadClick,this);var x=this._oColumnSet,t,r,p,n;var w=x.tree;var o;for(r=0;r'+p+"";}else{i.innerHTML=p;}},_destroyDraggableColumns:function(){var l,m;for(var k=0,j=this._oColumnSet.tree[0].length;k-2)&&(j1)){s=q+l;}}}for(var k=q;k0)||(n.length>0)){var m=this._oColumnSet,k;for(var j=0;j-1){if(this.getRecord(k[j])){return k[j];}j--;}return null;},getNextTrEl:function(l,i){var j=this.getTrIndex(l);if(j!==null){var k=this._elTbody.rows;if(i){while(j0){l=k[j-1];if(this.getRecord(l)){return l;}j--;}}else{if(j>0){return k[j-1];}}}return null;},getCellIndex:function(k){k=this.getTdEl(k);if(k){if(b.ie>0){var l=0,n=k.parentNode,m=n.childNodes,j=m.length;for(;l0){return j.cells[k]||null;}}}return null;},getFirstTdEl:function(j){var i=h.isValue(j)?this.getTrEl(j):this.getFirstTrEl();if(i){if(i.cells&&i.cells.length>0){return i.cells[0];}else{if(i.childNodes&&i.childNodes.length>0){return i.childNodes[0];}}}return null;},getLastTdEl:function(j){var i=h.isValue(j)?this.getTrEl(j):this.getLastTrEl();if(i){if(i.cells&&i.cells.length>0){return i.cells[i.cells.length-1];}else{if(i.childNodes&&i.childNodes.length>0){return i.childNodes[i.childNodes.length-1];}}}return null;},getNextTdEl:function(i){var m=this.getTdEl(i);if(m){var k=this.getCellIndex(m);var j=this.getTrEl(m);if(j.cells&&(j.cells.length)>0&&(k0&&(k0){if(j.cells&&j.cells.length>0){return j.cells[k-1];}else{if(j.childNodes&&j.childNodes.length>0){return j.childNodes[k-1];}}}else{var l=this.getPreviousTrEl(j);if(l){return this.getLastTdEl(l);}}}return null;},getAboveTdEl:function(j,i){var m=this.getTdEl(j);if(m){var l=this.getPreviousTrEl(m,i);if(l){var k=this.getCellIndex(m);if(l.cells&&l.cells.length>0){return l.cells[k]?l.cells[k]:null;}else{if(l.childNodes&&l.childNodes.length>0){return l.childNodes[k]?l.childNodes[k]:null;}}}}return null;},getBelowTdEl:function(j,i){var m=this.getTdEl(j);if(m){var l=this.getNextTrEl(m,i);if(l){var k=this.getCellIndex(m);if(l.cells&&l.cells.length>0){return l.cells[k]?l.cells[k]:null;}else{if(l.childNodes&&l.childNodes.length>0){return l.childNodes[k]?l.childNodes[k]:null;}}}}return null;},getThLinerEl:function(j){var i=this.getColumn(j);return(i)?i.getThLinerEl():null;},getThEl:function(k){var l;if(k instanceof YAHOO.widget.Column){var j=k;l=j.getThEl();if(l){return l;}}else{var i=c.get(k);if(i&&(i.ownerDocument==document)){if(i.nodeName.toLowerCase()!="th"){l=c.getAncestorByTagName(i,"th"); +}else{l=i;}return l;}}return null;},getTrIndex:function(m){var i=this.getRecord(m),k=this.getRecordIndex(i),l;if(i){l=this.getTrEl(i);if(l){return l.sectionRowIndex;}else{var j=this.get("paginator");if(j){return j.get("recordOffset")+k;}else{return k;}}}return null;},load:function(i){i=i||{};(i.datasource||this._oDataSource).sendRequest(i.request||this.get("initialRequest"),i.callback||{success:this.onDataReturnInitializeTable,failure:this.onDataReturnInitializeTable,scope:this,argument:this.getState()});},initializeTable:function(){this._bInit=true;this._oRecordSet.reset();var i=this.get("paginator");if(i){i.set("totalRecords",0);}this._unselectAllTrEls();this._unselectAllTdEls();this._aSelections=null;this._oAnchorRecord=null;this._oAnchorCell=null;this.set("sortedBy",null);},_runRenderChain:function(){this._oChainRender.run();},_getViewRecords:function(){var i=this.get("paginator");if(i){return this._oRecordSet.getRecords(i.getStartIndex(),i.getRowsPerPage());}else{return this._oRecordSet.getRecords();}},render:function(){this._oChainRender.stop();this.fireEvent("beforeRenderEvent");var r,p,o,s,l=this._getViewRecords();var m=this._elTbody,q=this.get("renderLoopSize"),t=l.length;if(t>0){m.style.display="none";while(m.lastChild){m.removeChild(m.lastChild);}m.style.display="";this._oChainRender.add({method:function(u){if((this instanceof d)&&this._sId){var k=u.nCurrentRecord,w=((u.nCurrentRecord+u.nLoopLength)>t)?t:(u.nCurrentRecord+u.nLoopLength),j,v;m.style.display="none";for(;k0)?Math.ceil(t/q):1,argument:{nCurrentRecord:0,nLoopLength:(q>0)?q:t},timeout:(q>0)?0:-1});this._oChainRender.add({method:function(i){if((this instanceof d)&&this._sId){while(m.rows.length>t){m.removeChild(m.lastChild);}this._setFirstRow();this._setLastRow();this._setRowStripes();this._setSelections();}},scope:this,timeout:(q>0)?0:-1});}else{var n=m.rows.length;if(n>0){this._oChainRender.add({method:function(k){if((this instanceof d)&&this._sId){var j=k.nCurrent,v=k.nLoopLength,u=(j-v<0)?0:j-v;m.style.display="none";for(;j>u;j--){m.deleteRow(-1);}m.style.display="";k.nCurrent=j;}},scope:this,iterations:(q>0)?Math.ceil(n/q):1,argument:{nCurrent:n,nLoopLength:(q>0)?q:n},timeout:(q>0)?0:-1});}}this._runRenderChain();},disable:function(){this._disabled=true;var i=this._elTable;var j=this._elMask;j.style.width=i.offsetWidth+"px";j.style.height=i.offsetHeight+"px";j.style.left=i.offsetLeft+"px";j.style.display="";this.fireEvent("disableEvent");},undisable:function(){this._disabled=false;this._elMask.style.display="none";this.fireEvent("undisableEvent");},isDisabled:function(){return this._disabled;},destroy:function(){var k=this.toString();this._oChainRender.stop();this._destroyColumnHelpers();var m;for(var l=0,j=this._oColumnSet.flat.length;lj.minWidth)?i:j.minWidth;j.width=i;this._setColumnWidth(j,i+"px");this.fireEvent("columnSetWidthEvent",{column:j,width:i});}else{if(i===null){j.width=i;this._setColumnWidth(j,"auto");this.validateColumnWidths(j);this.fireEvent("columnUnsetWidthEvent",{column:j});}}this._clearTrTemplateEl();}else{}},_setColumnWidth:function(j,i,k){if(j&&(j.getKeyIndex()!==null)){k=k||(((i==="")||(i==="auto"))?"visible":"hidden");if(!d._bDynStylesFallback){this._setColumnWidthDynStyles(j,i,k);}else{this._setColumnWidthDynFunction(j,i,k);}}else{}},_setColumnWidthDynStyles:function(m,l,n){var j=d._elDynStyleNode,k;if(!j){j=document.createElement("style");j.type="text/css";j=document.getElementsByTagName("head").item(0).appendChild(j);d._elDynStyleNode=j;}if(j){var i="."+this.getId()+"-col-"+m.getSanitizedKey()+" ."+d.CLASS_LINER;if(this._elTbody){this._elTbody.style.display="none";}k=d._oDynStyles[i];if(!k){if(j.styleSheet&&j.styleSheet.addRule){j.styleSheet.addRule(i,"overflow:"+n);j.styleSheet.addRule(i,"width:"+l);k=j.styleSheet.rules[j.styleSheet.rules.length-1];d._oDynStyles[i]=k;}else{if(j.sheet&&j.sheet.insertRule){j.sheet.insertRule(i+" {overflow:"+n+";width:"+l+";}",j.sheet.cssRules.length);k=j.sheet.cssRules[j.sheet.cssRules.length-1];d._oDynStyles[i]=k;}}}else{k.style.overflow=n;k.style.width=l;}if(this._elTbody){this._elTbody.style.display="";}}if(!k){d._bDynStylesFallback=true;this._setColumnWidthDynFunction(m,l);}},_setColumnWidthDynFunction:function(r,m,s){if(m=="auto"){m="";}var l=this._elTbody?this._elTbody.rows.length:0;if(!this._aDynFunctions[l]){var q,p,o;var t=["var colIdx=oColumn.getKeyIndex();","oColumn.getThLinerEl().style.overflow="];for(q=l-1,p=2;q>=0;--q){t[p++]="this._elTbody.rows[";t[p++]=q;t[p++]="].cells[colIdx].firstChild.style.overflow=";}t[p]="sOverflow;";t[p+1]="oColumn.getThLinerEl().style.width=";for(q=l-1,o=p+2;q>=0;--q){t[o++]="this._elTbody.rows[";t[o++]=q;t[o++]="].cells[colIdx].firstChild.style.width=";}t[o]="sWidth;";this._aDynFunctions[l]=new Function("oColumn","sWidth","sOverflow",t.join(""));}var n=this._aDynFunctions[l];if(n){n.call(this,r,m,s);}},validateColumnWidths:function(o){var l=this._elColgroup;var q=l.cloneNode(true);var p=false;var n=this._oColumnSet.keys;var k;if(o&&!o.hidden&&!o.width&&(o.getKeyIndex()!==null)){k=o.getThLinerEl();if((o.minWidth>0)&&(k.offsetWidth0)&&(k.offsetWidth>o.maxAutoWidth)){this._setColumnWidth(o,o.maxAutoWidth+"px","hidden");}}}else{for(var m=0,j=n.length;m0)&&(k.offsetWidth0)&&(k.offsetWidth>o.maxAutoWidth)){this._setColumnWidth(o,o.maxAutoWidth+"px","hidden");}}}}}if(p){l.parentNode.replaceChild(q,l);this._elColgroup=q;}},_clearMinWidth:function(i){if(i.getKeyIndex()!==null){this._elColgroup.childNodes[i.getKeyIndex()].style.width="";}},_restoreMinWidth:function(i){if(i.minWidth&&(i.getKeyIndex()!==null)){this._elColgroup.childNodes[i.getKeyIndex()].style.width=i.minWidth+"px";}},hideColumn:function(r){if(!(r instanceof YAHOO.widget.Column)){r=this.getColumn(r);}if(r&&!r.hidden&&r.getTreeIndex()!==null){var o=this.getTbodyEl().rows;var n=o.length;var m=this._oColumnSet.getDescendants(r);for(var q=0,s=m.length;q0){q=u;}}else{q=[q];}if(q!==null){q.sort(function(v,i){return YAHOO.util.Sort.compare(v,i);});this._destroyTheadEl();var k=this._oColumnSet.getDefinitions();p=k.splice(m,1)[0];this._initColumnSet(k);this._initTheadEl();for(o=q.length-1;o>-1;o--){this._removeColgroupColEl(q[o]);}var t=this._elTbody.rows;if(t.length>0){var n=this.get("renderLoopSize"),l=t.length; +this._oChainRender.add({method:function(y){if((this instanceof d)&&this._sId){var x=y.nCurrentRow,v=n>0?Math.min(x+n,t.length):t.length,z=y.aIndexes,w;for(;x-1;w--){t[x].removeChild(t[x].childNodes[z[w]]);}}y.nCurrentRow=x;}},iterations:(n>0)?Math.ceil(l/n):1,argument:{nCurrentRow:0,aIndexes:q},scope:this,timeout:(n>0)?0:-1});this._runRenderChain();}this.fireEvent("columnRemoveEvent",{column:p});return p;}}}},insertColumn:function(r,s){if(r instanceof YAHOO.widget.Column){r=r.getDefinition();}else{if(r.constructor!==Object){return;}}var x=this._oColumnSet;if(!h.isValue(s)||!h.isNumber(s)){s=x.tree[0].length;}this._destroyTheadEl();var z=this._oColumnSet.getDefinitions();z.splice(s,0,r);this._initColumnSet(z);this._initTheadEl();x=this._oColumnSet;var n=x.tree[0][s];var p,t,w=[];var l=x.getDescendants(n);for(p=0,t=l.length;p0){var y=w.sort(function(A,i){return YAHOO.util.Sort.compare(A,i);})[0];for(p=w.length-1;p>-1;p--){this._insertColgroupColEl(w[p]);}var v=this._elTbody.rows;if(v.length>0){var o=this.get("renderLoopSize"),m=v.length;var k=[],q;for(p=0,t=w.length;p0?Math.min(C+o,v.length):v.length,E;for(;C-1;B--){v[C].insertBefore(D.aTdTemplates[F[B]].cloneNode(true),E);}}D.nCurrentRow=C;}},iterations:(o>0)?Math.ceil(m/o):1,argument:{nCurrentRow:0,aTdTemplates:k,descKeyIndexes:w},scope:this,timeout:(o>0)?0:-1});this._runRenderChain();}this.fireEvent("columnInsertEvent",{column:r,index:s});return n;}},reorderColumn:function(q,r){if(!(q instanceof YAHOO.widget.Column)){q=this.getColumn(q);}if(q&&YAHOO.lang.isNumber(r)){var z=q.getTreeIndex();if((z!==null)&&(z!==r)){var p,s,l=q.getKeyIndex(),k,v=[],t;if(l===null){k=this._oColumnSet.getDescendants(q);for(p=0,s=k.length;p0){l=v;}}else{l=[l];}if(l!==null){l.sort(function(A,i){return YAHOO.util.Sort.compare(A,i);});this._destroyTheadEl();var w=this._oColumnSet.getDefinitions();var j=w.splice(z,1)[0];w.splice(r,0,j);this._initColumnSet(w);this._initTheadEl();var n=this._oColumnSet.tree[0][r];var y=n.getKeyIndex();if(y===null){v=[];k=this._oColumnSet.getDescendants(n);for(p=0,s=k.length;p0){y=v;}}else{y=[y];}var x=y.sort(function(A,i){return YAHOO.util.Sort.compare(A,i);})[0];this._reorderColgroupColEl(l,x);var u=this._elTbody.rows;if(u.length>0){var o=this.get("renderLoopSize"),m=u.length;this._oChainRender.add({method:function(D){if((this instanceof d)&&this._sId){var C=D.nCurrentRow,B,F,E,A=o>0?Math.min(C+o,u.length):u.length,H=D.aIndexes,G;for(;C-1;B--){F.push(G.removeChild(G.childNodes[H[B]]));}E=G.childNodes[x]||null;for(B=F.length-1;B>-1;B--){G.insertBefore(F[B],E);}}D.nCurrentRow=C;}},iterations:(o>0)?Math.ceil(m/o):1,argument:{nCurrentRow:0,aIndexes:l},scope:this,timeout:(o>0)?0:-1});this._runRenderChain();}this.fireEvent("columnReorderEvent",{column:n,oldIndex:z});return n;}}}},selectColumn:function(k){k=this.getColumn(k);if(k&&!k.selected){if(k.getKeyIndex()!==null){k.selected=true;var l=k.getThEl();c.addClass(l,d.CLASS_SELECTED);var j=this.getTbodyEl().rows;var i=this._oChainRender;i.add({method:function(m){if((this instanceof d)&&this._sId&&j[m.rowIndex]&&j[m.rowIndex].cells[m.cellIndex]){c.addClass(j[m.rowIndex].cells[m.cellIndex],d.CLASS_SELECTED);}m.rowIndex++;},scope:this,iterations:j.length,argument:{rowIndex:0,cellIndex:k.getKeyIndex()}});this._clearTrTemplateEl();this._elTbody.style.display="none";this._runRenderChain();this._elTbody.style.display="";this.fireEvent("columnSelectEvent",{column:k});}else{}}},unselectColumn:function(k){k=this.getColumn(k);if(k&&k.selected){if(k.getKeyIndex()!==null){k.selected=false;var l=k.getThEl();c.removeClass(l,d.CLASS_SELECTED);var j=this.getTbodyEl().rows;var i=this._oChainRender;i.add({method:function(m){if((this instanceof d)&&this._sId&&j[m.rowIndex]&&j[m.rowIndex].cells[m.cellIndex]){c.removeClass(j[m.rowIndex].cells[m.cellIndex],d.CLASS_SELECTED);}m.rowIndex++;},scope:this,iterations:j.length,argument:{rowIndex:0,cellIndex:k.getKeyIndex()}});this._clearTrTemplateEl();this._elTbody.style.display="none";this._runRenderChain();this._elTbody.style.display="";this.fireEvent("columnUnselectEvent",{column:k});}else{}}},getSelectedColumns:function(n){var k=[];var l=this._oColumnSet.keys;for(var m=0,j=l.length;mthis._oRecordSet.getLength())){return;}if(o&&h.isObject(o)){var m=this._oRecordSet.addRecord(o,k);if(m){var i;var j=this.get("paginator");if(j){var n=j.get("totalRecords");if(n!==e.Paginator.VALUE_UNLIMITED){j.set("totalRecords",n+1);}i=this.getRecordIndex(m);var l=(j.getPageRecords())[1];if(i<=l){this.render();}this.fireEvent("rowAddEvent",{record:m});return;}else{i=this.getRecordIndex(m);if(h.isNumber(i)){this._oChainRender.add({method:function(r){if((this instanceof d)&&this._sId){var s=r.record;var p=r.recIndex;var t=this._addTrEl(s);if(t){var q=(this._elTbody.rows[p])?this._elTbody.rows[p]:null;this._elTbody.insertBefore(t,q);if(p===0){this._setFirstRow();}if(q===null){this._setLastRow();}this._setRowStripes();this.hideTableMessage();this.fireEvent("rowAddEvent",{record:s});}}},argument:{record:m,recIndex:i},scope:this,timeout:(this.get("renderLoopSize")>0)?0:-1});this._runRenderChain();return;}}}}},addRows:function(k,n){if(h.isNumber(n)&&(n<0||n>this._oRecordSet.getLength())){return;}if(h.isArray(k)){var o=this._oRecordSet.addRecords(k,n);if(o){var s=this.getRecordIndex(o[0]);var r=this.get("paginator");if(r){var p=r.get("totalRecords");if(p!==e.Paginator.VALUE_UNLIMITED){r.set("totalRecords",p+o.length);}var q=(r.getPageRecords())[1];if(s<=q){this.render();}this.fireEvent("rowsAddEvent",{records:o});return;}else{var m=this.get("renderLoopSize");var j=s+k.length;var i=(j-s);var l=(s>=this._elTbody.rows.length);this._oChainRender.add({method:function(x){if((this instanceof d)&&this._sId){var y=x.aRecords,w=x.nCurrentRow,v=x.nCurrentRecord,t=m>0?Math.min(w+m,j):j,z=document.createDocumentFragment(),u=(this._elTbody.rows[w])?this._elTbody.rows[w]:null;for(;w0)?Math.ceil(j/m):1,argument:{nCurrentRow:s,nCurrentRecord:0,aRecords:o},scope:this,timeout:(m>0)?0:-1});this._oChainRender.add({method:function(u){var t=u.recIndex;if(t===0){this._setFirstRow();}if(u.isLast){this._setLastRow();}this._setRowStripes();this.fireEvent("rowsAddEvent",{records:o});},argument:{recIndex:s,isLast:l},scope:this,timeout:-1});this._runRenderChain();this.hideTableMessage();return;}}}},updateRow:function(u,k){var r=u;if(!h.isNumber(r)){r=this.getRecordIndex(u);}if(h.isNumber(r)&&(r>=0)){var s=this._oRecordSet,q=s.getRecord(r);if(q){var o=this._oRecordSet.setRecord(k,r),j=this.getTrEl(q),p=q?q.getData():null;if(o){var t=this._aSelections||[],n=0,l=q.getId(),m=o.getId();for(;n=i)||(r<=w)){this.render();}}else{if(j){this._updateTrEl(j,o);}else{this.getTbodyEl().appendChild(this._addTrEl(o));}}this.fireEvent("rowUpdateEvent",{record:o,oldData:p});}},scope:this,timeout:(this.get("renderLoopSize")>0)?0:-1});this._runRenderChain();return;}}}return;},updateRows:function(A,m){if(h.isArray(m)){var s=A,l=this._oRecordSet,o=l.getLength();if(!h.isNumber(A)){s=this.getRecordIndex(A);}if(h.isNumber(s)&&(s>=0)&&(s=r)||(E<=p)){this.render();}this.fireEvent("rowsAddEvent",{newRecords:G,oldRecords:B});return;}else{var k=this.get("renderLoopSize"),v=m.length,w=(E>=o),q=(E>o);this._oChainRender.add({method:function(K){if((this instanceof d)&&this._sId){var L=K.aRecords,J=K.nCurrentRow,I=K.nDataPointer,H=k>0?Math.min(J+k,s+L.length):s+L.length;for(;J=o)){this._elTbody.appendChild(this._addTrEl(L[I]));}else{this._updateTrEl(this._elTbody.rows[J],L[I]);}}K.nCurrentRow=J;K.nDataPointer=I;}},iterations:(k>0)?Math.ceil(v/k):1,argument:{nCurrentRow:s,aRecords:G,nDataPointer:0,isAdding:q},scope:this,timeout:(k>0)?0:-1});this._oChainRender.add({method:function(j){var i=j.recIndex;if(i===0){this._setFirstRow();}if(j.isLast){this._setLastRow();}this._setRowStripes();this.fireEvent("rowsAddEvent",{newRecords:G,oldRecords:B});},argument:{recIndex:s,isLast:w},scope:this,timeout:-1});this._runRenderChain();this.hideTableMessage();return;}}}}},deleteRow:function(s){var k=(h.isNumber(s))?s:this.getRecordIndex(s);if(h.isNumber(k)){var t=this.getRecord(k);if(t){var m=this.getTrIndex(k);var p=t.getId();var r=this._aSelections||[];for(var n=r.length-1;n>-1;n--){if((h.isString(r[n])&&(r[n]===p))||(h.isObject(r[n])&&(r[n].recordId===p))){r.splice(n,1);}}var l=this._oRecordSet.deleteRecord(k);if(l){var q=this.get("paginator");if(q){var o=q.get("totalRecords"),i=q.getPageRecords();if(o!==e.Paginator.VALUE_UNLIMITED){q.set("totalRecords",o-1);}if(!i||k<=i[1]){this.render();}this._oChainRender.add({method:function(){if((this instanceof d)&&this._sId){this.fireEvent("rowDeleteEvent",{recordIndex:k,oldData:l,trElIndex:m});}},scope:this,timeout:(this.get("renderLoopSize")>0)?0:-1});this._runRenderChain();}else{if(h.isNumber(m)){this._oChainRender.add({method:function(){if((this instanceof d)&&this._sId){var j=(k===this._oRecordSet.getLength());this._deleteTrEl(m);if(this._elTbody.rows.length>0){if(m===0){this._setFirstRow(); +}if(j){this._setLastRow();}if(m!=this._elTbody.rows.length){this._setRowStripes(m);}}this.fireEvent("rowDeleteEvent",{recordIndex:k,oldData:l,trElIndex:m});}},scope:this,timeout:(this.get("renderLoopSize")>0)?0:-1});this._runRenderChain();return;}}}}}return null;},deleteRows:function(y,s){var l=(h.isNumber(y))?y:this.getRecordIndex(y);if(h.isNumber(l)){var z=this.getRecord(l);if(z){var m=this.getTrIndex(l);var u=z.getId();var x=this._aSelections||[];for(var q=x.length-1;q>-1;q--){if((h.isString(x[q])&&(x[q]===u))||(h.isObject(x[q])&&(x[q].recordId===u))){x.splice(q,1);}}var n=l;var w=l;if(s&&h.isNumber(s)){n=(s>0)?l+s-1:l;w=(s>0)?l:l+s+1;s=(s>0)?s:s*-1;if(w<0){w=0;s=n-w+1;}}else{s=1;}var p=this._oRecordSet.deleteRecords(w,s);if(p){var v=this.get("paginator"),r=this.get("renderLoopSize");if(v){var t=v.get("totalRecords"),k=v.getPageRecords();if(t!==e.Paginator.VALUE_UNLIMITED){v.set("totalRecords",t-p.length);}if(!k||w<=k[1]){this.render();}this._oChainRender.add({method:function(j){if((this instanceof d)&&this._sId){this.fireEvent("rowsDeleteEvent",{recordIndex:w,oldData:p,count:s});}},scope:this,timeout:(r>0)?0:-1});this._runRenderChain();return;}else{if(h.isNumber(m)){var o=w;var i=s;this._oChainRender.add({method:function(B){if((this instanceof d)&&this._sId){var A=B.nCurrentRow,j=(r>0)?(Math.max(A-r,o)-1):o-1;for(;A>j;--A){this._deleteTrEl(A);}B.nCurrentRow=A;}},iterations:(r>0)?Math.ceil(s/r):1,argument:{nCurrentRow:n},scope:this,timeout:(r>0)?0:-1});this._oChainRender.add({method:function(){if(this._elTbody.rows.length>0){this._setFirstRow();this._setLastRow();this._setRowStripes();}this.fireEvent("rowsDeleteEvent",{recordIndex:w,oldData:p,count:s});},scope:this,timeout:-1});this._runRenderChain();return;}}}}}return null;},formatCell:function(j,l,m){if(!l){l=this.getRecord(j);}if(!m){m=this.getColumn(this.getCellIndex(j.parentNode));}if(l&&m){var i=m.field;var n=l.getData(i);var k=typeof m.formatter==="function"?m.formatter:d.Formatter[m.formatter+""]||d.Formatter.defaultFormatter;if(k){k.call(this,j,l,m,n);}else{j.innerHTML=n;}this.fireEvent("cellFormatEvent",{record:l,column:m,key:m.key,el:j});}else{}},updateCell:function(k,m,o,j){m=(m instanceof YAHOO.widget.Column)?m:this.getColumn(m);if(m&&m.getField()&&(k instanceof YAHOO.widget.Record)){var l=m.getField(),n=k.getData(l);this._oRecordSet.updateRecordValue(k,l,o);var i=this.getTdEl({record:k,column:m});if(i){this._oChainRender.add({method:function(){if((this instanceof d)&&this._sId){this.formatCell(i.firstChild,k,m);this.fireEvent("cellUpdateEvent",{record:k,column:m,oldData:n});}},scope:this,timeout:(this.get("renderLoopSize")>0)?0:-1});if(!j){this._runRenderChain();}}else{this.fireEvent("cellUpdateEvent",{record:k,column:m,oldData:n});}}},_updatePaginator:function(j){var i=this.get("paginator");if(i&&j!==i){i.unsubscribe("changeRequest",this.onPaginatorChangeRequest,this,true);}if(j){j.subscribe("changeRequest",this.onPaginatorChangeRequest,this,true);}},_handlePaginatorChange:function(l){if(l.prevValue===l.newValue){return;}var n=l.newValue,m=l.prevValue,k=this._defaultPaginatorContainers();if(m){if(m.getContainerNodes()[0]==k[0]){m.set("containers",[]);}m.destroy();if(k[0]){if(n&&!n.getContainerNodes().length){n.set("containers",k);}else{for(var j=k.length-1;j>=0;--j){if(k[j]){k[j].parentNode.removeChild(k[j]);}}}}}if(!this._bInit){this.render();}if(n){this.renderPaginator();}},_defaultPaginatorContainers:function(l){var j=this._sId+"-paginator0",k=this._sId+"-paginator1",i=c.get(j),m=c.get(k);if(l&&(!i||!m)){if(!i){i=document.createElement("div");i.id=j;c.addClass(i,d.CLASS_PAGINATOR);this._elContainer.insertBefore(i,this._elContainer.firstChild);}if(!m){m=document.createElement("div");m.id=k;c.addClass(m,d.CLASS_PAGINATOR);this._elContainer.appendChild(m);}}return[i,m];},_destroyPaginator:function(){var i=this.get("paginator");if(i){i.destroy();}},renderPaginator:function(){var i=this.get("paginator");if(!i){return;}if(!i.getContainerNodes().length){i.set("containers",this._defaultPaginatorContainers(true));}i.render();},doBeforePaginatorChange:function(i){this.showTableMessage(this.get("MSG_LOADING"),d.CLASS_LOADING);return true;},onPaginatorChangeRequest:function(l){var j=this.doBeforePaginatorChange(l);if(j){if(this.get("dynamicData")){var i=this.getState();i.pagination=l;var k=this.get("generateRequest")(i,this);this.unselectAllRows();this.unselectAllCells();var m={success:this.onDataReturnSetRows,failure:this.onDataReturnSetRows,argument:i,scope:this};this._oDataSource.sendRequest(k,m);}else{l.paginator.setStartIndex(l.recordOffset,true);l.paginator.setRowsPerPage(l.rowsPerPage,true);this.render();}}else{}},_elLastHighlightedTd:null,_aSelections:null,_oAnchorRecord:null,_oAnchorCell:null,_unselectAllTrEls:function(){var i=c.getElementsByClassName(d.CLASS_SELECTED,"tr",this._elTbody);c.removeClass(i,d.CLASS_SELECTED);},_getSelectionTrigger:function(){var l=this.get("selectionMode");var k={};var o,i,j,n,m;if((l=="cellblock")||(l=="cellrange")||(l=="singlecell")){o=this.getLastSelectedCell();if(!o){return null;}else{i=this.getRecord(o.recordId);j=this.getRecordIndex(i);n=this.getTrEl(i);m=this.getTrIndex(n);if(m===null){return null;}else{k.record=i;k.recordIndex=j;k.el=this.getTdEl(o);k.trIndex=m;k.column=this.getColumn(o.columnKey);k.colKeyIndex=k.column.getKeyIndex();k.cell=o;return k;}}}else{i=this.getLastSelectedRecord();if(!i){return null;}else{i=this.getRecord(i);j=this.getRecordIndex(i);n=this.getTrEl(i);m=this.getTrIndex(n);if(m===null){return null;}else{k.record=i;k.recordIndex=j;k.el=n;k.trIndex=m;return k;}}}},_getSelectionAnchor:function(k){var j=this.get("selectionMode");var l={};var m,o,i;if((j=="cellblock")||(j=="cellrange")||(j=="singlecell")){var n=this._oAnchorCell;if(!n){if(k){n=this._oAnchorCell=k.cell;}else{return null;}}m=this._oAnchorCell.record;o=this._oRecordSet.getRecordIndex(m);i=this.getTrIndex(m);if(i===null){if(o=l;n--){if(!this.isSelected(n)){this.selectRow(n);}}}}else{if(q.recordIndex=l;n--){this.selectRow(n);}}}else{this._oAnchorRecord=r;this.selectRow(r);}}else{if(o){this._oAnchorRecord=r;if(this.isSelected(r)){this.unselectRow(r);}else{this.selectRow(r);}}else{this._handleSingleSelectionByMouse(k);return;}}}}},_handleStandardSelectionByKey:function(m){var i=g.getCharCode(m);if((i==38)||(i==40)){var k=m.shiftKey;var j=this._getSelectionTrigger();if(!j){return null;}g.stopEvent(m);var l=this._getSelectionAnchor(j);if(k){if((i==40)&&(l.recordIndex<=j.trIndex)){this.selectRow(this.getNextTrEl(j.el));}else{if((i==38)&&(l.recordIndex>=j.trIndex)){this.selectRow(this.getPreviousTrEl(j.el));}else{this.unselectRow(j.el);}}}else{this._handleSingleSelectionByKey(m);}}},_handleSingleSelectionByMouse:function(k){var l=k.target;var j=this.getTrEl(l);if(j){var i=this.getRecord(j);this._oAnchorRecord=i;this.unselectAllRows();this.selectRow(i);}},_handleSingleSelectionByKey:function(l){var i=g.getCharCode(l);if((i==38)||(i==40)){var j=this._getSelectionTrigger();if(!j){return null;}g.stopEvent(l);var k;if(i==38){k=this.getPreviousTrEl(j.el);if(k===null){k=this.getFirstTrEl();}}else{if(i==40){k=this.getNextTrEl(j.el);if(k===null){k=this.getLastTrEl();}}}this.unselectAllRows();this.selectRow(k);this._oAnchorRecord=this.getRecord(k);}},_handleCellBlockSelectionByMouse:function(A){var B=A.target;var l=this.getTdEl(B);if(l){var z=A.event;var q=z.shiftKey;var m=z.ctrlKey||((navigator.userAgent.toLowerCase().indexOf("mac")!=-1)&&z.metaKey);var s=this.getTrEl(l);var r=this.getTrIndex(s);var v=this.getColumn(l);var w=v.getKeyIndex();var u=this.getRecord(s);var D=this._oRecordSet.getRecordIndex(u);var p={record:u,column:v};var t=this._getSelectionAnchor();var o=this.getTbodyEl().rows;var n,k,C,y,x;if(q&&m){if(t){if(this.isSelected(t.cell)){if(t.recordIndex===D){if(t.colKeyIndex=r;y--){for(x=k;x>=n;x--){this.selectCell(o[y].cells[x]);}}}}}else{if(t.recordIndex===D){if(t.colKeyIndext.colKeyIndex){this.unselectCell(C.cells[x]);}}else{if(C.sectionRowIndex===r){if(xw){this.unselectCell(C.cells[x]);}}else{if(C.sectionRowIndex==t.trIndex){if(x36)&&(j<41)){var u=this._getSelectionTrigger();if(!u){return null;}g.stopEvent(o);var r=this._getSelectionAnchor(u);var k,s,l,q,m;var p=this.getTbodyEl().rows;var n=u.el.parentNode;if(j==40){if(r.recordIndex<=u.recordIndex){m=this.getNextTrEl(u.el);if(m){s=r.colKeyIndex;l=u.colKeyIndex;if(s>l){for(k=s;k>=l;k--){q=m.cells[k];this.selectCell(q);}}else{for(k=s;k<=l;k++){q=m.cells[k];this.selectCell(q);}}}}else{s=Math.min(r.colKeyIndex,u.colKeyIndex);l=Math.max(r.colKeyIndex,u.colKeyIndex);for(k=s;k<=l;k++){this.unselectCell(n.cells[k]);}}}else{if(j==38){if(r.recordIndex>=u.recordIndex){m=this.getPreviousTrEl(u.el); +if(m){s=r.colKeyIndex;l=u.colKeyIndex;if(s>l){for(k=s;k>=l;k--){q=m.cells[k];this.selectCell(q);}}else{for(k=s;k<=l;k++){q=m.cells[k];this.selectCell(q);}}}}else{s=Math.min(r.colKeyIndex,u.colKeyIndex);l=Math.max(r.colKeyIndex,u.colKeyIndex);for(k=s;k<=l;k++){this.unselectCell(n.cells[k]);}}}else{if(j==39){if(r.colKeyIndex<=u.colKeyIndex){if(u.colKeyIndexl){for(k=s;k>=l;k--){q=p[k].cells[u.colKeyIndex+1];this.selectCell(q);}}else{for(k=s;k<=l;k++){q=p[k].cells[u.colKeyIndex+1];this.selectCell(q);}}}}else{s=Math.min(r.trIndex,u.trIndex);l=Math.max(r.trIndex,u.trIndex);for(k=s;k<=l;k++){this.unselectCell(p[k].cells[u.colKeyIndex]);}}}else{if(j==37){if(r.colKeyIndex>=u.colKeyIndex){if(u.colKeyIndex>0){s=r.trIndex;l=u.trIndex;if(s>l){for(k=s;k>=l;k--){q=p[k].cells[u.colKeyIndex-1];this.selectCell(q);}}else{for(k=s;k<=l;k++){q=p[k].cells[u.colKeyIndex-1];this.selectCell(q);}}}}else{s=Math.min(r.trIndex,u.trIndex);l=Math.max(r.trIndex,u.trIndex);for(k=s;k<=l;k++){this.unselectCell(p[k].cells[u.colKeyIndex]);}}}}}}}},_handleCellRangeSelectionByMouse:function(y){var z=y.target;var k=this.getTdEl(z);if(k){var x=y.event;var o=x.shiftKey;var l=x.ctrlKey||((navigator.userAgent.toLowerCase().indexOf("mac")!=-1)&&x.metaKey);var q=this.getTrEl(k);var p=this.getTrIndex(q);var t=this.getColumn(k);var u=t.getKeyIndex();var s=this.getRecord(q);var B=this._oRecordSet.getRecordIndex(s);var n={record:s,column:t};var r=this._getSelectionAnchor();var m=this.getTbodyEl().rows;var A,w,v;if(o&&l){if(r){if(this.isSelected(r.cell)){if(r.recordIndex===B){if(r.colKeyIndexr.colKeyIndex){this.unselectCell(A.cells[v]);}}else{if(A.sectionRowIndex===p){if(vu){this.unselectCell(A.cells[v]);}}else{if(A.sectionRowIndex==r.trIndex){if(v=r.colKeyIndex){this.selectCell(A.cells[v]);}}else{if(A.sectionRowIndex==p){if(v<=u){this.selectCell(A.cells[v]);}}else{this.selectCell(A.cells[v]);}}}}}else{for(w=p;w<=r.trIndex;w++){A=m[w];for(v=0;v=u){this.selectCell(A.cells[v]);}}else{if(A.sectionRowIndex==r.trIndex){if(v<=r.colKeyIndex){this.selectCell(A.cells[v]);}}else{this.selectCell(A.cells[v]);}}}}}}}else{this._oAnchorCell=n;this.selectCell(n);}}else{if(l){this._oAnchorCell=n;if(this.isSelected(n)){this.unselectCell(n);}else{this.selectCell(n);}}else{this._handleSingleCellSelectionByMouse(y);}}}}},_handleCellRangeSelectionByKey:function(n){var j=g.getCharCode(n);var r=n.shiftKey;if((j==9)||!r){this._handleSingleCellSelectionByKey(n);return;}if((j>36)&&(j<41)){var s=this._getSelectionTrigger();if(!s){return null;}g.stopEvent(n);var q=this._getSelectionAnchor(s);var k,l,p;var o=this.getTbodyEl().rows;var m=s.el.parentNode;if(j==40){l=this.getNextTrEl(s.el);if(q.recordIndex<=s.recordIndex){for(k=s.colKeyIndex+1;k=s.recordIndex){for(k=s.colKeyIndex-1;k>-1;k--){p=m.cells[k];this.selectCell(p);}if(l){for(k=m.cells.length-1;k>=s.colKeyIndex;k--){p=l.cells[k];this.selectCell(p);}}}else{for(k=s.colKeyIndex;k>-1;k--){this.unselectCell(m.cells[k]);}if(l){for(k=m.cells.length-1;k>s.colKeyIndex;k--){this.unselectCell(l.cells[k]);}}}}else{if(j==39){l=this.getNextTrEl(s.el);if(q.recordIndexs.recordIndex){this.unselectCell(m.cells[s.colKeyIndex]);if(s.colKeyIndex0){}else{}}else{if(q.recordIndex>s.recordIndex){if(s.colKeyIndex>0){p=m.cells[s.colKeyIndex-1];this.selectCell(p);}else{if(s.trIndex>0){p=l.cells[l.cells.length-1];this.selectCell(p); +}}}else{if(q.colKeyIndex>=s.colKeyIndex){if(s.colKeyIndex>0){p=m.cells[s.colKeyIndex-1];this.selectCell(p);}else{if(s.trIndex>0){p=l.cells[l.cells.length-1];this.selectCell(p);}}}else{this.unselectCell(m.cells[s.colKeyIndex]);if(s.colKeyIndex>0){}else{}}}}}}}}}},_handleSingleCellSelectionByMouse:function(n){var o=n.target;var k=this.getTdEl(o);if(k){var j=this.getTrEl(k);var i=this.getRecord(j);var m=this.getColumn(k);var l={record:i,column:m};this._oAnchorCell=l;this.unselectAllCells();this.selectCell(l);}},_handleSingleCellSelectionByKey:function(m){var i=g.getCharCode(m);if((i==9)||((i>36)&&(i<41))){var k=m.shiftKey;var j=this._getSelectionTrigger();if(!j){return null;}var l;if(i==40){l=this.getBelowTdEl(j.el);if(l===null){l=j.el;}}else{if(i==38){l=this.getAboveTdEl(j.el);if(l===null){l=j.el;}}else{if((i==39)||(!k&&(i==9))){l=this.getNextTdEl(j.el);if(l===null){return;}}else{if((i==37)||(k&&(i==9))){l=this.getPreviousTdEl(j.el);if(l===null){return;}}}}}g.stopEvent(m);this.unselectAllCells();this.selectCell(l);this._oAnchorCell={record:this.getRecord(l),column:this.getColumn(l)};}},getSelectedTrEls:function(){return c.getElementsByClassName(d.CLASS_SELECTED,"tr",this._elTbody);},selectRow:function(p){var o,i;if(p instanceof YAHOO.widget.Record){o=this._oRecordSet.getRecord(p);i=this.getTrEl(o);}else{if(h.isNumber(p)){o=this.getRecord(p);i=this.getTrEl(o);}else{i=this.getTrEl(p);o=this.getRecord(i);}}if(o){var n=this._aSelections||[];var m=o.getId();var l=-1;if(n.indexOf){l=n.indexOf(m);}else{for(var k=n.length-1;k>-1;k--){if(n[k]===m){l=k;break;}}}if(l>-1){n.splice(l,1);}n.push(m);this._aSelections=n;if(!this._oAnchorRecord){this._oAnchorRecord=o;}if(i){c.addClass(i,d.CLASS_SELECTED);}this.fireEvent("rowSelectEvent",{record:o,el:i});}else{}},unselectRow:function(p){var i=this.getTrEl(p);var o;if(p instanceof YAHOO.widget.Record){o=this._oRecordSet.getRecord(p);}else{if(h.isNumber(p)){o=this.getRecord(p);}else{o=this.getRecord(i);}}if(o){var n=this._aSelections||[];var m=o.getId();var l=-1;if(n.indexOf){l=n.indexOf(m);}else{for(var k=n.length-1;k>-1;k--){if(n[k]===m){l=k;break;}}}if(l>-1){n.splice(l,1);this._aSelections=n;c.removeClass(i,d.CLASS_SELECTED);this.fireEvent("rowUnselectEvent",{record:o,el:i});return;}}},unselectAllRows:function(){var k=this._aSelections||[],m,l=[];for(var i=k.length-1;i>-1;i--){if(h.isString(k[i])){m=k.splice(i,1);l[l.length]=this.getRecord(h.isArray(m)?m[0]:m);}}this._aSelections=k;this._unselectAllTrEls();this.fireEvent("unselectAllRowsEvent",{records:l});},_unselectAllTdEls:function(){var i=c.getElementsByClassName(d.CLASS_SELECTED,"td",this._elTbody);c.removeClass(i,d.CLASS_SELECTED);},getSelectedTdEls:function(){return c.getElementsByClassName(d.CLASS_SELECTED,"td",this._elTbody);},selectCell:function(i){var p=this.getTdEl(i);if(p){var o=this.getRecord(p);var q=this.getColumn(this.getCellIndex(p));var m=q.getKey();if(o&&m){var n=this._aSelections||[];var l=o.getId();for(var k=n.length-1;k>-1;k--){if((n[k].recordId===l)&&(n[k].columnKey===m)){n.splice(k,1);break;}}n.push({recordId:l,columnKey:m});this._aSelections=n;if(!this._oAnchorCell){this._oAnchorCell={record:o,column:q};}c.addClass(p,d.CLASS_SELECTED);this.fireEvent("cellSelectEvent",{record:o,column:q,key:m,el:p});return;}}},unselectCell:function(i){var o=this.getTdEl(i);if(o){var n=this.getRecord(o);var p=this.getColumn(this.getCellIndex(o));var l=p.getKey();if(n&&l){var m=this._aSelections||[];var q=n.getId();for(var k=m.length-1;k>-1;k--){if((m[k].recordId===q)&&(m[k].columnKey===l)){m.splice(k,1);this._aSelections=m;c.removeClass(o,d.CLASS_SELECTED);this.fireEvent("cellUnselectEvent",{record:n,column:p,key:l,el:o});return;}}}}},unselectAllCells:function(){var k=this._aSelections||[];for(var i=k.length-1;i>-1;i--){if(h.isObject(k[i])){k.splice(i,1);}}this._aSelections=k;this._unselectAllTdEls();this.fireEvent("unselectAllCellsEvent");},isSelected:function(p){if(p&&(p.ownerDocument==document)){return(c.hasClass(this.getTdEl(p),d.CLASS_SELECTED)||c.hasClass(this.getTrEl(p),d.CLASS_SELECTED));}else{var n,k,i;var m=this._aSelections;if(m&&m.length>0){if(p instanceof YAHOO.widget.Record){n=p;}else{if(h.isNumber(p)){n=this.getRecord(p);}}if(n){k=n.getId();if(m.indexOf){if(m.indexOf(k)>-1){return true;}}else{for(i=m.length-1;i>-1;i--){if(m[i]===k){return true;}}}}else{if(p.record&&p.column){k=p.record.getId();var l=p.column.getKey();for(i=m.length-1;i>-1;i--){if((m[i].recordId===k)&&(m[i].columnKey===l)){return true;}}}}}}return false;},getSelectedRows:function(){var i=[];var l=this._aSelections||[];for(var k=0;k0){for(var j=k.length-1;j>-1;j--){if(h.isString(k[j])){return k[j];}}}},getLastSelectedCell:function(){var k=this._aSelections;if(k&&k.length>0){for(var j=k.length-1;j>-1;j--){if(k[j].recordId&&k[j].columnKey){return k[j];}}}},highlightRow:function(k){var i=this.getTrEl(k);if(i){var j=this.getRecord(i);c.addClass(i,d.CLASS_HIGHLIGHTED);this.fireEvent("rowHighlightEvent",{record:j,el:i});return;}},unhighlightRow:function(k){var i=this.getTrEl(k);if(i){var j=this.getRecord(i);c.removeClass(i,d.CLASS_HIGHLIGHTED);this.fireEvent("rowUnhighlightEvent",{record:j,el:i});return;}},highlightCell:function(i){var l=this.getTdEl(i);if(l){if(this._elLastHighlightedTd){this.unhighlightCell(this._elLastHighlightedTd);}var k=this.getRecord(l);var m=this.getColumn(this.getCellIndex(l));var j=m.getKey();c.addClass(l,d.CLASS_HIGHLIGHTED);this._elLastHighlightedTd=l;this.fireEvent("cellHighlightEvent",{record:k,column:m,key:j,el:l});return;}},unhighlightCell:function(i){var k=this.getTdEl(i);if(k){var j=this.getRecord(k);c.removeClass(k,d.CLASS_HIGHLIGHTED);this._elLastHighlightedTd=null;this.fireEvent("cellUnhighlightEvent",{record:j,column:this.getColumn(this.getCellIndex(k)),key:this.getColumn(this.getCellIndex(k)).getKey(),el:k}); +return;}},addCellEditor:function(j,i){j.editor=i;j.editor.subscribe("showEvent",this._onEditorShowEvent,this,true);j.editor.subscribe("keydownEvent",this._onEditorKeydownEvent,this,true);j.editor.subscribe("revertEvent",this._onEditorRevertEvent,this,true);j.editor.subscribe("saveEvent",this._onEditorSaveEvent,this,true);j.editor.subscribe("cancelEvent",this._onEditorCancelEvent,this,true);j.editor.subscribe("blurEvent",this._onEditorBlurEvent,this,true);j.editor.subscribe("blockEvent",this._onEditorBlockEvent,this,true);j.editor.subscribe("unblockEvent",this._onEditorUnblockEvent,this,true);},getCellEditor:function(){return this._oCellEditor;},showCellEditor:function(p,q,l){p=this.getTdEl(p);if(p){l=this.getColumn(p);if(l&&l.editor){var j=this._oCellEditor;if(j){if(this._oCellEditor.cancel){this._oCellEditor.cancel();}else{if(j.isActive){this.cancelCellEditor();}}}if(l.editor instanceof YAHOO.widget.BaseCellEditor){j=l.editor;var n=j.attach(this,p);if(n){j.render();j.move();n=this.doBeforeShowCellEditor(j);if(n){j.show();this._oCellEditor=j;}}}else{if(!q||!(q instanceof YAHOO.widget.Record)){q=this.getRecord(p);}if(!l||!(l instanceof YAHOO.widget.Column)){l=this.getColumn(p);}if(q&&l){if(!this._oCellEditor||this._oCellEditor.container){this._initCellEditorEl();}j=this._oCellEditor;j.cell=p;j.record=q;j.column=l;j.validator=(l.editorOptions&&h.isFunction(l.editorOptions.validator))?l.editorOptions.validator:null;j.value=q.getData(l.key);j.defaultValue=null;var k=j.container;var o=c.getX(p);var m=c.getY(p);if(isNaN(o)||isNaN(m)){o=p.offsetLeft+c.getX(this._elTbody.parentNode)-this._elTbody.scrollLeft;m=p.offsetTop+c.getY(this._elTbody.parentNode)-this._elTbody.scrollTop+this._elThead.offsetHeight;}k.style.left=o+"px";k.style.top=m+"px";this.doBeforeShowCellEditor(this._oCellEditor);k.style.display="";g.addListener(k,"keydown",function(s,r){if((s.keyCode==27)){r.cancelCellEditor();r.focusTbodyEl();}else{r.fireEvent("editorKeydownEvent",{editor:r._oCellEditor,event:s});}},this);var i;if(h.isString(l.editor)){switch(l.editor){case"checkbox":i=d.editCheckbox;break;case"date":i=d.editDate;break;case"dropdown":i=d.editDropdown;break;case"radio":i=d.editRadio;break;case"textarea":i=d.editTextarea;break;case"textbox":i=d.editTextbox;break;default:i=null;}}else{if(h.isFunction(l.editor)){i=l.editor;}}if(i){i(this._oCellEditor,this);if(!l.editorOptions||!l.editorOptions.disableBtns){this.showCellEditorBtns(k);}j.isActive=true;this.fireEvent("editorShowEvent",{editor:j});return;}}}}}},_initCellEditorEl:function(){var i=document.createElement("div");i.id=this._sId+"-celleditor";i.style.display="none";i.tabIndex=0;c.addClass(i,d.CLASS_EDITOR);var k=c.getFirstChild(document.body);if(k){i=c.insertBefore(i,k);}else{i=document.body.appendChild(i);}var j={};j.container=i;j.value=null;j.isActive=false;this._oCellEditor=j;},doBeforeShowCellEditor:function(i){return true;},saveCellEditor:function(){if(this._oCellEditor){if(this._oCellEditor.save){this._oCellEditor.save();}else{if(this._oCellEditor.isActive){var i=this._oCellEditor.value;var j=this._oCellEditor.record.getData(this._oCellEditor.column.key);if(this._oCellEditor.validator){i=this._oCellEditor.value=this._oCellEditor.validator.call(this,i,j,this._oCellEditor);if(i===null){this.resetCellEditor();this.fireEvent("editorRevertEvent",{editor:this._oCellEditor,oldData:j,newData:i});return;}}this._oRecordSet.updateRecordValue(this._oCellEditor.record,this._oCellEditor.column.key,this._oCellEditor.value);this.formatCell(this._oCellEditor.cell.firstChild,this._oCellEditor.record,this._oCellEditor.column);this._oChainRender.add({method:function(){this.validateColumnWidths();},scope:this});this._oChainRender.run();this.resetCellEditor();this.fireEvent("editorSaveEvent",{editor:this._oCellEditor,oldData:j,newData:i});}}}},cancelCellEditor:function(){if(this._oCellEditor){if(this._oCellEditor.cancel){this._oCellEditor.cancel();}else{if(this._oCellEditor.isActive){this.resetCellEditor();this.fireEvent("editorCancelEvent",{editor:this._oCellEditor});}}}},destroyCellEditor:function(){if(this._oCellEditor){this._oCellEditor.destroy();this._oCellEditor=null;}},_onEditorShowEvent:function(i){this.fireEvent("editorShowEvent",i);},_onEditorKeydownEvent:function(i){this.fireEvent("editorKeydownEvent",i);},_onEditorRevertEvent:function(i){this.fireEvent("editorRevertEvent",i);},_onEditorSaveEvent:function(i){this.fireEvent("editorSaveEvent",i);},_onEditorCancelEvent:function(i){this.fireEvent("editorCancelEvent",i);},_onEditorBlurEvent:function(i){this.fireEvent("editorBlurEvent",i);},_onEditorBlockEvent:function(i){this.fireEvent("editorBlockEvent",i);},_onEditorUnblockEvent:function(i){this.fireEvent("editorUnblockEvent",i);},onEditorBlurEvent:function(i){if(i.editor.disableBtns){if(i.editor.save){i.editor.save();}}else{if(i.editor.cancel){i.editor.cancel();}}},onEditorBlockEvent:function(i){this.disable();},onEditorUnblockEvent:function(i){this.undisable();},doBeforeLoadData:function(i,j,k){return true;},onEventSortColumn:function(k){var i=k.event;var m=k.target;var j=this.getThEl(m)||this.getTdEl(m);if(j){var l=this.getColumn(j);if(l.sortable){g.stopEvent(i);this.sortColumn(l);}}else{}},onEventSelectColumn:function(i){this.selectColumn(i.target);},onEventHighlightColumn:function(i){this.highlightColumn(i.target);},onEventUnhighlightColumn:function(i){this.unhighlightColumn(i.target);},onEventSelectRow:function(j){var i=this.get("selectionMode");if(i=="single"){this._handleSingleSelectionByMouse(j);}else{this._handleStandardSelectionByMouse(j);}},onEventSelectCell:function(j){var i=this.get("selectionMode");if(i=="cellblock"){this._handleCellBlockSelectionByMouse(j);}else{if(i=="cellrange"){this._handleCellRangeSelectionByMouse(j);}else{this._handleSingleCellSelectionByMouse(j);}}},onEventHighlightRow:function(i){this.highlightRow(i.target);},onEventUnhighlightRow:function(i){this.unhighlightRow(i.target);},onEventHighlightCell:function(i){this.highlightCell(i.target); +},onEventUnhighlightCell:function(i){this.unhighlightCell(i.target);},onEventFormatCell:function(i){var l=i.target;var j=this.getTdEl(l);if(j){var k=this.getColumn(this.getCellIndex(j));this.formatCell(j.firstChild,this.getRecord(j),k);}else{}},onEventShowCellEditor:function(i){if(!this.isDisabled()){this.showCellEditor(i.target);}},onEventSaveCellEditor:function(i){if(this._oCellEditor){if(this._oCellEditor.save){this._oCellEditor.save();}else{this.saveCellEditor();}}},onEventCancelCellEditor:function(i){if(this._oCellEditor){if(this._oCellEditor.cancel){this._oCellEditor.cancel();}else{this.cancelCellEditor();}}},onDataReturnInitializeTable:function(i,j,k){if((this instanceof d)&&this._sId){this.initializeTable();this.onDataReturnSetRows(i,j,k);}},onDataReturnReplaceRows:function(m,l,n){if((this instanceof d)&&this._sId){this.fireEvent("dataReturnEvent",{request:m,response:l,payload:n});var j=this.doBeforeLoadData(m,l,n),k=this.get("paginator"),i=0;if(j&&l&&!l.error&&h.isArray(l.results)){this._oRecordSet.reset();if(this.get("dynamicData")){if(n&&n.pagination&&h.isNumber(n.pagination.recordOffset)){i=n.pagination.recordOffset;}else{if(k){i=k.getStartIndex();}}}this._oRecordSet.setRecords(l.results,i|0);this._handleDataReturnPayload(m,l,n);this.render();}else{if(j&&l.error){this.showTableMessage(this.get("MSG_ERROR"),d.CLASS_ERROR);}}}},onDataReturnAppendRows:function(j,k,l){if((this instanceof d)&&this._sId){this.fireEvent("dataReturnEvent",{request:j,response:k,payload:l});var i=this.doBeforeLoadData(j,k,l);if(i&&k&&!k.error&&h.isArray(k.results)){this.addRows(k.results);this._handleDataReturnPayload(j,k,l);}else{if(i&&k.error){this.showTableMessage(this.get("MSG_ERROR"),d.CLASS_ERROR);}}}},onDataReturnInsertRows:function(j,k,l){if((this instanceof d)&&this._sId){this.fireEvent("dataReturnEvent",{request:j,response:k,payload:l});var i=this.doBeforeLoadData(j,k,l);if(i&&k&&!k.error&&h.isArray(k.results)){this.addRows(k.results,(l?l.insertIndex:0));this._handleDataReturnPayload(j,k,l);}else{if(i&&k.error){this.showTableMessage(this.get("MSG_ERROR"),d.CLASS_ERROR);}}}},onDataReturnUpdateRows:function(j,k,l){if((this instanceof d)&&this._sId){this.fireEvent("dataReturnEvent",{request:j,response:k,payload:l});var i=this.doBeforeLoadData(j,k,l);if(i&&k&&!k.error&&h.isArray(k.results)){this.updateRows((l?l.updateIndex:0),k.results);this._handleDataReturnPayload(j,k,l);}else{if(i&&k.error){this.showTableMessage(this.get("MSG_ERROR"),d.CLASS_ERROR);}}}},onDataReturnSetRows:function(m,l,n){if((this instanceof d)&&this._sId){this.fireEvent("dataReturnEvent",{request:m,response:l,payload:n});var j=this.doBeforeLoadData(m,l,n),k=this.get("paginator"),i=0;if(j&&l&&!l.error&&h.isArray(l.results)){if(this.get("dynamicData")){if(n&&n.pagination&&h.isNumber(n.pagination.recordOffset)){i=n.pagination.recordOffset;}else{if(k){i=k.getStartIndex();}}this._oRecordSet.reset();}this._oRecordSet.setRecords(l.results,i|0);this._handleDataReturnPayload(m,l,n);this.render();}else{if(j&&l.error){this.showTableMessage(this.get("MSG_ERROR"),d.CLASS_ERROR);}}}else{}},handleDataReturnPayload:function(j,i,k){return k||{};},_handleDataReturnPayload:function(k,j,l){l=this.handleDataReturnPayload(k,j,l);if(l){var i=this.get("paginator");if(i){if(this.get("dynamicData")){if(e.Paginator.isNumeric(l.totalRecords)){i.set("totalRecords",l.totalRecords);}}else{i.set("totalRecords",this._oRecordSet.getLength());}if(h.isObject(l.pagination)){i.set("rowsPerPage",l.pagination.rowsPerPage);i.set("recordOffset",l.pagination.recordOffset);}}if(l.sortedBy){this.set("sortedBy",l.sortedBy);}else{if(l.sorting){this.set("sortedBy",l.sorting);}}}},showCellEditorBtns:function(k){var l=k.appendChild(document.createElement("div"));c.addClass(l,d.CLASS_BUTTON);var j=l.appendChild(document.createElement("button"));c.addClass(j,d.CLASS_DEFAULT);j.innerHTML="OK";g.addListener(j,"click",function(n,m){m.onEventSaveCellEditor(n,m);m.focusTbodyEl();},this,true);var i=l.appendChild(document.createElement("button"));i.innerHTML="Cancel";g.addListener(i,"click",function(n,m){m.onEventCancelCellEditor(n,m);m.focusTbodyEl();},this,true);},resetCellEditor:function(){var i=this._oCellEditor.container;i.style.display="none";g.purgeElement(i,true);i.innerHTML="";this._oCellEditor.value=null;this._oCellEditor.isActive=false;},getBody:function(){return this.getTbodyEl();},getCell:function(i){return this.getTdEl(i);},getRow:function(i){return this.getTrEl(i);},refreshView:function(){this.render();},select:function(k){if(!h.isArray(k)){k=[k];}for(var j=0;j0)?"-"+this._elTbody.offsetTop+"px":0;},_focusEl:function(l){l=l||this._elTbody;var k=this;this._storeScrollPositions();setTimeout(function(){setTimeout(function(){try{l.focus();k._restoreScrollPositions();}catch(m){}},0);},0);},_runRenderChain:function(){this._storeScrollPositions();this._oChainRender.run();},_storeScrollPositions:function(){this._nScrollTop=this._elBdContainer.scrollTop;this._nScrollLeft=this._elBdContainer.scrollLeft;},clearScrollPositions:function(){this._nScrollTop=0;this._nScrollLeft=0;},_restoreScrollPositions:function(){if(this._nScrollTop){this._elBdContainer.scrollTop=this._nScrollTop;this._nScrollTop=null;}if(this._nScrollLeft){this._elBdContainer.scrollLeft=this._nScrollLeft;this._elHdContainer.scrollLeft=this._nScrollLeft;this._nScrollLeft=null;}},_validateColumnWidth:function(n,k){if(!n.width&&!n.hidden){var p=n.getThEl();if(n._calculatedWidth){this._setColumnWidth(n,"auto","visible");}if(p.offsetWidth!==k.offsetWidth){var m=(p.offsetWidth>k.offsetWidth)?n.getThLinerEl():k.firstChild;var l=Math.max(0,(m.offsetWidth-(parseInt(d.getStyle(m,"paddingLeft"),10)|0)-(parseInt(d.getStyle(m,"paddingRight"),10)|0)),n.minWidth);var o="visible";if((n.maxAutoWidth>0)&&(l>n.maxAutoWidth)){l=n.maxAutoWidth;o="hidden";}this._elTbody.style.display="none";this._setColumnWidth(n,l+"px",o);n._calculatedWidth=l;this._elTbody.style.display="";}}},validateColumnWidths:function(s){var u=this._oColumnSet.keys,w=u.length,l=this.getFirstTrEl();if(a.ie){this._setOverhangValue(1);}if(u&&l&&(l.childNodes.length===w)){var m=this.get("width");if(m){this._elHdContainer.style.width="";this._elBdContainer.style.width="";}this._elContainer.style.width="";if(s&&c.isNumber(s.getKeyIndex())){this._validateColumnWidth(s,l.childNodes[s.getKeyIndex()]);}else{var t,k=[],o,q,r;for(q=0;qt.offsetWidth)?s.getThLinerEl():t.firstChild;var p=Math.max(0,(v.offsetWidth-(parseInt(d.getStyle(v,"paddingLeft"),10)|0)-(parseInt(d.getStyle(v,"paddingRight"),10)|0)),s.minWidth);var x="visible";if((s.maxAutoWidth>0)&&(p>s.maxAutoWidth)){p=s.maxAutoWidth;x="hidden";}k[k.length]=[s,p,x];}}}this._elTbody.style.display="none";for(q=0,r=k.length;ql.clientHeight)?(k.parentNode.clientWidth+19)+"px":(k.parentNode.clientWidth+2)+"px";}},_syncScrollX:function(){var k=this._elTbody,l=this._elBdContainer;if(!this.get("height")&&(a.ie)){l.style.height=(l.scrollWidth>l.offsetWidth)?(k.parentNode.offsetHeight+18)+"px":k.parentNode.offsetHeight+"px";}if(this._elTbody.rows.length===0){this._elMsgTbody.parentNode.style.width=this.getTheadEl().parentNode.offsetWidth+"px";}else{this._elMsgTbody.parentNode.style.width="";}},_syncScrollOverhang:function(){var l=this._elBdContainer,k=1;if((l.scrollHeight>l.clientHeight)&&(l.scrollWidth>l.clientWidth)){k=18;}this._setOverhangValue(k);},_setOverhangValue:function(n){var p=this._oColumnSet.headers[this._oColumnSet.headers.length-1]||[],l=p.length,k=this._sId+"-fixedth-",o=n+"px solid "+this.get("COLOR_COLUMNFILLER");this._elThead.style.display="none";for(var m=0;ml.minWidth)?k:l.minWidth;l.width=k;this._setColumnWidth(l,k+"px");this._syncScroll();this.fireEvent("columnSetWidthEvent",{column:l,width:k});}else{if(k===null){l.width=k;this._setColumnWidth(l,"auto");this.validateColumnWidths(l);this.fireEvent("columnUnsetWidthEvent",{column:l});}}this._clearTrTemplateEl();}else{}},scrollTo:function(m){var l=this.getTdEl(m);if(l){this.clearScrollPositions();this.getBdContainerEl().scrollLeft=l.offsetLeft;this.getBdContainerEl().scrollTop=l.parentNode.offsetTop;}else{var k=this.getTrEl(m);if(k){this.clearScrollPositions();this.getBdContainerEl().scrollTop=k.offsetTop;}}},showTableMessage:function(o,k){var p=this._elMsgTd;if(c.isString(o)){p.firstChild.innerHTML=o;}if(c.isString(k)){d.addClass(p.firstChild,k);}var n=this.getTheadEl();var l=n.parentNode;var m=l.offsetWidth;this._elMsgTbody.parentNode.style.width=this.getTheadEl().parentNode.offsetWidth+"px";this._elMsgTbody.style.display="";this.fireEvent("tableMsgShowEvent",{html:o,className:k});},_onColumnChange:function(k){var l=(k.column)?k.column:(k.editor)?k.editor.column:null;this._storeScrollPositions();this.validateColumnWidths(l);},_onScroll:function(m,l){l._elHdContainer.scrollLeft=l._elBdContainer.scrollLeft;if(l._oCellEditor&&l._oCellEditor.isActive){l.fireEvent("editorBlurEvent",{editor:l._oCellEditor});l.cancelCellEditor();}var n=j.getTarget(m);var k=n.nodeName.toLowerCase();l.fireEvent("tableScrollEvent",{event:m,target:n});},_onTheadKeydown:function(n,l){if(j.getCharCode(n)===9){setTimeout(function(){if((l instanceof h)&&l._sId){l._elBdContainer.scrollLeft=l._elHdContainer.scrollLeft;}},0);}var o=j.getTarget(n);var k=o.nodeName.toLowerCase();var m=true;while(o&&(k!="table")){switch(k){case"body":return;case"input":case"textarea":break;case"thead":m=l.fireEvent("theadKeyEvent",{target:o,event:n});break;default:break;}if(m===false){return;}else{o=o.parentNode;if(o){k=o.nodeName.toLowerCase();}}}l.fireEvent("tableKeyEvent",{target:(o||l._elContainer),event:n});}});})();(function(){var c=YAHOO.lang,f=YAHOO.util,e=YAHOO.widget,b=YAHOO.env.ua,d=f.Dom,i=f.Event,h=e.DataTable;e.BaseCellEditor=function(k,j){this._sId=this._sId||d.generateId(null,"yui-ceditor");YAHOO.widget.BaseCellEditor._nCount++;this._sType=k;this._initConfigs(j);this._initEvents();this._needsRender=true;};var a=e.BaseCellEditor;c.augmentObject(a,{_nCount:0,CLASS_CELLEDITOR:"yui-ceditor"});a.prototype={_sId:null,_sType:null,_oDataTable:null,_oColumn:null,_oRecord:null,_elTd:null,_elContainer:null,_elCancelBtn:null,_elSaveBtn:null,_initConfigs:function(k){if(k&&YAHOO.lang.isObject(k)){for(var j in k){if(j){this[j]=k[j];}}}},_initEvents:function(){this.createEvent("showEvent");this.createEvent("keydownEvent");this.createEvent("invalidDataEvent");this.createEvent("revertEvent");this.createEvent("saveEvent");this.createEvent("cancelEvent");this.createEvent("blurEvent");this.createEvent("blockEvent");this.createEvent("unblockEvent");},_initContainerEl:function(){if(this._elContainer){YAHOO.util.Event.purgeElement(this._elContainer,true);this._elContainer.innerHTML="";}var j=document.createElement("div");j.id=this.getId()+"-container";j.style.display="none";j.tabIndex=0;this.className=c.isArray(this.className)?this.className:this.className?[this.className]:[];this.className[this.className.length]=h.CLASS_EDITOR;j.className=this.className.join(" ");document.body.insertBefore(j,document.body.firstChild);this._elContainer=j;},_initShimEl:function(){if(this.useIFrame){if(!this._elIFrame){var j=document.createElement("iframe"); +j.src="javascript:false";j.frameBorder=0;j.scrolling="no";j.style.display="none";j.className=h.CLASS_EDITOR_SHIM;j.tabIndex=-1;j.role="presentation";j.title="Presentational iframe shim";document.body.insertBefore(j,document.body.firstChild);this._elIFrame=j;}}},_hide:function(){this.getContainerEl().style.display="none";if(this._elIFrame){this._elIFrame.style.display="none";}this.isActive=false;this.getDataTable()._oCellEditor=null;},asyncSubmitter:null,value:null,defaultValue:null,validator:null,resetInvalidData:true,isActive:false,LABEL_SAVE:"Save",LABEL_CANCEL:"Cancel",disableBtns:false,useIFrame:false,className:null,toString:function(){return"CellEditor instance "+this._sId;},getId:function(){return this._sId;},getDataTable:function(){return this._oDataTable;},getColumn:function(){return this._oColumn;},getRecord:function(){return this._oRecord;},getTdEl:function(){return this._elTd;},getContainerEl:function(){return this._elContainer;},destroy:function(){this.unsubscribeAll();var k=this.getColumn();if(k){k.editor=null;}var j=this.getContainerEl();if(j){i.purgeElement(j,true);j.parentNode.removeChild(j);}},render:function(){if(!this._needsRender){return;}this._initContainerEl();this._initShimEl();i.addListener(this.getContainerEl(),"keydown",function(l,j){if((l.keyCode==27)){var k=i.getTarget(l);if(k.nodeName&&k.nodeName.toLowerCase()==="select"){k.blur();}j.cancel();}j.fireEvent("keydownEvent",{editor:j,event:l});},this);this.renderForm();if(!this.disableBtns){this.renderBtns();}this.doAfterRender();this._needsRender=false;},renderBtns:function(){var l=this.getContainerEl().appendChild(document.createElement("div"));l.className=h.CLASS_BUTTON;var k=l.appendChild(document.createElement("button"));k.className=h.CLASS_DEFAULT;k.innerHTML=this.LABEL_SAVE;i.addListener(k,"click",function(m){this.save();},this,true);this._elSaveBtn=k;var j=l.appendChild(document.createElement("button"));j.innerHTML=this.LABEL_CANCEL;i.addListener(j,"click",function(m){this.cancel();},this,true);this._elCancelBtn=j;},attach:function(n,l){if(n instanceof YAHOO.widget.DataTable){this._oDataTable=n;l=n.getTdEl(l);if(l){this._elTd=l;var m=n.getColumn(l);if(m){this._oColumn=m;var j=n.getRecord(l);if(j){this._oRecord=j;var k=j.getData(this.getColumn().getField());this.value=(k!==undefined)?k:this.defaultValue;return true;}}}}return false;},move:function(){var m=this.getContainerEl(),l=this.getTdEl(),j=d.getX(l),n=d.getY(l);if(isNaN(j)||isNaN(n)){var k=this.getDataTable().getTbodyEl();j=l.offsetLeft+d.getX(k.parentNode)-k.scrollLeft;n=l.offsetTop+d.getY(k.parentNode)-k.scrollTop+this.getDataTable().getTheadEl().offsetHeight;}m.style.left=j+"px";m.style.top=n+"px";if(this._elIFrame){this._elIFrame.style.left=j+"px";this._elIFrame.style.top=n+"px";}},show:function(){var k=this.getContainerEl(),j=this._elIFrame;this.resetForm();this.isActive=true;k.style.display="";if(j){j.style.width=k.offsetWidth+"px";j.style.height=k.offsetHeight+"px";j.style.display="";}this.focus();this.fireEvent("showEvent",{editor:this});},block:function(){this.fireEvent("blockEvent",{editor:this});},unblock:function(){this.fireEvent("unblockEvent",{editor:this});},save:function(){var k=this.getInputValue();var l=k;if(this.validator){l=this.validator.call(this.getDataTable(),k,this.value,this);if(l===undefined){if(this.resetInvalidData){this.resetForm();}this.fireEvent("invalidDataEvent",{editor:this,oldData:this.value,newData:k});return;}}var m=this;var j=function(o,n){var p=m.value;if(o){m.value=n;m.getDataTable().updateCell(m.getRecord(),m.getColumn(),n);m._hide();m.fireEvent("saveEvent",{editor:m,oldData:p,newData:m.value});}else{m.resetForm();m.fireEvent("revertEvent",{editor:m,oldData:p,newData:n});}m.unblock();};this.block();if(c.isFunction(this.asyncSubmitter)){this.asyncSubmitter.call(this,j,l);}else{j(true,l);}},cancel:function(){if(this.isActive){this._hide();this.fireEvent("cancelEvent",{editor:this});}else{}},renderForm:function(){},doAfterRender:function(){},handleDisabledBtns:function(){},resetForm:function(){},focus:function(){},getInputValue:function(){}};c.augmentProto(a,f.EventProvider);e.CheckboxCellEditor=function(j){j=j||{};this._sId=this._sId||d.generateId(null,"yui-checkboxceditor");YAHOO.widget.BaseCellEditor._nCount++;e.CheckboxCellEditor.superclass.constructor.call(this,j.type||"checkbox",j);};c.extend(e.CheckboxCellEditor,a,{checkboxOptions:null,checkboxes:null,value:null,renderForm:function(){if(c.isArray(this.checkboxOptions)){var n,o,q,l,m,k;for(m=0,k=this.checkboxOptions.length;m';l=this.getContainerEl().appendChild(document.createElement("label"));l.htmlFor=q;l.innerHTML=c.isValue(n.label)?n.label:n;}var p=[];for(m=0;m';o=this.getContainerEl().appendChild(document.createElement("label"));o.htmlFor=r;o.innerHTML=(c.isValue(k.label))?k.label:k;}var q=[],s;for(var m=0;m420){j=this.getContainerEl().appendChild(document.createElement("form")).appendChild(document.createElement("input"));}else{j=this.getContainerEl().appendChild(document.createElement("input"));}j.type="text";this.textbox=j;i.addListener(j,"keypress",function(k){if((k.keyCode===13)){YAHOO.util.Event.preventDefault(k);this.save();}},this,true);if(this.disableBtns){this.handleDisabledBtns();}},move:function(){this.textbox.style.width=this.getTdEl().offsetWidth+"px";e.TextboxCellEditor.superclass.move.call(this);},resetForm:function(){this.textbox.value=c.isValue(this.value)?this.value.toString():"";},focus:function(){this.getDataTable()._focusEl(this.textbox);this.textbox.select();},getInputValue:function(){return this.textbox.value; +}});c.augmentObject(e.TextboxCellEditor,a);h.Editors={checkbox:e.CheckboxCellEditor,"date":e.DateCellEditor,dropdown:e.DropdownCellEditor,radio:e.RadioCellEditor,textarea:e.TextareaCellEditor,textbox:e.TextboxCellEditor};e.CellEditor=function(k,j){if(k&&h.Editors[k]){c.augmentObject(a,h.Editors[k]);return new h.Editors[k](j);}else{return new a(null,j);}};var g=e.CellEditor;c.augmentObject(g,a);})();YAHOO.register("datatable",YAHOO.widget.DataTable,{version:"2.9.0",build:"2800"}); + + diff --git a/rhodecode/templates/_data_table/_dt_elements.html b/rhodecode/templates/_data_table/_dt_elements.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/_data_table/_dt_elements.html @@ -0,0 +1,78 @@ +## DATA TABLE RE USABLE ELEMENTS +## usage: +## <%namespace name="dt" file="/_data_table/_dt_elements.html"/> + +<%def name="quick_menu(repo_name)"> + + + +<%def name="repo_name(name,rtype,private,fork_of)"> +
+ ##TYPE OF REPO + %if h.is_hg(rtype): + ${_('Mercurial repository')} + %elif h.is_git(rtype): + ${_('Git repository')} + %endif + + ##PRIVATE/PUBLIC + %if private: + ${_('private repository')} + %else: + ${_('public repository')} + %endif + + ##NAME + ${h.link_to(name,h.url('summary_home',repo_name=name),class_="repo_name")} + %if fork_of: + + ${_('fork')} + %endif +
+ + + + +<%def name="revision(name,rev,tip,author,last_msg)"> +
+ %if rev >= 0: +
${'r%s:%s' % (rev,h.short_id(tip))}
+ %else: + ${_('No changesets yet')} + %endif +
+ diff --git a/rhodecode/templates/admin/admin.html b/rhodecode/templates/admin/admin.html --- a/rhodecode/templates/admin/admin.html +++ b/rhodecode/templates/admin/admin.html @@ -24,5 +24,5 @@ ${c.log_data}
- - \ No newline at end of file + + diff --git a/rhodecode/templates/admin/admin_log.html b/rhodecode/templates/admin/admin_log.html --- a/rhodecode/templates/admin/admin_log.html +++ b/rhodecode/templates/admin/admin_log.html @@ -23,7 +23,7 @@ ${l.repository_name} %endif - + ${l.action_date} ${l.user_ip} @@ -36,7 +36,7 @@ ypjax(e.target.href,"user_log",function(){show_more_event();tooltip_activate();}); YUE.preventDefault(e); },'.pager_link'); - + YUE.delegate("user_log","click",function(e,matchedEl,container){ var el = e.target; YUD.setStyle(YUD.get(el.id.substring(1)),'display',''); @@ -48,6 +48,6 @@
${c.users_log.pager('$link_previous ~2~ $link_next')}
-%else: - ${_('No actions yet')} -%endif \ No newline at end of file +%else: + ${_('No actions yet')} +%endif diff --git a/rhodecode/templates/admin/ldap/ldap.html b/rhodecode/templates/admin/ldap/ldap.html --- a/rhodecode/templates/admin/ldap/ldap.html +++ b/rhodecode/templates/admin/ldap/ldap.html @@ -6,9 +6,9 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.url('admin_home'))} » - ${_('Ldap')} + ${_('Ldap')} <%def name="page_nav()"> @@ -19,7 +19,7 @@
- ${self.breadcrumbs()} + ${self.breadcrumbs()}
${h.form(url('ldap_settings'))}
@@ -84,20 +84,12 @@
${h.text('ldap_attr_email',class_='small')}
- +
${h.submit('save',_('Save'),class_="ui-button")} -
+
- - ${h.end_form()} + + ${h.end_form()} - - - - - - - - - + diff --git a/rhodecode/templates/admin/notifications/notifications.html b/rhodecode/templates/admin/notifications/notifications.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/admin/notifications/notifications.html @@ -0,0 +1,53 @@ +## -*- coding: utf-8 -*- +<%inherit file="/base/base.html"/> + +<%def name="title()"> + ${_('My Notifications')} ${c.rhodecode_user.username} - ${c.rhodecode_name} + + +<%def name="breadcrumbs_links()"> + ${_('My Notifications')} + + +<%def name="page_nav()"> + ${self.menu('admin')} + + +<%def name="main()"> +
+ +
+ ${self.breadcrumbs()} + ## +
+ %if c.notifications: +
+ ${_('Mark all read')} +
+ %endif +
+ <%include file='notifications_data.html'/> +
+
+ + diff --git a/rhodecode/templates/admin/notifications/notifications_data.html b/rhodecode/templates/admin/notifications/notifications_data.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/admin/notifications/notifications_data.html @@ -0,0 +1,28 @@ + +%if c.notifications: +<% +unread = lambda n:{False:'unread'}.get(n) +%> +
+
+ %for notification in c.notifications: +
+ +
${h.literal(notification.notification.subject)}
+
+ %endfor +
+
+%else: +
${_('No notifications here yet')}
+%endif diff --git a/rhodecode/templates/admin/notifications/show_notification.html b/rhodecode/templates/admin/notifications/show_notification.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/admin/notifications/show_notification.html @@ -0,0 +1,54 @@ +## -*- coding: utf-8 -*- +<%inherit file="/base/base.html"/> + +<%def name="title()"> + ${_('Show notification')} ${c.rhodecode_user.username} - ${c.rhodecode_name} + + +<%def name="breadcrumbs_links()"> + ${h.link_to(_('Notifications'),h.url('notifications'))} + » + ${_('Show notification')} + + +<%def name="page_nav()"> + ${self.menu('admin')} + + +<%def name="main()"> +
+ +
+ ${self.breadcrumbs()} + ## +
+
+
+
+
+ gravatar +
+
+ ${c.notification.description} +
+
+ +
+
+
${h.rst_w_mentions(c.notification.body)}
+
+
+
+ + diff --git a/rhodecode/templates/admin/permissions/permissions.html b/rhodecode/templates/admin/permissions/permissions.html --- a/rhodecode/templates/admin/permissions/permissions.html +++ b/rhodecode/templates/admin/permissions/permissions.html @@ -6,9 +6,9 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.url('admin_home'))} » - ${_('Permissions')} + ${_('Permissions')} <%def name="page_nav()"> @@ -19,7 +19,7 @@
- ${self.breadcrumbs()} + ${self.breadcrumbs()}

${_('Default permissions')}

${h.form(url('permission', id='default'),method='put')} @@ -35,21 +35,21 @@ ${h.checkbox('anonymous',True)}
- +
${h.select('default_perm','',c.perms_choices)} - + ${h.checkbox('overwrite_default','true')} -
-
+ +
@@ -57,7 +57,7 @@
${h.select('default_register','',c.register_choices)}
-
+
@@ -65,20 +65,13 @@
${h.select('default_create','',c.create_choices)}
-
- +
+
${h.submit('set',_('set'),class_="ui-button")} -
+ - + ${h.end_form()} - - - - - - - - + diff --git a/rhodecode/templates/admin/repos/repo_add.html b/rhodecode/templates/admin/repos/repo_add.html --- a/rhodecode/templates/admin/repos/repo_add.html +++ b/rhodecode/templates/admin/repos/repo_add.html @@ -6,9 +6,9 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} - » - ${h.link_to(_('Repositories'),h.url('repos'))} + ${h.link_to(_('Admin'),h.url('admin_home'))} + » + ${h.link_to(_('Repositories'),h.url('repos'))} » ${_('add new')} @@ -21,8 +21,8 @@
- ${self.breadcrumbs()} + ${self.breadcrumbs()}
<%include file="repo_add_base.html"/> -
- \ No newline at end of file + + diff --git a/rhodecode/templates/admin/repos/repo_add_base.html b/rhodecode/templates/admin/repos/repo_add_base.html --- a/rhodecode/templates/admin/repos/repo_add_base.html +++ b/rhodecode/templates/admin/repos/repo_add_base.html @@ -22,7 +22,7 @@
${h.text('clone_uri',class_="small")}
- +
@@ -30,7 +30,7 @@
${h.select('repo_group','',c.repo_groups,class_="medium")}
-
+
@@ -38,7 +38,7 @@
${h.select('repo_type','hg',c.backends,class_="small")}
-
+
@@ -57,7 +57,7 @@
${h.submit('add',_('add'),class_="ui-button")} -
+
- -${h.end_form()} + +${h.end_form()} diff --git a/rhodecode/templates/admin/repos/repo_add_create_repository.html b/rhodecode/templates/admin/repos/repo_add_create_repository.html --- a/rhodecode/templates/admin/repos/repo_add_create_repository.html +++ b/rhodecode/templates/admin/repos/repo_add_create_repository.html @@ -17,8 +17,8 @@
- ${self.breadcrumbs()} + ${self.breadcrumbs()}
<%include file="repo_add_base.html"/> -
- \ No newline at end of file + + diff --git a/rhodecode/templates/admin/repos/repo_edit.html b/rhodecode/templates/admin/repos/repo_edit.html --- a/rhodecode/templates/admin/repos/repo_edit.html +++ b/rhodecode/templates/admin/repos/repo_edit.html @@ -6,9 +6,9 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} - » - ${h.link_to(_('Repositories'),h.url('repos'))} + ${h.link_to(_('Admin'),h.url('admin_home'))} + » + ${h.link_to(_('Repositories'),h.url('repos'))} » ${_('edit')} » ${h.link_to(c.repo_info.just_name,h.url('summary_home',repo_name=c.repo_name))} @@ -21,7 +21,7 @@
- ${self.breadcrumbs()} + ${self.breadcrumbs()}
${h.form(url('repo', repo_name=c.repo_info.repo_name),method='put')}
@@ -42,7 +42,7 @@
${h.text('clone_uri',class_="medium")}
-
+
@@ -50,7 +50,7 @@
${h.select('repo_group','',c.repo_groups,class_="medium")}
-
+
@@ -58,7 +58,7 @@
${h.select('repo_type','hg',c.backends,class_="medium")}
-
+
@@ -67,7 +67,7 @@ ${h.textarea('description',cols=23,rows=5)}
- +
@@ -83,7 +83,7 @@
${h.checkbox('enable_statistics',value="True")}
-
+
@@ -91,7 +91,7 @@
${h.checkbox('enable_downloads',value="True")}
-
+
@@ -102,8 +102,8 @@
- - + +
@@ -111,87 +111,114 @@
<%include file="repo_edit_perms.html"/>
- -
- ${h.submit('save','Save',class_="ui-button")} - ${h.reset('reset','Reset',class_="ui-button")} -
-
+ +
+ ${h.submit('save','Save',class_="ui-button")} + ${h.reset('reset','Reset',class_="ui-button")} +
+
- + ${h.end_form()}
-
${_('Administration')}
+
${_('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')+"');")} -
+ ${h.submit('reset_stats_%s' % c.repo_info.repo_name,_('Reset current statistics'),class_="ui-btn",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} %
  • +
  • ${_('Stats gathered')}: ${c.stats_percentage}%
-
-
+
${h.end_form()} - + %if c.repo_info.clone_uri:

${_('Remote')}

${h.form(url('repo_pull', repo_name=c.repo_info.repo_name),method='put')}
- ${h.submit('remote_pull_%s' % c.repo_info.repo_name,_('Pull changes from remote location'),class_="pull_icon action_button",onclick="return confirm('"+_('Confirm to pull changes from remote side')+"');")} + ${h.submit('remote_pull_%s' % c.repo_info.repo_name,_('Pull changes from remote location'),class_="ui-btn",onclick="return confirm('"+_('Confirm to pull changes from remote side')+"');")} + +
-
+ ${h.end_form()} %endif - +

${_('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.submit('reset_cache_%s' % c.repo_info.repo_name,_('Invalidate repository cache'),class_="ui-btn",onclick="return confirm('"+_('Confirm to invalidate repository cache')+"');")}
-
+ ${h.end_form()} - +

${_('Public journal')}

${h.form(url('repo_public_journal', repo_name=c.repo_info.repo_name),method='put')}
-
${h.hidden('auth_token',str(h.get_token()))} +
%if c.in_public_journal: - ${h.submit('set_public_%s' % c.repo_info.repo_name,_('Remove from public journal'),class_="stop_following_icon action_button")} + ${h.submit('set_public_%s' % c.repo_info.repo_name,_('Remove from public journal'),class_="ui-btn")} %else: - ${h.submit('set_public_%s' % c.repo_info.repo_name,_('Add to public journal'),class_="start_following_icon action_button")} + ${h.submit('set_public_%s' % c.repo_info.repo_name,_('Add to public journal'),class_="ui-btn")} %endif -
+
+
+
    +
  • ${_('''All actions made on this repository will be accessible to everyone in public journal''')} +
  • +
+
${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.submit('remove_%s' % c.repo_info.repo_name,_('Remove this repository'),class_="ui-btn red",onclick="return confirm('"+_('Confirm to delete this repository')+"');")} +
+
+
    +
  • ${_('''This repository will be renamed in a special way in order to be unaccesible for RhodeCode and VCS systems. + If you need fully delete it from filesystem please do it manually''')} +
  • +
-
+ ${h.end_form()} - + +

${_('Set as fork')}

+ ${h.form(url('repo_as_fork', repo_name=c.repo_info.repo_name),method='put')} +
+
+ ${h.select('id_fork_of','',c.repos_list,class_="medium")} + ${h.submit('set_as_fork_%s' % c.repo_info.repo_name,_('set'),class_="ui-btn",)} +
+
+
    +
  • ${_('''Manually set this repository as a fork of another''')}
  • +
+
+
+ ${h.end_form()} + - + diff --git a/rhodecode/templates/admin/repos/repo_edit_perms.html b/rhodecode/templates/admin/repos/repo_edit_perms.html --- a/rhodecode/templates/admin/repos/repo_edit_perms.html +++ b/rhodecode/templates/admin/repos/repo_edit_perms.html @@ -1,4 +1,4 @@ - +
@@ -7,7 +7,7 @@ - ## USERS + ## USERS %for r2p in c.repo_info.repo_to_perm: %if r2p.user.username =='default' and c.repo_info.private: @@ -16,7 +16,7 @@ ${_('private repository')} - + %else: @@ -32,13 +32,13 @@ ${_('revoke')} - %endif + %endif %endif %endfor - - ## USERS GROUPS + + ## USERS GROUPS %for g2p in c.repo_info.users_group_to_perm: @@ -92,10 +92,10 @@ function ajaxActionUser(user_id, field_i var postData = '_method=delete&user_id=' + user_id; var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, callback, postData); }; - + function ajaxActionUsersGroup(users_group_id,field_id){ var sUrl = "${h.url('delete_repo_users_group',repo_name=c.repo_name)}"; - var callback = { + var callback = { success:function(o){ var tr = YUD.get(String(field_id)); tr.parentNode.removeChild(tr); @@ -104,7 +104,7 @@ function ajaxActionUsersGroup(users_grou alert("${_('Failed to remove users group')}"); }, }; - var postData = '_method=delete&users_group_id='+users_group_id; + var postData = '_method=delete&users_group_id='+users_group_id; var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, callback, postData); }; @@ -267,7 +267,7 @@ YAHOO.example.FnMultipleFields = functio membersAC.itemSelectEvent.subscribe(myHandler); if(ownerAC.itemSelectEvent){ - ownerAC.itemSelectEvent.subscribe(myHandler); + ownerAC.itemSelectEvent.subscribe(myHandler); } return { @@ -277,5 +277,5 @@ YAHOO.example.FnMultipleFields = functio ownerAC: ownerAC, }; }(); - - \ No newline at end of file + + diff --git a/rhodecode/templates/admin/repos/repos.html b/rhodecode/templates/admin/repos/repos.html --- a/rhodecode/templates/admin/repos/repos.html +++ b/rhodecode/templates/admin/repos/repos.html @@ -14,75 +14,111 @@ <%def name="main()">
- +
${self.breadcrumbs()} + ${h.link_to(_(u'ADD REPOSITORY'),h.url('new_repo'))} + +
- +
-
${_('none')} ${_('read')}${_('member')}
${r2p.user.username}${r2p.user.username}
${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'repository.none')}
- - - - - - - - - %for cnt,repo in enumerate(c.repos_list): - - - - - - - - - %endfor +
+ <%cnt=0%> + <%namespace name="dt" file="/_data_table/_dt_elements.html"/> + +
${_('Name')}${_('Description')}${_('Last change')}${_('Tip')}${_('Contact')}${_('action')}
- ## TYPE OF REPO - %if repo['dbrepo']['repo_type'] =='hg': - ${_('Mercurial repository')} - %elif repo['dbrepo']['repo_type'] =='git': - ${_('Git repository')} - %else: - - %endif - - ## PRIVATE/PUBLIC REPO - %if repo['dbrepo']['private']: - ${_('private')} - %else: - ${_('public')} - %endif - ${h.link_to(repo['name'],h.url('edit_repo',repo_name=repo['name']))} - - %if 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'],h.short_id(repo['tip'])), - h.url('changeset_home',repo_name=repo['name'],revision=repo['tip']), - class_="tooltip", - title=h.tooltip(repo['last_msg']))} - %else: - ${_('No changesets yet')} - %endif - ${h.person(repo['contact'])} - ${h.form(url('repo', 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.end_form()} -
+ + + + + + + + + + + + + %for cnt,repo in enumerate(c.repos_list,1): + + + + ##DESCRIPTION + + ##LAST CHANGE + + ##LAST REVISION + + + + + %endfor
${_('Name')}${_('Description')}${_('Last change')}${_('Tip')}${_('Contact')}${_('Action')}
+ ${dt.quick_menu(repo['name'])} + + ${dt.repo_name(repo['name'],repo['dbrepo']['repo_type'],repo['dbrepo']['private'],repo['dbrepo_fork'].get('repo_name'))} + + ${h.truncate(repo['description'],60)} + + ${h.age(repo['last_change'])} + + ${dt.revision(repo['name'],repo['rev'],repo['tip'],repo['author'],repo['last_msg'])} + ${h.person(repo['contact'])} + ${h.form(url('repo', 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: %s') % repo['name']+"');")} + ${h.end_form()} +
+ - - - + + + diff --git a/rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html b/rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html @@ -0,0 +1,270 @@ + + + + + + + + + + ## USERS + %for r2p in c.repos_group.repo_group_to_perm: + + + + + + + + + %endfor + + ## USERS GROUPS + %for g2p in c.repos_group.users_group_to_perm: + + + + + + + + + %endfor + + + + + + + + + + + +
${_('none')}${_('read')}${_('write')}${_('admin')}${_('member')}
${h.radio('u_perm_%s' % r2p.user.username,'group.none')}${h.radio('u_perm_%s' % r2p.user.username,'group.read')}${h.radio('u_perm_%s' % r2p.user.username,'group.write')}${h.radio('u_perm_%s' % r2p.user.username,'group.admin')} + ${r2p.user.username} + + %if r2p.user.username !='default': + + ${_('revoke')} + + %endif +
${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'group.none')}${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'group.read')}${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'group.write')}${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'group.admin')} + ${g2p.users_group.users_group_name} + + + ${_('revoke')} + +
${h.radio('perm_new_member','group.none')}${h.radio('perm_new_member','group.read')}${h.radio('perm_new_member','group.write')}${h.radio('perm_new_member','group.admin')} +
+ ${h.text('perm_new_member_name',class_='yui-ac-input')} + ${h.hidden('perm_new_member_type')} +
+
+
+ + ${_('Add another member')} + +
+ diff --git a/rhodecode/templates/admin/repos_groups/repos_groups.html b/rhodecode/templates/admin/repos_groups/repos_groups.html --- a/rhodecode/templates/admin/repos_groups/repos_groups.html +++ b/rhodecode/templates/admin/repos_groups/repos_groups.html @@ -5,14 +5,12 @@ <%def name="breadcrumbs()"> - - ${_('Groups')} + ${_('Groups')} %if c.group.parent_group: - » ${h.link_to(c.group.parent_group.name, - h.url('repos_group_home',group_name=c.group.parent_group.group_name))} + » ${h.link_to(c.group.parent_group.name,h.url('repos_group_home',group_name=c.group.parent_group.group_name))} %endif » "${c.group.name}" ${_('with')} - + <%def name="page_nav()"> @@ -20,4 +18,4 @@ <%def name="main()"> <%include file="/index_base.html" args="parent=self"/> - + diff --git a/rhodecode/templates/admin/repos_groups/repos_groups_add.html b/rhodecode/templates/admin/repos_groups/repos_groups_add.html --- a/rhodecode/templates/admin/repos_groups/repos_groups_add.html +++ b/rhodecode/templates/admin/repos_groups/repos_groups_add.html @@ -5,9 +5,9 @@ ${_('Add repos group')} - ${c.rhodecode_name} <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} - » - ${h.link_to(_('Repos groups'),h.url('repos_groups'))} + ${h.link_to(_('Admin'),h.url('admin_home'))} + » + ${h.link_to(_('Repos groups'),h.url('repos_groups'))} » ${_('add new repos group')} @@ -20,7 +20,7 @@
- ${self.breadcrumbs()} + ${self.breadcrumbs()}
${h.form(url('repos_groups'))} @@ -35,7 +35,7 @@ ${h.text('group_name',class_='medium')}
- +
@@ -44,7 +44,7 @@ ${h.textarea('group_description',cols=23,rows=5,class_="medium")}
- +
@@ -52,13 +52,13 @@
${h.select('group_parent_id','',c.repo_groups,class_="medium")}
-
- +
+
${h.submit('save',_('save'),class_="ui-button")} -
+ ${h.end_form()} - - + + diff --git a/rhodecode/templates/admin/repos_groups/repos_groups_edit.html b/rhodecode/templates/admin/repos_groups/repos_groups_edit.html --- a/rhodecode/templates/admin/repos_groups/repos_groups_edit.html +++ b/rhodecode/templates/admin/repos_groups/repos_groups_edit.html @@ -5,9 +5,9 @@ ${_('Edit repos group')} ${c.repos_group.name} - ${c.rhodecode_name} <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} - » - ${h.link_to(_('Repos groups'),h.url('repos_groups'))} + ${h.link_to(_('Admin'),h.url('admin_home'))} + » + ${h.link_to(_('Repos groups'),h.url('repos_groups'))} » ${_('edit repos group')} "${c.repos_group.name}" @@ -20,7 +20,7 @@
- ${self.breadcrumbs()} + ${self.breadcrumbs()}
${h.form(url('repos_group',id=c.repos_group.group_id),method='put')} @@ -35,7 +35,7 @@ ${h.text('group_name',class_='medium')}
- +
@@ -44,7 +44,7 @@ ${h.textarea('group_description',cols=23,rows=5,class_="medium")}
- +
@@ -52,13 +52,22 @@
${h.select('group_parent_id','',c.repo_groups,class_="medium")}
-
- +
+
+
+ +
+
+ <%include file="repos_group_edit_perms.html"/> +
+ +
- ${h.submit('save',_('save'),class_="ui-button")} -
+ ${h.submit('save',_('Save'),class_="ui-button")} + ${h.reset('reset',_('Reset'),class_="ui-button")} + ${h.end_form()} - - + + diff --git a/rhodecode/templates/admin/repos_groups/repos_groups_show.html b/rhodecode/templates/admin/repos_groups/repos_groups_show.html --- a/rhodecode/templates/admin/repos_groups/repos_groups_show.html +++ b/rhodecode/templates/admin/repos_groups/repos_groups_show.html @@ -20,25 +20,25 @@ + +
% if c.groups: - + - + - + ## REPO GROUPS - + % for gr in c.groups: + % endfor - +
${_('Group name')} ${_('Description')}${_('Number of repositories')}${_('Number of toplevel repositories')} ${_('action')}
@@ -51,18 +51,18 @@ ${gr.repositories.count()} ${h.form(url('repos_group', id=gr.group_id),method='delete')} - ${h.submit('remove_%s' % gr.name,'delete',class_="delete_icon action_button",onclick="return confirm('"+_('Confirm to delete this group')+"');")} + ${h.submit('remove_%s' % gr.name,'delete',class_="delete_icon action_button",onclick="return confirm('"+_('Confirm to delete this group: %s') % gr.name+"');")} ${h.end_form()} -
% else: ${_('There are no repositories groups yet')} % endif - +
- - - + + + diff --git a/rhodecode/templates/admin/settings/hooks.html b/rhodecode/templates/admin/settings/hooks.html --- a/rhodecode/templates/admin/settings/hooks.html +++ b/rhodecode/templates/admin/settings/hooks.html @@ -17,10 +17,10 @@
- ${self.breadcrumbs()} + ${self.breadcrumbs()}
- +

${_('Built in hooks - read only')}

@@ -36,29 +36,29 @@ % endfor
- +

${_('Custom hooks')}

${h.form(url('admin_setting', setting_id='hooks'),method='put')}
- + % for hook in c.custom_hooks:
-
+
${h.hidden('hook_ui_key',hook.ui_key)} ${h.hidden('hook_ui_value',hook.ui_value)} ${h.text('hook_ui_value_new',hook.ui_value,size=60)} - ${_('remove')}
- % endfor - + % endfor +
@@ -71,7 +71,7 @@
${h.submit('save',_('Save'),class_="ui-button")} -
+
${h.end_form()} @@ -92,5 +92,5 @@ function ajaxActionHook(hook_id,field_id var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, callback, postData); }; - - + + diff --git a/rhodecode/templates/admin/settings/settings.html b/rhodecode/templates/admin/settings/settings.html --- a/rhodecode/templates/admin/settings/settings.html +++ b/rhodecode/templates/admin/settings/settings.html @@ -17,15 +17,15 @@
- ${self.breadcrumbs()} + ${self.breadcrumbs()}
- +

${_('Remap and rescan repositories')}

${h.form(url('admin_setting', setting_id='mapping'),method='put')}
- +
@@ -40,19 +40,19 @@
- +
${h.submit('rescan',_('Rescan repositories'),class_="ui-button")} -
+
-
+
${h.end_form()} - +

${_('Whoosh indexing')}

${h.form(url('admin_setting', setting_id='whoosh'),method='put')}
- +
@@ -65,21 +65,21 @@
- +
${h.submit('reindex',_('Reindex'),class_="ui-button")} -
+
- + ${h.end_form()} - -

${_('Global application settings')}

+ +

${_('Global application settings')}

${h.form(url('admin_setting', setting_id='global'),method='put')}
- +
- +
@@ -88,7 +88,7 @@ ${h.text('rhodecode_title',size=30)}
- +
@@ -97,7 +97,7 @@ ${h.text('rhodecode_realm',size=30)}
- +
@@ -106,22 +106,22 @@ ${h.text('rhodecode_ga_code',size=30)}
- +
${h.submit('save',_('Save settings'),class_="ui-button")} ${h.reset('reset',_('Reset'),class_="ui-button")} -
+
- + ${h.end_form()} -

${_('Mercurial settings')}

+

${_('Mercurial settings')}

${h.form(url('admin_setting', setting_id='mercurial'),method='put')}
- +
- +
@@ -132,15 +132,12 @@
-
+
-
- ${h.link_to(_('advanced setup'),url('admin_edit_setting',setting_id='hooks'))} -
${h.checkbox('hooks_changegroup_update','True')} @@ -157,30 +154,32 @@
${h.checkbox('hooks_preoutgoing_pull_logger','True')} -
+
-
- +
+ ${h.link_to(_('advanced setup'),url('admin_edit_setting',setting_id='hooks'),class_="ui-btn")} +
+
- ${h.text('paths_root_path',size=30,readonly="readonly")} - ${_('unlock')}
- +
${h.submit('save',_('Save settings'),class_="ui-button")} ${h.reset('reset',_('Reset'),class_="ui-button")} -
+ - + ${h.end_form()} - + + +

${_('Test Email')}

+ ${h.form(url('admin_setting', setting_id='email'),method='put')} +
+ + +
+
+
+ +
+
+ ${h.text('test_email',size=30)} +
+
+ +
+ ${h.submit('send',_('Send'),class_="ui-button")} +
+
+
+ ${h.end_form()} + - + diff --git a/rhodecode/templates/admin/users/user_add.html b/rhodecode/templates/admin/users/user_add.html --- a/rhodecode/templates/admin/users/user_add.html +++ b/rhodecode/templates/admin/users/user_add.html @@ -5,9 +5,9 @@ ${_('Add user')} - ${c.rhodecode_name} <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} - » - ${h.link_to(_('Users'),h.url('users'))} + ${h.link_to(_('Admin'),h.url('admin_home'))} + » + ${h.link_to(_('Users'),h.url('users'))} » ${_('add new user')} @@ -20,7 +20,7 @@
- ${self.breadcrumbs()} + ${self.breadcrumbs()}
${h.form(url('users'))} @@ -35,7 +35,7 @@ ${h.text('username',class_='small')}
- +
@@ -44,7 +44,7 @@ ${h.password('password',class_='small')}
- +
@@ -52,8 +52,8 @@
${h.password('password_confirmation',class_="small",autocomplete="off")}
-
- +
+
@@ -62,7 +62,7 @@ ${h.text('name',class_='small')}
- +
@@ -71,7 +71,7 @@ ${h.text('lastname',class_='small')}
- +
@@ -80,21 +80,21 @@ ${h.text('email',class_='small')}
- +
- ${h.checkbox('active',value=True)} + ${h.checkbox('active',value=True,checked='checked')}
- +
${h.submit('save',_('save'),class_="ui-button")} -
+ ${h.end_form()} - - + + diff --git a/rhodecode/templates/admin/users/user_edit_my_account.html b/rhodecode/templates/admin/users/user_edit_my_account.html --- a/rhodecode/templates/admin/users/user_edit_my_account.html +++ b/rhodecode/templates/admin/users/user_edit_my_account.html @@ -18,13 +18,13 @@
- ${self.breadcrumbs()} + ${self.breadcrumbs()}
${h.form(url('admin_settings_my_account_update'),method='put')}
- +
gravatar
@@ -34,15 +34,15 @@
${_('Using')} ${c.user.email} %else:
${c.user.email} - %endif + %endif

-
+
${c.user.api_key}
-
+
@@ -52,7 +52,7 @@ ${h.text('username',class_="medium")}
- +
@@ -61,7 +61,7 @@ ${h.password('new_password',class_="medium",autocomplete="off")}
- +
@@ -70,7 +70,7 @@ ${h.password('password_confirmation',class_="medium",autocomplete="off")}
- +
@@ -79,7 +79,7 @@ ${h.text('name',class_="medium")}
- +
@@ -88,7 +88,7 @@ ${h.text('lastname',class_="medium")}
- +
@@ -97,30 +97,31 @@ ${h.text('email',class_="medium")}
- +
${h.submit('save',_('Save'),class_="ui-button")} ${h.reset('reset',_('Reset'),class_="ui-button")} -
+
- + ${h.end_form()} - +
-
${_('My repositories')} - +
+ + ${_('My repositories')}
%if h.HasPermissionAny('hg.admin','hg.create.repository')(): - %endif + + + %endif
@@ -129,98 +130,61 @@ ${_('Name')} ${_('revision')} - ${_('action')} + ${_('action')} %if c.user_repos: %for repo in c.user_repos: - %if repo['dbrepo']['repo_type'] =='hg': - ${_('Mercurial repository')} - %elif repo['dbrepo']['repo_type'] =='git': - ${_('Git repository')} + %if h.is_hg(repo['dbrepo']['repo_type']): + ${_('Mercurial repository')} + %elif h.is_git(repo['dbrepo']['repo_type']): + ${_('Git repository')} %else: - - %endif + + %endif %if repo['dbrepo']['private']: - ${_('private')} + ${_('private')} %else: - ${_('public')} + ${_('public')} %endif - + ${h.link_to(repo['name'], h.url('summary_home',repo_name=repo['name']),class_="repo_name")} %if repo['dbrepo_fork']: ${_('public')} - %endif - + %endif + ${("r%s:%s") % (repo['rev'],h.short_id(repo['tip']))} ${_('private')} ${h.form(url('repo_settings_delete', repo_name=repo['name']),method='delete')} - ${h.submit('remove_%s' % repo['name'],'',class_="delete_icon action_button",onclick="return confirm('Confirm to delete this repository');")} - ${h.end_form()} + ${h.submit('remove_%s' % repo['name'],'',class_="delete_icon action_button",onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo['name']+"');")} + ${h.end_form()} %endfor %else:
- ${_('No repositories yet')} + ${_('No repositories yet')} %if h.HasPermissionAny('hg.admin','hg.create.repository')(): - ${h.link_to(_('create one now'),h.url('admin_settings_create_repository'),class_="ui-button-small")} + ${h.link_to(_('create one now'),h.url('admin_settings_create_repository'),class_="ui-btn")} %endif
%endif
-
- - + + diff --git a/rhodecode/templates/admin/users/users.html b/rhodecode/templates/admin/users/users.html --- a/rhodecode/templates/admin/users/users.html +++ b/rhodecode/templates/admin/users/users.html @@ -22,8 +22,8 @@
  • ${h.link_to(_(u'ADD NEW USER'),h.url('new_user'))}
  • - - + +
    @@ -53,7 +53,7 @@ ${h.form(url('delete_user', id=user.user_id),method='delete')} ${h.submit('remove_',_('delete'),id="remove_user_%s" % user.user_id, - class_="delete_icon action_button",onclick="return confirm('"+_('Confirm to delete this user')+"');")} + class_="delete_icon action_button",onclick="return confirm('"+_('Confirm to delete this user: %s') % user.username+"');")} ${h.end_form()} diff --git a/rhodecode/templates/admin/users_groups/users_group_add.html b/rhodecode/templates/admin/users_groups/users_group_add.html --- a/rhodecode/templates/admin/users_groups/users_group_add.html +++ b/rhodecode/templates/admin/users_groups/users_group_add.html @@ -5,9 +5,9 @@ ${_('Add users group')} - ${c.rhodecode_name} <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} - » - ${h.link_to(_('Users groups'),h.url('users_groups'))} + ${h.link_to(_('Admin'),h.url('admin_home'))} + » + ${h.link_to(_('Users groups'),h.url('users_groups'))} » ${_('add new users group')} @@ -20,7 +20,7 @@
    - ${self.breadcrumbs()} + ${self.breadcrumbs()}
    ${h.form(url('users_groups'))} @@ -35,21 +35,21 @@ ${h.text('users_group_name',class_='small')}
    - +
    - ${h.checkbox('users_group_active',value=True)} + ${h.checkbox('users_group_active',value=True, checked='checked')}
    - +
    ${h.submit('save',_('save'),class_="ui-button")} -
    + ${h.end_form()} - - + + diff --git a/rhodecode/templates/admin/users_groups/users_group_edit.html b/rhodecode/templates/admin/users_groups/users_group_edit.html --- a/rhodecode/templates/admin/users_groups/users_group_edit.html +++ b/rhodecode/templates/admin/users_groups/users_group_edit.html @@ -6,9 +6,9 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} - » - ${h.link_to(_('UsersGroups'),h.url('users_groups'))} + ${h.link_to(_('Admin'),h.url('admin_home'))} + » + ${h.link_to(_('UsersGroups'),h.url('users_groups'))} » ${_('edit')} "${c.users_group.users_group_name}" @@ -21,7 +21,7 @@
    - ${self.breadcrumbs()} + ${self.breadcrumbs()}
    ${h.form(url('users_group', id=c.users_group.users_group_id),method='put', id='edit_users_group')} @@ -36,7 +36,7 @@ ${h.text('users_group_name',class_='small')}
    - +
    @@ -50,7 +50,7 @@
    - +
    - -
    @@ -59,193 +59,45 @@ ${h.select('users_group_members',[x[0] for x in c.group_members],c.group_members,multiple=True,size=8,style="min-width:210px")}
    ${_('Remove all elements')} - remove + remove
    - add + add
    - remove + remove
    ${_('Available members')}
    ${h.select('available_members',[],c.available_members,multiple=True,size=8,style="min-width:210px")}
    - add + add ${_('Add all elements')} -
    +
    -
    -
    + + + +
    - +
    ${h.submit('save',_('save'),class_="ui-button")} -
    - - -${h.end_form()} + + + +${h.end_form()} - -
    -
    ${_('Permissions')}
    +
    ${_('Permissions')}
    ${h.form(url('users_group_perm', id=c.users_group.users_group_id), method='put')}
    @@ -262,9 +114,167 @@
    ${h.submit('save',_('Save'),class_="ui-button")} ${h.reset('reset',_('Reset'),class_="ui-button")} -
    -
    +
    + + + ${h.end_form()} + + +
    + +
    +
    ${_('Group members')}
    +
    +
    +
      + %for user in c.group_members_obj: +
    • +
      +
      gravatar
      +
      ${user.username}
      +
      ${user.full_name}
      +
      +
    • + %endfor +
    - ${h.end_form()}
    - + + diff --git a/rhodecode/templates/admin/users_groups/users_groups.html b/rhodecode/templates/admin/users_groups/users_groups.html --- a/rhodecode/templates/admin/users_groups/users_groups.html +++ b/rhodecode/templates/admin/users_groups/users_groups.html @@ -22,8 +22,8 @@
  • ${h.link_to(_(u'ADD NEW USER GROUP'),h.url('new_users_group'))}
  • - - + +
    @@ -42,7 +42,7 @@ ${h.form(url('users_group', id=u_group.users_group_id),method='delete')} ${h.submit('remove_','delete',id="remove_group_%s" % u_group.users_group_id, - class_="delete_icon action_button",onclick="return confirm('Confirm to delete this users group');")} + class_="delete_icon action_button",onclick="return confirm('"+_('Confirm to delete this users group: %s') % u_group.users_group_name+"');")} ${h.end_form()} diff --git a/rhodecode/templates/base/base.html b/rhodecode/templates/base/base.html --- a/rhodecode/templates/base/base.html +++ b/rhodecode/templates/base/base.html @@ -3,73 +3,7 @@ - + -
    +
    <% messages = h.flash.pop_messages() %> % if messages: @@ -92,11 +26,11 @@ % endfor % endif -
    -
    +
    +
    ${next.main()}
    -
    +
    @@ -107,7 +41,7 @@ ${_('Submit a bug')}

    @@ -126,159 +60,143 @@ +<%def name="usermenu()"> +
    +
    + + %if c.rhodecode_user.username != 'default' and c.unread_notifications != 0: + + %endif +
    + +
    + <%def name="menu(current=None)"> - <% + <% def is_current(selected): if selected == current: return h.literal('class="current"') %> - %if current not in ['home','admin']: - ##REGULAR MENU + %if current not in ['home','admin']: + ##REGULAR MENU
    • - + ${_('Products')} - + -
    • - +
    • - + ${_('Summary')} - ${_('Summary')} - + ${_('Summary')} +
    • - ##
    • - ## - ## - ## ${_('Shortlog')} - ## - ## ${_('Shortlog')} - ## - ##
    • - + ${_('Changelog')} - ${_('Changelog')} - -
    • - + ${_('Changelog')} + + +
    • - + ${_('Switch to')} - ${_('Switch to')} - -
        -
      • - ${h.link_to('%s (%s)' % (_('branches'),len(c.rhodecode_repo.branches.values()),),h.url('branches_home',repo_name=c.repo_name),class_='branches childs')} -
          - %if c.rhodecode_repo.branches.values(): - %for cnt,branch in enumerate(c.rhodecode_repo.branches.items()): -
        • ${h.link_to('%s - %s' % (branch[0],h.short_id(branch[1])),h.url('files_home',repo_name=c.repo_name,revision=branch[1]))}
        • - %endfor - %else: -
        • ${h.link_to(_('There are no branches yet'),'#')}
        • - %endif -
        -
      • -
      • - ${h.link_to('%s (%s)' % (_('tags'),len(c.rhodecode_repo.tags.values()),),h.url('tags_home',repo_name=c.repo_name),class_='tags childs')} -
          - %if c.rhodecode_repo.tags.values(): - %for cnt,tag in enumerate(c.rhodecode_repo.tags.items()): -
        • ${h.link_to('%s - %s' % (tag[0],h.short_id(tag[1])),h.url('files_home',repo_name=c.repo_name,revision=tag[1]))}
        • - %endfor - %else: -
        • ${h.link_to(_('There are no tags yet'),'#')}
        • - %endif -
        -
      • + ${_('Switch to')} + +
      • - + ${_('Files')} - ${_('Files')} - -
      • - + ${_('Files')} + + +
      • - + ${_('Admin')} - ${_('Options')} + ${_('Options')}
          %if h.HasRepoPermissionAll('repository.admin')(c.repo_name): @@ -290,10 +208,10 @@ %endif
        • ${h.link_to(_('fork'),h.url('repo_fork_home',repo_name=c.repo_name),class_='fork')}
        • ${h.link_to(_('search'),h.url('search_repo',search_repo=c.repo_name),class_='search')}
        • - + % if h.HasPermissionAll('hg.admin')('access admin main page'):
        • - ${h.link_to(_('admin'),h.url('admin_home'),class_='admin')} + ${h.link_to(_('admin'),h.url('admin_home'),class_='admin')} <%def name="admin_menu()">
          • ${h.link_to(_('journal'),h.url('admin_home'),class_='journal')}
          • @@ -303,18 +221,18 @@
          • ${h.link_to(_('users groups'),h.url('users_groups'),class_='groups')}
          • ${h.link_to(_('permissions'),h.url('edit_permission',id='default'),class_='permissions')}
          • ${h.link_to(_('ldap'),h.url('ldap_home'),class_='ldap')}
          • -
          • ${h.link_to(_('settings'),h.url('admin_settings'),class_='settings')}
          • +
          • ${h.link_to(_('settings'),h.url('admin_settings'),class_='settings')}
          - + ${admin_menu()}
        • % endif -
        +
    • - +
    • - + ${_('Followers')} @@ -322,56 +240,99 @@
    • - + ${_('Forks')} ${c.repository_forks} -
    • - + + ${usermenu()}
    + %else: ##ROOT MENU - %endif + %endif diff --git a/rhodecode/templates/base/root.html b/rhodecode/templates/base/root.html --- a/rhodecode/templates/base/root.html +++ b/rhodecode/templates/base/root.html @@ -11,22 +11,21 @@ <%def name="css()"> - ## EXTRA FOR CSS ${self.css_extra()} <%def name="css_extra()"> - + ${self.css()} - + %if c.ga_code: %endif - + ## JAVASCRIPT ## <%def name="js()"> @@ -45,17 +44,21 @@ ## EXTRA FOR JS ${self.js_extra()} - + - - <%def name="js_extra()"> - + <%def name="js_extra()"> ${self.js()} ${next.body()} - \ No newline at end of file + diff --git a/rhodecode/templates/bookmarks/bookmarks.html b/rhodecode/templates/bookmarks/bookmarks.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/bookmarks/bookmarks.html @@ -0,0 +1,78 @@ +## -*- coding: utf-8 -*- +<%inherit file="/base/base.html"/> + +<%def name="title()"> + ${c.repo_name} ${_('Bookmarks')} - ${c.rhodecode_name} + + + +<%def name="breadcrumbs_links()"> + + ${h.link_to(u'Home',h.url('/'))} + » + ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))} + » + ${_('bookmarks')} + + +<%def name="page_nav()"> + ${self.menu('bookmarks')} + +<%def name="main()"> +
    + +
    + ${self.breadcrumbs()} +
    + +
    + <%include file='bookmarks_data.html'/> +
    +
    + + + + diff --git a/rhodecode/templates/bookmarks/bookmarks_data.html b/rhodecode/templates/bookmarks/bookmarks_data.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/bookmarks/bookmarks_data.html @@ -0,0 +1,33 @@ +%if c.repo_bookmarks: +
    + + + + + + + + + + %for cnt,book in enumerate(c.repo_bookmarks.items()): + + + + + + + %endfor +
    ${_('Name')}${_('Date')}${_('Author')}${_('Revision')}
    + + ${h.link_to(book[0], + h.url('files_home',repo_name=c.repo_name,revision=book[1].raw_id))} + + ${book[1].date}${h.person(book[1].author)} + +
    +
    +%else: + ${_('There are no bookmarks yet')} +%endif diff --git a/rhodecode/templates/branches/branches.html b/rhodecode/templates/branches/branches.html --- a/rhodecode/templates/branches/branches.html +++ b/rhodecode/templates/branches/branches.html @@ -6,15 +6,16 @@ <%def name="breadcrumbs_links()"> + ${h.link_to(u'Home',h.url('/'))} - » + » ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))} » ${_('branches')} <%def name="page_nav()"> - ${self.menu('branches')} + ${self.menu('branches')} <%def name="main()"> @@ -27,5 +28,50 @@
    <%include file='branches_data.html'/>
    - - \ No newline at end of file + + + + diff --git a/rhodecode/templates/branches/branches_data.html b/rhodecode/templates/branches/branches_data.html --- a/rhodecode/templates/branches/branches_data.html +++ b/rhodecode/templates/branches/branches_data.html @@ -1,53 +1,52 @@ -% if c.repo_branches: - +%if c.repo_branches: +
    +
    + + - - + %for cnt,branch in enumerate(c.repo_branches.items()): - + h.url('files_home',repo_name=c.repo_name,revision=branch[1].raw_id))} + + + - - - + + %endfor % if hasattr(c,'repo_closed_branches') and c.repo_closed_branches: %for cnt,branch in enumerate(c.repo_closed_branches.items()): - + + + - - - + %endfor - %endif + %endif
    ${_('name')} ${_('date')}${_('name')} ${_('author')} ${_('revision')}${_('links')}
    ${branch[1].date} - ${h.link_to(branch[0], - h.url('changeset_home',repo_name=c.repo_name,revision=branch[1].raw_id))} - - ${branch[1].date} ${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].raw_id))} - | - ${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=branch[1].raw_id))} -
    + +
    ${branch[1].date} - ${h.link_to(branch[0]+' [closed]', h.url('changeset_home',repo_name=c.repo_name,revision=branch[1].raw_id))} - - ${branch[1].date} ${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].raw_id))} - | - ${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=branch[1].raw_id))} + +
    + %else: ${_('There are no branches yet')} -%endif \ No newline at end of file +%endif diff --git a/rhodecode/templates/changelog/changelog.html b/rhodecode/templates/changelog/changelog.html --- a/rhodecode/templates/changelog/changelog.html +++ b/rhodecode/templates/changelog/changelog.html @@ -11,11 +11,11 @@ » ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))} » - ${_('Changelog')} - ${_('showing ')} ${c.size if c.size <= c.total_cs else c.total_cs} ${_('out of')} ${c.total_cs} ${_('revisions')} + ${_('Changelog')} - ${_('showing ')} ${c.size if c.size <= c.total_cs else c.total_cs} ${_('out of')} ${c.total_cs} ${_('revisions')} <%def name="page_nav()"> - ${self.menu('changelog')} + ${self.menu('changelog')} <%def name="main()"> @@ -33,52 +33,65 @@
    ${h.form(h.url.current(),method='get')} -
    - ${h.submit('set',_('Show'),class_="ui-button-small")} +
    + ${h.submit('set',_('Show'),class_="ui-btn")} ${h.text('size',size=1,value=c.size)} - ${_('revisions')} + ${_('revisions')}
    ${h.end_form()} +
    ${h.select('branch_filter',c.branch_name,c.branch_filters)}
    - + %for cnt,cs in enumerate(c.pagination): -
    +
    -
    +
    ${h.checkbox(cs.short_id,class_="changeset_range")} - ${_('commit')} ${cs.revision}: ${h.short_id(cs.raw_id)}@${cs.date} + ${cs.revision}:${h.short_id(cs.raw_id)}
    gravatar
    -
    ${h.person(cs.author)}
    - ##${h.email_or_none(cs.author)}
    +
    ${h.person(cs.author)}
    -
    ${h.link_to(h.wrap_paragraphs(cs.message),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
    -
    +
    ${cs.date}
    +
    +
    +
    ${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
    +
    ↓ ${_('show more')} ↓
    +
    - ${len(cs.affected_files)} -
    - %if len(cs.parents)>1: -
    - ${_('merge')}merge -
    - %endif - %if cs.parents: +
    ${len(cs.affected_files)}
    +
    + %if len(c.comments.get(cs.raw_id,[])) > 0: + + %endif +
    +
    + %if cs.parents: %for p_cs in reversed(cs.parents): -
    ${_('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)} +
    ${_('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: -
    ${_('No parents')}
    - %endif - + %else: +
    ${_('No parents')}
    + %endif + - %if cs.branch: + %if len(cs.parents)>1: + ${_('merge')} + %endif + %if h.is_hg(c.rhodecode_repo) and cs.branch: ${h.link_to(cs.branch,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))} %endif @@ -86,26 +99,26 @@ ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))} %endfor - -
    + +
    - + %endfor
    ${c.pagination.pager('$link_previous ~2~ $link_next')} -
    +
    - + %else: ${_('There are no changes yet')} - %endif + %endif - - \ No newline at end of file + + diff --git a/rhodecode/templates/changelog/changelog_details.html b/rhodecode/templates/changelog/changelog_details.html --- a/rhodecode/templates/changelog/changelog_details.html +++ b/rhodecode/templates/changelog/changelog_details.html @@ -1,9 +1,9 @@ -% if len(c.cs.affected_files) <= c.affected_files_cut_off: +% if len(c.cs.affected_files) <= c.affected_files_cut_off: ${len(c.cs.removed)} ${len(c.cs.changed)} ${len(c.cs.added)} % else: ! ! - ! -% endif \ No newline at end of file + ! +% endif diff --git a/rhodecode/templates/changeset/changeset.html b/rhodecode/templates/changeset/changeset.html --- a/rhodecode/templates/changeset/changeset.html +++ b/rhodecode/templates/changeset/changeset.html @@ -11,11 +11,11 @@ » ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))} » - ${_('Changeset')} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} + ${_('Changeset')} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} <%def name="page_nav()"> - ${self.menu('changelog')} + ${self.menu('changelog')} <%def name="main()"> @@ -27,19 +27,24 @@
    -
    - ${_('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.raw_id,diff='show'))} - » ${h.link_to(_('download diff'), - h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='download'))} -
    +
    + r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} +
    +
    + ${c.changeset.date} +
    +
    + + + ${c.ignorews_url()} + ${c.context_url()} +
    +
    ${len(c.comments)} comment(s) (${c.inline_cnt} ${_('inline')})
    -
    ${_('commit')} ${c.changeset.revision}: ${h.short_id(c.changeset.raw_id)}@${c.changeset.date}
    gravatar @@ -47,93 +52,141 @@ ${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.raw_id))}
    +
    ${h.urlify_commit(h.wrap_paragraphs(c.changeset.message),c.repo_name)}
    - % if len(c.changeset.affected_files) <= c.affected_files_cut_off: + % if len(c.changeset.affected_files) <= c.affected_files_cut_off: ${len(c.changeset.removed)} ${len(c.changeset.changed)} ${len(c.changeset.added)} % else: ! ! - ! - % endif -
    - %if len(c.changeset.parents)>1: -
    - ${_('merge')}merge -
    - %endif - + ! + % endif +
    + %if c.changeset.parents: %for p_cs in reversed(c.changeset.parents): -
    ${_('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)} +
    ${_('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: -
    ${_('No parents')}
    - %endif + %else: +
    ${_('No parents')}
    + %endif + %if len(c.changeset.parents)>1: + ${_('merge')} + %endif ${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.raw_id))} %endfor - -
    + +
    - - ${_('%s files affected with %s additions and %s deletions.') % (len(c.changeset.affected_files),c.lines_added,c.lines_deleted)} + + ${_('%s files affected with %s additions and %s deletions:') % (len(c.changeset.affected_files),c.lines_added,c.lines_deleted)}
    %for change,filenode,diff,cs1,cs2,stat in c.changes:
    -
    ${h.link_to(h.safe_unicode(filenode.path), - h.url.current(anchor=h.repo_name_slug('C%s' % h.safe_unicode(filenode.path))))}
    +
    + %if change != 'removed': + ${h.link_to(h.safe_unicode(filenode.path),c.anchor_url(filenode.changeset.raw_id,filenode.path)+"_target")} + %else: + ${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID('',filenode.path)))} + %endif +
    ${h.fancy_file_stats(stat)}
    %endfor % if c.cut_off: ${_('Changeset was too big and was cut off...')} % endif -
    +
    - + - - %for change,filenode,diff,cs1,cs2,stat in c.changes: - %if change !='removed': -
    -
    -
    -
    - - ${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name, - revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))} - - %if 1: - » ${h.link_to(_('diff'), - h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='diff'))} - » ${h.link_to(_('raw diff'), - h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='raw'))} - » ${h.link_to(_('download diff'), - h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='download'))} - %endif -
    -
    -
    - %if diff: - ${diff|n} - %else: - ${_('No changes in this file')} - %endif -
    -
    - %endif - %endfor - + + ## diff block + <%namespace name="diff_block" file="/changeset/diff_block.html"/> + ${diff_block.diff_block(c.changes)} + + ## template for inline comment form + <%namespace name="comment" file="/changeset/changeset_file_comment.html"/> + ${comment.comment_inline_form(c.changeset)} + + ${comment.comments(c.changeset)} + + + + diff --git a/rhodecode/templates/changeset/changeset_file_comment.html b/rhodecode/templates/changeset/changeset_file_comment.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/changeset/changeset_file_comment.html @@ -0,0 +1,105 @@ +## -*- coding: utf-8 -*- +## usage: +## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/> +## ${comment.comment_block(co)} +## +<%def name="comment_block(co)"> +
    +
    +
    + + + ${co.author.username} + + + ${h.age(co.modified_at)} + + %if h.HasPermissionAny('hg.admin', 'repository.admin')() or co.author.user_id == c.rhodecode_user.user_id: + + ${_('Delete')} + + %endif +
    +
    + ${h.rst_w_mentions(co.text)|n} +
    +
    +
    + + + +<%def name="comment_inline_form(changeset)"> + + + + +<%def name="comments(changeset)"> + +
    +
    ${len(c.comments)} comment(s) (${c.inline_cnt} ${_('inline')})
    + + %for path, lines in c.inline_comments: + + %endfor + + %for co in c.comments: + ${comment_block(co)} + %endfor + %if c.rhodecode_user.username != 'default': +
    + ${h.form(h.url('changeset_comment', repo_name=c.repo_name, revision=changeset.raw_id))} + ${_('Leave a comment')} +
    +
    + ${_('Comments parsed using')} RST ${_('syntax')} + ${_('with')} @mention ${_('support')} +
    + ${h.textarea('text')} +
    +
    + ${h.submit('save', _('Comment'), class_='ui-button')} +
    + ${h.end_form()} +
    + %endif +
    + diff --git a/rhodecode/templates/changeset/changeset_range.html b/rhodecode/templates/changeset/changeset_range.html --- a/rhodecode/templates/changeset/changeset_range.html +++ b/rhodecode/templates/changeset/changeset_range.html @@ -1,3 +1,4 @@ +## -*- coding: utf-8 -*- <%inherit file="/base/base.html"/> <%def name="title()"> @@ -13,7 +14,7 @@ <%def name="page_nav()"> - ${self.menu('changelog')} + ${self.menu('changelog')} <%def name="main()"> @@ -24,27 +25,23 @@
    -
    -
    +
    +

    ${_('Compare View')}

    +
    ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)} -

    ${_('Compare View')}

    - ##» ${h.link_to(_('raw diff'), - ##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.raw_id,diff='download'))}
    - +
    %for cs in c.cs_ranges: - + %endfor
    gravatar
    ${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
    ${h.person(cs.author)}
    ${cs.date}
    ${h.link_to(h.wrap_paragraphs(cs.message),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
    ${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name)}
    @@ -54,44 +51,39 @@ %for cs in c.cs_ranges:
    r${cs}
    %for change,filenode,diff,cs1,cs2,st in c.changes[cs.raw_id]: -
    ${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.repo_name_slug('C%s-%s' % (cs.short_id,h.safe_unicode(filenode.path)))))}
    +
    ${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID(cs.raw_id,filenode.path)))}
    %endfor - %endfor -
    + %endfor +
    - +
    - %for cs in c.cs_ranges: - %for change,filenode,diff,cs1,cs2,st in c.changes[cs.raw_id]: - %if change !='removed': -
    -
    -
    -
    - - ${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name, - revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))} - - %if 1: - » ${h.link_to(_('diff'), - h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='diff'))} - » ${h.link_to(_('raw diff'), - h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='raw'))} - » ${h.link_to(_('download diff'), - h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='download'))} - %endif -
    -
    -
    - %if diff: - ${diff|n} - %else: - ${_('No changes in this file')} - %endif -
    -
    - %endif - %endfor - %endfor + <%namespace name="comment" file="/changeset/changeset_file_comment.html"/> + <%namespace name="diff_block" file="/changeset/diff_block.html"/> + %for cs in c.cs_ranges: + ##${comment.comment_inline_form(cs)} + ## diff block +

    ${'r%s:%s' % (cs.revision,h.short_id(cs.raw_id))}

    + ${diff_block.diff_block(c.changes[cs.raw_id])} + ##${comment.comments(cs)} + + %endfor +
    - \ No newline at end of file + diff --git a/rhodecode/templates/changeset/diff_block.html b/rhodecode/templates/changeset/diff_block.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/changeset/diff_block.html @@ -0,0 +1,41 @@ +## -*- coding: utf-8 -*- +##usage: +## <%namespace name="diff_block" file="/changeset/diff_block.html"/> +## ${diff_block.diff_block(changes)} +## +<%def name="diff_block(changes)"> + +%for change,filenode,diff,cs1,cs2,stat in changes: + %if change !='removed': +
    +
    +
    +
    +
    + ${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name, + revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))} +
    +
    + + + + ${c.ignorews_url(h.FID(filenode.changeset.raw_id,filenode.path))} + ${c.context_url(h.FID(filenode.changeset.raw_id,filenode.path))} +
    + + + +
    +
    +
    +
    + ${diff|n} +
    +
    + %endif +%endfor + + diff --git a/rhodecode/templates/changeset/raw_changeset.html b/rhodecode/templates/changeset/raw_changeset.html --- a/rhodecode/templates/changeset/raw_changeset.html +++ b/rhodecode/templates/changeset/raw_changeset.html @@ -5,4 +5,4 @@ ${c.parent_tmpl} ${c.changeset.message} -${c.diffs|n} \ No newline at end of file +${c.diffs|n} diff --git a/rhodecode/templates/email_templates/changeset_comment.html b/rhodecode/templates/email_templates/changeset_comment.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/email_templates/changeset_comment.html @@ -0,0 +1,6 @@ +## -*- coding: utf-8 -*- +<%inherit file="main.html"/> + +

    ${subject}

    + +${body} diff --git a/rhodecode/templates/email_templates/default.html b/rhodecode/templates/email_templates/default.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/email_templates/default.html @@ -0,0 +1,4 @@ +## -*- coding: utf-8 -*- +<%inherit file="main.html"/> + +${body} diff --git a/rhodecode/templates/email_templates/main.html b/rhodecode/templates/email_templates/main.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/email_templates/main.html @@ -0,0 +1,9 @@ +${self.body()} + + +
    +-- +
    +
    +${_('This is an notification from RhodeCode.')} +
    diff --git a/rhodecode/templates/email_templates/password_reset.html b/rhodecode/templates/email_templates/password_reset.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/email_templates/password_reset.html @@ -0,0 +1,12 @@ +## -*- coding: utf-8 -*- +<%inherit file="main.html"/> + +Hello ${user} + +We received a request to create a new password for your account. + +You can generate it by clicking following URL: + +${reset_url} + +If you didn't request new password please ignore this email. diff --git a/rhodecode/templates/email_templates/registration.html b/rhodecode/templates/email_templates/registration.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/email_templates/registration.html @@ -0,0 +1,9 @@ +## -*- coding: utf-8 -*- +<%inherit file="main.html"/> + +A new user have registered in RhodeCode + +${body} + + +View this user here: ${registered_user_url} diff --git a/rhodecode/templates/errors/error_document.html b/rhodecode/templates/errors/error_document.html --- a/rhodecode/templates/errors/error_document.html +++ b/rhodecode/templates/errors/error_document.html @@ -6,10 +6,10 @@ %if c.redirect_time: - %endif + %endif - + - +
    -
    +
    ${c.rhodecode_name}

    ${c.error_message}

    - +

    ${c.error_explanation}

    - + %if c.redirect_time:

    ${_('You will be redirected to %s in %s seconds') % (c.redirect_module,c.redirect_time)}

    - %endif - + %endif +
    - + - diff --git a/rhodecode/templates/files/file_diff.html b/rhodecode/templates/files/file_diff.html --- a/rhodecode/templates/files/file_diff.html +++ b/rhodecode/templates/files/file_diff.html @@ -13,7 +13,7 @@ <%def name="page_nav()"> - ${self.menu('files')} + ${self.menu('files')} <%def name="main()">
    @@ -21,33 +21,27 @@
    ${self.breadcrumbs()}
    -
    -
    -
    -
    - ${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.changeset_2.raw_id,diff1=c.changeset_1.raw_id,diff='diff'))} - » ${h.link_to(_('raw diff'), - 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.changeset_2.raw_id,diff1=c.changeset_1.raw_id,diff='download'))} -
    -
    -
    - %if c.no_changes: - ${_('No changes')} - %elif c.big_diff: - ${_('Diff is to big to display')} ${h.link_to(_('raw diff'), - h.url.current(diff2=c.changeset_2.raw_id,diff1=c.changeset_1.raw_id,diff='raw'))} - %else: - ${c.cur_diff|n} - %endif -
    -
    +
    + ## diff block + <%namespace name="diff_block" file="/changeset/diff_block.html"/> + ${diff_block.diff_block(c.changes)}
    -
    - +
    + + diff --git a/rhodecode/templates/files/files.html b/rhodecode/templates/files/files.html --- a/rhodecode/templates/files/files.html +++ b/rhodecode/templates/files/files.html @@ -10,13 +10,13 @@ ${h.link_to(c.repo_name,h.url('files_home',repo_name=c.repo_name))} » ${_('files')} - %if c.files_list: + %if c.file: @ r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} - %endif + %endif <%def name="page_nav()"> - ${self.menu('files')} + ${self.menu('files')} <%def name="main()"> @@ -27,29 +27,22 @@ + +
    - %if c.files_list: -

    - ${_('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: - <%include file='files_source.html'/> - %endif - %else: -

    - ${_('Go back')} - ${_('No files at given path')}: "${c.f_path or "/"}" -

    - %endif - -
    + <%include file='files_ypjax.html'/> +
    - - - \ No newline at end of file + + + diff --git a/rhodecode/templates/files/files_add.html b/rhodecode/templates/files/files_add.html --- a/rhodecode/templates/files/files_add.html +++ b/rhodecode/templates/files/files_add.html @@ -20,7 +20,7 @@ <%def name="page_nav()"> - ${self.menu('files')} + ${self.menu('files')} <%def name="main()">
    @@ -31,8 +31,8 @@
  • ${_('branch')}: ${c.cs.branch} -
  • - + +
    @@ -46,16 +46,16 @@
    - + ${_('or')} ${_('Upload file')}
    -
    +
    @@ -66,45 +66,27 @@ ${_('use / to separate directories')}
    - + - +
    -
    +
    
     				    
                     
    ${_('commit message')}
    -
    - - ${h.submit('commit',_('Commit changes'),class_="ui-button-small-blue")} +
    + ${h.submit('commit',_('Commit changes'),class_="ui-btn")} + ${h.reset('reset',_('Reset'),class_="ui-btn")}
    ${h.end_form()} -
    +
    - - \ No newline at end of file + + diff --git a/rhodecode/templates/files/files_annotate.html b/rhodecode/templates/files/files_annotate.html --- a/rhodecode/templates/files/files_annotate.html +++ b/rhodecode/templates/files/files_annotate.html @@ -13,7 +13,7 @@ <%def name="page_nav()"> - ${self.menu('files')} + ${self.menu('files')} <%def name="main()">
    @@ -23,55 +23,55 @@ + +

    ${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.cs.revision,c.file.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.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.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.cs.raw_id,f_path=c.f_path))} - % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name): - % if not c.file.is_binary: - / ${h.link_to(_('edit'), - h.url('files_edit_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path))} - % endif - % endif -
    -
    ${_('History')}
    +
    +
    ${_('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-small")} - ${h.submit('show_rev','show at revision',class_="ui-button-small")} + ${h.submit('diff','diff to revision',class_="ui-btn")} + ${h.submit('show_rev','show at revision',class_="ui-btn")} ${h.end_form()}
    -
    +
    -
    -
    ${c.file.name}@r${c.file.last_changeset.revision}:${h.short_id(c.file.last_changeset.raw_id)}
    -
    "${c.file.message}"
    -
    +
    +
    +
    +
    ${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))}
    +
    ${h.format_byte_size(c.file.size,binary=True)}
    +
    ${c.file.mimetype}
    +
    + ${h.link_to(_('show source'),h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")} + ${h.link_to(_('show as raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")} + ${h.link_to(_('download as raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")} + % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name): + % if not c.file.is_binary: + ${h.link_to(_('edit'),h.url('files_edit_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")} + % endif + % endif +
    +
    +
    +
    + gravatar +
    +
    ${h.person(c.cs.author)}
    +
    +
    ${c.file.last_changeset.message}
    +
    %if c.file.is_binary: ${_('Binary file (%s)') % c.file.mimetype} - %else: + %else: % if c.file.size < c.cut_off_limit: ${h.pygmentize_annotation(c.repo_name,c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")} %else: @@ -81,13 +81,13 @@ - %endif + + %endif
    -
    + +
    - - + + diff --git a/rhodecode/templates/files/files_browser.html b/rhodecode/templates/files/files_browser.html --- a/rhodecode/templates/files/files_browser.html +++ b/rhodecode/templates/files/files_browser.html @@ -10,12 +10,12 @@
    ${h.form(h.url.current())}
    - ${_('view')}@rev - « + ${_('view')}@rev + « ${h.text('at_rev',value=c.changeset.revision,size=5)} - » - ## ${h.submit('view',_('view'),class_="ui-button-small")} -
    + » + ## ${h.submit('view',_('view'),class_="ui-btn")} +
    ${h.end_form()}
    @@ -24,136 +24,22 @@
    - -
    @@ -166,12 +52,12 @@ - + - %if c.files_list.parent: + %if c.file.parent: - @@ -180,16 +66,16 @@ %endif - - %for cnt,node in enumerate(c.files_list): + + %for cnt,node in enumerate(c.file): %endfor - +
    ${_('Last commiter')}
    - ${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('..',h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.file.parent.path),class_="browser-dir ypjax-link")}
    - ${h.link_to(node.name,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=h.safe_unicode(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=h.safe_unicode(node.path)),class_=file_class(node)+" ypjax-link")} %if node.is_file(): ${h.format_byte_size(node.size,binary=True)} - %endif + %endif %if node.is_file(): @@ -198,8 +84,9 @@ %if node.is_file(): - - ${'r%s:%s' % (node.last_changeset.revision,node.last_changeset.short_id)} +
    +
    ${'r%s:%s' % (node.last_changeset.revision,node.last_changeset.short_id)}
    +
    %endif
    @@ -210,14 +97,16 @@ %if node.is_file(): - ${node.last_changeset.author} - %endif + + ${h.person(node.last_changeset.author)} + + %endif
    - \ No newline at end of file + diff --git a/rhodecode/templates/files/files_edit.html b/rhodecode/templates/files/files_edit.html --- a/rhodecode/templates/files/files_edit.html +++ b/rhodecode/templates/files/files_edit.html @@ -20,7 +20,7 @@ <%def name="page_nav()"> - ${self.menu('files')} + ${self.menu('files')} <%def name="main()">
    @@ -31,34 +31,48 @@
  • ${_('branch')}: ${c.cs.branch} -
  • - + +

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

    ${h.form(h.url.current(),method='post',id='eform')}
    +
    +
    +
    +
    ${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))}
    +
    ${h.format_byte_size(c.file.size,binary=True)}
    +
    ${c.file.mimetype}
    +
    + ${h.link_to(_('show annotation'),h.url('files_annotate_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")} + ${h.link_to(_('show as raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")} + ${h.link_to(_('download as raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")} + % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name): + % if not c.file.is_binary: + ${h.link_to(_('source'),h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")} + % endif + % endif +
    +
    +
    ${_('Editing file')}: ${c.file.path}
    +
    
    -				
    +				
     				
    ${_('commit message')}
    -
    - - ${h.submit('commit',_('Commit changes'),class_="ui-button-small-blue")} +
    + ${h.submit('commit',_('Commit changes'),class_="ui-btn")} + ${h.reset('reset',_('Reset'),class_="ui-btn")}
    ${h.end_form()} -
    +
    - - \ No newline at end of file + + diff --git a/rhodecode/templates/files/files_source.html b/rhodecode/templates/files/files_source.html --- a/rhodecode/templates/files/files_source.html +++ b/rhodecode/templates/files/files_source.html @@ -1,62 +1,59 @@
    -
    ${_('Revision')}
    -
    - ${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)}
    -
    ${_('Mimetype')}
    -
    ${c.files_list.mimetype}
    -
    ${_('Options')}
    -
    ${h.link_to(_('show annotation'), - 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.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.changeset.raw_id,f_path=c.f_path))} - % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name): - % if not c.files_list.is_binary: - / ${h.link_to(_('edit'), - h.url('files_edit_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))} - % endif - % endif -
    -
    ${_('History')}
    +
    ${_('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.raw_id)} - ${h.select('diff1',c.files_list.last_changeset.raw_id,c.file_history)} - ${h.submit('diff','diff to revision',class_="ui-button-small")} - ${h.submit('show_rev','show at revision',class_="ui-button-small")} + ${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-btn")} + ${h.submit('show_rev','show at revision',class_="ui-btn")} ${h.end_form()}
    -
    + -
    -
    ${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}"
    +
    +
    +
    ${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))}
    +
    ${h.format_byte_size(c.file.size,binary=True)}
    +
    ${c.file.mimetype}
    +
    + ${h.link_to(_('show annotation'),h.url('files_annotate_home',repo_name=c.repo_name,revision=c.file.last_changeset.raw_id,f_path=c.f_path),class_="ui-btn")} + ${h.link_to(_('show as raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.file.last_changeset.raw_id,f_path=c.f_path),class_="ui-btn")} + ${h.link_to(_('download as raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.file.last_changeset.raw_id,f_path=c.f_path),class_="ui-btn")} + % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name): + % if not c.file.is_binary: + ${h.link_to(_('edit'),h.url('files_edit_home',repo_name=c.repo_name,revision=c.file.last_changeset.raw_id,f_path=c.f_path),class_="ui-btn")} + % endif + % endif +
    +
    +
    +
    + gravatar +
    +
    ${h.person(c.file.last_changeset.author)}
    +
    +
    ${h.urlify_commit(c.file.last_changeset.message,c.repo_name)}
    - %if c.files_list.is_binary: - ${_('Binary file (%s)') % c.files_list.mimetype} + %if c.file.is_binary: + ${_('Binary file (%s)') % c.file.mimetype} %else: - % if c.files_list.size < c.cut_off_limit: - ${h.pygmentize(c.files_list,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")} + % if c.file.size < c.cut_off_limit: + ${h.pygmentize(c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")} %else: ${_('File is too big to display')} ${h.link_to(_('show as raw'), - h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))} + h.url('files_raw_home',repo_name=c.repo_name,revision=c.file.last_changeset.raw_id,f_path=c.f_path))} %endif - %endif + %endif
    @@ -98,77 +95,10 @@ YUE.onDOMReady(function(){ YUE.on('show_rev','click',function(e){ YUE.preventDefault(e); - var cs = YAHOO.util.Dom.get('diff1').value; + var cs = YUD.get('diff1').value; var url = "${h.url('files_home',repo_name=c.repo_name,revision='__CS__',f_path=c.f_path)}".replace('__CS__',cs); window.location = url; }); - - function getIdentNode(n){ - //iterate thru nodes untill matched interesting node ! - - if (typeof n == 'undefined'){ - return -1 - } - - if(typeof n.id != "undefined" && n.id.match('L[0-9]+')){ - return n - } - else{ - return getIdentNode(n.parentNode); - } - } - - function getSelectionLink() { - //get selection from start/to nodes - if (typeof window.getSelection != "undefined") { - s = window.getSelection(); - - from = getIdentNode(s.anchorNode); - till = getIdentNode(s.focusNode); - - f_int = parseInt(from.id.replace('L','')); - t_int = parseInt(till.id.replace('L','')); - - if (f_int > t_int){ - //highlight from bottom - offset = -35; - ranges = [t_int,f_int]; - - } - else{ - //highligth from top - offset = 35; - ranges = [f_int,t_int]; - } - - if (ranges[0] != ranges[1]){ - if(YUD.get('linktt') == null){ - hl_div = document.createElement('div'); - hl_div.id = 'linktt'; - } - anchor = '#L'+ranges[0]+'-'+ranges[1]; - hl_div.innerHTML = ''; - l = document.createElement('a'); - l.href = location.href.substring(0,location.href.indexOf('#'))+anchor; - l.innerHTML = "${_('Selection link')}" - hl_div.appendChild(l); - - YUD.get('body').appendChild(hl_div); - - xy = YUD.getXY(till.id); - - YUD.addClass('linktt','yui-tt'); - YUD.setStyle('linktt','top',xy[1]+offset+'px'); - YUD.setStyle('linktt','left',xy[0]+'px'); - YUD.setStyle('linktt','visibility','visible'); - } - else{ - YUD.setStyle('linktt','visibility','hidden'); - } - } - } - - YUE.on('hlcode','mouseup',getSelectionLink) - + YUE.on('hlcode','mouseup',getSelectionLink("${_('Selection link')}")) }); diff --git a/rhodecode/templates/files/files_ypjax.html b/rhodecode/templates/files/files_ypjax.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/files/files_ypjax.html @@ -0,0 +1,15 @@ +%if c.file: +

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

    + %if c.file.is_dir(): + <%include file='files_browser.html'/> + %else: + <%include file='files_source.html'/> + %endif +%else: +

    + ${_('Go back')} + ${_('No files at given path')}: "${c.f_path or "/"}" +

    +%endif diff --git a/rhodecode/templates/followers/followers.html b/rhodecode/templates/followers/followers.html --- a/rhodecode/templates/followers/followers.html +++ b/rhodecode/templates/followers/followers.html @@ -7,7 +7,7 @@ <%def name="breadcrumbs_links()"> ${h.link_to(u'Home',h.url('/'))} - » + » ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))} » ${_('followers')} @@ -26,7 +26,7 @@
    ${c.followers_data} -
    +
    - - \ No newline at end of file + + diff --git a/rhodecode/templates/followers/followers_data.html b/rhodecode/templates/followers/followers_data.html --- a/rhodecode/templates/followers/followers_data.html +++ b/rhodecode/templates/followers/followers_data.html @@ -9,11 +9,11 @@ ${f.user.username} (${f.user.name} ${f.user.lastname})
    -
    ${_('Started following')} - +
    ${_('Started following')} - ${h.age(f.follows_from)}
    -
    -% endfor + +% endfor
    ${c.followers_pager.pager('$link_previous ~2~ $link_next')} -
    \ No newline at end of file + diff --git a/rhodecode/templates/settings/repo_fork.html b/rhodecode/templates/forks/fork.html rename from rhodecode/templates/settings/repo_fork.html rename to rhodecode/templates/forks/fork.html --- a/rhodecode/templates/settings/repo_fork.html +++ b/rhodecode/templates/forks/fork.html @@ -8,7 +8,7 @@ <%def name="breadcrumbs_links()"> ${h.link_to(u'Home',h.url('/'))} » - ${h.link_to(c.repo_info.repo_name,h.url('summary_home',repo_name=c.repo_info.repo_name))} + ${h.link_to(c.repo_info.repo_name,h.url('summary_home',repo_name=c.repo_info.repo_name))} » ${_('fork')} @@ -20,21 +20,30 @@
    - ${self.breadcrumbs()} + ${self.breadcrumbs()}
    ${h.form(url('repo_fork_create_home',repo_name=c.repo_info.repo_name))}
    -
    - -
    -
    - ${h.text('fork_name',class_="small")} - ${h.hidden('repo_type',c.repo_info.repo_type)} -
    -
    +
    + +
    +
    + ${h.text('repo_name',class_="small")} + ${h.hidden('repo_type',c.repo_info.repo_type)} + ${h.hidden('fork_parent_id',c.repo_info.repo_id)} +
    +
    +
    +
    + +
    +
    + ${h.select('repo_group','',c.repo_groups,class_="medium")} +
    +
    @@ -50,12 +59,28 @@
    ${h.checkbox('private',value="True")}
    +
    +
    +
    + +
    +
    + ${h.checkbox('copy_permissions',value="True")} +
    +
    +
    +
    + +
    +
    + ${h.checkbox('update_after_clone',value="True")} +
    ${h.submit('',_('fork this repository'),class_="ui-button")} -
    +
    -
    - ${h.end_form()} + + ${h.end_form()} - + diff --git a/rhodecode/templates/forks/forks.html b/rhodecode/templates/forks/forks.html --- a/rhodecode/templates/forks/forks.html +++ b/rhodecode/templates/forks/forks.html @@ -7,7 +7,7 @@ <%def name="breadcrumbs_links()"> ${h.link_to(u'Home',h.url('/'))} - » + » ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))} » ${_('forks')} @@ -26,7 +26,7 @@
    ${c.forks_data} -
    +
    - - \ No newline at end of file + + diff --git a/rhodecode/templates/forks/forks_data.html b/rhodecode/templates/forks/forks_data.html --- a/rhodecode/templates/forks/forks_data.html +++ b/rhodecode/templates/forks/forks_data.html @@ -8,15 +8,15 @@ gravatar - ${f.user.username} (${f.user.name} ${f.user.lastname}) / + ${f.user.username} (${f.user.name} ${f.user.lastname}) / ${h.link_to(f.repo_name,h.url('summary_home',repo_name=f.repo_name))}
    ${f.description}
    -
    ${_('forked')} - +
    ${_('forked')} - ${h.age(f.created_on)}
    -
    +
    % endfor
    @@ -29,7 +29,7 @@ }); ${c.forks_pager.pager('$link_previous ~2~ $link_next')} -
    + % else: - ${_('There are no forks yet')} -% endif \ No newline at end of file + ${_('There are no forks yet')} +% endif diff --git a/rhodecode/templates/index.html b/rhodecode/templates/index.html --- a/rhodecode/templates/index.html +++ b/rhodecode/templates/index.html @@ -1,14 +1,8 @@ ## -*- coding: utf-8 -*- <%inherit file="base/base.html"/> -<%def name="title()"> - ${_('Dashboard')} - ${c.rhodecode_name} - -<%def name="breadcrumbs()"> - ${c.rhodecode_name} - -<%def name="page_nav()"> - ${self.menu('home')} - +<%def name="title()">${_('Dashboard')} - ${c.rhodecode_name} +<%def name="breadcrumbs()"> +<%def name="page_nav()">${self.menu('home')} <%def name="main()"> <%include file="index_base.html" args="parent=self"/> - + diff --git a/rhodecode/templates/index_base.html b/rhodecode/templates/index_base.html --- a/rhodecode/templates/index_base.html +++ b/rhodecode/templates/index_base.html @@ -1,54 +1,60 @@ -<%page args="parent" /> +<%page args="parent" />
    - - ${parent.breadcrumbs()} ${_('repositories')} + ${parent.breadcrumbs()} 0 ${_('repositories')}
    %if c.rhodecode_user.username != 'default': %if h.HasPermissionAny('hg.admin','hg.create.repository')(): + ${h.link_to(_('ADD REPOSITORY'),h.url('admin_settings_create_repository'))} + + %endif %endif
    % if c.groups: - - +
    +
    + + + + + ## + + + + ## REPO GROUPS + % for gr in c.groups: - - - ## + + + ## this is commented out since for multi nested repos can be HEAVY! + ## in number of executed queries during traversing uncomment at will + ## - - - ## REPO GROUPS - - % for gr in c.groups: - - - - ## - - % endfor - -
    ${_('Group name')}${_('Description')}${_('Number of repositories')}
    ${_('Group name')}${_('Description')}${_('Number of repositories')} +
    + ${_('Repositories group')} + ${h.link_to(gr.name,url('repos_group_home',group_name=gr.group_name))} +
    +
    ${gr.group_description}${gr.repositories_recursive_count}
    -
    - ${_('Repositories group')} - ${h.link_to(gr.name,url('repos_group_home',group_name=gr.group_name))} -
    -
    ${gr.group_description}${gr.repositories.count()}
    + % endfor + + +
    % endif +
    + <%cnt=0%> + <%namespace name="dt" file="/_data_table/_dt_elements.html"/> + @@ -63,87 +69,37 @@ - %for cnt,repo in enumerate(c.repos_list): + %for cnt,repo in enumerate(c.repos_list,1): + ##QUICK MENU - ##DESCRIPTION - ##LAST CHANGE - + ##LAST CHANGE DATE + ##LAST REVISION + + ## + %endif: +
    - + ${dt.quick_menu(repo['name'])} - ## TYPE OF REPO -
    - %if repo['dbrepo']['repo_type'] =='hg': - ${_('Mercurial repository')} - %elif repo['dbrepo']['repo_type'] =='git': - ${_('Git repository')} - %endif - - ##PRIVATE/PUBLIC - %if 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['dbrepo_fork']: - - ${_('fork')} - %endif -
    + ##REPO NAME AND ICONS +
    + ${dt.repo_name(repo['name'],repo['dbrepo']['repo_type'],repo['dbrepo']['private'],repo['dbrepo_fork'].get('repo_name'))} ${h.truncate(repo['description'],60)} - - ${h.age(repo['last_change'])} - - %if repo['rev']>=0: - ${'r%s:%s' % (repo['rev'],h.short_id(repo['tip']))} - %else: - ${_('No changesets yet')} - %endif + ${h.age(repo['last_change'])} + ${dt.revision(repo['name'],repo['rev'],repo['tip'],repo['author'],repo['last_msg'])} + ${h.person(repo['contact'])} %if c.rhodecode_user.username != 'default': %else: - %endif: - %if c.rhodecode_user.username != 'default': @@ -156,71 +112,86 @@
    +
    - - - diff --git a/rhodecode/templates/journal/journal.html b/rhodecode/templates/journal/journal.html --- a/rhodecode/templates/journal/journal.html +++ b/rhodecode/templates/journal/journal.html @@ -10,60 +10,211 @@ ${self.menu('home')} <%def name="main()"> - +
    ${_('Journal')}
    +
    -
    ${c.journal_data}
    -
    -
    ${_('Following')}
    +
    + + ${_('My repos')} / ${_('Watched')} +
    + %if h.HasPermissionAny('hg.admin','hg.create.repository')(): + + %endif +
    + +
    + %if c.user_repos: +
    + + + + + + + + + + + <%namespace name="dt" file="/_data_table/_dt_elements.html"/> + %for repo in c.user_repos: + + ##QUICK MENU + + ##REPO NAME AND ICONS + + ##LAST REVISION + + ## + + + + %endfor + +
    ${_('Name')}${_('Tip')}${_('Action')}${_('Action')}
    + ${dt.quick_menu(repo['name'])} + + ${dt.repo_name(repo['name'],repo['dbrepo']['repo_type'],repo['dbrepo']['private'],repo['dbrepo_fork'].get('repo_name'))} + + ${dt.revision(repo['name'],repo['rev'],repo['tip'],repo['author'],repo['last_msg'])} + ${_('private')} + ${h.form(url('repo_settings_delete', repo_name=repo['name']),method='delete')} + ${h.submit('remove_%s' % repo['name'],'',class_="delete_icon action_button",onclick="return confirm('Confirm to delete this repository');")} + ${h.end_form()} +
    +
    + %else: +
    + ${_('No repositories yet')} + %if h.HasPermissionAny('hg.admin','hg.create.repository')(): + ${h.link_to(_('create one now'),h.url('admin_settings_create_repository'),class_="ui-btn")} + %endif +
    + %endif
    -
    - %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 --git a/rhodecode/templates/journal/journal_data.html b/rhodecode/templates/journal/journal_data.html --- a/rhodecode/templates/journal/journal_data.html +++ b/rhodecode/templates/journal/journal_data.html @@ -20,7 +20,7 @@ h.url('summary_home',repo_name=entry.repository.repo_name))} %else: ${entry.repository_name} - %endif + %endif
    ${h.literal(h.action_parser(entry)[1]())}
    @@ -30,18 +30,20 @@ %endfor %endfor - -
    - -${c.journal_pager.pager('$link_previous ~2~ $link_next')} -
    + +
    + + ${c.journal_pager.pager('$link_previous ~2~ $link_next')} +
    %else: - ${_('No entries yet')} -%endif \ No newline at end of file +
    + ${_('No entries yet')} +
    +%endif diff --git a/rhodecode/templates/journal/public_journal.html b/rhodecode/templates/journal/public_journal.html --- a/rhodecode/templates/journal/public_journal.html +++ b/rhodecode/templates/journal/public_journal.html @@ -10,7 +10,7 @@ ${self.menu('home')} <%def name="main()"> - +
    @@ -21,10 +21,10 @@
  • ${h.link_to(_('Atom'),h.url('public_journal_atom'),class_='atom_icon')} -
  • - - - + + + +
    +
    ${c.journal_data}
    - - + + diff --git a/rhodecode/templates/login.html b/rhodecode/templates/login.html --- a/rhodecode/templates/login.html +++ b/rhodecode/templates/login.html @@ -15,12 +15,12 @@ % endfor % endif - +
    ${_('Sign In to')} ${c.rhodecode_name}
    -
    +
    ${h.form(h.url.current(came_from=c.came_from))}
    @@ -33,8 +33,8 @@
    ${h.text('username',class_='focus',size=40)}
    - -
    + +
    @@ -42,14 +42,14 @@
    ${h.password('password',class_='focus',size=40)}
    - +
    - ##
    - ##
    - ## - ## - ##
    - ##
    +
    +
    + + +
    +
    ${h.submit('sign_in',_('Sign In'),class_="ui-button")}
    @@ -59,7 +59,7 @@ diff --git a/rhodecode/templates/password_reset.html b/rhodecode/templates/password_reset.html --- a/rhodecode/templates/password_reset.html +++ b/rhodecode/templates/password_reset.html @@ -6,7 +6,7 @@
    - +
    ${_('Reset your password to')} ${c.rhodecode_name}
    @@ -15,7 +15,7 @@
    - +
    @@ -24,13 +24,13 @@ ${h.text('email')}
    - +
    ${h.submit('send',_('Reset my password'),class_="ui-button")}
    ${_('Password reset link will be send to matching email address')}
    -
    +
    ${h.end_form()} @@ -38,7 +38,6 @@ YUE.onDOMReady(function(){ YUD.get('email').focus(); }) - -
    + +
    - diff --git a/rhodecode/templates/register.html b/rhodecode/templates/register.html --- a/rhodecode/templates/register.html +++ b/rhodecode/templates/register.html @@ -4,9 +4,9 @@ <%def name="title()"> ${_('Sign Up')} - ${c.rhodecode_name} - +
    - +
    ${_('Sign Up to')} ${c.rhodecode_name}
    @@ -23,7 +23,7 @@ ${h.text('username',class_="medium")}
    - +
    @@ -32,7 +32,7 @@ ${h.password('password',class_="medium")}
    - +
    @@ -41,7 +41,7 @@ ${h.password('password_confirmation',class_="medium")}
    - +
    @@ -50,7 +50,7 @@ ${h.text('name',class_="medium")}
    - +
    @@ -59,7 +59,7 @@ ${h.text('lastname',class_="medium")}
    - +
    @@ -68,7 +68,7 @@ ${h.text('email',class_="medium")}
    - +
    ${h.submit('sign_up',_('Sign Up'),class_="ui-button")} @@ -78,7 +78,7 @@
    ${_('Your account must wait for activation by administrator')}
    %endif
    -
    + ${h.end_form()} @@ -86,7 +86,6 @@ YUE.onDOMReady(function(){ YUD.get('username').focus(); }) - - + + - diff --git a/rhodecode/templates/repo_switcher_list.html b/rhodecode/templates/repo_switcher_list.html --- a/rhodecode/templates/repo_switcher_list.html +++ b/rhodecode/templates/repo_switcher_list.html @@ -1,23 +1,20 @@ ## -*- coding: utf-8 -*-
  • - +
  • - + %for repo in c.repos_list: - + %if repo['dbrepo']['private']:
  • - ${_('Private repository')} + ${_('Private repository')} ${h.link_to(repo['name'],h.url('summary_home',repo_name=repo['name']),class_="repo_name %s" % repo['dbrepo']['repo_type'])}
  • %else:
  • - ${_('Public repository')} + ${_('Public repository')} ${h.link_to(repo['name'],h.url('summary_home',repo_name=repo['name']),class_="repo_name %s" % repo['dbrepo']['repo_type'])}
  • - %endif -%endfor \ No newline at end of file + %endif +%endfor diff --git a/rhodecode/templates/search/search.html b/rhodecode/templates/search/search.html --- a/rhodecode/templates/search/search.html +++ b/rhodecode/templates/search/search.html @@ -2,11 +2,11 @@ <%inherit file="/base/base.html"/> <%def name="title()"> ${_('Search')} - ${'"%s"' % c.cur_query if c.cur_query else None} + ${'"%s"' % c.cur_query if c.cur_query else None} %if c.repo_name: ${_('in repository: ') + c.repo_name} %else: - ${_('in all repositories')} + ${_('in all repositories')} %endif - ${c.rhodecode_name} @@ -26,12 +26,12 @@ ${_('in repository: ') + c.repo_name} %else: ${_('in all repositories')} - %endif + %endif %if c.repo_name: - ${h.form(h.url('search_repo',search_repo=c.repo_name),method='get')} + ${h.form(h.url('search_repo',search_repo=c.repo_name),method='get')} %else: ${h.form(h.url('search'),method='get')} %endif @@ -40,13 +40,13 @@
    -
    +
    ${h.text('q',c.cur_query,class_="small")}
    -
    ${c.runtime}
    +
    ${c.runtime}
    @@ -58,14 +58,14 @@ ##('commit',_('Commit messages')), ('path',_('File names')), ##('repository',_('Repository names')), - ])} + ])}
    - + ${h.end_form()} - + %if c.cur_search == 'content': <%include file='search_content.html'/> %elif c.cur_search == 'path': @@ -74,7 +74,7 @@ <%include file='search_commit.html'/> %elif c.cur_search == 'repository': <%include file='search_repository.html'/> - %endif + %endif - + diff --git a/rhodecode/templates/search/search_content.html b/rhodecode/templates/search/search_content.html --- a/rhodecode/templates/search/search_content.html +++ b/rhodecode/templates/search/search_content.html @@ -5,10 +5,11 @@
    -
    ${h.link_to(h.literal('%s » %s' % (sr['repository'],sr['f_path'])), - h.url('files_home',repo_name=sr['repository'],revision='tip',f_path=sr['f_path']))}
    +
    ${h.link_to(h.literal('%s » %s' % (sr['repository'],sr['f_path'])), + h.url('files_home',repo_name=sr['repository'],revision='tip',f_path=sr['f_path']))} +
    -
    +
    ${h.literal(sr['content_short_hl'])}
    @@ -19,13 +20,13 @@
    ${_('Permission denied')}
    -
    +
    %endif - - %endif + + %endif %endfor %if c.cur_query and c.formated_results:
    ${c.formated_results.pager('$link_previous ~2~ $link_next')} -
    -%endif \ No newline at end of file + +%endif diff --git a/rhodecode/templates/search/search_path.html b/rhodecode/templates/search/search_path.html --- a/rhodecode/templates/search/search_path.html +++ b/rhodecode/templates/search/search_path.html @@ -1,27 +1,26 @@ ##path search - + ${h.end_form()} - - - + diff --git a/rhodecode/templates/shortlog/shortlog.html b/rhodecode/templates/shortlog/shortlog.html --- a/rhodecode/templates/shortlog/shortlog.html +++ b/rhodecode/templates/shortlog/shortlog.html @@ -8,7 +8,7 @@ <%def name="breadcrumbs_links()"> ${h.link_to(u'Home',h.url('/'))} - » + » ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))} » ${_('shortlog')} @@ -29,5 +29,5 @@ ${c.shortlog_data} - - \ No newline at end of file + + diff --git a/rhodecode/templates/shortlog/shortlog_data.html b/rhodecode/templates/shortlog/shortlog_data.html --- a/rhodecode/templates/shortlog/shortlog_data.html +++ b/rhodecode/templates/shortlog/shortlog_data.html @@ -1,31 +1,35 @@ ## -*- coding: utf-8 -*- -% if c.repo_changesets: - +%if c.repo_changesets: +
    - + + - - - %for cnt,cs in enumerate(c.repo_changesets): + + - - %endfor @@ -50,7 +49,7 @@ YUE.delegate("shortlog_data","click",function(e, matchedEl, container){ ypjax(e.target.href,"shortlog_data",function(){tooltip_activate();}); YUE.preventDefault(e); - },'.pager_link'); + },'.pager_link'); }); @@ -58,5 +57,27 @@ ${c.repo_changesets.pager('$link_previous ~2~ $link_next')} %else: - ${_('There are no changes yet')} + +%if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name): +

    ${_('Add or upload files directly via RhodeCode')}

    + %endif + + +

    ${_('Push new repo')}

    +
    +    ${c.rhodecode_repo.alias} clone ${c.clone_repo_url}
    +    ${c.rhodecode_repo.alias} add README # add first file
    +    ${c.rhodecode_repo.alias} commit -m "Initial" # commit with message
    +    ${c.rhodecode_repo.alias} push # push changes back
    +
    + +

    ${_('Existing repository?')}

    +
    +    ${c.rhodecode_repo.alias} push ${c.clone_repo_url}
    +
    +%endif diff --git a/rhodecode/templates/summary/summary.html b/rhodecode/templates/summary/summary.html --- a/rhodecode/templates/summary/summary.html +++ b/rhodecode/templates/summary/summary.html @@ -6,18 +6,25 @@ <%def name="breadcrumbs_links()"> ${h.link_to(u'Home',h.url('/'))} - » + » ${h.link_to(c.dbrepo.just_name,h.url('summary_home',repo_name=c.repo_name))} » ${_('summary')} <%def name="page_nav()"> - ${self.menu('summary')} + ${self.menu('summary')} <%def name="main()"> -
    + <% + summary = lambda n:{False:'summary-short'}.get(n) + %> + %if c.show_stats: +
    + %else: +
    + %endif
    ${self.breadcrumbs()} @@ -25,81 +32,81 @@
    - +
    -
    +
    -
    +
    +
    + %if c.rhodecode_user.username != 'default': + ${h.link_to(_('RSS'),h.url('rss_feed_home',repo_name=c.dbrepo.repo_name,api_key=c.rhodecode_user.api_key),class_='rss_icon')} + ${h.link_to(_('ATOM'),h.url('atom_feed_home',repo_name=c.dbrepo.repo_name,api_key=c.rhodecode_user.api_key),class_='atom_icon')} + %else: + ${h.link_to(_('RSS'),h.url('rss_feed_home',repo_name=c.dbrepo.repo_name),class_='rss_icon')} + ${h.link_to(_('ATOM'),h.url('atom_feed_home',repo_name=c.dbrepo.repo_name),class_='atom_icon')} + %endif +
    %if c.rhodecode_user.username != 'default': %if c.following: - + %else: %endif - %endif: - + %endif: ##REPO TYPE - %if c.dbrepo.repo_type =='hg': + %if h.is_hg(c.dbrepo): ${_('Mercurial repository')} %endif - %if c.dbrepo.repo_type =='git': + %if h.is_git(c.dbrepo): ${_('Git repository')} - %endif - - ##PUBLIC/PRIVATE + %endif + + ##PUBLIC/PRIVATE %if c.dbrepo.private: ${_('private repository')} %else: ${_('public repository')} %endif - + ##REPO NAME - ${h.repo_link(c.dbrepo.groups_and_repo)} - + ${h.repo_link(c.dbrepo.groups_and_repo)} + ##FORK %if c.dbrepo.fork: %endif ##REMOTE %if c.dbrepo.clone_uri: - %endif +
    + %endif
    - - +
    -
    +
    -
    ${h.urlify_text(c.dbrepo.description)}
    +
    ${h.urlify_text(c.dbrepo.description)}
    - - +
    -
    +
    -
    +
    gravatar
    @@ -108,595 +115,584 @@ ${_('Email')}: ${c.dbrepo.user.email}
    - +
    -
    - -
    -
    - ${'r%s:%s' % (h.get_changeset_safe(c.rhodecode_repo,'tip').revision, - h.get_changeset_safe(c.rhodecode_repo,'tip').short_id)} - - - ${h.age(c.rhodecode_repo.last_change)}
    - ${_('by')} ${h.get_changeset_safe(c.rhodecode_repo,'tip').author} - -
    -
    - -
    -
    +
    -
    - +
    + +
    ${_('Show by ID')}
    + +
    - +
    -
    - +
    +
    -
    -
    +
    + %if c.show_stats: +
    + %else: + ${_('Statistics are disabled for this repository')} + %if h.HasPermissionAll('hg.admin')('enable stats on from summary'): + ${h.link_to(_('enable'),h.url('edit_repo',repo_name=c.repo_name),class_="ui-btn")} + %endif + %endif
    - +
    -
    +
    -
    +
    %if len(c.rhodecode_repo.revisions) == 0: ${_('There are no downloads yet')} %elif c.enable_downloads is False: ${_('Downloads are disabled for this repository')} - %if h.HasPermissionAll('hg.admin')('enable stats on from summary'): - ${h.link_to(_('enable'),h.url('edit_repo',repo_name=c.repo_name),class_="ui-button-small")} - %endif + %if h.HasPermissionAll('hg.admin')('enable downloads on from summary'): + ${h.link_to(_('enable'),h.url('edit_repo',repo_name=c.repo_name),class_="ui-btn")} + %endif %else: ${h.select('download_options',c.rhodecode_repo.get_changeset().raw_id,c.download_options)} - %for cnt,archive in enumerate(c.rhodecode_repo._get_archives()): - %if cnt >=1: - | - %endif - ${h.link_to(archive['type'], - h.url('files_archive_home',repo_name=c.dbrepo.repo_name, - fname='tip'+archive['extension']),class_="archive_icon")} - %endfor + ${h.link_to('Download as zip',h.url('files_archive_home',repo_name=c.dbrepo.repo_name,fname='tip.zip'),class_="archive_icon ui-btn")} - ${_('with subrepos')} + + %endif
    - -
    -
    - -
    -
    - %if c.rhodecode_user.username != 'default': - ${h.link_to(_('RSS'),h.url('rss_feed_home',repo_name=c.dbrepo.repo_name,api_key=c.rhodecode_user.api_key),class_='rss_icon')} - ${h.link_to(_('Atom'),h.url('atom_feed_home',repo_name=c.dbrepo.repo_name,api_key=c.rhodecode_user.api_key),class_='atom_icon')} - %else: - ${h.link_to(_('RSS'),h.url('rss_feed_home',repo_name=c.dbrepo.repo_name),class_='rss_icon')} - ${h.link_to(_('Atom'),h.url('atom_feed_home',repo_name=c.dbrepo.repo_name),class_='atom_icon')} - %endif -
    -
    -
    +
    - -
    - +%if c.show_stats:
    ${_('Commit activity by day / author')}
    - +
    -
    +
    %if c.no_data: ${c.no_data_msg} %if h.HasPermissionAll('hg.admin')('enable stats on from summary'): - ${h.link_to(_('enable'),h.url('edit_repo',repo_name=c.repo_name),class_="ui-button-small")} - %endif - + ${h.link_to(_('enable'),h.url('edit_repo',repo_name=c.repo_name),class_="ui-btn")} + %endif %else: - ${_('Loaded in')} ${c.stats_percentage} % + ${_('Stats gathered: ')} ${c.stats_percentage}% %endif -
    +
    - +
    -
    ${_('commit message')}${_('revision')}${_('commit message')} ${_('age')} ${_('author')}${_('revision')} ${_('branch')} ${_('tags')}${_('links')}
    + + ${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}:${h.short_id(cs.raw_id)} - ${cs.branch} + + %if h.is_hg(c.rhodecode_repo): + ${cs.branch} + %endif + @@ -35,11 +39,6 @@ %endfor - ${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.raw_id))} -
    +
    - - +
    +
    +
    -
    - -
    -
    - -
    <%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 +
    +
    + +%if c.readme_data: +
    + +
    +
    + ${c.readme_data|n} +
    -
    -
    - -
    -
    - <%include file='../tags/tags_data.html'/> - %if c.repo_changesets: - ${h.link_to(_('show more'),h.url('tags_home',repo_name=c.repo_name))} - %endif -
    -
    -
    -
    - -
    -
    - <%include file='../branches/branches_data.html'/> - %if c.repo_changesets: - ${h.link_to(_('show more'),h.url('branches_home',repo_name=c.repo_name))} - %endif -
    -
    +%endif + + +%if c.show_stats: + + +%endif + + diff --git a/rhodecode/templates/switch_to_list.html b/rhodecode/templates/switch_to_list.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/switch_to_list.html @@ -0,0 +1,39 @@ +## -*- coding: utf-8 -*- +
  • + ${h.link_to('%s (%s)' % (_('branches'),len(c.rhodecode_repo.branches.values()),),h.url('branches_home',repo_name=c.repo_name),class_='branches childs')} +
      + %if c.rhodecode_repo.branches.values(): + %for cnt,branch in enumerate(c.rhodecode_repo.branches.items()): +
    • ${h.link_to('%s - %s' % (branch[0],h.short_id(branch[1])),h.url('files_home',repo_name=c.repo_name,revision=branch[1]))}
    • + %endfor + %else: +
    • ${h.link_to(_('There are no branches yet'),'#')}
    • + %endif +
    +
  • +
  • + ${h.link_to('%s (%s)' % (_('tags'),len(c.rhodecode_repo.tags.values()),),h.url('tags_home',repo_name=c.repo_name),class_='tags childs')} +
      + %if c.rhodecode_repo.tags.values(): + %for cnt,tag in enumerate(c.rhodecode_repo.tags.items()): +
    • ${h.link_to('%s - %s' % (tag[0],h.short_id(tag[1])),h.url('files_home',repo_name=c.repo_name,revision=tag[1]))}
    • + %endfor + %else: +
    • ${h.link_to(_('There are no tags yet'),'#')}
    • + %endif +
    +
  • +%if c.rhodecode_repo.alias == 'hg': +
  • + ${h.link_to('%s (%s)' % (_('bookmarks'),len(c.rhodecode_repo.bookmarks.values()),),h.url('bookmarks_home',repo_name=c.repo_name),class_='bookmarks childs')} +
      + %if c.rhodecode_repo.bookmarks.values(): + %for cnt,book in enumerate(c.rhodecode_repo.bookmarks.items()): +
    • ${h.link_to('%s - %s' % (book[0],h.short_id(book[1])),h.url('files_home',repo_name=c.repo_name,revision=book[1]))}
    • + %endfor + %else: +
    • ${h.link_to(_('There are no bookmarks yet'),'#')}
    • + %endif +
    +
  • +%endif diff --git a/rhodecode/templates/tags/tags.html b/rhodecode/templates/tags/tags.html --- a/rhodecode/templates/tags/tags.html +++ b/rhodecode/templates/tags/tags.html @@ -7,8 +7,9 @@ <%def name="breadcrumbs_links()"> + ${h.link_to(u'Home',h.url('/'))} - » + » ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))} » ${_('tags')} @@ -27,5 +28,49 @@
    <%include file='tags_data.html'/>
    - - \ No newline at end of file + + + diff --git a/rhodecode/templates/tags/tags_data.html b/rhodecode/templates/tags/tags_data.html --- a/rhodecode/templates/tags/tags_data.html +++ b/rhodecode/templates/tags/tags_data.html @@ -1,33 +1,34 @@ -%if c.repo_tags: - +%if c.repo_tags: +
    +
    + - - - - - + + + + + %for cnt,tag in enumerate(c.repo_tags.items()): - - + + + - - - + + %endfor
    ${_('date')}${_('name')}${_('author')}${_('revision')}${_('links')}${_('Name')}${_('Date')}${_('Author')}${_('Revision')}
    - ${tag[1].date} -
    ${h.link_to(tag[0], - h.url('changeset_home',repo_name=c.repo_name,revision=tag[1].raw_id))} + h.url('files_home',repo_name=c.repo_name,revision=tag[1].raw_id))} + - ${tag[1].date} ${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].raw_id))} - | - ${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name,revision=tag[1].raw_id))} -
    + +
    + %else: ${_('There are no tags yet')} -%endif \ No newline at end of file +%endif diff --git a/rhodecode/tests/__init__.py b/rhodecode/tests/__init__.py --- a/rhodecode/tests/__init__.py +++ b/rhodecode/tests/__init__.py @@ -8,9 +8,12 @@ This module initializes the application setup-app`) and provides the base testing objects. """ import os +import time +import logging from os.path import join as jn from unittest import TestCase +from tempfile import _RandomNameSequence from paste.deploy import loadapp from paste.script.appinstall import SetupCommand @@ -18,31 +21,47 @@ from pylons import config, url from routes.util import URLGenerator from webtest import TestApp -from rhodecode.model import meta -import logging - - -log = logging.getLogger(__name__) +from rhodecode.model.meta import Session +from rhodecode.model.db import User import pylons.test -__all__ = ['environ', 'url', 'TestController', 'TESTS_TMP_PATH', 'HG_REPO', - 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO', 'HG_FORK', 'GIT_FORK', - 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_ADMIN_PASS' ] +os.environ['TZ'] = 'UTC' +time.tzset() + +log = logging.getLogger(__name__) + +__all__ = [ + 'environ', 'url', 'TestController', 'TESTS_TMP_PATH', 'HG_REPO', + 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO', 'HG_FORK', 'GIT_FORK', + 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS', + 'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN', + 'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL' +] # Invoke websetup with the current config file -#SetupCommand('setup-app').run([config_file]) +# SetupCommand('setup-app').run([config_file]) ##RUNNING DESIRED TESTS # nosetests -x rhodecode.tests.functional.test_admin_settings:TestSettingsController.test_my_account -# nosetests --pdb --pdb-failures +# nosetests --pdb --pdb-failures environ = {} #SOME GLOBALS FOR TESTS -from tempfile import _RandomNameSequence + TESTS_TMP_PATH = jn('/', 'tmp', 'rc_test_%s' % _RandomNameSequence().next()) TEST_USER_ADMIN_LOGIN = 'test_admin' TEST_USER_ADMIN_PASS = 'test12' +TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com' + +TEST_USER_REGULAR_LOGIN = 'test_regular' +TEST_USER_REGULAR_PASS = 'test12' +TEST_USER_REGULAR_EMAIL = 'test_regular@mail.com' + +TEST_USER_REGULAR2_LOGIN = 'test_regular2' +TEST_USER_REGULAR2_PASS = 'test12' +TEST_USER_REGULAR2_EMAIL = 'test_regular2@mail.com' + HG_REPO = 'vcs_test_hg' GIT_REPO = 'vcs_test_git' @@ -60,12 +79,13 @@ class TestController(TestCase): self.app = TestApp(wsgiapp) url._push_object(URLGenerator(config['routes.map'], environ)) - self.sa = meta.Session + self.Session = Session self.index_location = config['app_conf']['index_dir'] TestCase.__init__(self, *args, **kwargs) def log_user(self, username=TEST_USER_ADMIN_LOGIN, password=TEST_USER_ADMIN_PASS): + self._logged_username = username response = self.app.post(url(controller='login', action='index'), {'username':username, 'password':password}) @@ -74,10 +94,16 @@ class TestController(TestCase): self.fail('could not login using %s %s' % (username, password)) self.assertEqual(response.status, '302 Found') - self.assertEqual(response.session['rhodecode_user'].username, username) - return response.follow() + ses = response.session['rhodecode_user'] + self.assertEqual(ses.get('username'), username) + response = response.follow() + self.assertEqual(ses.get('is_authenticated'), True) + + return response.session['rhodecode_user'] + + def _get_logged_user(self): + return User.get_by_username(self._logged_username) def checkSessionFlash(self, response, msg): self.assertTrue('flash' in response.session) self.assertTrue(msg in response.session['flash'][0][1]) - diff --git a/rhodecode/tests/test_concurency.py b/rhodecode/tests/_test_concurency.py old mode 100755 new mode 100644 rename from rhodecode/tests/test_concurency.py rename to rhodecode/tests/_test_concurency.py --- a/rhodecode/tests/test_concurency.py +++ b/rhodecode/tests/_test_concurency.py @@ -6,7 +6,8 @@ Test suite for making push/pull operations :created_on: Dec 30, 2010 - :copyright: (C) 2009-2011 Marcin Kuzminski + :author: marcink + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -53,7 +54,8 @@ add_cache(conf) USER = 'test_admin' PASS = 'test12' -HOST = '127.0.0.1:5000' +HOST = 'hg.local' +METHOD = 'pull' DEBUG = True log = logging.getLogger(__name__) @@ -80,7 +82,7 @@ class Command(object): def get_session(): engine = engine_from_config(conf, 'sqlalchemy.db1.') init_model(engine) - sa = meta.Session() + sa = meta.Session return sa @@ -153,9 +155,12 @@ def get_anonymous_access(): #============================================================================== # TESTS #============================================================================== -def test_clone_with_credentials(no_errors=False, repo=HG_REPO): +def test_clone_with_credentials(no_errors=False, repo=HG_REPO, method=METHOD, + seq=None): cwd = path = jn(TESTS_TMP_PATH, repo) + if seq == None: + seq = _RandomNameSequence().next() try: shutil.rmtree(path, ignore_errors=True) @@ -164,26 +169,43 @@ def test_clone_with_credentials(no_error except OSError: raise - - clone_url = 'http://%(user)s:%(pass)s@%(host)s/%(cloned_repo)s %(dest)s' % \ + clone_url = 'http://%(user)s:%(pass)s@%(host)s/%(cloned_repo)s' % \ {'user':USER, 'pass':PASS, 'host':HOST, - 'cloned_repo':repo, - 'dest':path + _RandomNameSequence().next()} + 'cloned_repo':repo, } - stdout, stderr = Command(cwd).execute('hg clone', clone_url) + dest = path + seq + if method == 'pull': + stdout, stderr = Command(cwd).execute('hg', method, '--cwd', dest, clone_url) + else: + stdout, stderr = Command(cwd).execute('hg', method, clone_url, dest) - if no_errors is False: - assert """adding file changes""" in stdout, 'no messages about cloning' - assert """abort""" not in stderr , 'got error from clone' + if no_errors is False: + assert """adding file changes""" in stdout, 'no messages about cloning' + assert """abort""" not in stderr , 'got error from clone' if __name__ == '__main__': try: create_test_user(force=False) + seq = None + import time - for i in range(int(sys.argv[2])): - test_clone_with_credentials(repo=sys.argv[1]) + try: + METHOD = sys.argv[3] + except: + pass + if METHOD == 'pull': + seq = _RandomNameSequence().next() + test_clone_with_credentials(repo=sys.argv[1], method='clone', + seq=seq) + s = time.time() + for i in range(1, int(sys.argv[2]) + 1): + print 'take', i + test_clone_with_credentials(repo=sys.argv[1], method=METHOD, + seq=seq) + print 'time taken %.3f' % (time.time() - s) except Exception, e: + raise sys.exit('stop on %s' % e) diff --git a/rhodecode/tests/functional/test_admin_ldap_settings.py b/rhodecode/tests/functional/test_admin_ldap_settings.py --- a/rhodecode/tests/functional/test_admin_ldap_settings.py +++ b/rhodecode/tests/functional/test_admin_ldap_settings.py @@ -1,5 +1,5 @@ from rhodecode.tests import * -from rhodecode.model.db import RhodeCodeSettings +from rhodecode.model.db import RhodeCodeSetting from nose.plugins.skip import SkipTest skip_ldap_test = False @@ -22,7 +22,7 @@ class TestLdapSettingsController(TestCon self.log_user() if skip_ldap_test: raise SkipTest('skipping due to missing ldap lib') - + test_url = url(controller='admin/ldap_settings', action='ldap_settings') @@ -41,7 +41,8 @@ class TestLdapSettingsController(TestCon 'ldap_attr_lastname':'tester', 'ldap_attr_email':'test@example.com' }) - new_settings = RhodeCodeSettings.get_ldap_settings() + new_settings = RhodeCodeSetting.get_ldap_settings() + print new_settings self.assertEqual(new_settings['ldap_host'], u'dc.example.com', 'fail db write compare') @@ -52,7 +53,7 @@ class TestLdapSettingsController(TestCon self.log_user() if skip_ldap_test: raise SkipTest('skipping due to missing ldap lib') - + test_url = url(controller='admin/ldap_settings', action='ldap_settings') @@ -70,13 +71,13 @@ class TestLdapSettingsController(TestCon 'ldap_attr_firstname':'', 'ldap_attr_lastname':'', 'ldap_attr_email':'' }) - + self.assertTrue("""The LDAP Login""" """ attribute of the CN must be specified""" in response.body) - - - + + + self.assertTrue("""Please """ """enter a number""" in response.body) diff --git a/rhodecode/tests/functional/test_admin_notifications.py b/rhodecode/tests/functional/test_admin_notifications.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/functional/test_admin_notifications.py @@ -0,0 +1,119 @@ +from rhodecode.tests import * +from rhodecode.model.db import Notification, User, UserNotification + +from rhodecode.model.user import UserModel +from rhodecode.model.notification import NotificationModel +from rhodecode.model.meta import Session + +class TestNotificationsController(TestController): + + + def tearDown(self): + for n in Notification.query().all(): + inst = Notification.get(n.notification_id) + Session.delete(inst) + Session.commit() + + def test_index(self): + self.log_user() + + u1 = UserModel().create_or_update(username='u1', password='qweqwe', + email='u1@rhodecode.org', + name='u1', lastname='u1').user_id + + response = self.app.get(url('notifications')) + self.assertTrue('''
    No notifications here yet
    ''' + in response.body) + + cur_user = self._get_logged_user() + + NotificationModel().create(created_by=u1, subject=u'test_notification_1', + body=u'notification_1', + recipients=[cur_user]) + Session.commit() + response = self.app.get(url('notifications')) + self.assertTrue(u'test_notification_1' in response.body) + +# def test_index_as_xml(self): +# response = self.app.get(url('formatted_notifications', format='xml')) +# +# def test_create(self): +# response = self.app.post(url('notifications')) +# +# def test_new(self): +# response = self.app.get(url('new_notification')) +# +# def test_new_as_xml(self): +# response = self.app.get(url('formatted_new_notification', format='xml')) +# +# def test_update(self): +# response = self.app.put(url('notification', notification_id=1)) +# +# def test_update_browser_fakeout(self): +# response = self.app.post(url('notification', notification_id=1), params=dict(_method='put')) + + def test_delete(self): + self.log_user() + cur_user = self._get_logged_user() + + u1 = UserModel().create_or_update(username='u1', password='qweqwe', + email='u1@rhodecode.org', + name='u1', lastname='u1') + u2 = UserModel().create_or_update(username='u2', password='qweqwe', + email='u2@rhodecode.org', + name='u2', lastname='u2') + + # make notifications + notification = NotificationModel().create(created_by=cur_user, + subject=u'test', + body=u'hi there', + recipients=[cur_user, u1, u2]) + Session.commit() + u1 = User.get(u1.user_id) + u2 = User.get(u2.user_id) + + # check DB + get_notif = lambda un:[x.notification for x in un] + self.assertEqual(get_notif(cur_user.notifications), [notification]) + self.assertEqual(get_notif(u1.notifications), [notification]) + self.assertEqual(get_notif(u2.notifications), [notification]) + cur_usr_id = cur_user.user_id + + + response = self.app.delete(url('notification', + notification_id= + notification.notification_id)) + + cur_user = User.get(cur_usr_id) + self.assertEqual(cur_user.notifications, []) + + +# def test_delete_browser_fakeout(self): +# response = self.app.post(url('notification', notification_id=1), params=dict(_method='delete')) + + def test_show(self): + self.log_user() + cur_user = self._get_logged_user() + u1 = UserModel().create_or_update(username='u1', password='qweqwe', + email='u1@rhodecode.org', + name='u1', lastname='u1') + u2 = UserModel().create_or_update(username='u2', password='qweqwe', + email='u2@rhodecode.org', + name='u2', lastname='u2') + + notification = NotificationModel().create(created_by=cur_user, + subject=u'test', + body=u'hi there', + recipients=[cur_user, u1, u2]) + + response = self.app.get(url('notification', + notification_id=notification.notification_id)) + +# def test_show_as_xml(self): +# response = self.app.get(url('formatted_notification', notification_id=1, format='xml')) +# +# def test_edit(self): +# response = self.app.get(url('edit_notification', notification_id=1)) +# +# def test_edit_as_xml(self): +# response = self.app.get(url('formatted_edit_notification', notification_id=1, format='xml')) diff --git a/rhodecode/tests/functional/test_admin_repos.py b/rhodecode/tests/functional/test_admin_repos.py --- a/rhodecode/tests/functional/test_admin_repos.py +++ b/rhodecode/tests/functional/test_admin_repos.py @@ -1,18 +1,16 @@ # -*- coding: utf-8 -*- import os -import vcs +from rhodecode.lib import vcs from rhodecode.model.db import Repository from rhodecode.tests import * class TestAdminReposController(TestController): - def __make_repo(self): pass - def test_index(self): self.log_user() response = self.app.get(url('repos')) @@ -32,11 +30,10 @@ class TestAdminReposController(TestContr 'repo_group':'', 'description':description, 'private':private}) - self.checkSessionFlash(response, 'created repository %s' % (repo_name)) #test if the repo was created in the database - new_repo = self.sa.query(Repository).filter(Repository.repo_name == + new_repo = self.Session.query(Repository).filter(Repository.repo_name == repo_name).one() self.assertEqual(new_repo.repo_name, repo_name) @@ -73,7 +70,7 @@ class TestAdminReposController(TestContr 'created repository %s' % (repo_name_unicode)) #test if the repo was created in the database - new_repo = self.sa.query(Repository).filter(Repository.repo_name == + new_repo = self.Session.query(Repository).filter(Repository.repo_name == repo_name_unicode).one() self.assertEqual(new_repo.repo_name, repo_name_unicode) @@ -113,7 +110,7 @@ class TestAdminReposController(TestContr 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() + new_repo = self.Session.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' @@ -162,7 +159,7 @@ class TestAdminReposController(TestContr response.session['flash'][0]) #test if the repo was created in the database - new_repo = self.sa.query(Repository).filter(Repository.repo_name == + new_repo = self.Session.query(Repository).filter(Repository.repo_name == repo_name).one() self.assertEqual(new_repo.repo_name, repo_name) @@ -182,7 +179,7 @@ class TestAdminReposController(TestContr response.follow() #check if repo was deleted from db - deleted_repo = self.sa.query(Repository).filter(Repository.repo_name + deleted_repo = self.Session.query(Repository).filter(Repository.repo_name == repo_name).scalar() self.assertEqual(deleted_repo, None) diff --git a/rhodecode/tests/functional/test_admin_settings.py b/rhodecode/tests/functional/test_admin_settings.py --- a/rhodecode/tests/functional/test_admin_settings.py +++ b/rhodecode/tests/functional/test_admin_settings.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from rhodecode.lib.auth import get_crypt_password, check_password -from rhodecode.model.db import User, RhodeCodeSettings +from rhodecode.model.db import User, RhodeCodeSetting from rhodecode.tests import * class TestAdminSettingsController(TestController): @@ -63,7 +63,7 @@ class TestAdminSettingsController(TestCo self.checkSessionFlash(response, 'Updated application settings') - self.assertEqual(RhodeCodeSettings + self.assertEqual(RhodeCodeSetting .get_app_settings()['rhodecode_ga_code'], new_ga_code) response = response.follow() @@ -85,7 +85,7 @@ class TestAdminSettingsController(TestCo self.assertTrue('Updated application settings' in response.session['flash'][0][1]) - self.assertEqual(RhodeCodeSettings + self.assertEqual(RhodeCodeSetting .get_app_settings()['rhodecode_ga_code'], new_ga_code) response = response.follow() @@ -109,7 +109,7 @@ class TestAdminSettingsController(TestCo )) self.checkSessionFlash(response, 'Updated application settings') - self.assertEqual(RhodeCodeSettings + self.assertEqual(RhodeCodeSetting .get_app_settings()['rhodecode_title'], new_title.decode('utf-8')) @@ -145,7 +145,7 @@ class TestAdminSettingsController(TestCo 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() + user = self.Session.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) 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) @@ -171,7 +171,7 @@ class TestAdminSettingsController(TestCo self.checkSessionFlash(response, 'Your account was updated successfully') - user = self.sa.query(User).filter(User.username == 'test_admin').one() + user = self.Session.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) diff --git a/rhodecode/tests/functional/test_admin_users.py b/rhodecode/tests/functional/test_admin_users.py --- a/rhodecode/tests/functional/test_admin_users.py +++ b/rhodecode/tests/functional/test_admin_users.py @@ -1,11 +1,13 @@ from rhodecode.tests import * -from rhodecode.model.db import User +from rhodecode.model.db import User, Permission from rhodecode.lib.auth import check_password from sqlalchemy.orm.exc import NoResultFound +from rhodecode.model.user import UserModel class TestAdminUsersController(TestController): def test_index(self): + self.log_user() response = self.app.get(url('users')) # Test response... @@ -21,30 +23,31 @@ class TestAdminUsersController(TestContr lastname = 'lastname' email = 'mail@mail.com' - response = self.app.post(url('users'), {'username':username, - 'password':password, - 'password_confirmation':password_confirmation, - 'name':name, - 'active':True, - 'lastname':lastname, - 'email':email}) + response = self.app.post(url('users'), + {'username':username, + 'password':password, + 'password_confirmation':password_confirmation, + 'name':name, + 'active':True, + 'lastname':lastname, + 'email':email}) - assert '''created user %s''' % (username) in response.session['flash'][0], 'No flash message about new user' + self.assertTrue('''created user %s''' % (username) in + response.session['flash'][0]) - new_user = self.sa.query(User).filter(User.username == username).one() - + new_user = self.Session.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' - + self.assertEqual(new_user.username,username) + self.assertEqual(check_password(password, new_user.password),True) + self.assertEqual(new_user.name,name) + self.assertEqual(new_user.lastname,lastname) + self.assertEqual(new_user.email,email) response.follow() response = response.follow() - assert """edit">newtestuser""" in response.body + self.assertTrue("""edit">newtestuser""" in response.body) def test_create_err(self): self.log_user() @@ -61,16 +64,17 @@ class TestAdminUsersController(TestContr '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 + self.assertTrue("""Invalid username""" in response.body) + self.assertTrue("""Please enter a value""" in response.body) + self.assertTrue("""An email address must contain a single @""" in response.body) def get_user(): - self.sa.query(User).filter(User.username == username).one() + self.Session.query(User).filter(User.username == username).one() self.assertRaises(NoResultFound, get_user), 'found user in database' def test_new(self): + self.log_user() response = self.app.get(url('new_user')) def test_new_as_xml(self): @@ -100,14 +104,17 @@ class TestAdminUsersController(TestContr response = response.follow() - new_user = self.sa.query(User).filter(User.username == username).one() + new_user = self.Session.query(User)\ + .filter(User.username == username).one() response = self.app.delete(url('user', id=new_user.user_id)) - assert """successfully deleted user""" in response.session['flash'][0], 'No info about user deletion' + self.assertTrue("""successfully deleted user""" in + response.session['flash'][0]) def test_delete_browser_fakeout(self): - response = self.app.post(url('user', id=1), params=dict(_method='delete')) + response = self.app.post(url('user', id=1), + params=dict(_method='delete')) def test_show(self): response = self.app.get(url('user', id=1)) @@ -116,7 +123,57 @@ class TestAdminUsersController(TestContr response = self.app.get(url('formatted_user', id=1, format='xml')) def test_edit(self): - response = self.app.get(url('edit_user', id=1)) + self.log_user() + user = User.get_by_username(TEST_USER_ADMIN_LOGIN) + response = self.app.get(url('edit_user', id=user.user_id)) + + + def test_add_perm_create_repo(self): + self.log_user() + perm_none = Permission.get_by_key('hg.create.none') + perm_create = Permission.get_by_key('hg.create.repository') + + user = User.get_by_username(TEST_USER_REGULAR_LOGIN) + + + #User should have None permission on creation repository + self.assertEqual(UserModel().has_perm(user, perm_none), False) + self.assertEqual(UserModel().has_perm(user, perm_create), False) + + response = self.app.post(url('user_perm', id=user.user_id), + params=dict(_method='put', + create_repo_perm=True)) + + perm_none = Permission.get_by_key('hg.create.none') + perm_create = Permission.get_by_key('hg.create.repository') + + user = User.get_by_username(TEST_USER_REGULAR_LOGIN) + #User should have None permission on creation repository + self.assertEqual(UserModel().has_perm(user, perm_none), False) + self.assertEqual(UserModel().has_perm(user, perm_create), True) + + def test_revoke_perm_create_repo(self): + self.log_user() + perm_none = Permission.get_by_key('hg.create.none') + perm_create = Permission.get_by_key('hg.create.repository') + + user = User.get_by_username(TEST_USER_REGULAR2_LOGIN) + + + #User should have None permission on creation repository + self.assertEqual(UserModel().has_perm(user, perm_none), False) + self.assertEqual(UserModel().has_perm(user, perm_create), False) + + response = self.app.post(url('user_perm', id=user.user_id), + params=dict(_method='put')) + + perm_none = Permission.get_by_key('hg.create.none') + perm_create = Permission.get_by_key('hg.create.repository') + + user = User.get_by_username(TEST_USER_REGULAR2_LOGIN) + #User should have None permission on creation repository + self.assertEqual(UserModel().has_perm(user, perm_none), True) + self.assertEqual(UserModel().has_perm(user, perm_create), False) def test_edit_as_xml(self): response = self.app.get(url('formatted_edit_user', id=1, format='xml')) diff --git a/rhodecode/tests/functional/test_admin_users_groups.py b/rhodecode/tests/functional/test_admin_users_groups.py --- a/rhodecode/tests/functional/test_admin_users_groups.py +++ b/rhodecode/tests/functional/test_admin_users_groups.py @@ -23,10 +23,6 @@ class TestAdminUsersGroupsController(Tes self.checkSessionFlash(response, 'created users group %s' % TEST_USERS_GROUP) - - - - def test_new(self): response = self.app.get(url('new_users_group')) @@ -52,13 +48,13 @@ class TestAdminUsersGroupsController(Tes 'created users group %s' % users_group_name) - gr = self.sa.query(UsersGroup)\ + gr = self.Session.query(UsersGroup)\ .filter(UsersGroup.users_group_name == users_group_name).one() response = self.app.delete(url('users_group', id=gr.users_group_id)) - gr = self.sa.query(UsersGroup)\ + gr = self.Session.query(UsersGroup)\ .filter(UsersGroup.users_group_name == users_group_name).scalar() @@ -89,7 +85,3 @@ class TestAdminUsersGroupsController(Tes def test_revoke_members(self): pass - - - - diff --git a/rhodecode/tests/functional/test_branches.py b/rhodecode/tests/functional/test_branches.py --- a/rhodecode/tests/functional/test_branches.py +++ b/rhodecode/tests/functional/test_branches.py @@ -4,15 +4,8 @@ class TestBranchesController(TestControl def test_index(self): self.log_user() - 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... + response = self.app.get(url(controller='branches', + action='index', repo_name=HG_REPO)) + response.mustcontain("""default""" % HG_REPO) + response.mustcontain("""git""" % HG_REPO) + response.mustcontain("""web""" % HG_REPO) diff --git a/rhodecode/tests/functional/test_changelog.py b/rhodecode/tests/functional/test_changelog.py --- a/rhodecode/tests/functional/test_changelog.py +++ b/rhodecode/tests/functional/test_changelog.py @@ -1,5 +1,6 @@ from rhodecode.tests import * + class TestChangelogController(TestController): def test_index_hg(self): @@ -7,23 +8,26 @@ class TestChangelogController(TestContro response = self.app.get(url(controller='changelog', action='index', repo_name=HG_REPO)) - self.assertTrue("""
    """ - in response.body) - self.assertTrue("""""" - in response.body) - self.assertTrue("""commit 154: 5e204e7583b9@2010-08-10 """ - """02:18:46""" in response.body) - self.assertTrue("""Small update at simplevcs app""" in response.body) + response.mustcontain("""
    """) + response.mustcontain( + """""" + ) + response.mustcontain( + """154:""" + """5e204e7583b9""" + ) + response.mustcontain("""Small update at simplevcs app""") - self.assertTrue("""3""" in response.body) + response.mustcontain( + """
    3
    """ + ) #pagination - response = self.app.get(url(controller='changelog', action='index', repo_name=HG_REPO), {'page':1}) response = self.app.get(url(controller='changelog', action='index', @@ -37,25 +41,26 @@ class TestChangelogController(TestContro response = self.app.get(url(controller='changelog', action='index', repo_name=HG_REPO), {'page':6}) - # Test response after pagination... - self.assertTrue("""""" - in response.body) - self.assertTrue("""commit 64: 46ad32a4f974@2010-04-20""" - """ 01:33:21"""in response.body) + response.mustcontain( + """""" + ) + response.mustcontain( + """64:""" + """46ad32a4f974""" + ) - self.assertTrue("""21"""in response.body) - self.assertTrue("""""" % HG_REPO in response.body) + response.mustcontain( + """
    21
    """ + ) - - - #def test_index_git(self): - # self.log_user() - # response = self.app.get(url(controller='changelog', action='index', repo_name=GIT_REPO)) + response.mustcontain( + """""" + """46ad32a4f974""" % HG_REPO + ) diff --git a/rhodecode/tests/functional/test_changeset_comments.py b/rhodecode/tests/functional/test_changeset_comments.py new file mode 100644 --- /dev/null +++ b/rhodecode/tests/functional/test_changeset_comments.py @@ -0,0 +1,139 @@ +from rhodecode.tests import * +from rhodecode.model.db import ChangesetComment, Notification, User, \ + UserNotification + +class TestChangeSetCommentrController(TestController): + + def setUp(self): + for x in ChangesetComment.query().all(): + self.Session.delete(x) + self.Session.commit() + + for x in Notification.query().all(): + self.Session.delete(x) + self.Session.commit() + + def tearDown(self): + for x in ChangesetComment.query().all(): + self.Session.delete(x) + self.Session.commit() + + for x in Notification.query().all(): + self.Session.delete(x) + self.Session.commit() + + def test_create(self): + self.log_user() + rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc' + text = u'CommentOnRevision' + + params = {'text':text} + response = self.app.post(url(controller='changeset', action='comment', + repo_name=HG_REPO, revision=rev), + params=params) + # Test response... + self.assertEqual(response.status, '302 Found') + response.follow() + + response = self.app.get(url(controller='changeset', action='index', + repo_name=HG_REPO, revision=rev)) + # test DB + self.assertEqual(ChangesetComment.query().count(), 1) + self.assertTrue('''
    %s ''' + '''comment(s) (0 inline)
    ''' % 1 in response.body) + + + self.assertEqual(Notification.query().count(), 1) + notification = Notification.query().all()[0] + + self.assertEqual(notification.type_, Notification.TYPE_CHANGESET_COMMENT) + self.assertTrue((u'/vcs_test_hg/changeset/27cd5cce30c96924232df' + 'fcd24178a07ffeb5dfc#comment-1') in notification.subject) + + def test_create_inline(self): + self.log_user() + rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc' + text = u'CommentOnRevision' + f_path = 'vcs/web/simplevcs/views/repository.py' + line = 'n1' + + params = {'text':text, 'f_path':f_path, 'line':line} + response = self.app.post(url(controller='changeset', action='comment', + repo_name=HG_REPO, revision=rev), + params=params) + # Test response... + self.assertEqual(response.status, '302 Found') + response.follow() + + response = self.app.get(url(controller='changeset', action='index', + repo_name=HG_REPO, revision=rev)) + #test DB + self.assertEqual(ChangesetComment.query().count(), 1) + self.assertTrue('''
    0 comment(s)''' + ''' (%s inline)
    ''' % 1 in response.body) + self.assertTrue('''
    ''' in response.body) + + self.assertEqual(Notification.query().count(), 1) + notification = Notification.query().all()[0] + + self.assertEqual(notification.type_, Notification.TYPE_CHANGESET_COMMENT) + self.assertTrue((u'/vcs_test_hg/changeset/27cd5cce30c96924232df' + 'fcd24178a07ffeb5dfc#comment-1') in notification.subject) + + def test_create_with_mention(self): + self.log_user() + + rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc' + text = u'@test_regular check CommentOnRevision' + + params = {'text':text} + response = self.app.post(url(controller='changeset', action='comment', + repo_name=HG_REPO, revision=rev), + params=params) + # Test response... + self.assertEqual(response.status, '302 Found') + response.follow() + + response = self.app.get(url(controller='changeset', action='index', + repo_name=HG_REPO, revision=rev)) + # test DB + self.assertEqual(ChangesetComment.query().count(), 1) + self.assertTrue('''
    %s ''' + '''comment(s) (0 inline)
    ''' % 1 in response.body) + + + self.assertEqual(Notification.query().count(), 2) + users = [x.user.username for x in UserNotification.query().all()] + + # test_regular get's notification by @mention + self.assertEqual(sorted(users), [u'test_admin', u'test_regular']) + + def test_delete(self): + self.log_user() + rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc' + text = u'CommentOnRevision' + + params = {'text':text} + response = self.app.post(url(controller='changeset', action='comment', + repo_name=HG_REPO, revision=rev), + params=params) + + comments = ChangesetComment.query().all() + self.assertEqual(len(comments), 1) + comment_id = comments[0].comment_id + + + self.app.delete(url(controller='changeset', + action='delete_comment', + repo_name=HG_REPO, + comment_id=comment_id)) + + comments = ChangesetComment.query().all() + self.assertEqual(len(comments), 0) + + response = self.app.get(url(controller='changeset', action='index', + repo_name=HG_REPO, revision=rev)) + self.assertTrue('''
    0 comment(s)''' + ''' (0 inline)
    ''' in response.body) diff --git a/rhodecode/tests/functional/test_files.py b/rhodecode/tests/functional/test_files.py --- a/rhodecode/tests/functional/test_files.py +++ b/rhodecode/tests/functional/test_files.py @@ -6,6 +6,7 @@ ARCHIVE_SPECS = { '.zip': ('application/zip', 'zip', ''), } + class TestFilesController(TestController): def test_index(self): @@ -15,32 +16,29 @@ class TestFilesController(TestController 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' - + response.mustcontain('docs') + response.mustcontain('tests') + response.mustcontain('vcs') + response.mustcontain('.hgignore') + response.mustcontain('MANIFEST.in') 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='/')) - - + 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' - - + response.mustcontain('docs') + response.mustcontain('tests') + response.mustcontain('README.rst') + response.mustcontain('1.1 KiB') + response.mustcontain('text/x-python') def test_index_different_branch(self): self.log_user() @@ -50,11 +48,7 @@ class TestFilesController(TestController revision='97e8b885c04894463c51898e14387d80c30ed1ee', f_path='/')) - - - assert """branch: git""" in response.body, 'missing or wrong branch info' - - + response.mustcontain("""branch: git""") def test_index_paging(self): self.log_user() @@ -70,7 +64,7 @@ class TestFilesController(TestController revision=r[1], f_path='/')) - assert """@ r%s:%s""" % (r[0], r[1][:12]) in response.body, 'missing info about current revision' + response.mustcontain("""@ r%s:%s""" % (r[0], r[1][:12])) def test_file_source(self): self.log_user() @@ -80,40 +74,40 @@ class TestFilesController(TestController f_path='vcs/nodes.py')) #test or history - assert """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + response.mustcontain(""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -126,16 +120,15 @@ class TestFilesController(TestController -""" in response.body +""") - - assert """
    "Partially implemented #16. filecontent/commit message/author/node name are safe_unicode now. + response.mustcontain("""
    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 +removed extra unicode conversion in diff.
    """) - assert """branch: default""" in response.body, 'missing or wrong branch info' + response.mustcontain("""branch: default""") def test_file_annotation(self): self.log_user() @@ -144,41 +137,41 @@ removed extra unicode conversion in diff revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc', f_path='vcs/nodes.py')) - print response.body - assert """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + response.mustcontain(""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -191,11 +184,10 @@ removed extra unicode conversion in diff -""" in response.body, 'missing or wrong history in annotation' + +""") - assert """branch: default""" in response.body, 'missing or wrong branch info' - - + response.mustcontain("""branch: default""") def test_archival(self): self.log_user() @@ -260,10 +252,11 @@ removed extra unicode conversion in diff revision=rev, f_path=f_path)) - assert """Revision %r does not exist for this repository""" % (rev) in response.session['flash'][0][1], 'No flash message' - assert """%s""" % (HG_REPO) in response.session['flash'][0][1], 'No flash message' + msg = """Revision %r does not exist for this repository""" % (rev) + self.checkSessionFlash(response, msg) - + msg = """%s""" % (HG_REPO) + self.checkSessionFlash(response, msg) def test_raw_file_wrong_f_path(self): self.log_user() @@ -273,7 +266,9 @@ removed extra unicode conversion in diff repo_name=HG_REPO, revision=rev, f_path=f_path)) - assert "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12]) in response.session['flash'][0][1], 'No flash message' + + msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12]) + self.checkSessionFlash(response, msg) #========================================================================== # RAW RESPONSE - PLAIN @@ -296,10 +291,11 @@ removed extra unicode conversion in diff repo_name=HG_REPO, revision=rev, f_path=f_path)) + msg = """Revision %r does not exist for this repository""" % (rev) + self.checkSessionFlash(response, msg) - assert """Revision %r does not exist for this repository""" % (rev) in response.session['flash'][0][1], 'No flash message' - assert """%s""" % (HG_REPO) in response.session['flash'][0][1], 'No flash message' - + msg = """%s""" % (HG_REPO) + self.checkSessionFlash(response, msg) def test_raw_wrong_f_path(self): self.log_user() @@ -309,5 +305,14 @@ removed extra unicode conversion in diff repo_name=HG_REPO, revision=rev, f_path=f_path)) + msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12]) + self.checkSessionFlash(response, msg) - assert "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12]) in response.session['flash'][0][1], 'No flash message' + def test_ajaxed_files_list(self): + self.log_user() + rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc' + response = self.app.get( + url('files_nodelist_home', repo_name=HG_REPO,f_path='/',revision=rev), + extra_environ={'HTTP_X_PARTIAL_XHR': '1'}, + ) + response.mustcontain("vcs/web/simplevcs/views/repository.py") diff --git a/rhodecode/tests/functional/test_forks.py b/rhodecode/tests/functional/test_forks.py --- a/rhodecode/tests/functional/test_forks.py +++ b/rhodecode/tests/functional/test_forks.py @@ -20,10 +20,13 @@ class TestForksController(TestController fork_name = HG_FORK description = 'fork of vcs test' repo_name = HG_REPO - response = self.app.post(url(controller='settings', + org_repo = Repository.get_by_repo_name(repo_name) + response = self.app.post(url(controller='forks', action='fork_create', repo_name=repo_name), - {'fork_name':fork_name, + {'repo_name':fork_name, + 'repo_group':'', + 'fork_parent_id':org_repo.repo_id, 'repo_type':'hg', 'description':description, 'private':'False'}) @@ -39,3 +42,45 @@ class TestForksController(TestController #remove this fork response = self.app.delete(url('repo', repo_name=fork_name)) + + + + def test_z_fork_create(self): + self.log_user() + fork_name = HG_FORK + description = 'fork of vcs test' + repo_name = HG_REPO + org_repo = Repository.get_by_repo_name(repo_name) + response = self.app.post(url(controller='forks', action='fork_create', + repo_name=repo_name), + {'repo_name':fork_name, + 'repo_group':'', + 'fork_parent_id':org_repo.repo_id, + 'repo_type':'hg', + 'description':description, + 'private':'False'}) + + #test if we have a message that fork is ok + self.assertTrue('forked %s repository as %s' \ + % (repo_name, fork_name) in response.session['flash'][0]) + + #test if the fork was created in the database + fork_repo = self.Session.query(Repository)\ + .filter(Repository.repo_name == fork_name).one() + + self.assertEqual(fork_repo.repo_name, fork_name) + self.assertEqual(fork_repo.fork.repo_name, repo_name) + + + #test if fork is visible in the list ? + response = response.follow() + + + # check if fork is marked as fork + # wait for cache to expire + import time + time.sleep(10) + response = self.app.get(url(controller='summary', action='index', + repo_name=fork_name)) + + self.assertTrue('Fork of %s' % repo_name in response.body) diff --git a/rhodecode/tests/functional/test_home.py b/rhodecode/tests/functional/test_home.py --- a/rhodecode/tests/functional/test_home.py +++ b/rhodecode/tests/functional/test_home.py @@ -1,22 +1,22 @@ 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 - self.assertTrue('ADD NEW REPOSITORY' in response.body) - self.assertTrue('href="/%s/summary"' % HG_REPO in response.body) - # Test response... + response.mustcontain('ADD REPOSITORY') + response.mustcontain('href="/%s/summary"' % HG_REPO) - self.assertTrue("""""" in response.body) - self.assertTrue("""""") + response.mustcontain("""""" in response.body) - - self.assertTrue("""r173:27cd5cce30c9""" - in response.body) + """open.png"/>""") + + response.mustcontain( +"""r173:27cd5cce30c9""") diff --git a/rhodecode/tests/functional/test_journal.py b/rhodecode/tests/functional/test_journal.py --- a/rhodecode/tests/functional/test_journal.py +++ b/rhodecode/tests/functional/test_journal.py @@ -16,10 +16,10 @@ class TestJournalController(TestControll def test_stop_following_repository(self): session = self.log_user() -# usr = self.sa.query(User).filter(User.username == 'test_admin').one() -# repo = self.sa.query(Repository).filter(Repository.repo_name == HG_REPO).one() +# usr = self.Session.query(User).filter(User.username == 'test_admin').one() +# repo = self.Session.query(Repository).filter(Repository.repo_name == HG_REPO).one() # -# followings = self.sa.query(UserFollowing)\ +# followings = self.Session.query(UserFollowing)\ # .filter(UserFollowing.user == usr)\ # .filter(UserFollowing.follows_repository == repo).all() # diff --git a/rhodecode/tests/functional/test_login.py b/rhodecode/tests/functional/test_login.py --- a/rhodecode/tests/functional/test_login.py +++ b/rhodecode/tests/functional/test_login.py @@ -1,12 +1,19 @@ # -*- coding: utf-8 -*- from rhodecode.tests import * -from rhodecode.model.db import User +from rhodecode.model.db import User, Notification from rhodecode.lib import generate_api_key from rhodecode.lib.auth import check_password - +from rhodecode.model.meta import Session class TestLoginController(TestController): + def tearDown(self): + for n in Notification.query().all(): + Session.delete(n) + + Session.commit() + self.assertEqual(Notification.query().all(), []) + def test_index(self): response = self.app.get(url(controller='login', action='index')) self.assertEqual(response.status, '200 OK') @@ -17,7 +24,7 @@ class TestLoginController(TestController {'username':'test_admin', 'password':'test12'}) self.assertEqual(response.status, '302 Found') - self.assertEqual(response.session['rhodecode_user'].username , + self.assertEqual(response.session['rhodecode_user'].get('username') , 'test_admin') response = response.follow() self.assertTrue('%s repository' % HG_REPO in response.body) @@ -28,7 +35,7 @@ class TestLoginController(TestController 'password':'test12'}) self.assertEqual(response.status, '302 Found') - self.assertEqual(response.session['rhodecode_user'].username , + self.assertEqual(response.session['rhodecode_user'].get('username') , 'test_regular') response = response.follow() self.assertTrue('%s repository' % HG_REPO in response.body) @@ -192,7 +199,7 @@ class TestLoginController(TestController self.assertEqual(response.status , '302 Found') 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() + ret = self.Session.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) @@ -224,8 +231,8 @@ class TestLoginController(TestController new.name = name new.lastname = lastname new.api_key = generate_api_key(username) - self.sa.add(new) - self.sa.commit() + self.Session.add(new) + self.Session.commit() response = self.app.post(url(controller='login', action='password_reset'), @@ -247,7 +254,6 @@ class TestLoginController(TestController # GOOD KEY key = User.get_by_username(username).api_key - response = self.app.get(url(controller='login', action='password_reset_confirmation', key=key)) diff --git a/rhodecode/tests/functional/test_settings.py b/rhodecode/tests/functional/test_settings.py --- a/rhodecode/tests/functional/test_settings.py +++ b/rhodecode/tests/functional/test_settings.py @@ -8,42 +8,3 @@ class TestSettingsController(TestControl response = self.app.get(url(controller='settings', action='index', repo_name=HG_REPO)) # Test response... - - def test_fork(self): - self.log_user() - response = self.app.get(url(controller='settings', action='fork', - repo_name=HG_REPO)) - - - def test_fork_create(self): - self.log_user() - fork_name = HG_FORK - description = 'fork of 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'}) - - #test if we have a message that fork is ok - 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() - - - #check if fork is marked as fork - response = self.app.get(url(controller='summary', action='index', - repo_name=fork_name)) - - assert 'Fork of %s' % repo_name in response.body, 'no message about that this repo is a fork' diff --git a/rhodecode/tests/functional/test_summary.py b/rhodecode/tests/functional/test_summary.py --- a/rhodecode/tests/functional/test_summary.py +++ b/rhodecode/tests/functional/test_summary.py @@ -7,18 +7,22 @@ class TestSummaryController(TestControll def test_index(self): self.log_user() + ID = Repository.get_by_repo_name(HG_REPO).repo_id response = self.app.get(url(controller='summary', - action='index', repo_name=HG_REPO)) + action='index', + repo_name=HG_REPO)) #repo type - self.assertTrue("""Mercurial """ - in response.body) - self.assertTrue("""public """ - in response.body) + response.mustcontain( + """Mercurial """ + ) + response.mustcontain( + """public """ + ) #codes stats self._enable_stats() @@ -26,7 +30,6 @@ class TestSummaryController(TestControll invalidate_cache('get_repo_cached_%s' % HG_REPO) response = self.app.get(url(controller='summary', action='index', repo_name=HG_REPO)) - response.mustcontain( """var data = [["py", {"count": 42, "desc": ["Python"]}], """ """["rst", {"count": 11, "desc": ["Rst"]}], """ @@ -38,10 +41,26 @@ class TestSummaryController(TestControll ) # clone url... - response.mustcontain("""""" % HG_REPO) + response.mustcontain("""""") + response.mustcontain("""""") + + def test_index_by_id(self): + self.log_user() + ID = Repository.get_by_repo_name(HG_REPO).repo_id + response = self.app.get(url(controller='summary', + action='index', + repo_name='_%s' % ID)) + + #repo type + response.mustcontain("""Mercurial """) + response.mustcontain("""public """) def _enable_stats(self): r = Repository.get_by_repo_name(HG_REPO) r.enable_statistics = True - self.sa.add(r) - self.sa.commit() + self.Session.add(r) + self.Session.commit() diff --git a/rhodecode/tests/functional/test_tags.py b/rhodecode/tests/functional/test_tags.py --- a/rhodecode/tests/functional/test_tags.py +++ b/rhodecode/tests/functional/test_tags.py @@ -5,9 +5,8 @@ class TestTagsController(TestController) def test_index(self): self.log_user() response = self.app.get(url(controller='tags', action='index', repo_name=HG_REPO)) - assert """tip""" % HG_REPO in response.body, 'wrong info about tip tag' - assert """0.1.4""" % HG_REPO in response.body, 'wrong info about 0.1.4 tag' - assert """0.1.3""" % HG_REPO in response.body, 'wrong info about 0.1.3 tag' - assert """0.1.2""" % HG_REPO in response.body, 'wrong info about 0.1.2 tag' - assert """0.1.1""" % HG_REPO in response.body, 'wrong info about 0.1.1 tag' - # Test response... + response.mustcontain("""tip""" % HG_REPO) + response.mustcontain("""0.1.4""" % HG_REPO) + response.mustcontain("""0.1.3""" % HG_REPO) + response.mustcontain("""0.1.2""" % HG_REPO) + response.mustcontain("""0.1.1""" % HG_REPO) diff --git a/rhodecode/tests/rhodecode_crawler.py b/rhodecode/tests/rhodecode_crawler.py --- a/rhodecode/tests/rhodecode_crawler.py +++ b/rhodecode/tests/rhodecode_crawler.py @@ -6,12 +6,12 @@ Test for crawling a project for memory usage This should be runned just as regular script together with a watch script that will show memory usage. - + watch -n1 ./rhodecode/tests/mem_watch :created_on: Apr 21, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -31,11 +31,10 @@ import cookielib import urllib import urllib2 -import vcs import time from os.path import join as jn - +from rhodecode.lib import vcs BASE_URI = 'http://127.0.0.1:5000/%s' PROJECT = 'CPython' @@ -52,7 +51,6 @@ o.addheaders = [ urllib2.install_opener(o) - def test_changelog_walk(pages=100): total_time = 0 for i in range(1, pages): @@ -67,7 +65,6 @@ def test_changelog_walk(pages=100): total_time += e print 'visited %s size:%s req:%s ms' % (full_uri, size, e) - print 'total_time', total_time print 'average on req', total_time / float(pages) @@ -103,6 +100,7 @@ def test_files_walk(limit=100): repo = vcs.get_repo(jn(PROJECT_PATH, PROJECT)) from rhodecode.lib.compat import OrderedSet + from rhodecode.lib.vcs.exceptions import RepositoryError paths_ = OrderedSet(['']) try: @@ -117,7 +115,7 @@ def test_files_walk(limit=100): for f in files: paths_.add(f.path) - except vcs.exception.RepositoryError, e: + except RepositoryError, e: pass cnt = 0 @@ -140,7 +138,6 @@ def test_files_walk(limit=100): print 'average on req', total_time / float(cnt) - test_changelog_walk(40) time.sleep(2) test_changeset_walk(limit=100) diff --git a/rhodecode/tests/test_hg_operations.py b/rhodecode/tests/test_hg_operations.py --- a/rhodecode/tests/test_hg_operations.py +++ b/rhodecode/tests/test_hg_operations.py @@ -6,7 +6,8 @@ Test suite for making push/pull operations :created_on: Dec 30, 2010 - :copyright: (C) 2009-2011 Marcin Kuzminski + :author: marcink + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -184,7 +185,6 @@ def test_clone_with_credentials(no_error if anonymous_access: print '\tenabled, disabling it ' set_anonymous_access(enable=False) - time.sleep(1) clone_url = 'http://%(user)s:%(pass)s@%(host)s/%(cloned_repo)s %(dest)s' % \ {'user':USER, @@ -217,7 +217,6 @@ def test_clone_anonymous(): if not anonymous_access: print '\tnot enabled, enabling it ' set_anonymous_access(enable=True) - time.sleep(1) clone_url = 'http://%(host)s/%(cloned_repo)s %(dest)s' % \ {'user':USER, @@ -386,7 +385,7 @@ if __name__ == '__main__': initial_logs = get_logs() print 'initial activity logs: %s' % len(initial_logs) - + s = time.time() #test_push_modify_file() test_clone_with_credentials() test_clone_wrong_credentials() @@ -399,3 +398,4 @@ if __name__ == '__main__': test_push_wrong_credentials() test_logs(initial_logs) + print 'finished ok in %.3f' % (time.time() - s) diff --git a/rhodecode/tests/test_libs.py b/rhodecode/tests/test_libs.py --- a/rhodecode/tests/test_libs.py +++ b/rhodecode/tests/test_libs.py @@ -5,9 +5,9 @@ Package for testing various lib/helper functions in rhodecode - + :created_on: Jun 9, 2011 - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2011-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -103,3 +103,12 @@ class TestLibs(unittest.TestCase): for case in test_cases: self.assertEqual(str2bool(case[0]), case[1]) + + def test_mention_extractor(self): + from rhodecode.lib import extract_mentioned_users + sample = ("@first hi there @marcink here's my email marcin@email.com " + "@lukaszb check it pls @ ttwelve @D[] @one@two@three " + "@MARCIN @maRCiN @2one_more22") + s = ['2one_more22', 'D', 'MARCIN', 'first', 'lukaszb', + 'maRCiN', 'marcink', 'one'] + self.assertEqual(s, extract_mentioned_users(sample)) diff --git a/rhodecode/tests/test_models.py b/rhodecode/tests/test_models.py --- a/rhodecode/tests/test_models.py +++ b/rhodecode/tests/test_models.py @@ -4,15 +4,35 @@ from rhodecode.tests import * from rhodecode.model.repos_group import ReposGroupModel from rhodecode.model.repo import RepoModel -from rhodecode.model.db import Group, User +from rhodecode.model.db import RepoGroup, User, Notification, UserNotification, \ + UsersGroup, UsersGroupMember, Permission from sqlalchemy.exc import IntegrityError +from rhodecode.model.user import UserModel + +from rhodecode.model.meta import Session +from rhodecode.model.notification import NotificationModel +from rhodecode.model.users_group import UsersGroupModel +from rhodecode.lib.auth import AuthUser + + +def _make_group(path, desc='desc', parent_id=None, + skip_if_exists=False): + + gr = RepoGroup.get_by_group_name(path) + if gr and skip_if_exists: + return gr + + gr = ReposGroupModel().create(path, desc, parent_id) + Session.commit() + return gr + class TestReposGroups(unittest.TestCase): def setUp(self): - self.g1 = self.__make_group('test1', skip_if_exists=True) - self.g2 = self.__make_group('test2', skip_if_exists=True) - self.g3 = self.__make_group('test3', skip_if_exists=True) + self.g1 = _make_group('test1', skip_if_exists=True) + self.g2 = _make_group('test2', skip_if_exists=True) + self.g3 = _make_group('test3', skip_if_exists=True) def tearDown(self): print 'out' @@ -25,101 +45,81 @@ class TestReposGroups(unittest.TestCase) def _check_folders(self): print os.listdir(TESTS_TMP_PATH) - def __make_group(self, path, desc='desc', parent_id=None, - skip_if_exists=False): - - gr = Group.get_by_group_name(path) - if gr and skip_if_exists: - return gr - - form_data = dict(group_name=path, - group_description=desc, - group_parent_id=parent_id) - gr = ReposGroupModel().create(form_data) - return gr - def __delete_group(self, id_): ReposGroupModel().delete(id_) - def __update_group(self, id_, path, desc='desc', parent_id=None): form_data = dict(group_name=path, group_description=desc, - group_parent_id=parent_id) + group_parent_id=parent_id, + perms_updates=[], + perms_new=[]) gr = ReposGroupModel().update(id_, form_data) return gr def test_create_group(self): - g = self.__make_group('newGroup') + g = _make_group('newGroup') self.assertEqual(g.full_path, 'newGroup') self.assertTrue(self.__check_path('newGroup')) - def test_create_same_name_group(self): - self.assertRaises(IntegrityError, lambda:self.__make_group('newGroup')) - + self.assertRaises(IntegrityError, lambda:_make_group('newGroup')) + Session.rollback() def test_same_subgroup(self): - sg1 = self.__make_group('sub1', parent_id=self.g1.group_id) + sg1 = _make_group('sub1', parent_id=self.g1.group_id) self.assertEqual(sg1.parent_group, self.g1) self.assertEqual(sg1.full_path, 'test1/sub1') self.assertTrue(self.__check_path('test1', 'sub1')) - ssg1 = self.__make_group('subsub1', parent_id=sg1.group_id) + ssg1 = _make_group('subsub1', parent_id=sg1.group_id) self.assertEqual(ssg1.parent_group, sg1) self.assertEqual(ssg1.full_path, 'test1/sub1/subsub1') self.assertTrue(self.__check_path('test1', 'sub1', 'subsub1')) - def test_remove_group(self): - sg1 = self.__make_group('deleteme') + sg1 = _make_group('deleteme') self.__delete_group(sg1.group_id) - self.assertEqual(Group.get(sg1.group_id), None) + self.assertEqual(RepoGroup.get(sg1.group_id), None) self.assertFalse(self.__check_path('deteteme')) - sg1 = self.__make_group('deleteme', parent_id=self.g1.group_id) + sg1 = _make_group('deleteme', parent_id=self.g1.group_id) self.__delete_group(sg1.group_id) - self.assertEqual(Group.get(sg1.group_id), None) + self.assertEqual(RepoGroup.get(sg1.group_id), None) self.assertFalse(self.__check_path('test1', 'deteteme')) - def test_rename_single_group(self): - sg1 = self.__make_group('initial') + sg1 = _make_group('initial') new_sg1 = self.__update_group(sg1.group_id, 'after') self.assertTrue(self.__check_path('after')) - self.assertEqual(Group.get_by_group_name('initial'), None) - + self.assertEqual(RepoGroup.get_by_group_name('initial'), None) def test_update_group_parent(self): - sg1 = self.__make_group('initial', parent_id=self.g1.group_id) + sg1 = _make_group('initial', parent_id=self.g1.group_id) new_sg1 = self.__update_group(sg1.group_id, 'after', parent_id=self.g1.group_id) self.assertTrue(self.__check_path('test1', 'after')) - self.assertEqual(Group.get_by_group_name('test1/initial'), None) - + self.assertEqual(RepoGroup.get_by_group_name('test1/initial'), None) new_sg1 = self.__update_group(sg1.group_id, 'after', parent_id=self.g3.group_id) self.assertTrue(self.__check_path('test3', 'after')) - self.assertEqual(Group.get_by_group_name('test3/initial'), None) - + self.assertEqual(RepoGroup.get_by_group_name('test3/initial'), None) new_sg1 = self.__update_group(sg1.group_id, 'hello') self.assertTrue(self.__check_path('hello')) - self.assertEqual(Group.get_by_group_name('hello'), new_sg1) - - + self.assertEqual(RepoGroup.get_by_group_name('hello'), new_sg1) def test_subgrouping_with_repo(self): - g1 = self.__make_group('g1') - g2 = self.__make_group('g2') + g1 = _make_group('g1') + g2 = _make_group('g2') # create new repo form_data = dict(repo_name='john', @@ -143,7 +143,6 @@ class TestReposGroups(unittest.TestCase) RepoModel().update(r.repo_name, form_data) self.assertEqual(r.repo_name, 'g1/john') - self.__update_group(g1.group_id, 'g1', parent_id=g2.group_id) self.assertTrue(self.__check_path('g2', 'g1')) @@ -151,3 +150,406 @@ class TestReposGroups(unittest.TestCase) self.assertEqual(r.repo_name, os.path.join('g2', 'g1', r.just_name)) +class TestUser(unittest.TestCase): + def __init__(self, methodName='runTest'): + Session.remove() + super(TestUser, self).__init__(methodName=methodName) + + def test_create_and_remove(self): + usr = UserModel().create_or_update(username=u'test_user', password=u'qweqwe', + email=u'u232@rhodecode.org', + name=u'u1', lastname=u'u1') + Session.commit() + self.assertEqual(User.get_by_username(u'test_user'), usr) + + # make users group + users_group = UsersGroupModel().create('some_example_group') + Session.commit() + + UsersGroupModel().add_user_to_group(users_group, usr) + Session.commit() + + self.assertEqual(UsersGroup.get(users_group.users_group_id), users_group) + self.assertEqual(UsersGroupMember.query().count(), 1) + UserModel().delete(usr.user_id) + Session.commit() + + self.assertEqual(UsersGroupMember.query().all(), []) + + +class TestNotifications(unittest.TestCase): + + def __init__(self, methodName='runTest'): + Session.remove() + self.u1 = UserModel().create_or_update(username=u'u1', + password=u'qweqwe', + email=u'u1@rhodecode.org', + name=u'u1', lastname=u'u1') + Session.commit() + self.u1 = self.u1.user_id + + self.u2 = UserModel().create_or_update(username=u'u2', + password=u'qweqwe', + email=u'u2@rhodecode.org', + name=u'u2', lastname=u'u3') + Session.commit() + self.u2 = self.u2.user_id + + self.u3 = UserModel().create_or_update(username=u'u3', + password=u'qweqwe', + email=u'u3@rhodecode.org', + name=u'u3', lastname=u'u3') + Session.commit() + self.u3 = self.u3.user_id + + super(TestNotifications, self).__init__(methodName=methodName) + + def _clean_notifications(self): + for n in Notification.query().all(): + Session.delete(n) + + Session.commit() + self.assertEqual(Notification.query().all(), []) + + def tearDown(self): + self._clean_notifications() + + def test_create_notification(self): + self.assertEqual([], Notification.query().all()) + self.assertEqual([], UserNotification.query().all()) + + usrs = [self.u1, self.u2] + notification = NotificationModel().create(created_by=self.u1, + subject=u'subj', body=u'hi there', + recipients=usrs) + Session.commit() + u1 = User.get(self.u1) + u2 = User.get(self.u2) + u3 = User.get(self.u3) + notifications = Notification.query().all() + self.assertEqual(len(notifications), 1) + + unotification = UserNotification.query()\ + .filter(UserNotification.notification == notification).all() + + self.assertEqual(notifications[0].recipients, [u1, u2]) + self.assertEqual(notification.notification_id, + notifications[0].notification_id) + self.assertEqual(len(unotification), len(usrs)) + self.assertEqual([x.user.user_id for x in unotification], usrs) + + def test_user_notifications(self): + self.assertEqual([], Notification.query().all()) + self.assertEqual([], UserNotification.query().all()) + + notification1 = NotificationModel().create(created_by=self.u1, + subject=u'subj', body=u'hi there1', + recipients=[self.u3]) + Session.commit() + notification2 = NotificationModel().create(created_by=self.u1, + subject=u'subj', body=u'hi there2', + recipients=[self.u3]) + Session.commit() + u3 = Session.query(User).get(self.u3) + + self.assertEqual(sorted([x.notification for x in u3.notifications]), + sorted([notification2, notification1])) + + def test_delete_notifications(self): + self.assertEqual([], Notification.query().all()) + self.assertEqual([], UserNotification.query().all()) + + notification = NotificationModel().create(created_by=self.u1, + subject=u'title', body=u'hi there3', + recipients=[self.u3, self.u1, self.u2]) + Session.commit() + notifications = Notification.query().all() + self.assertTrue(notification in notifications) + + Notification.delete(notification.notification_id) + Session.commit() + + notifications = Notification.query().all() + self.assertFalse(notification in notifications) + + un = UserNotification.query().filter(UserNotification.notification + == notification).all() + self.assertEqual(un, []) + + def test_delete_association(self): + + self.assertEqual([], Notification.query().all()) + self.assertEqual([], UserNotification.query().all()) + + notification = NotificationModel().create(created_by=self.u1, + subject=u'title', body=u'hi there3', + recipients=[self.u3, self.u1, self.u2]) + Session.commit() + + unotification = UserNotification.query()\ + .filter(UserNotification.notification == + notification)\ + .filter(UserNotification.user_id == self.u3)\ + .scalar() + + self.assertEqual(unotification.user_id, self.u3) + + NotificationModel().delete(self.u3, + notification.notification_id) + Session.commit() + + u3notification = UserNotification.query()\ + .filter(UserNotification.notification == + notification)\ + .filter(UserNotification.user_id == self.u3)\ + .scalar() + + self.assertEqual(u3notification, None) + + # notification object is still there + self.assertEqual(Notification.query().all(), [notification]) + + #u1 and u2 still have assignments + u1notification = UserNotification.query()\ + .filter(UserNotification.notification == + notification)\ + .filter(UserNotification.user_id == self.u1)\ + .scalar() + self.assertNotEqual(u1notification, None) + u2notification = UserNotification.query()\ + .filter(UserNotification.notification == + notification)\ + .filter(UserNotification.user_id == self.u2)\ + .scalar() + self.assertNotEqual(u2notification, None) + + def test_notification_counter(self): + self._clean_notifications() + self.assertEqual([], Notification.query().all()) + self.assertEqual([], UserNotification.query().all()) + + NotificationModel().create(created_by=self.u1, + subject=u'title', body=u'hi there_delete', + recipients=[self.u3, self.u1]) + Session.commit() + + self.assertEqual(NotificationModel() + .get_unread_cnt_for_user(self.u1), 1) + self.assertEqual(NotificationModel() + .get_unread_cnt_for_user(self.u2), 0) + self.assertEqual(NotificationModel() + .get_unread_cnt_for_user(self.u3), 1) + + notification = NotificationModel().create(created_by=self.u1, + subject=u'title', body=u'hi there3', + recipients=[self.u3, self.u1, self.u2]) + Session.commit() + + self.assertEqual(NotificationModel() + .get_unread_cnt_for_user(self.u1), 2) + self.assertEqual(NotificationModel() + .get_unread_cnt_for_user(self.u2), 1) + self.assertEqual(NotificationModel() + .get_unread_cnt_for_user(self.u3), 2) + + +class TestUsers(unittest.TestCase): + + def __init__(self, methodName='runTest'): + super(TestUsers, self).__init__(methodName=methodName) + + def setUp(self): + self.u1 = UserModel().create_or_update(username=u'u1', + password=u'qweqwe', + email=u'u1@rhodecode.org', + name=u'u1', lastname=u'u1') + + def tearDown(self): + perm = Permission.query().all() + for p in perm: + UserModel().revoke_perm(self.u1, p) + + UserModel().delete(self.u1) + Session.commit() + + def test_add_perm(self): + perm = Permission.query().all()[0] + UserModel().grant_perm(self.u1, perm) + Session.commit() + self.assertEqual(UserModel().has_perm(self.u1, perm), True) + + def test_has_perm(self): + perm = Permission.query().all() + for p in perm: + has_p = UserModel().has_perm(self.u1, p) + self.assertEqual(False, has_p) + + def test_revoke_perm(self): + perm = Permission.query().all()[0] + UserModel().grant_perm(self.u1, perm) + Session.commit() + self.assertEqual(UserModel().has_perm(self.u1, perm), True) + + #revoke + UserModel().revoke_perm(self.u1, perm) + Session.commit() + self.assertEqual(UserModel().has_perm(self.u1, perm), False) + + +class TestPermissions(unittest.TestCase): + def __init__(self, methodName='runTest'): + super(TestPermissions, self).__init__(methodName=methodName) + + def setUp(self): + self.u1 = UserModel().create_or_update( + username=u'u1', password=u'qweqwe', + email=u'u1@rhodecode.org', name=u'u1', lastname=u'u1' + ) + self.a1 = UserModel().create_or_update( + username=u'a1', password=u'qweqwe', + email=u'a1@rhodecode.org', name=u'a1', lastname=u'a1', admin=True + ) + Session.commit() + + def tearDown(self): + UserModel().delete(self.u1) + UserModel().delete(self.a1) + if hasattr(self, 'g1'): + ReposGroupModel().delete(self.g1.group_id) + if hasattr(self, 'g2'): + ReposGroupModel().delete(self.g2.group_id) + + if hasattr(self, 'ug1'): + UsersGroupModel().delete(self.ug1, force=True) + + Session.commit() + + def test_default_perms_set(self): + u1_auth = AuthUser(user_id=self.u1.user_id) + perms = { + 'repositories_groups': {}, + 'global': set([u'hg.create.repository', u'repository.read', + u'hg.register.manual_activate']), + 'repositories': {u'vcs_test_hg': u'repository.read'} + } + self.assertEqual(u1_auth.permissions['repositories'][HG_REPO], + perms['repositories'][HG_REPO]) + new_perm = 'repository.write' + RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1, perm=new_perm) + Session.commit() + + u1_auth = AuthUser(user_id=self.u1.user_id) + self.assertEqual(u1_auth.permissions['repositories'][HG_REPO], new_perm) + + def test_default_admin_perms_set(self): + a1_auth = AuthUser(user_id=self.a1.user_id) + perms = { + 'repositories_groups': {}, + 'global': set([u'hg.admin']), + 'repositories': {u'vcs_test_hg': u'repository.admin'} + } + self.assertEqual(a1_auth.permissions['repositories'][HG_REPO], + perms['repositories'][HG_REPO]) + new_perm = 'repository.write' + RepoModel().grant_user_permission(repo=HG_REPO, user=self.a1, perm=new_perm) + Session.commit() + # cannot really downgrade admins permissions !? they still get's set as + # admin ! + u1_auth = AuthUser(user_id=self.a1.user_id) + self.assertEqual(u1_auth.permissions['repositories'][HG_REPO], + perms['repositories'][HG_REPO]) + + def test_default_group_perms(self): + self.g1 = _make_group('test1', skip_if_exists=True) + self.g2 = _make_group('test2', skip_if_exists=True) + u1_auth = AuthUser(user_id=self.u1.user_id) + perms = { + 'repositories_groups': {u'test1': 'group.read', u'test2': 'group.read'}, + 'global': set([u'hg.create.repository', u'repository.read', u'hg.register.manual_activate']), + 'repositories': {u'vcs_test_hg': u'repository.read'} + } + self.assertEqual(u1_auth.permissions['repositories'][HG_REPO], + perms['repositories'][HG_REPO]) + self.assertEqual(u1_auth.permissions['repositories_groups'], + perms['repositories_groups']) + + def test_default_admin_group_perms(self): + self.g1 = _make_group('test1', skip_if_exists=True) + self.g2 = _make_group('test2', skip_if_exists=True) + a1_auth = AuthUser(user_id=self.a1.user_id) + perms = { + 'repositories_groups': {u'test1': 'group.admin', u'test2': 'group.admin'}, + 'global': set(['hg.admin']), + 'repositories': {u'vcs_test_hg': 'repository.admin'} + } + + self.assertEqual(a1_auth.permissions['repositories'][HG_REPO], + perms['repositories'][HG_REPO]) + self.assertEqual(a1_auth.permissions['repositories_groups'], + perms['repositories_groups']) + + def test_propagated_permission_from_users_group(self): + # make group + self.ug1 = UsersGroupModel().create('G1') + # add user to group + UsersGroupModel().add_user_to_group(self.ug1, self.u1) + + # set permission to lower + new_perm = 'repository.none' + RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1, perm=new_perm) + Session.commit() + u1_auth = AuthUser(user_id=self.u1.user_id) + self.assertEqual(u1_auth.permissions['repositories'][HG_REPO], + new_perm) + + # grant perm for group this should override permission from user + new_perm = 'repository.write' + RepoModel().grant_users_group_permission(repo=HG_REPO, + group_name=self.ug1, + perm=new_perm) + # check perms + u1_auth = AuthUser(user_id=self.u1.user_id) + perms = { + 'repositories_groups': {}, + 'global': set([u'hg.create.repository', u'repository.read', + u'hg.register.manual_activate']), + 'repositories': {u'vcs_test_hg': u'repository.read'} + } + self.assertEqual(u1_auth.permissions['repositories'][HG_REPO], + new_perm) + self.assertEqual(u1_auth.permissions['repositories_groups'], + perms['repositories_groups']) + + def test_propagated_permission_from_users_group_lower_weight(self): + # make group + self.ug1 = UsersGroupModel().create('G1') + # add user to group + UsersGroupModel().add_user_to_group(self.ug1, self.u1) + + # set permission to lower + new_perm_h = 'repository.write' + RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1, + perm=new_perm_h) + Session.commit() + u1_auth = AuthUser(user_id=self.u1.user_id) + self.assertEqual(u1_auth.permissions['repositories'][HG_REPO], + new_perm_h) + + # grant perm for group this should NOT override permission from user + # since it's lower than granted + new_perm_l = 'repository.read' + RepoModel().grant_users_group_permission(repo=HG_REPO, + group_name=self.ug1, + perm=new_perm_l) + # check perms + u1_auth = AuthUser(user_id=self.u1.user_id) + perms = { + 'repositories_groups': {}, + 'global': set([u'hg.create.repository', u'repository.read', + u'hg.register.manual_activate']), + 'repositories': {u'vcs_test_hg': u'repository.write'} + } + self.assertEqual(u1_auth.permissions['repositories'][HG_REPO], + new_perm_h) + self.assertEqual(u1_auth.permissions['repositories_groups'], + perms['repositories_groups']) diff --git a/rhodecode/websetup.py b/rhodecode/websetup.py --- a/rhodecode/websetup.py +++ b/rhodecode/websetup.py @@ -7,7 +7,7 @@ :created_on: Dec 11, 2010 :author: marcink - :copyright: (C) 2009-2011 Marcin Kuzminski + :copyright: (C) 2010-2012 Marcin Kuzminski :license: GPLv3, see COPYING for more details. """ # This program is free software: you can redistribute it and/or modify @@ -23,11 +23,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os import logging from rhodecode.config.environment import load_environment from rhodecode.lib.db_manage import DbManage +from rhodecode.model.meta import Session log = logging.getLogger(__name__) @@ -45,5 +45,5 @@ def setup_app(command, conf, vars): dbmanage.admin_prompt() dbmanage.create_permissions() dbmanage.populate_default_permissions() - + Session.commit() load_environment(conf.global_conf, conf.local_conf, initial=True) diff --git a/setup.cfg b/setup.cfg --- a/setup.cfg +++ b/setup.cfg @@ -11,6 +11,8 @@ verbosity=2 with-pylons=test.ini detailed-errors=1 nologcapture=1 +#pdb=1 +#pdb-failures=1 # Babel configuration [compile_catalog] diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -7,20 +7,21 @@ from rhodecode import requirements if __py_version__ < (2, 5): raise Exception('RhodeCode requires python 2.5 or later') - dependency_links = [ ] -classifiers = ['Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Framework :: Pylons', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU General Public License (GPL)', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.5', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', ] +classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Web Environment', + 'Framework :: Pylons', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', +] # additional files from project that goes somewhere in the filesystem diff --git a/test.ini b/test.ini --- a/test.ini +++ b/test.ini @@ -51,6 +51,8 @@ cut_off_limit = 256000 force_https = false commit_parse_limit = 25 use_gravatar = true +container_auth_enabled = false +proxypass_auth_enabled = false #################################### ### CELERY CONFIG #### @@ -87,22 +89,28 @@ beaker.cache.lock_dir=/tmp/data/cache/lo beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long beaker.cache.super_short_term.type=memory -beaker.cache.super_short_term.expire=10 +beaker.cache.super_short_term.expire=1 +beaker.cache.super_short_term.key_length = 256 beaker.cache.short_term.type=memory beaker.cache.short_term.expire=60 +beaker.cache.short_term.key_length = 256 beaker.cache.long_term.type=memory beaker.cache.long_term.expire=36000 +beaker.cache.long_term.key_length = 256 beaker.cache.sql_cache_short.type=memory -beaker.cache.sql_cache_short.expire=10 +beaker.cache.sql_cache_short.expire=1 +beaker.cache.sql_cache_short.key_length = 256 beaker.cache.sql_cache_med.type=memory beaker.cache.sql_cache_med.expire=360 +beaker.cache.sql_cache_med.key_length = 256 beaker.cache.sql_cache_long.type=file beaker.cache.sql_cache_long.expire=3600 +beaker.cache.sql_cache_long.key_length = 256 #################################### ### BEAKER SESSION #### @@ -143,7 +151,7 @@ logview.pylons.util = #eee ######################################################### sqlalchemy.db1.url = sqlite:///%(here)s/test.db #sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode_tests -#sqlalchemy.db1.echo = False +#sqlalchemy.db1.echo = false #sqlalchemy.db1.pool_recycle = 3600 sqlalchemy.convert_unicode = true