/lib/galaxy/web/base/controller.py
https://bitbucket.org/cistrome/cistrome-harvard/ · Python · 2973 lines · 2365 code · 173 blank · 435 comment · 416 complexity · 44e622a10caafdff829845c52ffeb39f MD5 · raw file
Large files are truncated click here to view the full file
- """
- Contains functionality needed in every web interface
- """
- import logging
- import operator
- import os
- import re
- from gettext import gettext
- import pkg_resources
- pkg_resources.require("SQLAlchemy >= 0.4")
- from sqlalchemy import func, and_, select
- from paste.httpexceptions import HTTPBadRequest, HTTPInternalServerError
- from paste.httpexceptions import HTTPNotImplemented, HTTPRequestRangeNotSatisfiable
- from galaxy import exceptions
- from galaxy.exceptions import ItemAccessibilityException, ItemDeletionException, ItemOwnershipException
- from galaxy.exceptions import MessageException
- from galaxy import web
- from galaxy import model
- from galaxy import security
- from galaxy import util
- from galaxy import objectstore
- from galaxy.web import error, url_for
- from galaxy.web.form_builder import AddressField, CheckboxField, SelectField, TextArea, TextField
- from galaxy.web.form_builder import build_select_field, HistoryField, PasswordField, WorkflowField, WorkflowMappingField
- from galaxy.workflow.modules import module_factory
- from galaxy.model.orm import eagerload, eagerload_all, desc
- from galaxy.security.validate_user_input import validate_publicname
- from galaxy.util.sanitize_html import sanitize_html
- from galaxy.model.item_attrs import Dictifiable, UsesAnnotations
- from galaxy.datatypes.interval import ChromatinInteractions
- from galaxy.datatypes.data import Text
- from galaxy.model import ExtendedMetadata, ExtendedMetadataIndex, LibraryDatasetDatasetAssociation, HistoryDatasetAssociation
- from galaxy.datatypes.metadata import FileParameter
- from galaxy.tools.parameters import RuntimeValue, visit_input_values
- from galaxy.tools.parameters.basic import DataToolParameter
- from galaxy.util.json import to_json_string
- from galaxy.workflow.modules import ToolModule
- from galaxy.workflow.steps import attach_ordered_steps
- log = logging.getLogger( __name__ )
- # States for passing messages
- SUCCESS, INFO, WARNING, ERROR = "done", "info", "warning", "error"
- def _is_valid_slug( slug ):
- """ Returns true if slug is valid. """
- VALID_SLUG_RE = re.compile( "^[a-z0-9\-]+$" )
- return VALID_SLUG_RE.match( slug )
- class BaseController( object ):
- """
- Base class for Galaxy web application controllers.
- """
- def __init__( self, app ):
- """Initialize an interface for application 'app'"""
- self.app = app
- self.sa_session = app.model.context
- def get_toolbox(self):
- """Returns the application toolbox"""
- return self.app.toolbox
- 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 == 'History':
- item_class = self.app.model.History
- elif class_name == 'HistoryDatasetAssociation':
- item_class = self.app.model.HistoryDatasetAssociation
- elif class_name == 'Page':
- item_class = self.app.model.Page
- elif class_name == 'StoredWorkflow':
- item_class = self.app.model.StoredWorkflow
- elif class_name == 'Visualization':
- item_class = self.app.model.Visualization
- elif class_name == 'Tool':
- item_class = self.app.model.Tool
- elif class_name == 'Job':
- item_class = self.app.model.Job
- elif class_name == 'User':
- item_class = self.app.model.User
- elif class_name == 'Group':
- item_class = self.app.model.Group
- elif class_name == 'Role':
- item_class = self.app.model.Role
- elif class_name == 'Quota':
- item_class = self.app.model.Quota
- elif class_name == 'Library':
- item_class = self.app.model.Library
- elif class_name == 'LibraryFolder':
- item_class = self.app.model.LibraryFolder
- elif class_name == 'LibraryDatasetDatasetAssociation':
- item_class = self.app.model.LibraryDatasetDatasetAssociation
- elif class_name == 'LibraryDataset':
- item_class = self.app.model.LibraryDataset
- elif class_name == 'ToolShedRepository':
- item_class = self.app.install_model.ToolShedRepository
- else:
- item_class = None
- return item_class
- def get_object( self, trans, id, class_name, check_ownership=False, check_accessible=False, deleted=None ):
- """
- Convenience method to get a model object with the specified checks.
- """
- try:
- decoded_id = trans.security.decode_id( id )
- except:
- raise MessageException( "Malformed %s id ( %s ) specified, unable to decode"
- % ( class_name, str( id ) ), type='error' )
- try:
- item_class = self.get_class( class_name )
- assert item_class is not None
- item = trans.sa_session.query( item_class ).get( decoded_id )
- assert item is not None
- except Exception, exc:
- log.exception( "Invalid %s id ( %s ) specified: %s" % ( class_name, id, str( exc ) ) )
- raise MessageException( "Invalid %s id ( %s ) specified" % ( class_name, id ), type="error" )
- if check_ownership or check_accessible:
- self.security_check( trans, item, check_ownership, check_accessible )
- if deleted == True and not item.deleted:
- raise ItemDeletionException( '%s "%s" is not deleted'
- % ( class_name, getattr( item, 'name', id ) ), type="warning" )
- elif deleted == False and item.deleted:
- raise ItemDeletionException( '%s "%s" is deleted'
- % ( class_name, getattr( item, 'name', id ) ), type="warning" )
- return item
- # this should be here - but catching errors from sharable item controllers that *should* have SharableItemMixin
- # but *don't* then becomes difficult
- #def security_check( self, trans, item, check_ownership=False, check_accessible=False ):
- # log.warn( 'BaseController.security_check: %s, %b, %b', str( item ), check_ownership, check_accessible )
- # # meant to be overridden in SharableSecurityMixin
- # return item
- def get_user( self, trans, id, check_ownership=False, check_accessible=False, deleted=None ):
- return self.get_object( trans, id, 'User', check_ownership=False, check_accessible=False, deleted=deleted )
- def get_group( self, trans, id, check_ownership=False, check_accessible=False, deleted=None ):
- return self.get_object( trans, id, 'Group', check_ownership=False, check_accessible=False, deleted=deleted )
- def get_role( self, trans, id, check_ownership=False, check_accessible=False, deleted=None ):
- return self.get_object( trans, id, 'Role', check_ownership=False, check_accessible=False, deleted=deleted )
- def encode_all_ids( self, trans, rval, recursive=False ):
- """
- Encodes all integer values in the dict rval whose keys are 'id' or end with '_id'
- It might be useful to turn this in to a decorator
- """
- if type( rval ) != dict:
- return rval
- for k, v in rval.items():
- if (k == 'id' or k.endswith( '_id' )) and v is not None and k not in ['tool_id']:
- try:
- rval[k] = trans.security.encode_id( v )
- except:
- pass # probably already encoded
- if (k.endswith("_ids") and type(v) == list):
- try:
- o = []
- for i in v:
- o.append(trans.security.encode_id( i ))
- rval[k] = o
- except:
- pass
- else:
- if recursive and type(v) == dict:
- rval[k] = self.encode_all_ids(trans, v, recursive)
- return rval
- # incoming param validation
- # should probably be in sep. serializer class/object _used_ by controller
- def validate_and_sanitize_basestring( self, key, val ):
- if not isinstance( val, basestring ):
- raise exceptions.RequestParameterInvalidException( '%s must be a string or unicode: %s'
- %( key, str( type( val ) ) ) )
- return unicode( sanitize_html( val, 'utf-8', 'text/html' ), 'utf-8' )
- def validate_and_sanitize_basestring_list( self, key, val ):
- try:
- assert isinstance( val, list )
- return [ unicode( sanitize_html( t, 'utf-8', 'text/html' ), 'utf-8' ) for t in val ]
- except ( AssertionError, TypeError ), err:
- raise exceptions.RequestParameterInvalidException( '%s must be a list of strings: %s'
- %( key, str( type( val ) ) ) )
- def validate_boolean( self, key, val ):
- if not isinstance( val, bool ):
- raise exceptions.RequestParameterInvalidException( '%s must be a boolean: %s'
- %( key, str( type( val ) ) ) )
- return val
- #TODO:
- #def validate_integer( self, key, val, min, max ):
- #def validate_float( self, key, val, min, max ):
- #def validate_number( self, key, val, min, max ):
- #def validate_genome_build( self, key, val ):
- Root = BaseController
- class BaseUIController( BaseController ):
- def get_object( self, trans, id, class_name, check_ownership=False, check_accessible=False, deleted=None ):
- try:
- return BaseController.get_object( self, trans, id, class_name,
- check_ownership=check_ownership, check_accessible=check_accessible, deleted=deleted )
- except MessageException:
- raise # handled in the caller
- except:
- log.exception( "Execption in get_object check for %s %s:" % ( class_name, str( id ) ) )
- raise Exception( 'Server error retrieving %s id ( %s ).' % ( class_name, str( id ) ) )
- class BaseAPIController( BaseController ):
- def get_object( self, trans, id, class_name, check_ownership=False, check_accessible=False, deleted=None ):
- try:
- return BaseController.get_object( self, trans, id, class_name,
- check_ownership=check_ownership, check_accessible=check_accessible, deleted=deleted )
- except ItemDeletionException, e:
- raise HTTPBadRequest( detail="Invalid %s id ( %s ) specified: %s" % ( class_name, str( id ), str( e ) ) )
- except MessageException, e:
- raise HTTPBadRequest( detail=e.err_msg )
- except Exception, e:
- log.exception( "Execption in get_object check for %s %s: %s" % ( class_name, str( id ), str( e ) ) )
- raise HTTPInternalServerError( comment=str( e ) )
- def validate_in_users_and_groups( self, trans, payload ):
- """
- For convenience, in_users and in_groups can be encoded IDs or emails/group names in the API.
- """
- def get_id( item, model_class, column ):
- try:
- return trans.security.decode_id( item )
- except:
- pass # maybe an email/group name
- # this will raise if the item is invalid
- return trans.sa_session.query( model_class ).filter( column == item ).first().id
- new_in_users = []
- new_in_groups = []
- invalid = []
- for item in util.listify( payload.get( 'in_users', [] ) ):
- try:
- new_in_users.append( get_id( item, trans.app.model.User, trans.app.model.User.table.c.email ) )
- except:
- invalid.append( item )
- for item in util.listify( payload.get( 'in_groups', [] ) ):
- try:
- new_in_groups.append( get_id( item, trans.app.model.Group, trans.app.model.Group.table.c.name ) )
- except:
- invalid.append( item )
- if invalid:
- msg = "The following value(s) for associated users and/or groups could not be parsed: %s." % ', '.join( invalid )
- msg += " Valid values are email addresses of users, names of groups, or IDs of both."
- raise Exception( msg )
- payload['in_users'] = map( str, new_in_users )
- payload['in_groups'] = map( str, new_in_groups )
- def not_implemented( self, trans, **kwd ):
- raise HTTPNotImplemented()
- class Datatype( object ):
- """Used for storing in-memory list of datatypes currently in the datatypes registry."""
- def __init__( self, extension, dtype, type_extension, mimetype, display_in_upload ):
- self.extension = extension
- self.dtype = dtype
- self.type_extension = type_extension
- self.mimetype = mimetype
- self.display_in_upload = display_in_upload
- #
- # -- Mixins for working with Galaxy objects. --
- #
- class CreatesUsersMixin:
- """
- Mixin centralizing logic for user creation between web and API controller.
- Web controller handles additional features such e-mail subscription, activation,
- user forms, etc.... API created users are much more vanilla for the time being.
- """
- def create_user( self, trans, email, username, password ):
- user = trans.app.model.User( email=email )
- user.set_password_cleartext( password )
- user.username = username
- if trans.app.config.user_activation_on:
- user.active = False
- else:
- user.active = True # Activation is off, every new user is active by default.
- trans.sa_session.add( user )
- trans.sa_session.flush()
- trans.app.security_agent.create_private_user_role( user )
- if trans.webapp.name == 'galaxy':
- # We set default user permissions, before we log in and set the default history permissions
- trans.app.security_agent.user_set_default_permissions( user,
- default_access_private=trans.app.config.new_user_dataset_access_role_default_private )
- return user
- class CreatesApiKeysMixin:
- """
- Mixing centralizing logic for creating API keys for user objects.
- """
- def create_api_key( self, trans, user ):
- guid = trans.app.security.get_new_guid()
- new_key = trans.app.model.APIKeys()
- new_key.user_id = user.id
- new_key.key = guid
- trans.sa_session.add( new_key )
- trans.sa_session.flush()
- return guid
- class SharableItemSecurityMixin:
- """ Mixin for handling security for sharable items. """
- def security_check( self, trans, item, check_ownership=False, check_accessible=False ):
- """ Security checks for an item: checks if (a) user owns item or (b) item is accessible to user. """
- # all items are accessible to an admin
- if trans.user and trans.user_is_admin():
- return item
- # Verify ownership: there is a current user and that user is the same as the item's
- if check_ownership:
- if not trans.user:
- raise ItemOwnershipException( "Must be logged in to manage Galaxy items", type='error' )
- if item.user != trans.user:
- raise ItemOwnershipException( "%s is not owned by the current user" % item.__class__.__name__, type='error' )
- # Verify accessible:
- # if it's part of a lib - can they access via security
- # if it's something else (sharable) have they been added to the item's users_shared_with_dot_users
- if check_accessible:
- if type( item ) in ( trans.app.model.LibraryFolder, trans.app.model.LibraryDatasetDatasetAssociation, trans.app.model.LibraryDataset ):
- if not trans.app.security_agent.can_access_library_item( trans.get_current_user_roles(), item, trans.user ):
- raise ItemAccessibilityException( "%s is not accessible to the current user" % item.__class__.__name__, type='error' )
- else:
- if ( item.user != trans.user ) and ( not item.importable ) and ( trans.user not in item.users_shared_with_dot_users ):
- raise ItemAccessibilityException( "%s is not accessible to the current user" % item.__class__.__name__, type='error' )
- return item
- class UsesHistoryMixin( SharableItemSecurityMixin ):
- """ Mixin for controllers that use History objects. """
- def get_history( self, trans, id, check_ownership=True, check_accessible=False, deleted=None ):
- """
- Get a History from the database by id, verifying ownership.
- """
- history = self.get_object( trans, id, 'History',
- check_ownership=check_ownership, check_accessible=check_accessible, deleted=deleted )
- history = self.security_check( trans, history, check_ownership, check_accessible )
- return history
- def get_user_histories( self, trans, user=None, include_deleted=False, only_deleted=False ):
- """
- Get all the histories for a given user (defaulting to `trans.user`)
- ordered by update time and filtered on whether they've been deleted.
- """
- # handle default and/or anonymous user (which still may not have a history yet)
- user = user or trans.user
- if not user:
- current_history = trans.get_history()
- return [ current_history ] if current_history else []
- history_model = trans.model.History
- query = ( trans.sa_session.query( history_model )
- .filter( history_model.user == user )
- .order_by( desc( history_model.table.c.update_time ) ) )
- if only_deleted:
- query = query.filter( history_model.deleted == True )
- elif not include_deleted:
- query = query.filter( history_model.deleted == False )
- return query.all()
- def get_history_datasets( self, trans, history, show_deleted=False, show_hidden=False, show_purged=False ):
- """ Returns history's datasets. """
- query = trans.sa_session.query( trans.model.HistoryDatasetAssociation ) \
- .filter( trans.model.HistoryDatasetAssociation.history == history ) \
- .options( eagerload( "children" ) ) \
- .join( "dataset" ) \
- .options( eagerload_all( "dataset.actions" ) ) \
- .order_by( trans.model.HistoryDatasetAssociation.hid )
- if not show_deleted:
- query = query.filter( trans.model.HistoryDatasetAssociation.deleted == False )
- if not show_purged:
- query = query.filter( trans.model.Dataset.purged == False )
- return query.all()
- def get_hda_state_counts( self, trans, history, include_deleted=False, include_hidden=False ):
- """
- Returns a dictionary with state counts for history's HDAs. Key is a
- dataset state, value is the number of states in that count.
- """
- # Build query to get (state, count) pairs.
- cols_to_select = [ trans.app.model.Dataset.table.c.state, func.count( '*' ) ]
- from_obj = trans.app.model.HistoryDatasetAssociation.table.join( trans.app.model.Dataset.table )
- conditions = [ trans.app.model.HistoryDatasetAssociation.table.c.history_id == history.id ]
- if not include_deleted:
- # Only count datasets that have not been deleted.
- conditions.append( trans.app.model.HistoryDatasetAssociation.table.c.deleted == False )
- if not include_hidden:
- # Only count datasets that are visible.
- conditions.append( trans.app.model.HistoryDatasetAssociation.table.c.visible == True )
- group_by = trans.app.model.Dataset.table.c.state
- query = select( columns=cols_to_select,
- from_obj=from_obj,
- whereclause=and_( *conditions ),
- group_by=group_by )
- # Initialize count dict with all states.
- state_count_dict = {}
- for k, state in trans.app.model.Dataset.states.items():
- state_count_dict[ state ] = 0
- # Process query results, adding to count dict.
- for row in trans.sa_session.execute( query ):
- state, count = row
- state_count_dict[ state ] = count
- return state_count_dict
- def get_hda_summary_dicts( self, trans, history ):
- """Returns a list of dictionaries containing summary information
- for each HDA in the given history.
- """
- hda_model = trans.model.HistoryDatasetAssociation
- # get state, name, etc.
- columns = ( hda_model.name, hda_model.hid, hda_model.id, hda_model.deleted,
- trans.model.Dataset.state )
- column_keys = [ "name", "hid", "id", "deleted", "state" ]
- query = ( trans.sa_session.query( *columns )
- .enable_eagerloads( False )
- .filter( hda_model.history == history )
- .join( trans.model.Dataset )
- .order_by( hda_model.hid ) )
- # build dictionaries, adding history id and encoding all ids
- hda_dicts = []
- for hda_tuple in query.all():
- hda_dict = dict( zip( column_keys, hda_tuple ) )
- hda_dict[ 'history_id' ] = history.id
- trans.security.encode_dict_ids( hda_dict )
- hda_dicts.append( hda_dict )
- return hda_dicts
- def _get_hda_state_summaries( self, trans, hda_dict_list ):
- """Returns two dictionaries (in a tuple): state_counts and state_ids.
- Each is keyed according to the possible hda states:
- _counts contains a sum of the datasets in each state
- _ids contains a list of the encoded ids for each hda in that state
- hda_dict_list should be a list of hda data in dictionary form.
- """
- #TODO: doc to rst
- # init counts, ids for each state
- state_counts = {}
- state_ids = {}
- for key, state in trans.app.model.Dataset.states.items():
- state_counts[ state ] = 0
- state_ids[ state ] = []
- for hda_dict in hda_dict_list:
- item_state = hda_dict['state']
- if not hda_dict['deleted']:
- state_counts[ item_state ] = state_counts[ item_state ] + 1
- # needs to return all ids (no deleted check)
- state_ids[ item_state ].append( hda_dict['id'] )
- return ( state_counts, state_ids )
- def _get_history_state_from_hdas( self, trans, history, hda_state_counts ):
- """Returns the history state based on the states of the HDAs it contains.
- """
- states = trans.app.model.Dataset.states
- num_hdas = sum( hda_state_counts.values() )
- # (default to ERROR)
- state = states.ERROR
- if num_hdas == 0:
- state = states.NEW
- else:
- if( ( hda_state_counts[ states.RUNNING ] > 0 )
- or ( hda_state_counts[ states.SETTING_METADATA ] > 0 )
- or ( hda_state_counts[ states.UPLOAD ] > 0 ) ):
- state = states.RUNNING
- elif hda_state_counts[ states.QUEUED ] > 0:
- state = states.QUEUED
- elif( ( hda_state_counts[ states.ERROR ] > 0 )
- or ( hda_state_counts[ states.FAILED_METADATA ] > 0 ) ):
- state = states.ERROR
- elif hda_state_counts[ states.OK ] == num_hdas:
- state = states.OK
- return state
- def get_history_dict( self, trans, history, hda_dictionaries=None ):
- """Returns history data in the form of a dictionary.
- """
- history_dict = history.to_dict( view='element', value_mapper={ 'id':trans.security.encode_id })
- history_dict[ 'user_id' ] = None
- if history.user_id:
- history_dict[ 'user_id' ] = trans.security.encode_id( history.user_id )
- history_dict[ 'nice_size' ] = history.get_disk_size( nice_size=True )
- history_dict[ 'annotation' ] = history.get_item_annotation_str( trans.sa_session, trans.user, history )
- if not history_dict[ 'annotation' ]:
- history_dict[ 'annotation' ] = ''
- #TODO: item_slug url
- if history_dict[ 'importable' ] and history_dict[ 'slug' ]:
- #TODO: this should be in History (or a superclass of)
- username_and_slug = ( '/' ).join(( 'u', history.user.username, 'h', history_dict[ 'slug' ] ))
- history_dict[ 'username_and_slug' ] = username_and_slug
- hda_summaries = hda_dictionaries if hda_dictionaries else self.get_hda_summary_dicts( trans, history )
- #TODO remove the following in v2
- ( state_counts, state_ids ) = self._get_hda_state_summaries( trans, hda_summaries )
- history_dict[ 'state_details' ] = state_counts
- history_dict[ 'state_ids' ] = state_ids
- history_dict[ 'state' ] = self._get_history_state_from_hdas( trans, history, state_counts )
- return history_dict
- def set_history_from_dict( self, trans, history, new_data ):
- """
- Changes history data using the given dictionary new_data.
- """
- #precondition: ownership of the history has already been checked
- #precondition: user is not None (many of these attributes require a user to set properly)
- user = trans.get_user()
- # published histories should always be importable
- if 'published' in new_data and new_data[ 'published' ] and not history.importable:
- new_data[ 'importable' ] = True
- # send what we can down into the model
- changed = history.set_from_dict( new_data )
- # the rest (often involving the trans) - do here
- #TODO: the next two could be an aspect/mixin
- #TODO: also need a way to check whether they've changed - assume they have for now
- if 'annotation' in new_data:
- history.add_item_annotation( trans.sa_session, user, history, new_data[ 'annotation' ] )
- changed[ 'annotation' ] = new_data[ 'annotation' ]
- if 'tags' in new_data:
- self.set_tags_from_list( trans, history, new_data[ 'tags' ], user=user )
- changed[ 'tags' ] = new_data[ 'tags' ]
- #TODO: sharing with user/permissions?
- if changed.keys():
- trans.sa_session.flush()
- # create a slug if none exists (setting importable to false should not remove the slug)
- if 'importable' in changed and changed[ 'importable' ] and not history.slug:
- self._create_history_slug( trans, history )
- return changed
- def _create_history_slug( self, trans, history ):
- #TODO: mixins need to die a quick, horrible death
- # (this is duplicate from SharableMixin which can't be added to UsesHistory without exposing various urls)
- cur_slug = history.slug
- # Setup slug base.
- if cur_slug is None or cur_slug == "":
- # Item can have either a name or a title.
- item_name = history.name
- slug_base = util.ready_name_for_url( item_name.lower() )
- else:
- slug_base = cur_slug
- # Using slug base, find a slug that is not taken. If slug is taken,
- # add integer to end.
- new_slug = slug_base
- count = 1
- while ( trans.sa_session.query( trans.app.model.History )
- .filter_by( user=history.user, slug=new_slug, importable=True )
- .count() != 0 ):
- # Slug taken; choose a new slug based on count. This approach can
- # handle numerous items with the same name gracefully.
- new_slug = '%s-%i' % ( slug_base, count )
- count += 1
- # Set slug and return.
- trans.sa_session.add( history )
- history.slug = new_slug
- trans.sa_session.flush()
- return history.slug == cur_slug
- class ExportsHistoryMixin:
- def serve_ready_history_export( self, trans, jeha ):
- assert jeha.ready
- if jeha.compressed:
- trans.response.set_content_type( 'application/x-gzip' )
- else:
- trans.response.set_content_type( 'application/x-tar' )
- disposition = 'attachment; filename="%s"' % jeha.export_name
- trans.response.headers["Content-Disposition"] = disposition
- return open( trans.app.object_store.get_filename( jeha.dataset ) )
- def queue_history_export( self, trans, history, gzip=True, include_hidden=False, include_deleted=False ):
- # Convert options to booleans.
- #
- if isinstance( gzip, basestring ):
- gzip = ( gzip in [ 'True', 'true', 'T', 't' ] )
- if isinstance( include_hidden, basestring ):
- include_hidden = ( include_hidden in [ 'True', 'true', 'T', 't' ] )
- if isinstance( include_deleted, basestring ):
- include_deleted = ( include_deleted in [ 'True', 'true', 'T', 't' ] )
- # Run job to do export.
- history_exp_tool = trans.app.toolbox.get_tool( '__EXPORT_HISTORY__' )
- params = {
- 'history_to_export': history,
- 'compress': gzip,
- 'include_hidden': include_hidden,
- 'include_deleted': include_deleted
- }
- history_exp_tool.execute( trans, incoming=params, history=history, set_output_hid=True )
- class ImportsHistoryMixin:
- def queue_history_import( self, trans, archive_type, archive_source ):
- # Run job to do import.
- history_imp_tool = trans.app.toolbox.get_tool( '__IMPORT_HISTORY__' )
- incoming = { '__ARCHIVE_SOURCE__' : archive_source, '__ARCHIVE_TYPE__' : archive_type }
- history_imp_tool.execute( trans, incoming=incoming )
- class UsesHistoryDatasetAssociationMixin:
- """
- Mixin for controllers that use HistoryDatasetAssociation objects.
- """
- def get_dataset( self, trans, dataset_id, check_ownership=True, check_accessible=False, check_state=True ):
- """
- Get an HDA object by id performing security checks using
- the current transaction.
- """
- try:
- dataset_id = trans.security.decode_id( dataset_id )
- except ( AttributeError, TypeError ):
- # DEPRECATION: We still support unencoded ids for backward compatibility
- try:
- dataset_id = int( dataset_id )
- except ValueError, v_err:
- raise HTTPBadRequest( "Invalid dataset id: %s." % str( dataset_id ) )
- try:
- data = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( int( dataset_id ) )
- except:
- raise HTTPRequestRangeNotSatisfiable( "Invalid dataset id: %s." % str( dataset_id ) )
- if check_ownership:
- # Verify ownership.
- user = trans.get_user()
- if not user:
- error( "Must be logged in to manage Galaxy items" )
- if data.history.user != user:
- error( "%s is not owned by current user" % data.__class__.__name__ )
- if check_accessible:
- current_user_roles = trans.get_current_user_roles()
- if not trans.app.security_agent.can_access_dataset( current_user_roles, data.dataset ):
- error( "You are not allowed to access this dataset" )
- if check_state and data.state == trans.model.Dataset.states.UPLOAD:
- return trans.show_error_message( "Please wait until this dataset finishes uploading "
- + "before attempting to view it." )
- return data
- def get_history_dataset_association( self, trans, history, dataset_id,
- check_ownership=True, check_accessible=False, check_state=False ):
- """
- Get a HistoryDatasetAssociation from the database by id, verifying ownership.
- """
- #TODO: duplicate of above? alias to above (or vis-versa)
- self.security_check( trans, history, check_ownership=check_ownership, check_accessible=check_accessible )
- hda = self.get_object( trans, dataset_id, 'HistoryDatasetAssociation',
- check_ownership=False, check_accessible=False )
- if check_accessible:
- if( not trans.user_is_admin()
- and not trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset ) ):
- error( "You are not allowed to access this dataset" )
- if check_state and hda.state == trans.model.Dataset.states.UPLOAD:
- error( "Please wait until this dataset finishes uploading before attempting to view it." )
- return hda
- def get_history_dataset_association_from_ids( self, trans, id, history_id ):
- # Just to echo other TODOs, there seems to be some overlap here, still
- # this block appears multiple places (dataset show, history_contents
- # show, upcoming history job show) so I am consolodating it here.
- # Someone smarter than me should determine if there is some redundancy here.
- # for anon users:
- #TODO: check login_required?
- #TODO: this isn't actually most_recently_used (as defined in histories)
- if( ( trans.user == None )
- and ( history_id == trans.security.encode_id( trans.history.id ) ) ):
- history = trans.history
- #TODO: dataset/hda by id (from history) OR check_ownership for anon user
- hda = self.get_history_dataset_association( trans, history, id,
- check_ownership=False, check_accessible=True )
- else:
- #TODO: do we really need the history?
- history = self.get_history( trans, history_id,
- check_ownership=False, check_accessible=True, deleted=False )
- hda = self.get_history_dataset_association( trans, history, id,
- check_ownership=False, check_accessible=True )
- return hda
- def get_hda_list( self, trans, hda_ids, check_ownership=True, check_accessible=False, check_state=True ):
- """
- Returns one or more datasets in a list.
- If a dataset is not found or is inaccessible to trans.user,
- add None in its place in the list.
- """
- # precondtion: dataset_ids is a list of encoded id strings
- hdas = []
- for id in hda_ids:
- hda = None
- try:
- hda = self.get_dataset( trans, id,
- check_ownership=check_ownership,
- check_accessible=check_accessible,
- check_state=check_state )
- except Exception, exception:
- pass
- hdas.append( hda )
- return hdas
- def get_data( self, dataset, preview=True ):
- """
- Gets a dataset's data.
- """
- # Get data from file, truncating if necessary.
- truncated = False
- dataset_data = None
- if os.path.exists( dataset.file_name ):
- if isinstance( dataset.datatype, Text ):
- max_peek_size = 1000000 # 1 MB
- if preview and os.stat( dataset.file_name ).st_size > max_peek_size:
- dataset_data = open( dataset.file_name ).read(max_peek_size)
- truncated = True
- else:
- dataset_data = open( dataset.file_name ).read(max_peek_size)
- truncated = False
- else:
- # For now, cannot get data from non-text datasets.
- dataset_data = None
- return truncated, dataset_data
- def check_dataset_state( self, trans, dataset ):
- """
- Returns a message if dataset is not ready to be used in visualization.
- """
- if not dataset:
- return dataset.conversion_messages.NO_DATA
- if dataset.state == trans.app.model.Job.states.ERROR:
- return dataset.conversion_messages.ERROR
- if dataset.state != trans.app.model.Job.states.OK:
- return dataset.conversion_messages.PENDING
- return None
- def get_hda_dict( self, trans, hda ):
- """Return full details of this HDA in dictionary form.
- """
- #precondition: the user's access to this hda has already been checked
- #TODO:?? postcondition: all ids are encoded (is this really what we want at this level?)
- expose_dataset_path = trans.user_is_admin() or trans.app.config.expose_dataset_path
- hda_dict = hda.to_dict( view='element', expose_dataset_path=expose_dataset_path )
- hda_dict[ 'api_type' ] = "file"
- # Add additional attributes that depend on trans can hence must be added here rather than at the model level.
- can_access_hda = trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset )
- can_access_hda = ( trans.user_is_admin() or can_access_hda )
- if not can_access_hda:
- return self.get_inaccessible_hda_dict( trans, hda )
- hda_dict[ 'accessible' ] = True
- #TODO: I'm unclear as to which access pattern is right
- hda_dict[ 'annotation' ] = hda.get_item_annotation_str( trans.sa_session, trans.user, hda )
- #annotation = getattr( hda, 'annotation', hda.get_item_annotation_str( trans.sa_session, trans.user, hda ) )
- # ---- return here if deleted AND purged OR can't access
- purged = ( hda.purged or hda.dataset.purged )
- if ( hda.deleted and purged ):
- #TODO: to_dict should really go AFTER this - only summary data
- return trans.security.encode_dict_ids( hda_dict )
- if expose_dataset_path:
- try:
- hda_dict[ 'file_name' ] = hda.file_name
- except objectstore.ObjectNotFound, onf:
- log.exception( 'objectstore.ObjectNotFound, HDA %s: %s', hda.id, onf )
- hda_dict[ 'download_url' ] = url_for( 'history_contents_display',
- history_id = trans.security.encode_id( hda.history.id ),
- history_content_id = trans.security.encode_id( hda.id ) )
- # indeces, assoc. metadata files, etc.
- meta_files = []
- for meta_type in hda.metadata.spec.keys():
- if isinstance( hda.metadata.spec[ meta_type ].param, FileParameter ):
- meta_files.append( dict( file_type=meta_type ) )
- if meta_files:
- hda_dict[ 'meta_files' ] = meta_files
- # currently, the viz reg is optional - handle on/off
- if trans.app.visualizations_registry:
- hda_dict[ 'visualizations' ] = trans.app.visualizations_registry.get_visualizations( trans, hda )
- else:
- hda_dict[ 'visualizations' ] = hda.get_visualizations()
- #TODO: it may also be wiser to remove from here and add as API call that loads the visualizations
- # when the visualizations button is clicked (instead of preloading/pre-checking)
- # ---- return here if deleted
- if hda.deleted and not purged:
- return trans.security.encode_dict_ids( hda_dict )
- return trans.security.encode_dict_ids( hda_dict )
- def get_inaccessible_hda_dict( self, trans, hda ):
- return trans.security.encode_dict_ids({
- 'id' : hda.id,
- 'history_id': hda.history.id,
- 'hid' : hda.hid,
- 'name' : hda.name,
- 'state' : hda.state,
- 'deleted' : hda.deleted,
- 'visible' : hda.visible,
- 'accessible': False
- })
- def get_hda_dict_with_error( self, trans, hda=None, history_id=None, id=None, error_msg='Error' ):
- return trans.security.encode_dict_ids({
- 'id' : hda.id if hda else id,
- 'history_id': hda.history.id if hda else history_id,
- 'hid' : hda.hid if hda else '(unknown)',
- 'name' : hda.name if hda else '(unknown)',
- 'error' : error_msg,
- 'state' : trans.model.Dataset.states.NEW
- })
- def get_display_apps( self, trans, hda ):
- display_apps = []
- for display_app in hda.get_display_applications( trans ).itervalues():
- app_links = []
- for link_app in display_app.links.itervalues():
- app_links.append({
- 'target': link_app.url.get( 'target_frame', '_blank' ),
- 'href' : link_app.get_display_url( hda, trans ),
- 'text' : gettext( link_app.name )
- })
- if app_links:
- display_apps.append( dict( label=display_app.name, links=app_links ) )
- return display_apps
- def get_old_display_applications( self, trans, hda ):
- display_apps = []
- if not trans.app.config.enable_old_display_applications:
- return display_apps
- for display_app in hda.datatype.get_display_types():
- target_frame, display_links = hda.datatype.get_display_links( hda,
- display_app, trans.app, trans.request.base )
- if len( display_links ) > 0:
- display_label = hda.datatype.get_display_label( display_app )
- app_links = []
- for display_name, display_link in display_links:
- app_links.append({
- 'target': target_frame,
- 'href' : display_link,
- 'text' : gettext( display_name )
- })
- if app_links:
- display_apps.append( dict( label=display_label, links=app_links ) )
- return display_apps
- def set_hda_from_dict( self, trans, hda, new_data ):
- """
- Changes HDA data using the given dictionary new_data.
- """
- # precondition: access of the hda has already been checked
- # send what we can down into the model
- changed = hda.set_from_dict( new_data )
- # the rest (often involving the trans) - do here
- if 'annotation' in new_data.keys() and trans.get_user():
- hda.add_item_annotation( trans.sa_session, trans.get_user(), hda, new_data[ 'annotation' ] )
- changed[ 'annotation' ] = new_data[ 'annotation' ]
- if 'tags' in new_data.keys() and trans.get_user():
- self.set_tags_from_list( trans, hda, new_data[ 'tags' ], user=trans.user )
- # sharing/permissions?
- # purged
- if changed.keys():
- trans.sa_session.flush()
- return changed
- def get_hda_job( self, hda ):
- # Get dataset's job.
- job = None
- for job_output_assoc in hda.creating_job_associations:
- job = job_output_assoc.job
- break
- return job
- def stop_hda_creating_job( self, hda ):
- """
- Stops an HDA's creating job if all the job's other outputs are deleted.
- """
- if hda.parent_id is None and len( hda.creating_job_associations ) > 0:
- # Mark associated job for deletion
- job = hda.creating_job_associations[0].job
- if job.state in [ self.app.model.Job.states.QUEUED, self.app.model.Job.states.RUNNING, self.app.model.Job.states.NEW ]:
- # Are *all* of the job's other output datasets deleted?
- if job.check_if_output_datasets_deleted():
- job.mark_deleted( self.app.config.track_jobs_in_database )
- self.app.job_manager.job_stop_queue.put( job.id )
- class UsesLibraryMixin:
- def get_library( self, trans, id, check_ownership=False, check_accessible=True ):
- l = self.get_object( trans, id, 'Library' )
- if check_accessible and not ( trans.user_is_admin() or trans.app.security_agent.can_access_library( trans.get_current_user_roles(), l ) ):
- error( "LibraryFolder is not accessible to the current user" )
- return l
- class UsesLibraryMixinItems( SharableItemSecurityMixin ):
- def get_library_folder( self, trans, id, check_ownership=False, check_accessible=True ):
- return self.get_object( trans, id, 'LibraryFolder',
- check_ownership=False, check_accessible=check_accessible )
- def get_library_dataset_dataset_association( self, trans, id, check_ownership=False, check_accessible=True ):
- return self.get_object( trans, id, 'LibraryDatasetDatasetAssociation',
- check_ownership=False, check_accessible=check_accessible )
- def get_library_dataset( self, trans, id, check_ownership=False, check_accessible=True ):
- return self.get_object( trans, id, 'LibraryDataset',
- check_ownership=False, check_accessible=check_accessible )
- #TODO: it makes no sense that I can get roles from a user but not user.is_admin()
- #def can_user_add_to_library_item( self, trans, user, item ):
- # if not user: return False
- # return ( ( user.is_admin() )
- # or ( trans.app.security_agent.can_add_library_item( user.all_roles(), item ) ) )
- def can_current_user_add_to_library_item( self, trans, item ):
- if not trans.user: return False
- return ( ( trans.user_is_admin() )
- or ( trans.app.security_agent.can_add_library_item( trans.get_current_user_roles(), item ) ) )
- def copy_hda_to_library_folder( self, trans, hda, library_folder, roles=None, ldda_message='' ):
- #PRECONDITION: permissions for this action on hda and library_folder have been checked
- roles = roles or []
- # this code was extracted from library_common.add_history_datasets_to_library
- #TODO: refactor library_common.add_history_datasets_to_library to use this for each hda to copy
- # create the new ldda and apply the folder perms to it
- ldda = hda.to_library_dataset_dataset_association( trans, target_folder=library_folder,
- roles=roles, ldda_message=ldda_message )
- self._apply_library_folder_permissions_to_ldda( trans, library_folder, ldda )
- self._apply_hda_permissions_to_ldda( trans, hda, ldda )
- #TODO:?? not really clear on how permissions are being traded here
- # seems like hda -> ldda permissions should be set in to_library_dataset_dataset_association
- # then they get reset in _apply_library_folder_permissions_to_ldda
- # then finally, re-applies hda -> ldda for missing actions in _apply_hda_permissions_to_ldda??
- return ldda
- def _apply_library_folder_permissions_to_ldda( self, trans, library_folder, ldda ):
- """
- Copy actions/roles from library folder to an ldda (and it's library_dataset).
- """
- #PRECONDITION: permissions for this action on library_folder and ldda have been checked
- security_agent = trans.app.security_agent
- security_agent.copy_library_permissions( trans, library_folder, ldda )
- security_agent.copy_library_permissions( trans, library_folder, ldda.library_dataset )
- return security_agent.get_permissions( ldda )
- def _apply_hda_permissions_to_ldda( self, trans, hda, ldda ):
- """
- Copy actions/roles from hda to ldda.library_dataset (and then ldda) if ldda
- doesn't already have roles for the given action.
- """
- #PRECONDITION: permissions for this action on hda and ldda have been checked
- # Make sure to apply any defined dataset permissions, allowing the permissions inherited from the
- # library_dataset to over-ride the same permissions on the dataset, if they exist.
- security_agent = trans.app.security_agent
- dataset_permissions_dict = security_agent.get_permissions( hda.dataset )
- library_dataset = ldda.library_dataset
- library_dataset_actions = [ permission.action for permission in library_dataset.actions ]
- # except that: if DATASET_MANAGE_PERMISSIONS exists in the hda.dataset permissions,
- # we need to instead apply those roles to the LIBRARY_MANAGE permission to the library dataset
- dataset_manage_permissions_action = security_agent.get_action( 'DATASET_MANAGE_PERMISSIONS' ).action
- library_manage_permissions_action = security_agent.get_action( 'LIBRARY_MANAGE' ).action
- #TODO: test this and remove if in loop below
- #TODO: doesn't handle action.action
- #if dataset_manage_permissions_action in dataset_permissions_dict:
- # managing_roles = dataset_permissions_dict.pop( dataset_manage_permissions_action )
- # dataset_permissions_dict[ library_manage_permissions_action ] = managing_roles
- flush_needed = False
- for action, dataset_permissions_roles in dataset_permissions_dict.items():
- if isinstance( action, security.Action ):
- action = action.action
- # alter : DATASET_MANAGE_PERMISSIONS -> LIBRARY_MANAGE (see above)
- if action == dataset_manage_permissions_action:
- action = library_manage_permissions_action
- #TODO: generalize to util.update_dict_without_overwrite
- # add the hda actions & roles to the library_dataset
- #NOTE: only apply an hda perm if it's NOT set in the library_dataset perms (don't overwrite)
- if action not in library_dataset_actions:
- for role in dataset_permissions_roles:
- ldps = trans.model.LibraryDatasetPermissions( action, library_dataset, role )
- ldps = [ ldps ] if not isinstance( ldps, list ) else ldps
- for ldp in ldps:
- trans.sa_session.add( ldp )
- flush_needed = True
- if flush_needed:
- trans.sa_session.flush()
- # finally, apply the new library_dataset to it's associated ldda (must be the same)
- security_agent.copy_library_permissions( trans, library_dataset, ldda )
- return security_agent.get_permissions( ldda )
- class UsesVisualizationMixin( UsesHistoryDatasetAssociationMixin, UsesLibraryMixinItems ):
- """
- Mixin for controllers that use Visualization objects.
- """
- viz_types =…