Extract some mixin's out of trans allowing trans-like functionilty outside web threads...

In particular break out mixins defining context-based utilities for interacting with app, an user, and a history. In downstream work on workflow scheduling this proves nessecary and sufficient to create an context for 'execute'-ing tools (create jobs) outside of web threads in response to workflow requests.

Unit tests for some of this.
This commit is contained in:
John Chilton
2014-08-18 10:28:40 -04:00
parent 283142c2b3
commit a5a13547cd
2 changed files with 246 additions and 146 deletions
+172 -146
View File
@@ -491,7 +491,178 @@ class WebApplication( base.WebApplication ):
return T( app )
class GalaxyWebTransaction( base.DefaultWebTransaction ):
class ProvidesAppContext( object ):
""" For transaction-like objects to provide Galaxy convience layer for
database and event handling.
Mixed in class must provide `app` property.
"""
def log_action( self, user=None, action=None, context=None, params=None):
"""
Application-level logging of user actions.
"""
if self.app.config.log_actions:
action = self.app.model.UserAction(action=action, context=context, params=unicode( to_json_string( params ) ) )
try:
if user:
action.user = user
else:
action.user = self.user
except:
action.user = None
try:
action.session_id = self.galaxy_session.id
except:
action.session_id = None
self.sa_session.add( action )
self.sa_session.flush()
def log_event( self, message, tool_id=None, **kwargs ):
"""
Application level logging. Still needs fleshing out (log levels and such)
Logging events is a config setting - if False, do not log.
"""
if self.app.config.log_events:
event = self.app.model.Event()
event.tool_id = tool_id
try:
event.message = message % kwargs
except:
event.message = message
try:
event.history = self.get_history()
except:
event.history = None
try:
event.history_id = self.history.id
except:
event.history_id = None
try:
event.user = self.user
except:
event.user = None
try:
event.session_id = self.galaxy_session.id
except:
event.session_id = None
self.sa_session.add( event )
self.sa_session.flush()
@property
def sa_session( self ):
"""
Returns a SQLAlchemy session -- currently just gets the current
session from the threadlocal session context, but this is provided
to allow migration toward a more SQLAlchemy 0.4 style of use.
"""
return self.app.model.context.current
def expunge_all( self ):
app = self.app
context = app.model.context
context.expunge_all()
# This is a bit hacky, should refctor this. Maybe refactor to app -> expunge_all()
if hasattr(app, 'install_model'):
install_model = app.install_model
if install_model != app.model:
install_model.context.expunge_all()
def get_toolbox(self):
"""Returns the application toolbox"""
return self.app.toolbox
@property
def model( self ):
return self.app.model
@property
def install_model( self ):
return self.app.install_model
def request_types(self):
if self.sa_session.query( self.app.model.RequestType ).filter_by( deleted=False ).count() > 0:
return True
return False
class ProvidesUserContext( object ):
""" For transaction-like objects to provide Galaxy convience layer for
reasoning about users.
Mixed in class must provide `user`, `api_inherit_admin`, and `app`
properties.
"""
@property
def anonymous( self ):
return self.user is None and not self.api_inherit_admin
def get_current_user_roles( self ):
user = self.user
if user:
roles = user.all_roles()
else:
roles = []
return roles
def user_is_admin( self ):
if self.api_inherit_admin:
return True
return self.user and self.user.email in self.app.config.admin_users_list
def user_can_do_run_as( self ):
run_as_users = [ user for user in self.app.config.get( "api_allow_run_as", "" ).split( "," ) if user ]
if not run_as_users:
return False
user_in_run_as_users = self.user and self.user.email in run_as_users
# Can do if explicitly in list or master_api_key supplied.
can_do_run_as = user_in_run_as_users or self.api_inherit_admin
return can_do_run_as
@property
def user_ftp_dir( self ):
identifier = self.app.config.ftp_upload_dir_identifier
return os.path.join( self.app.config.ftp_upload_dir, getattr(self.user, identifier) )
class ProvidesHistoryContext( object ):
""" For transaction-like objects to provide Galaxy convience layer for
reasoning about histories.
Mixed in class must provide `user`, `history`, and `app`
properties.
"""
def db_dataset_for( self, dbkey ):
"""
Returns the db_file dataset associated/needed by `dataset`, or `None`.
"""
# If no history, return None.
if self.history is None:
return None
if isinstance(self.history, Bunch):
# The API presents a Bunch for a history. Until the API is
# more fully featured for handling this, also return None.
return None
datasets = self.sa_session.query( self.app.model.HistoryDatasetAssociation ) \
.filter_by( deleted=False, history_id=self.history.id, extension="len" )
for ds in datasets:
if dbkey == ds.dbkey:
return ds
return None
@property
def db_builds( self ):
"""
Returns the builds defined by galaxy and the builds defined by
the user (chromInfo in history).
"""
# FIXME: This method should be removed
return self.app.genome_builds.get_genome_build_names( trans=self )
class GalaxyWebTransaction( base.DefaultWebTransaction, ProvidesAppContext, ProvidesUserContext, ProvidesHistoryContext ):
"""
Encapsulates web transaction specific state for the Galaxy application
(specifically the user's "cookie" session and history)
@@ -552,29 +723,6 @@ class GalaxyWebTransaction( base.DefaultWebTransaction ):
t = Translations.load( dirname='locale', locales=locales, domain='ginga' )
self.template_context.update( dict( _=t.ugettext, n_=t.ugettext, N_=t.ungettext ) )
@property
def anonymous( self ):
return self.user is None and not self.api_inherit_admin
@property
def sa_session( self ):
"""
Returns a SQLAlchemy session -- currently just gets the current
session from the threadlocal session context, but this is provided
to allow migration toward a more SQLAlchemy 0.4 style of use.
"""
return self.app.model.context.current
def expunge_all( self ):
app = self.app
context = app.model.context
context.expunge_all()
# This is a bit hacky, should refctor this. Maybe refactor to app -> expunge_all()
if hasattr(app, 'install_model'):
install_model = app.install_model
if install_model != app.model:
install_model.context.expunge_all()
def get_user( self ):
"""Return the current user if logged in or None."""
if self.galaxy_session:
@@ -592,57 +740,6 @@ class GalaxyWebTransaction( base.DefaultWebTransaction ):
user = property( get_user, set_user )
def log_action( self, user=None, action=None, context=None, params=None):
"""
Application-level logging of user actions.
"""
if self.app.config.log_actions:
action = self.app.model.UserAction(action=action, context=context, params=unicode( to_json_string( params ) ) )
try:
if user:
action.user = user
else:
action.user = self.user
except:
action.user = None
try:
action.session_id = self.galaxy_session.id
except:
action.session_id = None
self.sa_session.add( action )
self.sa_session.flush()
def log_event( self, message, tool_id=None, **kwargs ):
"""
Application level logging. Still needs fleshing out (log levels and such)
Logging events is a config setting - if False, do not log.
"""
if self.app.config.log_events:
event = self.app.model.Event()
event.tool_id = tool_id
try:
event.message = message % kwargs
except:
event.message = message
try:
event.history = self.get_history()
except:
event.history = None
try:
event.history_id = self.history.id
except:
event.history_id = None
try:
event.user = self.user
except:
event.user = None
try:
event.session_id = self.galaxy_session.id
except:
event.session_id = None
self.sa_session.add( event )
self.sa_session.flush()
def get_cookie( self, name='galaxysession' ):
"""Convenience method for getting a session cookie"""
try:
@@ -1100,44 +1197,10 @@ class GalaxyWebTransaction( base.DefaultWebTransaction ):
self.sa_session.flush()
return history
def get_current_user_roles( self ):
user = self.get_user()
if user:
roles = user.all_roles()
else:
roles = []
return roles
def user_is_admin( self ):
if self.api_inherit_admin:
return True
return self.user and self.user.email in self.app.config.admin_users_list
def user_can_do_run_as( self ):
run_as_users = [ user for user in self.app.config.get( "api_allow_run_as", "" ).split( "," ) if user ]
if not run_as_users:
return False
user_in_run_as_users = self.user and self.user.email in run_as_users
# Can do if explicitly in list or master_api_key supplied.
can_do_run_as = user_in_run_as_users or self.api_inherit_admin
return can_do_run_as
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
@property
def install_model( self ):
return self.app.install_model
def make_form_data( self, name, **kwargs ):
rval = self.template_context[name] = FormData()
rval.values.update( kwargs )
@@ -1250,43 +1313,6 @@ class GalaxyWebTransaction( base.DefaultWebTransaction ):
searchList=[context or kwargs, dict(caller=self)] )
return str(template)
@property
def db_builds( self ):
"""
Returns the builds defined by galaxy and the builds defined by
the user (chromInfo in history).
"""
# FIXME: This method should be removed
return self.app.genome_builds.get_genome_build_names( trans=self )
@property
def user_ftp_dir( self ):
identifier = self.app.config.ftp_upload_dir_identifier
return os.path.join( self.app.config.ftp_upload_dir, getattr(self.user, identifier) )
def db_dataset_for( self, dbkey ):
"""
Returns the db_file dataset associated/needed by `dataset`, or `None`.
"""
# If no history, return None.
if self.history is None:
return None
if isinstance(self.history, Bunch):
# The API presents a Bunch for a history. Until the API is
# more fully featured for handling this, also return None.
return None
datasets = self.sa_session.query( self.app.model.HistoryDatasetAssociation ) \
.filter_by( deleted=False, history_id=self.history.id, extension="len" )
for ds in datasets:
if dbkey == ds.dbkey:
return ds
return None
def request_types(self):
if self.sa_session.query( self.app.model.RequestType ).filter_by( deleted=False ).count() > 0:
return True
return False
class FormBuilder( object ):
"""
+74
View File
@@ -0,0 +1,74 @@
from galaxy import model
from galaxy.model import mapping
from galaxy.util import bunch
from galaxy.web import framework
class TestTransaction( framework.ProvidesAppContext ):
def __init__( self ):
self.app = TestApp()
def test_logging_events_off():
trans = TestTransaction()
trans.log_event( "test event 123" )
assert len( trans.sa_session.query( model.Event ).all() ) == 0
def test_logging_events_on():
trans = TestTransaction()
trans.app.config.log_events = True
trans.log_event( "test event 123" )
events = trans.sa_session.query( model.Event ).all()
assert len( events ) == 1
assert events[ 0 ].message == "test event 123"
def test_logging_actions_off():
trans = TestTransaction()
trans.log_action( "test action 123" )
assert len( trans.sa_session.query( model.Event ).all() ) == 0
def test_logging_actions_on():
trans = TestTransaction()
trans.app.config.log_actions = True
trans.log_action( None, "test action 123", context="the context", params=dict(foo="bar") )
actions = trans.sa_session.query( model.UserAction ).all()
assert len( actions ) == 1
assert actions[ 0 ].action == "test action 123"
def test_expunge_all():
trans = TestTransaction()
user = model.User( "foo", "bar1" )
trans.sa_session.add( user )
user.password = "bar2"
trans.sa_session.flush()
assert trans.sa_session.query( model.User ).first().password == "bar2"
trans.sa_session.expunge_all()
user.password = "bar3"
trans.sa_session.flush()
# Password unchange because not attached to session/context.
assert trans.sa_session.query( model.User ).first().password == "bar2"
class TestApp( object ):
def __init__( self ):
self.config = bunch.Bunch(
log_events=False,
log_actions=False,
)
self.model = mapping.init(
"/tmp",
"sqlite:///:memory:",
create_tables=True
)