7 new commits in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/83b09f742d31/
Changeset: 83b09f742d31
User: dannon
Date: 2013-04-17 01:01:32
Summary: Refactor api auth logic, again. This should resolve remote_user API issues and correctly allow access by key or session. This also re-allows access to the API *without* a session in the case of key auth -- probably need to review and make sure we fail gracefully if we were counting on that anywhere
Affected #: 2 files
diff -r 46133ca43f322fe8b042ac0c0b393fe72579cecc -r 83b09f742d3134168ef3925c712e1c0e54f0aeb7 lib/galaxy/web/framework/__init__.py
--- a/lib/galaxy/web/framework/__init__.py
+++ b/lib/galaxy/web/framework/__init__.py
@@ -112,34 +112,25 @@
def expose_api_anonymous( func, to_json=True ):
"""
- Expose this function via the API but don't require an API key.
+ Expose this function via the API but don't require a set user.
"""
- return expose_api( func, to_json=to_json, key_required=False )
+ return expose_api( func, to_json=to_json, user_required=False )
-def expose_api( func, to_json=True, key_required=True ):
+def expose_api( func, to_json=True, user_required=True ):
+ """
+ Expose this function via the API.
+ """
@wraps(func)
def decorator( self, trans, *args, **kwargs ):
def error( environ, start_response ):
start_response( error_status, [('Content-type', 'text/plain')] )
return error_message
error_status = '403 Forbidden'
- #If no key supplied, we use the existing session which may be an anonymous user.
- if key_required and not trans.user:
- try:
- if 'key' not in kwargs:
- raise NoResultFound( 'No key provided' )
- provided_key = trans.sa_session.query( trans.app.model.APIKeys ).filter( trans.app.model.APIKeys.table.c.key == kwargs['key'] ).one()
- except NoResultFound:
- error_message = 'Provided API key is not valid.'
- return error
- if provided_key.user.deleted:
- error_message = 'User account is deactivated, please contact an administrator.'
- return error
- newest_key = provided_key.user.api_keys[0]
- if newest_key.key != provided_key.key:
- error_message = 'Provided API key has expired.'
- return error
- trans.set_user( provided_key.user )
+ if trans.error_message:
+ return trans.error_message
+ if user_required and trans.user is None:
+ error_message = "API Authentication Required for this request"
+ return error
if trans.request.body:
def extract_payload_from_request(trans, func, kwargs):
content_type = trans.request.headers['content-type']
@@ -238,6 +229,7 @@
def form( *args, **kwargs ):
return FormBuilder( *args, **kwargs )
+
class WebApplication( base.WebApplication ):
def __init__( self, galaxy_app, session_cookie='galaxysession', name=None ):
@@ -326,6 +318,7 @@
controller_name = getattr( T, "controller_name", name )
self.add_api_controller( controller_name, T( app ) )
+
class GalaxyWebTransaction( base.DefaultWebTransaction ):
"""
Encapsulates web transaction specific state for the Galaxy application
@@ -346,13 +339,26 @@
# Flag indicating whether this is an API call and the API key user is an administrator
self.api_inherit_admin = False
self.__user = None
- # Always have a valid galaxy session
- self._ensure_valid_session( session_cookie )
- # Prevent deleted users from accessing Galaxy
- if self.app.config.use_remote_user and self.galaxy_session.user.deleted:
- self.response.send_redirect( url_for( '/static/user_disabled.html' ) )
- if self.app.config.require_login:
- self._ensure_logged_in_user( environ, session_cookie )
+ self.galaxy_session = None
+ self.error_message = None
+ if self.environ.get('is_api_request', False):
+ # With API requests, if there's a key, use it and associate the
+ # user with the transaction.
+ # If not, check for an active session but do not create one.
+ # If an error message is set here, it's sent back using
+ # trans.show_error in the response -- in expose_api.
+ self.error_message = self._authenticate_api( session_cookie )
+ else:
+ #This is a web request, get or create session.
+ self._ensure_valid_session( session_cookie )
+ if self.galaxy_session:
+ # When we've authenticated by session, we have to check the
+ # following.
+ # Prevent deleted users from accessing Galaxy
+ if self.app.config.use_remote_user and self.galaxy_session.user.deleted:
+ self.response.send_redirect( url_for( '/static/user_disabled.html' ) )
+ if self.app.config.require_login:
+ self._ensure_logged_in_user( environ, session_cookie )
def setup_i18n( self ):
locales = []
@@ -382,16 +388,17 @@
def get_user( self ):
"""Return the current user if logged in or None."""
- if self.__user:
+ if self.galaxy_session:
+ return self.galaxy_session.user
+ else:
return self.__user
- else:
- return self.galaxy_session.user
def set_user( self, user ):
"""Set the current user."""
- self.galaxy_session.user = user
- self.sa_session.add( self.galaxy_session )
- self.sa_session.flush()
+ if self.galaxy_session:
+ self.galaxy_session.user = user
+ self.sa_session.add( self.galaxy_session )
+ self.sa_session.flush()
self.__user = user
user = property( get_user, set_user )
@@ -473,6 +480,33 @@
except CookieError, e:
log.warning( "Error setting httponly attribute in cookie '%s': %s" % ( name, e ) )
+ def _authenticate_api( self, session_cookie ):
+ """
+ Authenticate for the API via key or session (if available).
+ """
+ api_key = self.request.params.get('key', None)
+ secure_id = self.get_cookie( name=session_cookie )
+ if self.environ.get('is_api_request', False) and api_key:
+ # Sessionless API transaction, we just need to associate a user.
+ try:
+ provided_key = self.sa_session.query( self.app.model.APIKeys ).filter( self.app.model.APIKeys.table.c.key == api_key ).one()
+ except NoResultFound:
+ return 'Provided API key is not valid.'
+ if provided_key.user.deleted:
+ return 'User account is deactivated, please contact an administrator.'
+ newest_key = provided_key.user.api_keys[0]
+ if newest_key.key != provided_key.key:
+ return 'Provided API key has expired.'
+ self.set_user( provided_key.user )
+ elif secure_id:
+ # API authentication via active session
+ # Associate user using existing session
+ self._ensure_valid_session( session_cookie )
+ else:
+ # Anonymous API interaction -- anything but @expose_api_anonymous will fail past here.
+ self.user = None
+ self.galaxy_session = None
+
def _ensure_valid_session( self, session_cookie, create=True):
"""
Ensure that a valid Galaxy session exists and is available as
@@ -505,26 +539,27 @@
.filter( and_( self.app.model.GalaxySession.table.c.session_key==session_key,
self.app.model.GalaxySession.table.c.is_valid==True ) ) \
.first()
- # If remote user is in use it can invalidate the session, so we need to to check some things now.
- if self.app.config.use_remote_user:
- assert "HTTP_REMOTE_USER" in self.environ, \
- "use_remote_user is set but no HTTP_REMOTE_USER variable"
- remote_user_email = self.environ[ 'HTTP_REMOTE_USER' ]
- if galaxy_session:
- # An existing session, make sure correct association exists
- if galaxy_session.user is None:
- # No user, associate
- galaxy_session.user = self.get_or_create_remote_user( remote_user_email )
- galaxy_session_requires_flush = True
- elif galaxy_session.user.email != remote_user_email:
- # Session exists but is not associated with the correct remote user
- invalidate_existing_session = True
+ # If remote user is in use it can invalidate the session, so we need to to check some things now.
+ if self.app.config.use_remote_user:
+ #If this is an api request, and they've passed a key, we let this go.
+ assert "HTTP_REMOTE_USER" in self.environ, \
+ "use_remote_user is set but no HTTP_REMOTE_USER variable"
+ remote_user_email = self.environ[ 'HTTP_REMOTE_USER' ]
+ if galaxy_session:
+ # An existing session, make sure correct association exists
+ if galaxy_session.user is None:
+ # No user, associate
+ galaxy_session.user = self.get_or_create_remote_user( remote_user_email )
+ galaxy_session_requires_flush = True
+ elif galaxy_session.user.email != remote_user_email:
+ # Session exists but is not associated with the correct remote user
+ invalidate_existing_session = True
+ user_for_new_session = self.get_or_create_remote_user( remote_user_email )
+ log.warning( "User logged in as '%s' externally, but has a cookie as '%s' invalidating session",
+ remote_user_email, galaxy_session.user.email )
+ else:
+ # No session exists, get/create user for new session
user_for_new_session = self.get_or_create_remote_user( remote_user_email )
- log.warning( "User logged in as '%s' externally, but has a cookie as '%s' invalidating session",
- remote_user_email, galaxy_session.user.email )
- else:
- # No session exists, get/create user for new session
- user_for_new_session = self.get_or_create_remote_user( remote_user_email )
else:
if galaxy_session is not None and galaxy_session.user and galaxy_session.user.external:
# Remote user support is not enabled, but there is an existing
@@ -605,6 +640,7 @@
pass
if self.request.path not in allowed_paths:
self.response.send_redirect( url_for( controller='root', action='index' ) )
+
def __create_new_session( self, prev_galaxy_session=None, user_for_new_session=None ):
"""
Create a new GalaxySession for this request, possibly with a connection
@@ -627,6 +663,7 @@
# The new session should be associated with the user
galaxy_session.user = user_for_new_session
return galaxy_session
+
def get_or_create_remote_user( self, remote_user_email ):
"""
Create a remote user with the email remote_user_email and return it
@@ -671,11 +708,13 @@
self.app.security_agent.user_set_default_permissions( user )
#self.log_event( "Automatically created account '%s'", user.email )
return user
+
def __update_session_cookie( self, name='galaxysession' ):
"""
Update the session cookie to match the current session.
"""
self.set_cookie( self.security.encode_guid( self.galaxy_session.session_key ), name=name, path=self.app.config.cookie_path )
+
def handle_user_login( self, user ):
"""
Login a new user (possibly newly created)
@@ -701,9 +740,9 @@
except:
users_last_session = None
last_accessed = False
- if prev_galaxy_session.current_history and \
- not prev_galaxy_session.current_history.deleted and \
- prev_galaxy_session.current_history.datasets:
+ if (prev_galaxy_session.current_history and not
+ prev_galaxy_session.current_history.deleted and
+ prev_galaxy_session.current_history.datasets):
if prev_galaxy_session.current_history.user is None or prev_galaxy_session.current_history.user == user:
# If the previous galaxy session had a history, associate it with the new
# session, but only if it didn't belong to a different user.
@@ -714,10 +753,9 @@
user.total_disk_usage += hda.quota_amount( user )
elif self.galaxy_session.current_history:
history = self.galaxy_session.current_history
- if not history and \
- users_last_session and \
- users_last_session.current_history and \
- not users_last_session.current_history.deleted:
+ if (not history and users_last_session and
+ users_last_session.current_history and not
+ users_last_session.current_history.deleted):
history = users_last_session.current_history
elif not history:
history = self.get_history( create=True )
@@ -736,6 +774,8 @@
self.sa_session.flush()
# This method is not called from the Galaxy reports, so the cookie will always be galaxysession
self.__update_session_cookie( name=cookie_name )
+
+
def handle_user_logout( self, logout_all=False ):
"""
Logout the current user:
@@ -988,6 +1028,7 @@
return True
return False
+
class FormBuilder( object ):
"""
Simple class describing an HTML form
@@ -999,17 +1040,22 @@
self.submit_text = submit_text
self.inputs = []
self.use_panels = use_panels
+
def add_input( self, type, name, label, value=None, error=None, help=None, use_label=True ):
self.inputs.append( FormInput( type, label, name, value, error, help, use_label ) )
return self
+
def add_text( self, name, label, value=None, error=None, help=None ):
return self.add_input( 'text', label, name, value, error, help )
+
def add_password( self, name, label, value=None, error=None, help=None ):
return self.add_input( 'password', label, name, value, error, help )
+
def add_select( self, name, label, value=None, options=[], error=None, help=None, use_label=True ):
self.inputs.append( SelectInput( name, label, value=value, options=options, error=error, help=help, use_label=use_label ) )
return self
+
class FormInput( object ):
"""
Simple class describing a form input element
@@ -1023,12 +1069,14 @@
self.help = help
self.use_label = use_label
+
class SelectInput( FormInput ):
""" A select form input. """
def __init__( self, name, label, value=None, options=[], error=None, help=None, use_label=True ):
FormInput.__init__( self, "select", name, label, value=value, error=error, help=help, use_label=use_label )
self.options = options
+
class FormData( object ):
"""
Class for passing data about a form to a template, very rudimentary, could
@@ -1038,6 +1086,7 @@
self.values = Bunch()
self.errors = Bunch()
+
class Bunch( dict ):
"""
Bunch based on a dict
@@ -1047,3 +1096,4 @@
return self[key]
def __setattr__( self, key, value ):
self[key] = value
+
diff -r 46133ca43f322fe8b042ac0c0b393fe72579cecc -r 83b09f742d3134168ef3925c712e1c0e54f0aeb7 lib/galaxy/web/framework/base.py
--- a/lib/galaxy/web/framework/base.py
+++ b/lib/galaxy/web/framework/base.py
@@ -138,6 +138,7 @@
environ[ 'is_api_request' ] = True
controllers = self.api_controllers
else:
+ environ[ 'is_api_request' ] = False
controllers = self.controllers
if map == None:
raise httpexceptions.HTTPNotFound( "No route for " + path_info )
https://bitbucket.org/galaxy/galaxy-central/commits/bf323c3cf3c2/
Changeset: bf323c3cf3c2
User: dannon
Date: 2013-04-17 01:03:39
Summary: Cleanup spacing in framework/base.py
Affected #: 1 file
diff -r 83b09f742d3134168ef3925c712e1c0e54f0aeb7 -r bf323c3cf3c28ebbecb7491abb87e58bf7f7a028 lib/galaxy/web/framework/base.py
--- a/lib/galaxy/web/framework/base.py
+++ b/lib/galaxy/web/framework/base.py
@@ -44,20 +44,21 @@
self.resource( member_name, collection_name, **kwargs )
routes.Mapper.resource_with_deleted = __resource_with_deleted
+
class WebApplication( object ):
"""
A simple web application which maps requests to objects using routes,
- and to methods on those objects in the CherryPy style. Thus simple
+ and to methods on those objects in the CherryPy style. Thus simple
argument mapping in the CherryPy style occurs automatically, but more
complicated encoding of arguments in the PATH_INFO can be performed
with routes.
"""
def __init__( self ):
"""
- Create a new web application object. To actually connect some
- controllers use `add_controller` and `add_route`. Call
+ Create a new web application object. To actually connect some
+ controllers use `add_controller` and `add_route`. Call
`finalize_config` when all controllers and routes have been added
- and `__call__` to handle a request (WSGI style).
+ and `__call__` to handle a request (WSGI style).
"""
self.controllers = dict()
self.api_controllers = dict()
@@ -69,36 +70,41 @@
self.transaction_factory = DefaultWebTransaction
# Set if trace logging is enabled
self.trace_logger = None
+
def add_ui_controller( self, controller_name, controller ):
"""
Add a controller class to this application. A controller class has
methods which handle web requests. To connect a URL to a controller's
method use `add_route`.
"""
- log.debug( "Enabling '%s' controller, class: %s",
+ log.debug( "Enabling '%s' controller, class: %s",
controller_name, controller.__class__.__name__ )
self.controllers[ controller_name ] = controller
+
def add_api_controller( self, controller_name, controller ):
log.debug( "Enabling '%s' API controller, class: %s",
controller_name, controller.__class__.__name__ )
self.api_controllers[ controller_name ] = controller
+
def add_route( self, route, **kwargs ):
"""
Add a route to match a URL with a method. Accepts all keyword
arguments of `routes.Mapper.connect`. Every route should result in
- at least a controller value which corresponds to one of the
- objects added with `add_controller`. It optionally may yield an
+ at least a controller value which corresponds to one of the
+ objects added with `add_controller`. It optionally may yield an
`action` argument which will be used to locate the method to call
on the controller. Additional arguments will be passed to the
- method as keyword args.
+ method as keyword args.
"""
self.mapper.connect( route, **kwargs )
+
def set_transaction_factory( self, transaction_factory ):
"""
Use the callable `transaction_factory` to create the transaction
which will be passed to requests.
"""
self.transaction_factory = transaction_factory
+
def finalize_config( self ):
"""
Call when application is completely configured and ready to serve
@@ -106,6 +112,7 @@
"""
# Create/compile the regular expressions for route mapping
self.mapper.create_regs( self.controllers.keys() )
+
def trace( self, **fields ):
if self.trace_logger:
self.trace_logger.log( "WebApplication", **fields )
@@ -173,7 +180,7 @@
raise httpexceptions.HTTPNotFound( "Action not exposed for " + path_info )
# Is the method callable
if not callable( method ):
- raise httpexceptions.HTTPNotFound( "Action not callable for " + path_info )
+ raise httpexceptions.HTTPNotFound( "Action not callable for " + path_info )
# Combine mapper args and query string / form args and call
kwargs = trans.request.params.mixed()
kwargs.update( map )
@@ -196,14 +203,14 @@
elif isinstance( body, tarfile.ExFileObject ):
# Stream the tarfile member back to the browser
body = iterate_file( body )
- start_response( trans.response.wsgi_status(),
+ start_response( trans.response.wsgi_status(),
trans.response.wsgi_headeritems() )
return body
else:
- start_response( trans.response.wsgi_status(),
+ start_response( trans.response.wsgi_status(),
trans.response.wsgi_headeritems() )
return self.make_body_iterable( trans, body )
-
+
def make_body_iterable( self, trans, body ):
if isinstance( body, ( types.GeneratorType, list, tuple ) ):
# Recursively stream the iterable
@@ -223,7 +230,8 @@
Allow handling of exceptions raised in controller methods.
"""
return False
-
+
+
class WSGIEnvironmentProperty( object ):
"""
Descriptor that delegates a property to a key in the environ member of the
@@ -237,6 +245,7 @@
if obj is None: return self
return obj.environ.get( self.key, self.default )
+
class LazyProperty( object ):
"""
Property that replaces itself with a calculated value the first time
@@ -250,12 +259,13 @@
setattr( obj, self.func.func_name, value )
return value
lazy_property = LazyProperty
-
+
+
class DefaultWebTransaction( object ):
"""
- Wraps the state of a single web transaction (request/response cycle).
+ Wraps the state of a single web transaction (request/response cycle).
- TODO: Provide hooks to allow application specific state to be included
+ TODO: Provide hooks to allow application specific state to be included
in here.
"""
def __init__( self, environ ):
@@ -274,7 +284,7 @@
return self.environ['beaker.session']
else:
return None
-
+
# For request.params, override cgi.FieldStorage.make_file to create persistent
# tempfiles. Necessary for externalizing the upload tool. It's a little hacky
# but for performance reasons it's way better to use Paste's tempfile than to
@@ -290,12 +300,13 @@
if self.outerboundary:
self.read_lines_to_outerboundary()
else:
- self.read_lines_to_eof()
+ self.read_lines_to_eof()
cgi.FieldStorage = FieldStorage
+
class Request( webob.Request ):
"""
- Encapsulates an HTTP request.
+ Encapsulates an HTTP request.
"""
def __init__( self, environ ):
"""
@@ -330,7 +341,7 @@
return self.environ['SCRIPT_NAME'] + self.environ['PATH_INFO']
@lazy_property
def browser_url( self ):
- return self.base + self.path
+ return self.base + self.path
# Descriptors that map properties to the associated environment
## scheme = WSGIEnvironmentProperty( 'wsgi.url_scheme' )
## remote_addr = WSGIEnvironmentProperty( 'REMOTE_ADDR' )
@@ -341,6 +352,7 @@
## query_string = WSGIEnvironmentProperty( 'QUERY_STRING' )
## path_info = WSGIEnvironmentProperty( 'PATH_INFO' )
+
class Response( object ):
"""
Describes an HTTP response. Currently very simple since the actual body
@@ -353,18 +365,22 @@
self.status = "200 OK"
self.headers = HeaderDict( { "content-type": "text/html" } )
self.cookies = SimpleCookie()
+
def set_content_type( self, type ):
"""
Sets the Content-Type header
"""
self.headers[ "content-type" ] = type
+
def get_content_type( self ):
return self.headers[ "content-type" ]
+
def send_redirect( self, url ):
"""
Send an HTTP redirect response to (target `url`)
"""
raise httpexceptions.HTTPFound( url, headers=self.wsgi_headeritems() )
+
def wsgi_headeritems( self ):
"""
Return headers in format appropriate for WSGI `start_response`
@@ -376,10 +392,11 @@
header, value = str( crumb ).split( ': ', 1 )
result.append( ( header, value ) )
return result
+
def wsgi_status( self ):
"""
Return status line in format appropriate for WSGI `start_response`
- """
+ """
if isinstance( self.status, int ):
exception = httpexceptions.get_exception( self.status )
return "%d %s" % ( exception.code, exception.title )
@@ -404,7 +421,7 @@
# Fall back on sending the file in chunks
else:
body = iterate_file( body )
- start_response( trans.response.wsgi_status(),
+ start_response( trans.response.wsgi_status(),
trans.response.wsgi_headeritems() )
return body
https://bitbucket.org/galaxy/galaxy-central/commits/356965791008/
Changeset: 356965791008
User: dannon
Date: 2013-04-17 01:06:42
Summary: Cleanup in framework/__init__.py
Affected #: 1 file
diff -r bf323c3cf3c28ebbecb7491abb87e58bf7f7a028 -r 356965791008f24aa26dc8043259765dcc0e7f7e lib/galaxy/web/framework/__init__.py
--- a/lib/galaxy/web/framework/__init__.py
+++ b/lib/galaxy/web/framework/__init__.py
@@ -713,7 +713,8 @@
"""
Update the session cookie to match the current session.
"""
- self.set_cookie( self.security.encode_guid( self.galaxy_session.session_key ), name=name, path=self.app.config.cookie_path )
+ self.set_cookie( self.security.encode_guid(self.galaxy_session.session_key ),
+ name=name, path=self.app.config.cookie_path )
def handle_user_login( self, user ):
"""
@@ -800,11 +801,13 @@
self.__update_session_cookie( name='galaxysession' )
elif self.webapp.name == 'tool_shed':
self.__update_session_cookie( name='galaxycommunitysession' )
+
def get_galaxy_session( self ):
"""
Return the current galaxy session
"""
return self.galaxy_session
+
def get_history( self, create=False ):
"""
Load the current history, creating a new one only if there is not
@@ -819,12 +822,15 @@
log.debug( "This request returned None from get_history(): %s" % self.request.browser_url )
return None
return history
+
def set_history( self, history ):
if history and not history.deleted:
self.galaxy_session.current_history = history
self.sa_session.add( self.galaxy_session )
self.sa_session.flush()
+
history = property( get_history, set_history )
+
def new_history( self, name=None ):
"""
Create a new history and associate it with the current session and
@@ -849,6 +855,7 @@
self.sa_session.add_all( ( self.galaxy_session, history ) )
self.sa_session.flush()
return history
+
def get_current_user_roles( self ):
user = self.get_user()
if user:
@@ -856,27 +863,34 @@
else:
roles = []
return roles
+
def user_is_admin( self ):
if self.api_inherit_admin:
return True
admin_users = [ x.strip() for x in self.app.config.get( "admin_users", "" ).split( "," ) ]
return self.user and admin_users and self.user.email in admin_users
+
def user_can_do_run_as( self ):
run_as_users = self.app.config.get( "api_allow_run_as", "" ).split( "," )
return self.user and run_as_users and self.user.email in run_as_users
+
def get_toolbox(self):
"""Returns the application toolbox"""
return self.app.toolbox
+
@base.lazy_property
def template_context( self ):
return dict()
+
@property
def model( self ):
return self.app.model
+
def make_form_data( self, name, **kwargs ):
rval = self.template_context[name] = FormData()
rval.values.update( kwargs )
return rval
+
def set_message( self, message, type=None ):
"""
Convenience method for setting the 'message' and 'message_type'
@@ -885,12 +899,14 @@
self.template_context['message'] = message
if type:
self.template_context['status'] = type
+
def get_message( self ):
"""
Convenience method for getting the 'message' element of the template
context.
"""
return self.template_context['message']
+
def show_message( self, message, type='info', refresh_frames=[], cont=None, use_panels=False, active_view="" ):
"""
Convenience method for displaying a simple page with a single message.
@@ -902,21 +918,25 @@
refreshed when the message is displayed
"""
return self.fill_template( "message.mako", status=type, message=message, refresh_frames=refresh_frames, cont=cont, use_panels=use_panels, active_view=active_view )
+
def show_error_message( self, message, refresh_frames=[], use_panels=False, active_view="" ):
"""
Convenience method for displaying an error message. See `show_message`.
"""
return self.show_message( message, 'error', refresh_frames, use_panels=use_panels, active_view=active_view )
+
def show_ok_message( self, message, refresh_frames=[], use_panels=False, active_view="" ):
"""
Convenience method for displaying an ok message. See `show_message`.
"""
return self.show_message( message, 'done', refresh_frames, use_panels=use_panels, active_view=active_view )
+
def show_warn_message( self, message, refresh_frames=[], use_panels=False, active_view="" ):
"""
Convenience method for displaying an warn message. See `show_message`.
"""
return self.show_message( message, 'warning', refresh_frames, use_panels=use_panels, active_view=active_view )
+
def show_form( self, form, header=None, template="form.mako", use_panels=False, active_view="" ):
"""
Convenience method for displaying a simple page with a single HTML
@@ -924,6 +944,7 @@
"""
return self.fill_template( template, form=form, header=header, use_panels=( form.use_panels or use_panels ),
active_view=active_view )
+
def fill_template(self, filename, **kwargs):
"""
Fill in a template, putting any keyword arguments on the context.
@@ -937,6 +958,7 @@
template = Template( file=os.path.join(self.app.config.template_path, filename),
searchList=[kwargs, self.template_context, dict(caller=self, t=self, h=helpers, util=util, request=self.request, response=self.response, app=self.app)] )
return str( template )
+
def fill_template_mako( self, filename, **kwargs ):
template = self.webapp.mako_template_lookup.get_template( filename )
template.output_encoding = 'utf-8'
@@ -944,6 +966,7 @@
data.update( self.template_context )
data.update( kwargs )
return template.render( **data )
+
def stream_template_mako( self, filename, **kwargs ):
template = self.webapp.mako_template_lookup.get_template( filename )
template.output_encoding = 'utf-8'
@@ -961,6 +984,7 @@
template.render_context( context )
return []
return render
+
def fill_template_string(self, template_string, context=None, **kwargs):
"""
Fill in a template, putting any keyword arguments on the context.
https://bitbucket.org/galaxy/galaxy-central/commits/18c73eeb77fe/
Changeset: 18c73eeb77fe
User: dannon
Date: 2013-04-17 01:07:02
Summary: Merge with galaxy-central
Affected #: 1 file
diff -r 356965791008f24aa26dc8043259765dcc0e7f7e -r 18c73eeb77feba055b2240412006dc3ce2e49fb0 lib/galaxy/visualization/data_providers/genome.py
--- a/lib/galaxy/visualization/data_providers/genome.py
+++ b/lib/galaxy/visualization/data_providers/genome.py
@@ -770,6 +770,10 @@
else:
pos = source.tell()
+ # If last line is a comment, there are no data lines.
+ if line.startswith( "#" ):
+ return []
+
# Match chrom naming format.
if line:
dataset_chrom = line.split()[0]
@@ -1272,7 +1276,7 @@
class IntervalIndexDataProvider( FilterableMixin, GenomeDataProvider ):
"""
- Interval index files used only for GFF files.
+ Interval index files used for GFF, Pileup files.
"""
col_name_data_attr_mapping = { 4 : { 'index': 4 , 'name' : 'Score' } }
@@ -1282,20 +1286,26 @@
source = open( self.original_dataset.file_name )
index = Indexes( self.converted_dataset.file_name )
out = open( filename, 'w' )
-
+
for region in regions:
# Write data from region.
chrom = region.chrom
start = region.start
end = region.end
- for start, end, offset in index.find(chrom, start, end):
+ for start, end, offset in index.find( chrom, start, end ):
source.seek( offset )
-
- reader = GFFReaderWrapper( source, fix_strand=True )
- feature = reader.next()
- for interval in feature.intervals:
- out.write( '\t'.join( interval.fields ) + '\n' )
-
+
+ # HACK: write differently depending on original dataset format.
+ if self.original_dataset.ext not in [ 'gff', 'gff3', 'gtf' ]:
+ line = source.readline()
+ out.write( line )
+ else:
+ reader = GFFReaderWrapper( source, fix_strand=True )
+ feature = reader.next()
+ for interval in feature.intervals:
+ out.write( '\t'.join( interval.fields ) + '\n' )
+
+ source.close()
out.close()
def get_iterator( self, chrom, start, end, **kwargs ):
https://bitbucket.org/galaxy/galaxy-central/commits/94caae7433a7/
Changeset: 94caae7433a7
User: dannon
Date: 2013-04-17 03:38:38
Summary: Disable select2 for Input Dataset steps on the run workflow page.
Affected #: 1 file
Diff not available.
https://bitbucket.org/galaxy/galaxy-central/commits/60d7b1a9ebec/
Changeset: 60d7b1a9ebec
User: dannon
Date: 2013-04-17 03:47:17
Summary: Update multiselect tooltip, now functional. Was still using tipsy's 'original-title' attribute.
Affected #: 1 file
Diff not available.
https://bitbucket.org/galaxy/galaxy-central/commits/ec36ff4ec0f6/
Changeset: ec36ff4ec0f6
User: dannon
Date: 2013-04-17 04:27:07
Summary: Add tooltips for (un)linking icon on run workflow page.
Affected #: 1 file
Diff not available.
Repository URL: https://bitbucket.org/galaxy/galaxy-central/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/98b6216e42f5/
Changeset: 98b6216e42f5
User: jgoecks
Date: 2013-04-17 01:01:50
Summary: Enable IntervalIndex data provider to work with pileup datasets.
Affected #: 1 file
diff -r 46133ca43f322fe8b042ac0c0b393fe72579cecc -r 98b6216e42f5beec89052cccf7c98820b978bf92 lib/galaxy/visualization/data_providers/genome.py
--- a/lib/galaxy/visualization/data_providers/genome.py
+++ b/lib/galaxy/visualization/data_providers/genome.py
@@ -770,6 +770,10 @@
else:
pos = source.tell()
+ # If last line is a comment, there are no data lines.
+ if line.startswith( "#" ):
+ return []
+
# Match chrom naming format.
if line:
dataset_chrom = line.split()[0]
@@ -1272,7 +1276,7 @@
class IntervalIndexDataProvider( FilterableMixin, GenomeDataProvider ):
"""
- Interval index files used only for GFF files.
+ Interval index files used for GFF, Pileup files.
"""
col_name_data_attr_mapping = { 4 : { 'index': 4 , 'name' : 'Score' } }
@@ -1282,20 +1286,26 @@
source = open( self.original_dataset.file_name )
index = Indexes( self.converted_dataset.file_name )
out = open( filename, 'w' )
-
+
for region in regions:
# Write data from region.
chrom = region.chrom
start = region.start
end = region.end
- for start, end, offset in index.find(chrom, start, end):
+ for start, end, offset in index.find( chrom, start, end ):
source.seek( offset )
-
- reader = GFFReaderWrapper( source, fix_strand=True )
- feature = reader.next()
- for interval in feature.intervals:
- out.write( '\t'.join( interval.fields ) + '\n' )
-
+
+ # HACK: write differently depending on original dataset format.
+ if self.original_dataset.ext not in [ 'gff', 'gff3', 'gtf' ]:
+ line = source.readline()
+ out.write( line )
+ else:
+ reader = GFFReaderWrapper( source, fix_strand=True )
+ feature = reader.next()
+ for interval in feature.intervals:
+ out.write( '\t'.join( interval.fields ) + '\n' )
+
+ source.close()
out.close()
def get_iterator( self, chrom, start, end, **kwargs ):
Repository URL: https://bitbucket.org/galaxy/galaxy-central/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
1 new commit in galaxy-central:
https://bitbucket.org/galaxy/galaxy-central/commits/95c90c1d9029/
Changeset: 95c90c1d9029
User: jgoecks
Date: 2013-04-16 20:36:27
Summary: Rewrite RawVcfDataProvider for completeness and efficiency.
Affected #: 1 file
diff -r 6e91867769eab9e97353cd0e6363393c8077aadc -r 95c90c1d90294032a0db5f3cb9312a967ac00e49 lib/galaxy/visualization/data_providers/genome.py
--- a/lib/galaxy/visualization/data_providers/genome.py
+++ b/lib/galaxy/visualization/data_providers/genome.py
@@ -759,29 +759,41 @@
"""
def get_iterator( self, chrom, start, end, **kwargs ):
- # Read first line in order to match chrom naming format.
- line = source.readline()
- dataset_chrom = line.split()[0]
- if not _chrom_naming_matches( chrom, dataset_chrom ):
- chrom = _convert_between_ucsc_and_ensemble_naming( chrom )
- # Undo read.
- source.seek( 0 )
+ source = open( self.original_dataset.file_name )
+
+ # Skip comments.
+ pos = 0
+ line = None
+ for line in source:
+ if not line.startswith("#"):
+ break
+ else:
+ pos = source.tell()
+
+ # Match chrom naming format.
+ if line:
+ dataset_chrom = line.split()[0]
+ if not _chrom_naming_matches( chrom, dataset_chrom ):
+ chrom = _convert_between_ucsc_and_ensemble_naming( chrom )
+
+ def line_in_region( vcf_line, chrom, start, end ):
+ """ Returns true if line is in region. """
+ variant_chrom, variant_start = vcf_line.split()[ 0:2 ]
+ # VCF format is 1-based.
+ variant_start = int( variant_start ) - 1
+ return variant_chrom == chrom and variant_start >= start and variant_start <= end
def line_filter_iter():
- for line in open( self.original_dataset.file_name ):
- if line.startswith("#"):
- continue
- variant = line.split()
- variant_chrom, variant_start, id, ref, alts = variant[ 0:5 ]
- variant_start = int( variant_start )
- longest_alt = -1
- for alt in alts:
- if len( alt ) > longest_alt:
- longest_alt = len( alt )
- variant_end = variant_start + abs( len( ref ) - longest_alt )
- if variant_chrom != chrom or variant_start > end or variant_end < start:
- continue
+ """ Yields lines in source that are in region chrom:start-end """
+ # Yield data line read above.
+ if line_in_region( line, chrom, start, end ):
yield line
+
+ # Search for and yield other data lines.
+ for data_line in source:
+ if line_in_region( data_line, chrom, start, end ):
+ print chrom, start, end, ">>>", data_line,
+ yield data_line
return line_filter_iter()
Repository URL: https://bitbucket.org/galaxy/galaxy-central/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.