PageRenderTime 146ms CodeModel.GetById 18ms app.highlight 109ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/galaxy/web/base/controller.py

https://bitbucket.org/cistrome/cistrome-harvard/
Python | 2973 lines | 2914 code | 31 blank | 28 comment | 60 complexity | 44e622a10caafdff829845c52ffeb39f MD5 | raw file
   1"""
   2Contains functionality needed in every web interface
   3"""
   4import logging
   5import operator
   6import os
   7import re
   8from gettext import gettext
   9
  10import pkg_resources
  11pkg_resources.require("SQLAlchemy >= 0.4")
  12from sqlalchemy import func, and_, select
  13
  14from paste.httpexceptions import HTTPBadRequest, HTTPInternalServerError
  15from paste.httpexceptions import HTTPNotImplemented, HTTPRequestRangeNotSatisfiable
  16from galaxy import exceptions
  17from galaxy.exceptions import ItemAccessibilityException, ItemDeletionException, ItemOwnershipException
  18from galaxy.exceptions import MessageException
  19
  20from galaxy import web
  21from galaxy import model
  22from galaxy import security
  23from galaxy import util
  24from galaxy import objectstore
  25
  26from galaxy.web import error, url_for
  27from galaxy.web.form_builder import AddressField, CheckboxField, SelectField, TextArea, TextField
  28from galaxy.web.form_builder import build_select_field, HistoryField, PasswordField, WorkflowField, WorkflowMappingField
  29from galaxy.workflow.modules import module_factory
  30from galaxy.model.orm import eagerload, eagerload_all, desc
  31from galaxy.security.validate_user_input import validate_publicname
  32from galaxy.util.sanitize_html import sanitize_html
  33from galaxy.model.item_attrs import Dictifiable, UsesAnnotations
  34
  35from galaxy.datatypes.interval import ChromatinInteractions
  36from galaxy.datatypes.data import Text
  37
  38from galaxy.model import ExtendedMetadata, ExtendedMetadataIndex, LibraryDatasetDatasetAssociation, HistoryDatasetAssociation
  39
  40from galaxy.datatypes.metadata import FileParameter
  41from galaxy.tools.parameters import RuntimeValue, visit_input_values
  42from galaxy.tools.parameters.basic import DataToolParameter
  43from galaxy.util.json import to_json_string
  44from galaxy.workflow.modules import ToolModule
  45from galaxy.workflow.steps import attach_ordered_steps
  46
  47
  48log = logging.getLogger( __name__ )
  49
  50# States for passing messages
  51SUCCESS, INFO, WARNING, ERROR = "done", "info", "warning", "error"
  52
  53def _is_valid_slug( slug ):
  54    """ Returns true if slug is valid. """
  55
  56    VALID_SLUG_RE = re.compile( "^[a-z0-9\-]+$" )
  57    return VALID_SLUG_RE.match( slug )
  58
  59
  60class BaseController( object ):
  61    """
  62    Base class for Galaxy web application controllers.
  63    """
  64
  65    def __init__( self, app ):
  66        """Initialize an interface for application 'app'"""
  67        self.app = app
  68        self.sa_session = app.model.context
  69
  70    def get_toolbox(self):
  71        """Returns the application toolbox"""
  72        return self.app.toolbox
  73
  74    def get_class( self, class_name ):
  75        """ Returns the class object that a string denotes. Without this method, we'd have to do eval(<class_name>). """
  76        if class_name == 'History':
  77            item_class = self.app.model.History
  78        elif class_name == 'HistoryDatasetAssociation':
  79            item_class = self.app.model.HistoryDatasetAssociation
  80        elif class_name == 'Page':
  81            item_class = self.app.model.Page
  82        elif class_name == 'StoredWorkflow':
  83            item_class = self.app.model.StoredWorkflow
  84        elif class_name == 'Visualization':
  85            item_class = self.app.model.Visualization
  86        elif class_name == 'Tool':
  87            item_class = self.app.model.Tool
  88        elif class_name == 'Job':
  89            item_class = self.app.model.Job
  90        elif class_name == 'User':
  91            item_class = self.app.model.User
  92        elif class_name == 'Group':
  93            item_class = self.app.model.Group
  94        elif class_name == 'Role':
  95            item_class = self.app.model.Role
  96        elif class_name == 'Quota':
  97            item_class = self.app.model.Quota
  98        elif class_name == 'Library':
  99            item_class = self.app.model.Library
 100        elif class_name == 'LibraryFolder':
 101            item_class = self.app.model.LibraryFolder
 102        elif class_name == 'LibraryDatasetDatasetAssociation':
 103            item_class = self.app.model.LibraryDatasetDatasetAssociation
 104        elif class_name == 'LibraryDataset':
 105            item_class = self.app.model.LibraryDataset
 106        elif class_name == 'ToolShedRepository':
 107            item_class = self.app.install_model.ToolShedRepository
 108        else:
 109            item_class = None
 110        return item_class
 111
 112    def get_object( self, trans, id, class_name, check_ownership=False, check_accessible=False, deleted=None ):
 113        """
 114        Convenience method to get a model object with the specified checks.
 115        """
 116        try:
 117            decoded_id = trans.security.decode_id( id )
 118        except:
 119            raise MessageException( "Malformed %s id ( %s ) specified, unable to decode"
 120                                    % ( class_name, str( id ) ), type='error' )
 121        try:
 122            item_class = self.get_class( class_name )
 123            assert item_class is not None
 124            item = trans.sa_session.query( item_class ).get( decoded_id )
 125            assert item is not None
 126        except Exception, exc:
 127            log.exception( "Invalid %s id ( %s ) specified: %s" % ( class_name, id, str( exc ) ) )
 128            raise MessageException( "Invalid %s id ( %s ) specified" % ( class_name, id ), type="error" )
 129
 130        if check_ownership or check_accessible:
 131            self.security_check( trans, item, check_ownership, check_accessible )
 132        if deleted == True and not item.deleted:
 133            raise ItemDeletionException( '%s "%s" is not deleted'
 134                                         % ( class_name, getattr( item, 'name', id ) ), type="warning" )
 135        elif deleted == False and item.deleted:
 136            raise ItemDeletionException( '%s "%s" is deleted'
 137                                         % ( class_name, getattr( item, 'name', id ) ), type="warning" )
 138        return item
 139
 140    # this should be here - but catching errors from sharable item controllers that *should* have SharableItemMixin
 141    #   but *don't* then becomes difficult
 142    #def security_check( self, trans, item, check_ownership=False, check_accessible=False ):
 143    #    log.warn( 'BaseController.security_check: %s, %b, %b', str( item ), check_ownership, check_accessible )
 144    #    # meant to be overridden in SharableSecurityMixin
 145    #    return item
 146
 147    def get_user( self, trans, id, check_ownership=False, check_accessible=False, deleted=None ):
 148        return self.get_object( trans, id, 'User', check_ownership=False, check_accessible=False, deleted=deleted )
 149
 150    def get_group( self, trans, id, check_ownership=False, check_accessible=False, deleted=None ):
 151        return self.get_object( trans, id, 'Group', check_ownership=False, check_accessible=False, deleted=deleted )
 152
 153    def get_role( self, trans, id, check_ownership=False, check_accessible=False, deleted=None ):
 154        return self.get_object( trans, id, 'Role', check_ownership=False, check_accessible=False, deleted=deleted )
 155
 156    def encode_all_ids( self, trans, rval, recursive=False ):
 157        """
 158        Encodes all integer values in the dict rval whose keys are 'id' or end with '_id'
 159
 160        It might be useful to turn this in to a decorator
 161        """
 162        if type( rval ) != dict:
 163            return rval
 164        for k, v in rval.items():
 165            if (k == 'id' or k.endswith( '_id' )) and v is not None and k not in ['tool_id']:
 166                try:
 167                    rval[k] = trans.security.encode_id( v )
 168                except:
 169                    pass # probably already encoded
 170            if (k.endswith("_ids") and type(v) == list):
 171                try:
 172                    o = []
 173                    for i in v:
 174                        o.append(trans.security.encode_id( i ))
 175                    rval[k] = o
 176                except:
 177                    pass
 178            else:
 179                if recursive and type(v) == dict:
 180                    rval[k] = self.encode_all_ids(trans, v, recursive)
 181        return rval
 182
 183    # incoming param validation
 184    # should probably be in sep. serializer class/object _used_ by controller
 185    def validate_and_sanitize_basestring( self, key, val ):
 186        if not isinstance( val, basestring ):
 187            raise exceptions.RequestParameterInvalidException( '%s must be a string or unicode: %s'
 188                                                               %( key, str( type( val ) ) ) )
 189        return unicode( sanitize_html( val, 'utf-8', 'text/html' ), 'utf-8' )
 190
 191    def validate_and_sanitize_basestring_list( self, key, val ):
 192        try:
 193            assert isinstance( val, list )
 194            return [ unicode( sanitize_html( t, 'utf-8', 'text/html' ), 'utf-8' ) for t in val ]
 195        except ( AssertionError, TypeError ), err:
 196            raise exceptions.RequestParameterInvalidException( '%s must be a list of strings: %s'
 197                                                               %( key, str( type( val ) ) ) )
 198
 199    def validate_boolean( self, key, val ):
 200        if not isinstance( val, bool ):
 201            raise exceptions.RequestParameterInvalidException( '%s must be a boolean: %s'
 202                                                               %( key, str( type( val ) ) ) )
 203        return val
 204
 205    #TODO:
 206    #def validate_integer( self, key, val, min, max ):
 207    #def validate_float( self, key, val, min, max ):
 208    #def validate_number( self, key, val, min, max ):
 209    #def validate_genome_build( self, key, val ):
 210
 211Root = BaseController
 212
 213
 214class BaseUIController( BaseController ):
 215
 216    def get_object( self, trans, id, class_name, check_ownership=False, check_accessible=False, deleted=None ):
 217        try:
 218            return BaseController.get_object( self, trans, id, class_name,
 219                check_ownership=check_ownership, check_accessible=check_accessible, deleted=deleted )
 220
 221        except MessageException:
 222            raise       # handled in the caller
 223        except:
 224            log.exception( "Execption in get_object check for %s %s:" % ( class_name, str( id ) ) )
 225            raise Exception( 'Server error retrieving %s id ( %s ).' % ( class_name, str( id ) ) )
 226
 227
 228class BaseAPIController( BaseController ):
 229
 230    def get_object( self, trans, id, class_name, check_ownership=False, check_accessible=False, deleted=None ):
 231        try:
 232            return BaseController.get_object( self, trans, id, class_name,
 233                check_ownership=check_ownership, check_accessible=check_accessible, deleted=deleted )
 234
 235        except ItemDeletionException, e:
 236            raise HTTPBadRequest( detail="Invalid %s id ( %s ) specified: %s" % ( class_name, str( id ), str( e ) ) )
 237        except MessageException, e:
 238            raise HTTPBadRequest( detail=e.err_msg )
 239        except Exception, e:
 240            log.exception( "Execption in get_object check for %s %s: %s" % ( class_name, str( id ), str( e ) ) )
 241            raise HTTPInternalServerError( comment=str( e ) )
 242
 243    def validate_in_users_and_groups( self, trans, payload ):
 244        """
 245        For convenience, in_users and in_groups can be encoded IDs or emails/group names in the API.
 246        """
 247        def get_id( item, model_class, column ):
 248            try:
 249                return trans.security.decode_id( item )
 250            except:
 251                pass # maybe an email/group name
 252            # this will raise if the item is invalid
 253            return trans.sa_session.query( model_class ).filter( column == item ).first().id
 254        new_in_users = []
 255        new_in_groups = []
 256        invalid = []
 257        for item in util.listify( payload.get( 'in_users', [] ) ):
 258            try:
 259                new_in_users.append( get_id( item, trans.app.model.User, trans.app.model.User.table.c.email ) )
 260            except:
 261                invalid.append( item )
 262        for item in util.listify( payload.get( 'in_groups', [] ) ):
 263            try:
 264                new_in_groups.append( get_id( item, trans.app.model.Group, trans.app.model.Group.table.c.name ) )
 265            except:
 266                invalid.append( item )
 267        if invalid:
 268            msg = "The following value(s) for associated users and/or groups could not be parsed: %s." % ', '.join( invalid )
 269            msg += "  Valid values are email addresses of users, names of groups, or IDs of both."
 270            raise Exception( msg )
 271        payload['in_users'] = map( str, new_in_users )
 272        payload['in_groups'] = map( str, new_in_groups )
 273
 274    def not_implemented( self, trans, **kwd ):
 275        raise HTTPNotImplemented()
 276
 277
 278class Datatype( object ):
 279    """Used for storing in-memory list of datatypes currently in the datatypes registry."""
 280
 281    def __init__( self, extension, dtype, type_extension, mimetype, display_in_upload ):
 282        self.extension = extension
 283        self.dtype = dtype
 284        self.type_extension = type_extension
 285        self.mimetype = mimetype
 286        self.display_in_upload = display_in_upload
 287
 288#
 289# -- Mixins for working with Galaxy objects. --
 290#
 291
 292
 293class CreatesUsersMixin:
 294    """
 295    Mixin centralizing logic for user creation between web and API controller.
 296
 297    Web controller handles additional features such e-mail subscription, activation,
 298    user forms, etc.... API created users are much more vanilla for the time being.
 299    """
 300
 301    def create_user( self, trans, email, username, password ):
 302        user = trans.app.model.User( email=email )
 303        user.set_password_cleartext( password )
 304        user.username = username
 305        if trans.app.config.user_activation_on:
 306            user.active = False
 307        else:
 308            user.active = True  # Activation is off, every new user is active by default.
 309        trans.sa_session.add( user )
 310        trans.sa_session.flush()
 311        trans.app.security_agent.create_private_user_role( user )
 312        if trans.webapp.name == 'galaxy':
 313            # We set default user permissions, before we log in and set the default history permissions
 314            trans.app.security_agent.user_set_default_permissions( user,
 315                                                                   default_access_private=trans.app.config.new_user_dataset_access_role_default_private )
 316        return user
 317
 318
 319class CreatesApiKeysMixin:
 320    """
 321    Mixing centralizing logic for creating API keys for user objects.
 322    """
 323
 324    def create_api_key( self, trans, user ):
 325        guid = trans.app.security.get_new_guid()
 326        new_key = trans.app.model.APIKeys()
 327        new_key.user_id = user.id
 328        new_key.key = guid
 329        trans.sa_session.add( new_key )
 330        trans.sa_session.flush()
 331        return guid
 332
 333
 334class SharableItemSecurityMixin:
 335    """ Mixin for handling security for sharable items. """
 336
 337    def security_check( self, trans, item, check_ownership=False, check_accessible=False ):
 338        """ Security checks for an item: checks if (a) user owns item or (b) item is accessible to user. """
 339        # all items are accessible to an admin
 340        if trans.user and trans.user_is_admin():
 341            return item
 342
 343        # Verify ownership: there is a current user and that user is the same as the item's
 344        if check_ownership:
 345            if not trans.user:
 346                raise ItemOwnershipException( "Must be logged in to manage Galaxy items", type='error' )
 347            if item.user != trans.user:
 348                raise ItemOwnershipException( "%s is not owned by the current user" % item.__class__.__name__, type='error' )
 349
 350        # Verify accessible:
 351        #   if it's part of a lib - can they access via security
 352        #   if it's something else (sharable) have they been added to the item's users_shared_with_dot_users
 353        if check_accessible:
 354            if type( item ) in ( trans.app.model.LibraryFolder, trans.app.model.LibraryDatasetDatasetAssociation, trans.app.model.LibraryDataset ):
 355                if not trans.app.security_agent.can_access_library_item( trans.get_current_user_roles(), item, trans.user ):
 356                    raise ItemAccessibilityException( "%s is not accessible to the current user" % item.__class__.__name__, type='error' )
 357            else:
 358                if ( item.user != trans.user ) and ( not item.importable ) and ( trans.user not in item.users_shared_with_dot_users ):
 359                    raise ItemAccessibilityException( "%s is not accessible to the current user" % item.__class__.__name__, type='error' )
 360        return item
 361
 362
 363class UsesHistoryMixin( SharableItemSecurityMixin ):
 364    """ Mixin for controllers that use History objects. """
 365
 366    def get_history( self, trans, id, check_ownership=True, check_accessible=False, deleted=None ):
 367        """
 368        Get a History from the database by id, verifying ownership.
 369        """
 370        history = self.get_object( trans, id, 'History',
 371            check_ownership=check_ownership, check_accessible=check_accessible, deleted=deleted )
 372        history = self.security_check( trans, history, check_ownership, check_accessible )
 373        return history
 374
 375    def get_user_histories( self, trans, user=None, include_deleted=False, only_deleted=False ):
 376        """
 377        Get all the histories for a given user (defaulting to `trans.user`)
 378        ordered by update time and filtered on whether they've been deleted.
 379        """
 380        # handle default and/or anonymous user (which still may not have a history yet)
 381        user = user or trans.user
 382        if not user:
 383            current_history = trans.get_history()
 384            return [ current_history ] if current_history else []
 385
 386        history_model = trans.model.History
 387        query = ( trans.sa_session.query( history_model )
 388            .filter( history_model.user == user )
 389            .order_by( desc( history_model.table.c.update_time ) ) )
 390        if only_deleted:
 391            query = query.filter( history_model.deleted == True )
 392        elif not include_deleted:
 393            query = query.filter( history_model.deleted == False )
 394
 395        return query.all()
 396
 397    def get_history_datasets( self, trans, history, show_deleted=False, show_hidden=False, show_purged=False ):
 398        """ Returns history's datasets. """
 399        query = trans.sa_session.query( trans.model.HistoryDatasetAssociation ) \
 400            .filter( trans.model.HistoryDatasetAssociation.history == history ) \
 401            .options( eagerload( "children" ) ) \
 402            .join( "dataset" ) \
 403            .options( eagerload_all( "dataset.actions" ) ) \
 404            .order_by( trans.model.HistoryDatasetAssociation.hid )
 405        if not show_deleted:
 406            query = query.filter( trans.model.HistoryDatasetAssociation.deleted == False )
 407        if not show_purged:
 408            query = query.filter( trans.model.Dataset.purged == False )
 409        return query.all()
 410
 411    def get_hda_state_counts( self, trans, history, include_deleted=False, include_hidden=False ):
 412        """
 413        Returns a dictionary with state counts for history's HDAs. Key is a
 414        dataset state, value is the number of states in that count.
 415        """
 416        # Build query to get (state, count) pairs.
 417        cols_to_select = [ trans.app.model.Dataset.table.c.state, func.count( '*' ) ]
 418        from_obj = trans.app.model.HistoryDatasetAssociation.table.join( trans.app.model.Dataset.table )
 419
 420        conditions = [ trans.app.model.HistoryDatasetAssociation.table.c.history_id == history.id ]
 421        if not include_deleted:
 422            # Only count datasets that have not been deleted.
 423            conditions.append( trans.app.model.HistoryDatasetAssociation.table.c.deleted == False )
 424        if not include_hidden:
 425            # Only count datasets that are visible.
 426            conditions.append( trans.app.model.HistoryDatasetAssociation.table.c.visible == True )
 427
 428        group_by = trans.app.model.Dataset.table.c.state
 429        query = select( columns=cols_to_select,
 430                        from_obj=from_obj,
 431                        whereclause=and_( *conditions ),
 432                        group_by=group_by )
 433
 434        # Initialize count dict with all states.
 435        state_count_dict = {}
 436        for k, state in trans.app.model.Dataset.states.items():
 437            state_count_dict[ state ] = 0
 438
 439        # Process query results, adding to count dict.
 440        for row in trans.sa_session.execute( query ):
 441            state, count = row
 442            state_count_dict[ state ] = count
 443
 444        return state_count_dict
 445
 446    def get_hda_summary_dicts( self, trans, history ):
 447        """Returns a list of dictionaries containing summary information
 448        for each HDA in the given history.
 449        """
 450        hda_model = trans.model.HistoryDatasetAssociation
 451
 452        # get state, name, etc.
 453        columns = ( hda_model.name, hda_model.hid, hda_model.id, hda_model.deleted,
 454                    trans.model.Dataset.state )
 455        column_keys = [ "name", "hid", "id", "deleted", "state" ]
 456
 457        query = ( trans.sa_session.query( *columns )
 458                    .enable_eagerloads( False )
 459                    .filter( hda_model.history == history )
 460                    .join( trans.model.Dataset )
 461                    .order_by( hda_model.hid ) )
 462
 463        # build dictionaries, adding history id and encoding all ids
 464        hda_dicts = []
 465        for hda_tuple in query.all():
 466            hda_dict = dict( zip( column_keys, hda_tuple ) )
 467            hda_dict[ 'history_id' ] = history.id
 468            trans.security.encode_dict_ids( hda_dict )
 469            hda_dicts.append( hda_dict )
 470        return hda_dicts
 471
 472    def _get_hda_state_summaries( self, trans, hda_dict_list ):
 473        """Returns two dictionaries (in a tuple): state_counts and state_ids.
 474        Each is keyed according to the possible hda states:
 475            _counts contains a sum of the datasets in each state
 476            _ids contains a list of the encoded ids for each hda in that state
 477
 478        hda_dict_list should be a list of hda data in dictionary form.
 479        """
 480        #TODO: doc to rst
 481        # init counts, ids for each state
 482        state_counts = {}
 483        state_ids = {}
 484        for key, state in trans.app.model.Dataset.states.items():
 485            state_counts[ state ] = 0
 486            state_ids[ state ] = []
 487
 488        for hda_dict in hda_dict_list:
 489            item_state = hda_dict['state']
 490            if not hda_dict['deleted']:
 491                state_counts[ item_state ] = state_counts[ item_state ] + 1
 492            # needs to return all ids (no deleted check)
 493            state_ids[ item_state ].append( hda_dict['id'] )
 494
 495        return ( state_counts, state_ids )
 496
 497    def _get_history_state_from_hdas( self, trans, history, hda_state_counts ):
 498        """Returns the history state based on the states of the HDAs it contains.
 499        """
 500        states = trans.app.model.Dataset.states
 501
 502        num_hdas = sum( hda_state_counts.values() )
 503        # (default to ERROR)
 504        state = states.ERROR
 505        if num_hdas == 0:
 506            state = states.NEW
 507
 508        else:
 509            if( ( hda_state_counts[ states.RUNNING ] > 0 )
 510            or  ( hda_state_counts[ states.SETTING_METADATA ] > 0 )
 511            or  ( hda_state_counts[ states.UPLOAD ] > 0 ) ):
 512                state = states.RUNNING
 513
 514            elif hda_state_counts[ states.QUEUED ] > 0:
 515                state = states.QUEUED
 516
 517            elif( ( hda_state_counts[ states.ERROR ] > 0 )
 518            or    ( hda_state_counts[ states.FAILED_METADATA ] > 0 ) ):
 519                state = states.ERROR
 520
 521            elif hda_state_counts[ states.OK ] == num_hdas:
 522                state = states.OK
 523
 524        return state
 525
 526    def get_history_dict( self, trans, history, hda_dictionaries=None ):
 527        """Returns history data in the form of a dictionary.
 528        """
 529        history_dict = history.to_dict( view='element', value_mapper={ 'id':trans.security.encode_id })
 530        history_dict[ 'user_id' ] = None
 531        if history.user_id:
 532            history_dict[ 'user_id' ] = trans.security.encode_id( history.user_id )
 533
 534        history_dict[ 'nice_size' ] = history.get_disk_size( nice_size=True )
 535        history_dict[ 'annotation' ] = history.get_item_annotation_str( trans.sa_session, trans.user, history )
 536        if not history_dict[ 'annotation' ]:
 537            history_dict[ 'annotation' ] = ''
 538        #TODO: item_slug url
 539        if history_dict[ 'importable' ] and history_dict[ 'slug' ]:
 540            #TODO: this should be in History (or a superclass of)
 541            username_and_slug = ( '/' ).join(( 'u', history.user.username, 'h', history_dict[ 'slug' ] ))
 542            history_dict[ 'username_and_slug' ] = username_and_slug
 543
 544        hda_summaries = hda_dictionaries if hda_dictionaries else self.get_hda_summary_dicts( trans, history )
 545        #TODO remove the following in v2
 546        ( state_counts, state_ids ) = self._get_hda_state_summaries( trans, hda_summaries )
 547        history_dict[ 'state_details' ] = state_counts
 548        history_dict[ 'state_ids' ] = state_ids
 549        history_dict[ 'state' ] = self._get_history_state_from_hdas( trans, history, state_counts )
 550
 551        return history_dict
 552
 553    def set_history_from_dict( self, trans, history, new_data ):
 554        """
 555        Changes history data using the given dictionary new_data.
 556        """
 557        #precondition: ownership of the history has already been checked
 558        #precondition: user is not None (many of these attributes require a user to set properly)
 559        user = trans.get_user()
 560
 561        # published histories should always be importable
 562        if 'published' in new_data and new_data[ 'published' ] and not history.importable:
 563            new_data[ 'importable' ] = True
 564        # send what we can down into the model
 565        changed = history.set_from_dict( new_data )
 566
 567        # the rest (often involving the trans) - do here
 568        #TODO: the next two could be an aspect/mixin
 569        #TODO: also need a way to check whether they've changed - assume they have for now
 570        if 'annotation' in new_data:
 571            history.add_item_annotation( trans.sa_session, user, history, new_data[ 'annotation' ] )
 572            changed[ 'annotation' ] = new_data[ 'annotation' ]
 573
 574        if 'tags' in new_data:
 575            self.set_tags_from_list( trans, history, new_data[ 'tags' ], user=user )
 576            changed[ 'tags' ] = new_data[ 'tags' ]
 577
 578        #TODO: sharing with user/permissions?
 579
 580        if changed.keys():
 581            trans.sa_session.flush()
 582
 583            # create a slug if none exists (setting importable to false should not remove the slug)
 584            if 'importable' in changed and changed[ 'importable' ] and not history.slug:
 585                self._create_history_slug( trans, history )
 586
 587        return changed
 588
 589    def _create_history_slug( self, trans, history ):
 590        #TODO: mixins need to die a quick, horrible death
 591        #   (this is duplicate from SharableMixin which can't be added to UsesHistory without exposing various urls)
 592        cur_slug = history.slug
 593
 594        # Setup slug base.
 595        if cur_slug is None or cur_slug == "":
 596            # Item can have either a name or a title.
 597            item_name = history.name
 598            slug_base = util.ready_name_for_url( item_name.lower() )
 599        else:
 600            slug_base = cur_slug
 601
 602        # Using slug base, find a slug that is not taken. If slug is taken,
 603        # add integer to end.
 604        new_slug = slug_base
 605        count = 1
 606        while ( trans.sa_session.query( trans.app.model.History )
 607                    .filter_by( user=history.user, slug=new_slug, importable=True )
 608                    .count() != 0 ):
 609            # Slug taken; choose a new slug based on count. This approach can
 610            # handle numerous items with the same name gracefully.
 611            new_slug = '%s-%i' % ( slug_base, count )
 612            count += 1
 613
 614        # Set slug and return.
 615        trans.sa_session.add( history )
 616        history.slug = new_slug
 617        trans.sa_session.flush()
 618        return history.slug == cur_slug
 619
 620
 621class ExportsHistoryMixin:
 622
 623    def serve_ready_history_export( self, trans, jeha ):
 624        assert jeha.ready
 625        if jeha.compressed:
 626            trans.response.set_content_type( 'application/x-gzip' )
 627        else:
 628            trans.response.set_content_type( 'application/x-tar' )
 629        disposition = 'attachment; filename="%s"' % jeha.export_name
 630        trans.response.headers["Content-Disposition"] = disposition
 631        return open( trans.app.object_store.get_filename( jeha.dataset ) )
 632
 633    def queue_history_export( self, trans, history, gzip=True, include_hidden=False, include_deleted=False ):
 634        # Convert options to booleans.
 635        #
 636        if isinstance( gzip, basestring ):
 637            gzip = ( gzip in [ 'True', 'true', 'T', 't' ] )
 638        if isinstance( include_hidden, basestring ):
 639            include_hidden = ( include_hidden in [ 'True', 'true', 'T', 't' ] )
 640        if isinstance( include_deleted, basestring ):
 641            include_deleted = ( include_deleted in [ 'True', 'true', 'T', 't' ] )
 642
 643        # Run job to do export.
 644        history_exp_tool = trans.app.toolbox.get_tool( '__EXPORT_HISTORY__' )
 645        params = {
 646            'history_to_export': history,
 647            'compress': gzip,
 648            'include_hidden': include_hidden,
 649            'include_deleted': include_deleted
 650        }
 651
 652        history_exp_tool.execute( trans, incoming=params, history=history, set_output_hid=True )
 653
 654
 655class ImportsHistoryMixin:
 656
 657    def queue_history_import( self, trans, archive_type, archive_source ):
 658        # Run job to do import.
 659        history_imp_tool = trans.app.toolbox.get_tool( '__IMPORT_HISTORY__' )
 660        incoming = { '__ARCHIVE_SOURCE__' : archive_source, '__ARCHIVE_TYPE__' : archive_type }
 661        history_imp_tool.execute( trans, incoming=incoming )
 662
 663
 664class UsesHistoryDatasetAssociationMixin:
 665    """
 666    Mixin for controllers that use HistoryDatasetAssociation objects.
 667    """
 668
 669    def get_dataset( self, trans, dataset_id, check_ownership=True, check_accessible=False, check_state=True ):
 670        """
 671        Get an HDA object by id performing security checks using
 672        the current transaction.
 673        """
 674        try:
 675            dataset_id = trans.security.decode_id( dataset_id )
 676        except ( AttributeError, TypeError ):
 677            # DEPRECATION: We still support unencoded ids for backward compatibility
 678            try:
 679                dataset_id = int( dataset_id )
 680            except ValueError, v_err:
 681                raise HTTPBadRequest( "Invalid dataset id: %s." % str( dataset_id ) )
 682
 683        try:
 684            data = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( int( dataset_id ) )
 685        except:
 686            raise HTTPRequestRangeNotSatisfiable( "Invalid dataset id: %s." % str( dataset_id ) )
 687
 688        if check_ownership:
 689            # Verify ownership.
 690            user = trans.get_user()
 691            if not user:
 692                error( "Must be logged in to manage Galaxy items" )
 693            if data.history.user != user:
 694                error( "%s is not owned by current user" % data.__class__.__name__ )
 695
 696        if check_accessible:
 697            current_user_roles = trans.get_current_user_roles()
 698
 699            if not trans.app.security_agent.can_access_dataset( current_user_roles, data.dataset ):
 700                error( "You are not allowed to access this dataset" )
 701
 702            if check_state and data.state == trans.model.Dataset.states.UPLOAD:
 703                    return trans.show_error_message( "Please wait until this dataset finishes uploading "
 704                                                   + "before attempting to view it." )
 705        return data
 706
 707    def get_history_dataset_association( self, trans, history, dataset_id,
 708                                         check_ownership=True, check_accessible=False, check_state=False ):
 709        """
 710        Get a HistoryDatasetAssociation from the database by id, verifying ownership.
 711        """
 712        #TODO: duplicate of above? alias to above (or vis-versa)
 713        self.security_check( trans, history, check_ownership=check_ownership, check_accessible=check_accessible )
 714        hda = self.get_object( trans, dataset_id, 'HistoryDatasetAssociation',
 715                               check_ownership=False, check_accessible=False )
 716
 717        if check_accessible:
 718            if( not trans.user_is_admin()
 719            and not trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset ) ):
 720                error( "You are not allowed to access this dataset" )
 721
 722            if check_state and hda.state == trans.model.Dataset.states.UPLOAD:
 723                error( "Please wait until this dataset finishes uploading before attempting to view it." )
 724        return hda
 725
 726    def get_history_dataset_association_from_ids( self, trans, id, history_id ):
 727        # Just to echo other TODOs, there seems to be some overlap here, still
 728        # this block appears multiple places (dataset show, history_contents
 729        # show, upcoming history job show) so I am consolodating it here.
 730        # Someone smarter than me should determine if there is some redundancy here.
 731
 732        # for anon users:
 733        #TODO: check login_required?
 734        #TODO: this isn't actually most_recently_used (as defined in histories)
 735        if( ( trans.user == None )
 736        and ( history_id == trans.security.encode_id( trans.history.id ) ) ):
 737            history = trans.history
 738            #TODO: dataset/hda by id (from history) OR check_ownership for anon user
 739            hda = self.get_history_dataset_association( trans, history, id,
 740                check_ownership=False, check_accessible=True )
 741        else:
 742            #TODO: do we really need the history?
 743            history = self.get_history( trans, history_id,
 744                check_ownership=False, check_accessible=True, deleted=False )
 745            hda = self.get_history_dataset_association( trans, history, id,
 746                check_ownership=False, check_accessible=True )
 747        return hda
 748
 749    def get_hda_list( self, trans, hda_ids, check_ownership=True, check_accessible=False, check_state=True ):
 750        """
 751        Returns one or more datasets in a list.
 752
 753        If a dataset is not found or is inaccessible to trans.user,
 754        add None in its place in the list.
 755        """
 756        # precondtion: dataset_ids is a list of encoded id strings
 757        hdas = []
 758        for id in hda_ids:
 759            hda = None
 760            try:
 761                hda = self.get_dataset( trans, id,
 762                    check_ownership=check_ownership,
 763                    check_accessible=check_accessible,
 764                    check_state=check_state )
 765            except Exception, exception:
 766                pass
 767            hdas.append( hda )
 768        return hdas
 769
 770    def get_data( self, dataset, preview=True ):
 771        """
 772        Gets a dataset's data.
 773        """
 774        # Get data from file, truncating if necessary.
 775        truncated = False
 776        dataset_data = None
 777        if os.path.exists( dataset.file_name ):
 778            if isinstance( dataset.datatype, Text ):
 779                max_peek_size = 1000000 # 1 MB
 780                if preview and os.stat( dataset.file_name ).st_size > max_peek_size:
 781                    dataset_data = open( dataset.file_name ).read(max_peek_size)
 782                    truncated = True
 783                else:
 784                    dataset_data = open( dataset.file_name ).read(max_peek_size)
 785                    truncated = False
 786            else:
 787                # For now, cannot get data from non-text datasets.
 788                dataset_data = None
 789        return truncated, dataset_data
 790
 791    def check_dataset_state( self, trans, dataset ):
 792        """
 793        Returns a message if dataset is not ready to be used in visualization.
 794        """
 795        if not dataset:
 796            return dataset.conversion_messages.NO_DATA
 797        if dataset.state == trans.app.model.Job.states.ERROR:
 798            return dataset.conversion_messages.ERROR
 799        if dataset.state != trans.app.model.Job.states.OK:
 800            return dataset.conversion_messages.PENDING
 801        return None
 802
 803    def get_hda_dict( self, trans, hda ):
 804        """Return full details of this HDA in dictionary form.
 805        """
 806        #precondition: the user's access to this hda has already been checked
 807        #TODO:?? postcondition: all ids are encoded (is this really what we want at this level?)
 808        expose_dataset_path = trans.user_is_admin() or trans.app.config.expose_dataset_path
 809        hda_dict = hda.to_dict( view='element', expose_dataset_path=expose_dataset_path )
 810        hda_dict[ 'api_type' ] = "file"
 811
 812        # Add additional attributes that depend on trans can hence must be added here rather than at the model level.
 813        can_access_hda = trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset )
 814        can_access_hda = ( trans.user_is_admin() or can_access_hda )
 815        if not can_access_hda:
 816            return self.get_inaccessible_hda_dict( trans, hda )
 817        hda_dict[ 'accessible' ] = True
 818
 819        #TODO: I'm unclear as to which access pattern is right
 820        hda_dict[ 'annotation' ] = hda.get_item_annotation_str( trans.sa_session, trans.user, hda )
 821        #annotation = getattr( hda, 'annotation', hda.get_item_annotation_str( trans.sa_session, trans.user, hda ) )
 822
 823        # ---- return here if deleted AND purged OR can't access
 824        purged = ( hda.purged or hda.dataset.purged )
 825        if ( hda.deleted and purged ):
 826            #TODO: to_dict should really go AFTER this - only summary data
 827            return trans.security.encode_dict_ids( hda_dict )
 828
 829        if expose_dataset_path:
 830            try:
 831                hda_dict[ 'file_name' ] = hda.file_name
 832            except objectstore.ObjectNotFound, onf:
 833                log.exception( 'objectstore.ObjectNotFound, HDA %s: %s', hda.id, onf )
 834
 835        hda_dict[ 'download_url' ] = url_for( 'history_contents_display',
 836            history_id = trans.security.encode_id( hda.history.id ),
 837            history_content_id = trans.security.encode_id( hda.id ) )
 838
 839        # indeces, assoc. metadata files, etc.
 840        meta_files = []
 841        for meta_type in hda.metadata.spec.keys():
 842            if isinstance( hda.metadata.spec[ meta_type ].param, FileParameter ):
 843                meta_files.append( dict( file_type=meta_type ) )
 844        if meta_files:
 845            hda_dict[ 'meta_files' ] = meta_files
 846
 847        # currently, the viz reg is optional - handle on/off
 848        if trans.app.visualizations_registry:
 849            hda_dict[ 'visualizations' ] = trans.app.visualizations_registry.get_visualizations( trans, hda )
 850        else:
 851            hda_dict[ 'visualizations' ] = hda.get_visualizations()
 852        #TODO: it may also be wiser to remove from here and add as API call that loads the visualizations
 853        #           when the visualizations button is clicked (instead of preloading/pre-checking)
 854
 855        # ---- return here if deleted
 856        if hda.deleted and not purged:
 857            return trans.security.encode_dict_ids( hda_dict )
 858
 859        return trans.security.encode_dict_ids( hda_dict )
 860
 861    def get_inaccessible_hda_dict( self, trans, hda ):
 862        return trans.security.encode_dict_ids({
 863            'id'        : hda.id,
 864            'history_id': hda.history.id,
 865            'hid'       : hda.hid,
 866            'name'      : hda.name,
 867            'state'     : hda.state,
 868            'deleted'   : hda.deleted,
 869            'visible'   : hda.visible,
 870            'accessible': False
 871        })
 872
 873    def get_hda_dict_with_error( self, trans, hda=None, history_id=None, id=None, error_msg='Error' ):
 874        return trans.security.encode_dict_ids({
 875            'id'        : hda.id if hda else id,
 876            'history_id': hda.history.id if hda else history_id,
 877            'hid'       : hda.hid if hda else '(unknown)',
 878            'name'      : hda.name if hda else '(unknown)',
 879            'error'     : error_msg,
 880            'state'     : trans.model.Dataset.states.NEW
 881        })
 882
 883    def get_display_apps( self, trans, hda ):
 884        display_apps = []
 885        for display_app in hda.get_display_applications( trans ).itervalues():
 886
 887            app_links = []
 888            for link_app in display_app.links.itervalues():
 889                app_links.append({
 890                    'target': link_app.url.get( 'target_frame', '_blank' ),
 891                    'href'  : link_app.get_display_url( hda, trans ),
 892                    'text'  : gettext( link_app.name )
 893                })
 894            if app_links:
 895                display_apps.append( dict( label=display_app.name, links=app_links ) )
 896
 897        return display_apps
 898
 899    def get_old_display_applications( self, trans, hda ):
 900        display_apps = []
 901        if not trans.app.config.enable_old_display_applications:
 902            return display_apps
 903
 904        for display_app in hda.datatype.get_display_types():
 905            target_frame, display_links = hda.datatype.get_display_links( hda,
 906                display_app, trans.app, trans.request.base )
 907
 908            if len( display_links ) > 0:
 909                display_label = hda.datatype.get_display_label( display_app )
 910
 911                app_links = []
 912                for display_name, display_link in display_links:
 913                    app_links.append({
 914                        'target': target_frame,
 915                        'href'  : display_link,
 916                        'text'  : gettext( display_name )
 917                    })
 918                if app_links:
 919                    display_apps.append( dict( label=display_label, links=app_links ) )
 920
 921        return display_apps
 922
 923    def set_hda_from_dict( self, trans, hda, new_data ):
 924        """
 925        Changes HDA data using the given dictionary new_data.
 926        """
 927        # precondition: access of the hda has already been checked
 928
 929        # send what we can down into the model
 930        changed = hda.set_from_dict( new_data )
 931        # the rest (often involving the trans) - do here
 932        if 'annotation' in new_data.keys() and trans.get_user():
 933            hda.add_item_annotation( trans.sa_session, trans.get_user(), hda, new_data[ 'annotation' ] )
 934            changed[ 'annotation' ] = new_data[ 'annotation' ]
 935        if 'tags' in new_data.keys() and trans.get_user():
 936            self.set_tags_from_list( trans, hda, new_data[ 'tags' ], user=trans.user )
 937        # sharing/permissions?
 938        # purged
 939
 940        if changed.keys():
 941            trans.sa_session.flush()
 942
 943        return changed
 944
 945    def get_hda_job( self, hda ):
 946        # Get dataset's job.
 947        job = None
 948        for job_output_assoc in hda.creating_job_associations:
 949            job = job_output_assoc.job
 950            break
 951        return job
 952
 953    def stop_hda_creating_job( self, hda ):
 954        """
 955        Stops an HDA's creating job if all the job's other outputs are deleted.
 956        """
 957        if hda.parent_id is None and len( hda.creating_job_associations ) > 0:
 958            # Mark associated job for deletion
 959            job = hda.creating_job_associations[0].job
 960            if job.state in [ self.app.model.Job.states.QUEUED, self.app.model.Job.states.RUNNING, self.app.model.Job.states.NEW ]:
 961                # Are *all* of the job's other output datasets deleted?
 962                if job.check_if_output_datasets_deleted():
 963                    job.mark_deleted( self.app.config.track_jobs_in_database )
 964                    self.app.job_manager.job_stop_queue.put( job.id )
 965
 966
 967class UsesLibraryMixin:
 968
 969    def get_library( self, trans, id, check_ownership=False, check_accessible=True ):
 970        l = self.get_object( trans, id, 'Library' )
 971        if check_accessible and not ( trans.user_is_admin() or trans.app.security_agent.can_access_library( trans.get_current_user_roles(), l ) ):
 972            error( "LibraryFolder is not accessible to the current user" )
 973        return l
 974
 975
 976class UsesLibraryMixinItems( SharableItemSecurityMixin ):
 977
 978    def get_library_folder( self, trans, id, check_ownership=False, check_accessible=True ):
 979        return self.get_object( trans, id, 'LibraryFolder',
 980                                check_ownership=False, check_accessible=check_accessible )
 981
 982    def get_library_dataset_dataset_association( self, trans, id, check_ownership=False, check_accessible=True ):
 983        return self.get_object( trans, id, 'LibraryDatasetDatasetAssociation',
 984                                check_ownership=False, check_accessible=check_accessible )
 985
 986    def get_library_dataset( self, trans, id, check_ownership=False, check_accessible=True ):
 987        return self.get_object( trans, id, 'LibraryDataset',
 988                                check_ownership=False, check_accessible=check_accessible )
 989
 990    #TODO: it makes no sense that I can get roles from a user but not user.is_admin()
 991    #def can_user_add_to_library_item( self, trans, user, item ):
 992    #    if not user: return False
 993    #    return (  ( user.is_admin() )
 994    #           or ( trans.app.security_agent.can_add_library_item( user.all_roles(), item ) ) )
 995
 996    def can_current_user_add_to_library_item( self, trans, item ):
 997        if not trans.user: return False
 998        return (  ( trans.user_is_admin() )
 999               or ( trans.app.security_agent.can_add_library_item( trans.get_current_user_roles(), item ) ) )
