[hg] galaxy 3647: Framework for the Galaxy Community Space.
details: http://www.bx.psu.edu/hg/galaxy/rev/f905e1415dd4 changeset: 3647:f905e1415dd4 user: Greg Von Kuster <greg@bx.psu.edu> date: Wed Apr 14 10:20:32 2010 -0400 description: Framework for the Galaxy Community Space. diffstat: community_wsgi.ini.sample | 71 +++ lib/galaxy/tags/tag_handler.py | 5 + lib/galaxy/webapps/community/__init__.py | 3 + lib/galaxy/webapps/community/app.py | 34 + lib/galaxy/webapps/community/base/controller.py | 24 + lib/galaxy/webapps/community/buildapp.py | 198 ++++++++++ lib/galaxy/webapps/community/config.py | 153 +++++++ lib/galaxy/webapps/community/controllers/__init__.py | 1 + lib/galaxy/webapps/community/controllers/tool_browser.py | 100 +++++ lib/galaxy/webapps/community/model/__init__.py | 187 +++++++++ lib/galaxy/webapps/community/model/mapping.py | 186 +++++++++ lib/galaxy/webapps/community/model/migrate/check.py | 105 +++++ lib/galaxy/webapps/community/model/migrate/migrate.cfg | 20 + lib/galaxy/webapps/community/model/migrate/versions/0001_initial_tables.py | 101 +++++ lib/galaxy/webapps/community/security/__init__.py | 96 ++++ run_community.sh | 4 + setup.sh | 1 + templates/webapps/community/base_panels.mako | 102 +++++ templates/webapps/community/index.mako | 57 ++ templates/webapps/community/message.mako | 1 + templates/webapps/community/tool/browse_tool.mako | 37 + templates/webapps/community/tool/grid.mako | 1 + 22 files changed, 1487 insertions(+), 0 deletions(-) diffs (1587 lines): diff -r 4c740255b9e7 -r f905e1415dd4 community_wsgi.ini.sample --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/community_wsgi.ini.sample Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,71 @@ +# ---- HTTP Server ---------------------------------------------------------- + +[server:main] + +use = egg:Paste#http +port = 9009 +host = 127.0.0.1 +use_threadpool = true +threadpool_workers = 10 + +# ---- Galaxy Webapps Community Interface ------------------------------------------------- + +[app:main] + +# Specifies the factory for the universe WSGI application +paste.app_factory = galaxy.webapps.community.buildapp:app_factory +log_level = DEBUG + +# Database connection +database_file = database/universe.sqlite +# You may use a SQLAlchemy connection string to specify an external database instead +#database_connection = postgres:///community_test?host=/var/run/postgresql + +# Where dataset files are saved +file_path = database/files +# Temporary storage for additional datasets, this should be shared through the cluster +new_file_path = database/tmp + +# Where templates are stored +template_path = lib/galaxy/webapps/community/templates + +# Session support (beaker) +use_beaker_session = True +session_type = memory +session_data_dir = %(here)s/database/beaker_sessions +session_key = galaxysessions +session_secret = changethisinproduction + +# Galaxy session security +id_secret = changethisinproductiontoo + +# Configuration for debugging middleware +debug = true +use_lint = false + +# NEVER enable this on a public site (even test or QA) +# use_interactive = true + +# Force everyone to log in (disable anonymous access) +require_login = False + +# path to sendmail +sendmail_path = /usr/sbin/sendmail + +# Write thread status periodically to 'heartbeat.log' (careful, uses disk space rapidly!) +## use_heartbeat = True + +# Profiling middleware (cProfile based) +## use_profile = True + +# Use the new iframe / javascript based layout +use_new_layout = true + +# Serving static files (needed if running standalone) +static_enabled = True +static_cache_time = 360 +static_dir = %(here)s/static/ +static_images_dir = %(here)s/static/images +static_favicon_dir = %(here)s/static/favicon.ico +static_scripts_dir = %(here)s/static/scripts/ +static_style_dir = %(here)s/static/june_2007_style/blue diff -r 4c740255b9e7 -r f905e1415dd4 lib/galaxy/tags/tag_handler.py --- a/lib/galaxy/tags/tag_handler.py Wed Apr 14 10:14:43 2010 -0400 +++ b/lib/galaxy/tags/tag_handler.py Wed Apr 14 10:20:32 2010 -0400 @@ -258,3 +258,8 @@ self.item_tag_assoc_info["Visualization"] = ItemTagAssocInfo( model.Visualization, model.VisualizationTagAssociation, model.VisualizationTagAssociation.table.c.visualization_id ) + +class CommunityTagHandler( TagHandler ): + def __init__( self ): + from galaxy.webapps.community import model + TagHandler.__init__( self ) diff -r 4c740255b9e7 -r f905e1415dd4 lib/galaxy/webapps/community/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/webapps/community/__init__.py Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,3 @@ +"""The Galaxy Reports application.""" + +from galaxy.web.framework import expose, url_for \ No newline at end of file diff -r 4c740255b9e7 -r f905e1415dd4 lib/galaxy/webapps/community/app.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/webapps/community/app.py Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,34 @@ +import sys, config +import galaxy.webapps.community.model +from galaxy.web import security +from galaxy.tags.tag_handler import CommunityTagHandler + +class UniverseApplication( object ): + """Encapsulates the state of a Universe application""" + def __init__( self, **kwargs ): + print >> sys.stderr, "python path is: " + ", ".join( sys.path ) + # Read config file and check for errors + self.config = config.Configuration( **kwargs ) + self.config.check() + config.configure_logging( self.config ) + # Determine the database url + if self.config.database_connection: + db_url = self.config.database_connection + else: + db_url = "sqlite://%s?isolation_level=IMMEDIATE" % self.config.database + # Initialize database / check for appropriate schema version + from galaxy.webapps.community.model.migrate.check import create_or_verify_database + create_or_verify_database( db_url, self.config.database_engine_options ) + # Setup the database engine and ORM + from galaxy.webapps.community.model import mapping + self.model = mapping.init( self.config.file_path, + db_url, + self.config.database_engine_options ) + # Security helper + self.security = security.SecurityHelper( id_secret=self.config.id_secret ) + # Tag handler + self.tag_handler = CommunityTagHandler() + # Load security policy + self.security_agent = self.model.security_agent + def shutdown( self ): + pass diff -r 4c740255b9e7 -r f905e1415dd4 lib/galaxy/webapps/community/base/controller.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/webapps/community/base/controller.py Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,24 @@ +"""Contains functionality needed in every webapp interface""" +import os, time, logging +# Pieces of Galaxy to make global in every controller +from galaxy import config, tools, web, util +from galaxy.web import error, form, url_for +from galaxy.webapps.community import model +from galaxy.model.orm import * + +from Cheetah.Template import Template + +log = logging.getLogger( __name__ ) + +class BaseController( object ): + """Base class for Galaxy webapp application controllers.""" + def __init__( self, app ): + """Initialize an interface for application 'app'""" + self.app = app + def get_class( self, class_name ): + """ Returns the class object that a string denotes. Without this method, we'd have to do eval(<class_name>). """ + if class_name == 'Tool': + item_class = model.Tool + else: + item_class = None + return item_class \ No newline at end of file diff -r 4c740255b9e7 -r f905e1415dd4 lib/galaxy/webapps/community/buildapp.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/webapps/community/buildapp.py Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,198 @@ +""" +Provides factory methods to assemble the Galaxy web application +""" + +import logging, atexit +import os, os.path, sys + +from inspect import isclass + +from paste.request import parse_formvars +from paste.util import import_string +from paste import httpexceptions +from paste.deploy.converters import asbool +import pkg_resources + +log = logging.getLogger( __name__ ) + +import config +import galaxy.webapps.community.model +import galaxy.webapps.community.model.mapping +import galaxy.web.framework + +def add_controllers( webapp, app ): + """ + Search for controllers in the 'galaxy.webapps.controllers' module and add + them to the webapp. + """ + from galaxy.webapps.community.base.controller import BaseController + import galaxy.webapps.community.controllers + controller_dir = galaxy.webapps.community.controllers.__path__[0] + for fname in os.listdir( controller_dir ): + if not fname.startswith( "_" ) and fname.endswith( ".py" ): + name = fname[:-3] + module_name = "galaxy.webapps.community.controllers." + name + module = __import__( module_name ) + for comp in module_name.split( "." )[1:]: + module = getattr( module, comp ) + # Look for a controller inside the modules + for key in dir( module ): + T = getattr( module, key ) + if isclass( T ) and T is not BaseController and issubclass( T, BaseController ): + webapp.add_controller( name, T( app ) ) + from galaxy.web.base.controller import BaseController + import galaxy.web.controllers + controller_dir = galaxy.web.controllers.__path__[0] + for fname in os.listdir( controller_dir ): + # TODO: fix this if we decide to use, we don't need to inspect all controllers... + if fname.startswith( 'user' ) and fname.endswith( ".py" ): + name = fname[:-3] + module_name = "galaxy.web.controllers." + name + module = __import__( module_name ) + for comp in module_name.split( "." )[1:]: + module = getattr( module, comp ) + # Look for a controller inside the modules + for key in dir( module ): + T = getattr( module, key ) + if isclass( T ) and T is not BaseController and issubclass( T, BaseController ): + webapp.add_controller( name, T( app ) ) + +def app_factory( global_conf, **kwargs ): + """Return a wsgi application serving the root object""" + # Create the Galaxy application unless passed in + if 'app' in kwargs: + app = kwargs.pop( 'app' ) + else: + try: + from galaxy.webapps.community.app import UniverseApplication + app = UniverseApplication( global_conf = global_conf, **kwargs ) + except: + import traceback, sys + traceback.print_exc() + sys.exit( 1 ) + atexit.register( app.shutdown ) + # Create the universe WSGI application + webapp = galaxy.web.framework.WebApplication( app, session_cookie='galaxycommunitysession' ) + add_controllers( webapp, app ) + # These two routes handle our simple needs at the moment + webapp.add_route( '/:controller/:action', action='index' ) + webapp.add_route( '/:action', controller='tool_browser', action='index' ) + webapp.finalize_config() + # Wrap the webapp in some useful middleware + if kwargs.get( 'middleware', True ): + webapp = wrap_in_middleware( webapp, global_conf, **kwargs ) + if kwargs.get( 'static_enabled', True ): + webapp = wrap_in_static( webapp, global_conf, **kwargs ) + # Close any pooled database connections before forking + try: + galaxy.webapps.community.model.mapping.metadata.engine.connection_provider._pool.dispose() + except: + pass + # Return + return webapp + +def wrap_in_middleware( app, global_conf, **local_conf ): + """Based on the configuration wrap `app` in a set of common and useful middleware.""" + # Merge the global and local configurations + conf = global_conf.copy() + conf.update(local_conf) + debug = asbool( conf.get( 'debug', False ) ) + # First put into place httpexceptions, which must be most closely + # wrapped around the application (it can interact poorly with + # other middleware): + app = httpexceptions.make_middleware( app, conf ) + log.debug( "Enabling 'httpexceptions' middleware" ) + # The recursive middleware allows for including requests in other + # requests or forwarding of requests, all on the server side. + if asbool(conf.get('use_recursive', True)): + from paste import recursive + app = recursive.RecursiveMiddleware( app, conf ) + log.debug( "Enabling 'recursive' middleware" ) + # Various debug middleware that can only be turned on if the debug + # flag is set, either because they are insecure or greatly hurt + # performance + if debug: + # Middleware to check for WSGI compliance + if asbool( conf.get( 'use_lint', True ) ): + from paste import lint + app = lint.make_middleware( app, conf ) + log.debug( "Enabling 'lint' middleware" ) + # Middleware to run the python profiler on each request + if asbool( conf.get( 'use_profile', False ) ): + import profile + app = profile.ProfileMiddleware( app, conf ) + log.debug( "Enabling 'profile' middleware" ) + # Middleware that intercepts print statements and shows them on the + # returned page + if asbool( conf.get( 'use_printdebug', True ) ): + from paste.debug import prints + app = prints.PrintDebugMiddleware( app, conf ) + log.debug( "Enabling 'print debug' middleware" ) + if debug and asbool( conf.get( 'use_interactive', False ) ): + # Interactive exception debugging, scary dangerous if publicly + # accessible, if not enabled we'll use the regular error printing + # middleware. + pkg_resources.require( "WebError" ) + from weberror import evalexception + app = evalexception.EvalException( app, conf, + templating_formatters=build_template_error_formatters() ) + log.debug( "Enabling 'eval exceptions' middleware" ) + else: + # Not in interactive debug mode, just use the regular error middleware + from paste.exceptions import errormiddleware + app = errormiddleware.ErrorMiddleware( app, conf ) + log.debug( "Enabling 'error' middleware" ) + # Transaction logging (apache access.log style) + if asbool( conf.get( 'use_translogger', True ) ): + from paste.translogger import TransLogger + app = TransLogger( app ) + log.debug( "Enabling 'trans logger' middleware" ) + # Config middleware just stores the paste config along with the request, + # not sure we need this but useful + from paste.deploy.config import ConfigMiddleware + app = ConfigMiddleware( app, conf ) + log.debug( "Enabling 'config' middleware" ) + # X-Forwarded-Host handling + from galaxy.web.framework.middleware.xforwardedhost import XForwardedHostMiddleware + app = XForwardedHostMiddleware( app ) + log.debug( "Enabling 'x-forwarded-host' middleware" ) + return app + +def wrap_in_static( app, global_conf, **local_conf ): + from paste.urlmap import URLMap + from galaxy.web.framework.middleware.static import CacheableStaticURLParser as Static + urlmap = URLMap() + # Merge the global and local configurations + conf = global_conf.copy() + conf.update(local_conf) + # Get cache time in seconds + cache_time = conf.get( "static_cache_time", None ) + if cache_time is not None: + cache_time = int( cache_time ) + # Send to dynamic app by default + urlmap["/"] = app + # Define static mappings from config + urlmap["/static"] = Static( conf.get( "static_dir" ), cache_time ) + urlmap["/images"] = Static( conf.get( "static_images_dir" ), cache_time ) + urlmap["/static/scripts"] = Static( conf.get( "static_scripts_dir" ), cache_time ) + urlmap["/static/style"] = Static( conf.get( "static_style_dir" ), cache_time ) + urlmap["/favicon.ico"] = Static( conf.get( "static_favicon_dir" ), cache_time ) + # URL mapper becomes the root webapp + return urlmap + +def build_template_error_formatters(): + """ + Build a list of template error formatters for WebError. When an error + occurs, WebError pass the exception to each function in this list until + one returns a value, which will be displayed on the error page. + """ + formatters = [] + # Formatter for mako + import mako.exceptions + def mako_html_data( exc_value ): + if isinstance( exc_value, ( mako.exceptions.CompileException, mako.exceptions.SyntaxException ) ): + return mako.exceptions.html_error_template().render( full=False, css=False ) + if isinstance( exc_value, AttributeError ) and exc_value.args[0].startswith( "'Undefined' object has no attribute" ): + return mako.exceptions.html_error_template().render( full=False, css=False ) + formatters.append( mako_html_data ) + return formatters diff -r 4c740255b9e7 -r f905e1415dd4 lib/galaxy/webapps/community/config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/webapps/community/config.py Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,153 @@ +""" +Universe configuration builder. +""" + +import sys, os +import logging, logging.config +from optparse import OptionParser +import ConfigParser +from galaxy.util import string_as_bool + +from galaxy import eggs +import pkg_resources + +log = logging.getLogger( __name__ ) + +def resolve_path( path, root ): + """If 'path' is relative make absolute by prepending 'root'""" + if not( os.path.isabs( path ) ): + path = os.path.join( root, path ) + return path + +class ConfigurationError( Exception ): + pass + +class Configuration( object ): + def __init__( self, **kwargs ): + self.config_dict = kwargs + self.root = kwargs.get( 'root_dir', '.' ) + # Collect the umask and primary gid from the environment + self.umask = os.umask( 077 ) # get the current umask + os.umask( self.umask ) # can't get w/o set, so set it back + self.gid = os.getgid() # if running under newgrp(1) we'll need to fix the group of data created on the cluster + # Database related configuration + self.database = resolve_path( kwargs.get( "database_file", "database/universe.d" ), self.root ) + self.database_connection = kwargs.get( "database_connection", False ) + self.database_engine_options = get_database_engine_options( kwargs ) + self.database_create_tables = string_as_bool( kwargs.get( "database_create_tables", "True" ) ) + # Where dataset files are stored + self.file_path = resolve_path( kwargs.get( "file_path", "database/files" ), self.root ) + self.new_file_path = resolve_path( kwargs.get( "new_file_path", "database/tmp" ), self.root ) + self.cookie_path = kwargs.get( "cookie_path", "/" ) + self.test_conf = resolve_path( kwargs.get( "test_conf", "" ), self.root ) + self.id_secret = kwargs.get( "id_secret", "USING THE DEFAULT IS NOT SECURE!" ) + self.use_remote_user = string_as_bool( kwargs.get( "use_remote_user", "False" ) ) + self.remote_user_maildomain = kwargs.get( "remote_user_maildomain", None ) + self.remote_user_logout_href = kwargs.get( "remote_user_logout_href", None ) + self.require_login = string_as_bool( kwargs.get( "require_login", "False" ) ) + self.allow_user_creation = string_as_bool( kwargs.get( "allow_user_creation", "True" ) ) + self.template_path = resolve_path( kwargs.get( "template_path", "templates" ), self.root ) + self.template_cache = resolve_path( kwargs.get( "template_cache_path", "database/compiled_templates/community" ), self.root ) + self.admin_users = kwargs.get( "admin_users", "" ) + self.sendmail_path = kwargs.get('sendmail_path',"/usr/sbin/sendmail") + self.mailing_join_addr = kwargs.get('mailing_join_addr',"galaxy-user-join@bx.psu.edu") + self.error_email_to = kwargs.get( 'error_email_to', None ) + self.smtp_server = kwargs.get( 'smtp_server', None ) + self.log_actions = string_as_bool( kwargs.get( 'log_actions', 'False' ) ) + self.brand = kwargs.get( 'brand', None ) + self.wiki_url = kwargs.get( 'wiki_url', 'http://g2.trac.bx.psu.edu/' ) + self.bugs_email = kwargs.get( 'bugs_email', None ) + self.blog_url = kwargs.get( 'blog_url', None ) + self.screencasts_url = kwargs.get( 'screencasts_url', None ) + self.log_events = False + self.cloud_controller_instance = False + # Parse global_conf and save the parser + global_conf = kwargs.get( 'global_conf', None ) + global_conf_parser = ConfigParser.ConfigParser() + self.global_conf_parser = global_conf_parser + if global_conf and "__file__" in global_conf: + global_conf_parser.read(global_conf['__file__']) + def get( self, key, default ): + return self.config_dict.get( key, default ) + def get_bool( self, key, default ): + if key in self.config_dict: + return string_as_bool( self.config_dict[key] ) + else: + return default + def check( self ): + # Check that required directories exist + for path in self.root, self.file_path, self.template_path: + if not os.path.isdir( path ): + raise ConfigurationError("Directory does not exist: %s" % path ) + def is_admin_user( self,user ): + """ + Determine if the provided user is listed in `admin_users`. + + NOTE: This is temporary, admin users will likely be specified in the + database in the future. + """ + admin_users = self.get( "admin_users", "" ).split( "," ) + return ( user is not None and user.email in admin_users ) + +def get_database_engine_options( kwargs ): + """ + Allow options for the SQLAlchemy database engine to be passed by using + the prefix "database_engine_option_". + """ + conversions = { + 'convert_unicode': string_as_bool, + 'pool_timeout': int, + 'echo': string_as_bool, + 'echo_pool': string_as_bool, + 'pool_recycle': int, + 'pool_size': int, + 'max_overflow': int, + 'pool_threadlocal': string_as_bool, + 'server_side_cursors': string_as_bool + } + prefix = "database_engine_option_" + prefix_len = len( prefix ) + rval = {} + for key, value in kwargs.iteritems(): + if key.startswith( prefix ): + key = key[prefix_len:] + if key in conversions: + value = conversions[key](value) + rval[ key ] = value + return rval + +def configure_logging( config ): + """ + Allow some basic logging configuration to be read from the cherrpy + config. + """ + # PasteScript will have already configured the logger if the appropriate + # sections were found in the config file, so we do nothing if the + # config has a loggers section, otherwise we do some simple setup + # using the 'log_*' values from the config. + if config.global_conf_parser.has_section( "loggers" ): + return + format = config.get( "log_format", "%(name)s %(levelname)s %(asctime)s %(message)s" ) + level = logging._levelNames[ config.get( "log_level", "DEBUG" ) ] + destination = config.get( "log_destination", "stdout" ) + log.info( "Logging at '%s' level to '%s'" % ( level, destination ) ) + # Get root logger + root = logging.getLogger() + # Set level + root.setLevel( level ) + # Turn down paste httpserver logging + if level <= logging.DEBUG: + logging.getLogger( "paste.httpserver.ThreadPool" ).setLevel( logging.WARN ) + # Remove old handlers + for h in root.handlers[:]: + root.removeHandler(h) + # Create handler + if destination == "stdout": + handler = logging.StreamHandler( sys.stdout ) + else: + handler = logging.FileHandler( destination ) + # Create formatter + formatter = logging.Formatter( format ) + # Hook everything up + handler.setFormatter( formatter ) + root.addHandler( handler ) diff -r 4c740255b9e7 -r f905e1415dd4 lib/galaxy/webapps/community/controllers/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/webapps/community/controllers/__init__.py Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,1 @@ +"""Galaxy community space controllers.""" \ No newline at end of file diff -r 4c740255b9e7 -r f905e1415dd4 lib/galaxy/webapps/community/controllers/tool_browser.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/webapps/community/controllers/tool_browser.py Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,100 @@ +import sys, os, operator, string, shutil, re, socket, urllib, time, logging +from cgi import escape, FieldStorage + +from galaxy.web.base.controller import * +from galaxy.webapps.community.base.controller import * +from galaxy.web.framework.helpers import time_ago, iff, grids +from galaxy.model.orm import * + +log = logging.getLogger( __name__ ) + +# States for passing messages +SUCCESS, INFO, WARNING, ERROR = "done", "info", "warning", "error" + +class ToolListGrid( grids.Grid ): + class NameColumn( grids.TextColumn ): + def get_value( self, trans, grid, tool ): + if tool.name: + return tool.name + return 'not set' + class CategoryColumn( grids.TextColumn ): + def get_value( self, trans, grid, tool ): + if tool.category: + return tool.category + return 'not set' + + # Grid definition + title = "Tools" + model_class = model.Tool + template='/webapps/community/tool/grid.mako' + default_sort_key = "category" + columns = [ + NameColumn( "Name", + key="name", + model_class=model.Tool, + attach_popup=False, + filterable="advanced" ), + CategoryColumn( "Category", + key="category", + model_class=model.Tool, + attach_popup=False, + filterable="advanced" ), + # Columns that are valid for filtering but are not visible. + grids.DeletedColumn( "Deleted", key="deleted", visible=False, filterable="advanced" ) + ] + columns.append( grids.MulticolFilterColumn( "Search", + cols_to_filter=[ columns[0], columns[1] ], + key="free-text-search", + visible=False, + filterable="standard" ) ) + global_actions = [ + grids.GridAction( "Upload tool", dict( controller='tool_browwser', action='upload' ) ) + ] + operations = [ + grids.GridOperation( "View versions", condition=( lambda item: not item.deleted ), allow_multiple=False ) + ] + standard_filters = [ + grids.GridColumnFilter( "Deleted", args=dict( deleted=True ) ), + grids.GridColumnFilter( "All", args=dict( deleted='All' ) ) + ] + default_filter = dict( name="All", category="All", deleted="False" ) + num_rows_per_page = 50 + preserve_state = False + use_paging = True + def build_initial_query( self, session ): + return session.query( self.model_class ) + def apply_default_filter( self, trans, query, **kwargs ): + return query.filter( self.model_class.deleted==False ) + +class ToolBrowserController( BaseController ): + + tool_list_grid = ToolListGrid() + + @web.expose + def index( self, trans, **kwd ): + params = util.Params( kwd ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + return trans.fill_template( '/webapps/community/index.mako', message=message, status=status ) + @web.expose + def browse_tools( self, trans, **kwargs ): + if 'operation' in kwargs: + operation = kwargs['operation'].lower() + if operation == "browse": + return trans.response.send_redirect( web.url_for( controller='tool_browser', + action='browse_tool', + **kwargs ) ) + # Render the list view + return self.tool_list_grid( trans, **kwargs ) + @web.expose + def browse_tool( self, trans, **kwd ): + params = util.Params( kwd ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + return trans.fill_template( '/webapps/community/tool/browse_tool.mako', + tools=tools, + message=message, + status=status ) + @web.expose + def upload( self, trans, **kwargs ): + pass diff -r 4c740255b9e7 -r f905e1415dd4 lib/galaxy/webapps/community/model/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/webapps/community/model/__init__.py Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,187 @@ +""" +Galaxy Community Space data model classes + +Naming: try to use class names that have a distinct plural form so that +the relationship cardinalities are obvious (e.g. prefer Dataset to Data) +""" +import os.path, os, errno, sys, codecs, operator, tempfile, logging +from galaxy.util.bunch import Bunch +from galaxy import util +from galaxy.util.hash_util import * +from galaxy.web.form_builder import * +log = logging.getLogger( __name__ ) +from sqlalchemy.orm import object_session + +class User( object ): + def __init__( self, email=None, password=None ): + self.email = email + self.password = password + self.external = False + self.deleted = False + self.purged = False + self.username = None + # Relationships + self.tools = [] + def set_password_cleartext( self, cleartext ): + """Set 'self.password' to the digest of 'cleartext'.""" + self.password = new_secure_hash( text_type=cleartext ) + def check_password( self, cleartext ): + """Check if 'cleartext' matches 'self.password' when hashed.""" + return self.password == new_secure_hash( text_type=cleartext ) + +class GalaxySession( object ): + def __init__( self, + id=None, + user=None, + remote_host=None, + remote_addr=None, + referer=None, + current_history=None, + session_key=None, + is_valid=False, + prev_session_id=None ): + self.id = id + self.user = user + self.remote_host = remote_host + self.remote_addr = remote_addr + self.referer = referer + self.current_history = current_history + self.session_key = session_key + self.is_valid = is_valid + self.prev_session_id = prev_session_id + self.histories = [] + def add_history( self, history, association=None ): + if association is None: + self.histories.append( GalaxySessionToHistoryAssociation( self, history ) ) + else: + self.histories.append( association ) + +class Tool( object ): + def __init__( self, guid=None, name=None, description=None, category=None, version=None, user_id=None, external_filename=None ): + self.guid = guid + self.name = name or "Unnamed tool" + self.description = description + self.category = category + self.version = version or "1.0.0" + self.user_id = user_id + self.external_filename = external_filename + +class Job( object ): + """ + A job represents a request to run a tool given input datasets, tool + parameters, and output datasets. + """ + states = Bunch( NEW = 'new', + UPLOAD = 'upload', + WAITING = 'waiting', + QUEUED = 'queued', + RUNNING = 'running', + OK = 'ok', + ERROR = 'error', + DELETED = 'deleted' ) + def __init__( self ): + self.session_id = None + self.tool_id = None + self.tool_version = None + self.command_line = None + self.param_filename = None + self.parameters = [] + self.input_datasets = [] + self.output_datasets = [] + self.output_library_datasets = [] + self.state = Job.states.NEW + self.info = None + self.job_runner_name = None + self.job_runner_external_id = None + def add_parameter( self, name, value ): + self.parameters.append( JobParameter( name, value ) ) + def add_input_dataset( self, name, dataset ): + self.input_datasets.append( JobToInputDatasetAssociation( name, dataset ) ) + def add_output_dataset( self, name, dataset ): + self.output_datasets.append( JobToOutputDatasetAssociation( name, dataset ) ) + def add_output_library_dataset( self, name, dataset ): + self.output_library_datasets.append( JobToOutputLibraryDatasetAssociation( name, dataset ) ) + def set_state( self, state ): + self.state = state + # For historical reasons state propogates down to datasets + for da in self.output_datasets: + da.dataset.state = state + def get_param_values( self, app ): + """ + Read encoded parameter values from the database and turn back into a + dict of tool parameter values. + """ + param_dict = dict( [ ( p.name, p.value ) for p in self.parameters ] ) + tool = app.toolbox.tools_by_id[self.tool_id] + param_dict = tool.params_from_strings( param_dict, app ) + return param_dict + def check_if_output_datasets_deleted( self ): + """ + Return true if all of the output datasets associated with this job are + in the deleted state + """ + for dataset_assoc in self.output_datasets: + dataset = dataset_assoc.dataset + # only the originator of the job can delete a dataset to cause + # cancellation of the job, no need to loop through history_associations + if not dataset.deleted: + return False + return True + def mark_deleted( self ): + """ + Mark this job as deleted, and mark any output datasets as discarded. + """ + self.state = Job.states.DELETED + self.info = "Job output deleted by user before job completed." + for dataset_assoc in self.output_datasets: + dataset = dataset_assoc.dataset + dataset.deleted = True + dataset.state = dataset.states.DISCARDED + for dataset in dataset.dataset.history_associations: + # propagate info across shared datasets + dataset.deleted = True + dataset.blurb = 'deleted' + dataset.peek = 'Job deleted' + dataset.info = 'Job output deleted by user before job completed' + +class Tag ( object ): + def __init__( self, id=None, type=None, parent_id=None, name=None ): + self.id = id + self.type = type + self.parent_id = parent_id + self.name = name + def __str__ ( self ): + return "Tag(id=%s, type=%i, parent_id=%s, name=%s)" % ( self.id, self.type, self.parent_id, self.name ) + +class ItemTagAssociation ( object ): + def __init__( self, id=None, user=None, item_id=None, tag_id=None, user_tname=None, value=None ): + self.id = id + self.user = user + self.item_id = item_id + self.tag_id = tag_id + self.user_tname = user_tname + self.value = None + self.user_value = None + +class ToolTagAssociation ( ItemTagAssociation ): + pass + +class ToolAnnotationAssociation( object ): + pass + +## ---- Utility methods ------------------------------------------------------- + +def directory_hash_id( id ): + s = str( id ) + l = len( s ) + # Shortcut -- ids 0-999 go under ../000/ + if l < 4: + return [ "000" ] + # Pad with zeros until a multiple of three + padded = ( ( 3 - len( s ) % 3 ) * "0" ) + s + # Drop the last three digits -- 1000 files per directory + padded = padded[:-3] + # Break into chunks of three + return [ padded[i*3:(i+1)*3] for i in range( len( padded ) // 3 ) ] + + diff -r 4c740255b9e7 -r f905e1415dd4 lib/galaxy/webapps/community/model/mapping.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/webapps/community/model/mapping.py Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,186 @@ +""" +Details of how the data model objects are mapped onto the relational database +are encapsulated here. +""" +import logging +log = logging.getLogger( __name__ ) + +import sys +import datetime + +from galaxy.webapps.community.model import * +from galaxy.model.orm import * +from galaxy.model.orm.ext.assignmapper import * +from galaxy.model.custom_types import * +from galaxy.util.bunch import Bunch +from galaxy.webapps.community.security import CommunityRBACAgent +from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.ext.associationproxy import association_proxy + +metadata = MetaData() +context = Session = scoped_session( sessionmaker( autoflush=False, autocommit=True ) ) + +# For backward compatibility with "context.current" +context.current = Session + +dialect_to_egg = { + "sqlite" : "pysqlite>=2", + "postgres" : "psycopg2", + "mysql" : "MySQL_python" +} + +# NOTE REGARDING TIMESTAMPS: +# It is currently difficult to have the timestamps calculated by the +# database in a portable way, so we're doing it in the client. This +# also saves us from needing to postfetch on postgres. HOWEVER: it +# relies on the client's clock being set correctly, so if clustering +# web servers, use a time server to ensure synchronization + +# Return the current time in UTC without any timezone information +now = datetime.datetime.utcnow + +User.table = Table( "galaxy_user", metadata, + Column( "id", Integer, primary_key=True), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "email", TrimmedString( 255 ), nullable=False ), + Column( "username", TrimmedString( 255 ), index=True, unique=True ), + Column( "password", TrimmedString( 40 ), nullable=False ), + Column( "external", Boolean, default=False ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "purged", Boolean, index=True, default=False ) ) + +GalaxySession.table = Table( "galaxy_session", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True, nullable=True ), + Column( "remote_host", String( 255 ) ), + Column( "remote_addr", String( 255 ) ), + Column( "referer", TEXT ), + Column( "session_key", TrimmedString( 255 ), index=True, unique=True ), # unique 128 bit random number coerced to a string + Column( "is_valid", Boolean, default=False ), + Column( "prev_session_id", Integer ) # saves a reference to the previous session so we have a way to chain them together + ) + +Tool.table = Table( "tool", metadata, + Column( "id", Integer, primary_key=True ), + Column( "guid", TrimmedString( 255 ), index=True, unique=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TrimmedString( 255 ), index=True, unique=True ), + Column( "description" , TEXT ), + Column( "category", TrimmedString( 255 ), index=True ), + Column( "version", TrimmedString( 255 ) ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "external_filename" , TEXT ), + Column( "deleted", Boolean, default=False ) ) + +Job.table = Table( "job", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "tool_id", Integer, ForeignKey( "tool.id" ), index=True ), + Column( "state", String( 64 ), index=True ), + Column( "info", TrimmedString( 255 ) ), + Column( "command_line", TEXT ), + Column( "param_filename", String( 1024 ) ), + Column( "runner_name", String( 255 ) ), + Column( "stdout", TEXT ), + Column( "stderr", TEXT ), + Column( "traceback", TEXT ), + Column( "session_id", Integer, ForeignKey( "galaxy_session.id" ), index=True, nullable=True ), + Column( "job_runner_name", String( 255 ) ), + Column( "job_runner_external_id", String( 255 ) ) ) + +Tag.table = Table( "tag", metadata, + Column( "id", Integer, primary_key=True ), + Column( "type", Integer ), + Column( "parent_id", Integer, ForeignKey( "tag.id" ) ), + Column( "name", TrimmedString(255) ), + UniqueConstraint( "name" ) ) + +ToolTagAssociation.table = Table( "tool_tag_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "tool_id", Integer, ForeignKey( "tool.id" ), index=True ), + Column( "tag_id", Integer, ForeignKey( "tag.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "user_tname", TrimmedString(255), index=True), + Column( "value", TrimmedString(255), index=True), + Column( "user_value", TrimmedString(255), index=True) ) + +ToolAnnotationAssociation.table = Table( "tool_annotation_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "tool_id", Integer, ForeignKey( "tool.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "annotation", TEXT, index=True) ) + +# With the tables defined we can define the mappers and setup the +# relationships between the model objects. +assign_mapper( context, User, User.table, + properties=dict( tools=relation( Tool, order_by=desc( Tool.table.c.update_time ) ), + active_tools=relation( Tool, primaryjoin=( ( Tool.table.c.user_id == User.table.c.id ) & ( not_( Tool.table.c.deleted ) ) ), order_by=desc( Tool.table.c.update_time ) ), + galaxy_sessions=relation( GalaxySession, order_by=desc( GalaxySession.table.c.update_time ) ) ) ) + +assign_mapper( context, GalaxySession, GalaxySession.table, + properties=dict( user=relation( User.mapper ) ) ) + +assign_mapper( context, Job, Job.table, + properties=dict( galaxy_session=relation( GalaxySession ), + tool=relation( Tool ) ) ) + +assign_mapper( context, Tag, Tag.table, + properties=dict( children=relation(Tag, backref=backref( 'parent', remote_side=[Tag.table.c.id] ) ) ) ) + +assign_mapper( context, ToolTagAssociation, ToolTagAssociation.table, + properties=dict( tag=relation(Tag, backref="tagged_tools"), user=relation( User ) ) ) + +assign_mapper( context, ToolAnnotationAssociation, ToolAnnotationAssociation.table, + properties=dict( tool=relation( Tool ), user=relation( User ) ) ) + +assign_mapper( context, Tool, Tool.table, + properties = dict( user=relation( User.mapper ) ) ) + +def guess_dialect_for_url( url ): + return (url.split(':', 1))[0] + +def load_egg_for_url( url ): + # Load the appropriate db module + dialect = guess_dialect_for_url( url ) + try: + egg = dialect_to_egg[dialect] + try: + pkg_resources.require( egg ) + log.debug( "%s egg successfully loaded for %s dialect" % ( egg, dialect ) ) + except: + # If the module's in the path elsewhere (i.e. non-egg), it'll still load. + log.warning( "%s egg not found, but an attempt will be made to use %s anyway" % ( egg, dialect ) ) + except KeyError: + # Let this go, it could possibly work with db's we don't support + log.error( "database_connection contains an unknown SQLAlchemy database dialect: %s" % dialect ) + +def init( file_path, url, engine_options={}, create_tables=False ): + """Connect mappings to the database""" + log.debug("###In init, file_path: %s" % str( file_path )) + # Connect dataset to the file path + Tool.file_path = file_path + # Load the appropriate db module + load_egg_for_url( url ) + # Create the database engine + engine = create_engine( url, **engine_options ) + # Connect the metadata to the database. + metadata.bind = engine + # Clear any existing contextual sessions and reconfigure + Session.remove() + Session.configure( bind=engine ) + # Create tables if needed + if create_tables: + metadata.create_all() + # Pack everything into a bunch + result = Bunch( **globals() ) + result.engine = engine + result.session = Session + result.create_tables = create_tables + #load local galaxy security policy + result.security_agent = CommunityRBACAgent( result ) + return result diff -r 4c740255b9e7 -r f905e1415dd4 lib/galaxy/webapps/community/model/migrate/check.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/webapps/community/model/migrate/check.py Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,105 @@ +import sys, os.path, logging + +from galaxy import eggs + +import pkg_resources +pkg_resources.require( "sqlalchemy-migrate" ) + +from migrate.versioning import repository, schema +from sqlalchemy import * +from sqlalchemy.exc import NoSuchTableError + +log = logging.getLogger( __name__ ) + +# path relative to galaxy +migrate_repository_directory = os.path.dirname( __file__ ).replace( os.getcwd() + os.path.sep, '', 1 ) +migrate_repository = repository.Repository( migrate_repository_directory ) +dialect_to_egg = { + "sqlite" : "pysqlite>=2", + "postgres" : "psycopg2", + "mysql" : "MySQL_python" +} + +def create_or_verify_database( url, engine_options={} ): + """ + Check that the database is use-able, possibly creating it if empty (this is + the only time we automatically create tables, otherwise we force the + user to do it using the management script so they can create backups). + + 1) Empty database --> initialize with latest version and return + 2) Database older than migration support --> fail and require manual update + 3) Database at state where migrate support introduced --> add version control information but make no changes (might still require manual update) + 4) Database versioned but out of date --> fail with informative message, user must run "sh manage_db.sh upgrade" + + """ + dialect = ( url.split( ':', 1 ) )[0] + try: + egg = dialect_to_egg[dialect] + try: + pkg_resources.require( egg ) + log.debug( "%s egg successfully loaded for %s dialect" % ( egg, dialect ) ) + except: + # If the module is in the path elsewhere (i.e. non-egg), it'll still load. + log.warning( "%s egg not found, but an attempt will be made to use %s anyway" % ( egg, dialect ) ) + except KeyError: + # Let this go, it could possibly work with db's we don't support + log.error( "database_connection contains an unknown SQLAlchemy database dialect: %s" % dialect ) + # Create engine and metadata + engine = create_engine( url, **engine_options ) + meta = MetaData( bind=engine ) + # Try to load dataset table + try: + galaxy_user_table = Table( "galaxy_user", meta, autoload=True ) + except NoSuchTableError: + # No 'galaxy_user' table means a completely uninitialized database, which + # is fine, init the database in a versioned state + log.info( "No database, initializing" ) + # Database might or might not be versioned + try: + # Declare the database to be under a repository's version control + db_schema = schema.ControlledSchema.create( engine, migrate_repository ) + except: + # The database is already under version control + db_schema = schema.ControlledSchema( engine, migrate_repository ) + # Apply all scripts to get to current version + migrate_to_current_version( engine, db_schema ) + return + try: + version_table = Table( "migrate_version", meta, autoload=True ) + except NoSuchTableError: + # The database exists but is not yet under migrate version control, so init with version 1 + log.info( "Adding version control to existing database" ) + try: + metadata_file_table = Table( "metadata_file", meta, autoload=True ) + schema.ControlledSchema.create( engine, migrate_repository, version=2 ) + except NoSuchTableError: + schema.ControlledSchema.create( engine, migrate_repository, version=1 ) + # Verify that the code and the DB are in sync + db_schema = schema.ControlledSchema( engine, migrate_repository ) + if migrate_repository.versions.latest != db_schema.version: + raise Exception( "Your database has version '%d' but this code expects version '%d'. Please backup your database and then migrate the schema by running 'sh manage_db.sh upgrade'." + % ( db_schema.version, migrate_repository.versions.latest ) ) + else: + log.info( "At database version %d" % db_schema.version ) + +def migrate_to_current_version( engine, schema ): + # Changes to get to current version + changeset = schema.changeset( None ) + for ver, change in changeset: + nextver = ver + changeset.step + log.info( 'Migrating %s -> %s... ' % ( ver, nextver ) ) + old_stdout = sys.stdout + class FakeStdout( object ): + def __init__( self ): + self.buffer = [] + def write( self, s ): + self.buffer.append( s ) + def flush( self ): + pass + sys.stdout = FakeStdout() + try: + schema.runchange( ver, change, changeset.step ) + finally: + for message in "".join( sys.stdout.buffer ).split( "\n" ): + log.info( message ) + sys.stdout = old_stdout diff -r 4c740255b9e7 -r f905e1415dd4 lib/galaxy/webapps/community/model/migrate/migrate.cfg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/webapps/community/model/migrate/migrate.cfg Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,20 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=Galaxy + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=[] diff -r 4c740255b9e7 -r f905e1415dd4 lib/galaxy/webapps/community/model/migrate/versions/0001_initial_tables.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/webapps/community/model/migrate/versions/0001_initial_tables.py Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,101 @@ +""" +Migration script to create initial tables. +""" + +from sqlalchemy import * +from migrate import * + +import datetime +now = datetime.datetime.utcnow + +# Need our custom types, but don't import anything else from model +from galaxy.model.custom_types import * + +import logging +log = logging.getLogger( __name__ ) + +metadata = MetaData( migrate_engine ) + +User.table = Table( "galaxy_user", metadata, + Column( "id", Integer, primary_key=True), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "email", TrimmedString( 255 ), nullable=False ), + Column( "username", TrimmedString( 255 ), index=True, unique=True ), + Column( "password", TrimmedString( 40 ), nullable=False ), + Column( "external", Boolean, default=False ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "purged", Boolean, index=True, default=False ) ) + +GalaxySession.table = Table( "galaxy_session", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True, nullable=True ), + Column( "remote_host", String( 255 ) ), + Column( "remote_addr", String( 255 ) ), + Column( "referer", TEXT ), + Column( "session_key", TrimmedString( 255 ), index=True, unique=True ), # unique 128 bit random number coerced to a string + Column( "is_valid", Boolean, default=False ), + Column( "prev_session_id", Integer ) # saves a reference to the previous session so we have a way to chain them together + ) + +Tool.table = Table( "tool", metadata, + Column( "id", Integer, primary_key=True ), + Column( "guid", TrimmedString( 255 ), index=True, unique=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TrimmedString( 255 ), index=True, unique=True ), + Column( "description" , TEXT ), + Column( "category", TrimmedString( 255 ), index=True ), + Column( "version", TrimmedString( 255 ) ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "external_filename" , TEXT ), + Column( "deleted", Boolean, default=False ) ) + +Job.table = Table( "job", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "tool_id", Integer, ForeignKey( "tool.id" ), index=True ), + Column( "state", String( 64 ), index=True ), + Column( "info", TrimmedString( 255 ) ), + Column( "command_line", TEXT ), + Column( "param_filename", String( 1024 ) ), + Column( "runner_name", String( 255 ) ), + Column( "stdout", TEXT ), + Column( "stderr", TEXT ), + Column( "traceback", TEXT ), + Column( "session_id", Integer, ForeignKey( "galaxy_session.id" ), index=True, nullable=True ), + Column( "job_runner_name", String( 255 ) ), + Column( "job_runner_external_id", String( 255 ) ) ) + +Tag.table = Table( "tag", metadata, + Column( "id", Integer, primary_key=True ), + Column( "type", Integer ), + Column( "parent_id", Integer, ForeignKey( "tag.id" ) ), + Column( "name", TrimmedString(255) ), + UniqueConstraint( "name" ) ) + +ToolTagAssociation.table = Table( "tool_tag_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "tool_id", Integer, ForeignKey( "tool.id" ), index=True ), + Column( "tag_id", Integer, ForeignKey( "tag.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "user_tname", TrimmedString(255), index=True), + Column( "value", TrimmedString(255), index=True), + Column( "user_value", TrimmedString(255), index=True) ) + +ToolAnnotationAssociation.table = Table( "tool_annotation_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "tool_id", Integer, ForeignKey( "tool.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "annotation", TEXT, index=True) ) + +def upgrade(): + print __doc__ + metadata.create_all() + +def downgrade(): + # Operations to reverse the above upgrade go here. + pass diff -r 4c740255b9e7 -r f905e1415dd4 lib/galaxy/webapps/community/security/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/webapps/community/security/__init__.py Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,96 @@ +""" +Galaxy Community Space Security +""" +import logging, socket, operator +from datetime import datetime, timedelta +from galaxy.util.bunch import Bunch +from galaxy.util import listify +from galaxy.model.orm import * + +log = logging.getLogger(__name__) + +class Action( object ): + def __init__( self, action, description, model ): + self.action = action + self.description = description + self.model = model + +class RBACAgent: + """Class that handles galaxy community space security""" + permitted_actions = Bunch() + def get_action( self, name, default=None ): + """Get a permitted action by its dict key or action name""" + for k, v in self.permitted_actions.items(): + if k == name or v.action == name: + return v + return default + def get_actions( self ): + """Get all permitted actions as a list of Action objects""" + return self.permitted_actions.__dict__.values() + def get_item_actions( self, action, item ): + raise 'No valid method of retrieving action (%s) for item %s.' % ( action, item ) + def create_private_user_role( self, user ): + raise "Unimplemented Method" + def get_private_user_role( self, user ): + raise "Unimplemented Method" + def convert_permitted_action_strings( self, permitted_action_strings ): + """ + When getting permitted actions from an untrusted source like a + form, ensure that they match our actual permitted actions. + """ + return filter( lambda x: x is not None, [ self.permitted_actions.get( action_string ) for action_string in permitted_action_strings ] ) + +class CommunityRBACAgent( RBACAgent ): + def __init__( self, model, permitted_actions=None ): + self.model = model + if permitted_actions: + self.permitted_actions = permitted_actions + @property + def sa_session( self ): + """Returns a SQLAlchemy session""" + return self.model.context + + def allow_action( self, roles, action, item ): + """ + Method for checking a permission for the current user ( based on roles ) to perform a + specific action on an item + """ + item_actions = self.get_item_actions( action, item ) + if not item_actions: + return action.model == 'restrict' + ret_val = False + for item_action in item_actions: + if item_action.role in roles: + ret_val = True + break + return ret_val + def get_item_actions( self, action, item ): + # item must be one of: Dataset, Library, LibraryFolder, LibraryDataset, LibraryDatasetDatasetAssociation + return [ permission for permission in item.actions if permission.action == action.action ] + def create_private_user_role( self, user ): + # Create private role + role = self.model.Role( name=user.email, description='Private Role for ' + user.email, type=self.model.Role.types.PRIVATE ) + self.sa_session.add( role ) + self.sa_session.flush() + # Add user to role + self.associate_components( role=role, user=user ) + return role + def get_private_user_role( self, user, auto_create=False ): + role = self.sa_session.query( self.model.Role ) \ + .filter( and_( self.model.Role.table.c.name == user.email, + self.model.Role.table.c.type == self.model.Role.types.PRIVATE ) ) \ + .first() + if not role: + if auto_create: + return self.create_private_user_role( user ) + else: + return None + return role + +def get_permitted_actions( filter=None ): + '''Utility method to return a subset of RBACAgent's permitted actions''' + if filter is None: + return RBACAgent.permitted_actions + tmp_bunch = Bunch() + [ tmp_bunch.__dict__.__setitem__(k, v) for k, v in RBACAgent.permitted_actions.items() if k.startswith( filter ) ] + return tmp_bunch diff -r 4c740255b9e7 -r f905e1415dd4 run_community.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/run_community.sh Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,4 @@ +#!/bin/sh + +cd `dirname $0` +python ./scripts/paster.py serve community_wsgi.ini --pid-file=community_webapp.pid --log-file=community_webapp.log $@ diff -r 4c740255b9e7 -r f905e1415dd4 setup.sh --- a/setup.sh Wed Apr 14 10:14:43 2010 -0400 +++ b/setup.sh Wed Apr 14 10:20:32 2010 -0400 @@ -31,6 +31,7 @@ DIRS=" database database/files +database/tools database/tmp database/compiled_templates database/job_working_directory diff -r 4c740255b9e7 -r f905e1415dd4 templates/webapps/community/base_panels.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/webapps/community/base_panels.mako Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,102 @@ +<%inherit file="/base_panels.mako"/> + +## Default title +<%def name="title()">Galaxy Community Space</%def> + +## Masthead +<%def name="masthead()"> + + ## Tab area, fills entire width + <div style="position: absolute; top: 0; left: 0; width: 100%; text-align: center"> + + <table class="tab-group" border="0" cellspacing="0" style="margin: auto;"> + <tr> + + <%def name="tab( id, display, href, target='_parent', visible=True, extra_class='' )"> + <% + cls = "tab" + if extra_class: + cls += " " + extra_class + if self.active_view == id: + cls += " active" + style = "" + if not visible: + style = "display: none;" + %> + <td class="${cls}" style="${style}"><a target="${target}" href="${href}">${display}</a></td> + </%def> + + ${tab( "admin", "Admin", h.url_for( controller='/webapps/community/admin', action='index' ), extra_class="admin-only", visible=( trans.user and app.config.is_admin_user( trans.user ) ) )} + + <td class="tab"> + <a>Help</a> + <div class="submenu"> + <ul> + <li><a href="${app.config.get( "bugs_email", "mailto:galaxy-bugs@bx.psu.edu" )}">Email comments, bug reports, or suggestions</a></li> + <li><a target="_blank" href="${app.config.get( "wiki_url", "http://bitbucket.org/galaxy/galaxy-central/wiki" )}">Galaxy Wiki</a></li> + <li><a target="_blank" href="${app.config.get( "screencasts_url", "http://galaxycast.org" )}">Video tutorials (screencasts)</a></li> + <li><a target="_blank" href="${app.config.get( "citation_url", "http://bitbucket.org/galaxy/galaxy-central/wiki/Citations" )}">How to Cite Galaxy</a></li> + </ul> + </div> + </td> + + ## User tab. + <% + cls = "tab" + if self.active_view == 'user': + cls += " active" + %> + <td class="${cls}"> + <a>User</a> + <% + if trans.user: + user_email = trans.user.email + style1 = "display: none;" + style2 = ""; + else: + user_email = "" + style1 = "" + style2 = "display: none;" + %> + <div class="submenu"> + <ul class="loggedout-only" style="${style1}"> + <li><a href="${h.url_for( controller='/user', action='login', webapp='community' )}">Login</a></li> + %if app.config.allow_user_creation: + <li><a href="${h.url_for( controller='/user', action='create', webapp='community' )}">Register</a></li> + %endif + </ul> + <ul class="loggedin-only" style="${style2}"> + %if app.config.use_remote_user: + %if app.config.remote_user_logout_href: + <li><a href="${app.config.remote_user_logout_href}" target="_top">Logout</a></li> + %endif + %else: + <li>Logged in as <span id="user-email">${user_email}</span></li> + <li><a target="galaxy_main" href="${h.url_for( controller='/user', action='index', webapp='community' )}">Preferences</a></li> + <% + if app.config.require_login: + logout_url = h.url_for( controller='/root', action='index', webapp='community', m_c='user', m_a='logout' ) + else: + logout_url = h.url_for( controller='/user', action='logout', webapp='community' ) + %> + <li><a target="_top" href="${logout_url}">Logout</a></li> + %endif + </ul> + </div> + </td> + </tr> + </table> + </div> + + ## Logo, layered over tabs to be clickable + <div class="title" style="position: absolute; top: 0; left: 0;"> + <a href="${app.config.get( 'logo_url', '/' )}"> + <img border="0" src="${h.url_for('/static/images/galaxyIcon_noText.png')}" style="width: 26px; vertical-align: top;"> + Galaxy + %if app.config.brand: + <span class='brand'>/ ${app.config.brand}</span> + %endif + </a> + </div> + +</%def> diff -r 4c740255b9e7 -r f905e1415dd4 templates/webapps/community/index.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/webapps/community/index.mako Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,57 @@ +<%inherit file="/webapps/community/base_panels.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +<%def name="init()"> + <% + self.has_left_panel=True + self.has_right_panel=False + self.active_view="tools" + %> + %if trans.app.config.require_login and not trans.user: + <script type="text/javascript"> + if ( window != top ) { + top.location.href = location.href; + } + </script> + %endif +</%def> + +<%def name="left_panel()"> + <div class="unified-panel-header" unselectable="on"> + <div class='unified-panel-header-inner'>Community</div> + </div> + <div class="page-container" style="padding: 10px;"> + <div class="toolMenu"> + <div class="toolSectionList"> + <div class="toolSectionPad"></div> + <div class="toolSectionTitle"> + <span>Tools</span> + </div> + <div class="toolSectionBody"> + <div class="toolSectionBg"> + <div class="toolTitle"><a href="${h.url_for( controller='tool_browser', action='browse_tools' )}" target="galaxy_main">Browse tools</a></div> + </div> + </div> + <div class="toolSectionPad"></div> + <div class="toolSectionTitle"> + <span>Forum</span> + </div> + <div class="toolSectionBody"> + <div class="toolSectionBg"> + <div class="toolTitle"><a href="${h.url_for( controller='forum', action='browse_forums' )}" target="galaxy_main">Forums</a></div> + </div> + </div> + </div> + </div> + </div> +</%def> + +<%def name="center_panel()"> + <% + if trans.app.config.require_login and not trans.user: + center_url = h.url_for( controller='user', action='login', message=message, status=status ) + else: + center_url = h.url_for( controller='tool_browser', action='browse_tools', message=message, status=status ) + %> + <iframe name="galaxy_main" id="galaxy_main" frameborder="0" style="position: absolute; width: 100%; height: 100%;" src="${center_url}"> </iframe> +</%def> diff -r 4c740255b9e7 -r f905e1415dd4 templates/webapps/community/message.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/webapps/community/message.mako Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,1 @@ +<%inherit file="/message.mako"/> diff -r 4c740255b9e7 -r f905e1415dd4 templates/webapps/community/tool/browse_tool.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/webapps/community/tool/browse_tool.mako Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,37 @@ +<%namespace file="/message.mako" import="render_msg" /> + +<%! + def inherit(context): + if context.get('use_panels'): + return '/webapps/community/base_panels.mako' + else: + return '/base.mako' +%> +<%inherit file="${inherit(context)}"/> + +<%def name="title()">Browse Tool</%def> + +<h2>Galaxy Tool</h2> + +%if message: + ${render_msg( message, status )} +%endif + +%if not tools: + There are no tools +%else: + <table class="grid"> + <thead> + <tr> + <th>Name</th> + <th>Description</th> + </tr> + </thead> + <tbody> + <tr class="formRow id="toolRow"> + <td><a href="${h.url_for( controller='tool_browser', action='browse', id=trans.security.encode_id( tool.id ) )}">${tool.name}</a></td> + <td>${tool.description}</td> + </tr> + </tbody> + </table> +%endif diff -r 4c740255b9e7 -r f905e1415dd4 templates/webapps/community/tool/grid.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/webapps/community/tool/grid.mako Wed Apr 14 10:20:32 2010 -0400 @@ -0,0 +1,1 @@ +<%inherit file="/grid_base.mako"/>
participants (1)
-
Greg Von Kuster