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