1000
1001    def copy_hda_to_library_folder( self, trans, hda, library_folder, roles=None, ldda_message='' ):
1002        #PRECONDITION: permissions for this action on hda and library_folder have been checked
1003        roles = roles or []
1004
1005        # this code was extracted from library_common.add_history_datasets_to_library
1006        #TODO: refactor library_common.add_history_datasets_to_library to use this for each hda to copy
1007
1008        # create the new ldda and apply the folder perms to it
1009        ldda = hda.to_library_dataset_dataset_association( trans, target_folder=library_folder,
1010                                                           roles=roles, ldda_message=ldda_message )
1011        self._apply_library_folder_permissions_to_ldda( trans, library_folder, ldda )
1012        self._apply_hda_permissions_to_ldda( trans, hda, ldda )
1013        #TODO:?? not really clear on how permissions are being traded here
1014        #   seems like hda -> ldda permissions should be set in to_library_dataset_dataset_association
1015        #   then they get reset in _apply_library_folder_permissions_to_ldda
1016        #   then finally, re-applies hda -> ldda for missing actions in _apply_hda_permissions_to_ldda??
1017        return ldda
1018
1019    def _apply_library_folder_permissions_to_ldda( self, trans, library_folder, ldda ):
1020        """
1021        Copy actions/roles from library folder to an ldda (and it's library_dataset).
1022        """
1023        #PRECONDITION: permissions for this action on library_folder and ldda have been checked
1024        security_agent = trans.app.security_agent
1025        security_agent.copy_library_permissions( trans, library_folder, ldda )
1026        security_agent.copy_library_permissions( trans, library_folder, ldda.library_dataset )
1027        return security_agent.get_permissions( ldda )
1028
1029    def _apply_hda_permissions_to_ldda( self, trans, hda, ldda ):
1030        """
1031        Copy actions/roles from hda to ldda.library_dataset (and then ldda) if ldda
1032        doesn't already have roles for the given action.
1033        """
1034        #PRECONDITION: permissions for this action on hda and ldda have been checked
1035        # Make sure to apply any defined dataset permissions, allowing the permissions inherited from the
1036        #   library_dataset to over-ride the same permissions on the dataset, if they exist.
1037        security_agent = trans.app.security_agent
1038        dataset_permissions_dict = security_agent.get_permissions( hda.dataset )
1039        library_dataset = ldda.library_dataset
1040        library_dataset_actions = [ permission.action for permission in library_dataset.actions ]
1041
1042        # except that: if DATASET_MANAGE_PERMISSIONS exists in the hda.dataset permissions,
1043        #   we need to instead apply those roles to the LIBRARY_MANAGE permission to the library dataset
1044        dataset_manage_permissions_action = security_agent.get_action( 'DATASET_MANAGE_PERMISSIONS' ).action
1045        library_manage_permissions_action = security_agent.get_action( 'LIBRARY_MANAGE' ).action
1046        #TODO: test this and remove if in loop below
1047        #TODO: doesn't handle action.action
1048        #if dataset_manage_permissions_action in dataset_permissions_dict:
1049        #    managing_roles = dataset_permissions_dict.pop( dataset_manage_permissions_action )
1050        #    dataset_permissions_dict[ library_manage_permissions_action ] = managing_roles
1051
1052        flush_needed = False
1053        for action, dataset_permissions_roles in dataset_permissions_dict.items():
1054            if isinstance( action, security.Action ):
1055                action = action.action
1056
1057            # alter : DATASET_MANAGE_PERMISSIONS -> LIBRARY_MANAGE (see above)
1058            if action == dataset_manage_permissions_action:
1059                action = library_manage_permissions_action
1060
1061            #TODO: generalize to util.update_dict_without_overwrite
1062            # add the hda actions & roles to the library_dataset
1063            #NOTE: only apply an hda perm if it's NOT set in the library_dataset perms (don't overwrite)
1064            if action not in library_dataset_actions:
1065                for role in dataset_permissions_roles:
1066                    ldps = trans.model.LibraryDatasetPermissions( action, library_dataset, role )
1067                    ldps = [ ldps ] if not isinstance( ldps, list ) else ldps
1068                    for ldp in ldps:
1069                        trans.sa_session.add( ldp )
1070                        flush_needed = True
1071
1072        if flush_needed:
1073            trans.sa_session.flush()
1074
1075        # finally, apply the new library_dataset to it's associated ldda (must be the same)
1076        security_agent.copy_library_permissions( trans, library_dataset, ldda )
1077        return security_agent.get_permissions( ldda )
1078
1079
1080class UsesVisualizationMixin( UsesHistoryDatasetAssociationMixin, UsesLibraryMixinItems ):
1081    """
1082    Mixin for controllers that use Visualization objects.
1083    """
1084
1085    viz_types = [ "trackster" ]
1086
1087    def get_visualization( self, trans, id, check_ownership=True, check_accessible=False ):
1088        """
1089        Get a Visualization from the database by id, verifying ownership.
1090        """
1091        # Load workflow from database
1092        try:
1093            visualization = trans.sa_session.query( trans.model.Visualization ).get( trans.security.decode_id( id ) )
1094        except TypeError:
1095            visualization = None
1096        if not visualization:
1097            error( "Visualization not found" )
1098        else:
1099            return self.security_check( trans, visualization, check_ownership, check_accessible )
1100
1101    def get_visualizations_by_user( self, trans, user, order_by=None, query_only=False ):
1102        """
1103        Return query or query results of visualizations filtered by a user.
1104
1105        Set `order_by` to a column or list of columns to change the order
1106        returned. Defaults to `DEFAULT_ORDER_BY`.
1107        Set `query_only` to return just the query for further filtering or
1108        processing.
1109        """
1110        #TODO: move into model (as class attr)
1111        DEFAULT_ORDER_BY = [ model.Visualization.title ]
1112        if not order_by:
1113            order_by = DEFAULT_ORDER_BY
1114        if not isinstance( order_by, list ):
1115            order_by = [ order_by ]
1116        query = trans.sa_session.query( model.Visualization )
1117        query = query.filter( model.Visualization.user == user )
1118        if order_by:
1119            query = query.order_by( *order_by )
1120        if query_only:
1121            return query
1122        return query.all()
1123
1124    def get_visualizations_shared_with_user( self, trans, user, order_by=None, query_only=False ):
1125        """
1126        Return query or query results for visualizations shared with the given user.
1127
1128        Set `order_by` to a column or list of columns to change the order
1129        returned. Defaults to `DEFAULT_ORDER_BY`.
1130        Set `query_only` to return just the query for further filtering or
1131        processing.
1132        """
1133        DEFAULT_ORDER_BY = [ model.Visualization.title ]
1134        if not order_by:
1135            order_by = DEFAULT_ORDER_BY
1136        if not isinstance( order_by, list ):
1137            order_by = [ order_by ]
1138        query = trans.sa_session.query( model.Visualization ).join( model.VisualizationUserShareAssociation )
1139        query = query.filter( model.VisualizationUserShareAssociation.user_id == user.id )
1140        # remove duplicates when a user shares with themselves?
1141        query = query.filter( model.Visualization.user_id != user.id )
1142        if order_by:
1143            query = query.order_by( *order_by )
1144        if query_only:
1145            return query
1146        return query.all()
1147
1148    def get_published_visualizations( self, trans, exclude_user=None, order_by=None, query_only=False ):
1149        """
1150        Return query or query results for published visualizations optionally excluding
1151        the user in `exclude_user`.
1152
1153        Set `order_by` to a column or list of columns to change the order
1154        returned. Defaults to `DEFAULT_ORDER_BY`.
1155        Set `query_only` to return just the query for further filtering or
1156        processing.
1157        """
1158        DEFAULT_ORDER_BY = [ model.Visualization.title ]
1159        if not order_by:
1160            order_by = DEFAULT_ORDER_BY
1161        if not isinstance( order_by, list ):
1162            order_by = [ order_by ]
1163        query = trans.sa_session.query( model.Visualization )
1164        query = query.filter( model.Visualization.published == True )
1165        if exclude_user:
1166            query = query.filter( model.Visualization.user != exclude_user )
1167        if order_by:
1168            query = query.order_by( *order_by )
1169        if query_only:
1170            return query
1171        return query.all()
1172
1173    #TODO: move into model (to_dict)
1174    def get_visualization_summary_dict( self, visualization ):
1175        """
1176        Return a set of summary attributes for a visualization in dictionary form.
1177        NOTE: that encoding ids isn't done here should happen at the caller level.
1178        """
1179        #TODO: deleted
1180        #TODO: importable
1181        return {
1182            'id'        : visualization.id,
1183            'title'     : visualization.title,
1184            'type'      : visualization.type,
1185            'dbkey'     : visualization.dbkey,
1186        }
1187
1188    def get_visualization_dict( self, visualization ):
1189        """
1190        Return a set of detailed attributes for a visualization in dictionary form.
1191        The visualization's latest_revision is returned in its own sub-dictionary.
1192        NOTE: that encoding ids isn't done here should happen at the caller level.
1193        """
1194        return {
1195            'model_class': 'Visualization',
1196            'id'        : visualization.id,
1197            'title'     : visualization.title,
1198            'type'      : visualization.type,
1199            'user_id'   : visualization.user.id,
1200            'dbkey'     : visualization.dbkey,
1201            'slug'      : visualization.slug,
1202            # to_dict only the latest revision (allow older to be fetched elsewhere)
1203            'latest_revision' : self.get_visualization_revision_dict( visualization.latest_revision ),
1204            'revisions' : [ r.id for r in visualization.revisions ],
1205        }
1206
1207    def get_visualization_revision_dict( self, revision ):
1208        """
1209        Return a set of detailed attributes for a visualization in dictionary form.
1210        NOTE: that encoding ids isn't done here should happen at the caller level.
1211        """
1212        return {
1213            'model_class': 'VisualizationRevision',
1214            'id'        : revision.id,
1215            'visualization_id' : revision.visualization.id,
1216            'title'     : revision.title,
1217            'dbkey'     : revision.dbkey,
1218            'config'    : revision.config,
1219        }
1220
1221    def import_visualization( self, trans, id, user=None ):
1222        """
1223        Copy the visualization with the given id and associate the copy
1224        with the given user (defaults to trans.user).
1225
1226        Raises `ItemAccessibilityException` if `user` is not passed and
1227        the current user is anonymous, and if the visualization is not `importable`.
1228        Raises `ItemDeletionException` if the visualization has been deleted.
1229        """
1230        # default to trans.user, error if anon
1231        if not user:
1232            if not trans.user:
1233                raise ItemAccessibilityException( "You must be logged in to import Galaxy visualizations" )
1234            user = trans.user
1235
1236        # check accessibility
1237        visualization = self.get_visualization( trans, id, check_ownership=False )
1238        if not visualization.importable:
1239            raise ItemAccessibilityException( "The owner of this visualization has disabled imports via this link." )
1240        if visualization.deleted:
1241            raise ItemDeletionException( "You can't import this visualization because it has been deleted." )
1242
1243        # copy vis and alter title
1244        #TODO: need to handle custom db keys.
1245        imported_visualization = visualization.copy( user=user, title="imported: " + visualization.title )
1246        trans.sa_session.add( imported_visualization )
1247        trans.sa_session.flush()
1248        return imported_visualization
1249
1250    def create_visualization( self, trans, type, title="Untitled Visualization", slug=None,
1251                              dbkey=None, annotation=None, config={}, save=True ):
1252        """
1253        Create visualiation and first revision.
1254        """
1255        visualization = self._create_visualization( trans, title, type, dbkey, slug, annotation, save )
1256        #TODO: handle this error structure better either in _create or here
1257        if isinstance( visualization, dict ):
1258            err_dict = visualization
1259            raise ValueError( err_dict[ 'title_err' ] or err_dict[ 'slug_err' ] )
1260
1261        # Create and save first visualization revision
1262        revision = trans.model.VisualizationRevision( visualization=visualization, title=title,
1263                                                      config=config, dbkey=dbkey )
1264        visualization.latest_revision = revision
1265
1266        if save:
1267            session = trans.sa_session
1268            session.add( revision )
1269            session.flush()
1270
1271        return visualization
1272
1273    def add_visualization_revision( self, trans, visualization, config, title, dbkey ):
1274        """
1275        Adds a new `VisualizationRevision` to the given `visualization` with
1276        the given parameters and set its parent visualization's `latest_revision`
1277        to the new revision.
1278        """
1279        #precondition: only add new revision on owned vis's
1280        #TODO:?? should we default title, dbkey, config? to which: visualization or latest_revision?
1281        revision = trans.model.VisualizationRevision( visualization, title, dbkey, config )
1282        visualization.latest_revision = revision
1283        #TODO:?? does this automatically add revision to visualzation.revisions?
1284        trans.sa_session.add( revision )
1285        trans.sa_session.flush()
1286        return revision
1287
1288    def save_visualization( self, trans, config, type, id=None, title=None, dbkey=None, slug=None, annotation=None ):
1289        session = trans.sa_session
1290
1291        # Create/get visualization.
1292        if not id:
1293            # Create new visualization.
1294            vis = self._create_visualization( trans, title, type, dbkey, slug, annotation )
1295        else:
1296            decoded_id = trans.security.decode_id( id )
1297            vis = session.query( trans.model.Visualization ).get( decoded_id )
1298
1299        # Create new VisualizationRevision that will be attached to the viz
1300        vis_rev = trans.model.VisualizationRevision()
1301        vis_rev.visualization = vis
1302        # do NOT alter the dbkey
1303        vis_rev.dbkey = vis.dbkey
1304        # do alter the title and config
1305        vis_rev.title = title
1306
1307        # -- Validate config. --
1308
1309        if vis.type == 'trackster':
1310            def unpack_track( track_dict ):
1311                """ Unpack a track from its json. """
1312                dataset_dict = track_dict[ 'dataset' ]
1313                return {
1314                    "dataset_id": trans.security.decode_id( dataset_dict['id'] ),
1315                    "hda_ldda": dataset_dict.get('hda_ldda', 'hda'),
1316                    "track_type": track_dict['track_type'],
1317                    "prefs": track_dict['prefs'],
1318                    "mode": track_dict['mode'],
1319                    "filters": track_dict['filters'],
1320                    "tool_state": track_dict['tool_state']
1321                }
1322
1323            def unpack_collection( collection_json ):
1324                """ Unpack a collection from its json. """
1325                unpacked_drawables = []
1326                drawables = collection_json[ 'drawables' ]
1327                for drawable_json in drawables:
1328                    if 'track_type' in drawable_json:
1329                        drawable = unpack_track( drawable_json )
1330                    else:
1331                        drawable = unpack_collection( drawable_json )
1332                    unpacked_drawables.append( drawable )
1333                return {
1334                    "obj_type": collection_json[ 'obj_type' ],
1335                    "drawables": unpacked_drawables,
1336                    "prefs": collection_json.get( 'prefs' , [] ),
1337                    "filters": collection_json.get( 'filters', None )
1338                }
1339
1340            # TODO: unpack and validate bookmarks:
1341            def unpack_bookmarks( bookmarks_json ):
1342                return bookmarks_json
1343
1344            # Unpack and validate view content.
1345            view_content = unpack_collection( config[ 'view' ] )
1346            bookmarks = unpack_bookmarks( config[ 'bookmarks' ] )
1347            vis_rev.config = { "view": view_content, "bookmarks": bookmarks }
1348            # Viewport from payload
1349            if 'viewport' in config:
1350                chrom = config['viewport']['chrom']
1351                start = config['viewport']['start']
1352                end = config['viewport']['end']
1353                overview = config['viewport']['overview']
1354                vis_rev.config[ "viewport" ] = { 'chrom': chrom, 'start': start, 'end': end, 'overview': overview }
1355        else:
1356            # Default action is to save the config as is with no validation.
1357            vis_rev.config = config
1358
1359        vis.latest_revision = vis_rev
1360        session.add( vis_rev )
1361        session.flush()
1362        encoded_id = trans.security.encode_id( vis.id )
1363        return { "vis_id": encoded_id, "url": url_for( controller='visualization', action=vis.type, id=encoded_id ) }
1364
1365    def get_tool_def( self, trans, hda ):
1366        """ Returns definition of an interactive tool for an HDA. """
1367
1368        job = self.get_hda_job( hda )
1369        if not job:
1370            return None
1371        tool = trans.app.toolbox.get_tool( job.tool_id )
1372        if not tool:
1373            return None
1374
1375        # Tool must have a Trackster configuration.
1376        if not tool.trackster_conf:
1377            return None
1378
1379        # -- Get tool definition and add input values from job. --
1380        tool_dict = tool.to_dict( trans, io_details=True )
1381        tool_param_values = dict( [ ( p.name, p.value ) for p in job.parameters ] )
1382        tool_param_values = tool.params_from_strings( tool_param_values, trans.app, ignore_errors=True )
1383
1384        # Only get values for simple inputs for now.
1385        inputs_dict = [ i for i in tool_dict[ 'inputs' ] if i[ 'type' ] not in [ 'data', 'hidden_data', 'conditional' ] ]
1386        for t_input in inputs_dict:
1387            # Add value to tool.
1388            if 'name' in t_input:
1389                name = t_input[ 'name' ]
1390                if name in tool_param_values:
1391                    value = tool_param_values[ name ]
1392                    if isinstance( value, Dictifiable ):
1393                        value = value.to_dict()
1394                    t_input[ 'value' ] = value
1395
1396        return tool_dict
1397
1398    def get_visualization_config( self, trans, visualization ):
1399        """ Returns a visualization's configuration. Only works for trackster visualizations right now. """
1400        config = None
1401        if visualization.type in [ 'trackster', 'genome' ]:
1402            # Unpack Trackster config.
1403            latest_revision = visualization.latest_revision
1404            bookmarks = latest_revision.config.get( 'bookmarks', [] )
1405
1406            def pack_track( track_dict ):
1407                dataset_id = track_dict['dataset_id']
1408                hda_ldda = track_dict.get('hda_ldda', 'hda')
1409                if hda_ldda == 'ldda':
1410                    # HACK: need to encode library dataset ID because get_hda_or_ldda
1411                    # only works for encoded datasets.
1412                    dataset_id = trans.security.encode_id( dataset_id )
1413                dataset = self.get_hda_or_ldda( trans, hda_ldda, dataset_id )
1414
1415                try:
1416                    prefs = track_dict['prefs']
1417                except KeyError:
1418                    prefs = {}
1419
1420                track_data_provider = trans.app.data_provider_registry.get_data_provider( trans,
1421                                                                                          original_dataset=dataset,
1422                                                                                          source='data' )
1423                return {
1424                    "track_type": dataset.datatype.track_type,
1425                    "dataset": trans.security.encode_dict_ids( dataset.to_dict() ),
1426                    "prefs": prefs,
1427                    "mode": track_dict.get( 'mode', 'Auto' ),
1428                    "filters": track_dict.get( 'filters', { 'filters' : track_data_provider.get_filters() } ),
1429                    "tool": self.get_tool_def( trans, dataset ),
1430                    "tool_state": track_dict.get( 'tool_state', {} )
1431                }
1432
1433            def pack_collection( collection_dict ):
1434                drawables = []
1435                for drawable_dict in collection_dict[ 'drawables' ]:
1436                    if 'track_type' in drawable_dict:
1437                        drawables.append( pack_track( drawable_dict ) )
1438                    else:
1439                        drawables.append( pack_collection( drawable_dict ) )
1440                return {
1441                    'obj_type': collection_dict[ 'obj_type' ],
1442                    'drawables': drawables,
1443                    'prefs': collection_dict.get( 'prefs', [] ),
1444                    'filters': collection_dict.get( 'filters', {} )
1445                }
1446
1447            def encode_dbkey( dbkey ):
1448                """
1449                Encodes dbkey as needed. For now, prepends user's public name
1450                to custom dbkey keys.
1451                """
1452                encoded_dbkey = dbkey
1453                user = visualization.user
1454                if 'dbkeys' in user.preferences and dbkey in user.preferences[ 'dbkeys' ]:
1455                    encoded_dbkey = "%s:%s" % ( user.username, dbkey )
1456                return encoded_dbkey
1457
1458            # Set tracks.
1459            tracks = []
1460            if 'tracks' in latest_revision.config:
1461                # Legacy code.
1462                for track_dict in visualization.latest_revision.config[ 'tracks' ]:
1463                    tracks.append( pack_track( track_dict ) )
1464            elif 'view' in latest_revision.config:
1465                for drawable_dict in visualization.latest_revision.config[ 'view' ][ 'drawables' ]:
1466                    if 'track_type' in drawable_dict:
1467                        tracks.append( pack_track( drawable_dict ) )
1468                    else:
1469                        tracks.append( pack_collection( drawable_dict ) )
1470
1471            config = {  "title": visualization.title,
1472                        "vis_id": trans.security.encode_id( visualization.id ),
1473                        "tracks": tracks,
1474                        "bookmarks": bookmarks,
1475                        "chrom": "",
1476                        "dbkey": encode_dbkey( visualization.dbkey ) }
1477
1478            if 'viewport' in latest_revision.config:
1479                config['viewport'] = latest_revision.config['viewport']
1480        else:
1481            # Default action is to return config unaltered.
1482            latest_revision = visualization.latest_revision
1483            config = latest_revision.config
1484
1485        return config
1486
1487    def get_new_track_config( self, trans, dataset ):
1488        """
1489        Returns track configuration dict for a dataset.
1490        """
1491        # Get data provider.
1492        track_data_provider = trans.app.data_provider_registry.get_data_provider( trans, original_dataset=dataset )
1493
1494        if isinstance( dataset, trans.app.model.HistoryDatasetAssociation ):
1495            hda_ldda = "hda"
1496        elif isinstance( dataset, trans.app.model.LibraryDatasetDatasetAssociation ):
1497            hda_ldda = "ldda"
1498
1499        # Get track definition.
1500        return {
1501            "track_type": dataset.datatype.track_type,
1502            "name": dataset.name,
1503            "dataset": trans.security.encode_dict_ids( dataset.to_dict() ),
1504            "prefs": {},
1505            "filters": { 'filters' : track_data_provider.get_filters() },
1506            "tool": self.get_tool_def( trans, dataset ),
1507            "tool_state": {}
1508        }
1509
1510    def get_hda_or_ldda( self, trans, hda_ldda, dataset_id ):
1511        """ Returns either HDA or LDDA for hda/ldda and id combination. """
1512        if hda_ldda == "hda":
1513            return self.get_dataset( trans, dataset_id, check_ownership=False, check_accessible=True )
1514        else:
1515            return self.get_library_dataset_dataset_association( trans, dataset_id )
1516
1517    # -- Helper functions --
1518
1519    def _create_visualization( self, trans, title, type, dbkey=None, slug=None, annotation=None, save=True ):
1520        """ Create visualization but not first revision. Returns Visualization object. """
1521        user = trans.get_user()
1522
1523        # Error checking.
1524        title_err = slug_err = ""
1525        if not title:
1526            title_err = "visualization name is required"
1527        elif slug and not _is_valid_slug( slug ):
1528            slug_err = "visualization identifier must consist of only lowercase letters, numbers, and the '-' character"
1529        elif slug and trans.sa_session.query( trans.model.Visualization ).filter_by( user=user, slug=slug, deleted=False ).first():
1530            slug_err = "visualization identifier must be unique"
1531
1532        if title_err or slug_err:
1533            return { 'title_err': title_err, 'slug_err': slug_err }
1534
1535        # Create visualization
1536        visualization = trans.model.Visualization( user=user, title=title, dbkey=dbkey, type=type )
1537        if slug:
1538            visualization.slug = slug
1539        else:
1540            self.create_item_slug( trans.sa_session, visualization )
1541        if annotation:
1542            annotation = sanitize_html( annotation, 'utf-8', 'text/html' )
1543            #TODO: if this is to stay in the mixin, UsesAnnotations should be added to the superclasses
1544            #   right now this is depending on the classes that include this mixin to have UsesAnnotations
1545            self.add_item_annotation( trans.sa_session, trans.user, visualization, annotation )
1546
1547        if save:
1548            session = trans.sa_session
1549            session.add( visualization )
1550            session.flush()
1551
1552        return visualization
1553
1554    def _get_genome_data( self, trans, dataset, dbkey=None ):
1555        """
1556        Returns genome-wide data for dataset if available; if not, message is returned.
1557        """
1558        rval = None
1559
1560        # Get data sources.
1561        data_sources = dataset.get_datasources( trans )
1562        query_dbkey = dataset.dbkey
1563        if query_dbkey == "?":
1564            query_dbkey = dbkey
1565        chroms_info = self.app.genomes.chroms( trans, dbkey=query_dbkey )
1566
1567        # If there are no messages (messages indicate data is not ready/available), get data.
1568        messages_list = [ data_source_dict[ 'message' ] for data_source_dict in data_sources.values() ]
1569        message = self._get_highest_priority_msg( messages_list )
1570        if message:
1571            rval = message
1572        else:
1573            # HACK: chromatin interactions tracks use data as source.
1574            source = 'index'
1575            if isinstance( dataset.datatype, ChromatinInteractions ):
1576                source = 'data'
1577
1578            data_provider = trans.app.data_provider_registry.get_data_provider( trans,
1579                                                                                original_dataset=dataset,
1580                                                                                source=source )
1581            # HACK: pass in additional params which are used for only some
1582            # types of data providers; level, cutoffs used for summary tree,
1583            # num_samples for BBI, and interchromosomal used for chromatin interactions.
1584            rval = data_provider.get_genome_data( chroms_info,
1585                                                  level=4, detail_cutoff=0, draw_cutoff=0,
1586                                                  num_samples=150,
1587                                                  interchromosomal=True )
1588
1589        return rval
1590
1591    # FIXME: this method probably belongs down in the model.Dataset class.
1592    def _get_highest_priority_msg( self, message_list ):
1593        """
1594        Returns highest priority message from a list of messages.
1595        """
1596        return_message = None
1597
1598        # For now, priority is: job error (dict), no converter, pending.
1599        for message in message_list:
1600            if message is not None:
1601                if isinstance(message, dict):
1602                    return_message = message
1603                    break
1604                elif message == "no converter":
1605                    return_message = message
1606                elif return_message == None and message == "pending":
1607                    return_message = message
1608        return return_message
1609
1610
1611class UsesStoredWorkflowMixin( SharableItemSecurityMixin, UsesAnnotations ):
1612    """ Mixin for controllers that use StoredWorkflow objects. """
1613
1614    def get_stored_workflow( self, trans, id, check_ownership=True, check_accessible=False ):
1615        """ Get a StoredWorkflow from the database by id, verifying ownership. """
1616        # Load workflow from database
1617        try:
1618            workflow = trans.sa_session.query( trans.model.StoredWorkflow ).get( trans.security.decode_id( id ) )
1619        except TypeError:
1620            workflow = None
1621
1622        if not workflow:
1623            error( "Workflow not found" )
1624        else:
1625            self.security_check( trans, workflow, check_ownership, check_accessible )
1626
1627            # Older workflows may be missing slugs, so set them here.
1628            if not workflow.slug:
1629                self.create_item_slug( trans.sa_session, workflow )
1630                trans.sa_session.flush()
1631
1632        return workflow
1633
1634    def get_stored_workflow_steps( self, trans, stored_workflow ):
1635        """ Restores states for a stored workflow's steps. """
1636        for step in stored_workflow.latest_workflow.steps:
1637            step.upgrade_messages = {}
1638            if step.type == 'tool' or step.type is None:
1639                # Restore the tool state for the step
1640                module = module_factory.from_workflow_step( trans, step )
1641                if module:
1642                    #Check if tool was upgraded
1643                    step.upgrade_messages = module.check_and_update_state()
1644                    # Any connected input needs to have value DummyDataset (these
1645                    # are not persisted so we need to do it every time)
1646                    module.add_dummy_datasets( connections=step.input_connections )
1647                    # Store state with the step
1648                    step.module = module
1649                    step.state = module.state
1650                else:
1651                    step.upgrade_messages = "Unknown Tool ID"
1652                    step.module = None
1653                    step.state = None
1654            else:
1655                ## Non-tool specific stuff?
1656                step.module = module_factory.from_workflow_step( trans, step )
1657                step.state = step.module.get_runtime_state()
1658            # Connections by input name
1659            step.input_connections_by_name = dict( ( conn.input_name, conn ) for conn in step.input_connections )
1660
1661    def _import_shared_workflow( self, trans, stored):
1662        """ """
1663        # Copy workflow.
1664        imported_stored = model.StoredWorkflow()
1665        imported_stored.name = "imported: " + stored.name
1666        imported_stored.latest_workflow = stored.latest_workflow
1667        imported_stored.user = trans.user
1668        # Save new workflow.
1669        session = trans.sa_session
1670        session.add( imported_stored )
1671        session.flush()
1672
1673        # Copy annotations.
1674        self.copy_item_annotation( session, stored.user, stored, imported_stored.user, imported_stored )
1675        for order_index, step in enumerate( stored.latest_workflow.steps ):
1676            self.copy_item_annotation( session, stored.user, step, \
1677                                        imported_stored.user, imported_stored.latest_workflow.steps[order_index] )
1678        session.flush()
1679        return imported_stored
1680
1681    def _workflow_from_dict( self, trans, data, source=None, add_to_menu=False ):
1682        """
1683        Creates a workflow from a dict. Created workflow is stored in the database and returned.
1684        """
1685
1686        # Put parameters in workflow mode
1687        trans.workflow_building_mode = True
1688        # Create new workflow from incoming dict
1689        workflow = model.Workflow()
1690        # If there's a source, put it in the workflow name.
1691        if source:
1692            name = "%s (imported from %s)" % ( data['name'], source )
1693        else:
1694            name = data['name']
1695        workflow.name = name
1696        # Assume no errors until we find a step that has some
1697        workflow.has_errors = False
1698        # Create each step
1699        steps = []
1700        # The editor will provide ids for each step that we don't need to save,
1701        # but do need to use to make connections
1702        steps_by_external_id = {}
1703        # Keep track of tools required by the workflow that are not available in
1704        # the local Galaxy instance.  Each tuple in the list of missing_tool_tups
1705        # will be ( tool_id, tool_name, tool_version ).
1706        missing_tool_tups = []
1707        # First pass to build step objects and populate basic values
1708        for step_dict in data[ 'steps' ].itervalues():
1709            # Create the model class for the step
1710            step = model.WorkflowStep()
1711            steps.append( step )
1712            steps_by_external_id[ step_dict['id' ] ] = step
1713            # FIXME: Position should be handled inside module
1714            step.position = step_dict['position']
1715            module = module_factory.from_dict( trans, step_dict, secure=False )
1716            module.save_to_step( step )
1717            if module.type == 'tool' and module.tool is None:
1718                # A required tool is not available in the local Galaxy instance.
1719                missing_tool_tup = ( step_dict[ 'tool_id' ], step_dict[ 'name' ], step_dict[ 'tool_version' ] )
1720                if missing_tool_tup not in missing_tool_tups:
1721                    missing_tool_tups.append( missing_tool_tup )
1722                # Save the entire step_dict in the unused config field, be parsed later
1723                # when we do have the tool
1724                step.config = to_json_string(step_dict)
1725            if step.tool_errors:
1726                workflow.has_errors = True
1727            # Stick this in the step temporarily
1728            step.temp_input_connections = step_dict['input_connections']
1729            # Save step annotation.
1730            annotation = step_dict[ 'annotation' ]
1731            if annotation:
1732                annotation = sanitize_html( annotation, 'utf-8', 'text/html' )
1733                self.add_item_annotation( trans.sa_session, trans.get_user(), step, annotation )
1734        # Second pass to deal with connections between steps
1735        for step in steps:
1736            # Input connections
1737            for input_name, conn_list in step.temp_input_connections.iteritems():
1738                if not conn_list:
1739                    continue
1740                if not isinstance(conn_list, list):  # Older style singleton connection
1741                    conn_list = [conn_list]
1742                for conn_dict in conn_list:
1743                    conn = model.WorkflowStepConnection()
1744                    conn.input_step = step
1745                    conn.input_name = input_name
1746                    conn.output_name = conn_dict['output_name']
1747                    conn.output_step = steps_by_external_id[ conn_dict['id'] ]
1748            del step.temp_input_connections
1749
1750        # Order the steps if possible
1751        attach_ordered_steps( workflow, steps )
1752
1753        # Connect up
1754        stored = model.StoredWorkflow()
1755        stored.name = workflow.name
1756        workflow.stored_workflow = stored
1757        stored.latest_workflow = workflow
1758        stored.user = trans.user
1759        if data[ 'annotation' ]:
1760            self.add_item_annotation( trans.sa_session, stored.user, stored, data[ 'annotation' ] )
1761
1762        # Persist
1763        trans.sa_session.add( stored )
1764        trans.sa_session.flush()
1765
1766        if add_to_menu:
1767            if trans.user.stored_workflow_menu_entries == None:
1768                trans.user.stored_workflow_menu_entries = []
1769            menuEntry = model.StoredWorkflowMenuEntry()
1770            menuEntry.stored_workflow = stored
1771            trans.user.stored_workflow_menu_entries.append( menuEntry )
1772            trans.sa_session.flush()
1773
1774        return stored, missing_tool_tups
1775
1776    def _workflow_to_dict( self, trans, stored ):
1777        """
1778        Converts a workflow to a dict of attributes suitable for exporting.
1779        """
1780        workflow = stored.latest_workflow
1781        workflow_annotation = self.get_item_annotation_obj( trans.sa_session, trans.user, stored )
1782        annotation_str = ""
1783        if workflow_annotation:
1784            annotation_str = workflow_annotation.annotation
1785        # Pack workflow data into a dictionary and return
1786        data = {}
1787        data['a_galaxy_workflow'] = 'true' # Placeholder for identifying galaxy workflow
1788        data['format-version'] = "0.1"
1789        data['name'] = workflow.name
1790        data['annotation'] = annotation_str
1791        data['steps'] = {}
1792        # For each step, rebuild the form and encode the state
1793        for step in workflow.steps:
1794            # Load from database representation
1795            module = module_factory.from_workflow_step( trans, step )
1796            if not module:
1797                return None
1798            # Get user annotation.
1799            step_annotation = self.get_item_annotation_obj(trans.sa_session, trans.user, step )
1800            annotation_str = ""
1801            if step_annotation:
1802                annotation_str = step_annotation.annotation
1803            # Step info
1804            step_dict = {
1805                'id': step.order_index,
1806                'type': module.type,
1807                'tool_id': module.get_tool_id(),
1808                'tool_version' : step.tool_version,
1809                'name': module.get_name(),
1810                'tool_state': module.get_state( secure=False ),
1811                'tool_errors': module.get_errors(),
1812                ## 'data_inputs': module.get_data_inputs(),
1813                ## 'data_outputs': module.get_data_outputs(),
1814                'annotation' : annotation_str
1815            }
1816            # Add post-job actions to step dict.
1817            if module.type == 'tool':
1818                pja_dict = {}
1819                for pja in step.post_job_actions:
1820                    pja_dict[pja.action_type+pja.output_name] = dict( action_type = pja.action_type,
1821                                                                      output_name = pja.output_name,
1822                                                                      action_arguments = pja.action_arguments )
1823                step_dict[ 'post_job_actions' ] = pja_dict
1824            # Data inputs
1825            step_dict['inputs'] = []
1826            if module.type == "data_input":
1827                # Get input dataset name; default to 'Input Dataset'
1828                name = module.state.get( 'name', 'Input Dataset')
1829                step_dict['inputs'].append( { "name" : name, "description" : annotation_str } )
1830            else:
1831                # Step is a tool and may have runtime inputs.
1832                for name, val in module.state.inputs.items():
1833                    input_type = type( val )
1834                    if input_type == RuntimeValue:
1835                        step_dict['inputs'].append( { "name" : name, "description" : "runtime parameter for tool %s" % module.get_name() } )
1836                    elif input_type == dict:
1837                        # Input type is described by a dict, e.g. indexed parameters.
1838                        for partval in val.values():
1839                            if type( partval ) == RuntimeValue:
1840                                step_dict['inputs'].append( { "name" : name, "description" : "runtime parameter for tool %s" % module.get_name() } )
1841            # User outputs
1842            step_dict['user_outputs'] = []
1843            """
1844            module_outputs = module.get_data_outputs()
1845            step_outputs = trans.sa_session.query( WorkflowOutput ).filter( step=step )
1846            for output in step_outputs:
1847                name = output.output_name
1848                annotation = ""
1849                for module_output in module_outputs:
1850                    if module_output.get( 'name', None ) == name:
1851                        output_type = module_output.get( 'extension', '' )
1852                        break
1853                data['outputs'][name] = { 'name' : name, 'annotation' : annotation, 'type' : output_type }
1854            """
1855
1856            # All step outputs
1857            step_dict['outputs'] = []
1858            if type( module ) is ToolModule:
1859                for output in module.get_data_outputs():
1860                    step_dict['outputs'].append( { 'name' : output['name'], 'type' : output['extensions'][0] } )
1861            # Connections
1862            input_connections = step.input_connections
1863            if step.type is None or step.type == 'tool':
1864                # Determine full (prefixed) names of valid input datasets
1865                data_input_names = {}
1866                def callback( input, value, prefixed_name, prefixed_label ):
1867                    if isinstance( input, DataToolParameter ):
1868                        data_input_names[ prefixed_name ] = True
1869
1870                # FIXME: this updates modules silently right now; messages from updates should be provided.
1871                module.check_and_update_state()
1872                visit_input_values( module.tool.inputs, module.state.inputs, callback )
1873                # Filter
1874                # FIXME: this removes connection without displaying a message currently!
1875                input_connections = [ conn for conn in input_connections if conn.input_name in data_input_names ]
1876            # Encode input connections as dictionary
1877            input_conn_dict = {}
1878            unique_input_names = set( [conn.input_name for conn in input_connections] )
1879            for input_name in unique_input_names:
1880                input_conn_dict[ input_name ] = \
1881                    [ dict( id=conn.output_step.order_index, output_name=conn.output_name ) for conn in input_connections if conn.input_name == input_name ]
1882            # Preserve backward compatability. Previously Galaxy
1883            # assumed input connections would be dictionaries not
1884            # lists of dictionaries, so replace any singleton list
1885            # with just the dictionary so that workflows exported from
1886            # newer Galaxy instances can be used with older Galaxy
1887            # instances if they do no include multiple input
1888            # tools. This should be removed at some point. Mirrored
1889            # hack in _workflow_from_dict should never be removed so
1890            # existing workflow exports continue to function.
1891            for input_name, input_conn in dict(input_conn_dict).iteritems():
1892                if len(input_conn) == 1:
1893                    input_conn_dict[input_name] = input_conn[0]
1894            step_dict['input_connections'] = input_conn_dict
1895            # Position
1896            step_dict['position'] = step.position
1897            # Add to return value
1898            data['steps'][step.order_index] = step_dict
1899        return data
1900
1901
1902class UsesFormDefinitionsMixin:
1903    """Mixin for controllers that use Galaxy form objects."""
1904
1905    def get_all_forms( self, trans, all_versions=False, filter=None, form_type='All' ):
1906        """
1907        Return all the latest forms from the form_definition_current table
1908        if all_versions is set to True. Otherwise return all the versions
1909        of all the forms from the form_definition table.
1910        """
1911        if all_versions:
1912            return trans.sa_session.query( trans.app.model.FormDefinition )
1913        if filter:
1914            fdc_list = trans.sa_session.query( trans.app.model.FormDefinitionCurrent ).filter_by( **filter )
1915        else:
1916            fdc_list = trans.sa_session.query( trans.app.model.FormDefinitionCurrent )
1917        if form_type == 'All':
1918            return [ fdc.latest_form for fdc in fdc_list ]
1919        else:
1920            return [ fdc.latest_form for fdc in fdc_list if fdc.latest_form.type == form_type ]
1921
1922    def get_all_forms_by_type( self, trans, cntrller, form_type ):
1923        forms = self.get_all_forms( trans,
1924                                    filter=dict( deleted=False ),
1925                                    form_type=form_type )
1926        if not forms:
1927            message = "There are no forms on which to base the template, so create a form and then add the template."
1928            return trans.response.send_redirect( web.url_for( controller='forms',
1929                                                              action='create_form_definition',
1930                                                              cntrller=cntrller,
1931                                                              message=message,
1932                                                              status='done',
1933                                                              form_type=form_type ) )
1934        return forms
1935
1936    @web.expose
1937    def add_template( self, trans, cntrller, item_type, form_type, **kwd ):
1938        params = util.Params( kwd )
1939        form_id = params.get( 'form_id', 'none' )
1940        message = util.restore_text( params.get( 'message', ''  ) )
1941        action = ''
1942        status = params.get( 'status', 'done' )
1943        forms = self.get_all_forms_by_type( trans, cntrller, form_type )
1944        # form_type must be one of: RUN_DETAILS_TEMPLATE, LIBRARY_INFO_TEMPLATE
1945        in_library = form_type == trans.model.FormDefinition.types.LIBRARY_INFO_TEMPLATE
1946        in_sample_tracking = form_type == trans.model.FormDefinition.types.RUN_DETAILS_TEMPLATE
1947        if in_library:
1948            show_deleted = util.string_as_bool( params.get( 'show_deleted', False ) )
1949            use_panels = util.string_as_bool( params.get( 'use_panels', False ) )
1950            library_id = params.get( 'library_id', None )
1951            folder_id = params.get( 'folder_id', None )
1952            ldda_id = params.get( 'ldda_id', None )
1953            is_admin = trans.user_is_admin() and cntrller in [ 'library_admin', 'requests_admin' ]
1954            current_user_roles = trans.get_current_user_roles()
1955        elif in_sample_tracking:
1956            request_type_id = params.get( 'request_type_id', None )
1957            sample_id = params.get( 'sample_id', None )
1958        try:
1959            if in_sample_tracking:
1960                item, item_desc, action, id = self.get_item_and_stuff( trans,
1961                                                                       item_type=item_type,
1962                                                                       request_type_id=request_type_id,
1963                                                                       sample_id=sample_id )
1964            elif in_library:
1965                item, item_desc, action, id = self.get_item_and_stuff( trans,
1966                                                                       item_type=item_type,
1967                                                                       library_id=library_id,
1968                                                                       folder_id=folder_id,
1969                                                                       ldda_id=ldda_id,
1970                                                                       is_admin=is_admin )
1971            if not item:
1972                message = "Invalid %s id ( %s ) specified." % ( item_desc, str( id ) )
1973                if in_sample_tracking:
1974                    return trans.response.send_redirect( web.url_for( controller='request_type',
1975                                                                      action='browse_request_types',
1976                                                                      id=request_type_id,
1977                                                                      message=util.sanitize_text( message ),
1978                                                                      status='error' ) )
1979                if in_library:
1980                    return trans.response.send_redirect( web.url_for( controller='library_common',
1981                                                                      action='browse_library',
1982                                                                      cntrller=cntrller,
1983                                                                      id=library_id,
1984                                                                      show_deleted=show_deleted,
1985                                                                      message=util.sanitize_text( message ),
1986                                                                      status='error' ) )
1987        except ValueError:
1988            # At this point, the client has already redirected, so this is just here to prevent the unnecessary traceback
1989            return None
1990        if in_library:
1991            # Make sure the user is authorized to do what they are trying to do.
1992            authorized = True
1993            if not ( is_admin or trans.app.security_agent.can_modify_library_item( current_user_roles, item ) ):
1994                authorized = False
1995                unauthorized = 'modify'
1996            if not ( is_admin or trans.app.security_agent.can_access_library_item( current_user_roles, item, trans.user ) ):
1997                authorized = False
1998                unauthorized = 'access'
1999            if not authorized:
2000                message = "You are not authorized to %s %s '%s'." % ( unauthorized, item_desc, item.name )
2001                return trans.response.send_redirect( web.url_for( controller='library_common',
2002                                                                  action='browse_library',
2003                                                                  cntrller=cntrller,
2004                                                                  id=library_id,
2005                                                                  show_deleted=show_deleted,
2006                                                                  message=util.sanitize_text( message ),
2007                                                                  status='error' ) )
2008            # If the inheritable checkbox is checked, the param will be in the request
2009            inheritable = CheckboxField.is_checked( params.get( 'inheritable', '' ) )
2010        if params.get( 'add_template_button', False ):
2011            if form_id not in [ None, 'None', 'none' ]:
2012                form = trans.sa_session.query( trans.app.model.FormDefinition ).get( trans.security.decode_id( form_id ) )
2013                form_values = trans.app.model.FormValues( form, {} )
2014                trans.sa_session.add( form_values )
2015                trans.sa_session.flush()
2016                if item_type == 'library':
2017                    assoc = trans.model.LibraryInfoAssociation( item, form, form_values, inheritable=inheritable )
2018                elif item_type == 'folder':
2019                    assoc = trans.model.LibraryFolderInfoAssociation( item, form, form_values, inheritable=inheritable )
2020                elif item_type == 'ldda':
2021                    assoc = trans.model.LibraryDatasetDatasetInfoAssociation( item, form, form_values )
2022                elif item_type in [ 'request_type', 'sample' ]:
2023                    run = trans.model.Run( form, form_values )
2024                    trans.sa_session.add( run )
2025                    trans.sa_session.flush()
2026                    if item_type == 'request_type':
2027                        # Delete current RequestTypeRunAssociation, if one exists.
2028                        rtra = item.run_details
2029                        if rtra:
2030                            trans.sa_session.delete( rtra )
2031                            trans.sa_session.flush()
2032                        # Add the new RequestTypeRunAssociation.  Templates associated with a RequestType
2033                        # are automatically inherited to the samples.
2034                        assoc = trans.model.RequestTypeRunAssociation( item, run )
2035                    elif item_type == 'sample':
2036                        assoc = trans.model.SampleRunAssociation( item, run )
2037                trans.sa_session.add( assoc )
2038                trans.sa_session.flush()
2039                message = 'A template based on the form "%s" has been added to this %s.' % ( form.name, item_desc )
2040                new_kwd = dict( action=action,
2041                                cntrller=cntrller,
2042                                message=util.sanitize_text( message ),
2043                                status='done' )
2044                if in_sample_tracking:
2045                    new_kwd.update( dict( controller='request_type',
2046                                          request_type_id=request_type_id,
2047                                          sample_id=sample_id,
2048                                          id=id ) )
2049                    return trans.response.send_redirect( web.url_for( **new_kwd ) )
2050                elif in_library:
2051                    new_kwd.update( dict( controller='library_common',
2052                                          use_panels=use_panels,
2053                                          library_id=library_id,
2054                                          folder_id=folder_id,
2055                                          id=id,
2056                                          show_deleted=show_deleted ) )
2057                    return trans.response.send_redirect( web.url_for( **new_kwd ) )
2058            else:
2059                message = "Select a form on which to base the template."
2060                status = "error"
2061        form_id_select_field = self.build_form_id_select_field( trans, forms, selected_value=kwd.get( 'form_id', 'none' ) )
2062        try:
2063            decoded_form_id = trans.security.decode_id( form_id )
2064        except:
2065            decoded_form_id = None
2066        if decoded_form_id:
2067            for form in forms:
2068                if decoded_form_id == form.id:
2069                    widgets = form.get_widgets( trans.user )
2070                    break
2071        else:
2072            widgets = []
2073        new_kwd = dict( cntrller=cntrller,
2074                        item_name=item.name,
2075                        item_desc=item_desc,
2076                        item_type=item_type,
2077                        form_type=form_type,
2078                        widgets=widgets,
2079                        form_id_select_field=form_id_select_field,
2080                        message=message,
2081                        status=status )
2082        if in_sample_tracking:
2083            new_kwd.update( dict( request_type_id=request_type_id,
2084                                  sample_id=sample_id ) )
2085        elif in_library:
2086            new_kwd.update( dict( use_panels=use_panels,
2087                                  library_id=library_id,
2088                                  folder_id=folder_id,
2089                                  ldda_id=ldda_id,
2090                                  inheritable_checked=inheritable,
2091                                  show_deleted=show_deleted ) )
2092        return trans.fill_template( '/common/select_template.mako',
2093                                    **new_kwd )
2094
2095    @web.expose
2096    def edit_template( self, trans, cntrller, item_type, form_type, **kwd ):
2097        # Edit the template itself, keeping existing field contents, if any.
2098        params = util.Params( kwd )
2099        message = util.restore_text( params.get( 'message', ''  ) )
2100        edited = util.string_as_bool( params.get( 'edited', False ) )
2101        action = ''
2102        # form_type must be one of: RUN_DETAILS_TEMPLATE, LIBRARY_INFO_TEMPLATE
2103        in_library = form_type == trans.model.FormDefinition.types.LIBRARY_INFO_TEMPLATE
2104        in_sample_tracking = form_type == trans.model.FormDefinition.types.RUN_DETAILS_TEMPLATE
2105        if in_library:
2106            show_deleted = util.string_as_bool( params.get( 'show_deleted', False ) )
2107            use_panels = util.string_as_bool( params.get( 'use_panels', False ) )
2108            library_id = params.get( 'library_id', None )
2109            folder_id = params.get( 'folder_id', None )
2110            ldda_id = params.get( 'ldda_id', None )
2111            is_admin = trans.user_is_admin() and cntrller in [ 'library_admin', 'requests_admin' ]
2112            current_user_roles = trans.get_current_user_roles()
2113        elif in_sample_tracking:
2114            request_type_id = params.get( 'request_type_id', None )
2115            sample_id = params.get( 'sample_id', None )
2116        try:
2117            if in_library:
2118                item, item_desc, action, id = self.get_item_and_stuff( trans,
2119                                                                       item_type=item_type,
2120                                                                       library_id=library_id,
2121                                                                       folder_id=folder_id,
2122                                                                       ldda_id=ldda_id,
2123                                                                       is_admin=is_admin )
2124            elif in_sample_tracking:
2125                item, item_desc, action, id = self.get_item_and_stuff( trans,
2126                                                                       item_type=item_type,
2127                                                                       request_type_id=request_type_id,
2128                                                                       sample_id=sample_id )
2129        except ValueError:
2130            return None
2131        if in_library:
2132            if not ( is_admin or trans.app.security_agent.can_modify_library_item( current_user_roles, item ) ):
2133                message = "You are not authorized to modify %s '%s'." % ( item_desc, item.name )
2134                return trans.response.send_redirect( web.url_for( controller='library_common',
2135                                                                  action='browse_library',
2136                                                                  cntrller=cntrller,
2137                                                                  id=library_id,
2138                                                                  show_deleted=show_deleted,
2139                                                                  message=util.sanitize_text( message ),
2140                                                                  status='error' ) )
2141        # An info_association must exist at this point
2142        if in_library:
2143            info_association, inherited = item.get_info_association( restrict=True )
2144        elif in_sample_tracking:
2145            # Here run_details is a RequestTypeRunAssociation
2146            rtra = item.run_details
2147            info_association = rtra.run
2148        template = info_association.template
2149        if edited:
2150            # The form on which the template is based has been edited, so we need to update the
2151            # info_association with the current form
2152            fdc = trans.sa_session.query( trans.app.model.FormDefinitionCurrent ).get( template.form_definition_current_id )
2153            info_association.template = fdc.latest_form
2154            trans.sa_session.add( info_association )
2155            trans.sa_session.flush()
2156            message = "The template for this %s has been updated with your changes." % item_desc
2157            new_kwd = dict( action=action,
2158                            cntrller=cntrller,
2159                            id=id,
2160                            message=util.sanitize_text( message ),
2161                            status='done' )
2162            if in_library:
2163                new_kwd.update( dict( controller='library_common',
2164                                      use_panels=use_panels,
2165                                      library_id=library_id,
2166                                      folder_id=folder_id,
2167                                      show_deleted=show_deleted ) )
2168                return trans.response.send_redirect( web.url_for( **new_kwd ) )
2169            elif in_sample_tracking:
2170                new_kwd.update( dict( controller='request_type',
2171                                      request_type_id=request_type_id,
2172                                      sample_id=sample_id ) )
2173                return trans.response.send_redirect( web.url_for( **new_kwd ) )
2174        # "template" is a FormDefinition, so since we're changing it, we need to use the latest version of it.
2175        vars = dict( id=trans.security.encode_id( template.form_definition_current_id ),
2176                     response_redirect=web.url_for( controller='request_type',
2177                                                    action='edit_template',
2178                                                    cntrller=cntrller,
2179                                                    item_type=item_type,
2180                                                    form_type=form_type,
2181                                                    edited=True,
2182                                                    **kwd ) )
2183        return trans.response.send_redirect( web.url_for( controller='forms', action='edit_form_definition', **vars ) )
2184
2185    @web.expose
2186    def edit_template_info( self, trans, cntrller, item_type, form_type, **kwd ):
2187        # Edit the contents of the template fields without altering the template itself.
2188        params = util.Params( kwd )
2189        # form_type must be one of: RUN_DETAILS_TEMPLATE, LIBRARY_INFO_TEMPLATE
2190        in_library = form_type == trans.model.FormDefinition.types.LIBRARY_INFO_TEMPLATE
2191        in_sample_tracking = form_type == trans.model.FormDefinition.types.RUN_DETAILS_TEMPLATE
2192        if in_library:
2193            library_id = params.get( 'library_id', None )
2194            folder_id = params.get( 'folder_id', None )
2195            ldda_id = params.get( 'ldda_id', None )
2196            show_deleted = util.string_as_bool( params.get( 'show_deleted', False ) )
2197            use_panels = util.string_as_bool( params.get( 'use_panels', False ) )
2198            is_admin = ( trans.user_is_admin() and cntrller == 'library_admin' )
2199            current_user_roles = trans.get_current_user_roles()
2200        elif in_sample_tracking:
2201            request_type_id = params.get( 'request_type_id', None )
2202            sample_id = params.get( 'sample_id', None )
2203            sample = trans.sa_session.query( trans.model.Sample ).get( trans.security.decode_id( sample_id ) )
2204        message = util.restore_text( params.get( 'message', ''  ) )
2205        try:
2206            if in_library:
2207                item, item_desc, action, id = self.get_item_and_stuff( trans,
2208                                                                       item_type=item_type,
2209                                                                       library_id=library_id,
2210                                                                       folder_id=folder_id,
2211                                                                       ldda_id=ldda_id,
2212                                                                       is_admin=is_admin )
2213            elif in_sample_tracking:
2214                item, item_desc, action, id = self.get_item_and_stuff( trans,
2215                                                                       item_type=item_type,
2216                                                                       request_type_id=request_type_id,
2217                                                                       sample_id=sample_id )
2218        except ValueError:
2219            if cntrller == 'api':
2220                trans.response.status = 400
2221                return None
2222            return None
2223        if in_library:
2224            if not ( is_admin or trans.app.security_agent.can_modify_library_item( current_user_roles, item ) ):
2225                message = "You are not authorized to modify %s '%s'." % ( item_desc, item.name )
2226                if cntrller == 'api':
2227                    trans.response.status = 400
2228                    return message
2229                return trans.response.send_redirect( web.url_for( controller='library_common',
2230                                                                  action='browse_library',
2231                                                                  cntrller=cntrller,
2232                                                                  id=library_id,
2233                                                                  show_deleted=show_deleted,
2234                                                                  message=util.sanitize_text( message ),
2235                                                                  status='error' ) )
2236        # We need the type of each template field widget
2237        widgets = item.get_template_widgets( trans )
2238        # The list of widgets may include an AddressField which we need to save if it is new
2239        for index, widget_dict in enumerate( widgets ):
2240            widget = widget_dict[ 'widget' ]
2241            if isinstance( widget, AddressField ):
2242                value = util.restore_text( params.get( widget.name, '' ) )
2243                if value == 'new':
2244                    if params.get( 'edit_info_button', False ):
2245                        if self.field_param_values_ok( widget.name, 'AddressField', **kwd ):
2246                            # Save the new address
2247                            address = trans.app.model.UserAddress( user=trans.user )
2248                            self.save_widget_field( trans, address, widget.name, **kwd )
2249                            widget.value = str( address.id )
2250                        else:
2251                            message = 'Required fields are missing contents.'
2252                            if cntrller == 'api':
2253                                trans.response.status = 400
2254                                return message
2255                            new_kwd = dict( action=action,
2256                                            id=id,
2257                                            message=util.sanitize_text( message ),
2258                                            status='error' )
2259                            if in_library:
2260                                new_kwd.update( dict( controller='library_common',
2261                                                      cntrller=cntrller,
2262                                                      use_panels=use_panels,
2263                                                      library_id=library_id,
2264                                                      folder_id=folder_id,
2265                                                      show_deleted=show_deleted ) )
2266                                return trans.response.send_redirect( web.url_for( **new_kwd ) )
2267                            if in_sample_tracking:
2268                                new_kwd.update( dict( controller='request_type',
2269                                                      request_type_id=request_type_id,
2270                                                      sample_id=sample_id ) )
2271                                return trans.response.send_redirect( web.url_for( **new_kwd ) )
2272                    else:
2273                        # Form was submitted via refresh_on_change
2274                        widget.value = 'new'
2275                elif value == unicode( 'none' ):
2276                    widget.value = ''
2277                else:
2278                    widget.value = value
2279            elif isinstance( widget, CheckboxField ):
2280                # We need to check the value from kwd since util.Params would have munged the list if
2281                # the checkbox is checked.
2282                value = kwd.get( widget.name, '' )
2283                if CheckboxField.is_checked( value ):
2284                    widget.value = 'true'
2285            else:
2286                widget.value = util.restore_text( params.get( widget.name, '' ) )
2287        # Save updated template field contents
2288        field_contents = self.clean_field_contents( widgets, **kwd )
2289        if field_contents:
2290            if in_library:
2291                # In in a library, since information templates are inherited, the template fields can be displayed
2292                # on the information page for a folder or ldda when it has no info_association object.  If the user
2293                #  has added field contents on an inherited template via a parent's info_association, we'll need to
2294                # create a new form_values and info_association for the current object.  The value for the returned
2295                # inherited variable is not applicable at this level.
2296                info_association, inherited = item.get_info_association( restrict=True )
2297            elif in_sample_tracking:
2298                assoc = item.run_details
2299                if item_type == 'request_type' and assoc:
2300                    # If we're dealing with a RequestType, assoc will be a ReuqestTypeRunAssociation.
2301                    info_association = assoc.run
2302                elif item_type == 'sample' and assoc:
2303                    # If we're dealing with a Sample, assoc will be a SampleRunAssociation if the
2304                    # Sample has one.  If the Sample does not have a SampleRunAssociation, assoc will
2305                    # be the Sample's RequestType RequestTypeRunAssociation, in which case we need to
2306                    # create a SampleRunAssociation using the inherited template from the RequestType.
2307                    if isinstance( assoc, trans.model.RequestTypeRunAssociation ):
2308                        form_definition = assoc.run.template
2309                        new_form_values = trans.model.FormValues( form_definition, {} )
2310                        trans.sa_session.add( new_form_values )
2311                        trans.sa_session.flush()
2312                        new_run = trans.model.Run( form_definition, new_form_values )
2313                        trans.sa_session.add( new_run )
2314                        trans.sa_session.flush()
2315                        sra = trans.model.SampleRunAssociation( item, new_run )
2316                        trans.sa_session.add( sra )
2317                        trans.sa_session.flush()
2318                        info_association = sra.run
2319                    else:
2320                       info_association = assoc.run
2321                else:
2322                    info_association = None
2323            if info_association:
2324                template = info_association.template
2325                info = info_association.info
2326                form_values = trans.sa_session.query( trans.app.model.FormValues ).get( info.id )
2327                # Update existing content only if it has changed
2328                flush_required = False
2329                for field_contents_key, field_contents_value in field_contents.items():
2330                    if field_contents_key in form_values.content:
2331                        if form_values.content[ field_contents_key ] != field_contents_value:
2332                            flush_required = True
2333                            form_values.content[ field_contents_key ] = field_contents_value
2334                    else:
2335                        flush_required = True
2336                        form_values.content[ field_contents_key ] = field_contents_value
2337                if flush_required:
2338                    trans.sa_session.add( form_values )
2339                    trans.sa_session.flush()
2340            else:
2341                if in_library:
2342                    # Inherit the next available info_association so we can get the template
2343                    info_association, inherited = item.get_info_association()
2344                    template = info_association.template
2345                    # Create a new FormValues object
2346                    form_values = trans.app.model.FormValues( template, field_contents )
2347                    trans.sa_session.add( form_values )
2348                    trans.sa_session.flush()
2349                    # Create a new info_association between the current library item and form_values
2350                    if item_type == 'folder':
2351                        # A LibraryFolder is a special case because if it inherited the template from it's parent,
2352                        # we want to set inheritable to True for it's info_association.  This allows for the default
2353                        # inheritance to be False for each level in the Library hierarchy unless we're creating a new
2354                        # level in the hierarchy, in which case we'll inherit the "inheritable" setting from the parent
2355                        # level.
2356                        info_association = trans.app.model.LibraryFolderInfoAssociation( item, template, form_values, inheritable=inherited )
2357                        trans.sa_session.add( info_association )
2358                        trans.sa_session.flush()
2359                    elif item_type == 'ldda':
2360                        info_association = trans.app.model.LibraryDatasetDatasetInfoAssociation( item, template, form_values )
2361                        trans.sa_session.add( info_association )
2362                        trans.sa_session.flush()
2363        message = 'The information has been updated.'
2364        if cntrller == 'api':
2365            return 200, message
2366        new_kwd = dict( action=action,
2367                        cntrller=cntrller,
2368                        id=id,
2369                        message=util.sanitize_text( message ),
2370                        status='done' )
2371        if in_library:
2372            new_kwd.update( dict( controller='library_common',
2373                                  use_panels=use_panels,
2374                                  library_id=library_id,
2375                                  folder_id=folder_id,
2376                                  show_deleted=show_deleted ) )
2377        if in_sample_tracking:
2378            new_kwd.update( dict( controller='requests_common',
2379                                  cntrller='requests_admin',
2380                                  id=trans.security.encode_id( sample.id ),
2381                                  sample_id=sample_id ) )
2382        return trans.response.send_redirect( web.url_for( **new_kwd ) )
2383
2384    @web.expose
2385    def delete_template( self, trans, cntrller, item_type, form_type, **kwd ):
2386        params = util.Params( kwd )
2387        # form_type must be one of: RUN_DETAILS_TEMPLATE, LIBRARY_INFO_TEMPLATE
2388        in_library = form_type == trans.model.FormDefinition.types.LIBRARY_INFO_TEMPLATE
2389        in_sample_tracking = form_type == trans.model.FormDefinition.types.RUN_DETAILS_TEMPLATE
2390        if in_library:
2391            is_admin = ( trans.user_is_admin() and cntrller == 'library_admin' )
2392            current_user_roles = trans.get_current_user_roles()
2393            show_deleted = util.string_as_bool( params.get( 'show_deleted', False ) )
2394            use_panels = util.string_as_bool( params.get( 'use_panels', False ) )
2395            library_id = params.get( 'library_id', None )
2396            folder_id = params.get( 'folder_id', None )
2397            ldda_id = params.get( 'ldda_id', None )
2398        elif in_sample_tracking:
2399            request_type_id = params.get( 'request_type_id', None )
2400            sample_id = params.get( 'sample_id', None )
2401        #id = params.get( 'id', None )
2402        message = util.restore_text( params.get( 'message', ''  ) )
2403        try:
2404            if in_library:
2405                item, item_desc, action, id = self.get_item_and_stuff( trans,
2406                                                                       item_type=item_type,
2407                                                                       library_id=library_id,
2408                                                                       folder_id=folder_id,
2409                                                                       ldda_id=ldda_id,
2410                                                                       is_admin=is_admin )
2411            elif in_sample_tracking:
2412                item, item_desc, action, id = self.get_item_and_stuff( trans,
2413                                                                       item_type=item_type,
2414                                                                       request_type_id=request_type_id,
2415                                                                       sample_id=sample_id )
2416        except ValueError:
2417            return None
2418        if in_library:
2419            if not ( is_admin or trans.app.security_agent.can_modify_library_item( current_user_roles, item ) ):
2420                message = "You are not authorized to modify %s '%s'." % ( item_desc, item.name )
2421                return trans.response.send_redirect( web.url_for( controller='library_common',
2422                                                                  action='browse_library',
2423                                                                  cntrller=cntrller,
2424                                                                  id=library_id,
2425                                                                  show_deleted=show_deleted,
2426                                                                  message=util.sanitize_text( message ),
2427                                                                  status='error' ) )
2428        if in_library:
2429            info_association, inherited = item.get_info_association()
2430        elif in_sample_tracking:
2431            info_association = item.run_details
2432        if not info_association:
2433            message = "There is no template for this %s" % item_type
2434        else:
2435            if in_library:
2436                info_association.deleted = True
2437                trans.sa_session.add( info_association )
2438                trans.sa_session.flush()
2439            elif in_sample_tracking:
2440                trans.sa_session.delete( info_association )
2441                trans.sa_session.flush()
2442            message = 'The template for this %s has been deleted.' % item_type
2443        new_kwd = dict( action=action,
2444                        cntrller=cntrller,
2445                        id=id,
2446                        message=util.sanitize_text( message ),
2447                        status='done' )
2448        if in_library:
2449            new_kwd.update( dict( controller='library_common',
2450                                  use_panels=use_panels,
2451                                  library_id=library_id,
2452                                  folder_id=folder_id,
2453                                  show_deleted=show_deleted ) )
2454            return trans.response.send_redirect( web.url_for( **new_kwd ) )
2455        if in_sample_tracking:
2456            new_kwd.update( dict( controller='request_type',
2457                                  request_type_id=request_type_id,
2458                                  sample_id=sample_id ) )
2459            return trans.response.send_redirect( web.url_for( **new_kwd ) )
2460
2461    def widget_fields_have_contents( self, widgets ):
2462        # Return True if any of the fields in widgets contain contents, widgets is a list of dictionaries that looks something like:
2463        # [{'widget': <galaxy.web.form_builder.TextField object at 0x10867aa10>, 'helptext': 'Field 0 help (Optional)', 'label': 'Field 0'}]
2464        for i, field in enumerate( widgets ):
2465            if ( isinstance( field[ 'widget' ], TextArea ) or isinstance( field[ 'widget' ], TextField ) ) and field[ 'widget' ].value:
2466                return True
2467            if isinstance( field[ 'widget' ], SelectField ) and field[ 'widget' ].options:
2468                for option_label, option_value, selected in field[ 'widget' ].options:
2469                    if selected:
2470                        return True
2471            if isinstance( field[ 'widget' ], CheckboxField ) and field[ 'widget' ].checked:
2472                return True
2473            if isinstance( field[ 'widget' ], WorkflowField ) and str( field[ 'widget' ].value ).lower() not in [ 'none' ]:
2474                return True
2475            if isinstance( field[ 'widget' ], WorkflowMappingField ) and str( field[ 'widget' ].value ).lower() not in [ 'none' ]:
2476                return True
2477            if isinstance( field[ 'widget' ], HistoryField ) and str( field[ 'widget' ].value ).lower() not in [ 'none' ]:
2478                return True
2479            if isinstance( field[ 'widget' ], AddressField ) and str( field[ 'widget' ].value ).lower() not in [ 'none' ]:
2480                return True
2481        return False
2482
2483    def clean_field_contents( self, widgets, **kwd ):
2484        field_contents = {}
2485        for index, widget_dict in enumerate( widgets ):
2486            widget = widget_dict[ 'widget' ]
2487            value = kwd.get( widget.name, ''  )
2488            if isinstance( widget, CheckboxField ):
2489                # CheckboxField values are lists if the checkbox is checked
2490                value = str( widget.is_checked( value ) ).lower()
2491            elif isinstance( widget, AddressField ):
2492                # If the address was new, is has already been saved and widget.value is the new address.id
2493                value = widget.value
2494            field_contents[ widget.name ] = util.restore_text( value )
2495        return field_contents
2496
2497    def field_param_values_ok( self, widget_name, widget_type, **kwd ):
2498        # Make sure required fields have contents, etc
2499        params = util.Params( kwd )
2500        if widget_type == 'AddressField':
2501            if not util.restore_text( params.get( '%s_short_desc' % widget_name, '' ) ) \
2502                or not util.restore_text( params.get( '%s_name' % widget_name, '' ) ) \
2503                or not util.restore_text( params.get( '%s_institution' % widget_name, '' ) ) \
2504                or not util.restore_text( params.get( '%s_address' % widget_name, '' ) ) \
2505                or not util.restore_text( params.get( '%s_city' % widget_name, '' ) ) \
2506                or not util.restore_text( params.get( '%s_state' % widget_name, '' ) ) \
2507                or not util.restore_text( params.get( '%s_postal_code' % widget_name, '' ) ) \
2508                or not util.restore_text( params.get( '%s_country' % widget_name, '' ) ):
2509                return False
2510        return True
2511
2512    def save_widget_field( self, trans, field_obj, widget_name, **kwd ):
2513        # Save a form_builder field object
2514        params = util.Params( kwd )
2515        if isinstance( field_obj, trans.model.UserAddress ):
2516            field_obj.desc = util.restore_text( params.get( '%s_short_desc' % widget_name, '' ) )
2517            field_obj.name = util.restore_text( params.get( '%s_name' % widget_name, '' ) )
2518            field_obj.institution = util.restore_text( params.get( '%s_institution' % widget_name, '' ) )
2519            field_obj.address = util.restore_text( params.get( '%s_address' % widget_name, '' ) )
2520            field_obj.city = util.restore_text( params.get( '%s_city' % widget_name, '' ) )
2521            field_obj.state = util.restore_text( params.get( '%s_state' % widget_name, '' ) )
2522            field_obj.postal_code = util.restore_text( params.get( '%s_postal_code' % widget_name, '' ) )
2523            field_obj.country = util.restore_text( params.get( '%s_country' % widget_name, '' ) )
2524            field_obj.phone = util.restore_text( params.get( '%s_phone' % widget_name, '' ) )
2525            trans.sa_session.add( field_obj )
2526            trans.sa_session.flush()
2527
2528    def get_form_values( self, trans, user, form_definition, **kwd ):
2529        '''
2530        Returns the name:value dictionary containing all the form values
2531        '''
2532        params = util.Params( kwd )
2533        values = {}
2534        for index, field in enumerate( form_definition.fields ):
2535            field_type = field[ 'type' ]
2536            field_name = field[ 'name' ]
2537            input_value = params.get( field_name, '' )
2538            if field_type == AddressField.__name__:
2539                input_text_value = util.restore_text( input_value )
2540                if input_text_value == 'new':
2541                    # Save this new address in the list of this user's addresses
2542                    user_address = trans.model.UserAddress( user=user )
2543                    self.save_widget_field( trans, user_address, field_name, **kwd )
2544                    trans.sa_session.refresh( user )
2545                    field_value = int( user_address.id )
2546                elif input_text_value in [ '', 'none', 'None', None ]:
2547                    field_value = ''
2548                else:
2549                    field_value = int( input_text_value )
2550            elif field_type == CheckboxField.__name__:
2551                field_value = CheckboxField.is_checked( input_value )
2552            elif field_type == PasswordField.__name__:
2553                field_value = kwd.get( field_name, '' )
2554            else:
2555                field_value = util.restore_text( input_value )
2556            values[ field_name ] = field_value
2557        return values
2558
2559    def populate_widgets_from_kwd( self, trans, widgets, **kwd ):
2560        # A form submitted via refresh_on_change requires us to populate the widgets with the contents of
2561        # the form fields the user may have entered so that when the form refreshes the contents are retained.
2562        params = util.Params( kwd )
2563        populated_widgets = []
2564        for widget_dict in widgets:
2565            widget = widget_dict[ 'widget' ]
2566            if params.get( widget.name, False ):
2567                # The form included a field whose contents should be used to set the
2568                # value of the current widget (widget.name is the name set by the
2569                # user when they defined the FormDefinition).
2570                if isinstance( widget, AddressField ):
2571                    value = util.restore_text( params.get( widget.name, '' ) )
2572                    if value == 'none':
2573                        value = ''
2574                    widget.value = value
2575                    widget_dict[ 'widget' ] = widget
2576                    # Populate the AddressField params with the form field contents
2577                    widget_params_dict = {}
2578                    for field_name, label, help_text in widget.fields():
2579                        form_param_name = '%s_%s' % ( widget.name, field_name )
2580                        widget_params_dict[ form_param_name ] = util.restore_text( params.get( form_param_name, '' ) )
2581                    widget.params = widget_params_dict
2582                elif isinstance( widget, CheckboxField ):
2583                    # Check the value from kwd since util.Params would have
2584                    # stringify'd the list if the checkbox is checked.
2585                    value = kwd.get( widget.name, '' )
2586                    if CheckboxField.is_checked( value ):
2587                        widget.value = 'true'
2588                        widget_dict[ 'widget' ] = widget
2589                elif isinstance( widget, SelectField ):
2590                    # Ensure the selected option remains selected.
2591                    value = util.restore_text( params.get( widget.name, '' ) )
2592                    processed_options = []
2593                    for option_label, option_value, option_selected in widget.options:
2594                        selected = value == option_value
2595                        processed_options.append( ( option_label, option_value, selected ) )
2596                    widget.options = processed_options
2597                else:
2598                    widget.value = util.restore_text( params.get( widget.name, '' ) )
2599                    widget_dict[ 'widget' ] = widget
2600            populated_widgets.append( widget_dict )
2601        return populated_widgets
2602
2603    def get_item_and_stuff( self, trans, item_type, **kwd ):
2604        # Return an item, description, action and an id based on the item_type.  Valid item_types are
2605        # library, folder, ldda, request_type, sample.
2606        if item_type == 'library':
2607            library_id = kwd.get( 'library_id', None )
2608            id = library_id
2609            try:
2610                item = trans.sa_session.query( trans.app.model.Library ).get( trans.security.decode_id( library_id ) )
2611            except:
2612                item = None
2613            item_desc = 'data library'
2614            action = 'library_info'
2615        elif item_type == 'folder':
2616            folder_id = kwd.get( 'folder_id', None )
2617            id = folder_id
2618            try:
2619                item = trans.sa_session.query( trans.app.model.LibraryFolder ).get( trans.security.decode_id( folder_id ) )
2620            except:
2621                item = None
2622            item_desc = 'folder'
2623            action = 'folder_info'
2624        elif item_type == 'ldda':
2625            ldda_id = kwd.get( 'ldda_id', None )
2626            id = ldda_id
2627            try:
2628                item = trans.sa_session.query( trans.app.model.LibraryDatasetDatasetAssociation ).get( trans.security.decode_id( ldda_id ) )
2629            except:
2630                item = None
2631            item_desc = 'dataset'
2632            action = 'ldda_edit_info'
2633        elif item_type == 'request_type':
2634            request_type_id = kwd.get( 'request_type_id', None )
2635            id = request_type_id
2636            try:
2637                item = trans.sa_session.query( trans.app.model.RequestType ).get( trans.security.decode_id( request_type_id ) )
2638            except:
2639                item = None
2640            item_desc = 'request type'
2641            action = 'view_editable_request_type'
2642        elif item_type == 'sample':
2643            sample_id = kwd.get( 'sample_id', None )
2644            id = sample_id
2645            try:
2646                item = trans.sa_session.query( trans.app.model.Sample ).get( trans.security.decode_id( sample_id ) )
2647            except:
2648                item = None
2649            item_desc = 'sample'
2650            action = 'view_sample'
2651        else:
2652            item = None
2653            #message = "Invalid item type ( %s )" % str( item_type )
2654            item_desc = None
2655            action = None
2656            id = None
2657        return item, item_desc, action, id
2658
2659    def build_form_id_select_field( self, trans, forms, selected_value='none' ):
2660        return build_select_field( trans,
2661                                   objs=forms,
2662                                   label_attr='name',
2663                                   select_field_name='form_id',
2664                                   selected_value=selected_value,
2665                                   refresh_on_change=True )
2666
2667
2668class SharableMixin:
2669    """ Mixin for a controller that manages an item that can be shared. """
2670
2671    # -- Implemented methods. --
2672
2673    def _is_valid_slug( self, slug ):
2674        """ Returns true if slug is valid. """
2675        return _is_valid_slug( slug )
2676
2677    @web.expose
2678    @web.require_login( "share Galaxy items" )
2679    def set_public_username( self, trans, id, username, **kwargs ):
2680        """ Set user's public username and delegate to sharing() """
2681        user = trans.get_user()
2682        message = validate_publicname( trans, username, user )
2683        if message:
2684            return trans.fill_template( '/sharing_base.mako', item=self.get_item( trans, id ), message=message, status='error' )
2685        user.username = username
2686        trans.sa_session.flush
2687        return self.sharing( trans, id, **kwargs )
2688
2689    @web.expose
2690    @web.require_login( "modify Galaxy items" )
2691    def set_slug_async( self, trans, id, new_slug ):
2692        item = self.get_item( trans, id )
2693        if item:
2694            # Only update slug if slug is not already in use.
2695            if trans.sa_session.query( item.__class__ ).filter_by( user=item.user, slug=new_slug, importable=True ).count() == 0:
2696                item.slug = new_slug
2697                trans.sa_session.flush()
2698
2699        return item.slug
2700
2701    def _make_item_accessible( self, sa_session, item ):
2702        """ Makes item accessible--viewable and importable--and sets item's slug.
2703            Does not flush/commit changes, however. Item must have name, user,
2704            importable, and slug attributes. """
2705        item.importable = True
2706        self.create_item_slug( sa_session, item )
2707
2708    def create_item_slug( self, sa_session, item ):
2709        """ Create/set item slug. Slug is unique among user's importable items
2710            for item's class. Returns true if item's slug was set/changed; false
2711            otherwise.
2712        """
2713        cur_slug = item.slug
2714
2715        # Setup slug base.
2716        if cur_slug is None or cur_slug == "":
2717            # Item can have either a name or a title.
2718            if hasattr( item, 'name' ):
2719                item_name = item.name
2720            elif hasattr( item, 'title' ):
2721                item_name = item.title
2722            slug_base = util.ready_name_for_url( item_name.lower() )
2723        else:
2724            slug_base = cur_slug
2725
2726        # Using slug base, find a slug that is not taken. If slug is taken,
2727        # add integer to end.
2728        new_slug = slug_base
2729        count = 1
2730        while sa_session.query( item.__class__ ).filter_by( user=item.user, slug=new_slug, importable=True ).count() != 0:
2731            # Slug taken; choose a new slug based on count. This approach can
2732            # handle numerous items with the same name gracefully.
2733            new_slug = '%s-%i' % ( slug_base, count )
2734            count += 1
2735
2736        # Set slug and return.
2737        item.slug = new_slug
2738        return item.slug == cur_slug
2739
2740    # -- Abstract methods. --
2741
2742    @web.expose
2743    @web.require_login( "share Galaxy items" )
2744    def sharing( self, trans, id, **kwargs ):
2745        """ Handle item sharing. """
2746        raise "Unimplemented Method"
2747
2748    @web.expose
2749    @web.require_login( "share Galaxy items" )
2750    def share( self, trans, id=None, email="", **kwd ):
2751        """ Handle sharing an item with a particular user. """
2752        raise "Unimplemented Method"
2753
2754    @web.expose
2755    def display_by_username_and_slug( self, trans, username, slug ):
2756        """ Display item by username and slug. """
2757        raise "Unimplemented Method"
2758
2759    @web.json
2760    @web.require_login( "get item name and link" )
2761    def get_name_and_link_async( self, trans, id=None ):
2762        """ Returns item's name and link. """
2763        raise "Unimplemented Method"
2764
2765    @web.expose
2766    @web.require_login("get item content asynchronously")
2767    def get_item_content_async( self, trans, id ):
2768        """ Returns item content in HTML format. """
2769        raise "Unimplemented Method"
2770
2771    def get_item( self, trans, id ):
2772        """ Return item based on id. """
2773        raise "Unimplemented Method"
2774
2775
2776class UsesQuotaMixin( object ):
2777
2778    def get_quota( self, trans, id, check_ownership=False, check_accessible=False, deleted=None ):
2779        return self.get_object( trans, id, 'Quota', check_ownership=False, check_accessible=False, deleted=deleted )
2780
2781
2782class UsesTagsMixin( object ):
2783
2784    def get_tag_handler( self, trans ):
2785        return trans.app.tag_handler
2786
2787    def _get_user_tags( self, trans, item_class_name, id ):
2788        user = trans.user
2789        tagged_item = self._get_tagged_item( trans, item_class_name, id )
2790        return [ tag for tag in tagged_item.tags if ( tag.user == user ) ]
2791
2792    def _get_tagged_item( self, trans, item_class_name, id, check_ownership=True ):
2793        tagged_item = self.get_object( trans, id, item_class_name, check_ownership=check_ownership, check_accessible=True )
2794        return tagged_item
2795
2796    def _remove_items_tag( self, trans, item_class_name, id, tag_name ):
2797        """Remove a tag from an item."""
2798        user = trans.user
2799        tagged_item = self._get_tagged_item( trans, item_class_name, id )
2800        deleted = tagged_item and self.get_tag_handler( trans ).remove_item_tag( trans, user, tagged_item, tag_name )
2801        trans.sa_session.flush()
2802        return deleted
2803
2804    def _apply_item_tag( self, trans, item_class_name, id, tag_name, tag_value=None ):
2805        user = trans.user
2806        tagged_item = self._get_tagged_item( trans, item_class_name, id )
2807        tag_assoc = self.get_tag_handler( trans ).apply_item_tag( trans, user, tagged_item, tag_name, tag_value )
2808        trans.sa_session.flush()
2809        return tag_assoc
2810
2811    def _get_item_tag_assoc( self, trans, item_class_name, id, tag_name ):
2812        user = trans.user
2813        tagged_item = self._get_tagged_item( trans, item_class_name, id )
2814        log.debug( "In get_item_tag_assoc with tagged_item %s" % tagged_item )
2815        return self.get_tag_handler( trans )._get_item_tag_assoc( user, tagged_item, tag_name )
2816
2817    def set_tags_from_list( self, trans, item, new_tags_list, user=None ):
2818        #precondition: item is already security checked against user
2819        #precondition: incoming tags is a list of sanitized/formatted strings
2820        user = user or trans.user
2821
2822        # based on controllers/tag retag_async: delete all old, reset to entire new
2823        trans.app.tag_handler.delete_item_tags( trans, user, item )
2824        new_tags_str = ','.join( new_tags_list )
2825        trans.app.tag_handler.apply_item_tags( trans, user, item, unicode( new_tags_str.encode( 'utf-8' ), 'utf-8' ) )
2826        trans.sa_session.flush()
2827        return item.tags
2828
2829    def get_user_tags_used( self, trans, user=None ):
2830        """
2831        Return a list of distinct 'user_tname:user_value' strings that the
2832        given user has used.
2833
2834        user defaults to trans.user.
2835        Returns an empty list if no user is given and trans.user is anonymous.
2836        """
2837        #TODO: for lack of a UsesUserMixin - placing this here - maybe into UsesTags, tho
2838        user = user or trans.user
2839        if not user:
2840            return []
2841
2842        # get all the taggable model TagAssociations
2843        tag_models = [ v.tag_assoc_class for v in trans.app.tag_handler.item_tag_assoc_info.values() ]
2844        # create a union of subqueries for each for this user - getting only the tname and user_value
2845        all_tags_query = None
2846        for tag_model in tag_models:
2847            subq = ( trans.sa_session.query( tag_model.user_tname, tag_model.user_value )
2848                        .filter( tag_model.user == trans.user ) )
2849            all_tags_query = subq if all_tags_query is None else all_tags_query.union( subq )
2850
2851        # if nothing init'd the query, bail
2852        if all_tags_query is None:
2853            return []
2854
2855        # boil the tag tuples down into a sorted list of DISTINCT name:val strings
2856        tags = all_tags_query.distinct().all()
2857        tags = [( ( name + ':' + val ) if val else name ) for name, val in tags ]
2858        return sorted( tags )
2859
2860
2861
2862class UsesExtendedMetadataMixin( SharableItemSecurityMixin ):
2863    """ Mixin for getting and setting item extended metadata. """
2864
2865    def get_item_extended_metadata_obj( self, trans, item ):
2866        """
2867        Given an item object (such as a LibraryDatasetDatasetAssociation), find the object
2868        of the associated extended metadata
2869        """
2870        if item.extended_metadata:
2871            return item.extended_metadata
2872        return None
2873
2874    def set_item_extended_metadata_obj( self, trans, item, extmeta_obj, check_writable=False):
2875        if item.__class__ == LibraryDatasetDatasetAssociation:
2876            if not check_writable or trans.app.security_agent.can_modify_library_item( trans.get_current_user_roles(), item, trans.user ):
2877                item.extended_metadata = extmeta_obj
2878                trans.sa_session.flush()
2879        if item.__class__ == HistoryDatasetAssociation:
2880            history = None
2881            if check_writable:
2882                history = self.security_check( trans, item, check_ownership=True, check_accessible=True )
2883            else:
2884                history = self.security_check( trans, item, check_ownership=False, check_accessible=True )
2885            if history:
2886                item.extended_metadata = extmeta_obj
2887                trans.sa_session.flush()
2888
2889    def unset_item_extended_metadata_obj( self, trans, item, check_writable=False):
2890        if item.__class__ == LibraryDatasetDatasetAssociation:
2891            if not check_writable or trans.app.security_agent.can_modify_library_item( trans.get_current_user_roles(), item, trans.user ):
2892                item.extended_metadata = None
2893                trans.sa_session.flush()
2894        if item.__class__ == HistoryDatasetAssociation:
2895            history = None
2896            if check_writable:
2897                history = self.security_check( trans, item, check_ownership=True, check_accessible=True )
2898            else:
2899                history = self.security_check( trans, item, check_ownership=False, check_accessible=True )
2900            if history:
2901                item.extended_metadata = None
2902                trans.sa_session.flush()
2903
2904    def create_extended_metadata(self, trans, extmeta):
2905        """
2906        Create/index an extended metadata object. The returned object is
2907        not associated with any items
2908        """
2909        ex_meta = ExtendedMetadata(extmeta)
2910        trans.sa_session.add( ex_meta )
2911        trans.sa_session.flush()
2912        for path, value in self._scan_json_block(extmeta):
2913            meta_i = ExtendedMetadataIndex(ex_meta, path, value)
2914            trans.sa_session.add(meta_i)
2915        trans.sa_session.flush()
2916        return ex_meta
2917
2918    def delete_extended_metadata( self, trans, item):
2919        if item.__class__ == ExtendedMetadata:
2920            trans.sa_session.delete( item )
2921            trans.sa_session.flush()
2922
2923    def _scan_json_block(self, meta, prefix=""):
2924        """
2925        Scan a json style data structure, and emit all fields and their values.
2926        Example paths
2927
2928        Data
2929        { "data" : [ 1, 2, 3 ] }
2930
2931        Path:
2932        /data == [1,2,3]
2933
2934        /data/[0] == 1
2935
2936        """
2937        if isinstance(meta, dict):
2938            for a in meta:
2939                for path, value in self._scan_json_block(meta[a], prefix + "/" + a):
2940                    yield path, value
2941        elif isinstance(meta, list):
2942            for i, a in enumerate(meta):
2943                for path, value in self._scan_json_block(a, prefix + "[%d]" % (i)):
2944                    yield path, value
2945        else:
2946            #BUG: Everything is cast to string, which can lead to false positives
2947            #for cross type comparisions, ie "True" == True
2948            yield prefix, ("%s" % (meta)).encode("utf8", errors='replace')
2949
2950
2951"""
2952Deprecated: `BaseController` used to be available under the name `Root`
2953"""
2954class ControllerUnavailable( Exception ):
2955    pass
2956
2957## ---- Utility methods -------------------------------------------------------
2958
2959def sort_by_attr( seq, attr ):
2960    """
2961    Sort the sequence of objects by object's attribute
2962    Arguments:
2963    seq  - the list or any sequence (including immutable one) of objects to sort.
2964    attr - the name of attribute to sort by
2965    """
2966    # Use the "Schwartzian transform"
2967    # Create the auxiliary list of tuples where every i-th tuple has form
2968    # (seq[i].attr, i, seq[i]) and sort it. The second item of tuple is needed not
2969    # only to provide stable sorting, but mainly to eliminate comparison of objects
2970    # (which can be expensive or prohibited) in case of equal attribute values.
2971    intermed = map( None, map( getattr, seq, ( attr, ) * len( seq ) ), xrange( len( seq ) ), seq )
2972    intermed.sort()
2973    return map( operator.getitem, intermed, ( -1, ) * len( intermed ) )