38 new commits in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/57d618633209/ Changeset: 57d618633209 Branch: data_library User: martenson Date: 2013-10-08 22:41:08 Summary: start data_library branch Affected #: 3 files diff -r b61927f340f30a636204ec560522acf9b76fd262 -r 57d618633209a35342f25a13a220fedce71f7a3f .hgignore --- a/.hgignore +++ b/.hgignore @@ -115,3 +115,6 @@ *.rej *~ + +syntax: regexp +^static/AAA_scratch$ \ No newline at end of file diff -r b61927f340f30a636204ec560522acf9b76fd262 -r 57d618633209a35342f25a13a220fedce71f7a3f lib/galaxy/webapps/galaxy/api/folder_contents.py --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -1,5 +1,5 @@ """ -API operations on the contents of a library. +API operations on the contents of a folder. """ import logging, os, string, shutil, urllib, re, socket from cgi import escape, FieldStorage @@ -11,12 +11,17 @@ log = logging.getLogger( __name__ ) class FolderContentsController( BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems ): + """ + Class controls retrieval, creation and updating of folder contents. + """ @web.expose_api def index( self, trans, folder_id, **kwd ): """ GET /api/folders/{encoded_folder_id}/contents + Displays a collection (list) of a folder's contents (files and folders). + The /api/library_contents/{encoded_library_id}/contents lists everything in a library recursively, which is not what we want here. We could add a parameter to use the recursive @@ -25,7 +30,11 @@ rval = [] current_user_roles = trans.get_current_user_roles() + def traverse( folder ): + """ + Load contents of the folder (folders and datasets). + """ admin = trans.user_is_admin() rval = [] for subfolder in folder.active_folders: @@ -50,6 +59,8 @@ try: folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( decoded_folder_id ) +# log.debug("XXXXXXXXXXXXXXXXXXXXXXXXXXX folder.parent_library" + str(folder.parent_library.id)) +# log.debug("XXXXXXXXXXXXXXXXXXXXXXXXXXX folder.parent_id" + str(folder.parent_id)) parent_library = folder.parent_library except: folder = None @@ -62,15 +73,43 @@ if not folder or not ( trans.user_is_admin() or trans.app.security_agent.can_access_library_item( current_user_roles, folder, trans.user ) ): trans.response.status = 400 return "Invalid folder id ( %s ) specified." % str( folder_id ) + # TODO MARTEN Can it be that predecessors of current folder have different access rights? aka user shouldn't see them? + + # Search the path upwards and load the whole route of names and ids for breadcrumb purposes. + path_to_library = [] + + def build_path ( folder ): +# log.debug("XXXXXXXXXXXXXXXXXXXXXXX folder.parent_id BEFORE " + str(folder.parent_id)) +# log.debug("XXXXXXXXXXXXXXXXXXXXXXX folder.parent_library.id BEFORE " + str(folder.parent_library.id)) + if ( folder.parent_id != folder.parent_library.id ) and ( folder.parent_id is not None ): + log.debug("XXXXXXXXXXXXXXXXXXXXXXX LOADING UPPER FOLDER WITH ID: " + str(folder.parent_id)) + upper_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( folder.parent_id ) + log.debug("XXXXXXXXXXXXXXXXXXXXXXX upper_folder.id " + str(upper_folder.id)) + log.debug("XXXXXXXXXXXXXXXXXXXXXXX upper_folder.name " + str(upper_folder.name)) + path_to_library.append( ( upper_folder.id, upper_folder.name ) ) + path_to_library.extend( build_path( upper_folder ) ) + else: + pass + return path_to_library + + full_path = build_path( folder ) for content in traverse( folder ): + return_item = {} encoded_id = trans.security.encode_id( content.id ) if content.api_type == 'folder': + encoded_parent_library_id = trans.security.encode_id( content.parent_library.id ) encoded_id = 'F' + encoded_id - rval.append( dict( id = encoded_id, + if content.parent_id is not None: # For folder return its parent's id for browsing back. + encoded_parent_id = 'F' + trans.security.encode_id( content.parent_id ) + return_item.update ( dict ( parent_id = encoded_parent_id ) ) + return_item.update( dict( id = encoded_id, type = content.api_type, name = content.name, + library_id = encoded_parent_library_id, + full_path = full_path, url = url_for( 'folder_contents', folder_id=encoded_id ) ) ) + rval.append( return_item ) return rval @web.expose_api diff -r b61927f340f30a636204ec560522acf9b76fd262 -r 57d618633209a35342f25a13a220fedce71f7a3f lib/galaxy/webapps/galaxy/api/libraries.py --- a/lib/galaxy/webapps/galaxy/api/libraries.py +++ b/lib/galaxy/webapps/galaxy/api/libraries.py @@ -49,7 +49,7 @@ trans.model.Library.table.c.id.in_( accessible_restricted_library_ids ) ) ) rval = [] for library in query: - item = library.to_dict() + item = library.to_dict( view='element' ) item['url'] = url_for( route, id=trans.security.encode_id( library.id ) ) item['id'] = trans.security.encode_id( item['id'] ) rval.append( item ) @@ -131,6 +131,9 @@ rval['name'] = name rval['id'] = encoded_id return rval + + def edit( self, trans, payload, **kwd ): + return "Not implemented yet" @web.expose_api def delete( self, trans, id, **kwd ): https://bitbucket.org/galaxy/galaxy-central/commits/db8fd059cf4c/ Changeset: db8fd059cf4c Branch: data_library User: martenson Date: 2013-10-17 18:56:34 Summary: working reverse traversing towards root library for every folder = good for path building Affected #: 1 file diff -r 57d618633209a35342f25a13a220fedce71f7a3f -r db8fd059cf4cae9a6b7dc8bdbe7be5d071325d09 lib/galaxy/webapps/galaxy/api/folder_contents.py --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -27,11 +27,11 @@ we want here. We could add a parameter to use the recursive style, but this is meant to act similar to an "ls" directory listing. """ - rval = [] + folder_contents = [] current_user_roles = trans.get_current_user_roles() - def traverse( folder ): + def load_folder_contents( folder ): """ Load contents of the folder (folders and datasets). """ @@ -76,41 +76,55 @@ # TODO MARTEN Can it be that predecessors of current folder have different access rights? aka user shouldn't see them? # Search the path upwards and load the whole route of names and ids for breadcrumb purposes. - path_to_library = [] + path_to_root = [] def build_path ( folder ): -# log.debug("XXXXXXXXXXXXXXXXXXXXXXX folder.parent_id BEFORE " + str(folder.parent_id)) -# log.debug("XXXXXXXXXXXXXXXXXXXXXXX folder.parent_library.id BEFORE " + str(folder.parent_library.id)) - if ( folder.parent_id != folder.parent_library.id ) and ( folder.parent_id is not None ): - log.debug("XXXXXXXXXXXXXXXXXXXXXXX LOADING UPPER FOLDER WITH ID: " + str(folder.parent_id)) + path_to_root = [] + # We are almost in root + log.debug( "XXXXXXXXXXXXXXXXXXXXXXX folder.parent_id: " + str( folder.parent_id ) ) + log.debug( "XXXXXXXXXXXXXXXXXXXXXXX folder.parent_library.id: " + str( folder.parent_library.id ) ) + if folder.parent_id is None: + log.debug( "XXXXXXXXXXXXXXXXXXXXXXX ALMOST ROOT FOLDER! ADDING: " + str( folder.name ) ) + path_to_root.append( ( folder.id, folder.name ) ) +# upper_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( folder.parent_library.id ) +# path_to_root.append( ( upper_folder.id, upper_folder.name ) ) + else: + # We add the current folder and traverse up one folder. + log.debug( "XXXXXXXXXXXXXXXXXXXXXXX ADDING THIS FOLDER AND TRAVERSING UP: " + str( folder.name ) ) + path_to_root.append( ( folder.id, folder.name ) ) upper_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( folder.parent_id ) - log.debug("XXXXXXXXXXXXXXXXXXXXXXX upper_folder.id " + str(upper_folder.id)) - log.debug("XXXXXXXXXXXXXXXXXXXXXXX upper_folder.name " + str(upper_folder.name)) - path_to_library.append( ( upper_folder.id, upper_folder.name ) ) - path_to_library.extend( build_path( upper_folder ) ) - else: - pass - return path_to_library + path_to_root.extend( build_path( upper_folder ) ) +# path_to_root = path_to_root.reverse() +# return path_to_root[::-1] + return path_to_root - full_path = build_path( folder ) + # Return the reversed path so it starts with the library node. + full_path = build_path( folder )[::-1] - for content in traverse( folder ): + # Go through every item in the folder and include its meta-data. + for content_item in load_folder_contents( folder ): return_item = {} - encoded_id = trans.security.encode_id( content.id ) - if content.api_type == 'folder': - encoded_parent_library_id = trans.security.encode_id( content.parent_library.id ) + encoded_id = trans.security.encode_id( content_item.id ) + + # For folder return also hierarchy values + if content_item.api_type == 'folder': + encoded_parent_library_id = trans.security.encode_id( content_item.parent_library.id ) encoded_id = 'F' + encoded_id - if content.parent_id is not None: # For folder return its parent's id for browsing back. - encoded_parent_id = 'F' + trans.security.encode_id( content.parent_id ) + if content_item.parent_id is not None: # Return folder's parent id for browsing back. + encoded_parent_id = 'F' + trans.security.encode_id( content_item.parent_id ) return_item.update ( dict ( parent_id = encoded_parent_id ) ) + + # For every item return also the default meta-data return_item.update( dict( id = encoded_id, - type = content.api_type, - name = content.name, + type = content_item.api_type, + name = content_item.name, library_id = encoded_parent_library_id, full_path = full_path, url = url_for( 'folder_contents', folder_id=encoded_id ) ) ) - rval.append( return_item ) - return rval + folder_contents.append( return_item ) + if len( folder_contents ) == 0: + folder_contents.append( dict( full_path = full_path ) ) + return folder_contents @web.expose_api def show( self, trans, id, library_id, **kwd ): https://bitbucket.org/galaxy/galaxy-central/commits/1eda20e058eb/ Changeset: 1eda20e058eb Branch: data_library User: martenson Date: 2013-10-21 18:31:38 Summary: initial frontend commit Affected #: 3 files diff -r db8fd059cf4cae9a6b7dc8bdbe7be5d071325d09 -r 1eda20e058ebb08a4f4b71ed6988f6f106b62fdb lib/galaxy/webapps/galaxy/api/folder_contents.py --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -27,7 +27,7 @@ we want here. We could add a parameter to use the recursive style, but this is meant to act similar to an "ls" directory listing. """ - folder_contents = [] + folder_container = [] current_user_roles = trans.get_current_user_roles() @@ -100,7 +100,8 @@ # Return the reversed path so it starts with the library node. full_path = build_path( folder )[::-1] - + folder_container.append( dict( full_path = full_path ) ) + folder_contents = [] # Go through every item in the folder and include its meta-data. for content_item in load_folder_contents( folder ): return_item = {} @@ -118,13 +119,11 @@ return_item.update( dict( id = encoded_id, type = content_item.api_type, name = content_item.name, - library_id = encoded_parent_library_id, - full_path = full_path, - url = url_for( 'folder_contents', folder_id=encoded_id ) ) ) + library_id = encoded_parent_library_id + ) ) folder_contents.append( return_item ) - if len( folder_contents ) == 0: - folder_contents.append( dict( full_path = full_path ) ) - return folder_contents + folder_container.append( dict( folder_contents = folder_contents ) ) + return folder_container @web.expose_api def show( self, trans, id, library_id, **kwd ): diff -r db8fd059cf4cae9a6b7dc8bdbe7be5d071325d09 -r 1eda20e058ebb08a4f4b71ed6988f6f106b62fdb static/AAA_scratch/data_library/index.html --- /dev/null +++ b/static/AAA_scratch/data_library/index.html @@ -0,0 +1,265 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Data Library</title> + <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.1.1/css/bootstrap.min.css"> +</head> +<body> + + + <div class="container"> + <h1><a onclick="router.navigate('', {trigger: true})">Data Library</a></h1> + <hr /> + <div class="page"></div> + </div> + +<!-- LIBRARY LIST TEMPLATE --> + <script type="text/template" id="library-list-template"> + <table class="table striped"> + <thead> + <th>name</th> + <th>description</th> + <th>synopsis</th> + <th>model type</th> + <th>id</th> + + </thead> + <tbody> + <% _.each(libraries, function(library) { %> + <tr> + <td><a href="#/folders/<%- library.id %>"><%- library.get('name') %></a></td> + <td><%= htmlEncode(library.get('description')) %></td> + <td><%= htmlEncode(library.get('synopsis')) %></td> + <td><%= htmlEncode(library.get('model_class')) %></td> + <td><a href="#/folders/<%- library.id %>"><%= htmlEncode(library.get('id')) %></a></td> + </tr> + <% }); %> + </tbody> + </table> + </script> + + +<!-- FOLDERS LIST TEMPLATE --> + <script type="text/template" id="folder-content-template"> + <table class="table striped"> + <thead> + <th>name</th> + <th>type</th> + <th>parent id</th> + <th>id</th> + <th>library id</th> + </thead> + <tbody> + <% _.each(items, function(content_item) { %> + <tr> + <% if (content_item.get('type') === 'folder') { %> + <td><a href="#/folders/<%- content_item.id %>?library_id=<%- content_item.get('library_id') %>"><%- content_item.get('name') %></a></td> + <% } else { %> + <td><%- content_item.get('name') %></td> + <% } %> + <td><%= htmlEncode(content_item.get('type')) %></td> + <td><%= htmlEncode(content_item.get('parent_id')) %></td> + <td><a href="#/folders/<%- content_item.id %>"><%= htmlEncode(content_item.get('id')) %></a></td> + <td><a href="#/folders/<%- content_item.get('library_id') %>"><%= htmlEncode(content_item.get('library_id')) %></a></td> + </tr> + <% }); %> + </tbody> + </table> + + <hr/> + <div> + <% if ( typeof libraries !== undefined ) { %> + <% _.each(libraries.models, function(library) { %> + <%- library.get('name') %><br/> + <% }); %> + <% } else { %> + libraries are undefined + <% } %> + </div> + </script> + + + <script src="jquery-1.10.2.js" type="text/javascript"></script> + <script src="underscore.js" type="text/javascript"></script> + <script src="backbone.js" type="text/javascript"></script> + <script src="backbone.queryparams.js" type="text/javascript"></script> + + + <script> + + // ENCODE HTML + function htmlEncode(value){ + return $('<div/>').text(value).html(); + } + + // NESTED COLLECTIONS + function nestCollection(model, attributeName, nestedCollection) { + //setup nested references + for (var i = 0; i < nestedCollection.length; i++) { + model.attributes[attributeName][i] = nestedCollection.at(i).attributes; + } + + //create empty arrays if none + nestedCollection.bind('add', function (initiative) { + if (!model.get(attributeName)) { + model.attributes[attributeName] = []; + } + model.get(attributeName).push(initiative.attributes); + }); + + nestedCollection.bind('remove', function (initiative) { + var updateObj = {}; + updateObj[attributeName] = _.without(model.get(attributeName), initiative.attributes); + model.set(updateObj); + }); + return nestedCollection; + } + + var api_key = 'e48b2b2589b242a50538f8f9fa8e5ee9'; + + // MODIFICATIONS DONE JUST BEFORE THE REQUEST ITSELF + $.ajaxPrefilter( function( options, originalOptions, jqXHR ) { + options.url = 'http://martenson.bx.psu.edu:8080' + options.url + '?key=' + api_key; + }); + + // SERIALIZE JSON + $.fn.serializeObject = function() { + var o = {}; + var a = this.serializeArray(); + $.each(a, function() { + if (o[this.name] !== undefined) { + if (!o[this.name].push) { + o[this.name] = [o[this.name]]; + } + o[this.name].push(this.value || ''); + } else { + o[this.name] = this.value || ''; + } + }); + return o; + }; + + // LIBRARY + var Library = Backbone.Model.extend({ + urlRoot: '/api/libraries', + }); + var Libraries = Backbone.Collection.extend({ + url: '/api/libraries', + model: Library + }); + + // ITEM + var Item = Backbone.Model.extend({ + }) + + // FOLDER + var Folder = Backbone.Collection.extend({ + model: Item + }) + + // Container including metadata + var FolderContainer = Backbone.Model.extend({ + defaults: { + folder: new Folder(), + full_path: "unknown", + urlRoot: "/api/folders/", + id: "unknown" + }, + parse: function(obj) { + full_path = obj[0].full_path; + // update the inner collection + this.get("folder").reset(obj[1].folder_contents); + + // this mightn't be necessary + // delete obj.folder; + + return obj; + } + }) + + var LibraryListView = Backbone.View.extend({ + el: '.page', + render: function () { + var that = this; + if (typeof libraries === "undefined") { + libraries = new Libraries(); + } + libraries.fetch({ + success: function (libraries) { + var template = _.template($('#library-list-template').html(), {libraries: libraries.models}); + that.$el.html(template); + } + }) + } + }); + + + var FolderContentView = Backbone.View.extend({ + el: '.page', + render: function (options, params) { + var that = this; + + var folderContainer = new FolderContainer({id: options.id}); + folderContainer.url = folderContainer.attributes.urlRoot + options.id + '/contents'; + // var folder = new Folder({id: options.id}); + + folderContainer.fetch({ + success: function (container) { + folderContainer.attributes.folder = container.attributes.folder; + var template = _.template($('#folder-content-template').html(), {items: folderContainer.attributes.folder.models, id: options.id}); + that.$el.html(template); + } + }) + + // folder.fetch({ + // success: function (folder) { + // folderContainer.folder = folder; + // var template = _.template($('#folder-content-template').html(), {items: folderContainer.folder.models, id: options.id}); + // that.$el.html(template); + // } + // }) + + // var folder = new Folder({id: options.id}); + // folder.url = folder.urlRoot + options.id + '/contents'; + // folder.fetch({ + // success: function (folder) { + // // console.log(JSON.stringify(folder)) + // //create the template + // var template = _.template($('#folder-content-template').html(), {items: folder.models, id: options.id}); + // that.$el.html(template); + // } + // }) + + }, + }); + + + var libraryListView = new LibraryListView(); + var folderContentView = new FolderContentView(); + + var Router = Backbone.Router.extend({ + routes: { + "" : "libraries", + "folders/:id" : "folder_content" + } + }); + + var router = new Router; + + router.on('route:libraries', function() { + // render libraries list + libraryListView.render(); + }) + + router.on('route:folder_content', function(id, params) { + // render folder's content + folderContentView.render({id: id, params: params}); + }) + + Backbone.history.start(); + </script> + + +</body> +</html> diff -r db8fd059cf4cae9a6b7dc8bdbe7be5d071325d09 -r 1eda20e058ebb08a4f4b71ed6988f6f106b62fdb static/AAA_scratch/data_library/index_working.html --- /dev/null +++ b/static/AAA_scratch/data_library/index_working.html @@ -0,0 +1,261 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Data Library</title> + <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.1.1/css/bootstrap.min.css"> +</head> +<body> + + + <div class="container"> + <h1>Data Library</h1> + <hr /> + <div class="page"></div> + </div> + +<!-- LIBRARY LIST TEMPLATE --> + <script type="text/template" id="library-list-template"> + <a href="#/new" class="btn btn-primary">New Library</a> + <hr /> + <table class="table striped"> + <thead> + <th>name</th> + <th>description</th> + <th>synopsis</th> + <th></th> + <th></th> + </thead> + <tbody> + <% _.each(libraries, function(library) { %> + <tr> + <td><a href="#/folders/<%- library.id %>"><%- library.get('name') %></a></td> + <td><%= htmlEncode(library.get('description')) %></td> + <td><%= htmlEncode(library.get('synopsis')) %></td> + <td><a href="#/edit/<%= library.id %>" class="btn">Edit</td> + <td><button data-library-id="<%- library.id %>" class="btn btn-danger delete">Delete</button></td> + </tr> + <% }); %> + </tbody> + </table> + </script> + +<!-- LIBRARY EDIT/CREATE TEMPLATE --> + <script type="text/template" id="library-edit-template"> + <form class="library-edit-form"> + <legend>Create Library</legend> + <label>Name of the Library</label> + <input type="text" name="name" /> + <label>Description</label> + <input type="text" name="description" /> + <label>Synopsis</label> + <input type="text" name="synopsis" /> + <hr /> + <button type="submit" class="btn">Create Library</button> + </form> + </script> + + +<!-- FOLDERS LIST TEMPLATE --> + <script type="text/template" id="folders-list-template"> + <table class="table striped"> + <thead> + <th>name</th> + <th>type</th> + </thead> + <tbody> + <% _.each(folders, function(folder) { %> + <tr> + <% if (folder.get('type') === 'folder') { %> + <td><a href="#/folders/<%- folder.id %>"><%- folder.get('name') %></a></td> + <% } else { %> + <td><%- folder.get('name') %></td> + <% } %> + <td><%= htmlEncode(folder.get('type')) %></td> + </tr> + <% }); %> + </tbody> + </table> + </script> + + + <script src="jquery-1.10.2.js" type="text/javascript"></script> + <script src="underscore.js" type="text/javascript"></script> + <script src="backbone.js" type="text/javascript"></script> + + + <script> + + // ENCODE HTML + function htmlEncode(value){ + return $('<div/>').text(value).html(); + } + + var api_key = 'e48b2b2589b242a50538f8f9fa8e5ee9'; + + // MODIFICATIONS DONE JUST BEFORE THE REQUEST ITSELF + $.ajaxPrefilter( function( options, originalOptions, jqXHR ) { + options.url = 'http://martenson.bx.psu.edu:8080' + options.url + '?key=' + api_key; + }); + + // SERIALIZE JSON + $.fn.serializeObject = function() { + var o = {}; + var a = this.serializeArray(); + $.each(a, function() { + if (o[this.name] !== undefined) { + if (!o[this.name].push) { + o[this.name] = [o[this.name]]; + } + o[this.name].push(this.value || ''); + } else { + o[this.name] = this.value || ''; + } + }); + return o; + }; + +// LIBRARY + var Library = Backbone.Model.extend({ + urlRoot: '/api/libraries' + }); + var Libraries = Backbone.Collection.extend({ + url: '/api/libraries' + }); +// FOLDER + var Folder = Backbone.Model.extend({ + urlRoot: '/api/folders/' + }) + var Folders = Backbone.Collection.extend({ + model: Folder, + urlRoot: '/api/folders/' + + }) + + var LibraryEditView = Backbone.View.extend({ + el: '.page', + events: { + 'submit .library-edit-form': 'saveLibrary' + }, + saveLibrary: function (ev) { + var libraryDetails = $(ev.currentTarget).serializeObject(); + var library = new Library(); + library.save(libraryDetails, { + success: function (library) { + router.navigate('', {trigger: true}) + } + }); + console.log(libraryDetails); + return false; + }, + render: function (options) { + var that = this; + if (options.id) { + this.library = new Library({id: options.id}); + this.library.fetch({ + success: function(library){ + var template = _.template($('#library-edit-template').html(), {library: library}); + that.$el.html(template); + } + }); + } else { + var template = _.template($('#library-edit-template').html(), {user: null}); + this.$el.html(template); + } + } + }); + + + var LibraryListView = Backbone.View.extend({ + el: '.page', + events: { + 'click .delete' : 'deleteLibrary' + }, + render: function () { + var that = this; + var libraries = new Libraries(); + libraries.fetch({ + success: function (libraries) { + var template = _.template($('#library-list-template').html(), {libraries: libraries.models}); + that.$el.html(template); + } + }) + }, + deleteLibrary : function (ev) { + // DELETE /libraries/{id} + var id_to_delete = $(ev.target).attr('data-library-id'); + var libraryToDelete = new Library({id: id_to_delete}) + libraryToDelete.destroy({ + success: function () { + libraryListView.render(); + } + }) + return false + }, + createLibrary : function (ev) { + // PUT /libraries + + } + }); + + + var FoldersListView = Backbone.View.extend({ + el: '.page', + render: function (options) { + var that = this; + var folders = new Folders({id: options.id}); + folders.url = folders.urlRoot + options.id + '/contents'; + folders.fetch({ + success: function (folders) { + var template = _.template($('#folders-list-template').html(), {folders: folders.models, id: options.id}); + that.$el.html(template); + } + }) + }, + }); + + + var libraryListView = new LibraryListView(); + var libraryEditView = new LibraryEditView(); + var foldersListView = new FoldersListView(); + + + var Router = Backbone.Router.extend({ + routes: { + "" : "home", + "new" : "edit_library", + "edit/:id" : "edit_library", + "delete/:id" : "delete_library", + "folders/:id" : "folders" + } + }); + + var router = new Router; + + router.on('route:home', function() { + // render libraries list + libraryListView.render(); + }) + router.on('route:edit_library', function(id) { + // render library edit form + libraryEditView.render({id: id}); + }) + router.on('route:delete_library', function() { + // render delete library form + libraryDeleteView.render(); + }) + // router.on('route:folders', function(parent_id, top_level) { + // // render folders list + // foldersListView.render({id: parent_id, top_level: true}); + // }) + router.on('route:folders', function(id) { + // render folders list + foldersListView.render({id: id}); + }) + + Backbone.history.start(); + </script> + + +</body> +</html> https://bitbucket.org/galaxy/galaxy-central/commits/dc735a38cf76/ Changeset: dc735a38cf76 Branch: data_library User: martenson Date: 2013-10-24 20:22:26 Summary: refactoring, modals start Affected #: 4 files diff -r 1eda20e058ebb08a4f4b71ed6988f6f106b62fdb -r dc735a38cf76f93772027b34e489db1ef9d2d0a5 lib/galaxy/model/__init__.py --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -1835,7 +1835,7 @@ return name class LibraryFolder( object, Dictifiable ): - dict_element_visible_keys = ( 'id', 'parent_id', 'name', 'description', 'item_count', 'genome_build' ) + dict_element_visible_keys = ( 'id', 'parent_id', 'name', 'description', 'item_count', 'genome_build', 'update_time' ) def __init__( self, name=None, description=None, item_count=0, order_id=None ): self.name = name or "Unnamed folder" self.description = description diff -r 1eda20e058ebb08a4f4b71ed6988f6f106b62fdb -r dc735a38cf76f93772027b34e489db1ef9d2d0a5 lib/galaxy/webapps/galaxy/api/folder_contents.py --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -15,42 +15,37 @@ Class controls retrieval, creation and updating of folder contents. """ + def load_folder_contents( self, trans, folder ): + """ + Loads all contents of the folder (folders and data sets) but only in the first level. + """ + current_user_roles = trans.get_current_user_roles() + is_admin = trans.user_is_admin() + content_items = [] + for subfolder in folder.active_folders: + if not is_admin: + can_access, folder_ids = trans.app.security_agent.check_folder_contents( trans.user, current_user_roles, subfolder ) + if (is_admin or can_access) and not subfolder.deleted: + subfolder.api_type = 'folder' + content_items.append( subfolder ) + for dataset in folder.datasets: + if not is_admin: + can_access = trans.app.security_agent.can_access_dataset( current_user_roles, dataset.library_dataset_dataset_association.dataset ) + if (is_admin or can_access) and not dataset.deleted: + dataset.api_type = 'file' + content_items.append( dataset ) + return content_items + @web.expose_api def index( self, trans, folder_id, **kwd ): """ GET /api/folders/{encoded_folder_id}/contents - Displays a collection (list) of a folder's contents (files and folders). - - The /api/library_contents/{encoded_library_id}/contents - lists everything in a library recursively, which is not what - we want here. We could add a parameter to use the recursive - style, but this is meant to act similar to an "ls" directory listing. + Encoded folder ID is prepended with 'F' if it is a folder as opposed to a data set which does not have it. """ folder_container = [] current_user_roles = trans.get_current_user_roles() - - def load_folder_contents( folder ): - """ - Load contents of the folder (folders and datasets). - """ - admin = trans.user_is_admin() - rval = [] - for subfolder in folder.active_folders: - if not admin: - can_access, folder_ids = trans.app.security_agent.check_folder_contents( trans.user, current_user_roles, subfolder ) - if (admin or can_access) and not subfolder.deleted: - subfolder.api_type = 'folder' - rval.append( subfolder ) - for ld in folder.datasets: - if not admin: - can_access = trans.app.security_agent.can_access_dataset( current_user_roles, ld.library_dataset_dataset_association.dataset ) - if (admin or can_access) and not ld.deleted: - ld.api_type = 'file' - rval.append( ld ) - return rval - try: decoded_folder_id = trans.security.decode_id( folder_id[-16:] ) except TypeError: @@ -59,69 +54,68 @@ try: folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( decoded_folder_id ) -# log.debug("XXXXXXXXXXXXXXXXXXXXXXXXXXX folder.parent_library" + str(folder.parent_library.id)) -# log.debug("XXXXXXXXXXXXXXXXXXXXXXXXXXX folder.parent_id" + str(folder.parent_id)) parent_library = folder.parent_library except: folder = None - log.error( "FolderContentsController.index: Unable to retrieve folder %s" - % folder_id ) + log.error( "FolderContentsController.index: Unable to retrieve folder with ID: %s" % folder_id ) - # TODO: Find the API's path to this folder if necessary. - # This was needed in recursive descent, but it's not needed - # for "ls"-style content checking: - if not folder or not ( trans.user_is_admin() or trans.app.security_agent.can_access_library_item( current_user_roles, folder, trans.user ) ): + # We didn't find the folder or user does not have an access to it. + if not folder: trans.response.status = 400 return "Invalid folder id ( %s ) specified." % str( folder_id ) - # TODO MARTEN Can it be that predecessors of current folder have different access rights? aka user shouldn't see them? - # Search the path upwards and load the whole route of names and ids for breadcrumb purposes. + if not ( trans.user_is_admin() or trans.app.security_agent.can_access_library_item( current_user_roles, folder, trans.user ) ): + log.warning( "SECURITY: User (id: %s) without proper access rights is trying to load folder with ID of %s" % ( trans.user.id, folder.id ) ) + trans.response.status = 400 + return "Invalid folder id ( %s ) specified." % str( folder_id ) + path_to_root = [] - def build_path ( folder ): + """ + Search the path upwards recursively and load the whole route of names and ids for breadcrumb purposes. + """ path_to_root = [] # We are almost in root - log.debug( "XXXXXXXXXXXXXXXXXXXXXXX folder.parent_id: " + str( folder.parent_id ) ) - log.debug( "XXXXXXXXXXXXXXXXXXXXXXX folder.parent_library.id: " + str( folder.parent_library.id ) ) if folder.parent_id is None: log.debug( "XXXXXXXXXXXXXXXXXXXXXXX ALMOST ROOT FOLDER! ADDING: " + str( folder.name ) ) - path_to_root.append( ( folder.id, folder.name ) ) + path_to_root.append( ( 'F' + trans.security.encode_id( folder.id ), folder.name ) ) # upper_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( folder.parent_library.id ) # path_to_root.append( ( upper_folder.id, upper_folder.name ) ) else: # We add the current folder and traverse up one folder. log.debug( "XXXXXXXXXXXXXXXXXXXXXXX ADDING THIS FOLDER AND TRAVERSING UP: " + str( folder.name ) ) - path_to_root.append( ( folder.id, folder.name ) ) + path_to_root.append( ( 'F' + trans.security.encode_id( folder.id ), folder.name ) ) upper_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( folder.parent_id ) path_to_root.extend( build_path( upper_folder ) ) -# path_to_root = path_to_root.reverse() -# return path_to_root[::-1] return path_to_root # Return the reversed path so it starts with the library node. full_path = build_path( folder )[::-1] folder_container.append( dict( full_path = full_path ) ) + folder_contents = [] # Go through every item in the folder and include its meta-data. - for content_item in load_folder_contents( folder ): + for content_item in self.load_folder_contents( trans, folder ): return_item = {} encoded_id = trans.security.encode_id( content_item.id ) # For folder return also hierarchy values if content_item.api_type == 'folder': - encoded_parent_library_id = trans.security.encode_id( content_item.parent_library.id ) +# encoded_parent_library_id = trans.security.encode_id( content_item.parent_library.id ) encoded_id = 'F' + encoded_id - if content_item.parent_id is not None: # Return folder's parent id for browsing back. - encoded_parent_id = 'F' + trans.security.encode_id( content_item.parent_id ) - return_item.update ( dict ( parent_id = encoded_parent_id ) ) +# if content_item.parent_id is not None: # Return folder's parent id for browsing back. +# encoded_parent_id = 'F' + trans.security.encode_id( content_item.parent_id ) + last_updated = content_item.update_time.strftime( "%Y-%m-%d %I:%M %p" ) + return_item.update ( dict ( item_count = content_item.item_count, last_updated = last_updated ) ) # For every item return also the default meta-data return_item.update( dict( id = encoded_id, type = content_item.api_type, - name = content_item.name, - library_id = encoded_parent_library_id + name = content_item.name + ) ) folder_contents.append( return_item ) + # Put the data in the container folder_container.append( dict( folder_contents = folder_contents ) ) return folder_container diff -r 1eda20e058ebb08a4f4b71ed6988f6f106b62fdb -r dc735a38cf76f93772027b34e489db1ef9d2d0a5 lib/galaxy/webapps/galaxy/api/libraries.py --- a/lib/galaxy/webapps/galaxy/api/libraries.py +++ b/lib/galaxy/webapps/galaxy/api/libraries.py @@ -51,7 +51,7 @@ for library in query: item = library.to_dict( view='element' ) item['url'] = url_for( route, id=trans.security.encode_id( library.id ) ) - item['id'] = trans.security.encode_id( item['id'] ) + item['id'] = 'F' + trans.security.encode_id( item['id'] ) rval.append( item ) return rval diff -r 1eda20e058ebb08a4f4b71ed6988f6f106b62fdb -r dc735a38cf76f93772027b34e489db1ef9d2d0a5 static/AAA_scratch/data_library/index.html --- a/static/AAA_scratch/data_library/index.html +++ b/static/AAA_scratch/data_library/index.html @@ -1,10 +1,13 @@ -<!doctype html> +<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Data Library</title> - <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.1.1/css/bootstrap.min.css"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <!-- Bootstrap --> + <link href="css/bootstrap.min.css" rel="stylesheet" media="screen"></head> + <body> @@ -16,7 +19,7 @@ <!-- LIBRARY LIST TEMPLATE --><script type="text/template" id="library-list-template"> - <table class="table striped"> + <table class="table table-striped"><thead><th>name</th><th>description</th> @@ -42,45 +45,47 @@ <!-- FOLDERS LIST TEMPLATE --><script type="text/template" id="folder-content-template"> - <table class="table striped"> + + <% _.each(path, function(path_item) { %> + <% if (path_item[0] != id) { %> + <a href='#/folders/<%- path_item[0] %>'><%- path_item[1] %></a> | + <% } else { %> + <%- path_item[1] %> + <% } %> + <% }); %> + + <table class="table table-hover table-condensed"><thead> + <th style="text-align: center; width: 20px; "><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th><th>name</th><th>type</th> - <th>parent id</th> - <th>id</th> - <th>library id</th></thead><tbody><% _.each(items, function(content_item) { %> - <tr> + <tr class="folder_row" id="<%- content_item.id %>"> + <td style="text-align: center; "><input style="margin: 0;" type="checkbox"></td><% if (content_item.get('type') === 'folder') { %> - <td><a href="#/folders/<%- content_item.id %>?library_id=<%- content_item.get('library_id') %>"><%- content_item.get('name') %></a></td> + <td><a href="#/folders/<%- content_item.id %>"><%- content_item.get('name') %></a> + <% if (content_item.get('item_count') === 0) { %> + <span class="muted">(empty folder)</span> + <% } %> + </td><% } else { %><td><%- content_item.get('name') %></td><% } %><td><%= htmlEncode(content_item.get('type')) %></td> - <td><%= htmlEncode(content_item.get('parent_id')) %></td> - <td><a href="#/folders/<%- content_item.id %>"><%= htmlEncode(content_item.get('id')) %></a></td> - <td><a href="#/folders/<%- content_item.get('library_id') %>"><%= htmlEncode(content_item.get('library_id')) %></a></td></tr><% }); %></tbody></table> - <hr/> - <div> - <% if ( typeof libraries !== undefined ) { %> - <% _.each(libraries.models, function(library) { %> - <%- library.get('name') %><br/> - <% }); %> - <% } else { %> - libraries are undefined - <% } %> - </div> + </script> + <script src="require.js"></script><script src="jquery-1.10.2.js" type="text/javascript"></script> + <script src="js/bootstrap.min.js"></script><script src="underscore.js" type="text/javascript"></script><script src="backbone.js" type="text/javascript"></script><script src="backbone.queryparams.js" type="text/javascript"></script> @@ -88,34 +93,27 @@ <script> + // configure require + // require.config({ + // baseUrl: "/", + // shim: { + // "libs/underscore": { exports: "_" }, + // "libs/backbone/backbone": { exports: "Backbone" }, + // "libs/backbone/backbone-relational": ["libs/backbone/backbone"] + // } + // }); + + // require(['galaxy.modal'], function(modal) + // { + // Galaxy.modal = new modal.GalaxyModal(); + // }); + + // ENCODE HTML function htmlEncode(value){ return $('<div/>').text(value).html(); } - // NESTED COLLECTIONS - function nestCollection(model, attributeName, nestedCollection) { - //setup nested references - for (var i = 0; i < nestedCollection.length; i++) { - model.attributes[attributeName][i] = nestedCollection.at(i).attributes; - } - - //create empty arrays if none - nestedCollection.bind('add', function (initiative) { - if (!model.get(attributeName)) { - model.attributes[attributeName] = []; - } - model.get(attributeName).push(initiative.attributes); - }); - - nestedCollection.bind('remove', function (initiative) { - var updateObj = {}; - updateObj[attributeName] = _.without(model.get(attributeName), initiative.attributes); - model.set(updateObj); - }); - return nestedCollection; - } - var api_key = 'e48b2b2589b242a50538f8f9fa8e5ee9'; // MODIFICATIONS DONE JUST BEFORE THE REQUEST ITSELF @@ -167,13 +165,10 @@ id: "unknown" }, parse: function(obj) { - full_path = obj[0].full_path; + this.full_path = obj[0].full_path; + // this.folder.reset(obj[1].folder_contents); // update the inner collection this.get("folder").reset(obj[1].folder_contents); - - // this mightn't be necessary - // delete obj.folder; - return obj; } }) @@ -197,41 +192,39 @@ var FolderContentView = Backbone.View.extend({ el: '.page', + events: { + 'click #select-all-checkboxes' : 'selectAll', + 'click .folder_row' : 'selectClicked' + }, render: function (options, params) { var that = this; var folderContainer = new FolderContainer({id: options.id}); folderContainer.url = folderContainer.attributes.urlRoot + options.id + '/contents'; - // var folder = new Folder({id: options.id}); folderContainer.fetch({ success: function (container) { - folderContainer.attributes.folder = container.attributes.folder; - var template = _.template($('#folder-content-template').html(), {items: folderContainer.attributes.folder.models, id: options.id}); + // folderContainer.attributes.folder = container.attributes.folder; + var template = _.template($('#folder-content-template').html(), {path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id}); that.$el.html(template); } }) - - // folder.fetch({ - // success: function (folder) { - // folderContainer.folder = folder; - // var template = _.template($('#folder-content-template').html(), {items: folderContainer.folder.models, id: options.id}); - // that.$el.html(template); - // } - // }) - - // var folder = new Folder({id: options.id}); - // folder.url = folder.urlRoot + options.id + '/contents'; - // folder.fetch({ - // success: function (folder) { - // // console.log(JSON.stringify(folder)) - // //create the template - // var template = _.template($('#folder-content-template').html(), {items: folder.models, id: options.id}); - // that.$el.html(template); - // } - // }) - }, + selectAll : function (ev) { + var selected = ev.target.checked; + // Iterate each checkbox + $(':checkbox').each(function () { this.checked = selected; }); + }, + selectClicked : function (ev) { + var checkbox = $("#" + ev.target.parentElement.id).find(':checkbox') + if (checkbox[0] != undefined) { + if (checkbox[0].checked){ + checkbox[0].checked = ''; + } else { + checkbox[0].checked = 'selected'; + } + } + } }); @@ -258,8 +251,7 @@ }) Backbone.history.start(); + </script> - - </body></html> https://bitbucket.org/galaxy/galaxy-central/commits/518ded2c3f30/ Changeset: 518ded2c3f30 Branch: data_library User: martenson Date: 2013-11-06 23:15:41 Summary: work in progress on data libraries, integrated within galaxy framework with require, modularized Affected #: 7 files diff -r dc735a38cf76f93772027b34e489db1ef9d2d0a5 -r 518ded2c3f30cca803a29f76da2e80505b166647 lib/galaxy/webapps/galaxy/controllers/library.py --- a/lib/galaxy/webapps/galaxy/controllers/library.py +++ b/lib/galaxy/webapps/galaxy/controllers/library.py @@ -76,6 +76,22 @@ library_list_grid = LibraryListGrid() + + @web.expose + def list( self, trans, **kwd ): + params = util.Params( kwd ) + + # define app configuration for generic mako template + app = { + 'jscript' : "galaxy.library" + } + + # fill template + return trans.fill_template('galaxy.panels.mako', config = {'app' : app}) + + +# return trans.fill_template( "/library/list.mako") + @web.expose def index( self, trans, **kwd ): params = util.Params( kwd ) diff -r dc735a38cf76f93772027b34e489db1ef9d2d0a5 -r 518ded2c3f30cca803a29f76da2e80505b166647 static/AAA_scratch/data_library/index.html --- a/static/AAA_scratch/data_library/index.html +++ b/static/AAA_scratch/data_library/index.html @@ -83,7 +83,7 @@ </script> - <script src="require.js"></script> + <script src="jquery-1.10.2.js" type="text/javascript"></script><script src="js/bootstrap.min.js"></script><script src="underscore.js" type="text/javascript"></script> @@ -93,22 +93,6 @@ <script> - // configure require - // require.config({ - // baseUrl: "/", - // shim: { - // "libs/underscore": { exports: "_" }, - // "libs/backbone/backbone": { exports: "Backbone" }, - // "libs/backbone/backbone-relational": ["libs/backbone/backbone"] - // } - // }); - - // require(['galaxy.modal'], function(modal) - // { - // Galaxy.modal = new modal.GalaxyModal(); - // }); - - // ENCODE HTML function htmlEncode(value){ return $('<div/>').text(value).html(); @@ -245,9 +229,9 @@ libraryListView.render(); }) - router.on('route:folder_content', function(id, params) { + router.on('route:folder_content', function(id) { // render folder's content - folderContentView.render({id: id, params: params}); + folderContentView.render({id: id}); }) Backbone.history.start(); diff -r dc735a38cf76f93772027b34e489db1ef9d2d0a5 -r 518ded2c3f30cca803a29f76da2e80505b166647 static/scripts/galaxy.library.js --- /dev/null +++ b/static/scripts/galaxy.library.js @@ -0,0 +1,333 @@ +// MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +// === GALAXY LIBRARY MODULE ==== +// MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM + +// global variables +var view = null; +var library_router = null; + +// dependencies +define(["galaxy.modal", "galaxy.master", "galaxy.library.router"], function(mod_modal, mod_master, router) { + +// MMMMMMMMMMMMMMM +// === Models ==== +// MMMMMMMMMMMMMMM + + // LIBRARY + var Library = Backbone.Model.extend({ + urlRoot: '/api/libraries', + }); + + // LIBRARIES + var Libraries = Backbone.Collection.extend({ + url: '/api/libraries', + model: Library + }); + + // ITEM + var Item = Backbone.Model.extend({ + }) + + // FOLDER + var Folder = Backbone.Collection.extend({ + model: Item + }) + + // Container for folder contents (folders, items and metadata). + var FolderContainer = Backbone.Model.extend({ + defaults: { + folder: new Folder(), + full_path: "unknown", + urlRoot: "/api/folders/", + id: "unknown" + }, + parse: function(obj) { + this.full_path = obj[0].full_path; + // this.folder.reset(obj[1].folder_contents); + // update the inner collection + this.get("folder").reset(obj[1].folder_contents); + return obj; + } + }) + +// MMMMMMMMMMMMMM +// === Views ==== +// MMMMMMMMMMMMMM + +// galaxy folder +var FolderContentView = Backbone.View.extend({ + el : '#center', + // initialize + initialize : function(){ + // view = this; + //set up the library router + // this.set_up_router(); + + //render + // this.render(); + }, + // set up router + set_up_router : function(){ + if (library_router === null){ + library_router = new router.LibraryRouter(); + Backbone.history.start(); + } + }, + template_folder : function (){ + tmpl_array = []; + + tmpl_array.push('<% _.each(path, function(path_item) { %>'); + tmpl_array.push('<% if (path_item[0] != id) { %>'); + tmpl_array.push('<a href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a> |'); + tmpl_array.push('<% } else { %>'); + tmpl_array.push('<%- path_item[1] %>'); + tmpl_array.push('<% } %>'); + tmpl_array.push('<% }); %>'); + tmpl_array.push('<table class="table table-hover table-condensed">'); + tmpl_array.push(' <thead>'); + tmpl_array.push(' <th style="text-align: center; width: 20px; "><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th>'); + tmpl_array.push(' <th>name</th>'); + tmpl_array.push(' <th>type</th>'); + tmpl_array.push(' </thead>'); + tmpl_array.push(' <tbody>'); + tmpl_array.push(' <% _.each(items, function(content_item) { %>'); + tmpl_array.push(' <tr class="folder_row" id="<%- content_item.id %>">'); + tmpl_array.push(' <td style="text-align: center; "><input style="margin: 0;" type="checkbox"></td>'); + tmpl_array.push(' <% if (content_item.get("type") === "folder") { %>'); + tmpl_array.push(' <td><a href="#/folders/<%- content_item.id %>"><%- content_item.get("name") %></a>'); + tmpl_array.push(' <% if (content_item.get("item_count") === 0) { %>'); + tmpl_array.push(' <span class="muted">(empty folder)</span>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' </td>'); + tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <td><%- content_item.get("name") %></td>'); + tmpl_array.push(' <% } %> '); + tmpl_array.push(' <td><%= _.escape(content_item.get("type")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push(' '); + tmpl_array.push(' '); + tmpl_array.push(' '); + tmpl_array.push(' </tbody>'); + tmpl_array.push('</table>'); + + return tmpl_array.join(''); + }, + events: { + 'click #select-all-checkboxes' : 'selectAll', + 'click .folder_row' : 'selectClicked' + }, + render: function (options) { + var that = this; + + var folderContainer = new FolderContainer({id: options.id}); + folderContainer.url = folderContainer.attributes.urlRoot + options.id + '/contents'; + + folderContainer.fetch({ + success: function (container) { + // folderContainer.attributes.folder = container.attributes.folder; + var template = _.template(that.template_folder(), {path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id}); + that.$el.html(template); + } + }) + }, + selectAll : function (ev) { + var selected = ev.target.checked; + // Iterate each checkbox + $(':checkbox').each(function () { this.checked = selected; }); + }, + selectClicked : function (ev) { + var checkbox = $("#" + ev.target.parentElement.id).find(':checkbox') + if (checkbox[0] != undefined) { + if (checkbox[0].checked){ + checkbox[0].checked = ''; + } else { + checkbox[0].checked = 'selected'; + } + } + } +}); + + +// galaxy library +var GalaxyLibraryview = Backbone.View.extend({ + el: '#center', + + events: { + 'click #create_new_library_btn' : 'show_library_modal' + }, + + + // initialize + initialize : function(){ + // view = this; + //set up the libray router + // this.set_up_router(); + + //render + // this.render(); + }, + + // set up router + set_up_router : function(){ + if (library_router === null){ + library_router = new router.LibraryRouter(); + Backbone.history.start(); + } + }, + + // template + template_library_list : function (){ + tmpl_array = []; + + tmpl_array.push(''); + tmpl_array.push('<h1>Welcome to the data libraries</h1>'); + tmpl_array.push('<a href="" id="create_new_library_btn" class="btn btn-primary">New Library</a>'); + tmpl_array.push('<table class="table table-striped">'); + tmpl_array.push(' <thead>'); + tmpl_array.push(' <th>name</th>'); + tmpl_array.push(' <th>description</th>'); + tmpl_array.push(' <th>synopsis</th> '); + tmpl_array.push(' <th>model type</th> '); + tmpl_array.push(' <th>id</th> '); + tmpl_array.push(' </thead>'); + tmpl_array.push(' <tbody>'); + tmpl_array.push(' <% _.each(libraries, function(library) { %>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <td><a href="#/folders/<%- library.id %>"><%- library.get("name") %></a></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("description")) %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("synopsis")) %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("model_class")) %></td>'); + tmpl_array.push(' <td><a href="#/folders/<%- library.id %>"><%= _.escape(library.get("id")) %></a></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push(''); + tmpl_array.push(''); + tmpl_array.push(''); + tmpl_array.push(''); + tmpl_array.push(' </tbody>'); + tmpl_array.push('</table>'); + + return tmpl_array.join(''); + }, + + // render + render: function () { + var that = this; + // if (typeof libraries === "undefined") { + libraries = new Libraries(); + // } + libraries.fetch({ + success: function (libraries) { + var template = _.template(that.template_library_list(), {libraries: libraries.models}); + that.$el.html(template); + } + }) + }, + + // own modal + modal : null, + + // show/hide create library modal + show_library_modal : function (e) + { + // prevent default + e.preventDefault(); + + // create modal + if (!this.modal){ + // make modal + var self = this; + this.modal = new mod_modal.GalaxyModal( + { + title : 'Create New Library', + body : this.template_new_library(), + buttons : { + 'Create' : function() {self.create_new_library_event()}, + 'Close' : function() {self.modal.hide()} + } + }); + } + + // show modal + this.modal.show(); + }, + create_new_library_event: function(){ + var libraryDetails = this.serialize_new_library(); + var library = new Library(); + library.save(libraryDetails, { + success: function (library) { + + library_router.navigate('', {trigger: true}) + } + }); + // console.log(libraryDetails); + return false; + }, + serialize_new_library : function(){ + return { + name: $("input[name='Name']").val(), + description: $("input[name='Description']").val(), + synopsis: $("input[name='Synopsis']").val() + }; + }, + // load html template + template_new_library: function() + { + tmpl_array = []; + tmpl_array.push('<div id="new_library_modal">'); + tmpl_array.push('<form>'); + tmpl_array.push('<input type="text" name="Name" value="" placeholder="Name">'); + tmpl_array.push('<input type="text" name="Description" value="" placeholder="Description">'); + // tmpl_array.push('<label>Synopsis</label>'); + tmpl_array.push('<input type="text" name="Synopsis" value="" placeholder="Synopsis">'); + tmpl_array.push(''); + tmpl_array.push('</form>'); + tmpl_array.push('</div>'); + return tmpl_array.join(''); + + + // return '<div id="'+ id +'"></div>'; + } +}); + +// galaxy library wrapper View +var GalaxyLibrary = Backbone.View.extend({ + folderContentView : null, + galaxyLibraryview : null, + initialize : function(){ + + folderContentView = new FolderContentView(); + galaxyLibraryview = new GalaxyLibraryview(); + + library_router = new router.LibraryRouter(); + + library_router.on('route:libraries', function() { + // render libraries list + galaxyLibraryview.render(); + }) + + library_router.on('route:folder_content', function(id) { + // render folder's contents + folderContentView.render({id: id}); + + }) + + // library_router.on('route:show_library_modal', function() { + // // render folder's contents + // galaxyLibraryview.show_library_modal(); + + // }) + + Backbone.history.start(); + + return this + } +}); + +// return +return { + GalaxyApp: GalaxyLibrary +}; + +}); diff -r dc735a38cf76f93772027b34e489db1ef9d2d0a5 -r 518ded2c3f30cca803a29f76da2e80505b166647 static/scripts/galaxy.library.router.js --- /dev/null +++ b/static/scripts/galaxy.library.router.js @@ -0,0 +1,21 @@ +define( [], function() { + +/** + * -- Routers -- + */ + +/** + * Router for library browser. + */ +var LibraryRouter = Backbone.Router.extend({ + routes: { + "" : "libraries", + "folders/:id" : "folder_content" + } +}); + +return { + LibraryRouter: LibraryRouter, +}; + +}) \ No newline at end of file diff -r dc735a38cf76f93772027b34e489db1ef9d2d0a5 -r 518ded2c3f30cca803a29f76da2e80505b166647 static/style/Gruntfile.js --- a/static/style/Gruntfile.js +++ b/static/style/Gruntfile.js @@ -6,7 +6,7 @@ var theme = grunt.option( 'theme', 'blue' ); var out = 'blue' - var lessFiles = [ 'base', 'autocomplete_tagging', 'embed_item', 'iphone', 'masthead', 'library', 'trackster' ]; + var lessFiles = [ 'base', 'autocomplete_tagging', 'embed_item', 'iphone', 'masthead', 'library', 'new_library', 'trackster' ]; var _ = grunt.util._; var fmt = _.sprintf; diff -r dc735a38cf76f93772027b34e489db1ef9d2d0a5 -r 518ded2c3f30cca803a29f76da2e80505b166647 templates/base/base_panels.mako --- a/templates/base/base_panels.mako +++ b/templates/base/base_panels.mako @@ -102,12 +102,13 @@ }); ## load galaxy js-modules - require(['galaxy.master', 'galaxy.frame', 'galaxy.modal', 'galaxy.upload'], function(master, frame, modal, upload) + require(['galaxy.master', 'galaxy.frame', 'galaxy.modal', 'galaxy.upload', 'galaxy.library'], function(master, frame, modal, upload, library) { Galaxy.master = new master.GalaxyMaster(); Galaxy.frame_manager = new frame.GalaxyFrameManager(); Galaxy.modal = new modal.GalaxyModal(); - ##Galaxy.upload = new upload.GalaxyUpload(); + Galaxy.upload = new upload.GalaxyUpload(); + ##Galaxy.library = new library.GalaxyLibrary(); }); </script></%def> diff -r dc735a38cf76f93772027b34e489db1ef9d2d0a5 -r 518ded2c3f30cca803a29f76da2e80505b166647 templates/webapps/galaxy/base_panels.mako --- a/templates/webapps/galaxy/base_panels.mako +++ b/templates/webapps/galaxy/base_panels.mako @@ -126,6 +126,7 @@ ## 'Shared Items' or Libraries tab. <% menu_options = [ + [ _('New Libraries'), h.url_for( controller='/library', action='list') ], [ _('Data Libraries'), h.url_for( controller='/library', action='index') ], None, [ _('Published Histories'), h.url_for( controller='/history', action='list_published' ) ], https://bitbucket.org/galaxy/galaxy-central/commits/3ea8be4161ab/ Changeset: 3ea8be4161ab Branch: data_library User: martenson Date: 2013-11-12 22:48:56 Summary: transfer into modal window, requirejs, notifications, import to history, galaxy.modal changes, API for library datasets, start of API versioning Affected #: 6 files diff -r 518ded2c3f30cca803a29f76da2e80505b166647 -r 3ea8be4161ab0a9ed2e1c17160c9d3d52d41b555 lib/galaxy/model/__init__.py --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -2000,6 +2000,7 @@ genome_build = ldda.dbkey, misc_info = ldda.info, misc_blurb = ldda.blurb, + peek = ( lambda ldda: ldda.display_peek() if ldda.peek and ldda.peek != 'no peek' else None )( ldda ), template_data = template_data ) if ldda.dataset.uuid is None: rval['uuid'] = None diff -r 518ded2c3f30cca803a29f76da2e80505b166647 -r 3ea8be4161ab0a9ed2e1c17160c9d3d52d41b555 lib/galaxy/webapps/galaxy/api/lda_datasets.py --- /dev/null +++ b/lib/galaxy/webapps/galaxy/api/lda_datasets.py @@ -0,0 +1,38 @@ +""" +API operations on the contents of a dataset from library. +""" +from galaxy import web +from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixinItems + +import logging +log = logging.getLogger( __name__ ) + +class DatasetsController( BaseAPIController, UsesLibraryMixinItems ): + + @web.expose_api + def index( self, trans, **kwd ): + """ + GET /api/libraries/datasets + """ + trans.response.status = 501 + return 'not implemented' + + @web.expose_api + def show( self, trans, id, **kwd ): + """ + GET /api/libraries/datasets/{encoded_dataset_id} + Displays information about and/or content of a dataset identified by the lda ID. + """ + # Get dataset. + try: + dataset = self.get_library_dataset( trans, id = id ) + except Exception, e: + return str( e ) + try: + # Default: return dataset as dict. + rval = dataset.to_dict() + except Exception, e: + rval = "Error in dataset API at listing contents: " + str( e ) + log.error( rval + ": %s" % str(e), exc_info=True ) + trans.response.status = 500 + return rval \ No newline at end of file diff -r 518ded2c3f30cca803a29f76da2e80505b166647 -r 3ea8be4161ab0a9ed2e1c17160c9d3d52d41b555 lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -46,18 +46,19 @@ atexit.register( app.shutdown ) # Create the universe WSGI application webapp = GalaxyWebApplication( app, session_cookie='galaxysession', name='galaxy' ) - # Handle displaying tool help images and README file images contained in repositories installed from the tool shed. - webapp.add_route( '/admin_toolshed/static/images/:repository_id/:image_file', - controller='admin_toolshed', - action='display_image_in_repository', - repository_id=None, - image_file=None ) + + # Add the controllers folder webapp.add_ui_controllers( 'galaxy.webapps.galaxy.controllers', app ) + # Add the api folder + webapp.add_api_controllers( 'galaxy.webapps.galaxy.api', app ) + # Add the api folder VERSION 2 + webapp.add_api_controllers( 'galaxy.webapps.galaxy.api.v2', app ) + # Force /history to go to /root/history -- needed since the tests assume this webapp.add_route( '/history', controller='root', action='history' ) # Force /activate to go to the controller webapp.add_route( '/activate', controller='user', action='activate' ) - # These two routes handle our simple needs at the moment + webapp.add_route( '/async/:tool_id/:data_id/:data_secret', controller='async', action='index', tool_id=None, data_id=None, data_secret=None ) webapp.add_route( '/:controller/:action', action='index' ) webapp.add_route( '/:action', controller='root', action='index' ) @@ -66,8 +67,8 @@ webapp.add_route( '/datasets/:dataset_id/display/{filename:.+?}', controller='dataset', action='display', dataset_id=None, filename=None) webapp.add_route( '/datasets/:dataset_id/:action/:filename', controller='dataset', action='index', dataset_id=None, filename=None) webapp.add_route( '/display_application/:dataset_id/:app_name/:link_name/:user_id/:app_action/:action_param', - controller='dataset', action='display_application', dataset_id=None, user_id=None, - app_name = None, link_name = None, app_action = None, action_param = None ) + controller='dataset', action='display_application', dataset_id=None, user_id=None, + app_name = None, link_name = None, app_action = None, action_param = None ) webapp.add_route( '/u/:username/d/:slug/:filename', controller='dataset', action='display_by_username_and_slug', filename=None ) webapp.add_route( '/u/:username/p/:slug', controller='page', action='display_by_username_and_slug' ) webapp.add_route( '/u/:username/h/:slug', controller='history', action='display_by_username_and_slug' ) @@ -75,22 +76,6 @@ webapp.add_route( '/u/:username/v/:slug', controller='visualization', action='display_by_username_and_slug' ) webapp.add_route( '/search', controller='search', action='index' ) - # Add the web API - webapp.add_api_controllers( 'galaxy.webapps.galaxy.api', app ) - # The /folders section is experimental at this point: - log.debug( "app.config.api_folders: %s" % app.config.api_folders ) - webapp.mapper.resource( 'folder', 'folders', path_prefix='/api' ) - webapp.mapper.resource( 'content', 'contents', - controller='folder_contents', - name_prefix='folder_', - path_prefix='/api/folders/:folder_id', - parent_resources=dict( member_name='folder', collection_name='folders' ) ) - webapp.mapper.resource( 'content', - 'contents', - controller='library_contents', - name_prefix='library_', - path_prefix='/api/libraries/:library_id', - parent_resources=dict( member_name='library', collection_name='libraries' ) ) webapp.mapper.resource( 'content', 'contents', controller='history_contents', @@ -102,10 +87,6 @@ controller="datasets", action="display", conditions=dict(method=["GET"])) - webapp.mapper.resource( 'permission', - 'permissions', - path_prefix='/api/libraries/:library_id', - parent_resources=dict( member_name='library', collection_name='libraries' ) ) webapp.mapper.resource( 'user', 'users', controller='group_users', @@ -127,11 +108,6 @@ _add_item_tags_controller( webapp, name_prefix="workflow_", path_prefix='/api/workflows/:workflow_id' ) - - _add_item_extended_metadata_controller( webapp, - name_prefix="library_dataset_", - path_prefix='/api/libraries/:library_id/contents/:library_content_id' ) - _add_item_annotation_controller( webapp, name_prefix="history_content_", path_prefix='/api/histories/:history_id/contents/:history_content_id' ) @@ -147,31 +123,58 @@ path_prefix='/api/histories/:history_id/contents/:history_content_id' ) webapp.mapper.resource( 'dataset', 'datasets', path_prefix='/api' ) - webapp.mapper.resource_with_deleted( 'library', 'libraries', path_prefix='/api' ) webapp.mapper.resource( 'sample', 'samples', path_prefix='/api' ) webapp.mapper.resource( 'request', 'requests', path_prefix='/api' ) webapp.mapper.resource( 'form', 'forms', path_prefix='/api' ) webapp.mapper.resource( 'request_type', 'request_types', path_prefix='/api' ) webapp.mapper.resource( 'role', 'roles', path_prefix='/api' ) webapp.mapper.resource( 'group', 'groups', path_prefix='/api' ) - webapp.mapper.resource_with_deleted( 'quota', 'quotas', path_prefix='/api' ) webapp.mapper.resource( 'tool', 'tools', path_prefix='/api' ) - webapp.mapper.resource_with_deleted( 'user', 'users', path_prefix='/api' ) webapp.mapper.resource( 'genome', 'genomes', path_prefix='/api' ) webapp.mapper.resource( 'visualization', 'visualizations', path_prefix='/api' ) webapp.mapper.resource( 'workflow', 'workflows', path_prefix='/api' ) - webapp.mapper.resource_with_deleted( 'history', 'histories', path_prefix='/api' ) webapp.mapper.resource( 'configuration', 'configuration', path_prefix='/api' ) #webapp.mapper.connect( 'run_workflow', '/api/workflow/{workflow_id}/library/{library_id}', controller='workflows', action='run', workflow_id=None, library_id=None, conditions=dict(method=["GET"]) ) webapp.mapper.resource( 'search', 'search', path_prefix='/api' ) + webapp.mapper.resource_with_deleted( 'quota', 'quotas', path_prefix='/api' ) + webapp.mapper.resource_with_deleted( 'history', 'histories', path_prefix='/api' ) + webapp.mapper.resource_with_deleted( 'user', 'users', path_prefix='/api' ) + + # ======================= + # ===== LIBRARY API ===== + # ======================= + + webapp.mapper.connect( 'show_lda_item', '/api/libraries/datasets/:id', controller='lda_datasets', action='show', conditions=dict(method=["GET"]) ) + + webapp.mapper.resource_with_deleted( 'library', 'libraries', path_prefix='/api' ) + webapp.mapper.resource( 'folder', 'folders', path_prefix='/api' ) + + webapp.mapper.resource( 'content', 'contents', + controller='folder_contents', + name_prefix='folder_', + path_prefix='/api/folders/:folder_id', + parent_resources=dict( member_name='folder', collection_name='folders' ) ) + webapp.mapper.resource( 'content', + 'contents', + controller='library_contents', + name_prefix='library_', + path_prefix='/api/libraries/:library_id', + parent_resources=dict( member_name='library', collection_name='libraries' ) ) + webapp.mapper.resource( 'permission', + 'permissions', + path_prefix='/api/libraries/:library_id', + parent_resources=dict( member_name='library', collection_name='libraries' ) ) + _add_item_extended_metadata_controller( webapp, + name_prefix="library_dataset_", + path_prefix='/api/libraries/:library_id/contents/:library_content_id' ) + + # add as a non-ATOM API call to support the notion of a 'current/working' history unique to the history resource - webapp.mapper.connect( "set_as_current", "/api/histories/{id}/set_as_current", - controller="histories", action="set_as_current", conditions=dict( method=["POST"] ) ) + webapp.mapper.connect( "set_as_current", "/api/histories/{id}/set_as_current", controller="histories", action="set_as_current", conditions=dict( method=["POST"] ) ) # visualizations registry generic template renderer - webapp.add_route( '/visualization/show/:visualization_name', - controller='visualization', action='render', visualization_name=None ) + webapp.add_route( '/visualization/show/:visualization_name', controller='visualization', action='render', visualization_name=None ) # "POST /api/workflows/import" => ``workflows.import_workflow()``. # Defines a named route "import_workflow". @@ -179,6 +182,15 @@ webapp.mapper.connect("workflow_dict", '/api/workflows/{workflow_id}/download', controller='workflows', action='workflow_dict', conditions=dict(method=['GET'])) # Preserve the following download route for now for dependent applications -- deprecate at some point webapp.mapper.connect("workflow_dict", '/api/workflows/download/{workflow_id}', controller='workflows', action='workflow_dict', conditions=dict(method=['GET'])) + + + # Handle displaying tool help images and README file images contained in repositories installed from the tool shed. + webapp.add_route( '/admin_toolshed/static/images/:repository_id/:image_file', + controller='admin_toolshed', + action='display_image_in_repository', + repository_id=None, + image_file=None ) + # Galaxy API for tool shed features. webapp.mapper.resource( 'tool_shed_repository', 'tool_shed_repositories', @@ -191,6 +203,7 @@ path_prefix='/api', new={ 'install_repository_revision' : 'POST' }, parent_resources=dict( member_name='tool_shed_repository', collection_name='tool_shed_repositories' ) ) + # Connect logger from app if app.trace_logger: webapp.trace_logger = app.trace_logger diff -r 518ded2c3f30cca803a29f76da2e80505b166647 -r 3ea8be4161ab0a9ed2e1c17160c9d3d52d41b555 lib/galaxy/webapps/galaxy/controllers/library.py --- a/lib/galaxy/webapps/galaxy/controllers/library.py +++ b/lib/galaxy/webapps/galaxy/controllers/library.py @@ -80,18 +80,13 @@ @web.expose def list( self, trans, **kwd ): params = util.Params( kwd ) - # define app configuration for generic mako template app = { 'jscript' : "galaxy.library" } - # fill template return trans.fill_template('galaxy.panels.mako', config = {'app' : app}) - - -# return trans.fill_template( "/library/list.mako") - + @web.expose def index( self, trans, **kwd ): params = util.Params( kwd ) diff -r 518ded2c3f30cca803a29f76da2e80505b166647 -r 3ea8be4161ab0a9ed2e1c17160c9d3d52d41b555 static/scripts/galaxy.library.js --- a/static/scripts/galaxy.library.js +++ b/static/scripts/galaxy.library.js @@ -7,7 +7,7 @@ var library_router = null; // dependencies -define(["galaxy.modal", "galaxy.master", "galaxy.library.router"], function(mod_modal, mod_master, router) { +define(["galaxy.modal", "galaxy.master"], function(mod_modal, mod_master) { // MMMMMMMMMMMMMMM // === Models ==== @@ -15,40 +15,65 @@ // LIBRARY var Library = Backbone.Model.extend({ - urlRoot: '/api/libraries', - }); + urlRoot: '/api/libraries' + }); // LIBRARIES var Libraries = Backbone.Collection.extend({ url: '/api/libraries', model: Library - }); + }); // ITEM var Item = Backbone.Model.extend({ + urlRoot : '/api/libraries/datasets' }) // FOLDER var Folder = Backbone.Collection.extend({ model: Item - }) + }) - // Container for folder contents (folders, items and metadata). + // CONTAINER for folder contents (folders, items and metadata). var FolderContainer = Backbone.Model.extend({ - defaults: { - folder: new Folder(), - full_path: "unknown", - urlRoot: "/api/folders/", - id: "unknown" - }, - parse: function(obj) { - this.full_path = obj[0].full_path; - // this.folder.reset(obj[1].folder_contents); + defaults : { + folder : new Folder(), + full_path : "unknown", + urlRoot : "/api/folders/", + id : "unknown" + }, + parse : function(obj) { + this.full_path = obj[0].full_path; // update the inner collection this.get("folder").reset(obj[1].folder_contents); return obj; + } + }) + + // HISTORY ITEM + var HistoryItem = Backbone.Model.extend({ + urlRoot : '/api/histories/' + }); + + // HISTORY + var GalaxyHistory = Backbone.Model.extend({ + url : '/api/histories/' + }); + + // HISTORIES + var GalaxyHistories = Backbone.Collection.extend({ + url : '/api/histories', + model : GalaxyHistory + }); + + //ROUTER + var LibraryRouter = Backbone.Router.extend({ + routes: { + "" : "libraries", + "folders/:id" : "folder_content" } - }) + }); + // MMMMMMMMMMMMMM // === Views ==== @@ -56,67 +81,155 @@ // galaxy folder var FolderContentView = Backbone.View.extend({ - el : '#center', + //main element + el : '#center', // initialize initialize : function(){ - // view = this; - //set up the library router - // this.set_up_router(); + this.folders = []; + }, + // set up + templateFolder : function (){ + var tmpl_array = []; - //render - // this.render(); - }, - // set up router - set_up_router : function(){ - if (library_router === null){ - library_router = new router.LibraryRouter(); - Backbone.history.start(); - } + tmpl_array.push('<a href="#">Libraries</a> | '); + tmpl_array.push('<% _.each(path, function(path_item) { %>'); //breadcrumb + tmpl_array.push('<% if (path_item[0] != id) { %>'); + tmpl_array.push('<a href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a> |'); + tmpl_array.push('<% } else { %>'); + tmpl_array.push('<%- path_item[1] %>'); + tmpl_array.push('<% } %>'); + tmpl_array.push('<% }); %>'); + tmpl_array.push('<table class="table table-hover table-condensed">'); + tmpl_array.push(' <thead>'); + tmpl_array.push(' <th style="text-align: center; width: 20px; "><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th>'); + tmpl_array.push(' <th>name</th>'); + tmpl_array.push(' <th>type</th>'); + tmpl_array.push(' </thead>'); + tmpl_array.push(' <tbody>'); + tmpl_array.push(' <% _.each(items, function(content_item) { %>'); + tmpl_array.push(' <tr class="folder_row" id="<%- content_item.id %>">'); + tmpl_array.push(' <td style="text-align: center; "><input style="margin: 0;" type="checkbox"></td>'); + tmpl_array.push(' <% if (content_item.get("type") === "folder") { %>'); //folder + tmpl_array.push(' <td><a href="#/folders/<%- content_item.id %>"><%- content_item.get("name") %></a>'); + tmpl_array.push(' <% if (content_item.get("item_count") === 0) { %>'); //empty folder + tmpl_array.push(' <span class="muted">(empty folder)</span>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' </td>'); + tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <td><a class="library-dataset" href="#"><%- content_item.get("name") %></a></td>'); //dataset + tmpl_array.push(' <% } %> '); + tmpl_array.push(' <td><%= _.escape(content_item.get("type")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push(' '); + tmpl_array.push(' '); + tmpl_array.push(' '); + tmpl_array.push(' </tbody>'); + tmpl_array.push('</table>'); + + return tmpl_array.join(''); }, - template_folder : function (){ - tmpl_array = []; + templateDatasetModal : function(){ + var tmpl_array = []; - tmpl_array.push('<% _.each(path, function(path_item) { %>'); - tmpl_array.push('<% if (path_item[0] != id) { %>'); - tmpl_array.push('<a href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a> |'); - tmpl_array.push('<% } else { %>'); - tmpl_array.push('<%- path_item[1] %>'); - tmpl_array.push('<% } %>'); - tmpl_array.push('<% }); %>'); - tmpl_array.push('<table class="table table-hover table-condensed">'); - tmpl_array.push(' <thead>'); - tmpl_array.push(' <th style="text-align: center; width: 20px; "><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th>'); - tmpl_array.push(' <th>name</th>'); - tmpl_array.push(' <th>type</th>'); - tmpl_array.push(' </thead>'); - tmpl_array.push(' <tbody>'); - tmpl_array.push(' <% _.each(items, function(content_item) { %>'); - tmpl_array.push(' <tr class="folder_row" id="<%- content_item.id %>">'); - tmpl_array.push(' <td style="text-align: center; "><input style="margin: 0;" type="checkbox"></td>'); - tmpl_array.push(' <% if (content_item.get("type") === "folder") { %>'); - tmpl_array.push(' <td><a href="#/folders/<%- content_item.id %>"><%- content_item.get("name") %></a>'); - tmpl_array.push(' <% if (content_item.get("item_count") === 0) { %>'); - tmpl_array.push(' <span class="muted">(empty folder)</span>'); - tmpl_array.push(' <% } %>'); - tmpl_array.push(' </td>'); - tmpl_array.push(' <% } else { %>'); - tmpl_array.push(' <td><%- content_item.get("name") %></td>'); - tmpl_array.push(' <% } %> '); - tmpl_array.push(' <td><%= _.escape(content_item.get("type")) %></td>'); - tmpl_array.push(' </tr>'); - tmpl_array.push(' <% }); %>'); - tmpl_array.push(' '); - tmpl_array.push(' '); - tmpl_array.push(' '); - tmpl_array.push(' </tbody>'); - tmpl_array.push('</table>'); + tmpl_array.push('<div id="dataset_info_modal">'); + tmpl_array.push(' <table class="table table-striped table-condensed">'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Name</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("name")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Data type</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("data_type")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Genome build</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("genome_build")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <th scope="row">Size</th>'); + tmpl_array.push(' <td><%= _.escape(size) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Date uploaded</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("date_uploaded")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Uploaded by</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("uploaded_by")) %></td>'); + tmpl_array.push(' </tr>'); + // tmpl_array.push(' </table>'); + // tmpl_array.push(' <hr/>'); + // tmpl_array.push(' <table class="table table-striped">'); + tmpl_array.push(' <tr scope="row">'); + tmpl_array.push(' <th scope="row">Data Lines</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_data_lines")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <th scope="row">Comment Lines</th>'); + tmpl_array.push(' <% if (item.get("metadata_comment_lines") === "") { %>'); //folder + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_comment_lines")) %></td>'); + tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <td scope="row">unknown</td>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Number of Columns</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_columns")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Column Types</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_column_types")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Miscellaneous information</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("misc_blurb")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' '); + tmpl_array.push(' </table>'); + // tmpl_array.push(' <hr/>'); + tmpl_array.push(' <pre class="peek">'); + tmpl_array.push(' </pre>'); + tmpl_array.push('</div>'); - return tmpl_array.join(''); - }, - events: { - 'click #select-all-checkboxes' : 'selectAll', - 'click .folder_row' : 'selectClicked' - }, + return tmpl_array.join(''); + }, + templateHistorySelectInModal : function(){ + var tmpl_array = []; + + tmpl_array.push('<span id="history_modal_footer" style="width:60%;">'); + tmpl_array.push('<select name="history_import" style="width:60%; margin-left: 2em; "> '); + tmpl_array.push(' <% _.each(histories, function(history) { %>'); //history select box + tmpl_array.push(' <option value="<%= _.escape(history.get("id")) %>"><%= _.escape(history.get("name")) %></option>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push('</span>'); + tmpl_array.push('</div>'); + tmpl_array.push(''); + + return tmpl_array.join(''); + }, + // to string + size_to_string : function (size) + { + // identify unit + var unit = ""; + if (size >= 100000000000) { size = size / 100000000000; unit = "TB"; } else + if (size >= 100000000) { size = size / 100000000; unit = "GB"; } else + if (size >= 100000) { size = size / 100000; unit = "MB"; } else + if (size >= 100) { size = size / 100; unit = "KB"; } else + { size = size * 10; unit = "b"; } + // return formatted string + return (Math.round(size) / 10) + unit; + }, + events: { + 'click #select-all-checkboxes' : 'selectAll', + 'click .folder_row' : 'selectClicked', + 'click .library-dataset' : 'showDatasetDetails' + }, + //self modal + modal : null, + //loaded folders + folders : null, + //render the view render: function (options) { var that = this; @@ -125,80 +238,131 @@ folderContainer.fetch({ success: function (container) { - // folderContainer.attributes.folder = container.attributes.folder; - var template = _.template(that.template_folder(), {path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id}); - that.$el.html(template); + // folderContainer.attributes.folder = container.attributes.folder; + var template = _.template(that.templateFolder(), {path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id}); + that.$el.html(template); } - }) - }, - selectAll : function (ev) { - var selected = ev.target.checked; - // Iterate each checkbox - $(':checkbox').each(function () { this.checked = selected; }); - }, - selectClicked : function (ev) { - var checkbox = $("#" + ev.target.parentElement.id).find(':checkbox') - if (checkbox[0] != undefined) { - if (checkbox[0].checked){ - checkbox[0].checked = ''; - } else { - checkbox[0].checked = 'selected'; - } + }) + }, + //show modal with dataset info + showDatasetDetails : function(e){ + // prevent default + e.preventDefault(); + + //TODO check whether we already have the data + + //load the ID of the row + var id = $(e.target).parent().parent().attr('id'); + + //create new item + var item = new Item(); + var histories = new GalaxyHistories(); + item.id = id; + var self = this; + //fetch the dataset info + item.fetch({ + success: function (item) { + //fetch user histories for import + histories.fetch({ + success: function (histories){self.modalFetchSuccess(item, histories)} + }); + } + }); + }, + + modalFetchSuccess : function(item, histories){ + var histories_modal = this.templateHistorySelectInModal(); + var size = this.size_to_string(item.get('file_size')); + var template = _.template(this.templateDatasetModal(), { item : item, size : size }); + // make modal + var self = this; + this.modal = new mod_modal.GalaxyModal({ + title : 'Dataset Details', + body : template, + buttons : { + 'Import' : function() {self.importIntoHistory()}, + // 'Notify' : function() {self.modal.showNotification("TEST")}, + 'Close' : function() {self.modal.hide()} + } + }); + $(".peek").html(item.get("peek")); + // this.modal.hideButton('Import'); + // $(this.modal.elMain).find('.modal-footer').prepend("<div>BUBUBUBU" + "</div>"); + var history_footer_tmpl = _.template(this.templateHistorySelectInModal(), {histories : histories.models}); + + $(this.modal.elMain).find('.buttons').prepend(history_footer_tmpl); + + // show the prepared modal + this.modal.show(); + }, + + importIntoHistory : function(){ + var history_id = 'a0c15f4d91084599'; + var library_dataset_id = '03501d7626bd192f'; + + var historyItem = new HistoryItem(); + var self = this; + historyItem.url = historyItem.urlRoot + history_id + '/contents'; + console.log(historyItem); + historyItem.save({ content : library_dataset_id, source : 'library' }, { success : function(){ + self.modal.showNotification('Dataset imported', 3000, '#e1f4e0', '#32a427'); + } + }); + }, + + selectAll : function (ev) { + var selected = ev.target.checked; + // Iterate each checkbox + $(':checkbox').each(function () { this.checked = selected; }); + }, + selectClicked : function (ev) { + var checkbox = $("#" + ev.target.parentElement.id).find(':checkbox') + if (checkbox[0] != undefined) { + if (checkbox[0].checked){ + checkbox[0].checked = ''; + } else { + checkbox[0].checked = 'selected'; + } + } } - } -}); + }); // galaxy library var GalaxyLibraryview = Backbone.View.extend({ el: '#center', - events: { + events: { 'click #create_new_library_btn' : 'show_library_modal' - }, + }, - // initialize initialize : function(){ - // view = this; - //set up the libray router - // this.set_up_router(); - - //render - // this.render(); }, - // set up router - set_up_router : function(){ - if (library_router === null){ - library_router = new router.LibraryRouter(); - Backbone.history.start(); - } - }, - // template template_library_list : function (){ tmpl_array = []; tmpl_array.push(''); tmpl_array.push('<h1>Welcome to the data libraries</h1>'); - tmpl_array.push('<a href="" id="create_new_library_btn" class="btn btn-primary">New Library</a>'); + tmpl_array.push('<a href="" id="create_new_library_btn" class="btn btn-primary icon-file ">New Library</a>'); tmpl_array.push('<table class="table table-striped">'); tmpl_array.push(' <thead>'); tmpl_array.push(' <th>name</th>'); tmpl_array.push(' <th>description</th>'); tmpl_array.push(' <th>synopsis</th> '); tmpl_array.push(' <th>model type</th> '); - tmpl_array.push(' <th>id</th> '); + // tmpl_array.push(' <th>id</th> '); tmpl_array.push(' </thead>'); tmpl_array.push(' <tbody>'); tmpl_array.push(' <% _.each(libraries, function(library) { %>'); - tmpl_array.push(' <tr>'); - tmpl_array.push(' <td><a href="#/folders/<%- library.id %>"><%- library.get("name") %></a></td>'); - tmpl_array.push(' <td><%= _.escape(library.get("description")) %></td>'); - tmpl_array.push(' <td><%= _.escape(library.get("synopsis")) %></td>'); - tmpl_array.push(' <td><%= _.escape(library.get("model_class")) %></td>'); - tmpl_array.push(' <td><a href="#/folders/<%- library.id %>"><%= _.escape(library.get("id")) %></a></td>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <td><a href="#/folders/<%- library.id %>"><%- library.get("name") %></a></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("description")) %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("synopsis")) %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("model_class")) %></td>'); + // tmpl_array.push(' <td><a href="#/folders/<%- library.id %>"><%= _.escape(library.get("id")) %></a></td>'); tmpl_array.push(' </tr>'); tmpl_array.push(' <% }); %>'); tmpl_array.push(''); @@ -219,18 +383,17 @@ // } libraries.fetch({ success: function (libraries) { - var template = _.template(that.template_library_list(), {libraries: libraries.models}); + var template = _.template(that.template_library_list(), { libraries : libraries.models }); that.$el.html(template); - } - }) + } + }) }, // own modal modal : null, // show/hide create library modal - show_library_modal : function (e) - { + show_library_modal : function (e){ // prevent default e.preventDefault(); @@ -255,15 +418,21 @@ create_new_library_event: function(){ var libraryDetails = this.serialize_new_library(); var library = new Library(); + var self = this; library.save(libraryDetails, { success: function (library) { - - library_router.navigate('', {trigger: true}) - } - }); - // console.log(libraryDetails); + self.modal.hide(); + self.clear_library_modal(); + self.render(); + } + }); return false; }, + clear_library_modal : function(){ + $("input[name='Name']").val(''); + $("input[name='Description']").val(''); + $("input[name='Synopsis']").val(''); + }, serialize_new_library : function(){ return { name: $("input[name='Name']").val(), @@ -279,7 +448,6 @@ tmpl_array.push('<form>'); tmpl_array.push('<input type="text" name="Name" value="" placeholder="Name">'); tmpl_array.push('<input type="text" name="Description" value="" placeholder="Description">'); - // tmpl_array.push('<label>Synopsis</label>'); tmpl_array.push('<input type="text" name="Synopsis" value="" placeholder="Synopsis">'); tmpl_array.push(''); tmpl_array.push('</form>'); @@ -300,18 +468,18 @@ folderContentView = new FolderContentView(); galaxyLibraryview = new GalaxyLibraryview(); - library_router = new router.LibraryRouter(); - + library_router = new LibraryRouter(); + library_router.on('route:libraries', function() { // render libraries list galaxyLibraryview.render(); - }) - + }) + library_router.on('route:folder_content', function(id) { // render folder's contents folderContentView.render({id: id}); - - }) + + }) // library_router.on('route:show_library_modal', function() { // // render folder's contents @@ -319,10 +487,10 @@ // }) - Backbone.history.start(); +Backbone.history.start(); - return this - } +return this +} }); // return diff -r 518ded2c3f30cca803a29f76da2e80505b166647 -r 3ea8be4161ab0a9ed2e1c17160c9d3d52d41b555 static/scripts/galaxy.modal.js --- a/static/scripts/galaxy.modal.js +++ b/static/scripts/galaxy.modal.js @@ -82,6 +82,7 @@ this.$footer = (this.$el).find('.modal-footer'); this.$buttons = (this.$el).find('.buttons'); this.$backdrop = (this.$el).find('.modal-backdrop'); + this.$notification = (this.$el).find('.notification-modal'); // append body this.$body.html(options.body); @@ -122,6 +123,35 @@ this.$buttons.find('#' + String(name).toLowerCase()).prop('disabled', true); }, + // hide buttons + hideButton: function(name) { + this.$buttons.find('#' + String(name).toLowerCase()).hide(); + }, + // show buttons + showButton: function(name) { + this.$buttons.find('#' + String(name).toLowerCase()).show(); + }, + + // show notification + showNotification : function(message, duration, bgColor, txtColor) { + // defaults + var duration = typeof duration !== 'undefined' ? duration : 1500; + var bgColor = typeof bgColor !== 'undefined' ? bgColor : "#F4E0E1"; + var txtColor = typeof txtColor !== 'undefined' ? txtColor : "#A42732"; + + var HTMLmessage = "<div class='notification-message' style='text-align:center; line-height:16px; '> " + message + " </div>"; + this.$notification.html("<div id='notification-bar' style='display:none; float: right; height: 16px; width:100%; background-color: " + bgColor + "; z-index: 100; color: " + txtColor + ";border-bottom: 1px solid " + txtColor + ";'>" + HTMLmessage + "</div>"); + + var self = this; + /*animate the bar*/ + $('#notification-bar').slideDown(function() { + setTimeout(function() { + $('#notification-bar').slideUp(function() {self.$notification.html('');}); + }, duration); + }); + + }, + // returns scroll top for body element scrollTop: function() { @@ -137,10 +167,11 @@ return '<div class="modal">' + '<div class="modal-backdrop fade in" style="z-index: -1;"></div>' + '<div class="modal-dialog">' + - '<div class="modal-content">' + + '<div class="modal-content"">' + '<div class="modal-header">' + '<button type="button" class="close" style="display: none;">×</button>' + '<h4 class="title">' + title + '</h4>' + + '<span class="notification-modal"></span>' + '</div>' + '<div class="modal-body"></div>' + '<div class="modal-footer">' + https://bitbucket.org/galaxy/galaxy-central/commits/2bb7df095a5a/ Changeset: 2bb7df095a5a Branch: data_library User: martenson Date: 2013-12-03 22:43:06 Summary: slight refactoring and bugfix of db_next_hid generator Affected #: 1 file diff -r 3ea8be4161ab0a9ed2e1c17160c9d3d52d41b555 -r 2bb7df095a5a23e3dce2e02fb963a30ff3b7d377 lib/galaxy/model/mapping.py --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -1988,8 +1988,18 @@ table = self.table trans = conn.begin() try: - next_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() - table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid + 1 ) ) + +# log.debug("XXXXXXXXXXXXXXX NEW GENERATOOOOOR") +# next_hid = table.update() \ +# .returning( table.c.hid_counter ) \ +# .where( table.c.id == self.id ) \ +# .values( hid_counter = hid_counter + 1 ) +# log.debug('XXXXXXXXXXXXXXXXXXXXXXXXXXX NEXT HID: ' + str(next_hid)) + + + current_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() + next_hid = current_hid + 1 + table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid ) ) trans.commit() return next_hid except: https://bitbucket.org/galaxy/galaxy-central/commits/4e53d7b759d1/ Changeset: 4e53d7b759d1 Branch: data_library User: martenson Date: 2013-12-03 22:43:25 Summary: added dropdown menu plugin to bootstrap Affected #: 1 file diff -r 2bb7df095a5a23e3dce2e02fb963a30ff3b7d377 -r 4e53d7b759d1999758a30611e5d897e3e1d20782 static/scripts/libs/bootstrap.js --- a/static/scripts/libs/bootstrap.js +++ b/static/scripts/libs/bootstrap.js @@ -575,3 +575,158 @@ } }(window.jQuery); + +/* ======================================================================== + * Bootstrap: dropdown.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#dropdowns + * ======================================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle=dropdown]' + var Dropdown = function (element) { + var $el = $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we we use a backdrop because click events don't delegate + $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus) + } + + $parent.trigger(e = $.Event('show.bs.dropdown')) + + if (e.isDefaultPrevented()) return + + $parent + .toggleClass('open') + .trigger('shown.bs.dropdown') + + $this.focus() + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27)/.test(e.keyCode)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) { + if (e.which == 27) $parent.find(toggle).focus() + return $this.click() + } + + var $items = $('[role=menu] li:not(.divider):visible a', $parent) + + if (!$items.length) return + + var index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index=0 + + $items.eq(index).focus() + } + + function clearMenus() { + $(backdrop).remove() + $(toggle).each(function (e) { + var $parent = getParent($(this)) + if (!$parent.hasClass('open')) return + $parent.trigger(e = $.Event('hide.bs.dropdown')) + if (e.isDefaultPrevented()) return + $parent.removeClass('open').trigger('hidden.bs.dropdown') + }) + } + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + var old = $.fn.dropdown + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('dropdown') + + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== + + $(document) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api' , toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + +}(window.jQuery); \ No newline at end of file https://bitbucket.org/galaxy/galaxy-central/commits/44399512777b/ Changeset: 44399512777b Branch: data_library User: martenson Date: 2013-12-03 22:44:22 Summary: library work, galaxy modal changes, api v2 preparation, API tweaks, API for dataset downloading implementation Affected #: 9 files diff -r 4e53d7b759d1999758a30611e5d897e3e1d20782 -r 44399512777bdefb39ea3c99cc04b3f7947c5d33 lib/galaxy/model/__init__.py --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -1768,7 +1768,7 @@ class Library( object, Dictifiable ): permitted_actions = get_permitted_actions( filter='LIBRARY' ) dict_collection_visible_keys = ( 'id', 'name' ) - dict_element_visible_keys = ( 'id', 'deleted', 'name', 'description', 'synopsis' ) + dict_element_visible_keys = ( 'id', 'deleted', 'name', 'description', 'synopsis', 'root_folder_id' ) def __init__( self, name=None, description=None, synopsis=None, root_folder=None ): self.name = name or "Unnamed library" self.description = description diff -r 4e53d7b759d1999758a30611e5d897e3e1d20782 -r 44399512777bdefb39ea3c99cc04b3f7947c5d33 lib/galaxy/webapps/galaxy/api/datasets.py --- a/lib/galaxy/webapps/galaxy/api/datasets.py +++ b/lib/galaxy/webapps/galaxy/api/datasets.py @@ -1,5 +1,5 @@ """ -API operations on the contents of a dataset. +API operations on the contents of a history dataset. """ from galaxy import web from galaxy.visualization.data_providers.genome import FeatureLocationIndexDataProvider diff -r 4e53d7b759d1999758a30611e5d897e3e1d20782 -r 44399512777bdefb39ea3c99cc04b3f7947c5d33 lib/galaxy/webapps/galaxy/api/folder_contents.py --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -42,19 +42,34 @@ GET /api/folders/{encoded_folder_id}/contents Displays a collection (list) of a folder's contents (files and folders). Encoded folder ID is prepended with 'F' if it is a folder as opposed to a data set which does not have it. + Full path is provided as a separate object in response providing data for breadcrumb path building. """ folder_container = [] current_user_roles = trans.get_current_user_roles() + +# log.debug( 'XXXXXXXXXXXXXXXXXXXXX GOT id: ' + folder_id ) +# log.debug( 'XXXXXXXXXXXXXXXXXXXXX decode id: ' + folder_id[1:] ) +# log.debug( 'XXXXXXXXXXXXXXXXXXXXX call decode: ' + str(trans.security.decode_id( folder_id[1:] )) ) - try: - decoded_folder_id = trans.security.decode_id( folder_id[-16:] ) - except TypeError: - trans.response.status = 400 - return "Malformed folder id ( %s ) specified, unable to decode." % str( folder_id ) +# try: +# decoded_folder_id = trans.security.decode_id( folder_id[-16:] ) +# except TypeError: +# trans.response.status = 400 +# return "Malformed folder id ( %s ) specified, unable to decode." % str( folder_id ) +# log.debug( 'XXXXXXXXXXXXXXXXXXXXX decoded id: ' + decoded_folder_id ) + + if ( folder_id.startswith( 'F' ) ): + try: + decoded_folder_id = trans.security.decode_id( folder_id[1:] ) + except TypeError: + trans.response.status = 400 + return "Malformed folder id ( %s ) specified, unable to decode." % str( folder_id ) try: folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( decoded_folder_id ) - parent_library = folder.parent_library +# log.debug( 'XXXXXXXXXXXXXXXXXXXXX FOLDER id: ' + str(folder.id) ) +# log.debug( 'XXXXXXXXXXXXXXXXXXXXX FOLDER name: ' + str(folder.name) ) +# parent_library = folder.parent_library except: folder = None log.error( "FolderContentsController.index: Unable to retrieve folder with ID: %s" % folder_id ) @@ -77,13 +92,17 @@ path_to_root = [] # We are almost in root if folder.parent_id is None: - log.debug( "XXXXXXXXXXXXXXXXXXXXXXX ALMOST ROOT FOLDER! ADDING: " + str( folder.name ) ) + +# log.debug( "XXXXXXXXXXXXXXXXXXXXXXX ALMOST ROOT FOLDER! ADDING ID: " + str( folder.id ) ) +# log.debug( "XXXXXXXXXXXXXXXXXXXXXXX ALMOST ROOT FOLDER! ADDING NAME: " + str( folder.name ) ) path_to_root.append( ( 'F' + trans.security.encode_id( folder.id ), folder.name ) ) # upper_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( folder.parent_library.id ) # path_to_root.append( ( upper_folder.id, upper_folder.name ) ) else: # We add the current folder and traverse up one folder. - log.debug( "XXXXXXXXXXXXXXXXXXXXXXX ADDING THIS FOLDER AND TRAVERSING UP: " + str( folder.name ) ) +# log.debug( "XXXXXXXXXXXXXXXXXXXXXXX folder.parent_id " + str( folder.parent_id ) ) +# log.debug( "XXXXXXXXXXXXXXXXXXXXXXX ALMOST ROOT FOLDER! ADDING ID: " + str( folder.id ) ) +# log.debug( "XXXXXXXXXXXXXXXXXXXXXXX ADDING THIS FOLDER AND TRAVERSING UP: " + str( folder.name ) ) path_to_root.append( ( 'F' + trans.security.encode_id( folder.id ), folder.name ) ) upper_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( folder.parent_id ) path_to_root.extend( build_path( upper_folder ) ) diff -r 4e53d7b759d1999758a30611e5d897e3e1d20782 -r 44399512777bdefb39ea3c99cc04b3f7947c5d33 lib/galaxy/webapps/galaxy/api/lda_datasets.py --- a/lib/galaxy/webapps/galaxy/api/lda_datasets.py +++ b/lib/galaxy/webapps/galaxy/api/lda_datasets.py @@ -1,27 +1,69 @@ """ -API operations on the contents of a dataset from library. +API operations on the dataset from library. """ -from galaxy import web +import glob +import logging +import operator +import os +import os.path +import string +import sys +import tarfile +import tempfile +import urllib +import urllib2 +import zipfile +from galaxy.security import Action +from galaxy import util, web +from galaxy.util.streamball import StreamBall from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixinItems import logging log = logging.getLogger( __name__ ) +# Test for available compression types +# tmpd = tempfile.mkdtemp() +# comptypes = [] +# for comptype in ( 'gz', 'bz2' ): +# tmpf = os.path.join( tmpd, 'compression_test.tar.' + comptype ) +# try: +# archive = tarfile.open( tmpf, 'w:' + comptype ) +# archive.close() +# comptypes.append( comptype ) +# except tarfile.CompressionError: +# log.exception( "Compression error when testing %s compression. This option will be disabled for library downloads." % comptype ) +# try: +# os.unlink( tmpf ) +# except OSError: +# pass +ziptype = '32' +# tmpf = os.path.join( tmpd, 'compression_test.zip' ) +# try: +# archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED, True ) +# archive.close() +# comptypes.append( 'zip' ) +# ziptype = '64' +# except RuntimeError: +# log.exception( "Compression error when testing zip compression. This option will be disabled for library downloads." ) +# except (TypeError, zipfile.LargeZipFile): +# # ZIP64 is only in Python2.5+. Remove TypeError when 2.4 support is dropped +# log.warning( 'Max zip file size is 2GB, ZIP64 not supported' ) +# comptypes.append( 'zip' ) +# try: +# os.unlink( tmpf ) +# except OSError: +# pass +# os.rmdir( tmpd ) + + + class DatasetsController( BaseAPIController, UsesLibraryMixinItems ): @web.expose_api - def index( self, trans, **kwd ): - """ - GET /api/libraries/datasets - """ - trans.response.status = 501 - return 'not implemented' - - @web.expose_api def show( self, trans, id, **kwd ): """ GET /api/libraries/datasets/{encoded_dataset_id} - Displays information about and/or content of a dataset identified by the lda ID. + Displays information about the dataset identified by the lda ID. """ # Get dataset. try: @@ -35,4 +77,165 @@ rval = "Error in dataset API at listing contents: " + str( e ) log.error( rval + ": %s" % str(e), exc_info=True ) trans.response.status = 500 - return rval \ No newline at end of file + + rval['id'] = trans.security.encode_id(rval['id']); + rval['ldda_id'] = trans.security.encode_id(rval['ldda_id']); + rval['folder_id'] = 'f' + trans.security.encode_id(rval['folder_id']) + + return rval + + @web.expose + def download( self, trans, format, **kwd ): + """ + POST /api/libraries/datasets/download/{format} + POST data: ldda_ids = [] + Downloads dataset(s) in the requested format. + """ + lddas = [] +# is_admin = trans.user_is_admin() +# current_user_roles = trans.get_current_user_roles() + + datasets_to_download = kwd['ldda_ids%5B%5D'] + + if ( datasets_to_download != None ): + datasets_to_download = util.listify( datasets_to_download ) + for dataset_id in datasets_to_download: + try: + ldda = trans.sa_session.query( trans.app.model.LibraryDatasetDatasetAssociation ).get( trans.security.decode_id( dataset_id ) ) + assert not ldda.dataset.purged + lddas.append( ldda ) + except: + ldda = None + message += "Invalid library dataset id (%s) specified. " % str( dataset_id ) + + if format in [ 'zip','tgz','tbz' ]: + error = False + killme = string.punctuation + string.whitespace + trantab = string.maketrans(killme,'_'*len(killme)) + try: + outext = 'zip' + if format == 'zip': + # Can't use mkstemp - the file must not exist first + tmpd = tempfile.mkdtemp() + util.umask_fix_perms( tmpd, trans.app.config.umask, 0777, self.app.config.gid ) + tmpf = os.path.join( tmpd, 'library_download.' + format ) + if ziptype == '64' and trans.app.config.upstream_gzip: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_STORED, True ) + elif ziptype == '64': + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED, True ) + elif trans.app.config.upstream_gzip: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_STORED ) + else: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED ) + archive.add = lambda x, y: archive.write( x, y.encode('CP437') ) + elif format == 'tgz': + if trans.app.config.upstream_gzip: + archive = StreamBall( 'w|' ) + outext = 'tar' + else: + archive = StreamBall( 'w|gz' ) + outext = 'tgz' + elif format == 'tbz': + archive = StreamBall( 'w|bz2' ) + outext = 'tbz2' + except ( OSError, zipfile.BadZipfile ): + error = True + log.exception( "Unable to create archive for download" ) + message = "Unable to create archive for download, please report this error" + status = 'error' + except: + error = True + log.exception( "Unexpected error %s in create archive for download" % sys.exc_info()[0] ) + message = "Unable to create archive for download, please report - %s" % sys.exc_info()[0] + status = 'error' + if not error: + composite_extensions = trans.app.datatypes_registry.get_composite_extensions() + seen = [] + for ldda in lddas: + if ldda.dataset.state in [ 'new', 'upload', 'queued', 'running', 'empty', 'discarded' ]: + continue + ext = ldda.extension + is_composite = ext in composite_extensions + path = "" + parent_folder = ldda.library_dataset.folder + while parent_folder is not None: + # Exclude the now-hidden "root folder" + if parent_folder.parent is None: + path = os.path.join( parent_folder.library_root[0].name, path ) + break + path = os.path.join( parent_folder.name, path ) + parent_folder = parent_folder.parent + path += ldda.name + while path in seen: + path += '_' + seen.append( path ) + zpath = os.path.split(path)[-1] # comes as base_name/fname + outfname,zpathext = os.path.splitext(zpath) + if is_composite: + # need to add all the components from the extra_files_path to the zip + if zpathext == '': + zpath = '%s.html' % zpath # fake the real nature of the html file + try: + archive.add(ldda.dataset.file_name,zpath) # add the primary of a composite set + except IOError: + error = True + log.exception( "Unable to add composite parent %s to temporary library download archive" % ldda.dataset.file_name) + message = "Unable to create archive for download, please report this error" + status = 'error' + continue + flist = glob.glob(os.path.join(ldda.dataset.extra_files_path,'*.*')) # glob returns full paths + for fpath in flist: + efp,fname = os.path.split(fpath) + if fname > '': + fname = fname.translate(trantab) + try: + archive.add( fpath,fname ) + except IOError: + error = True + log.exception( "Unable to add %s to temporary library download archive %s" % (fname,outfname)) + message = "Unable to create archive for download, please report this error" + status = 'error' + continue + else: # simple case + try: + archive.add( ldda.dataset.file_name, path ) + except IOError: + error = True + log.exception( "Unable to write %s to temporary library download archive" % ldda.dataset.file_name) + message = "Unable to create archive for download, please report this error" + status = 'error' + if not error: + lname = 'selected_dataset' + fname = lname.replace( ' ', '_' ) + '_files' + if format == 'zip': + archive.close() + trans.response.set_content_type( "application/octet-stream" ) + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) + archive = util.streamball.ZipBall(tmpf, tmpd) + archive.wsgi_status = trans.response.wsgi_status() + archive.wsgi_headeritems = trans.response.wsgi_headeritems() + return archive.stream + else: + trans.response.set_content_type( "application/x-tar" ) + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) + archive.wsgi_status = trans.response.wsgi_status() + archive.wsgi_headeritems = trans.response.wsgi_headeritems() + return archive.stream + elif format == 'uncompressed': + if len(lddas) != 1: + return 'Wrong request' + else: + single_dataset = lddas[0] + trans.response.set_content_type( single_dataset.get_mime() ) + fStat = os.stat( ldda.file_name ) + trans.response.headers[ 'Content-Length' ] = int( fStat.st_size ) + valid_chars = '.,^_-()[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + fname = ldda.name + fname = ''.join( c in valid_chars and c or '_' for c in fname )[ 0:150 ] + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s"' % fname + try: + return open( single_dataset.file_name ) + except: + return 'This dataset contains no content' + else: + return 'Wrong format'; \ No newline at end of file diff -r 4e53d7b759d1999758a30611e5d897e3e1d20782 -r 44399512777bdefb39ea3c99cc04b3f7947c5d33 lib/galaxy/webapps/galaxy/api/libraries.py --- a/lib/galaxy/webapps/galaxy/api/libraries.py +++ b/lib/galaxy/webapps/galaxy/api/libraries.py @@ -52,6 +52,7 @@ item = library.to_dict( view='element' ) item['url'] = url_for( route, id=trans.security.encode_id( library.id ) ) item['id'] = 'F' + trans.security.encode_id( item['id'] ) + item['root_folder_id'] = 'F' + trans.security.encode_id( item['root_folder_id'] ) rval.append( item ) return rval diff -r 4e53d7b759d1999758a30611e5d897e3e1d20782 -r 44399512777bdefb39ea3c99cc04b3f7947c5d33 lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -146,6 +146,7 @@ # ======================= webapp.mapper.connect( 'show_lda_item', '/api/libraries/datasets/:id', controller='lda_datasets', action='show', conditions=dict(method=["GET"]) ) + webapp.mapper.connect( 'download_lda_items', '/api/libraries/datasets/download/:format', controller='lda_datasets', action='download', conditions=dict(method=["POST"]) ) webapp.mapper.resource_with_deleted( 'library', 'libraries', path_prefix='/api' ) webapp.mapper.resource( 'folder', 'folders', path_prefix='/api' ) diff -r 4e53d7b759d1999758a30611e5d897e3e1d20782 -r 44399512777bdefb39ea3c99cc04b3f7947c5d33 static/scripts/galaxy.library.js --- a/static/scripts/galaxy.library.js +++ b/static/scripts/galaxy.library.js @@ -5,6 +5,9 @@ // global variables var view = null; var library_router = null; +var responses = []; +var KEYCODE_ENTER = 13; +var KEYCODE_ESC = 27; // dependencies define(["galaxy.modal", "galaxy.master"], function(mod_modal, mod_master) { @@ -70,52 +73,99 @@ var LibraryRouter = Backbone.Router.extend({ routes: { "" : "libraries", - "folders/:id" : "folder_content" + "folders/:id" : "folder_content", + "folders/:folder_id/download/:format" : "download" } }); // MMMMMMMMMMMMMM -// === Views ==== +// === VIEWS ==== // MMMMMMMMMMMMMM // galaxy folder var FolderContentView = Backbone.View.extend({ - //main element + // main element definition el : '#center', + // progress percentage + progress: 0, + // progress rate per one item + progressStep: 1, + // last selected history in modal for UX + lastSelectedHistory: '', + // self modal + modal : null, + // loaded folders + folders : null, + // initialize initialize : function(){ this.folders = []; + this.queue = jQuery.Deferred(); + this.queue.resolve(); }, + +// MMMMMMMMMMMMMMMMMM +// === TEMPLATES ==== +// MMMMMMMMMMMMMMMMMM + // set up templateFolder : function (){ var tmpl_array = []; - tmpl_array.push('<a href="#">Libraries</a> | '); + // CONTAINER + tmpl_array.push('<div id="library_container" style="width: 90%; margin: auto; margin-top: 2em; ">'); + tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to Martin.</h3>'); + + // TOOLBAR + tmpl_array.push('<div id="library_folder_toolbar" >'); + tmpl_array.push(' <button id="toolbtn_create_folder" class="btn btn-primary" type="button">new folder</button>'); + tmpl_array.push(' <button id="toolbtn_bulk_import" style="display: none; margin-left: 0.5em;" type="button">Import into History</button>'); + + tmpl_array.push(' <div id="toolbtn_dl" class="btn-group" style="margin-left: 0.5em; display: none; ">'); + tmpl_array.push(' <button id="drop_toggle" type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">'); + tmpl_array.push(' Download <span class="caret"></span>'); + tmpl_array.push(' </button>'); + tmpl_array.push(' <ul class="dropdown-menu" role="menu">'); + // tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/uncompressed">uncompressed</a></li>'); + // tmpl_array.push(' <li class="divider"></li>'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/tgz">.tar.gz</a></li>'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/tbz">.tar.bz</a></li>'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/zip">.zip</a></li>'); + tmpl_array.push(' </ul>'); + tmpl_array.push(' </div>'); + + tmpl_array.push('</div>'); + + // BREADCRUMBS + tmpl_array.push('<a href="#">Libraries</a><b>|</b> '); tmpl_array.push('<% _.each(path, function(path_item) { %>'); //breadcrumb - tmpl_array.push('<% if (path_item[0] != id) { %>'); - tmpl_array.push('<a href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a> |'); - tmpl_array.push('<% } else { %>'); - tmpl_array.push('<%- path_item[1] %>'); - tmpl_array.push('<% } %>'); - tmpl_array.push('<% }); %>'); - tmpl_array.push('<table class="table table-hover table-condensed">'); - tmpl_array.push(' <thead>'); - tmpl_array.push(' <th style="text-align: center; width: 20px; "><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th>'); - tmpl_array.push(' <th>name</th>'); - tmpl_array.push(' <th>type</th>'); - tmpl_array.push(' </thead>'); - tmpl_array.push(' <tbody>'); - tmpl_array.push(' <% _.each(items, function(content_item) { %>'); - tmpl_array.push(' <tr class="folder_row" id="<%- content_item.id %>">'); - tmpl_array.push(' <td style="text-align: center; "><input style="margin: 0;" type="checkbox"></td>'); + tmpl_array.push('<% if (path_item[0] != id) { %>'); + tmpl_array.push('<a href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a><b>|</b> '); + tmpl_array.push('<% } else { %>'); + tmpl_array.push('<%- path_item[1] %>'); + tmpl_array.push('<% } %>'); + tmpl_array.push('<% }); %>'); + + // FODLER CONTENT + tmpl_array.push('<table id="folder_table" class="table table-hover table-condensed">'); + tmpl_array.push(' <thead>'); + tmpl_array.push(' <th style="text-align: center; width: 20px; "><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th>'); + tmpl_array.push(' <th>name</th>'); + tmpl_array.push(' <th>type</th>'); + tmpl_array.push(' </thead>'); + tmpl_array.push(' <tbody>'); + tmpl_array.push(' <% _.each(items, function(content_item) { %>'); + tmpl_array.push(' <tr class="folder_row" id="<%- content_item.id %>">'); tmpl_array.push(' <% if (content_item.get("type") === "folder") { %>'); //folder - tmpl_array.push(' <td><a href="#/folders/<%- content_item.id %>"><%- content_item.get("name") %></a>'); + tmpl_array.push(' <td style="text-align: center; "></td>'); + tmpl_array.push(' <td><a href="#/folders/<%- content_item.id %>"><%- content_item.get("name") %></a>'); tmpl_array.push(' <% if (content_item.get("item_count") === 0) { %>'); //empty folder tmpl_array.push(' <span class="muted">(empty folder)</span>'); tmpl_array.push(' <% } %>'); tmpl_array.push(' </td>'); tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <td style="text-align: center; "><input style="margin: 0;" type="checkbox"></td>'); tmpl_array.push(' <td><a class="library-dataset" href="#"><%- content_item.get("name") %></a></td>'); //dataset tmpl_array.push(' <% } %> '); tmpl_array.push(' <td><%= _.escape(content_item.get("type")) %></td>'); @@ -123,10 +173,10 @@ tmpl_array.push(' <% }); %>'); tmpl_array.push(' '); tmpl_array.push(' '); - tmpl_array.push(' '); tmpl_array.push(' </tbody>'); tmpl_array.push('</table>'); + tmpl_array.push('</div>'); return tmpl_array.join(''); }, templateDatasetModal : function(){ @@ -135,7 +185,7 @@ tmpl_array.push('<div id="dataset_info_modal">'); tmpl_array.push(' <table class="table table-striped table-condensed">'); tmpl_array.push(' <tr>'); - tmpl_array.push(' <th scope="row">Name</th>'); + tmpl_array.push(' <th scope="row" id="id_row" data-id="<%= _.escape(item.get("ldda_id")) %>">Name</th>'); tmpl_array.push(' <td><%= _.escape(item.get("name")) %></td>'); tmpl_array.push(' </tr>'); tmpl_array.push(' <tr>'); @@ -157,9 +207,6 @@ tmpl_array.push(' <th scope="row">Uploaded by</th>'); tmpl_array.push(' <td><%= _.escape(item.get("uploaded_by")) %></td>'); tmpl_array.push(' </tr>'); - // tmpl_array.push(' </table>'); - // tmpl_array.push(' <hr/>'); - // tmpl_array.push(' <table class="table table-striped">'); tmpl_array.push(' <tr scope="row">'); tmpl_array.push(' <th scope="row">Data Lines</th>'); tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_data_lines")) %></td>'); @@ -183,31 +230,45 @@ tmpl_array.push(' <th scope="row">Miscellaneous information</th>'); tmpl_array.push(' <td scope="row"><%= _.escape(item.get("misc_blurb")) %></td>'); tmpl_array.push(' </tr>'); - tmpl_array.push(' <tr>'); - tmpl_array.push(' '); tmpl_array.push(' </table>'); - // tmpl_array.push(' <hr/>'); tmpl_array.push(' <pre class="peek">'); tmpl_array.push(' </pre>'); tmpl_array.push('</div>'); return tmpl_array.join(''); }, + templateHistorySelectInModal : function(){ var tmpl_array = []; - tmpl_array.push('<span id="history_modal_footer" style="width:60%;">'); - tmpl_array.push('<select name="history_import" style="width:60%; margin-left: 2em; "> '); + tmpl_array.push('<span id="history_modal_combo" style="width:90%; margin-left: 1em; margin-right: 1em; ">'); + tmpl_array.push('Select history: '); + tmpl_array.push('<select id="dataset_import_single" name="dataset_import_single" style="width:50%; margin-bottom: 1em; "> '); tmpl_array.push(' <% _.each(histories, function(history) { %>'); //history select box - tmpl_array.push(' <option value="<%= _.escape(history.get("id")) %>"><%= _.escape(history.get("name")) %></option>'); - tmpl_array.push(' <% }); %>'); - tmpl_array.push('</span>'); - tmpl_array.push('</div>'); - tmpl_array.push(''); + tmpl_array.push(' <option value="<%= _.escape(history.get("id")) %>"><%= _.escape(history.get("name")) %></option>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push('</select>'); + tmpl_array.push('</span>'); - return tmpl_array.join(''); - }, - // to string + return tmpl_array.join(''); + }, + + templateBulkImportInModal : function(){ + var tmpl_array = []; + + tmpl_array.push('<span id="history_modal_combo_bulk" style="width:90%; margin-left: 1em; margin-right: 1em; ">'); + tmpl_array.push('Select history: '); + tmpl_array.push('<select id="dataset_import_bulk" name="dataset_import_bulk" style="width:50%; margin-bottom: 1em; "> '); + tmpl_array.push(' <% _.each(histories, function(history) { %>'); //history select box + tmpl_array.push(' <option value="<%= _.escape(history.get("id")) %>"><%= _.escape(history.get("name")) %></option>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push('</select>'); + tmpl_array.push('</span>'); + + return tmpl_array.join(''); + }, + + // convert size to nice string size_to_string : function (size) { // identify unit @@ -220,17 +281,26 @@ // return formatted string return (Math.round(size) / 10) + unit; }, - events: { + +// MMMMMMMMMMMMMMM +// === EVENTS ==== +// MMMMMMMMMMMMMMM + + // event binding + events: { 'click #select-all-checkboxes' : 'selectAll', 'click .folder_row' : 'selectClicked', + 'click #toolbtn_bulk_import' : 'modalBulkImport', + 'click #toolbtn_dl' : 'bulkDownload', 'click .library-dataset' : 'showDatasetDetails' }, - //self modal - modal : null, - //loaded folders - folders : null, - //render the view + + //render the folder view render: function (options) { + //hack to show scrollbars + $("#center").css('overflow','auto'); + + view = this; var that = this; var folderContainer = new FolderContainer({id: options.id}); @@ -241,15 +311,16 @@ // folderContainer.attributes.folder = container.attributes.folder; var template = _.template(that.templateFolder(), {path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id}); that.$el.html(template); - } - }) - }, - //show modal with dataset info + } + }) + }, + + //show modal with current dataset info showDatasetDetails : function(e){ // prevent default e.preventDefault(); - //TODO check whether we already have the data +//TODO check whether we already have the data //load the ID of the row var id = $(e.target).parent().parent().attr('id'); @@ -259,53 +330,90 @@ var histories = new GalaxyHistories(); item.id = id; var self = this; + //fetch the dataset info item.fetch({ success: function (item) { - //fetch user histories for import +// TODO can render here already + //fetch user histories for import purposes histories.fetch({ - success: function (histories){self.modalFetchSuccess(item, histories)} + success: function (histories){self.renderModalAfterFetch(item, histories)} }); } }); }, - modalFetchSuccess : function(item, histories){ - var histories_modal = this.templateHistorySelectInModal(); + // show the current dataset in a modal + renderModalAfterFetch : function(item, histories){ var size = this.size_to_string(item.get('file_size')); var template = _.template(this.templateDatasetModal(), { item : item, size : size }); + this.modal = null; // make modal var self = this; this.modal = new mod_modal.GalaxyModal({ title : 'Dataset Details', body : template, buttons : { - 'Import' : function() {self.importIntoHistory()}, - // 'Notify' : function() {self.modal.showNotification("TEST")}, - 'Close' : function() {self.modal.hide()} + 'Import' : function() { self.importCurrentIntoHistory() }, + 'Download' : function() { self.downloadCurrent() }, + 'Close' : function() { self.modal.hide(); $('.modal').html(''); self.modal = null; } // TODO refill nicely modal with data } }); $(".peek").html(item.get("peek")); - // this.modal.hideButton('Import'); - // $(this.modal.elMain).find('.modal-footer').prepend("<div>BUBUBUBU" + "</div>"); var history_footer_tmpl = _.template(this.templateHistorySelectInModal(), {histories : histories.models}); + $(this.modal.elMain).find('.buttons').prepend(history_footer_tmpl); - $(this.modal.elMain).find('.buttons').prepend(history_footer_tmpl); + // preset last selected history if we know it + if (self.lastSelectedHistory.length > 0) { + $(this.modal.elMain).find('#dataset_import_single').val(self.lastSelectedHistory); + } // show the prepared modal this.modal.show(); }, - importIntoHistory : function(){ - var history_id = 'a0c15f4d91084599'; - var library_dataset_id = '03501d7626bd192f'; + // download dataset shown currently in modal + downloadCurrent : function(){ + //disable the buttons + this.modal.disableButton('Import'); + this.modal.disableButton('Download'); + var library_dataset_id = []; + library_dataset_id.push($('#id_row').attr('data-id')); + var url = '/api/libraries/datasets/download/uncompressed'; + var data = {'ldda_ids' : library_dataset_id}; + + // we assume the view is existent + folderContentView.processDownload(url, data); + this.modal.enableButton('Import'); + this.modal.enableButton('Download'); + }, + + // import dataset shown currently in modal into selected history + importCurrentIntoHistory : function(){ + //disable the buttons + this.modal.disableButton('Import'); + this.modal.disableButton('Download'); + + var history_id = $(this.modal.elMain).find('select[name=dataset_import_single] option:selected').val(); + this.lastSelectedHistory = history_id; //save selected history for further use + + var library_dataset_id = $('#id_row').attr('data-id'); var historyItem = new HistoryItem(); var self = this; historyItem.url = historyItem.urlRoot + history_id + '/contents'; - console.log(historyItem); + + // save the dataset into selected history historyItem.save({ content : library_dataset_id, source : 'library' }, { success : function(){ self.modal.showNotification('Dataset imported', 3000, '#e1f4e0', '#32a427'); + //enable the buttons + self.modal.enableButton('Import'); + self.modal.enableButton('Download'); + }, error : function(){ + self.modal.showNotification('An error occured! Dataset not imported. Please try again later.', 5000, '#f4e0e1', '#a42732'); + //enable the buttons + self.modal.enableButton('Import'); + self.modal.enableButton('Download'); } }); }, @@ -314,9 +422,11 @@ var selected = ev.target.checked; // Iterate each checkbox $(':checkbox').each(function () { this.checked = selected; }); + this.showTools(); }, - selectClicked : function (ev) { - var checkbox = $("#" + ev.target.parentElement.id).find(':checkbox') + + selectClicked : function (ev) { + var checkbox = $("#" + ev.target.parentElement.id).find(':checkbox') if (checkbox[0] != undefined) { if (checkbox[0].checked){ checkbox[0].checked = ''; @@ -324,11 +434,161 @@ checkbox[0].checked = 'selected'; } } - } + this.showTools(); + }, + + // show toolbar in case something is selected + showTools : function(){ + var checkedValues = $('#folder_table').find(':checked'); + if(checkedValues.length > 0){ + $('#toolbtn_bulk_import').show(); + $('#toolbtn_dl').show(); + } else { + $('#toolbtn_bulk_import').hide(); + $('#toolbtn_dl').hide(); + } + + }, + + // show bulk import modal + modalBulkImport : function(){ + var self = this; + // fetch histories + var histories = new GalaxyHistories(); + histories.fetch({ + success: function (histories){ + // make modal + var history_modal_tmpl = _.template(self.templateBulkImportInModal(), {histories : histories.models}); + self.modal = new mod_modal.GalaxyModal({ + title : 'Import into History', + body : history_modal_tmpl, + buttons : { + 'Import' : function() {self.importAllIntoHistory()}, + 'Close' : function() {self.modal.hide()} + } + }); + // show the prepared modal + self.modal.show(); + } + }); + }, + + // import all selected datasets into history + importAllIntoHistory : function (){ + //disable the button + this.modal.disableButton('Import'); + + var history_id = $("select[name=dataset_import_bulk] option:selected").val(); + var history_name = $("select[name=dataset_import_bulk] option:selected").text(); + + var dataset_ids = []; + $('#folder_table').find(':checked').each(function(){ + if (this.parentElement.parentElement.id != '') { + dataset_ids.push(this.parentElement.parentElement.id); + } + }); + var progress_bar_tmpl = _.template(this.templateProgressBar(), { history_name : history_name }); + $(this.modal.elMain).find('.modal-body').html(progress_bar_tmpl); + + // init the progress bar + var progressStep = 100 / dataset_ids.length; + this.initProgress(progressStep); + + // prepare the dataset objects to be imported + var datasets_to_import = []; + for (var i = dataset_ids.length - 1; i >= 0; i--) { + library_dataset_id = dataset_ids[i]; + var historyItem = new HistoryItem(); + var self = this; + historyItem.url = historyItem.urlRoot + history_id + '/contents'; + historyItem.content = library_dataset_id; + historyItem.source = 'library'; + datasets_to_import.push(historyItem); + }; + + // call the recursive function to call ajax one after each other + this.chainCall(datasets_to_import); + }, + + chainCall: function(history_item_set){ + var self = this; + var popped_item = history_item_set.pop(); + if (typeof popped_item === "undefined") { + self.modal.showNotification('All datasets imported', 3000, '#e1f4e0', '#32a427'); + // enable button again + self.modal.enableButton('Import'); + return + } + var promise = $.when(popped_item.save({content: popped_item.content, source: popped_item.source})).done(function(a1){ + self.updateProgress(); + responses.push(a1); + self.chainCall(history_item_set); + }); + }, + + initProgress: function(progressStep){ + this.progress = 0; + this.progressStep = progressStep; + }, + updateProgress: function(){ + this.progress += this.progressStep; + $('.progress-bar').width(Math.round(this.progress) + '%'); + txt_representation = Math.round(this.progress) + '% Complete'; + $('.completion_span').text(txt_representation); + }, + + // progress bar + templateProgressBar : function (){ + var tmpl_array = []; + + tmpl_array.push('<div class="import_text">'); + tmpl_array.push('Importing selected datasets to history <b><%= _.escape(history_name) %></b>'); + tmpl_array.push('</div>'); + tmpl_array.push('<div class="progress">'); + tmpl_array.push(' <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 00%;">'); + tmpl_array.push(' <span class="completion_span">0% Complete</span>'); + tmpl_array.push(' </div>'); + tmpl_array.push('</div>'); + tmpl_array.push(''); + + return tmpl_array.join(''); + }, + + // download selected datasets + download : function(folder_id, format){ + var dataset_ids = []; + $('#folder_table').find(':checked').each(function(){ + if (this.parentElement.parentElement.id != '') { + dataset_ids.push(this.parentElement.parentElement.id); + } + }); + + var url = '/api/libraries/datasets/download/' + format; + var data = {'ldda_ids' : dataset_ids}; + this.processDownload(url, data); + }, + + // create hidden form and submit through POST to initialize download + processDownload: function(url, data, method){ + //url and data options required + if( url && data ){ + //data can be string of parameters or array/object + data = typeof data == 'string' ? data : $.param(data); + //split params into form inputs + var inputs = ''; + $.each(data.split('&'), function(){ + var pair = this.split('='); + inputs+='<input type="hidden" name="'+ pair[0] +'" value="'+ pair[1] +'" />'; + }); + //send request + $('<form action="'+ url +'" method="'+ (method||'post') +'">'+inputs+'</form>') + .appendTo('body').submit().remove(); + }; + } + }); - -// galaxy library +// galaxy library view var GalaxyLibraryview = Backbone.View.extend({ el: '#center', @@ -343,9 +603,10 @@ // template template_library_list : function (){ tmpl_array = []; + tmpl_array.push('<div id="library_container" style="width: 90%; margin: auto; margin-top: 2em; overflow: auto !important; ">'); tmpl_array.push(''); - tmpl_array.push('<h1>Welcome to the data libraries</h1>'); + tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to Martin.</h3>'); tmpl_array.push('<a href="" id="create_new_library_btn" class="btn btn-primary icon-file ">New Library</a>'); tmpl_array.push('<table class="table table-striped">'); tmpl_array.push(' <thead>'); @@ -353,30 +614,27 @@ tmpl_array.push(' <th>description</th>'); tmpl_array.push(' <th>synopsis</th> '); tmpl_array.push(' <th>model type</th> '); - // tmpl_array.push(' <th>id</th> '); tmpl_array.push(' </thead>'); tmpl_array.push(' <tbody>'); tmpl_array.push(' <% _.each(libraries, function(library) { %>'); - tmpl_array.push(' <tr>'); - tmpl_array.push(' <td><a href="#/folders/<%- library.id %>"><%- library.get("name") %></a></td>'); - tmpl_array.push(' <td><%= _.escape(library.get("description")) %></td>'); - tmpl_array.push(' <td><%= _.escape(library.get("synopsis")) %></td>'); - tmpl_array.push(' <td><%= _.escape(library.get("model_class")) %></td>'); - // tmpl_array.push(' <td><a href="#/folders/<%- library.id %>"><%= _.escape(library.get("id")) %></a></td>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <td><a href="#/folders/<%- library.get("root_folder_id") %>"><%- library.get("name") %></a></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("description")) %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("synopsis")) %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("model_class")) %></td>'); tmpl_array.push(' </tr>'); tmpl_array.push(' <% }); %>'); - tmpl_array.push(''); - tmpl_array.push(''); - tmpl_array.push(''); - tmpl_array.push(''); tmpl_array.push(' </tbody>'); tmpl_array.push('</table>'); - + + tmpl_array.push('</div>'); return tmpl_array.join(''); }, // render render: function () { + //hack to show scrollbars + $("#center").css('overflow','auto'); var that = this; // if (typeof libraries === "undefined") { libraries = new Libraries(); @@ -394,12 +652,11 @@ // show/hide create library modal show_library_modal : function (e){ - // prevent default + // prevent default, may be unnecessary e.preventDefault(); // create modal if (!this.modal){ - // make modal var self = this; this.modal = new mod_modal.GalaxyModal( { @@ -412,9 +669,11 @@ }); } - // show modal + // show prepared modal this.modal.show(); }, + + // create the new library from modal create_new_library_event: function(){ var libraryDetails = this.serialize_new_library(); var library = new Library(); @@ -428,11 +687,15 @@ }); return false; }, + + // clear the library modal once saved clear_library_modal : function(){ $("input[name='Name']").val(''); $("input[name='Description']").val(''); $("input[name='Synopsis']").val(''); }, + + // serialize data from the form serialize_new_library : function(){ return { name: $("input[name='Name']").val(), @@ -440,10 +703,12 @@ synopsis: $("input[name='Synopsis']").val() }; }, - // load html template + + // template for new library modal template_new_library: function() { tmpl_array = []; + tmpl_array.push('<div id="new_library_modal">'); tmpl_array.push('<form>'); tmpl_array.push('<input type="text" name="Name" value="" placeholder="Name">'); @@ -452,10 +717,8 @@ tmpl_array.push(''); tmpl_array.push('</form>'); tmpl_array.push('</div>'); + return tmpl_array.join(''); - - - // return '<div id="'+ id +'"></div>'; } }); @@ -473,19 +736,28 @@ library_router.on('route:libraries', function() { // render libraries list galaxyLibraryview.render(); - }) + }); library_router.on('route:folder_content', function(id) { // render folder's contents folderContentView.render({id: id}); + }); - }) + library_router.on('route:download', function(folder_id, format) { + // send download stream + if (typeof folderContentView === 'undefined'){ + alert('you cant touch this!'); + // } else if (folderContentView.modal !== null){ + // folderContentView.download(folder_id, format); + } else if ($('#center').find(':checked').length === 0) { // coming from outside of the library app + library_router.navigate('folders/' + folder_id, {trigger: true, replace: true}); + // TODO show message of redirection + } else { + folderContentView.download(folder_id, format); + library_router.navigate('folders/' + folder_id, {trigger: false, replace: true}); + } - // library_router.on('route:show_library_modal', function() { - // // render folder's contents - // galaxyLibraryview.show_library_modal(); - - // }) + }); Backbone.history.start(); diff -r 4e53d7b759d1999758a30611e5d897e3e1d20782 -r 44399512777bdefb39ea3c99cc04b3f7947c5d33 static/scripts/galaxy.modal.js --- a/static/scripts/galaxy.modal.js +++ b/static/scripts/galaxy.modal.js @@ -20,9 +20,28 @@ // initialize initialize : function(options) { + self = this; // create if (options) this.create(options); + // bind the ESC key to hide() function + $(document).on('keyup', function(event){ + if (event.keyCode == 27) { self.hide(); } + }) + // bind the click anywhere to hide() function... + $('html').on('click', function(event){ + self.hide(); + }) + // ...but don't hide if the click is on modal content + $('.modal-content').on('click', function(event){ + event.stopPropagation(); + }) + }, + + // destroy + destroy : function(){ + this.hide(); + $('.modal').html(''); }, // adds and displays a new frame/window https://bitbucket.org/galaxy/galaxy-central/commits/f466834f693f/ Changeset: f466834f693f Branch: data_library User: martenson Date: 2013-12-10 19:01:19 Summary: improved styles for libraries, added features to modal window, API tweaks Affected #: 5 files diff -r 44399512777bdefb39ea3c99cc04b3f7947c5d33 -r f466834f693ff7ae0f60cf121d97f67a5338ea95 lib/galaxy/model/mapping.py --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -1988,15 +1988,6 @@ table = self.table trans = conn.begin() try: - -# log.debug("XXXXXXXXXXXXXXX NEW GENERATOOOOOR") -# next_hid = table.update() \ -# .returning( table.c.hid_counter ) \ -# .where( table.c.id == self.id ) \ -# .values( hid_counter = hid_counter + 1 ) -# log.debug('XXXXXXXXXXXXXXXXXXXXXXXXXXX NEXT HID: ' + str(next_hid)) - - current_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() next_hid = current_hid + 1 table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid ) ) diff -r 44399512777bdefb39ea3c99cc04b3f7947c5d33 -r f466834f693ff7ae0f60cf121d97f67a5338ea95 lib/galaxy/webapps/galaxy/api/folder_contents.py --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -46,17 +46,6 @@ """ folder_container = [] current_user_roles = trans.get_current_user_roles() - -# log.debug( 'XXXXXXXXXXXXXXXXXXXXX GOT id: ' + folder_id ) -# log.debug( 'XXXXXXXXXXXXXXXXXXXXX decode id: ' + folder_id[1:] ) -# log.debug( 'XXXXXXXXXXXXXXXXXXXXX call decode: ' + str(trans.security.decode_id( folder_id[1:] )) ) - -# try: -# decoded_folder_id = trans.security.decode_id( folder_id[-16:] ) -# except TypeError: -# trans.response.status = 400 -# return "Malformed folder id ( %s ) specified, unable to decode." % str( folder_id ) -# log.debug( 'XXXXXXXXXXXXXXXXXXXXX decoded id: ' + decoded_folder_id ) if ( folder_id.startswith( 'F' ) ): try: @@ -67,9 +56,6 @@ try: folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( decoded_folder_id ) -# log.debug( 'XXXXXXXXXXXXXXXXXXXXX FOLDER id: ' + str(folder.id) ) -# log.debug( 'XXXXXXXXXXXXXXXXXXXXX FOLDER name: ' + str(folder.name) ) -# parent_library = folder.parent_library except: folder = None log.error( "FolderContentsController.index: Unable to retrieve folder with ID: %s" % folder_id ) @@ -92,17 +78,9 @@ path_to_root = [] # We are almost in root if folder.parent_id is None: - -# log.debug( "XXXXXXXXXXXXXXXXXXXXXXX ALMOST ROOT FOLDER! ADDING ID: " + str( folder.id ) ) -# log.debug( "XXXXXXXXXXXXXXXXXXXXXXX ALMOST ROOT FOLDER! ADDING NAME: " + str( folder.name ) ) path_to_root.append( ( 'F' + trans.security.encode_id( folder.id ), folder.name ) ) -# upper_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( folder.parent_library.id ) -# path_to_root.append( ( upper_folder.id, upper_folder.name ) ) else: # We add the current folder and traverse up one folder. -# log.debug( "XXXXXXXXXXXXXXXXXXXXXXX folder.parent_id " + str( folder.parent_id ) ) -# log.debug( "XXXXXXXXXXXXXXXXXXXXXXX ALMOST ROOT FOLDER! ADDING ID: " + str( folder.id ) ) -# log.debug( "XXXXXXXXXXXXXXXXXXXXXXX ADDING THIS FOLDER AND TRAVERSING UP: " + str( folder.name ) ) path_to_root.append( ( 'F' + trans.security.encode_id( folder.id ), folder.name ) ) upper_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( folder.parent_id ) path_to_root.extend( build_path( upper_folder ) ) @@ -113,25 +91,40 @@ folder_container.append( dict( full_path = full_path ) ) folder_contents = [] + time_updated = '' + time_created = '' # Go through every item in the folder and include its meta-data. for content_item in self.load_folder_contents( trans, folder ): +# rval = content_item.to_dict() return_item = {} encoded_id = trans.security.encode_id( content_item.id ) + time_updated = content_item.update_time.strftime( "%Y-%m-%d %I:%M %p" ) + time_created = content_item.create_time.strftime( "%Y-%m-%d %I:%M %p" ) +# log.debug('XXXXXXXXXXXXXXXXXXX api type: ' + str(content_item.api_type)) +# log.debug('XXXXXXXX ALL: ' + str(content_item.__dict__)) # For folder return also hierarchy values if content_item.api_type == 'folder': -# encoded_parent_library_id = trans.security.encode_id( content_item.parent_library.id ) encoded_id = 'F' + encoded_id -# if content_item.parent_id is not None: # Return folder's parent id for browsing back. -# encoded_parent_id = 'F' + trans.security.encode_id( content_item.parent_id ) - last_updated = content_item.update_time.strftime( "%Y-%m-%d %I:%M %p" ) - return_item.update ( dict ( item_count = content_item.item_count, last_updated = last_updated ) ) - +# time_updated = content_item.update_time.strftime( "%Y-%m-%d %I:%M %p" ) + return_item.update ( dict ( item_count = content_item.item_count ) ) + + if content_item.api_type == 'file': +# log.debug('XXXXX content item class: ' + str(content_item.__class__)) + library_dataset_dict = content_item.to_dict() + library_dataset_dict['data_type'] + library_dataset_dict['file_size'] + library_dataset_dict['date_uploaded'] + return_item.update ( dict ( data_type = library_dataset_dict['data_type'], + file_size = library_dataset_dict['file_size'], + date_uploaded = library_dataset_dict['date_uploaded'] ) ) + # For every item return also the default meta-data return_item.update( dict( id = encoded_id, type = content_item.api_type, - name = content_item.name - + name = content_item.name, + time_updated = time_updated, + time_created = time_created ) ) folder_contents.append( return_item ) # Put the data in the container diff -r 44399512777bdefb39ea3c99cc04b3f7947c5d33 -r f466834f693ff7ae0f60cf121d97f67a5338ea95 static/scripts/galaxy.library.js --- a/static/scripts/galaxy.library.js +++ b/static/scripts/galaxy.library.js @@ -6,8 +6,16 @@ var view = null; var library_router = null; var responses = []; -var KEYCODE_ENTER = 13; -var KEYCODE_ESC = 27; + + +// load required libraries +require([ + // load js libraries + 'utils/galaxy.css', + ], function(css){ + // load css + css.load_file("static/style/library.css"); +}); // dependencies define(["galaxy.modal", "galaxy.master"], function(mod_modal, mod_master) { @@ -115,20 +123,18 @@ // CONTAINER tmpl_array.push('<div id="library_container" style="width: 90%; margin: auto; margin-top: 2em; ">'); - tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to Martin.</h3>'); + tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to <a href="mailto:marten@bx.psu.edu?Subject=DataLibraries_Feedback" target="_blank">Marten</a>.</h3>'); // TOOLBAR tmpl_array.push('<div id="library_folder_toolbar" >'); - tmpl_array.push(' <button id="toolbtn_create_folder" class="btn btn-primary" type="button">new folder</button>'); - tmpl_array.push(' <button id="toolbtn_bulk_import" style="display: none; margin-left: 0.5em;" type="button">Import into History</button>'); + tmpl_array.push(' <button title="Create New Folder" id="toolbtn_create_folder" class="btn btn-primary" type="button"><span class="fa fa-icon-plus"></span><span class="fa fa-icon-folder-close"></span> folder</button>'); + tmpl_array.push(' <button id="toolbtn_bulk_import" class="btn btn-primary" style="display: none; margin-left: 0.5em;" type="button"><span class="fa fa-icon-external-link"></span> to history</button>'); tmpl_array.push(' <div id="toolbtn_dl" class="btn-group" style="margin-left: 0.5em; display: none; ">'); - tmpl_array.push(' <button id="drop_toggle" type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">'); - tmpl_array.push(' Download <span class="caret"></span>'); + tmpl_array.push(' <button id="drop_toggle" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">'); + tmpl_array.push(' <span class="fa fa-icon-download"></span> download <span class="caret"></span>'); tmpl_array.push(' </button>'); tmpl_array.push(' <ul class="dropdown-menu" role="menu">'); - // tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/uncompressed">uncompressed</a></li>'); - // tmpl_array.push(' <li class="divider"></li>'); tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/tgz">.tar.gz</a></li>'); tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/tbz">.tar.bz</a></li>'); tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/zip">.zip</a></li>'); @@ -138,41 +144,64 @@ tmpl_array.push('</div>'); // BREADCRUMBS - tmpl_array.push('<a href="#">Libraries</a><b>|</b> '); - tmpl_array.push('<% _.each(path, function(path_item) { %>'); //breadcrumb + tmpl_array.push('<div class="library_breadcrumb">'); + tmpl_array.push('<a title="Return to the list of libraries" href="#">Libraries</a><b>|</b> '); + tmpl_array.push('<% _.each(path, function(path_item) { %>'); tmpl_array.push('<% if (path_item[0] != id) { %>'); - tmpl_array.push('<a href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a><b>|</b> '); + tmpl_array.push('<a title="Return to this folder" href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a><b>|</b> '); tmpl_array.push('<% } else { %>'); - tmpl_array.push('<%- path_item[1] %>'); + tmpl_array.push('<span title="You are in this folder"><%- path_item[1] %></span>'); tmpl_array.push('<% } %>'); tmpl_array.push('<% }); %>'); + tmpl_array.push('</div>'); - // FODLER CONTENT - tmpl_array.push('<table id="folder_table" class="table table-hover table-condensed">'); + // FOLDER CONTENT + tmpl_array.push('<table id="folder_table" class="table table-condensed">'); tmpl_array.push(' <thead>'); tmpl_array.push(' <th style="text-align: center; width: 20px; "><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th>'); + tmpl_array.push(' <th class="button_heading">view</th>'); tmpl_array.push(' <th>name</th>'); - tmpl_array.push(' <th>type</th>'); + tmpl_array.push(' <th>data type</th>'); + tmpl_array.push(' <th>size</th>'); + tmpl_array.push(' <th>date</th>'); tmpl_array.push(' </thead>'); tmpl_array.push(' <tbody>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td><button title="Go to parent folder" type="button" data-id="<%- upper_folder_id %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-arrow-up"></span> .. go up</td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' </tr>'); tmpl_array.push(' <% _.each(items, function(content_item) { %>'); - tmpl_array.push(' <tr class="folder_row" id="<%- content_item.id %>">'); - tmpl_array.push(' <% if (content_item.get("type") === "folder") { %>'); //folder - tmpl_array.push(' <td style="text-align: center; "></td>'); - tmpl_array.push(' <td><a href="#/folders/<%- content_item.id %>"><%- content_item.get("name") %></a>'); - tmpl_array.push(' <% if (content_item.get("item_count") === 0) { %>'); //empty folder + tmpl_array.push(' <tr class="folder_row light" id="<%- content_item.id %>">'); + tmpl_array.push(' <% if (content_item.get("type") === "folder") { %>'); // folder + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td><button title="Open this folder" type="button" data-id="<%- content_item.id %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-folder-open"></span> browse</td>'); + tmpl_array.push(' <td><%- content_item.get("name") %>'); + tmpl_array.push(' <% if (content_item.get("item_count") === 0) { %>'); // empty folder tmpl_array.push(' <span class="muted">(empty folder)</span>'); tmpl_array.push(' <% } %>'); tmpl_array.push(' </td>'); + tmpl_array.push(' <td>folder</td>'); // data type + tmpl_array.push(' <td><%= _.escape(content_item.get("item_count")) %> item(s)</td>'); // size tmpl_array.push(' <% } else { %>'); tmpl_array.push(' <td style="text-align: center; "><input style="margin: 0;" type="checkbox"></td>'); - tmpl_array.push(' <td><a class="library-dataset" href="#"><%- content_item.get("name") %></a></td>'); //dataset + tmpl_array.push(' <td>'); + tmpl_array.push(' <button title="See details of this dataset" type="button" class="library-dataset btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-eye-open"></span> details'); + tmpl_array.push(' </button>'); + tmpl_array.push(' </td>'); + tmpl_array.push(' <td><%- content_item.get("name") %></td>'); // dataset + tmpl_array.push(' <td><%= _.escape(content_item.get("data_type")) %></td>'); // data type + tmpl_array.push(' <td><%= _.escape(content_item.get("readable_size")) %></td>'); // size tmpl_array.push(' <% } %> '); - tmpl_array.push(' <td><%= _.escape(content_item.get("type")) %></td>'); + tmpl_array.push(' <td><%= _.escape(content_item.get("time_updated")) %></td>'); // time updated tmpl_array.push(' </tr>'); tmpl_array.push(' <% }); %>'); tmpl_array.push(' '); - tmpl_array.push(' '); tmpl_array.push(' </tbody>'); tmpl_array.push('</table>'); @@ -292,7 +321,9 @@ 'click .folder_row' : 'selectClicked', 'click #toolbtn_bulk_import' : 'modalBulkImport', 'click #toolbtn_dl' : 'bulkDownload', - 'click .library-dataset' : 'showDatasetDetails' + 'click .library-dataset' : 'showDatasetDetails', + 'click #toolbtn_create_folder' : 'createFolderModal', + 'click .btn_open_folder' : 'navigateToFolder' }, //render the folder view @@ -308,22 +339,52 @@ folderContainer.fetch({ success: function (container) { - // folderContainer.attributes.folder = container.attributes.folder; - var template = _.template(that.templateFolder(), {path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id}); + + // prepare nice size strings + for (var i = 0; i < folderContainer.attributes.folder.models.length; i++) { + var model = folderContainer.attributes.folder.models[i] + if (model.get('type') === 'file'){ + model.set('readable_size', that.size_to_string(model.get('file_size'))) + } + }; + + // find the upper id + var path = folderContainer.full_path; + var upper_folder_id; + if (path.length === 1){ // library is above us + upper_folder_id = 0; + } else { + upper_folder_id = path[path.length-2][0]; + } + + var template = _.template(that.templateFolder(), { path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id, upper_folder_id: upper_folder_id }); + // var template = _.template(that.templateFolder(), { path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id }); that.$el.html(template); } }) }, + // handles the click on 'open' and 'upper' folder icons + navigateToFolder : function(event){ + var folder_id = $(event.target).attr('data-id'); + if (typeof folder_id === 'undefined') { + return false; + } else if (folder_id === '0'){ + library_router.navigate('#', {trigger: true, replace: true}); + } else { + library_router.navigate('folders/' + folder_id, {trigger: true, replace: true}); + } + }, + //show modal with current dataset info - showDatasetDetails : function(e){ + showDatasetDetails : function(event){ // prevent default - e.preventDefault(); + event.preventDefault(); //TODO check whether we already have the data //load the ID of the row - var id = $(e.target).parent().parent().attr('id'); + var id = $(event.target).parent().parent().attr('id'); //create new item var item = new Item(); @@ -356,7 +417,7 @@ buttons : { 'Import' : function() { self.importCurrentIntoHistory() }, 'Download' : function() { self.downloadCurrent() }, - 'Close' : function() { self.modal.hide(); $('.modal').html(''); self.modal = null; } // TODO refill nicely modal with data + 'Close' : function() { self.modal.hide(); $('.modal').remove(); self.modal = null; } // TODO refill nicely modal with data } }); $(".peek").html(item.get("peek")); @@ -405,12 +466,12 @@ // save the dataset into selected history historyItem.save({ content : library_dataset_id, source : 'library' }, { success : function(){ - self.modal.showNotification('Dataset imported', 3000, '#e1f4e0', '#32a427'); + self.modal.showNotification('Dataset imported', 3000, 'success'); //enable the buttons self.modal.enableButton('Import'); self.modal.enableButton('Download'); }, error : function(){ - self.modal.showNotification('An error occured! Dataset not imported. Please try again later.', 5000, '#f4e0e1', '#a42732'); + self.modal.showNotification('An error occured! Dataset not imported. Please try again later.', 5000, 'error'); //enable the buttons self.modal.enableButton('Import'); self.modal.enableButton('Download'); @@ -418,20 +479,32 @@ }); }, - selectAll : function (ev) { - var selected = ev.target.checked; + // select all datasets + selectAll : function (event) { + var selected = event.target.checked; // Iterate each checkbox $(':checkbox').each(function () { this.checked = selected; }); this.showTools(); }, - selectClicked : function (ev) { - var checkbox = $("#" + ev.target.parentElement.id).find(':checkbox') + // click checkbox on folder click + selectClicked : function (event) { + var checkbox = $("#" + event.target.parentElement.id).find(':checkbox') if (checkbox[0] != undefined) { if (checkbox[0].checked){ checkbox[0].checked = ''; + // $(event.target.parentElement).css('background-color', '').css('color', ''); + $(event.target.parentElement).removeClass('dark'); + $(event.target.parentElement).find('a').removeClass('dark'); + $(event.target.parentElement).addClass('light'); + $(event.target.parentElement).find('a').addClass('light'); } else { checkbox[0].checked = 'selected'; + $(event.target.parentElement).removeClass('light'); + $(event.target.parentElement).find('a').removeClass('light'); + $(event.target.parentElement).addClass('dark'); + $(event.target.parentElement).find('a').addClass('dark'); + // $(event.target.parentElement).css('background-color', '#8389a1').css('color', 'white'); } } this.showTools(); @@ -464,7 +537,7 @@ body : history_modal_tmpl, buttons : { 'Import' : function() {self.importAllIntoHistory()}, - 'Close' : function() {self.modal.hide()} + 'Close' : function() {self.modal.hide(); $('.modal').remove(); self.modal = null;} } }); // show the prepared modal @@ -514,7 +587,7 @@ var self = this; var popped_item = history_item_set.pop(); if (typeof popped_item === "undefined") { - self.modal.showNotification('All datasets imported', 3000, '#e1f4e0', '#32a427'); + self.modal.showNotification('All datasets imported', 3000, 'success'); // enable button again self.modal.enableButton('Import'); return @@ -584,6 +657,11 @@ $('<form action="'+ url +'" method="'+ (method||'post') +'">'+inputs+'</form>') .appendTo('body').submit().remove(); }; + }, + + // shows modal for creating folder + createFolderModal: function(){ + alert('creating folder'); } }); @@ -606,11 +684,12 @@ tmpl_array.push('<div id="library_container" style="width: 90%; margin: auto; margin-top: 2em; overflow: auto !important; ">'); tmpl_array.push(''); - tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to Martin.</h3>'); + tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to <a href="mailto:marten@bx.psu.edu?Subject=DataLibraries_Feedback" target="_blank">Marten</a>.</h3>'); tmpl_array.push('<a href="" id="create_new_library_btn" class="btn btn-primary icon-file ">New Library</a>'); - tmpl_array.push('<table class="table table-striped">'); + tmpl_array.push('<table class="table table-condensed">'); tmpl_array.push(' <thead>'); - tmpl_array.push(' <th>name</th>'); + tmpl_array.push(' <th class="button_heading"></th>'); + tmpl_array.push(' <th>name</th>'); tmpl_array.push(' <th>description</th>'); tmpl_array.push(' <th>synopsis</th> '); tmpl_array.push(' <th>model type</th> '); @@ -618,7 +697,9 @@ tmpl_array.push(' <tbody>'); tmpl_array.push(' <% _.each(libraries, function(library) { %>'); tmpl_array.push(' <tr>'); - tmpl_array.push(' <td><a href="#/folders/<%- library.get("root_folder_id") %>"><%- library.get("name") %></a></td>'); + tmpl_array.push(' <td><button title="Open this library" type="button" data-id="<%- library.get("root_folder_id") %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-folder-open"></span> browse</td>'); + tmpl_array.push(' <td><%- library.get("name") %></td>'); tmpl_array.push(' <td><%= _.escape(library.get("description")) %></td>'); tmpl_array.push(' <td><%= _.escape(library.get("synopsis")) %></td>'); tmpl_array.push(' <td><%= _.escape(library.get("model_class")) %></td>'); @@ -651,12 +732,11 @@ modal : null, // show/hide create library modal - show_library_modal : function (e){ - // prevent default, may be unnecessary - e.preventDefault(); + show_library_modal : function (event){ + event.preventDefault(); + event.stopPropagation(); // create modal - if (!this.modal){ var self = this; this.modal = new mod_modal.GalaxyModal( { @@ -667,7 +747,6 @@ 'Close' : function() {self.modal.hide()} } }); - } // show prepared modal this.modal.show(); @@ -676,6 +755,7 @@ // create the new library from modal create_new_library_event: function(){ var libraryDetails = this.serialize_new_library(); + var valid = this.validate_new_library(libraryDetails); var library = new Library(); var self = this; library.save(libraryDetails, { @@ -683,7 +763,10 @@ self.modal.hide(); self.clear_library_modal(); self.render(); - } + }, + error: function(){ + self.modal.showNotification('An error occured', 5000, 'error'); + } }); return false; }, @@ -704,6 +787,11 @@ }; }, + validate_new_library: function(library){ + + }, + + // template for new library modal template_new_library: function() { @@ -714,7 +802,6 @@ tmpl_array.push('<input type="text" name="Name" value="" placeholder="Name">'); tmpl_array.push('<input type="text" name="Description" value="" placeholder="Description">'); tmpl_array.push('<input type="text" name="Synopsis" value="" placeholder="Synopsis">'); - tmpl_array.push(''); tmpl_array.push('</form>'); tmpl_array.push('</div>'); diff -r 44399512777bdefb39ea3c99cc04b3f7947c5d33 -r f466834f693ff7ae0f60cf121d97f67a5338ea95 static/scripts/galaxy.modal.js --- a/static/scripts/galaxy.modal.js +++ b/static/scripts/galaxy.modal.js @@ -1,6 +1,5 @@ -/* - galaxy modal -*/ +// galaxy modal + // dependencies define(["libs/backbone/backbone-relational"], function() { @@ -24,24 +23,48 @@ // create if (options) this.create(options); - // bind the ESC key to hide() function - $(document).on('keyup', function(event){ - if (event.keyCode == 27) { self.hide(); } - }) - // bind the click anywhere to hide() function... - $('html').on('click', function(event){ - self.hide(); - }) - // ...but don't hide if the click is on modal content - $('.modal-content').on('click', function(event){ - event.stopPropagation(); - }) + + this.bindClick(event, self); }, + // bind the click-to-hide function + bindClick: function(event, that) { + // bind the ESC key to hide() function + $(document).on('keyup', function(event){ + if (event.keyCode == 27) { self.hide(); } + }) + // bind the click anywhere to hide() function... + $('html').on('click', function(event){ + that.hide(); + }) + // ...but don't hide if the click is on modal content + $('.modal-content').on('click', function(event){ + event.stopPropagation(); + }) + }, + + // unbind the click-to-hide function + unbindClick: function(event, that){ + // bind the ESC key to hide() function + $(document).off('keyup', function(event){ + if (event.keyCode == 27) { that.hide(); } + }) + // unbind the click anywhere to hide() function... + $('html').off('click', function(event){ + that.hide(); + }) + // ...but don't hide if the click is on modal content + $('.modal-content').off('click', function(event){ + event.stopPropagation(); + }) + }, + + // destroy destroy : function(){ this.hide(); - $('.modal').html(''); + this.unbindClick(); + $('.modal').remove(); }, // adds and displays a new frame/window @@ -152,16 +175,28 @@ }, // show notification - showNotification : function(message, duration, bgColor, txtColor) { + showNotification : function(message, duration, type) { // defaults var duration = typeof duration !== 'undefined' ? duration : 1500; - var bgColor = typeof bgColor !== 'undefined' ? bgColor : "#F4E0E1"; - var txtColor = typeof txtColor !== 'undefined' ? txtColor : "#A42732"; + // var bgColor = typeof bgColor !== 'undefined' ? bgColor : "#F4E0E1"; + // var txtColor = typeof txtColor !== 'undefined' ? txtColor : "#A42732"; + var bgColor; + var txtColor; + + if (type === 'error'){ + bgColor = '#f4e0e1'; + txtColor = '#a42732'; + // } else if (type === 'success'){ + } else { // success is default + bgColor = '#e1f4e0'; + txtColor = '#32a427'; + } var HTMLmessage = "<div class='notification-message' style='text-align:center; line-height:16px; '> " + message + " </div>"; this.$notification.html("<div id='notification-bar' style='display:none; float: right; height: 16px; width:100%; background-color: " + bgColor + "; z-index: 100; color: " + txtColor + ";border-bottom: 1px solid " + txtColor + ";'>" + HTMLmessage + "</div>"); var self = this; + /*animate the bar*/ $('#notification-bar').slideDown(function() { setTimeout(function() { diff -r 44399512777bdefb39ea3c99cc04b3f7947c5d33 -r f466834f693ff7ae0f60cf121d97f67a5338ea95 static/style/src/less/library.less --- a/static/style/src/less/library.less +++ b/static/style/src/less/library.less @@ -29,6 +29,50 @@ background-color: @table-bg-accent; } + +tr.light td +{ + background-color: white; + color: black; +} +tr.light:hover td +{ + background-color: #f5f5f5; + color: #8389a1; +} +tr.dark td +{ + background-color: #8389a1; + color: white; +} +tr.dark:hover td +{ + background-color: #bbbfd0; + color: white; +} +a.dark:hover +{ + color: yellow; + // text-decoration: none; +} +a.dark +{ + color: white; + // text-decoration: none; +} +th.button_heading +{ + width: 7em; +} +div.library_breadcrumb{ + padding-top: 0.8em; + padding-bottom: 0.8em; +} +div.library_breadcrumb a:hover{ + color:green; +} + + img.expanderIcon { padding-right: 4px; } https://bitbucket.org/galaxy/galaxy-central/commits/e2b8d3481d66/ Changeset: e2b8d3481d66 User: martenson Date: 2013-12-11 00:02:36 Summary: Merge data libraries branch to default Affected #: 19 files diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 .hgignore --- a/.hgignore +++ b/.hgignore @@ -116,3 +116,6 @@ *.rej *~ + +syntax: regexp +^static/AAA_scratch$ \ No newline at end of file diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 lib/galaxy/model/__init__.py --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -1827,7 +1827,7 @@ class Library( object, Dictifiable ): permitted_actions = get_permitted_actions( filter='LIBRARY' ) dict_collection_visible_keys = ( 'id', 'name' ) - dict_element_visible_keys = ( 'id', 'deleted', 'name', 'description', 'synopsis' ) + dict_element_visible_keys = ( 'id', 'deleted', 'name', 'description', 'synopsis', 'root_folder_id' ) def __init__( self, name=None, description=None, synopsis=None, root_folder=None ): self.name = name or "Unnamed library" self.description = description @@ -1894,7 +1894,7 @@ return name class LibraryFolder( object, Dictifiable ): - dict_element_visible_keys = ( 'id', 'parent_id', 'name', 'description', 'item_count', 'genome_build' ) + dict_element_visible_keys = ( 'id', 'parent_id', 'name', 'description', 'item_count', 'genome_build', 'update_time' ) def __init__( self, name=None, description=None, item_count=0, order_id=None ): self.name = name or "Unnamed folder" self.description = description @@ -2060,6 +2060,7 @@ genome_build = ldda.dbkey, misc_info = ldda.info, misc_blurb = ldda.blurb, + peek = ( lambda ldda: ldda.display_peek() if ldda.peek and ldda.peek != 'no peek' else None )( ldda ), template_data = template_data ) if ldda.dataset.uuid is None: rval['uuid'] = None diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 lib/galaxy/model/mapping.py --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -1878,8 +1878,9 @@ table = self.table trans = conn.begin() try: - next_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() - table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid + 1 ) ) + current_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() + next_hid = current_hid + 1 + table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid ) ) trans.commit() return next_hid except: diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 lib/galaxy/webapps/galaxy/api/datasets.py --- a/lib/galaxy/webapps/galaxy/api/datasets.py +++ b/lib/galaxy/webapps/galaxy/api/datasets.py @@ -1,5 +1,5 @@ """ -API operations on the contents of a dataset. +API operations on the contents of a history dataset. """ from galaxy import web from galaxy.visualization.data_providers.genome import FeatureLocationIndexDataProvider diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 lib/galaxy/webapps/galaxy/api/folder_contents.py --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -1,5 +1,5 @@ """ -API operations on the contents of a library. +API operations on the contents of a folder. """ import logging, os, string, shutil, urllib, re, socket from cgi import escape, FieldStorage @@ -11,67 +11,125 @@ log = logging.getLogger( __name__ ) class FolderContentsController( BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems ): + """ + Class controls retrieval, creation and updating of folder contents. + """ + + def load_folder_contents( self, trans, folder ): + """ + Loads all contents of the folder (folders and data sets) but only in the first level. + """ + current_user_roles = trans.get_current_user_roles() + is_admin = trans.user_is_admin() + content_items = [] + for subfolder in folder.active_folders: + if not is_admin: + can_access, folder_ids = trans.app.security_agent.check_folder_contents( trans.user, current_user_roles, subfolder ) + if (is_admin or can_access) and not subfolder.deleted: + subfolder.api_type = 'folder' + content_items.append( subfolder ) + for dataset in folder.datasets: + if not is_admin: + can_access = trans.app.security_agent.can_access_dataset( current_user_roles, dataset.library_dataset_dataset_association.dataset ) + if (is_admin or can_access) and not dataset.deleted: + dataset.api_type = 'file' + content_items.append( dataset ) + return content_items @web.expose_api def index( self, trans, folder_id, **kwd ): """ GET /api/folders/{encoded_folder_id}/contents Displays a collection (list) of a folder's contents (files and folders). - The /api/library_contents/{encoded_library_id}/contents - lists everything in a library recursively, which is not what - we want here. We could add a parameter to use the recursive - style, but this is meant to act similar to an "ls" directory listing. + Encoded folder ID is prepended with 'F' if it is a folder as opposed to a data set which does not have it. + Full path is provided as a separate object in response providing data for breadcrumb path building. """ - rval = [] + folder_container = [] current_user_roles = trans.get_current_user_roles() - def traverse( folder ): - admin = trans.user_is_admin() - rval = [] - for subfolder in folder.active_folders: - if not admin: - can_access, folder_ids = trans.app.security_agent.check_folder_contents( trans.user, current_user_roles, subfolder ) - if (admin or can_access) and not subfolder.deleted: - subfolder.api_type = 'folder' - rval.append( subfolder ) - for ld in folder.datasets: - if not admin: - can_access = trans.app.security_agent.can_access_dataset( current_user_roles, ld.library_dataset_dataset_association.dataset ) - if (admin or can_access) and not ld.deleted: - ld.api_type = 'file' - rval.append( ld ) - return rval - - try: - decoded_folder_id = trans.security.decode_id( folder_id[-16:] ) - except TypeError: - trans.response.status = 400 - return "Malformed folder id ( %s ) specified, unable to decode." % str( folder_id ) + if ( folder_id.startswith( 'F' ) ): + try: + decoded_folder_id = trans.security.decode_id( folder_id[1:] ) + except TypeError: + trans.response.status = 400 + return "Malformed folder id ( %s ) specified, unable to decode." % str( folder_id ) try: folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( decoded_folder_id ) - parent_library = folder.parent_library except: folder = None - log.error( "FolderContentsController.index: Unable to retrieve folder %s" - % folder_id ) + log.error( "FolderContentsController.index: Unable to retrieve folder with ID: %s" % folder_id ) - # TODO: Find the API's path to this folder if necessary. - # This was needed in recursive descent, but it's not needed - # for "ls"-style content checking: - if not folder or not ( trans.user_is_admin() or trans.app.security_agent.can_access_library_item( current_user_roles, folder, trans.user ) ): + # We didn't find the folder or user does not have an access to it. + if not folder: trans.response.status = 400 return "Invalid folder id ( %s ) specified." % str( folder_id ) + + if not ( trans.user_is_admin() or trans.app.security_agent.can_access_library_item( current_user_roles, folder, trans.user ) ): + log.warning( "SECURITY: User (id: %s) without proper access rights is trying to load folder with ID of %s" % ( trans.user.id, folder.id ) ) + trans.response.status = 400 + return "Invalid folder id ( %s ) specified." % str( folder_id ) + + path_to_root = [] + def build_path ( folder ): + """ + Search the path upwards recursively and load the whole route of names and ids for breadcrumb purposes. + """ + path_to_root = [] + # We are almost in root + if folder.parent_id is None: + path_to_root.append( ( 'F' + trans.security.encode_id( folder.id ), folder.name ) ) + else: + # We add the current folder and traverse up one folder. + path_to_root.append( ( 'F' + trans.security.encode_id( folder.id ), folder.name ) ) + upper_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( folder.parent_id ) + path_to_root.extend( build_path( upper_folder ) ) + return path_to_root + + # Return the reversed path so it starts with the library node. + full_path = build_path( folder )[::-1] + folder_container.append( dict( full_path = full_path ) ) + + folder_contents = [] + time_updated = '' + time_created = '' + # Go through every item in the folder and include its meta-data. + for content_item in self.load_folder_contents( trans, folder ): +# rval = content_item.to_dict() + return_item = {} + encoded_id = trans.security.encode_id( content_item.id ) + time_updated = content_item.update_time.strftime( "%Y-%m-%d %I:%M %p" ) + time_created = content_item.create_time.strftime( "%Y-%m-%d %I:%M %p" ) +# log.debug('XXXXXXXXXXXXXXXXXXX api type: ' + str(content_item.api_type)) +# log.debug('XXXXXXXX ALL: ' + str(content_item.__dict__)) + + # For folder return also hierarchy values + if content_item.api_type == 'folder': + encoded_id = 'F' + encoded_id +# time_updated = content_item.update_time.strftime( "%Y-%m-%d %I:%M %p" ) + return_item.update ( dict ( item_count = content_item.item_count ) ) - for content in traverse( folder ): - encoded_id = trans.security.encode_id( content.id ) - if content.api_type == 'folder': - encoded_id = 'F' + encoded_id - rval.append( dict( id = encoded_id, - type = content.api_type, - name = content.name, - url = url_for( 'folder_contents', folder_id=encoded_id ) ) ) - return rval + if content_item.api_type == 'file': +# log.debug('XXXXX content item class: ' + str(content_item.__class__)) + library_dataset_dict = content_item.to_dict() + library_dataset_dict['data_type'] + library_dataset_dict['file_size'] + library_dataset_dict['date_uploaded'] + return_item.update ( dict ( data_type = library_dataset_dict['data_type'], + file_size = library_dataset_dict['file_size'], + date_uploaded = library_dataset_dict['date_uploaded'] ) ) + + # For every item return also the default meta-data + return_item.update( dict( id = encoded_id, + type = content_item.api_type, + name = content_item.name, + time_updated = time_updated, + time_created = time_created + ) ) + folder_contents.append( return_item ) + # Put the data in the container + folder_container.append( dict( folder_contents = folder_contents ) ) + return folder_container @web.expose_api def show( self, trans, id, library_id, **kwd ): diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 lib/galaxy/webapps/galaxy/api/lda_datasets.py --- /dev/null +++ b/lib/galaxy/webapps/galaxy/api/lda_datasets.py @@ -0,0 +1,241 @@ +""" +API operations on the dataset from library. +""" +import glob +import logging +import operator +import os +import os.path +import string +import sys +import tarfile +import tempfile +import urllib +import urllib2 +import zipfile +from galaxy.security import Action +from galaxy import util, web +from galaxy.util.streamball import StreamBall +from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixinItems + +import logging +log = logging.getLogger( __name__ ) + +# Test for available compression types +# tmpd = tempfile.mkdtemp() +# comptypes = [] +# for comptype in ( 'gz', 'bz2' ): +# tmpf = os.path.join( tmpd, 'compression_test.tar.' + comptype ) +# try: +# archive = tarfile.open( tmpf, 'w:' + comptype ) +# archive.close() +# comptypes.append( comptype ) +# except tarfile.CompressionError: +# log.exception( "Compression error when testing %s compression. This option will be disabled for library downloads." % comptype ) +# try: +# os.unlink( tmpf ) +# except OSError: +# pass +ziptype = '32' +# tmpf = os.path.join( tmpd, 'compression_test.zip' ) +# try: +# archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED, True ) +# archive.close() +# comptypes.append( 'zip' ) +# ziptype = '64' +# except RuntimeError: +# log.exception( "Compression error when testing zip compression. This option will be disabled for library downloads." ) +# except (TypeError, zipfile.LargeZipFile): +# # ZIP64 is only in Python2.5+. Remove TypeError when 2.4 support is dropped +# log.warning( 'Max zip file size is 2GB, ZIP64 not supported' ) +# comptypes.append( 'zip' ) +# try: +# os.unlink( tmpf ) +# except OSError: +# pass +# os.rmdir( tmpd ) + + + +class DatasetsController( BaseAPIController, UsesLibraryMixinItems ): + + @web.expose_api + def show( self, trans, id, **kwd ): + """ + GET /api/libraries/datasets/{encoded_dataset_id} + Displays information about the dataset identified by the lda ID. + """ + # Get dataset. + try: + dataset = self.get_library_dataset( trans, id = id ) + except Exception, e: + return str( e ) + try: + # Default: return dataset as dict. + rval = dataset.to_dict() + except Exception, e: + rval = "Error in dataset API at listing contents: " + str( e ) + log.error( rval + ": %s" % str(e), exc_info=True ) + trans.response.status = 500 + + rval['id'] = trans.security.encode_id(rval['id']); + rval['ldda_id'] = trans.security.encode_id(rval['ldda_id']); + rval['folder_id'] = 'f' + trans.security.encode_id(rval['folder_id']) + + return rval + + @web.expose + def download( self, trans, format, **kwd ): + """ + POST /api/libraries/datasets/download/{format} + POST data: ldda_ids = [] + Downloads dataset(s) in the requested format. + """ + lddas = [] +# is_admin = trans.user_is_admin() +# current_user_roles = trans.get_current_user_roles() + + datasets_to_download = kwd['ldda_ids%5B%5D'] + + if ( datasets_to_download != None ): + datasets_to_download = util.listify( datasets_to_download ) + for dataset_id in datasets_to_download: + try: + ldda = trans.sa_session.query( trans.app.model.LibraryDatasetDatasetAssociation ).get( trans.security.decode_id( dataset_id ) ) + assert not ldda.dataset.purged + lddas.append( ldda ) + except: + ldda = None + message += "Invalid library dataset id (%s) specified. " % str( dataset_id ) + + if format in [ 'zip','tgz','tbz' ]: + error = False + killme = string.punctuation + string.whitespace + trantab = string.maketrans(killme,'_'*len(killme)) + try: + outext = 'zip' + if format == 'zip': + # Can't use mkstemp - the file must not exist first + tmpd = tempfile.mkdtemp() + util.umask_fix_perms( tmpd, trans.app.config.umask, 0777, self.app.config.gid ) + tmpf = os.path.join( tmpd, 'library_download.' + format ) + if ziptype == '64' and trans.app.config.upstream_gzip: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_STORED, True ) + elif ziptype == '64': + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED, True ) + elif trans.app.config.upstream_gzip: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_STORED ) + else: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED ) + archive.add = lambda x, y: archive.write( x, y.encode('CP437') ) + elif format == 'tgz': + if trans.app.config.upstream_gzip: + archive = StreamBall( 'w|' ) + outext = 'tar' + else: + archive = StreamBall( 'w|gz' ) + outext = 'tgz' + elif format == 'tbz': + archive = StreamBall( 'w|bz2' ) + outext = 'tbz2' + except ( OSError, zipfile.BadZipfile ): + error = True + log.exception( "Unable to create archive for download" ) + message = "Unable to create archive for download, please report this error" + status = 'error' + except: + error = True + log.exception( "Unexpected error %s in create archive for download" % sys.exc_info()[0] ) + message = "Unable to create archive for download, please report - %s" % sys.exc_info()[0] + status = 'error' + if not error: + composite_extensions = trans.app.datatypes_registry.get_composite_extensions() + seen = [] + for ldda in lddas: + if ldda.dataset.state in [ 'new', 'upload', 'queued', 'running', 'empty', 'discarded' ]: + continue + ext = ldda.extension + is_composite = ext in composite_extensions + path = "" + parent_folder = ldda.library_dataset.folder + while parent_folder is not None: + # Exclude the now-hidden "root folder" + if parent_folder.parent is None: + path = os.path.join( parent_folder.library_root[0].name, path ) + break + path = os.path.join( parent_folder.name, path ) + parent_folder = parent_folder.parent + path += ldda.name + while path in seen: + path += '_' + seen.append( path ) + zpath = os.path.split(path)[-1] # comes as base_name/fname + outfname,zpathext = os.path.splitext(zpath) + if is_composite: + # need to add all the components from the extra_files_path to the zip + if zpathext == '': + zpath = '%s.html' % zpath # fake the real nature of the html file + try: + archive.add(ldda.dataset.file_name,zpath) # add the primary of a composite set + except IOError: + error = True + log.exception( "Unable to add composite parent %s to temporary library download archive" % ldda.dataset.file_name) + message = "Unable to create archive for download, please report this error" + status = 'error' + continue + flist = glob.glob(os.path.join(ldda.dataset.extra_files_path,'*.*')) # glob returns full paths + for fpath in flist: + efp,fname = os.path.split(fpath) + if fname > '': + fname = fname.translate(trantab) + try: + archive.add( fpath,fname ) + except IOError: + error = True + log.exception( "Unable to add %s to temporary library download archive %s" % (fname,outfname)) + message = "Unable to create archive for download, please report this error" + status = 'error' + continue + else: # simple case + try: + archive.add( ldda.dataset.file_name, path ) + except IOError: + error = True + log.exception( "Unable to write %s to temporary library download archive" % ldda.dataset.file_name) + message = "Unable to create archive for download, please report this error" + status = 'error' + if not error: + lname = 'selected_dataset' + fname = lname.replace( ' ', '_' ) + '_files' + if format == 'zip': + archive.close() + trans.response.set_content_type( "application/octet-stream" ) + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) + archive = util.streamball.ZipBall(tmpf, tmpd) + archive.wsgi_status = trans.response.wsgi_status() + archive.wsgi_headeritems = trans.response.wsgi_headeritems() + return archive.stream + else: + trans.response.set_content_type( "application/x-tar" ) + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) + archive.wsgi_status = trans.response.wsgi_status() + archive.wsgi_headeritems = trans.response.wsgi_headeritems() + return archive.stream + elif format == 'uncompressed': + if len(lddas) != 1: + return 'Wrong request' + else: + single_dataset = lddas[0] + trans.response.set_content_type( single_dataset.get_mime() ) + fStat = os.stat( ldda.file_name ) + trans.response.headers[ 'Content-Length' ] = int( fStat.st_size ) + valid_chars = '.,^_-()[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + fname = ldda.name + fname = ''.join( c in valid_chars and c or '_' for c in fname )[ 0:150 ] + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s"' % fname + try: + return open( single_dataset.file_name ) + except: + return 'This dataset contains no content' + else: + return 'Wrong format'; \ No newline at end of file diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 lib/galaxy/webapps/galaxy/api/libraries.py --- a/lib/galaxy/webapps/galaxy/api/libraries.py +++ b/lib/galaxy/webapps/galaxy/api/libraries.py @@ -49,9 +49,10 @@ trans.model.Library.table.c.id.in_( accessible_restricted_library_ids ) ) ) rval = [] for library in query: - item = library.to_dict() + item = library.to_dict( view='element' ) item['url'] = url_for( route, id=trans.security.encode_id( library.id ) ) - item['id'] = trans.security.encode_id( item['id'] ) + item['id'] = 'F' + trans.security.encode_id( item['id'] ) + item['root_folder_id'] = 'F' + trans.security.encode_id( item['root_folder_id'] ) rval.append( item ) return rval @@ -131,6 +132,9 @@ rval['name'] = name rval['id'] = encoded_id return rval + + def edit( self, trans, payload, **kwd ): + return "Not implemented yet" @web.expose_api def delete( self, trans, id, **kwd ): diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -46,6 +46,7 @@ atexit.register( app.shutdown ) # Create the universe WSGI application webapp = GalaxyWebApplication( app, session_cookie='galaxysession', name='galaxy' ) + # Add the controllers folder # Handle displaying tool help images and README file images contained in repositories installed from the tool shed. webapp.add_route( '/admin_toolshed/static/images/:repository_id/:image_file', controller='admin_toolshed', @@ -53,11 +54,17 @@ repository_id=None, image_file=None ) webapp.add_ui_controllers( 'galaxy.webapps.galaxy.controllers', app ) + # Add the api folder + webapp.add_api_controllers( 'galaxy.webapps.galaxy.api', app ) + # Add the api folder VERSION 2 + webapp.add_api_controllers( 'galaxy.webapps.galaxy.api.v2', app ) + # Force /history to go to /root/history -- needed since the tests assume this webapp.add_route( '/history', controller='root', action='history' ) # Force /activate to go to the controller webapp.add_route( '/activate', controller='user', action='activate' ) # These two routes handle our simple needs at the moment + webapp.add_route( '/async/:tool_id/:data_id/:data_secret', controller='async', action='index', tool_id=None, data_id=None, data_secret=None ) webapp.add_route( '/:controller/:action', action='index' ) webapp.add_route( '/:action', controller='root', action='index' ) @@ -66,8 +73,6 @@ webapp.add_route( '/datasets/:dataset_id/display/{filename:.+?}', controller='dataset', action='display', dataset_id=None, filename=None) webapp.add_route( '/datasets/:dataset_id/:action/:filename', controller='dataset', action='index', dataset_id=None, filename=None) webapp.add_route( '/display_application/:dataset_id/:app_name/:link_name/:user_id/:app_action/:action_param', - controller='dataset', action='display_application', dataset_id=None, user_id=None, - app_name = None, link_name = None, app_action = None, action_param = None ) webapp.add_route( '/u/:username/d/:slug/:filename', controller='dataset', action='display_by_username_and_slug', filename=None ) webapp.add_route( '/u/:username/p/:slug', controller='page', action='display_by_username_and_slug' ) webapp.add_route( '/u/:username/h/:slug', controller='history', action='display_by_username_and_slug' ) @@ -171,9 +176,44 @@ #webapp.mapper.connect( 'run_workflow', '/api/workflow/{workflow_id}/library/{library_id}', controller='workflows', action='run', workflow_id=None, library_id=None, conditions=dict(method=["GET"]) ) webapp.mapper.resource( 'search', 'search', path_prefix='/api' ) + webapp.mapper.resource_with_deleted( 'quota', 'quotas', path_prefix='/api' ) + webapp.mapper.resource_with_deleted( 'history', 'histories', path_prefix='/api' ) + webapp.mapper.resource_with_deleted( 'user', 'users', path_prefix='/api' ) + + # ======================= + # ===== LIBRARY API ===== + # ======================= + + webapp.mapper.connect( 'show_lda_item', '/api/libraries/datasets/:id', controller='lda_datasets', action='show', conditions=dict(method=["GET"]) ) + webapp.mapper.connect( 'download_lda_items', '/api/libraries/datasets/download/:format', controller='lda_datasets', action='download', conditions=dict(method=["POST"]) ) + + webapp.mapper.resource_with_deleted( 'library', 'libraries', path_prefix='/api' ) + webapp.mapper.resource( 'folder', 'folders', path_prefix='/api' ) + + webapp.mapper.resource( 'content', 'contents', + controller='folder_contents', + name_prefix='folder_', + path_prefix='/api/folders/:folder_id', + parent_resources=dict( member_name='folder', collection_name='folders' ) ) + webapp.mapper.resource( 'content', + 'contents', + controller='library_contents', + name_prefix='library_', + path_prefix='/api/libraries/:library_id', + parent_resources=dict( member_name='library', collection_name='libraries' ) ) + webapp.mapper.resource( 'permission', + 'permissions', + path_prefix='/api/libraries/:library_id', + parent_resources=dict( member_name='library', collection_name='libraries' ) ) + _add_item_extended_metadata_controller( webapp, + name_prefix="library_dataset_", + path_prefix='/api/libraries/:library_id/contents/:library_content_id' ) + + # add as a non-ATOM API call to support the notion of a 'current/working' history unique to the history resource webapp.mapper.connect( "set_as_current", "/api/histories/{id}/set_as_current", controller="histories", action="set_as_current", conditions=dict( method=["POST"] ) ) + webapp.mapper.connect( "set_as_current", "/api/histories/{id}/set_as_current", controller="histories", action="set_as_current", conditions=dict( method=["POST"] ) ) webapp.mapper.connect( "create_api_key", "/api/users/:user_id/api_key", controller="users", action="api_key", user_id=None, conditions=dict( method=["POST"] ) ) @@ -181,6 +221,7 @@ # visualizations registry generic template renderer webapp.add_route( '/visualization/show/:visualization_name', controller='visualization', action='render', visualization_name=None ) + webapp.add_route( '/visualization/show/:visualization_name', controller='visualization', action='render', visualization_name=None ) # "POST /api/workflows/import" => ``workflows.import_workflow()``. # Defines a named route "import_workflow". @@ -188,6 +229,15 @@ webapp.mapper.connect("workflow_dict", '/api/workflows/{workflow_id}/download', controller='workflows', action='workflow_dict', conditions=dict(method=['GET'])) # Preserve the following download route for now for dependent applications -- deprecate at some point webapp.mapper.connect("workflow_dict", '/api/workflows/download/{workflow_id}', controller='workflows', action='workflow_dict', conditions=dict(method=['GET'])) + + + # Handle displaying tool help images and README file images contained in repositories installed from the tool shed. + webapp.add_route( '/admin_toolshed/static/images/:repository_id/:image_file', + controller='admin_toolshed', + action='display_image_in_repository', + repository_id=None, + image_file=None ) + # Galaxy API for tool shed features. webapp.mapper.resource( 'tool_shed_repository', 'tool_shed_repositories', diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 lib/galaxy/webapps/galaxy/controllers/library.py --- a/lib/galaxy/webapps/galaxy/controllers/library.py +++ b/lib/galaxy/webapps/galaxy/controllers/library.py @@ -76,6 +76,17 @@ library_list_grid = LibraryListGrid() + + @web.expose + def list( self, trans, **kwd ): + params = util.Params( kwd ) + # define app configuration for generic mako template + app = { + 'jscript' : "galaxy.library" + } + # fill template + return trans.fill_template('galaxy.panels.mako', config = {'app' : app}) + @web.expose def index( self, trans, **kwd ): params = util.Params( kwd ) diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 static/scripts/galaxy.library.js --- /dev/null +++ b/static/scripts/galaxy.library.js @@ -0,0 +1,860 @@ +// MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +// === GALAXY LIBRARY MODULE ==== +// MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM + +// global variables +var view = null; +var library_router = null; +var responses = []; + + +// load required libraries +// require([ +// // load js libraries +// 'utils/galaxy.css', +// ], function(css){ +// // load css +// css.load_file("static/style/library.css"); +// }); + +// dependencies +define(["galaxy.modal", "galaxy.masthead", "utils/galaxy.utils"], function(mod_modal, mod_masthead, mod_utils) { + +// MMMMMMMMMMMMMMM +// === Models ==== +// MMMMMMMMMMMMMMM + + // LIBRARY + var Library = Backbone.Model.extend({ + urlRoot: '/api/libraries' + }); + + // LIBRARIES + var Libraries = Backbone.Collection.extend({ + url: '/api/libraries', + model: Library + }); + + // ITEM + var Item = Backbone.Model.extend({ + urlRoot : '/api/libraries/datasets' + }) + + // FOLDER + var Folder = Backbone.Collection.extend({ + model: Item + }) + + // CONTAINER for folder contents (folders, items and metadata). + var FolderContainer = Backbone.Model.extend({ + defaults : { + folder : new Folder(), + full_path : "unknown", + urlRoot : "/api/folders/", + id : "unknown" + }, + parse : function(obj) { + this.full_path = obj[0].full_path; + // update the inner collection + this.get("folder").reset(obj[1].folder_contents); + return obj; + } + }) + + // HISTORY ITEM + var HistoryItem = Backbone.Model.extend({ + urlRoot : '/api/histories/' + }); + + // HISTORY + var GalaxyHistory = Backbone.Model.extend({ + url : '/api/histories/' + }); + + // HISTORIES + var GalaxyHistories = Backbone.Collection.extend({ + url : '/api/histories', + model : GalaxyHistory + }); + + //ROUTER + var LibraryRouter = Backbone.Router.extend({ + routes: { + "" : "libraries", + "folders/:id" : "folder_content", + "folders/:folder_id/download/:format" : "download" + } + }); + + +// MMMMMMMMMMMMMM +// === VIEWS ==== +// MMMMMMMMMMMMMM + +// galaxy folder +var FolderContentView = Backbone.View.extend({ + // main element definition + el : '#center', + // progress percentage + progress: 0, + // progress rate per one item + progressStep: 1, + // last selected history in modal for UX + lastSelectedHistory: '', + // self modal + modal : null, + // loaded folders + folders : null, + + // initialize + initialize : function(){ + this.folders = []; + this.queue = jQuery.Deferred(); + this.queue.resolve(); + }, + +// MMMMMMMMMMMMMMMMMM +// === TEMPLATES ==== +// MMMMMMMMMMMMMMMMMM + + // set up + templateFolder : function (){ + var tmpl_array = []; + + // CONTAINER + tmpl_array.push('<div id="library_container" style="width: 90%; margin: auto; margin-top: 2em; ">'); + tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to <a href="mailto:marten@bx.psu.edu?Subject=DataLibraries_Feedback" target="_blank">Marten</a>.</h3>'); + + // TOOLBAR + tmpl_array.push('<div id="library_folder_toolbar" >'); + tmpl_array.push(' <button title="Create New Folder" id="toolbtn_create_folder" class="btn btn-primary" type="button"><span class="fa fa-icon-plus"></span><span class="fa fa-icon-folder-close"></span> folder</button>'); + tmpl_array.push(' <button id="toolbtn_bulk_import" class="btn btn-primary" style="display: none; margin-left: 0.5em;" type="button"><span class="fa fa-icon-external-link"></span> to history</button>'); + + tmpl_array.push(' <div id="toolbtn_dl" class="btn-group" style="margin-left: 0.5em; display: none; ">'); + tmpl_array.push(' <button id="drop_toggle" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">'); + tmpl_array.push(' <span class="fa fa-icon-download"></span> download <span class="caret"></span>'); + tmpl_array.push(' </button>'); + tmpl_array.push(' <ul class="dropdown-menu" role="menu">'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/tgz">.tar.gz</a></li>'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/tbz">.tar.bz</a></li>'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/zip">.zip</a></li>'); + tmpl_array.push(' </ul>'); + tmpl_array.push(' </div>'); + + tmpl_array.push('</div>'); + + // BREADCRUMBS + tmpl_array.push('<div class="library_breadcrumb">'); + tmpl_array.push('<a title="Return to the list of libraries" href="#">Libraries</a><b>|</b> '); + tmpl_array.push('<% _.each(path, function(path_item) { %>'); + tmpl_array.push('<% if (path_item[0] != id) { %>'); + tmpl_array.push('<a title="Return to this folder" href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a><b>|</b> '); + tmpl_array.push('<% } else { %>'); + tmpl_array.push('<span title="You are in this folder"><%- path_item[1] %></span>'); + tmpl_array.push('<% } %>'); + tmpl_array.push('<% }); %>'); + tmpl_array.push('</div>'); + + // FOLDER CONTENT + tmpl_array.push('<table id="folder_table" class="table table-condensed">'); + tmpl_array.push(' <thead>'); + tmpl_array.push(' <th style="text-align: center; width: 20px; "><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th>'); + tmpl_array.push(' <th class="button_heading">view</th>'); + tmpl_array.push(' <th>name</th>'); + tmpl_array.push(' <th>data type</th>'); + tmpl_array.push(' <th>size</th>'); + tmpl_array.push(' <th>date</th>'); + tmpl_array.push(' </thead>'); + tmpl_array.push(' <tbody>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td><button title="Go to parent folder" type="button" data-id="<%- upper_folder_id %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-arrow-up"></span> .. go up</td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% _.each(items, function(content_item) { %>'); + tmpl_array.push(' <tr class="folder_row light" id="<%- content_item.id %>">'); + tmpl_array.push(' <% if (content_item.get("type") === "folder") { %>'); // folder + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td><button title="Open this folder" type="button" data-id="<%- content_item.id %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-folder-open"></span> browse</td>'); + tmpl_array.push(' <td><%- content_item.get("name") %>'); + tmpl_array.push(' <% if (content_item.get("item_count") === 0) { %>'); // empty folder + tmpl_array.push(' <span class="muted">(empty folder)</span>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' </td>'); + tmpl_array.push(' <td>folder</td>'); // data type + tmpl_array.push(' <td><%= _.escape(content_item.get("item_count")) %> item(s)</td>'); // size + tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <td style="text-align: center; "><input style="margin: 0;" type="checkbox"></td>'); + tmpl_array.push(' <td>'); + tmpl_array.push(' <button title="See details of this dataset" type="button" class="library-dataset btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-eye-open"></span> details'); + tmpl_array.push(' </button>'); + tmpl_array.push(' </td>'); + tmpl_array.push(' <td><%- content_item.get("name") %></td>'); // dataset + tmpl_array.push(' <td><%= _.escape(content_item.get("data_type")) %></td>'); // data type + tmpl_array.push(' <td><%= _.escape(content_item.get("readable_size")) %></td>'); // size + tmpl_array.push(' <% } %> '); + tmpl_array.push(' <td><%= _.escape(content_item.get("time_updated")) %></td>'); // time updated + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push(' '); + tmpl_array.push(' </tbody>'); + tmpl_array.push('</table>'); + + tmpl_array.push('</div>'); + return tmpl_array.join(''); + }, + templateDatasetModal : function(){ + var tmpl_array = []; + + tmpl_array.push('<div id="dataset_info_modal">'); + tmpl_array.push(' <table class="table table-striped table-condensed">'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row" id="id_row" data-id="<%= _.escape(item.get("ldda_id")) %>">Name</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("name")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Data type</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("data_type")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Genome build</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("genome_build")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <th scope="row">Size</th>'); + tmpl_array.push(' <td><%= _.escape(size) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Date uploaded</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("date_uploaded")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Uploaded by</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("uploaded_by")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr scope="row">'); + tmpl_array.push(' <th scope="row">Data Lines</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_data_lines")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <th scope="row">Comment Lines</th>'); + tmpl_array.push(' <% if (item.get("metadata_comment_lines") === "") { %>'); //folder + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_comment_lines")) %></td>'); + tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <td scope="row">unknown</td>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Number of Columns</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_columns")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Column Types</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_column_types")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Miscellaneous information</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("misc_blurb")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' </table>'); + tmpl_array.push(' <pre class="peek">'); + tmpl_array.push(' </pre>'); + tmpl_array.push('</div>'); + + return tmpl_array.join(''); + }, + + templateHistorySelectInModal : function(){ + var tmpl_array = []; + + tmpl_array.push('<span id="history_modal_combo" style="width:90%; margin-left: 1em; margin-right: 1em; ">'); + tmpl_array.push('Select history: '); + tmpl_array.push('<select id="dataset_import_single" name="dataset_import_single" style="width:50%; margin-bottom: 1em; "> '); + tmpl_array.push(' <% _.each(histories, function(history) { %>'); //history select box + tmpl_array.push(' <option value="<%= _.escape(history.get("id")) %>"><%= _.escape(history.get("name")) %></option>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push('</select>'); + tmpl_array.push('</span>'); + + return tmpl_array.join(''); + }, + + templateBulkImportInModal : function(){ + var tmpl_array = []; + + tmpl_array.push('<span id="history_modal_combo_bulk" style="width:90%; margin-left: 1em; margin-right: 1em; ">'); + tmpl_array.push('Select history: '); + tmpl_array.push('<select id="dataset_import_bulk" name="dataset_import_bulk" style="width:50%; margin-bottom: 1em; "> '); + tmpl_array.push(' <% _.each(histories, function(history) { %>'); //history select box + tmpl_array.push(' <option value="<%= _.escape(history.get("id")) %>"><%= _.escape(history.get("name")) %></option>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push('</select>'); + tmpl_array.push('</span>'); + + return tmpl_array.join(''); + }, + + // convert size to nice string + size_to_string : function (size) + { + // identify unit + var unit = ""; + if (size >= 100000000000) { size = size / 100000000000; unit = "TB"; } else + if (size >= 100000000) { size = size / 100000000; unit = "GB"; } else + if (size >= 100000) { size = size / 100000; unit = "MB"; } else + if (size >= 100) { size = size / 100; unit = "KB"; } else + { size = size * 10; unit = "b"; } + // return formatted string + return (Math.round(size) / 10) + unit; + }, + +// MMMMMMMMMMMMMMM +// === EVENTS ==== +// MMMMMMMMMMMMMMM + + // event binding + events: { + 'click #select-all-checkboxes' : 'selectAll', + 'click .folder_row' : 'selectClicked', + 'click #toolbtn_bulk_import' : 'modalBulkImport', + 'click #toolbtn_dl' : 'bulkDownload', + 'click .library-dataset' : 'showDatasetDetails', + 'click #toolbtn_create_folder' : 'createFolderModal', + 'click .btn_open_folder' : 'navigateToFolder' + }, + + //render the folder view + render: function (options) { + //hack to show scrollbars + $("#center").css('overflow','auto'); + + view = this; + var that = this; + + var folderContainer = new FolderContainer({id: options.id}); + folderContainer.url = folderContainer.attributes.urlRoot + options.id + '/contents'; + + folderContainer.fetch({ + success: function (container) { + + // prepare nice size strings + for (var i = 0; i < folderContainer.attributes.folder.models.length; i++) { + var model = folderContainer.attributes.folder.models[i] + if (model.get('type') === 'file'){ + model.set('readable_size', that.size_to_string(model.get('file_size'))) + } + }; + + // find the upper id + var path = folderContainer.full_path; + var upper_folder_id; + if (path.length === 1){ // library is above us + upper_folder_id = 0; + } else { + upper_folder_id = path[path.length-2][0]; + } + + var template = _.template(that.templateFolder(), { path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id, upper_folder_id: upper_folder_id }); + // var template = _.template(that.templateFolder(), { path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id }); + that.$el.html(template); + } + }) + }, + + // handles the click on 'open' and 'upper' folder icons + navigateToFolder : function(event){ + var folder_id = $(event.target).attr('data-id'); + if (typeof folder_id === 'undefined') { + return false; + } else if (folder_id === '0'){ + library_router.navigate('#', {trigger: true, replace: true}); + } else { + library_router.navigate('folders/' + folder_id, {trigger: true, replace: true}); + } + }, + + //show modal with current dataset info + showDatasetDetails : function(event){ + // prevent default + event.preventDefault(); + +//TODO check whether we already have the data + + //load the ID of the row + var id = $(event.target).parent().parent().attr('id'); + + //create new item + var item = new Item(); + var histories = new GalaxyHistories(); + item.id = id; + var self = this; + + //fetch the dataset info + item.fetch({ + success: function (item) { +// TODO can render here already + //fetch user histories for import purposes + histories.fetch({ + success: function (histories){self.renderModalAfterFetch(item, histories)} + }); + } + }); + }, + + // show the current dataset in a modal + renderModalAfterFetch : function(item, histories){ + var size = this.size_to_string(item.get('file_size')); + var template = _.template(this.templateDatasetModal(), { item : item, size : size }); + this.modal = null; + // make modal + var self = this; + this.modal = new mod_modal.GalaxyModal({ + title : 'Dataset Details', + body : template, + buttons : { + 'Import' : function() { self.importCurrentIntoHistory() }, + 'Download' : function() { self.downloadCurrent() }, + 'Close' : function() { self.modal.hide(); $('.modal').remove(); self.modal = null; } // TODO refill nicely modal with data + } + }); + $(".peek").html(item.get("peek")); + var history_footer_tmpl = _.template(this.templateHistorySelectInModal(), {histories : histories.models}); + $(this.modal.elMain).find('.buttons').prepend(history_footer_tmpl); + + // preset last selected history if we know it + if (self.lastSelectedHistory.length > 0) { + $(this.modal.elMain).find('#dataset_import_single').val(self.lastSelectedHistory); + } + + // show the prepared modal + this.modal.show(); + }, + + // download dataset shown currently in modal + downloadCurrent : function(){ + //disable the buttons + this.modal.disableButton('Import'); + this.modal.disableButton('Download'); + + var library_dataset_id = []; + library_dataset_id.push($('#id_row').attr('data-id')); + var url = '/api/libraries/datasets/download/uncompressed'; + var data = {'ldda_ids' : library_dataset_id}; + + // we assume the view is existent + folderContentView.processDownload(url, data); + this.modal.enableButton('Import'); + this.modal.enableButton('Download'); + }, + + // import dataset shown currently in modal into selected history + importCurrentIntoHistory : function(){ + //disable the buttons + this.modal.disableButton('Import'); + this.modal.disableButton('Download'); + + var history_id = $(this.modal.elMain).find('select[name=dataset_import_single] option:selected').val(); + this.lastSelectedHistory = history_id; //save selected history for further use + + var library_dataset_id = $('#id_row').attr('data-id'); + var historyItem = new HistoryItem(); + var self = this; + historyItem.url = historyItem.urlRoot + history_id + '/contents'; + + // save the dataset into selected history + historyItem.save({ content : library_dataset_id, source : 'library' }, { success : function(){ + self.modal.showNotification('Dataset imported', 3000, 'success'); + //enable the buttons + self.modal.enableButton('Import'); + self.modal.enableButton('Download'); + }, error : function(){ + self.modal.showNotification('An error occured! Dataset not imported. Please try again later.', 5000, 'error'); + //enable the buttons + self.modal.enableButton('Import'); + self.modal.enableButton('Download'); + } + }); + }, + + // select all datasets + selectAll : function (event) { + var selected = event.target.checked; + // Iterate each checkbox + $(':checkbox').each(function () { this.checked = selected; }); + this.showTools(); + }, + + // click checkbox on folder click + selectClicked : function (event) { + var checkbox = $("#" + event.target.parentElement.id).find(':checkbox') + if (checkbox[0] != undefined) { + if (checkbox[0].checked){ + checkbox[0].checked = ''; + // $(event.target.parentElement).css('background-color', '').css('color', ''); + $(event.target.parentElement).removeClass('dark'); + $(event.target.parentElement).find('a').removeClass('dark'); + $(event.target.parentElement).addClass('light'); + $(event.target.parentElement).find('a').addClass('light'); + } else { + checkbox[0].checked = 'selected'; + $(event.target.parentElement).removeClass('light'); + $(event.target.parentElement).find('a').removeClass('light'); + $(event.target.parentElement).addClass('dark'); + $(event.target.parentElement).find('a').addClass('dark'); + // $(event.target.parentElement).css('background-color', '#8389a1').css('color', 'white'); + } + } + this.showTools(); + }, + + // show toolbar in case something is selected + showTools : function(){ + var checkedValues = $('#folder_table').find(':checked'); + if(checkedValues.length > 0){ + $('#toolbtn_bulk_import').show(); + $('#toolbtn_dl').show(); + } else { + $('#toolbtn_bulk_import').hide(); + $('#toolbtn_dl').hide(); + } + + }, + + // show bulk import modal + modalBulkImport : function(){ + var self = this; + // fetch histories + var histories = new GalaxyHistories(); + histories.fetch({ + success: function (histories){ + // make modal + var history_modal_tmpl = _.template(self.templateBulkImportInModal(), {histories : histories.models}); + self.modal = new mod_modal.GalaxyModal({ + title : 'Import into History', + body : history_modal_tmpl, + buttons : { + 'Import' : function() {self.importAllIntoHistory()}, + 'Close' : function() {self.modal.hide(); $('.modal').remove(); self.modal = null;} + } + }); + // show the prepared modal + self.modal.show(); + } + }); + }, + + // import all selected datasets into history + importAllIntoHistory : function (){ + //disable the button + this.modal.disableButton('Import'); + + var history_id = $("select[name=dataset_import_bulk] option:selected").val(); + var history_name = $("select[name=dataset_import_bulk] option:selected").text(); + + var dataset_ids = []; + $('#folder_table').find(':checked').each(function(){ + if (this.parentElement.parentElement.id != '') { + dataset_ids.push(this.parentElement.parentElement.id); + } + }); + var progress_bar_tmpl = _.template(this.templateProgressBar(), { history_name : history_name }); + $(this.modal.elMain).find('.modal-body').html(progress_bar_tmpl); + + // init the progress bar + var progressStep = 100 / dataset_ids.length; + this.initProgress(progressStep); + + // prepare the dataset objects to be imported + var datasets_to_import = []; + for (var i = dataset_ids.length - 1; i >= 0; i--) { + library_dataset_id = dataset_ids[i]; + var historyItem = new HistoryItem(); + var self = this; + historyItem.url = historyItem.urlRoot + history_id + '/contents'; + historyItem.content = library_dataset_id; + historyItem.source = 'library'; + datasets_to_import.push(historyItem); + }; + + // call the recursive function to call ajax one after each other + this.chainCall(datasets_to_import); + }, + + chainCall: function(history_item_set){ + var self = this; + var popped_item = history_item_set.pop(); + if (typeof popped_item === "undefined") { + self.modal.showNotification('All datasets imported', 3000, 'success'); + // enable button again + self.modal.enableButton('Import'); + return + } + var promise = $.when(popped_item.save({content: popped_item.content, source: popped_item.source})).done(function(a1){ + self.updateProgress(); + responses.push(a1); + self.chainCall(history_item_set); + }); + }, + + initProgress: function(progressStep){ + this.progress = 0; + this.progressStep = progressStep; + }, + updateProgress: function(){ + this.progress += this.progressStep; + $('.progress-bar').width(Math.round(this.progress) + '%'); + txt_representation = Math.round(this.progress) + '% Complete'; + $('.completion_span').text(txt_representation); + }, + + // progress bar + templateProgressBar : function (){ + var tmpl_array = []; + + tmpl_array.push('<div class="import_text">'); + tmpl_array.push('Importing selected datasets to history <b><%= _.escape(history_name) %></b>'); + tmpl_array.push('</div>'); + tmpl_array.push('<div class="progress">'); + tmpl_array.push(' <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 00%;">'); + tmpl_array.push(' <span class="completion_span">0% Complete</span>'); + tmpl_array.push(' </div>'); + tmpl_array.push('</div>'); + tmpl_array.push(''); + + return tmpl_array.join(''); + }, + + // download selected datasets + download : function(folder_id, format){ + var dataset_ids = []; + $('#folder_table').find(':checked').each(function(){ + if (this.parentElement.parentElement.id != '') { + dataset_ids.push(this.parentElement.parentElement.id); + } + }); + + var url = '/api/libraries/datasets/download/' + format; + var data = {'ldda_ids' : dataset_ids}; + this.processDownload(url, data); + }, + + // create hidden form and submit through POST to initialize download + processDownload: function(url, data, method){ + //url and data options required + if( url && data ){ + //data can be string of parameters or array/object + data = typeof data == 'string' ? data : $.param(data); + //split params into form inputs + var inputs = ''; + $.each(data.split('&'), function(){ + var pair = this.split('='); + inputs+='<input type="hidden" name="'+ pair[0] +'" value="'+ pair[1] +'" />'; + }); + //send request + $('<form action="'+ url +'" method="'+ (method||'post') +'">'+inputs+'</form>') + .appendTo('body').submit().remove(); + }; + }, + + // shows modal for creating folder + createFolderModal: function(){ + alert('creating folder'); + } + + }); + +// galaxy library view +var GalaxyLibraryview = Backbone.View.extend({ + el: '#center', + + events: { + 'click #create_new_library_btn' : 'show_library_modal' + }, + + // initialize + initialize : function(){ + }, + + // template + template_library_list : function (){ + tmpl_array = []; + tmpl_array.push('<div id="library_container" style="width: 90%; margin: auto; margin-top: 2em; overflow: auto !important; ">'); + + tmpl_array.push(''); + tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to <a href="mailto:marten@bx.psu.edu?Subject=DataLibraries_Feedback" target="_blank">Marten</a>.</h3>'); + tmpl_array.push('<a href="" id="create_new_library_btn" class="btn btn-primary icon-file ">New Library</a>'); + tmpl_array.push('<table class="table table-condensed">'); + tmpl_array.push(' <thead>'); + tmpl_array.push(' <th class="button_heading"></th>'); + tmpl_array.push(' <th>name</th>'); + tmpl_array.push(' <th>description</th>'); + tmpl_array.push(' <th>synopsis</th> '); + tmpl_array.push(' <th>model type</th> '); + tmpl_array.push(' </thead>'); + tmpl_array.push(' <tbody>'); + tmpl_array.push(' <% _.each(libraries, function(library) { %>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <td><button title="Open this library" type="button" data-id="<%- library.get("root_folder_id") %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-folder-open"></span> browse</td>'); + tmpl_array.push(' <td><%- library.get("name") %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("description")) %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("synopsis")) %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("model_class")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push(' </tbody>'); + tmpl_array.push('</table>'); + + tmpl_array.push('</div>'); + return tmpl_array.join(''); + }, + + // render + render: function () { + //hack to show scrollbars + $("#center").css('overflow','auto'); + var that = this; + // if (typeof libraries === "undefined") { + libraries = new Libraries(); + // } + libraries.fetch({ + success: function (libraries) { + var template = _.template(that.template_library_list(), { libraries : libraries.models }); + that.$el.html(template); + } + }) + }, + + // own modal + modal : null, + + // show/hide create library modal + show_library_modal : function (event){ + event.preventDefault(); + event.stopPropagation(); + + // create modal + var self = this; + this.modal = new mod_modal.GalaxyModal( + { + title : 'Create New Library', + body : this.template_new_library(), + buttons : { + 'Create' : function() {self.create_new_library_event()}, + 'Close' : function() {self.modal.hide()} + } + }); + + // show prepared modal + this.modal.show(); + }, + + // create the new library from modal + create_new_library_event: function(){ + var libraryDetails = this.serialize_new_library(); + var valid = this.validate_new_library(libraryDetails); + var library = new Library(); + var self = this; + library.save(libraryDetails, { + success: function (library) { + self.modal.hide(); + self.clear_library_modal(); + self.render(); + }, + error: function(){ + self.modal.showNotification('An error occured', 5000, 'error'); + } + }); + return false; + }, + + // clear the library modal once saved + clear_library_modal : function(){ + $("input[name='Name']").val(''); + $("input[name='Description']").val(''); + $("input[name='Synopsis']").val(''); + }, + + // serialize data from the form + serialize_new_library : function(){ + return { + name: $("input[name='Name']").val(), + description: $("input[name='Description']").val(), + synopsis: $("input[name='Synopsis']").val() + }; + }, + + validate_new_library: function(library){ + + }, + + + // template for new library modal + template_new_library: function() + { + tmpl_array = []; + + tmpl_array.push('<div id="new_library_modal">'); + tmpl_array.push('<form>'); + tmpl_array.push('<input type="text" name="Name" value="" placeholder="Name">'); + tmpl_array.push('<input type="text" name="Description" value="" placeholder="Description">'); + tmpl_array.push('<input type="text" name="Synopsis" value="" placeholder="Synopsis">'); + tmpl_array.push('</form>'); + tmpl_array.push('</div>'); + + return tmpl_array.join(''); + } +}); + +// galaxy library wrapper View +var GalaxyLibrary = Backbone.View.extend({ + folderContentView : null, + galaxyLibraryview : null, + initialize : function(){ + + folderContentView = new FolderContentView(); + galaxyLibraryview = new GalaxyLibraryview(); + + library_router = new LibraryRouter(); + + library_router.on('route:libraries', function() { + // render libraries list + galaxyLibraryview.render(); + }); + + library_router.on('route:folder_content', function(id) { + // render folder's contents + folderContentView.render({id: id}); + }); + + library_router.on('route:download', function(folder_id, format) { + // send download stream + if (typeof folderContentView === 'undefined'){ + alert('you cant touch this!'); + // } else if (folderContentView.modal !== null){ + // folderContentView.download(folder_id, format); + } else if ($('#center').find(':checked').length === 0) { // coming from outside of the library app + library_router.navigate('folders/' + folder_id, {trigger: true, replace: true}); + // TODO show message of redirection + } else { + folderContentView.download(folder_id, format); + library_router.navigate('folders/' + folder_id, {trigger: false, replace: true}); + } + + }); + +Backbone.history.start(); + +return this +} +}); + +// return +return { + GalaxyApp: GalaxyLibrary +}; + +}); diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 static/scripts/galaxy.library.router.js --- /dev/null +++ b/static/scripts/galaxy.library.router.js @@ -0,0 +1,21 @@ +define( [], function() { + +/** + * -- Routers -- + */ + +/** + * Router for library browser. + */ +var LibraryRouter = Backbone.Router.extend({ + routes: { + "" : "libraries", + "folders/:id" : "folder_content" + } +}); + +return { + LibraryRouter: LibraryRouter, +}; + +}) \ No newline at end of file diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 static/scripts/galaxy.menu.js --- a/static/scripts/galaxy.menu.js +++ b/static/scripts/galaxy.menu.js @@ -58,6 +58,12 @@ tab_shared.add({ title : "Data Libraries", content : "library/index", + content : "library/index" + }); + + tab_shared.add({ + title : "New Libraries", + content : "library/list", divider : true }); diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 static/scripts/galaxy.modal.js --- a/static/scripts/galaxy.modal.js +++ b/static/scripts/galaxy.modal.js @@ -1,6 +1,3 @@ -/* - galaxy modal -*/ // dependencies define([], function() { @@ -25,8 +22,51 @@ // initialize initialize : function(options) { + self = this; if (options) this.create(options); + + this.bindClick(event, self); + }, + + // bind the click-to-hide function + bindClick: function(event, that) { + // bind the ESC key to hide() function + $(document).on('keyup', function(event){ + if (event.keyCode == 27) { self.hide(); } + }) + // bind the click anywhere to hide() function... + $('html').on('click', function(event){ + that.hide(); + }) + // ...but don't hide if the click is on modal content + $('.modal-content').on('click', function(event){ + event.stopPropagation(); + }) + }, + + // unbind the click-to-hide function + unbindClick: function(event, that){ + // bind the ESC key to hide() function + $(document).off('keyup', function(event){ + if (event.keyCode == 27) { that.hide(); } + }) + // unbind the click anywhere to hide() function... + $('html').off('click', function(event){ + that.hide(); + }) + // ...but don't hide if the click is on modal content + $('.modal-content').off('click', function(event){ + event.stopPropagation(); + }) + }, + + + // destroy + destroy : function(){ + this.hide(); + this.unbindClick(); + $('.modal').remove(); }, // adds and displays a new frame/window @@ -87,6 +127,7 @@ this.$footer = (this.$el).find('.modal-footer'); this.$buttons = (this.$el).find('.buttons'); this.$backdrop = (this.$el).find('.modal-backdrop'); + this.$notification = (this.$el).find('.notification-modal'); // append body this.$body.html(this.options.body); @@ -120,6 +161,47 @@ this.$buttons.find('#' + String(name).toLowerCase()).prop('disabled', true); }, + // hide buttons + hideButton: function(name) { + this.$buttons.find('#' + String(name).toLowerCase()).hide(); + }, + // show buttons + showButton: function(name) { + this.$buttons.find('#' + String(name).toLowerCase()).show(); + }, + + // show notification + showNotification : function(message, duration, type) { + // defaults + var duration = typeof duration !== 'undefined' ? duration : 1500; + // var bgColor = typeof bgColor !== 'undefined' ? bgColor : "#F4E0E1"; + // var txtColor = typeof txtColor !== 'undefined' ? txtColor : "#A42732"; + var bgColor; + var txtColor; + + if (type === 'error'){ + bgColor = '#f4e0e1'; + txtColor = '#a42732'; + // } else if (type === 'success'){ + } else { // success is default + bgColor = '#e1f4e0'; + txtColor = '#32a427'; + } + + var HTMLmessage = "<div class='notification-message' style='text-align:center; line-height:16px; '> " + message + " </div>"; + this.$notification.html("<div id='notification-bar' style='display:none; float: right; height: 16px; width:100%; background-color: " + bgColor + "; z-index: 100; color: " + txtColor + ";border-bottom: 1px solid " + txtColor + ";'>" + HTMLmessage + "</div>"); + + var self = this; + + /*animate the bar*/ + $('#notification-bar').slideDown(function() { + setTimeout(function() { + $('#notification-bar').slideUp(function() {self.$notification.html('');}); + }, duration); + }); + + }, + // returns scroll top for body element scrollTop: function() { @@ -135,10 +217,10 @@ return '<div class="modal">' + '<div class="modal-backdrop fade in" style="z-index: -1;"></div>' + '<div class="modal-dialog">' + - '<div class="modal-content">' + '<div class="modal-header">' + '<button type="button" class="close" style="display: none;">×</button>' + '<h4 class="title">' + title + '</h4>' + + '<span class="notification-modal"></span>' + '</div>' + '<div class="modal-body"></div>' + '<div class="modal-footer">' + diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 static/scripts/libs/bootstrap.js --- a/static/scripts/libs/bootstrap.js +++ b/static/scripts/libs/bootstrap.js @@ -575,3 +575,158 @@ } }(window.jQuery); + +/* ======================================================================== + * Bootstrap: dropdown.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#dropdowns + * ======================================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle=dropdown]' + var Dropdown = function (element) { + var $el = $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we we use a backdrop because click events don't delegate + $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus) + } + + $parent.trigger(e = $.Event('show.bs.dropdown')) + + if (e.isDefaultPrevented()) return + + $parent + .toggleClass('open') + .trigger('shown.bs.dropdown') + + $this.focus() + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27)/.test(e.keyCode)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) { + if (e.which == 27) $parent.find(toggle).focus() + return $this.click() + } + + var $items = $('[role=menu] li:not(.divider):visible a', $parent) + + if (!$items.length) return + + var index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index=0 + + $items.eq(index).focus() + } + + function clearMenus() { + $(backdrop).remove() + $(toggle).each(function (e) { + var $parent = getParent($(this)) + if (!$parent.hasClass('open')) return + $parent.trigger(e = $.Event('hide.bs.dropdown')) + if (e.isDefaultPrevented()) return + $parent.removeClass('open').trigger('hidden.bs.dropdown') + }) + } + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + var old = $.fn.dropdown + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('dropdown') + + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== + + $(document) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api' , toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + +}(window.jQuery); \ No newline at end of file diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 static/style/Gruntfile.js --- a/static/style/Gruntfile.js +++ b/static/style/Gruntfile.js @@ -6,7 +6,7 @@ var theme = grunt.option( 'theme', 'blue' ); var out = 'blue' - var lessFiles = [ 'base', 'autocomplete_tagging', 'embed_item', 'iphone', 'masthead', 'library', 'trackster' ]; + var lessFiles = [ 'base', 'autocomplete_tagging', 'embed_item', 'iphone', 'masthead', 'library', 'new_library', 'trackster' ]; var _ = grunt.util._; var fmt = _.sprintf; diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 static/style/src/less/library.less --- a/static/style/src/less/library.less +++ b/static/style/src/less/library.less @@ -29,6 +29,50 @@ background-color: @table-bg-accent; } + +tr.light td +{ + background-color: white; + color: black; +} +tr.light:hover td +{ + background-color: #f5f5f5; + color: #8389a1; +} +tr.dark td +{ + background-color: #8389a1; + color: white; +} +tr.dark:hover td +{ + background-color: #bbbfd0; + color: white; +} +a.dark:hover +{ + color: yellow; + // text-decoration: none; +} +a.dark +{ + color: white; + // text-decoration: none; +} +th.button_heading +{ + width: 7em; +} +div.library_breadcrumb{ + padding-top: 0.8em; + padding-bottom: 0.8em; +} +div.library_breadcrumb a:hover{ + color:green; +} + + img.expanderIcon { padding-right: 4px; } diff -r d5217f01960360a9d7298fa679b839c806cab893 -r e2b8d3481d668203bfad7efe306925c72c644839 templates/base/base_panels.mako --- a/templates/base/base_panels.mako +++ b/templates/base/base_panels.mako @@ -357,4 +357,4 @@ ## Scripts can be loaded later since they progressively add features to ## the panels, but do not change layout ${self.late_javascripts()} -</html> +</html> \ No newline at end of file https://bitbucket.org/galaxy/galaxy-central/commits/fb3161b21a4b/ Changeset: fb3161b21a4b User: martenson Date: 2013-12-11 00:12:14 Summary: aiding the merge of data_library branch Affected #: 5 files diff -r e2b8d3481d668203bfad7efe306925c72c644839 -r fb3161b21a4bfc1c5bd2f6bea8dc63c9c5bb4b38 .hgignore --- a/.hgignore +++ b/.hgignore @@ -114,8 +114,4 @@ *.orig .DS_Store *.rej -*~ - - -syntax: regexp -^static/AAA_scratch$ \ No newline at end of file +*~ \ No newline at end of file diff -r e2b8d3481d668203bfad7efe306925c72c644839 -r fb3161b21a4bfc1c5bd2f6bea8dc63c9c5bb4b38 lib/galaxy/webapps/galaxy/api/folder_contents.py --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -100,8 +100,6 @@ encoded_id = trans.security.encode_id( content_item.id ) time_updated = content_item.update_time.strftime( "%Y-%m-%d %I:%M %p" ) time_created = content_item.create_time.strftime( "%Y-%m-%d %I:%M %p" ) -# log.debug('XXXXXXXXXXXXXXXXXXX api type: ' + str(content_item.api_type)) -# log.debug('XXXXXXXX ALL: ' + str(content_item.__dict__)) # For folder return also hierarchy values if content_item.api_type == 'folder': @@ -110,7 +108,6 @@ return_item.update ( dict ( item_count = content_item.item_count ) ) if content_item.api_type == 'file': -# log.debug('XXXXX content item class: ' + str(content_item.__class__)) library_dataset_dict = content_item.to_dict() library_dataset_dict['data_type'] library_dataset_dict['file_size'] diff -r e2b8d3481d668203bfad7efe306925c72c644839 -r fb3161b21a4bfc1c5bd2f6bea8dc63c9c5bb4b38 static/scripts/galaxy.library.router.js --- a/static/scripts/galaxy.library.router.js +++ /dev/null @@ -1,21 +0,0 @@ -define( [], function() { - -/** - * -- Routers -- - */ - -/** - * Router for library browser. - */ -var LibraryRouter = Backbone.Router.extend({ - routes: { - "" : "libraries", - "folders/:id" : "folder_content" - } -}); - -return { - LibraryRouter: LibraryRouter, -}; - -}) \ No newline at end of file diff -r e2b8d3481d668203bfad7efe306925c72c644839 -r fb3161b21a4bfc1c5bd2f6bea8dc63c9c5bb4b38 static/scripts/galaxy.menu.js --- a/static/scripts/galaxy.menu.js +++ b/static/scripts/galaxy.menu.js @@ -57,7 +57,6 @@ tab_shared.add({ title : "Data Libraries", - content : "library/index", content : "library/index" }); diff -r e2b8d3481d668203bfad7efe306925c72c644839 -r fb3161b21a4bfc1c5bd2f6bea8dc63c9c5bb4b38 static/style/Gruntfile.js --- a/static/style/Gruntfile.js +++ b/static/style/Gruntfile.js @@ -6,7 +6,7 @@ var theme = grunt.option( 'theme', 'blue' ); var out = 'blue' - var lessFiles = [ 'base', 'autocomplete_tagging', 'embed_item', 'iphone', 'masthead', 'library', 'new_library', 'trackster' ]; + var lessFiles = [ 'base', 'autocomplete_tagging', 'embed_item', 'iphone', 'masthead', 'library', 'trackster' ]; var _ = grunt.util._; var fmt = _.sprintf; https://bitbucket.org/galaxy/galaxy-central/commits/96f52c9fd562/ Changeset: 96f52c9fd562 User: martenson Date: 2013-12-11 00:21:06 Summary: Merged galaxy/galaxy-central into default Affected #: 3 files diff -r fb3161b21a4bfc1c5bd2f6bea8dc63c9c5bb4b38 -r 96f52c9fd562fe160fc8ac71aa6cddd902d23e0f config/plugins/visualizations/scatterplot/templates/scatterplot.mako --- a/config/plugins/visualizations/scatterplot/templates/scatterplot.mako +++ b/config/plugins/visualizations/scatterplot/templates/scatterplot.mako @@ -15,7 +15,6 @@ <script type="text/javascript" src="/static/scripts/libs/jquery/jquery.migrate.js"></script><script type="text/javascript" src="/static/scripts/libs/underscore.js"></script><script type="text/javascript" src="/static/scripts/libs/backbone/backbone.js"></script> -<script type="text/javascript" src="/static/scripts/libs/backbone/backbone-relational.js"></script><script type="text/javascript" src="/static/scripts/libs/handlebars.runtime.js"></script><script type="text/javascript" src="/static/scripts/libs/d3.js"></script><script type="text/javascript" src="/static/scripts/libs/bootstrap.js"></script> diff -r fb3161b21a4bfc1c5bd2f6bea8dc63c9c5bb4b38 -r 96f52c9fd562fe160fc8ac71aa6cddd902d23e0f lib/galaxy/web/base/controller.py --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -474,6 +474,9 @@ """Returns history data in the form of a dictionary. """ history_dict = history.to_dict( view='element', value_mapper={ 'id':trans.security.encode_id }) + history_dict[ 'user_id' ] = None + if history.user_id: + history_dict[ 'user_id' ] = trans.security.encode_id( history.user_id ) history_dict[ 'nice_size' ] = history.get_disk_size( nice_size=True ) history_dict[ 'annotation' ] = history.get_item_annotation_str( trans.sa_session, trans.user, history ) diff -r fb3161b21a4bfc1c5bd2f6bea8dc63c9c5bb4b38 -r 96f52c9fd562fe160fc8ac71aa6cddd902d23e0f lib/tool_shed/galaxy_install/tool_dependencies/install_util.py --- a/lib/tool_shed/galaxy_install/tool_dependencies/install_util.py +++ b/lib/tool_shed/galaxy_install/tool_dependencies/install_util.py @@ -93,7 +93,7 @@ env_file_builder.append_line( action="source", value=required_tool_dependency_env_file_path ) return_code = env_file_builder.return_code if return_code: - error_message = 'Error defining env.sh file for package %s, return_code: %s' % ( str( package_name, str( return_code ) ) ) + error_message = 'Error defining env.sh file for package %s, return_code: %s' % ( str( package_name ), str( return_code ) ) tool_dependency = tool_dependency_util.handle_tool_dependency_installation_error( app, tool_dependency, error_message, https://bitbucket.org/galaxy/galaxy-central/commits/42e74befc796/ Changeset: 42e74befc796 User: martenson Date: 2013-12-11 00:29:50 Summary: aid after merge data_library branch Affected #: 1 file diff -r 96f52c9fd562fe160fc8ac71aa6cddd902d23e0f -r 42e74befc7962afb93baba6197273565755e086c lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -73,6 +73,8 @@ webapp.add_route( '/datasets/:dataset_id/display/{filename:.+?}', controller='dataset', action='display', dataset_id=None, filename=None) webapp.add_route( '/datasets/:dataset_id/:action/:filename', controller='dataset', action='index', dataset_id=None, filename=None) webapp.add_route( '/display_application/:dataset_id/:app_name/:link_name/:user_id/:app_action/:action_param', + controller='dataset', action='display_application', dataset_id=None, user_id=None, + app_name = None, link_name = None, app_action = None, action_param = None ) webapp.add_route( '/u/:username/d/:slug/:filename', controller='dataset', action='display_by_username_and_slug', filename=None ) webapp.add_route( '/u/:username/p/:slug', controller='page', action='display_by_username_and_slug' ) webapp.add_route( '/u/:username/h/:slug', controller='history', action='display_by_username_and_slug' ) https://bitbucket.org/galaxy/galaxy-central/commits/8fcfddb3e1b5/ Changeset: 8fcfddb3e1b5 User: martenson Date: 2013-12-11 16:41:39 Summary: aiding problematic merging of data_library branch, removing duplicates, removing API v2 relicts Affected #: 3 files diff -r 42e74befc7962afb93baba6197273565755e086c -r 8fcfddb3e1b57ad627ebab3619920552a6e967ff .hgignore --- a/.hgignore +++ b/.hgignore @@ -114,4 +114,5 @@ *.orig .DS_Store *.rej -*~ \ No newline at end of file +*~ + diff -r 42e74befc7962afb93baba6197273565755e086c -r 8fcfddb3e1b57ad627ebab3619920552a6e967ff lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -46,7 +46,6 @@ atexit.register( app.shutdown ) # Create the universe WSGI application webapp = GalaxyWebApplication( app, session_cookie='galaxysession', name='galaxy' ) - # Add the controllers folder # Handle displaying tool help images and README file images contained in repositories installed from the tool shed. webapp.add_route( '/admin_toolshed/static/images/:repository_id/:image_file', controller='admin_toolshed', @@ -54,17 +53,11 @@ repository_id=None, image_file=None ) webapp.add_ui_controllers( 'galaxy.webapps.galaxy.controllers', app ) - # Add the api folder - webapp.add_api_controllers( 'galaxy.webapps.galaxy.api', app ) - # Add the api folder VERSION 2 - webapp.add_api_controllers( 'galaxy.webapps.galaxy.api.v2', app ) - # Force /history to go to /root/history -- needed since the tests assume this webapp.add_route( '/history', controller='root', action='history' ) # Force /activate to go to the controller webapp.add_route( '/activate', controller='user', action='activate' ) # These two routes handle our simple needs at the moment - webapp.add_route( '/async/:tool_id/:data_id/:data_secret', controller='async', action='index', tool_id=None, data_id=None, data_secret=None ) webapp.add_route( '/:controller/:action', action='index' ) webapp.add_route( '/:action', controller='root', action='index' ) @@ -73,8 +66,8 @@ webapp.add_route( '/datasets/:dataset_id/display/{filename:.+?}', controller='dataset', action='display', dataset_id=None, filename=None) webapp.add_route( '/datasets/:dataset_id/:action/:filename', controller='dataset', action='index', dataset_id=None, filename=None) webapp.add_route( '/display_application/:dataset_id/:app_name/:link_name/:user_id/:app_action/:action_param', - controller='dataset', action='display_application', dataset_id=None, user_id=None, - app_name = None, link_name = None, app_action = None, action_param = None ) + controller='dataset', action='display_application', dataset_id=None, user_id=None, + app_name = None, link_name = None, app_action = None, action_param = None ) webapp.add_route( '/u/:username/d/:slug/:filename', controller='dataset', action='display_by_username_and_slug', filename=None ) webapp.add_route( '/u/:username/p/:slug', controller='page', action='display_by_username_and_slug' ) webapp.add_route( '/u/:username/h/:slug', controller='history', action='display_by_username_and_slug' ) @@ -82,22 +75,12 @@ webapp.add_route( '/u/:username/v/:slug', controller='visualization', action='display_by_username_and_slug' ) webapp.add_route( '/search', controller='search', action='index' ) - # Add the web API + # ================ + # ===== API ===== + # ================ + webapp.add_api_controllers( 'galaxy.webapps.galaxy.api', app ) - # The /folders section is experimental at this point: - log.debug( "app.config.api_folders: %s" % app.config.api_folders ) - webapp.mapper.resource( 'folder', 'folders', path_prefix='/api' ) - webapp.mapper.resource( 'content', 'contents', - controller='folder_contents', - name_prefix='folder_', - path_prefix='/api/folders/:folder_id', - parent_resources=dict( member_name='folder', collection_name='folders' ) ) - webapp.mapper.resource( 'content', - 'contents', - controller='library_contents', - name_prefix='library_', - path_prefix='/api/libraries/:library_id', - parent_resources=dict( member_name='library', collection_name='libraries' ) ) + webapp.mapper.resource( 'content', 'contents', controller='history_contents', @@ -109,10 +92,6 @@ controller="datasets", action="display", conditions=dict(method=["GET"])) - webapp.mapper.resource( 'permission', - 'permissions', - path_prefix='/api/libraries/:library_id', - parent_resources=dict( member_name='library', collection_name='libraries' ) ) webapp.mapper.resource( 'user', 'users', controller='group_users', @@ -134,11 +113,6 @@ _add_item_tags_controller( webapp, name_prefix="workflow_", path_prefix='/api/workflows/:workflow_id' ) - - _add_item_extended_metadata_controller( webapp, - name_prefix="library_dataset_", - path_prefix='/api/libraries/:library_id/contents/:library_content_id' ) - _add_item_annotation_controller( webapp, name_prefix="history_content_", path_prefix='/api/histories/:history_id/contents/:history_content_id' ) @@ -148,7 +122,6 @@ _add_item_annotation_controller( webapp, name_prefix="workflow_", path_prefix='/api/workflows/:workflow_id' ) - _add_item_provenance_controller( webapp, name_prefix="history_content_", path_prefix='/api/histories/:history_id/contents/:history_content_id' ) @@ -178,13 +151,12 @@ #webapp.mapper.connect( 'run_workflow', '/api/workflow/{workflow_id}/library/{library_id}', controller='workflows', action='run', workflow_id=None, library_id=None, conditions=dict(method=["GET"]) ) webapp.mapper.resource( 'search', 'search', path_prefix='/api' ) - webapp.mapper.resource_with_deleted( 'quota', 'quotas', path_prefix='/api' ) - webapp.mapper.resource_with_deleted( 'history', 'histories', path_prefix='/api' ) - webapp.mapper.resource_with_deleted( 'user', 'users', path_prefix='/api' ) - # ======================= # ===== LIBRARY API ===== # ======================= + + # The /folders section is experimental at this point: + log.debug( "app.config.api_folders: %s" % app.config.api_folders ) webapp.mapper.connect( 'show_lda_item', '/api/libraries/datasets/:id', controller='lda_datasets', action='show', conditions=dict(method=["GET"]) ) webapp.mapper.connect( 'download_lda_items', '/api/libraries/datasets/download/:format', controller='lda_datasets', action='download', conditions=dict(method=["POST"]) ) @@ -232,6 +204,9 @@ # Preserve the following download route for now for dependent applications -- deprecate at some point webapp.mapper.connect("workflow_dict", '/api/workflows/download/{workflow_id}', controller='workflows', action='workflow_dict', conditions=dict(method=['GET'])) + # ==================== + # ===== TOOLSHED ===== + # ==================== # Handle displaying tool help images and README file images contained in repositories installed from the tool shed. webapp.add_route( '/admin_toolshed/static/images/:repository_id/:image_file', @@ -239,7 +214,7 @@ action='display_image_in_repository', repository_id=None, image_file=None ) - + # Galaxy API for tool shed features. webapp.mapper.resource( 'tool_shed_repository', 'tool_shed_repositories', @@ -253,6 +228,7 @@ path_prefix='/api', new={ 'install_repository_revision' : 'POST' }, parent_resources=dict( member_name='tool_shed_repository', collection_name='tool_shed_repositories' ) ) + # Connect logger from app if app.trace_logger: webapp.trace_logger = app.trace_logger @@ -273,11 +249,6 @@ galaxy.model.mapping.metadata.engine.connection_provider._pool.dispose() except: pass - # Close any pooled database connections before forking - try: - galaxy.model.tool_shed_install.mapping.metadata.engine.connection_provider._pool.dispose() - except: - pass # Return return webapp https://bitbucket.org/galaxy/galaxy-central/commits/4506583cb1bc/ Changeset: 4506583cb1bc User: martenson Date: 2013-12-11 16:51:21 Summary: aiding merg problems with data_library branch Affected #: 1 file diff -r 8fcfddb3e1b57ad627ebab3619920552a6e967ff -r 4506583cb1bc66db01d98c1619b6829fcda31e00 lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -46,12 +46,6 @@ atexit.register( app.shutdown ) # Create the universe WSGI application webapp = GalaxyWebApplication( app, session_cookie='galaxysession', name='galaxy' ) - # Handle displaying tool help images and README file images contained in repositories installed from the tool shed. - webapp.add_route( '/admin_toolshed/static/images/:repository_id/:image_file', - controller='admin_toolshed', - action='display_image_in_repository', - repository_id=None, - image_file=None ) webapp.add_ui_controllers( 'galaxy.webapps.galaxy.controllers', app ) # Force /history to go to /root/history -- needed since the tests assume this webapp.add_route( '/history', controller='root', action='history' ) @@ -187,7 +181,6 @@ # add as a non-ATOM API call to support the notion of a 'current/working' history unique to the history resource webapp.mapper.connect( "set_as_current", "/api/histories/{id}/set_as_current", controller="histories", action="set_as_current", conditions=dict( method=["POST"] ) ) - webapp.mapper.connect( "set_as_current", "/api/histories/{id}/set_as_current", controller="histories", action="set_as_current", conditions=dict( method=["POST"] ) ) webapp.mapper.connect( "create_api_key", "/api/users/:user_id/api_key", controller="users", action="api_key", user_id=None, conditions=dict( method=["POST"] ) ) @@ -195,7 +188,6 @@ # visualizations registry generic template renderer webapp.add_route( '/visualization/show/:visualization_name', controller='visualization', action='render', visualization_name=None ) - webapp.add_route( '/visualization/show/:visualization_name', controller='visualization', action='render', visualization_name=None ) # "POST /api/workflows/import" => ``workflows.import_workflow()``. # Defines a named route "import_workflow". https://bitbucket.org/galaxy/galaxy-central/commits/2fb8dfdb5b19/ Changeset: 2fb8dfdb5b19 User: martenson Date: 2013-12-11 16:53:42 Summary: aiding merg problems with data_library branch Affected #: 1 file diff -r 4506583cb1bc66db01d98c1619b6829fcda31e00 -r 2fb8dfdb5b19378a5bc61ee713aa4da3f9767a91 lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -241,6 +241,11 @@ galaxy.model.mapping.metadata.engine.connection_provider._pool.dispose() except: pass + # Close any pooled database connections before forking + try: + galaxy.model.tool_shed_install.mapping.metadata.engine.connection_provider._pool.dispose() + except: + pass # Return return webapp https://bitbucket.org/galaxy/galaxy-central/commits/78c39d32ac09/ Changeset: 78c39d32ac09 User: martenson Date: 2013-12-11 17:01:45 Summary: aiding merg problems with data_library branch Affected #: 1 file diff -r 2fb8dfdb5b19378a5bc61ee713aa4da3f9767a91 -r 78c39d32ac09a796966cc192636f4f8b1ba1bc07 templates/base/base_panels.mako --- a/templates/base/base_panels.mako +++ b/templates/base/base_panels.mako @@ -357,4 +357,4 @@ ## Scripts can be loaded later since they progressively add features to ## the panels, but do not change layout ${self.late_javascripts()} -</html> \ No newline at end of file +</html> https://bitbucket.org/galaxy/galaxy-central/commits/8528640574a5/ Changeset: 8528640574a5 User: martenson Date: 2013-12-11 17:08:59 Summary: aiding merg problems with data_library branch Affected #: 1 file diff -r 78c39d32ac09a796966cc192636f4f8b1ba1bc07 -r 8528640574a5b981ffb0940477abb0bd65ea2885 static/scripts/galaxy.modal.js --- a/static/scripts/galaxy.modal.js +++ b/static/scripts/galaxy.modal.js @@ -217,11 +217,12 @@ return '<div class="modal">' + '<div class="modal-backdrop fade in" style="z-index: -1;"></div>' + '<div class="modal-dialog">' + + '<div class="modal-content">' + '<div class="modal-header">' + '<button type="button" class="close" style="display: none;">×</button>' + '<h4 class="title">' + title + '</h4>' + '<span class="notification-modal"></span>' + - '</div>' + + '</div>' + '<div class="modal-body"></div>' + '<div class="modal-footer">' + '<div class="buttons" style="float: right;"></div>' + https://bitbucket.org/galaxy/galaxy-central/commits/181b293ea659/ Changeset: 181b293ea659 User: martenson Date: 2013-12-11 17:19:42 Summary: Merge with second head of ‘default’ branch Affected #: 17 files diff -r df88d6121d48b0b193d34445ce49916adf17a2d2 -r 181b293ea65936b7edb338bcbeac3bdea8e53063 lib/galaxy/model/__init__.py --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -1827,7 +1827,7 @@ class Library( object, Dictifiable ): permitted_actions = get_permitted_actions( filter='LIBRARY' ) dict_collection_visible_keys = ( 'id', 'name' ) - dict_element_visible_keys = ( 'id', 'deleted', 'name', 'description', 'synopsis' ) + dict_element_visible_keys = ( 'id', 'deleted', 'name', 'description', 'synopsis', 'root_folder_id' ) def __init__( self, name=None, description=None, synopsis=None, root_folder=None ): self.name = name or "Unnamed library" self.description = description @@ -1894,7 +1894,7 @@ return name class LibraryFolder( object, Dictifiable ): - dict_element_visible_keys = ( 'id', 'parent_id', 'name', 'description', 'item_count', 'genome_build' ) + dict_element_visible_keys = ( 'id', 'parent_id', 'name', 'description', 'item_count', 'genome_build', 'update_time' ) def __init__( self, name=None, description=None, item_count=0, order_id=None ): self.name = name or "Unnamed folder" self.description = description @@ -2060,6 +2060,7 @@ genome_build = ldda.dbkey, misc_info = ldda.info, misc_blurb = ldda.blurb, + peek = ( lambda ldda: ldda.display_peek() if ldda.peek and ldda.peek != 'no peek' else None )( ldda ), template_data = template_data ) if ldda.dataset.uuid is None: rval['uuid'] = None diff -r df88d6121d48b0b193d34445ce49916adf17a2d2 -r 181b293ea65936b7edb338bcbeac3bdea8e53063 lib/galaxy/model/mapping.py --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -1878,8 +1878,9 @@ table = self.table trans = conn.begin() try: - next_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() - table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid + 1 ) ) + current_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() + next_hid = current_hid + 1 + table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid ) ) trans.commit() return next_hid except: diff -r df88d6121d48b0b193d34445ce49916adf17a2d2 -r 181b293ea65936b7edb338bcbeac3bdea8e53063 lib/galaxy/webapps/galaxy/api/datasets.py --- a/lib/galaxy/webapps/galaxy/api/datasets.py +++ b/lib/galaxy/webapps/galaxy/api/datasets.py @@ -1,5 +1,5 @@ """ -API operations on the contents of a dataset. +API operations on the contents of a history dataset. """ from galaxy import web from galaxy.visualization.data_providers.genome import FeatureLocationIndexDataProvider diff -r df88d6121d48b0b193d34445ce49916adf17a2d2 -r 181b293ea65936b7edb338bcbeac3bdea8e53063 lib/galaxy/webapps/galaxy/api/folder_contents.py --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -1,5 +1,5 @@ """ -API operations on the contents of a library. +API operations on the contents of a folder. """ import logging, os, string, shutil, urllib, re, socket from cgi import escape, FieldStorage @@ -11,67 +11,122 @@ log = logging.getLogger( __name__ ) class FolderContentsController( BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems ): + """ + Class controls retrieval, creation and updating of folder contents. + """ + + def load_folder_contents( self, trans, folder ): + """ + Loads all contents of the folder (folders and data sets) but only in the first level. + """ + current_user_roles = trans.get_current_user_roles() + is_admin = trans.user_is_admin() + content_items = [] + for subfolder in folder.active_folders: + if not is_admin: + can_access, folder_ids = trans.app.security_agent.check_folder_contents( trans.user, current_user_roles, subfolder ) + if (is_admin or can_access) and not subfolder.deleted: + subfolder.api_type = 'folder' + content_items.append( subfolder ) + for dataset in folder.datasets: + if not is_admin: + can_access = trans.app.security_agent.can_access_dataset( current_user_roles, dataset.library_dataset_dataset_association.dataset ) + if (is_admin or can_access) and not dataset.deleted: + dataset.api_type = 'file' + content_items.append( dataset ) + return content_items @web.expose_api def index( self, trans, folder_id, **kwd ): """ GET /api/folders/{encoded_folder_id}/contents Displays a collection (list) of a folder's contents (files and folders). - The /api/library_contents/{encoded_library_id}/contents - lists everything in a library recursively, which is not what - we want here. We could add a parameter to use the recursive - style, but this is meant to act similar to an "ls" directory listing. + Encoded folder ID is prepended with 'F' if it is a folder as opposed to a data set which does not have it. + Full path is provided as a separate object in response providing data for breadcrumb path building. """ - rval = [] + folder_container = [] current_user_roles = trans.get_current_user_roles() - def traverse( folder ): - admin = trans.user_is_admin() - rval = [] - for subfolder in folder.active_folders: - if not admin: - can_access, folder_ids = trans.app.security_agent.check_folder_contents( trans.user, current_user_roles, subfolder ) - if (admin or can_access) and not subfolder.deleted: - subfolder.api_type = 'folder' - rval.append( subfolder ) - for ld in folder.datasets: - if not admin: - can_access = trans.app.security_agent.can_access_dataset( current_user_roles, ld.library_dataset_dataset_association.dataset ) - if (admin or can_access) and not ld.deleted: - ld.api_type = 'file' - rval.append( ld ) - return rval - - try: - decoded_folder_id = trans.security.decode_id( folder_id[-16:] ) - except TypeError: - trans.response.status = 400 - return "Malformed folder id ( %s ) specified, unable to decode." % str( folder_id ) + if ( folder_id.startswith( 'F' ) ): + try: + decoded_folder_id = trans.security.decode_id( folder_id[1:] ) + except TypeError: + trans.response.status = 400 + return "Malformed folder id ( %s ) specified, unable to decode." % str( folder_id ) try: folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( decoded_folder_id ) - parent_library = folder.parent_library except: folder = None - log.error( "FolderContentsController.index: Unable to retrieve folder %s" - % folder_id ) + log.error( "FolderContentsController.index: Unable to retrieve folder with ID: %s" % folder_id ) - # TODO: Find the API's path to this folder if necessary. - # This was needed in recursive descent, but it's not needed - # for "ls"-style content checking: - if not folder or not ( trans.user_is_admin() or trans.app.security_agent.can_access_library_item( current_user_roles, folder, trans.user ) ): + # We didn't find the folder or user does not have an access to it. + if not folder: trans.response.status = 400 return "Invalid folder id ( %s ) specified." % str( folder_id ) + + if not ( trans.user_is_admin() or trans.app.security_agent.can_access_library_item( current_user_roles, folder, trans.user ) ): + log.warning( "SECURITY: User (id: %s) without proper access rights is trying to load folder with ID of %s" % ( trans.user.id, folder.id ) ) + trans.response.status = 400 + return "Invalid folder id ( %s ) specified." % str( folder_id ) + + path_to_root = [] + def build_path ( folder ): + """ + Search the path upwards recursively and load the whole route of names and ids for breadcrumb purposes. + """ + path_to_root = [] + # We are almost in root + if folder.parent_id is None: + path_to_root.append( ( 'F' + trans.security.encode_id( folder.id ), folder.name ) ) + else: + # We add the current folder and traverse up one folder. + path_to_root.append( ( 'F' + trans.security.encode_id( folder.id ), folder.name ) ) + upper_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( folder.parent_id ) + path_to_root.extend( build_path( upper_folder ) ) + return path_to_root + + # Return the reversed path so it starts with the library node. + full_path = build_path( folder )[::-1] + folder_container.append( dict( full_path = full_path ) ) + + folder_contents = [] + time_updated = '' + time_created = '' + # Go through every item in the folder and include its meta-data. + for content_item in self.load_folder_contents( trans, folder ): +# rval = content_item.to_dict() + return_item = {} + encoded_id = trans.security.encode_id( content_item.id ) + time_updated = content_item.update_time.strftime( "%Y-%m-%d %I:%M %p" ) + time_created = content_item.create_time.strftime( "%Y-%m-%d %I:%M %p" ) + + # For folder return also hierarchy values + if content_item.api_type == 'folder': + encoded_id = 'F' + encoded_id +# time_updated = content_item.update_time.strftime( "%Y-%m-%d %I:%M %p" ) + return_item.update ( dict ( item_count = content_item.item_count ) ) - for content in traverse( folder ): - encoded_id = trans.security.encode_id( content.id ) - if content.api_type == 'folder': - encoded_id = 'F' + encoded_id - rval.append( dict( id = encoded_id, - type = content.api_type, - name = content.name, - url = url_for( 'folder_contents', folder_id=encoded_id ) ) ) - return rval + if content_item.api_type == 'file': + library_dataset_dict = content_item.to_dict() + library_dataset_dict['data_type'] + library_dataset_dict['file_size'] + library_dataset_dict['date_uploaded'] + return_item.update ( dict ( data_type = library_dataset_dict['data_type'], + file_size = library_dataset_dict['file_size'], + date_uploaded = library_dataset_dict['date_uploaded'] ) ) + + # For every item return also the default meta-data + return_item.update( dict( id = encoded_id, + type = content_item.api_type, + name = content_item.name, + time_updated = time_updated, + time_created = time_created + ) ) + folder_contents.append( return_item ) + # Put the data in the container + folder_container.append( dict( folder_contents = folder_contents ) ) + return folder_container @web.expose_api def show( self, trans, id, library_id, **kwd ): diff -r df88d6121d48b0b193d34445ce49916adf17a2d2 -r 181b293ea65936b7edb338bcbeac3bdea8e53063 lib/galaxy/webapps/galaxy/api/lda_datasets.py --- /dev/null +++ b/lib/galaxy/webapps/galaxy/api/lda_datasets.py @@ -0,0 +1,241 @@ +""" +API operations on the dataset from library. +""" +import glob +import logging +import operator +import os +import os.path +import string +import sys +import tarfile +import tempfile +import urllib +import urllib2 +import zipfile +from galaxy.security import Action +from galaxy import util, web +from galaxy.util.streamball import StreamBall +from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixinItems + +import logging +log = logging.getLogger( __name__ ) + +# Test for available compression types +# tmpd = tempfile.mkdtemp() +# comptypes = [] +# for comptype in ( 'gz', 'bz2' ): +# tmpf = os.path.join( tmpd, 'compression_test.tar.' + comptype ) +# try: +# archive = tarfile.open( tmpf, 'w:' + comptype ) +# archive.close() +# comptypes.append( comptype ) +# except tarfile.CompressionError: +# log.exception( "Compression error when testing %s compression. This option will be disabled for library downloads." % comptype ) +# try: +# os.unlink( tmpf ) +# except OSError: +# pass +ziptype = '32' +# tmpf = os.path.join( tmpd, 'compression_test.zip' ) +# try: +# archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED, True ) +# archive.close() +# comptypes.append( 'zip' ) +# ziptype = '64' +# except RuntimeError: +# log.exception( "Compression error when testing zip compression. This option will be disabled for library downloads." ) +# except (TypeError, zipfile.LargeZipFile): +# # ZIP64 is only in Python2.5+. Remove TypeError when 2.4 support is dropped +# log.warning( 'Max zip file size is 2GB, ZIP64 not supported' ) +# comptypes.append( 'zip' ) +# try: +# os.unlink( tmpf ) +# except OSError: +# pass +# os.rmdir( tmpd ) + + + +class DatasetsController( BaseAPIController, UsesLibraryMixinItems ): + + @web.expose_api + def show( self, trans, id, **kwd ): + """ + GET /api/libraries/datasets/{encoded_dataset_id} + Displays information about the dataset identified by the lda ID. + """ + # Get dataset. + try: + dataset = self.get_library_dataset( trans, id = id ) + except Exception, e: + return str( e ) + try: + # Default: return dataset as dict. + rval = dataset.to_dict() + except Exception, e: + rval = "Error in dataset API at listing contents: " + str( e ) + log.error( rval + ": %s" % str(e), exc_info=True ) + trans.response.status = 500 + + rval['id'] = trans.security.encode_id(rval['id']); + rval['ldda_id'] = trans.security.encode_id(rval['ldda_id']); + rval['folder_id'] = 'f' + trans.security.encode_id(rval['folder_id']) + + return rval + + @web.expose + def download( self, trans, format, **kwd ): + """ + POST /api/libraries/datasets/download/{format} + POST data: ldda_ids = [] + Downloads dataset(s) in the requested format. + """ + lddas = [] +# is_admin = trans.user_is_admin() +# current_user_roles = trans.get_current_user_roles() + + datasets_to_download = kwd['ldda_ids%5B%5D'] + + if ( datasets_to_download != None ): + datasets_to_download = util.listify( datasets_to_download ) + for dataset_id in datasets_to_download: + try: + ldda = trans.sa_session.query( trans.app.model.LibraryDatasetDatasetAssociation ).get( trans.security.decode_id( dataset_id ) ) + assert not ldda.dataset.purged + lddas.append( ldda ) + except: + ldda = None + message += "Invalid library dataset id (%s) specified. " % str( dataset_id ) + + if format in [ 'zip','tgz','tbz' ]: + error = False + killme = string.punctuation + string.whitespace + trantab = string.maketrans(killme,'_'*len(killme)) + try: + outext = 'zip' + if format == 'zip': + # Can't use mkstemp - the file must not exist first + tmpd = tempfile.mkdtemp() + util.umask_fix_perms( tmpd, trans.app.config.umask, 0777, self.app.config.gid ) + tmpf = os.path.join( tmpd, 'library_download.' + format ) + if ziptype == '64' and trans.app.config.upstream_gzip: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_STORED, True ) + elif ziptype == '64': + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED, True ) + elif trans.app.config.upstream_gzip: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_STORED ) + else: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED ) + archive.add = lambda x, y: archive.write( x, y.encode('CP437') ) + elif format == 'tgz': + if trans.app.config.upstream_gzip: + archive = StreamBall( 'w|' ) + outext = 'tar' + else: + archive = StreamBall( 'w|gz' ) + outext = 'tgz' + elif format == 'tbz': + archive = StreamBall( 'w|bz2' ) + outext = 'tbz2' + except ( OSError, zipfile.BadZipfile ): + error = True + log.exception( "Unable to create archive for download" ) + message = "Unable to create archive for download, please report this error" + status = 'error' + except: + error = True + log.exception( "Unexpected error %s in create archive for download" % sys.exc_info()[0] ) + message = "Unable to create archive for download, please report - %s" % sys.exc_info()[0] + status = 'error' + if not error: + composite_extensions = trans.app.datatypes_registry.get_composite_extensions() + seen = [] + for ldda in lddas: + if ldda.dataset.state in [ 'new', 'upload', 'queued', 'running', 'empty', 'discarded' ]: + continue + ext = ldda.extension + is_composite = ext in composite_extensions + path = "" + parent_folder = ldda.library_dataset.folder + while parent_folder is not None: + # Exclude the now-hidden "root folder" + if parent_folder.parent is None: + path = os.path.join( parent_folder.library_root[0].name, path ) + break + path = os.path.join( parent_folder.name, path ) + parent_folder = parent_folder.parent + path += ldda.name + while path in seen: + path += '_' + seen.append( path ) + zpath = os.path.split(path)[-1] # comes as base_name/fname + outfname,zpathext = os.path.splitext(zpath) + if is_composite: + # need to add all the components from the extra_files_path to the zip + if zpathext == '': + zpath = '%s.html' % zpath # fake the real nature of the html file + try: + archive.add(ldda.dataset.file_name,zpath) # add the primary of a composite set + except IOError: + error = True + log.exception( "Unable to add composite parent %s to temporary library download archive" % ldda.dataset.file_name) + message = "Unable to create archive for download, please report this error" + status = 'error' + continue + flist = glob.glob(os.path.join(ldda.dataset.extra_files_path,'*.*')) # glob returns full paths + for fpath in flist: + efp,fname = os.path.split(fpath) + if fname > '': + fname = fname.translate(trantab) + try: + archive.add( fpath,fname ) + except IOError: + error = True + log.exception( "Unable to add %s to temporary library download archive %s" % (fname,outfname)) + message = "Unable to create archive for download, please report this error" + status = 'error' + continue + else: # simple case + try: + archive.add( ldda.dataset.file_name, path ) + except IOError: + error = True + log.exception( "Unable to write %s to temporary library download archive" % ldda.dataset.file_name) + message = "Unable to create archive for download, please report this error" + status = 'error' + if not error: + lname = 'selected_dataset' + fname = lname.replace( ' ', '_' ) + '_files' + if format == 'zip': + archive.close() + trans.response.set_content_type( "application/octet-stream" ) + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) + archive = util.streamball.ZipBall(tmpf, tmpd) + archive.wsgi_status = trans.response.wsgi_status() + archive.wsgi_headeritems = trans.response.wsgi_headeritems() + return archive.stream + else: + trans.response.set_content_type( "application/x-tar" ) + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) + archive.wsgi_status = trans.response.wsgi_status() + archive.wsgi_headeritems = trans.response.wsgi_headeritems() + return archive.stream + elif format == 'uncompressed': + if len(lddas) != 1: + return 'Wrong request' + else: + single_dataset = lddas[0] + trans.response.set_content_type( single_dataset.get_mime() ) + fStat = os.stat( ldda.file_name ) + trans.response.headers[ 'Content-Length' ] = int( fStat.st_size ) + valid_chars = '.,^_-()[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + fname = ldda.name + fname = ''.join( c in valid_chars and c or '_' for c in fname )[ 0:150 ] + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s"' % fname + try: + return open( single_dataset.file_name ) + except: + return 'This dataset contains no content' + else: + return 'Wrong format'; \ No newline at end of file diff -r df88d6121d48b0b193d34445ce49916adf17a2d2 -r 181b293ea65936b7edb338bcbeac3bdea8e53063 lib/galaxy/webapps/galaxy/api/libraries.py --- a/lib/galaxy/webapps/galaxy/api/libraries.py +++ b/lib/galaxy/webapps/galaxy/api/libraries.py @@ -49,9 +49,10 @@ trans.model.Library.table.c.id.in_( accessible_restricted_library_ids ) ) ) rval = [] for library in query: - item = library.to_dict() + item = library.to_dict( view='element' ) item['url'] = url_for( route, id=trans.security.encode_id( library.id ) ) - item['id'] = trans.security.encode_id( item['id'] ) + item['id'] = 'F' + trans.security.encode_id( item['id'] ) + item['root_folder_id'] = 'F' + trans.security.encode_id( item['root_folder_id'] ) rval.append( item ) return rval @@ -131,6 +132,9 @@ rval['name'] = name rval['id'] = encoded_id return rval + + def edit( self, trans, payload, **kwd ): + return "Not implemented yet" @web.expose_api def delete( self, trans, id, **kwd ): diff -r df88d6121d48b0b193d34445ce49916adf17a2d2 -r 181b293ea65936b7edb338bcbeac3bdea8e53063 lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -46,12 +46,6 @@ atexit.register( app.shutdown ) # Create the universe WSGI application webapp = GalaxyWebApplication( app, session_cookie='galaxysession', name='galaxy' ) - # Handle displaying tool help images and README file images contained in repositories installed from the tool shed. - webapp.add_route( '/admin_toolshed/static/images/:repository_id/:image_file', - controller='admin_toolshed', - action='display_image_in_repository', - repository_id=None, - image_file=None ) webapp.add_ui_controllers( 'galaxy.webapps.galaxy.controllers', app ) # Force /history to go to /root/history -- needed since the tests assume this webapp.add_route( '/history', controller='root', action='history' ) @@ -75,22 +69,12 @@ webapp.add_route( '/u/:username/v/:slug', controller='visualization', action='display_by_username_and_slug' ) webapp.add_route( '/search', controller='search', action='index' ) - # Add the web API + # ================ + # ===== API ===== + # ================ + webapp.add_api_controllers( 'galaxy.webapps.galaxy.api', app ) - # The /folders section is experimental at this point: - log.debug( "app.config.api_folders: %s" % app.config.api_folders ) - webapp.mapper.resource( 'folder', 'folders', path_prefix='/api' ) - webapp.mapper.resource( 'content', 'contents', - controller='folder_contents', - name_prefix='folder_', - path_prefix='/api/folders/:folder_id', - parent_resources=dict( member_name='folder', collection_name='folders' ) ) - webapp.mapper.resource( 'content', - 'contents', - controller='library_contents', - name_prefix='library_', - path_prefix='/api/libraries/:library_id', - parent_resources=dict( member_name='library', collection_name='libraries' ) ) + webapp.mapper.resource( 'content', 'contents', controller='history_contents', @@ -102,10 +86,6 @@ controller="datasets", action="display", conditions=dict(method=["GET"])) - webapp.mapper.resource( 'permission', - 'permissions', - path_prefix='/api/libraries/:library_id', - parent_resources=dict( member_name='library', collection_name='libraries' ) ) webapp.mapper.resource( 'user', 'users', controller='group_users', @@ -127,11 +107,6 @@ _add_item_tags_controller( webapp, name_prefix="workflow_", path_prefix='/api/workflows/:workflow_id' ) - - _add_item_extended_metadata_controller( webapp, - name_prefix="library_dataset_", - path_prefix='/api/libraries/:library_id/contents/:library_content_id' ) - _add_item_annotation_controller( webapp, name_prefix="history_content_", path_prefix='/api/histories/:history_id/contents/:history_content_id' ) @@ -141,7 +116,6 @@ _add_item_annotation_controller( webapp, name_prefix="workflow_", path_prefix='/api/workflows/:workflow_id' ) - _add_item_provenance_controller( webapp, name_prefix="history_content_", path_prefix='/api/histories/:history_id/contents/:history_content_id' ) @@ -171,6 +145,39 @@ #webapp.mapper.connect( 'run_workflow', '/api/workflow/{workflow_id}/library/{library_id}', controller='workflows', action='run', workflow_id=None, library_id=None, conditions=dict(method=["GET"]) ) webapp.mapper.resource( 'search', 'search', path_prefix='/api' ) + # ======================= + # ===== LIBRARY API ===== + # ======================= + + # The /folders section is experimental at this point: + log.debug( "app.config.api_folders: %s" % app.config.api_folders ) + + webapp.mapper.connect( 'show_lda_item', '/api/libraries/datasets/:id', controller='lda_datasets', action='show', conditions=dict(method=["GET"]) ) + webapp.mapper.connect( 'download_lda_items', '/api/libraries/datasets/download/:format', controller='lda_datasets', action='download', conditions=dict(method=["POST"]) ) + + webapp.mapper.resource_with_deleted( 'library', 'libraries', path_prefix='/api' ) + webapp.mapper.resource( 'folder', 'folders', path_prefix='/api' ) + + webapp.mapper.resource( 'content', 'contents', + controller='folder_contents', + name_prefix='folder_', + path_prefix='/api/folders/:folder_id', + parent_resources=dict( member_name='folder', collection_name='folders' ) ) + webapp.mapper.resource( 'content', + 'contents', + controller='library_contents', + name_prefix='library_', + path_prefix='/api/libraries/:library_id', + parent_resources=dict( member_name='library', collection_name='libraries' ) ) + webapp.mapper.resource( 'permission', + 'permissions', + path_prefix='/api/libraries/:library_id', + parent_resources=dict( member_name='library', collection_name='libraries' ) ) + _add_item_extended_metadata_controller( webapp, + name_prefix="library_dataset_", + path_prefix='/api/libraries/:library_id/contents/:library_content_id' ) + + # add as a non-ATOM API call to support the notion of a 'current/working' history unique to the history resource webapp.mapper.connect( "set_as_current", "/api/histories/{id}/set_as_current", controller="histories", action="set_as_current", conditions=dict( method=["POST"] ) ) @@ -188,6 +195,18 @@ webapp.mapper.connect("workflow_dict", '/api/workflows/{workflow_id}/download', controller='workflows', action='workflow_dict', conditions=dict(method=['GET'])) # Preserve the following download route for now for dependent applications -- deprecate at some point webapp.mapper.connect("workflow_dict", '/api/workflows/download/{workflow_id}', controller='workflows', action='workflow_dict', conditions=dict(method=['GET'])) + + # ==================== + # ===== TOOLSHED ===== + # ==================== + + # Handle displaying tool help images and README file images contained in repositories installed from the tool shed. + webapp.add_route( '/admin_toolshed/static/images/:repository_id/:image_file', + controller='admin_toolshed', + action='display_image_in_repository', + repository_id=None, + image_file=None ) + # Galaxy API for tool shed features. webapp.mapper.resource( 'tool_shed_repository', 'tool_shed_repositories', @@ -201,6 +220,7 @@ path_prefix='/api', new={ 'install_repository_revision' : 'POST' }, parent_resources=dict( member_name='tool_shed_repository', collection_name='tool_shed_repositories' ) ) + # Connect logger from app if app.trace_logger: webapp.trace_logger = app.trace_logger @@ -221,7 +241,7 @@ galaxy.model.mapping.metadata.engine.connection_provider._pool.dispose() except: pass - # Close any pooled database connections before forking + # Close any pooled database connections before forking try: galaxy.model.tool_shed_install.mapping.metadata.engine.connection_provider._pool.dispose() except: diff -r df88d6121d48b0b193d34445ce49916adf17a2d2 -r 181b293ea65936b7edb338bcbeac3bdea8e53063 lib/galaxy/webapps/galaxy/controllers/library.py --- a/lib/galaxy/webapps/galaxy/controllers/library.py +++ b/lib/galaxy/webapps/galaxy/controllers/library.py @@ -76,6 +76,17 @@ library_list_grid = LibraryListGrid() + + @web.expose + def list( self, trans, **kwd ): + params = util.Params( kwd ) + # define app configuration for generic mako template + app = { + 'jscript' : "galaxy.library" + } + # fill template + return trans.fill_template('galaxy.panels.mako', config = {'app' : app}) + @web.expose def index( self, trans, **kwd ): params = util.Params( kwd ) diff -r df88d6121d48b0b193d34445ce49916adf17a2d2 -r 181b293ea65936b7edb338bcbeac3bdea8e53063 static/scripts/galaxy.library.js --- /dev/null +++ b/static/scripts/galaxy.library.js @@ -0,0 +1,860 @@ +// MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +// === GALAXY LIBRARY MODULE ==== +// MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM + +// global variables +var view = null; +var library_router = null; +var responses = []; + + +// load required libraries +// require([ +// // load js libraries +// 'utils/galaxy.css', +// ], function(css){ +// // load css +// css.load_file("static/style/library.css"); +// }); + +// dependencies +define(["galaxy.modal", "galaxy.masthead", "utils/galaxy.utils"], function(mod_modal, mod_masthead, mod_utils) { + +// MMMMMMMMMMMMMMM +// === Models ==== +// MMMMMMMMMMMMMMM + + // LIBRARY + var Library = Backbone.Model.extend({ + urlRoot: '/api/libraries' + }); + + // LIBRARIES + var Libraries = Backbone.Collection.extend({ + url: '/api/libraries', + model: Library + }); + + // ITEM + var Item = Backbone.Model.extend({ + urlRoot : '/api/libraries/datasets' + }) + + // FOLDER + var Folder = Backbone.Collection.extend({ + model: Item + }) + + // CONTAINER for folder contents (folders, items and metadata). + var FolderContainer = Backbone.Model.extend({ + defaults : { + folder : new Folder(), + full_path : "unknown", + urlRoot : "/api/folders/", + id : "unknown" + }, + parse : function(obj) { + this.full_path = obj[0].full_path; + // update the inner collection + this.get("folder").reset(obj[1].folder_contents); + return obj; + } + }) + + // HISTORY ITEM + var HistoryItem = Backbone.Model.extend({ + urlRoot : '/api/histories/' + }); + + // HISTORY + var GalaxyHistory = Backbone.Model.extend({ + url : '/api/histories/' + }); + + // HISTORIES + var GalaxyHistories = Backbone.Collection.extend({ + url : '/api/histories', + model : GalaxyHistory + }); + + //ROUTER + var LibraryRouter = Backbone.Router.extend({ + routes: { + "" : "libraries", + "folders/:id" : "folder_content", + "folders/:folder_id/download/:format" : "download" + } + }); + + +// MMMMMMMMMMMMMM +// === VIEWS ==== +// MMMMMMMMMMMMMM + +// galaxy folder +var FolderContentView = Backbone.View.extend({ + // main element definition + el : '#center', + // progress percentage + progress: 0, + // progress rate per one item + progressStep: 1, + // last selected history in modal for UX + lastSelectedHistory: '', + // self modal + modal : null, + // loaded folders + folders : null, + + // initialize + initialize : function(){ + this.folders = []; + this.queue = jQuery.Deferred(); + this.queue.resolve(); + }, + +// MMMMMMMMMMMMMMMMMM +// === TEMPLATES ==== +// MMMMMMMMMMMMMMMMMM + + // set up + templateFolder : function (){ + var tmpl_array = []; + + // CONTAINER + tmpl_array.push('<div id="library_container" style="width: 90%; margin: auto; margin-top: 2em; ">'); + tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to <a href="mailto:marten@bx.psu.edu?Subject=DataLibraries_Feedback" target="_blank">Marten</a>.</h3>'); + + // TOOLBAR + tmpl_array.push('<div id="library_folder_toolbar" >'); + tmpl_array.push(' <button title="Create New Folder" id="toolbtn_create_folder" class="btn btn-primary" type="button"><span class="fa fa-icon-plus"></span><span class="fa fa-icon-folder-close"></span> folder</button>'); + tmpl_array.push(' <button id="toolbtn_bulk_import" class="btn btn-primary" style="display: none; margin-left: 0.5em;" type="button"><span class="fa fa-icon-external-link"></span> to history</button>'); + + tmpl_array.push(' <div id="toolbtn_dl" class="btn-group" style="margin-left: 0.5em; display: none; ">'); + tmpl_array.push(' <button id="drop_toggle" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">'); + tmpl_array.push(' <span class="fa fa-icon-download"></span> download <span class="caret"></span>'); + tmpl_array.push(' </button>'); + tmpl_array.push(' <ul class="dropdown-menu" role="menu">'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/tgz">.tar.gz</a></li>'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/tbz">.tar.bz</a></li>'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/zip">.zip</a></li>'); + tmpl_array.push(' </ul>'); + tmpl_array.push(' </div>'); + + tmpl_array.push('</div>'); + + // BREADCRUMBS + tmpl_array.push('<div class="library_breadcrumb">'); + tmpl_array.push('<a title="Return to the list of libraries" href="#">Libraries</a><b>|</b> '); + tmpl_array.push('<% _.each(path, function(path_item) { %>'); + tmpl_array.push('<% if (path_item[0] != id) { %>'); + tmpl_array.push('<a title="Return to this folder" href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a><b>|</b> '); + tmpl_array.push('<% } else { %>'); + tmpl_array.push('<span title="You are in this folder"><%- path_item[1] %></span>'); + tmpl_array.push('<% } %>'); + tmpl_array.push('<% }); %>'); + tmpl_array.push('</div>'); + + // FOLDER CONTENT + tmpl_array.push('<table id="folder_table" class="table table-condensed">'); + tmpl_array.push(' <thead>'); + tmpl_array.push(' <th style="text-align: center; width: 20px; "><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th>'); + tmpl_array.push(' <th class="button_heading">view</th>'); + tmpl_array.push(' <th>name</th>'); + tmpl_array.push(' <th>data type</th>'); + tmpl_array.push(' <th>size</th>'); + tmpl_array.push(' <th>date</th>'); + tmpl_array.push(' </thead>'); + tmpl_array.push(' <tbody>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td><button title="Go to parent folder" type="button" data-id="<%- upper_folder_id %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-arrow-up"></span> .. go up</td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% _.each(items, function(content_item) { %>'); + tmpl_array.push(' <tr class="folder_row light" id="<%- content_item.id %>">'); + tmpl_array.push(' <% if (content_item.get("type") === "folder") { %>'); // folder + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td><button title="Open this folder" type="button" data-id="<%- content_item.id %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-folder-open"></span> browse</td>'); + tmpl_array.push(' <td><%- content_item.get("name") %>'); + tmpl_array.push(' <% if (content_item.get("item_count") === 0) { %>'); // empty folder + tmpl_array.push(' <span class="muted">(empty folder)</span>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' </td>'); + tmpl_array.push(' <td>folder</td>'); // data type + tmpl_array.push(' <td><%= _.escape(content_item.get("item_count")) %> item(s)</td>'); // size + tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <td style="text-align: center; "><input style="margin: 0;" type="checkbox"></td>'); + tmpl_array.push(' <td>'); + tmpl_array.push(' <button title="See details of this dataset" type="button" class="library-dataset btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-eye-open"></span> details'); + tmpl_array.push(' </button>'); + tmpl_array.push(' </td>'); + tmpl_array.push(' <td><%- content_item.get("name") %></td>'); // dataset + tmpl_array.push(' <td><%= _.escape(content_item.get("data_type")) %></td>'); // data type + tmpl_array.push(' <td><%= _.escape(content_item.get("readable_size")) %></td>'); // size + tmpl_array.push(' <% } %> '); + tmpl_array.push(' <td><%= _.escape(content_item.get("time_updated")) %></td>'); // time updated + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push(' '); + tmpl_array.push(' </tbody>'); + tmpl_array.push('</table>'); + + tmpl_array.push('</div>'); + return tmpl_array.join(''); + }, + templateDatasetModal : function(){ + var tmpl_array = []; + + tmpl_array.push('<div id="dataset_info_modal">'); + tmpl_array.push(' <table class="table table-striped table-condensed">'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row" id="id_row" data-id="<%= _.escape(item.get("ldda_id")) %>">Name</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("name")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Data type</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("data_type")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Genome build</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("genome_build")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <th scope="row">Size</th>'); + tmpl_array.push(' <td><%= _.escape(size) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Date uploaded</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("date_uploaded")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Uploaded by</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("uploaded_by")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr scope="row">'); + tmpl_array.push(' <th scope="row">Data Lines</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_data_lines")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <th scope="row">Comment Lines</th>'); + tmpl_array.push(' <% if (item.get("metadata_comment_lines") === "") { %>'); //folder + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_comment_lines")) %></td>'); + tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <td scope="row">unknown</td>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Number of Columns</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_columns")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Column Types</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_column_types")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Miscellaneous information</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("misc_blurb")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' </table>'); + tmpl_array.push(' <pre class="peek">'); + tmpl_array.push(' </pre>'); + tmpl_array.push('</div>'); + + return tmpl_array.join(''); + }, + + templateHistorySelectInModal : function(){ + var tmpl_array = []; + + tmpl_array.push('<span id="history_modal_combo" style="width:90%; margin-left: 1em; margin-right: 1em; ">'); + tmpl_array.push('Select history: '); + tmpl_array.push('<select id="dataset_import_single" name="dataset_import_single" style="width:50%; margin-bottom: 1em; "> '); + tmpl_array.push(' <% _.each(histories, function(history) { %>'); //history select box + tmpl_array.push(' <option value="<%= _.escape(history.get("id")) %>"><%= _.escape(history.get("name")) %></option>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push('</select>'); + tmpl_array.push('</span>'); + + return tmpl_array.join(''); + }, + + templateBulkImportInModal : function(){ + var tmpl_array = []; + + tmpl_array.push('<span id="history_modal_combo_bulk" style="width:90%; margin-left: 1em; margin-right: 1em; ">'); + tmpl_array.push('Select history: '); + tmpl_array.push('<select id="dataset_import_bulk" name="dataset_import_bulk" style="width:50%; margin-bottom: 1em; "> '); + tmpl_array.push(' <% _.each(histories, function(history) { %>'); //history select box + tmpl_array.push(' <option value="<%= _.escape(history.get("id")) %>"><%= _.escape(history.get("name")) %></option>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push('</select>'); + tmpl_array.push('</span>'); + + return tmpl_array.join(''); + }, + + // convert size to nice string + size_to_string : function (size) + { + // identify unit + var unit = ""; + if (size >= 100000000000) { size = size / 100000000000; unit = "TB"; } else + if (size >= 100000000) { size = size / 100000000; unit = "GB"; } else + if (size >= 100000) { size = size / 100000; unit = "MB"; } else + if (size >= 100) { size = size / 100; unit = "KB"; } else + { size = size * 10; unit = "b"; } + // return formatted string + return (Math.round(size) / 10) + unit; + }, + +// MMMMMMMMMMMMMMM +// === EVENTS ==== +// MMMMMMMMMMMMMMM + + // event binding + events: { + 'click #select-all-checkboxes' : 'selectAll', + 'click .folder_row' : 'selectClicked', + 'click #toolbtn_bulk_import' : 'modalBulkImport', + 'click #toolbtn_dl' : 'bulkDownload', + 'click .library-dataset' : 'showDatasetDetails', + 'click #toolbtn_create_folder' : 'createFolderModal', + 'click .btn_open_folder' : 'navigateToFolder' + }, + + //render the folder view + render: function (options) { + //hack to show scrollbars + $("#center").css('overflow','auto'); + + view = this; + var that = this; + + var folderContainer = new FolderContainer({id: options.id}); + folderContainer.url = folderContainer.attributes.urlRoot + options.id + '/contents'; + + folderContainer.fetch({ + success: function (container) { + + // prepare nice size strings + for (var i = 0; i < folderContainer.attributes.folder.models.length; i++) { + var model = folderContainer.attributes.folder.models[i] + if (model.get('type') === 'file'){ + model.set('readable_size', that.size_to_string(model.get('file_size'))) + } + }; + + // find the upper id + var path = folderContainer.full_path; + var upper_folder_id; + if (path.length === 1){ // library is above us + upper_folder_id = 0; + } else { + upper_folder_id = path[path.length-2][0]; + } + + var template = _.template(that.templateFolder(), { path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id, upper_folder_id: upper_folder_id }); + // var template = _.template(that.templateFolder(), { path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id }); + that.$el.html(template); + } + }) + }, + + // handles the click on 'open' and 'upper' folder icons + navigateToFolder : function(event){ + var folder_id = $(event.target).attr('data-id'); + if (typeof folder_id === 'undefined') { + return false; + } else if (folder_id === '0'){ + library_router.navigate('#', {trigger: true, replace: true}); + } else { + library_router.navigate('folders/' + folder_id, {trigger: true, replace: true}); + } + }, + + //show modal with current dataset info + showDatasetDetails : function(event){ + // prevent default + event.preventDefault(); + +//TODO check whether we already have the data + + //load the ID of the row + var id = $(event.target).parent().parent().attr('id'); + + //create new item + var item = new Item(); + var histories = new GalaxyHistories(); + item.id = id; + var self = this; + + //fetch the dataset info + item.fetch({ + success: function (item) { +// TODO can render here already + //fetch user histories for import purposes + histories.fetch({ + success: function (histories){self.renderModalAfterFetch(item, histories)} + }); + } + }); + }, + + // show the current dataset in a modal + renderModalAfterFetch : function(item, histories){ + var size = this.size_to_string(item.get('file_size')); + var template = _.template(this.templateDatasetModal(), { item : item, size : size }); + this.modal = null; + // make modal + var self = this; + this.modal = new mod_modal.GalaxyModal({ + title : 'Dataset Details', + body : template, + buttons : { + 'Import' : function() { self.importCurrentIntoHistory() }, + 'Download' : function() { self.downloadCurrent() }, + 'Close' : function() { self.modal.hide(); $('.modal').remove(); self.modal = null; } // TODO refill nicely modal with data + } + }); + $(".peek").html(item.get("peek")); + var history_footer_tmpl = _.template(this.templateHistorySelectInModal(), {histories : histories.models}); + $(this.modal.elMain).find('.buttons').prepend(history_footer_tmpl); + + // preset last selected history if we know it + if (self.lastSelectedHistory.length > 0) { + $(this.modal.elMain).find('#dataset_import_single').val(self.lastSelectedHistory); + } + + // show the prepared modal + this.modal.show(); + }, + + // download dataset shown currently in modal + downloadCurrent : function(){ + //disable the buttons + this.modal.disableButton('Import'); + this.modal.disableButton('Download'); + + var library_dataset_id = []; + library_dataset_id.push($('#id_row').attr('data-id')); + var url = '/api/libraries/datasets/download/uncompressed'; + var data = {'ldda_ids' : library_dataset_id}; + + // we assume the view is existent + folderContentView.processDownload(url, data); + this.modal.enableButton('Import'); + this.modal.enableButton('Download'); + }, + + // import dataset shown currently in modal into selected history + importCurrentIntoHistory : function(){ + //disable the buttons + this.modal.disableButton('Import'); + this.modal.disableButton('Download'); + + var history_id = $(this.modal.elMain).find('select[name=dataset_import_single] option:selected').val(); + this.lastSelectedHistory = history_id; //save selected history for further use + + var library_dataset_id = $('#id_row').attr('data-id'); + var historyItem = new HistoryItem(); + var self = this; + historyItem.url = historyItem.urlRoot + history_id + '/contents'; + + // save the dataset into selected history + historyItem.save({ content : library_dataset_id, source : 'library' }, { success : function(){ + self.modal.showNotification('Dataset imported', 3000, 'success'); + //enable the buttons + self.modal.enableButton('Import'); + self.modal.enableButton('Download'); + }, error : function(){ + self.modal.showNotification('An error occured! Dataset not imported. Please try again later.', 5000, 'error'); + //enable the buttons + self.modal.enableButton('Import'); + self.modal.enableButton('Download'); + } + }); + }, + + // select all datasets + selectAll : function (event) { + var selected = event.target.checked; + // Iterate each checkbox + $(':checkbox').each(function () { this.checked = selected; }); + this.showTools(); + }, + + // click checkbox on folder click + selectClicked : function (event) { + var checkbox = $("#" + event.target.parentElement.id).find(':checkbox') + if (checkbox[0] != undefined) { + if (checkbox[0].checked){ + checkbox[0].checked = ''; + // $(event.target.parentElement).css('background-color', '').css('color', ''); + $(event.target.parentElement).removeClass('dark'); + $(event.target.parentElement).find('a').removeClass('dark'); + $(event.target.parentElement).addClass('light'); + $(event.target.parentElement).find('a').addClass('light'); + } else { + checkbox[0].checked = 'selected'; + $(event.target.parentElement).removeClass('light'); + $(event.target.parentElement).find('a').removeClass('light'); + $(event.target.parentElement).addClass('dark'); + $(event.target.parentElement).find('a').addClass('dark'); + // $(event.target.parentElement).css('background-color', '#8389a1').css('color', 'white'); + } + } + this.showTools(); + }, + + // show toolbar in case something is selected + showTools : function(){ + var checkedValues = $('#folder_table').find(':checked'); + if(checkedValues.length > 0){ + $('#toolbtn_bulk_import').show(); + $('#toolbtn_dl').show(); + } else { + $('#toolbtn_bulk_import').hide(); + $('#toolbtn_dl').hide(); + } + + }, + + // show bulk import modal + modalBulkImport : function(){ + var self = this; + // fetch histories + var histories = new GalaxyHistories(); + histories.fetch({ + success: function (histories){ + // make modal + var history_modal_tmpl = _.template(self.templateBulkImportInModal(), {histories : histories.models}); + self.modal = new mod_modal.GalaxyModal({ + title : 'Import into History', + body : history_modal_tmpl, + buttons : { + 'Import' : function() {self.importAllIntoHistory()}, + 'Close' : function() {self.modal.hide(); $('.modal').remove(); self.modal = null;} + } + }); + // show the prepared modal + self.modal.show(); + } + }); + }, + + // import all selected datasets into history + importAllIntoHistory : function (){ + //disable the button + this.modal.disableButton('Import'); + + var history_id = $("select[name=dataset_import_bulk] option:selected").val(); + var history_name = $("select[name=dataset_import_bulk] option:selected").text(); + + var dataset_ids = []; + $('#folder_table').find(':checked').each(function(){ + if (this.parentElement.parentElement.id != '') { + dataset_ids.push(this.parentElement.parentElement.id); + } + }); + var progress_bar_tmpl = _.template(this.templateProgressBar(), { history_name : history_name }); + $(this.modal.elMain).find('.modal-body').html(progress_bar_tmpl); + + // init the progress bar + var progressStep = 100 / dataset_ids.length; + this.initProgress(progressStep); + + // prepare the dataset objects to be imported + var datasets_to_import = []; + for (var i = dataset_ids.length - 1; i >= 0; i--) { + library_dataset_id = dataset_ids[i]; + var historyItem = new HistoryItem(); + var self = this; + historyItem.url = historyItem.urlRoot + history_id + '/contents'; + historyItem.content = library_dataset_id; + historyItem.source = 'library'; + datasets_to_import.push(historyItem); + }; + + // call the recursive function to call ajax one after each other + this.chainCall(datasets_to_import); + }, + + chainCall: function(history_item_set){ + var self = this; + var popped_item = history_item_set.pop(); + if (typeof popped_item === "undefined") { + self.modal.showNotification('All datasets imported', 3000, 'success'); + // enable button again + self.modal.enableButton('Import'); + return + } + var promise = $.when(popped_item.save({content: popped_item.content, source: popped_item.source})).done(function(a1){ + self.updateProgress(); + responses.push(a1); + self.chainCall(history_item_set); + }); + }, + + initProgress: function(progressStep){ + this.progress = 0; + this.progressStep = progressStep; + }, + updateProgress: function(){ + this.progress += this.progressStep; + $('.progress-bar').width(Math.round(this.progress) + '%'); + txt_representation = Math.round(this.progress) + '% Complete'; + $('.completion_span').text(txt_representation); + }, + + // progress bar + templateProgressBar : function (){ + var tmpl_array = []; + + tmpl_array.push('<div class="import_text">'); + tmpl_array.push('Importing selected datasets to history <b><%= _.escape(history_name) %></b>'); + tmpl_array.push('</div>'); + tmpl_array.push('<div class="progress">'); + tmpl_array.push(' <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 00%;">'); + tmpl_array.push(' <span class="completion_span">0% Complete</span>'); + tmpl_array.push(' </div>'); + tmpl_array.push('</div>'); + tmpl_array.push(''); + + return tmpl_array.join(''); + }, + + // download selected datasets + download : function(folder_id, format){ + var dataset_ids = []; + $('#folder_table').find(':checked').each(function(){ + if (this.parentElement.parentElement.id != '') { + dataset_ids.push(this.parentElement.parentElement.id); + } + }); + + var url = '/api/libraries/datasets/download/' + format; + var data = {'ldda_ids' : dataset_ids}; + this.processDownload(url, data); + }, + + // create hidden form and submit through POST to initialize download + processDownload: function(url, data, method){ + //url and data options required + if( url && data ){ + //data can be string of parameters or array/object + data = typeof data == 'string' ? data : $.param(data); + //split params into form inputs + var inputs = ''; + $.each(data.split('&'), function(){ + var pair = this.split('='); + inputs+='<input type="hidden" name="'+ pair[0] +'" value="'+ pair[1] +'" />'; + }); + //send request + $('<form action="'+ url +'" method="'+ (method||'post') +'">'+inputs+'</form>') + .appendTo('body').submit().remove(); + }; + }, + + // shows modal for creating folder + createFolderModal: function(){ + alert('creating folder'); + } + + }); + +// galaxy library view +var GalaxyLibraryview = Backbone.View.extend({ + el: '#center', + + events: { + 'click #create_new_library_btn' : 'show_library_modal' + }, + + // initialize + initialize : function(){ + }, + + // template + template_library_list : function (){ + tmpl_array = []; + tmpl_array.push('<div id="library_container" style="width: 90%; margin: auto; margin-top: 2em; overflow: auto !important; ">'); + + tmpl_array.push(''); + tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to <a href="mailto:marten@bx.psu.edu?Subject=DataLibraries_Feedback" target="_blank">Marten</a>.</h3>'); + tmpl_array.push('<a href="" id="create_new_library_btn" class="btn btn-primary icon-file ">New Library</a>'); + tmpl_array.push('<table class="table table-condensed">'); + tmpl_array.push(' <thead>'); + tmpl_array.push(' <th class="button_heading"></th>'); + tmpl_array.push(' <th>name</th>'); + tmpl_array.push(' <th>description</th>'); + tmpl_array.push(' <th>synopsis</th> '); + tmpl_array.push(' <th>model type</th> '); + tmpl_array.push(' </thead>'); + tmpl_array.push(' <tbody>'); + tmpl_array.push(' <% _.each(libraries, function(library) { %>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <td><button title="Open this library" type="button" data-id="<%- library.get("root_folder_id") %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-folder-open"></span> browse</td>'); + tmpl_array.push(' <td><%- library.get("name") %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("description")) %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("synopsis")) %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("model_class")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push(' </tbody>'); + tmpl_array.push('</table>'); + + tmpl_array.push('</div>'); + return tmpl_array.join(''); + }, + + // render + render: function () { + //hack to show scrollbars + $("#center").css('overflow','auto'); + var that = this; + // if (typeof libraries === "undefined") { + libraries = new Libraries(); + // } + libraries.fetch({ + success: function (libraries) { + var template = _.template(that.template_library_list(), { libraries : libraries.models }); + that.$el.html(template); + } + }) + }, + + // own modal + modal : null, + + // show/hide create library modal + show_library_modal : function (event){ + event.preventDefault(); + event.stopPropagation(); + + // create modal + var self = this; + this.modal = new mod_modal.GalaxyModal( + { + title : 'Create New Library', + body : this.template_new_library(), + buttons : { + 'Create' : function() {self.create_new_library_event()}, + 'Close' : function() {self.modal.hide()} + } + }); + + // show prepared modal + this.modal.show(); + }, + + // create the new library from modal + create_new_library_event: function(){ + var libraryDetails = this.serialize_new_library(); + var valid = this.validate_new_library(libraryDetails); + var library = new Library(); + var self = this; + library.save(libraryDetails, { + success: function (library) { + self.modal.hide(); + self.clear_library_modal(); + self.render(); + }, + error: function(){ + self.modal.showNotification('An error occured', 5000, 'error'); + } + }); + return false; + }, + + // clear the library modal once saved + clear_library_modal : function(){ + $("input[name='Name']").val(''); + $("input[name='Description']").val(''); + $("input[name='Synopsis']").val(''); + }, + + // serialize data from the form + serialize_new_library : function(){ + return { + name: $("input[name='Name']").val(), + description: $("input[name='Description']").val(), + synopsis: $("input[name='Synopsis']").val() + }; + }, + + validate_new_library: function(library){ + + }, + + + // template for new library modal + template_new_library: function() + { + tmpl_array = []; + + tmpl_array.push('<div id="new_library_modal">'); + tmpl_array.push('<form>'); + tmpl_array.push('<input type="text" name="Name" value="" placeholder="Name">'); + tmpl_array.push('<input type="text" name="Description" value="" placeholder="Description">'); + tmpl_array.push('<input type="text" name="Synopsis" value="" placeholder="Synopsis">'); + tmpl_array.push('</form>'); + tmpl_array.push('</div>'); + + return tmpl_array.join(''); + } +}); + +// galaxy library wrapper View +var GalaxyLibrary = Backbone.View.extend({ + folderContentView : null, + galaxyLibraryview : null, + initialize : function(){ + + folderContentView = new FolderContentView(); + galaxyLibraryview = new GalaxyLibraryview(); + + library_router = new LibraryRouter(); + + library_router.on('route:libraries', function() { + // render libraries list + galaxyLibraryview.render(); + }); + + library_router.on('route:folder_content', function(id) { + // render folder's contents + folderContentView.render({id: id}); + }); + + library_router.on('route:download', function(folder_id, format) { + // send download stream + if (typeof folderContentView === 'undefined'){ + alert('you cant touch this!'); + // } else if (folderContentView.modal !== null){ + // folderContentView.download(folder_id, format); + } else if ($('#center').find(':checked').length === 0) { // coming from outside of the library app + library_router.navigate('folders/' + folder_id, {trigger: true, replace: true}); + // TODO show message of redirection + } else { + folderContentView.download(folder_id, format); + library_router.navigate('folders/' + folder_id, {trigger: false, replace: true}); + } + + }); + +Backbone.history.start(); + +return this +} +}); + +// return +return { + GalaxyApp: GalaxyLibrary +}; + +}); diff -r df88d6121d48b0b193d34445ce49916adf17a2d2 -r 181b293ea65936b7edb338bcbeac3bdea8e53063 static/scripts/galaxy.menu.js --- a/static/scripts/galaxy.menu.js +++ b/static/scripts/galaxy.menu.js @@ -57,7 +57,12 @@ tab_shared.add({ title : "Data Libraries", - content : "library/index", + content : "library/index" + }); + + tab_shared.add({ + title : "New Libraries", + content : "library/list", divider : true }); diff -r df88d6121d48b0b193d34445ce49916adf17a2d2 -r 181b293ea65936b7edb338bcbeac3bdea8e53063 static/scripts/galaxy.modal.js --- a/static/scripts/galaxy.modal.js +++ b/static/scripts/galaxy.modal.js @@ -1,6 +1,3 @@ -/* - galaxy modal -*/ // dependencies define([], function() { @@ -25,8 +22,51 @@ // initialize initialize : function(options) { + self = this; if (options) this.create(options); + + this.bindClick(event, self); + }, + + // bind the click-to-hide function + bindClick: function(event, that) { + // bind the ESC key to hide() function + $(document).on('keyup', function(event){ + if (event.keyCode == 27) { self.hide(); } + }) + // bind the click anywhere to hide() function... + $('html').on('click', function(event){ + that.hide(); + }) + // ...but don't hide if the click is on modal content + $('.modal-content').on('click', function(event){ + event.stopPropagation(); + }) + }, + + // unbind the click-to-hide function + unbindClick: function(event, that){ + // bind the ESC key to hide() function + $(document).off('keyup', function(event){ + if (event.keyCode == 27) { that.hide(); } + }) + // unbind the click anywhere to hide() function... + $('html').off('click', function(event){ + that.hide(); + }) + // ...but don't hide if the click is on modal content + $('.modal-content').off('click', function(event){ + event.stopPropagation(); + }) + }, + + + // destroy + destroy : function(){ + this.hide(); + this.unbindClick(); + $('.modal').remove(); }, // adds and displays a new frame/window @@ -87,6 +127,7 @@ this.$footer = (this.$el).find('.modal-footer'); this.$buttons = (this.$el).find('.buttons'); this.$backdrop = (this.$el).find('.modal-backdrop'); + this.$notification = (this.$el).find('.notification-modal'); // append body this.$body.html(this.options.body); @@ -120,6 +161,47 @@ this.$buttons.find('#' + String(name).toLowerCase()).prop('disabled', true); }, + // hide buttons + hideButton: function(name) { + this.$buttons.find('#' + String(name).toLowerCase()).hide(); + }, + // show buttons + showButton: function(name) { + this.$buttons.find('#' + String(name).toLowerCase()).show(); + }, + + // show notification + showNotification : function(message, duration, type) { + // defaults + var duration = typeof duration !== 'undefined' ? duration : 1500; + // var bgColor = typeof bgColor !== 'undefined' ? bgColor : "#F4E0E1"; + // var txtColor = typeof txtColor !== 'undefined' ? txtColor : "#A42732"; + var bgColor; + var txtColor; + + if (type === 'error'){ + bgColor = '#f4e0e1'; + txtColor = '#a42732'; + // } else if (type === 'success'){ + } else { // success is default + bgColor = '#e1f4e0'; + txtColor = '#32a427'; + } + + var HTMLmessage = "<div class='notification-message' style='text-align:center; line-height:16px; '> " + message + " </div>"; + this.$notification.html("<div id='notification-bar' style='display:none; float: right; height: 16px; width:100%; background-color: " + bgColor + "; z-index: 100; color: " + txtColor + ";border-bottom: 1px solid " + txtColor + ";'>" + HTMLmessage + "</div>"); + + var self = this; + + /*animate the bar*/ + $('#notification-bar').slideDown(function() { + setTimeout(function() { + $('#notification-bar').slideUp(function() {self.$notification.html('');}); + }, duration); + }); + + }, + // returns scroll top for body element scrollTop: function() { @@ -139,7 +221,8 @@ '<div class="modal-header">' + '<button type="button" class="close" style="display: none;">×</button>' + '<h4 class="title">' + title + '</h4>' + - '</div>' + + '<span class="notification-modal"></span>' + + '</div>' + '<div class="modal-body"></div>' + '<div class="modal-footer">' + '<div class="buttons" style="float: right;"></div>' + diff -r df88d6121d48b0b193d34445ce49916adf17a2d2 -r 181b293ea65936b7edb338bcbeac3bdea8e53063 static/scripts/libs/bootstrap.js --- a/static/scripts/libs/bootstrap.js +++ b/static/scripts/libs/bootstrap.js @@ -575,3 +575,158 @@ } }(window.jQuery); + +/* ======================================================================== + * Bootstrap: dropdown.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#dropdowns + * ======================================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle=dropdown]' + var Dropdown = function (element) { + var $el = $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we we use a backdrop because click events don't delegate + $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus) + } + + $parent.trigger(e = $.Event('show.bs.dropdown')) + + if (e.isDefaultPrevented()) return + + $parent + .toggleClass('open') + .trigger('shown.bs.dropdown') + + $this.focus() + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27)/.test(e.keyCode)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) { + if (e.which == 27) $parent.find(toggle).focus() + return $this.click() + } + + var $items = $('[role=menu] li:not(.divider):visible a', $parent) + + if (!$items.length) return + + var index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index=0 + + $items.eq(index).focus() + } + + function clearMenus() { + $(backdrop).remove() + $(toggle).each(function (e) { + var $parent = getParent($(this)) + if (!$parent.hasClass('open')) return + $parent.trigger(e = $.Event('hide.bs.dropdown')) + if (e.isDefaultPrevented()) return + $parent.removeClass('open').trigger('hidden.bs.dropdown') + }) + } + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + var old = $.fn.dropdown + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('dropdown') + + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== + + $(document) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api' , toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + +}(window.jQuery); \ No newline at end of file diff -r df88d6121d48b0b193d34445ce49916adf17a2d2 -r 181b293ea65936b7edb338bcbeac3bdea8e53063 static/style/src/less/library.less --- a/static/style/src/less/library.less +++ b/static/style/src/less/library.less @@ -29,6 +29,50 @@ background-color: @table-bg-accent; } + +tr.light td +{ + background-color: white; + color: black; +} +tr.light:hover td +{ + background-color: #f5f5f5; + color: #8389a1; +} +tr.dark td +{ + background-color: #8389a1; + color: white; +} +tr.dark:hover td +{ + background-color: #bbbfd0; + color: white; +} +a.dark:hover +{ + color: yellow; + // text-decoration: none; +} +a.dark +{ + color: white; + // text-decoration: none; +} +th.button_heading +{ + width: 7em; +} +div.library_breadcrumb{ + padding-top: 0.8em; + padding-bottom: 0.8em; +} +div.library_breadcrumb a:hover{ + color:green; +} + + img.expanderIcon { padding-right: 4px; } https://bitbucket.org/galaxy/galaxy-central/commits/17c16125da1a/ Changeset: 17c16125da1a User: martenson Date: 2013-12-11 17:22:38 Summary: Merged galaxy/galaxy-central into default Affected #: 4 files diff -r 181b293ea65936b7edb338bcbeac3bdea8e53063 -r 17c16125da1a00a53a020ff0fd84e5676dbabaf1 lib/galaxy/model/mapping.py --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -1050,8 +1050,11 @@ Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), Column( "key", TrimmedString( 32 ), index=True, unique=True ) ) + # With the tables defined we can define the mappers and setup the # relationships between the model objects. +def simple_mapping( model, **kwds ): + mapper( model, model.table, properties=kwds ) mapper( model.Sample, model.Sample.table, @@ -1200,64 +1203,62 @@ mapper( model.ValidationError, model.ValidationError.table ) -mapper( model.HistoryDatasetAssociation, model.HistoryDatasetAssociation.table, - properties=dict( - dataset=relation( - model.Dataset, - primaryjoin=( model.Dataset.table.c.id == model.HistoryDatasetAssociation.table.c.dataset_id ), lazy=False ), - # .history defined in History mapper - copied_from_history_dataset_association=relation( - model.HistoryDatasetAssociation, - primaryjoin=( model.HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == model.HistoryDatasetAssociation.table.c.id ), - remote_side=[model.HistoryDatasetAssociation.table.c.id], - uselist=False ), - copied_to_history_dataset_associations=relation( - model.HistoryDatasetAssociation, - primaryjoin=( model.HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == model.HistoryDatasetAssociation.table.c.id ) ), - copied_from_library_dataset_dataset_association=relation( - model.LibraryDatasetDatasetAssociation, - primaryjoin=( model.HistoryDatasetAssociation.table.c.copied_from_library_dataset_dataset_association_id == model.LibraryDatasetDatasetAssociation.table.c.id ), - uselist=False ), - copied_to_library_dataset_dataset_associations=relation( - model.LibraryDatasetDatasetAssociation, - primaryjoin=( model.HistoryDatasetAssociation.table.c.copied_from_library_dataset_dataset_association_id == model.LibraryDatasetDatasetAssociation.table.c.id ) ), - implicitly_converted_datasets=relation( - model.ImplicitlyConvertedDatasetAssociation, - primaryjoin=( model.ImplicitlyConvertedDatasetAssociation.table.c.hda_parent_id == model.HistoryDatasetAssociation.table.c.id ) ), - implicitly_converted_parent_datasets=relation( - model.ImplicitlyConvertedDatasetAssociation, - primaryjoin=( model.ImplicitlyConvertedDatasetAssociation.table.c.hda_id == model.HistoryDatasetAssociation.table.c.id ) ), - children=relation( - model.HistoryDatasetAssociation, - primaryjoin=( model.HistoryDatasetAssociation.table.c.parent_id == model.HistoryDatasetAssociation.table.c.id ), - backref=backref( "parent", primaryjoin=( model.HistoryDatasetAssociation.table.c.parent_id == model.HistoryDatasetAssociation.table.c.id ), remote_side=[model.HistoryDatasetAssociation.table.c.id], uselist=False ) ), - visible_children=relation( - model.HistoryDatasetAssociation, - primaryjoin=( ( model.HistoryDatasetAssociation.table.c.parent_id == model.HistoryDatasetAssociation.table.c.id ) & ( model.HistoryDatasetAssociation.table.c.visible == True ) ) ), - tags=relation( model.HistoryDatasetAssociationTagAssociation, order_by=model.HistoryDatasetAssociationTagAssociation.table.c.id, backref='history_tag_associations' ), - annotations=relation( model.HistoryDatasetAssociationAnnotationAssociation, order_by=model.HistoryDatasetAssociationAnnotationAssociation.table.c.id, backref="hdas" ), - ratings=relation( model.HistoryDatasetAssociationRatingAssociation, order_by=model.HistoryDatasetAssociationRatingAssociation.table.c.id, backref="hdas" ) ) - ) +simple_mapping( model.HistoryDatasetAssociation, + dataset=relation( + model.Dataset, + primaryjoin=( model.Dataset.table.c.id == model.HistoryDatasetAssociation.table.c.dataset_id ), lazy=False ), + # .history defined in History mapper + copied_from_history_dataset_association=relation( + model.HistoryDatasetAssociation, + primaryjoin=( model.HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == model.HistoryDatasetAssociation.table.c.id ), + remote_side=[model.HistoryDatasetAssociation.table.c.id], + uselist=False ), + copied_to_history_dataset_associations=relation( + model.HistoryDatasetAssociation, + primaryjoin=( model.HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == model.HistoryDatasetAssociation.table.c.id ) ), + copied_from_library_dataset_dataset_association=relation( + model.LibraryDatasetDatasetAssociation, + primaryjoin=( model.HistoryDatasetAssociation.table.c.copied_from_library_dataset_dataset_association_id == model.LibraryDatasetDatasetAssociation.table.c.id ), + uselist=False ), + copied_to_library_dataset_dataset_associations=relation( + model.LibraryDatasetDatasetAssociation, + primaryjoin=( model.HistoryDatasetAssociation.table.c.copied_from_library_dataset_dataset_association_id == model.LibraryDatasetDatasetAssociation.table.c.id ) ), + implicitly_converted_datasets=relation( + model.ImplicitlyConvertedDatasetAssociation, + primaryjoin=( model.ImplicitlyConvertedDatasetAssociation.table.c.hda_parent_id == model.HistoryDatasetAssociation.table.c.id ) ), + implicitly_converted_parent_datasets=relation( + model.ImplicitlyConvertedDatasetAssociation, + primaryjoin=( model.ImplicitlyConvertedDatasetAssociation.table.c.hda_id == model.HistoryDatasetAssociation.table.c.id ) ), + children=relation( + model.HistoryDatasetAssociation, + primaryjoin=( model.HistoryDatasetAssociation.table.c.parent_id == model.HistoryDatasetAssociation.table.c.id ), + backref=backref( "parent", primaryjoin=( model.HistoryDatasetAssociation.table.c.parent_id == model.HistoryDatasetAssociation.table.c.id ), remote_side=[model.HistoryDatasetAssociation.table.c.id], uselist=False ) ), + visible_children=relation( + model.HistoryDatasetAssociation, + primaryjoin=( ( model.HistoryDatasetAssociation.table.c.parent_id == model.HistoryDatasetAssociation.table.c.id ) & ( model.HistoryDatasetAssociation.table.c.visible == True ) ) ), + tags=relation( model.HistoryDatasetAssociationTagAssociation, order_by=model.HistoryDatasetAssociationTagAssociation.table.c.id, backref='history_tag_associations' ), + annotations=relation( model.HistoryDatasetAssociationAnnotationAssociation, order_by=model.HistoryDatasetAssociationAnnotationAssociation.table.c.id, backref="hdas" ), + ratings=relation( model.HistoryDatasetAssociationRatingAssociation, order_by=model.HistoryDatasetAssociationRatingAssociation.table.c.id, backref="hdas" ) +) -mapper( model.Dataset, model.Dataset.table, - properties=dict( - history_associations=relation( - model.HistoryDatasetAssociation, - primaryjoin=( model.Dataset.table.c.id == model.HistoryDatasetAssociation.table.c.dataset_id ) ), - active_history_associations=relation( - model.HistoryDatasetAssociation, - primaryjoin=( ( model.Dataset.table.c.id == model.HistoryDatasetAssociation.table.c.dataset_id ) & ( model.HistoryDatasetAssociation.table.c.deleted == False ) & ( model.HistoryDatasetAssociation.table.c.purged == False ) ) ), - purged_history_associations=relation( - model.HistoryDatasetAssociation, - primaryjoin=( ( model.Dataset.table.c.id == model.HistoryDatasetAssociation.table.c.dataset_id ) & ( model.HistoryDatasetAssociation.table.c.purged == True ) ) ), - library_associations=relation( - model.LibraryDatasetDatasetAssociation, - primaryjoin=( model.Dataset.table.c.id == model.LibraryDatasetDatasetAssociation.table.c.dataset_id ) ), - active_library_associations=relation( - model.LibraryDatasetDatasetAssociation, - primaryjoin=( ( model.Dataset.table.c.id == model.LibraryDatasetDatasetAssociation.table.c.dataset_id ) & ( model.LibraryDatasetDatasetAssociation.table.c.deleted == False ) ) ), - tags=relation(model.DatasetTagAssociation, order_by=model.DatasetTagAssociation.table.c.id, backref='datasets') - ) ) +simple_mapping( model.Dataset, + history_associations=relation( + model.HistoryDatasetAssociation, + primaryjoin=( model.Dataset.table.c.id == model.HistoryDatasetAssociation.table.c.dataset_id ) ), + active_history_associations=relation( + model.HistoryDatasetAssociation, + primaryjoin=( ( model.Dataset.table.c.id == model.HistoryDatasetAssociation.table.c.dataset_id ) & ( model.HistoryDatasetAssociation.table.c.deleted == False ) & ( model.HistoryDatasetAssociation.table.c.purged == False ) ) ), + purged_history_associations=relation( + model.HistoryDatasetAssociation, + primaryjoin=( ( model.Dataset.table.c.id == model.HistoryDatasetAssociation.table.c.dataset_id ) & ( model.HistoryDatasetAssociation.table.c.purged == True ) ) ), + library_associations=relation( + model.LibraryDatasetDatasetAssociation, + primaryjoin=( model.Dataset.table.c.id == model.LibraryDatasetDatasetAssociation.table.c.dataset_id ) ), + active_library_associations=relation( + model.LibraryDatasetDatasetAssociation, + primaryjoin=( ( model.Dataset.table.c.id == model.LibraryDatasetDatasetAssociation.table.c.dataset_id ) & ( model.LibraryDatasetDatasetAssociation.table.c.deleted == False ) ) ), + tags=relation(model.DatasetTagAssociation, order_by=model.DatasetTagAssociation.table.c.id, backref='datasets') +) mapper( model.HistoryDatasetAssociationDisplayAtAuthorization, model.HistoryDatasetAssociationDisplayAtAuthorization.table, properties=dict( history_dataset_association = relation( model.HistoryDatasetAssociation ), @@ -1751,90 +1752,63 @@ ) ) # Tag tables. +simple_mapping( model.Tag, + children=relation(model.Tag, backref=backref( 'parent', remote_side=[model.Tag.table.c.id] ) ) +) -mapper( model.Tag, model.Tag.table, - properties=dict( children=relation(model.Tag, backref=backref( 'parent', remote_side=[model.Tag.table.c.id] ) ) - ) ) -mapper( model.HistoryTagAssociation, model.HistoryTagAssociation.table, - properties=dict( tag=relation(model.Tag, backref="tagged_histories"), user=relation( model.User ) ) - ) +def tag_mapping( tag_association_class, backref_name ): + simple_mapping( tag_association_class, tag=relation( model.Tag, backref=backref_name), user=relation( model.User ) ) -mapper( model.DatasetTagAssociation, model.DatasetTagAssociation.table, - properties=dict( tag=relation(model.Tag, backref="tagged_datasets"), user=relation( model.User ) ) - ) +tag_mapping( model.HistoryTagAssociation, "tagged_histories" ) -mapper( model.HistoryDatasetAssociationTagAssociation, model.HistoryDatasetAssociationTagAssociation.table, - properties=dict( tag=relation(model.Tag, backref="tagged_history_dataset_associations"), user=relation( model.User ) ) - ) +tag_mapping( model.DatasetTagAssociation, "tagged_datasets" ) -mapper( model.PageTagAssociation, model.PageTagAssociation.table, - properties=dict( tag=relation(model.Tag, backref="tagged_pages"), user=relation( model.User ) ) - ) +tag_mapping( model.HistoryDatasetAssociationTagAssociation, "tagged_history_dataset_associations" ) -mapper( model.StoredWorkflowTagAssociation, model.StoredWorkflowTagAssociation.table, - properties=dict( tag=relation(model.Tag, backref="tagged_workflows"), user=relation( model.User ) ) - ) +tag_mapping( model.PageTagAssociation, "tagged_pages" ) -mapper( model.WorkflowStepTagAssociation, model.WorkflowStepTagAssociation.table, - properties=dict( tag=relation(model.Tag, backref="tagged_workflow_steps"), user=relation( model.User ) ) - ) +tag_mapping( model.StoredWorkflowTagAssociation, "tagged_workflows" ) -mapper( model.VisualizationTagAssociation, model.VisualizationTagAssociation.table, - properties=dict( tag=relation(model.Tag, backref="tagged_visualizations"), user=relation( model.User ) ) - ) +tag_mapping( model.WorkflowStepTagAssociation, "tagged_workflow_steps" ) -mapper( model.ToolTagAssociation, model.ToolTagAssociation.table, - properties=dict( tag=relation(model.Tag, backref="tagged_tools"), user=relation( model.User ) ) - ) +tag_mapping( model.VisualizationTagAssociation, "tagged_visualizations" ) + +tag_mapping( model.ToolTagAssociation, "tagged_tools" ) + # Annotation tables. +def annotation_mapping( annotation_class, **kwds ): + kwds = dict( [ (key, relation( value ) ) for key, value in kwds.iteritems() ] ) + simple_mapping( annotation_class, **dict(user=relation( model.User ), **kwds ) ) -mapper( model.HistoryAnnotationAssociation, model.HistoryAnnotationAssociation.table, - properties=dict( history=relation( model.History ), user=relation( model.User ) ) - ) +annotation_mapping( model.HistoryAnnotationAssociation, history=model.History ) -mapper( model.HistoryDatasetAssociationAnnotationAssociation, model.HistoryDatasetAssociationAnnotationAssociation.table, - properties=dict( hda=relation( model.HistoryDatasetAssociation ), user=relation( model.User ) ) - ) +annotation_mapping( model.HistoryDatasetAssociationAnnotationAssociation, hda=model.HistoryDatasetAssociation ) -mapper( model.StoredWorkflowAnnotationAssociation, model.StoredWorkflowAnnotationAssociation.table, - properties=dict( stored_workflow=relation( model.StoredWorkflow ), user=relation( model.User ) ) - ) +annotation_mapping( model.StoredWorkflowAnnotationAssociation, stored_workflow=model.StoredWorkflow ) -mapper( model.WorkflowStepAnnotationAssociation, model.WorkflowStepAnnotationAssociation.table, - properties=dict( workflow_step=relation( model.WorkflowStep ), user=relation( model.User ) ) - ) +annotation_mapping( model.WorkflowStepAnnotationAssociation, workflow_step=model.WorkflowStep ) -mapper( model.PageAnnotationAssociation, model.PageAnnotationAssociation.table, - properties=dict( page=relation( model.Page ), user=relation( model.User ) ) - ) +annotation_mapping( model.PageAnnotationAssociation, page=model.Page ) -mapper( model.VisualizationAnnotationAssociation, model.VisualizationAnnotationAssociation.table, - properties=dict( visualization=relation( model.Visualization ), user=relation( model.User ) ) - ) +annotation_mapping( model.VisualizationAnnotationAssociation, visualization=model.Visualization ) + # Rating tables. +def rating_mapping( rating_class, **kwds ): + kwds = dict( [ (key, relation( value ) ) for key, value in kwds.iteritems() ] ) + simple_mapping( rating_class, **dict(user=relation( model.User ), **kwds ) ) -mapper( model.HistoryRatingAssociation, model.HistoryRatingAssociation.table, - properties=dict( history=relation( model.History ), user=relation( model.User ) ) - ) +rating_mapping( model.HistoryRatingAssociation, history=model.History ) -mapper( model.HistoryDatasetAssociationRatingAssociation, model.HistoryDatasetAssociationRatingAssociation.table, - properties=dict( hda=relation( model.HistoryDatasetAssociation ), user=relation( model.User ) ) - ) +rating_mapping( model.HistoryDatasetAssociationRatingAssociation, hda=model.HistoryDatasetAssociation ) -mapper( model.StoredWorkflowRatingAssociation, model.StoredWorkflowRatingAssociation.table, - properties=dict( stored_workflow=relation( model.StoredWorkflow ), user=relation( model.User ) ) - ) +rating_mapping( model.StoredWorkflowRatingAssociation, stored_workflow=model.StoredWorkflow ) -mapper( model.PageRatingAssociation, model.PageRatingAssociation.table, - properties=dict( page=relation( model.Page ), user=relation( model.User ) ) - ) +rating_mapping( model.PageRatingAssociation, page=model.Page ) -mapper( model.VisualizationRatingAssociation, model.VisualizationRatingAssociation.table, - properties=dict( visualization=relation( model.Visualization ), user=relation( model.User ) ) - ) +rating_mapping( model.VisualizationRatingAssociation, visualizaiton=model.Visualization ) #Data Manager tables mapper( model.DataManagerHistoryAssociation, model.DataManagerHistoryAssociation.table, diff -r 181b293ea65936b7edb338bcbeac3bdea8e53063 -r 17c16125da1a00a53a020ff0fd84e5676dbabaf1 lib/galaxy/tools/__init__.py --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -3003,10 +3003,10 @@ # Add link details. if link_details: # Add details for creating a hyperlink to the tool. - if not self.tool_type.startswith( 'data_source' ): - link = url_for( '/tool_runner', tool_id=self.id ) + if not isinstance( self, DataSourceTool ): + link = url_for( controller='tool_runner', tool_id=self.id ) else: - link = url_for( self.action, **self.get_static_param_values( trans ) ) + link = url_for( controller='tool_runner', action='data_source_redirect', tool_id=self.id ) # Basic information tool_dict.update( { 'link': link, diff -r 181b293ea65936b7edb338bcbeac3bdea8e53063 -r 17c16125da1a00a53a020ff0fd84e5676dbabaf1 lib/galaxy/webapps/galaxy/controllers/tool_runner.py --- a/lib/galaxy/webapps/galaxy/controllers/tool_runner.py +++ b/lib/galaxy/webapps/galaxy/controllers/tool_runner.py @@ -6,6 +6,7 @@ import galaxy.util from galaxy import web from galaxy.tools import DefaultToolState +from galaxy.tools import DataSourceTool from galaxy.tools.actions import upload_common from galaxy.tools.parameters import params_to_incoming from galaxy.tools.parameters import visit_input_values @@ -233,6 +234,35 @@ **vars ) @web.expose + def data_source_redirect( self, trans, tool_id=None ): + """ + Redirects a user accessing a Data Source tool to its target action link. + This method will subvert mix-mode content blocking in several browsers when + accessing non-https data_source tools from an https galaxy server. + + Tested as working on Safari 7.0 and FireFox 26 + Subverting did not work on Chrome 31 + """ + if tool_id is None: + return trans.response.send_redirect( url_for( controller="root", action="welcome" ) ) + tool_version_select_field, tools, tool = self.__get_tool_components( tool_id, + tool_version=None, + get_loaded_tools_by_lineage=False, + set_selected=False ) + # No tool matching the tool id, display an error (shouldn't happen) + if not tool: + log.error( "data_source_redirect called with tool id '%s' but no such tool exists", tool_id ) + trans.log_event( "Tool id '%s' does not exist" % tool_id ) + trans.response.status = 404 + return "Tool '%s' does not exist, kwd=%s " % ( tool_id, kwd ) + + if isinstance( tool, DataSourceTool ): + link = url_for( tool.action, **tool.get_static_param_values( trans ) ) + else: + link = url_for( controller='tool_runner', tool_id=tool.id ) + return trans.response.send_redirect( link ) + + @web.expose def redirect( self, trans, redirect_url=None, **kwd ): if not redirect_url: return trans.show_error_message( "Required URL for redirection missing" ) diff -r 181b293ea65936b7edb338bcbeac3bdea8e53063 -r 17c16125da1a00a53a020ff0fd84e5676dbabaf1 test/unit/test_galaxy_mapping.py --- a/test/unit/test_galaxy_mapping.py +++ b/test/unit/test_galaxy_mapping.py @@ -3,10 +3,139 @@ class MappingTests( unittest.TestCase ): + + def test_annotations( self ): + model = self.model + + u = model.User( email="annotator@example.com", password="password" ) + self.persist( u ) + + def persist_and_check_annotation( annotation_class, **kwds ): + annotated_association = annotation_class() + annotated_association.annotation = "Test Annotation" + annotated_association.user = u + for key, value in kwds.iteritems(): + setattr(annotated_association, key, value) + self.persist( annotated_association ) + self.expunge() + stored_annotation = self.query( annotation_class ).all()[0] + assert stored_annotation.annotation == "Test Annotation" + assert stored_annotation.user.email == "annotator@example.com" + + sw = model.StoredWorkflow() + sw.user = u + self.persist( sw ) + persist_and_check_annotation( model.StoredWorkflowAnnotationAssociation, stored_workflow=sw ) + + workflow = model.Workflow() + workflow.stored_workflow = sw + self.persist( workflow ) + + ws = model.WorkflowStep() + ws.workflow = workflow + self.persist( ws ) + persist_and_check_annotation( model.WorkflowStepAnnotationAssociation, workflow_step=ws ) + + h = model.History( name="History for Annotation", user=u) + self.persist( h ) + persist_and_check_annotation( model.HistoryAnnotationAssociation, history=h ) + + d1 = model.HistoryDatasetAssociation( extension="txt", history=h, create_dataset=True, sa_session=model.session ) + self.persist( d1 ) + persist_and_check_annotation( model.HistoryDatasetAssociationAnnotationAssociation, hda=d1 ) + + page = model.Page() + page.user = u + self.persist( page ) + persist_and_check_annotation( model.PageAnnotationAssociation, page=page ) + + visualization = model.Visualization() + visualization.user = u + self.persist( visualization ) + persist_and_check_annotation( model.VisualizationAnnotationAssociation, visualization=visualization ) + + def test_ratings( self ): + model = self.model + + u = model.User( email="rater@example.com", password="password" ) + self.persist( u ) + + def persist_and_check_rating( rating_class, **kwds ): + rating_association = rating_class() + rating_association.rating = 5 + rating_association.user = u + for key, value in kwds.iteritems(): + setattr(rating_association, key, value) + self.persist( rating_association ) + self.expunge() + stored_annotation = self.query( rating_class ).all()[0] + assert stored_annotation.rating == 5 + assert stored_annotation.user.email == "rater@example.com" + + sw = model.StoredWorkflow() + sw.user = u + self.persist( sw ) + persist_and_check_rating( model.StoredWorkflowRatingAssociation, stored_workflow=sw ) + + h = model.History( name="History for Rating", user=u) + self.persist( h ) + persist_and_check_rating( model.HistoryRatingAssociation, history=h ) + + d1 = model.HistoryDatasetAssociation( extension="txt", history=h, create_dataset=True, sa_session=model.session ) + self.persist( d1 ) + persist_and_check_rating( model.HistoryDatasetAssociationRatingAssociation, hda=d1 ) + + page = model.Page() + page.user = u + self.persist( page ) + persist_and_check_rating( model.PageRatingAssociation, page=page ) + + visualization = model.Visualization() + visualization.user = u + self.persist( visualization ) + persist_and_check_rating( model.VisualizationRatingAssociation, visualization=visualization ) + + def test_tags( self ): + model = self.model + + my_tag = model.Tag(name="Test Tag") + u = model.User( email="tagger@example.com", password="password" ) + self.persist( my_tag, u ) + + def tag_and_test( taggable_object, tag_association_class, backref_name ): + assert len( getattr(self.query( model.Tag ).filter( model.Tag.name == "Test Tag" ).all()[0], backref_name) ) == 0 + + tag_association = tag_association_class() + tag_association.tag = my_tag + taggable_object.tags = [ tag_association ] + self.persist( tag_association, taggable_object ) + + assert len( getattr(self.query( model.Tag ).filter( model.Tag.name == "Test Tag" ).all()[0], backref_name) ) == 1 + + sw = model.StoredWorkflow() + sw.user = u + #self.persist( sw ) + tag_and_test( sw, model.StoredWorkflowTagAssociation, "tagged_workflows" ) + + h = model.History( name="History for Tagging", user=u) + tag_and_test( h, model.HistoryTagAssociation, "tagged_histories" ) + + d1 = model.HistoryDatasetAssociation( extension="txt", history=h, create_dataset=True, sa_session=model.session ) + tag_and_test( d1, model.HistoryDatasetAssociationTagAssociation, "tagged_history_dataset_associations" ) + + page = model.Page() + page.user = u + tag_and_test( page, model.PageTagAssociation, "tagged_pages" ) + + visualization = model.Visualization() + visualization.user = u + tag_and_test( visualization, model.VisualizationTagAssociation, "tagged_visualizations" ) + def test_basic( self ): - # Start the database and connect the mapping - model = mapping.init( "/tmp", "sqlite:///:memory:", create_tables=True ) - assert model.engine is not None + model = self.model + + original_user_count = len( model.session.query( model.User ).all() ) + # Make some changes and commit them u = model.User( email="james@foo.bar.baz", password="password" ) # gs = model.GalaxySession() @@ -14,40 +143,64 @@ #h1.queries.append( model.Query( "h1->q1" ) ) #h1.queries.append( model.Query( "h1->q2" ) ) h2 = model.History( name=( "H" * 1024 ) ) - model.session.add_all( ( u, h1, h2 ) ) + self.persist( u, h1, h2 ) #q1 = model.Query( "h2->q1" ) metadata = dict( chromCol=1, startCol=2, endCol=3 ) d1 = model.HistoryDatasetAssociation( extension="interval", metadata=metadata, history=h2, create_dataset=True, sa_session=model.session ) #h2.queries.append( q1 ) #h2.queries.append( model.Query( "h2->q2" ) ) - model.session.add( ( d1 ) ) - model.session.flush() - model.session.expunge_all() + self.persist( d1 ) + # Check users = model.session.query( model.User ).all() - assert len( users ) == 1 - assert users[0].email == "james@foo.bar.baz" - assert users[0].password == "password" - assert len( users[0].histories ) == 1 - assert users[0].histories[0].name == "History 1" + assert len( users ) == original_user_count + 1 + user = [user for user in users if user.email == "james@foo.bar.baz"][0] + assert user.email == "james@foo.bar.baz" + assert user.password == "password" + assert len( user.histories ) == 1 + assert user.histories[0].name == "History 1" hists = model.session.query( model.History ).all() - assert hists[0].name == "History 1" - assert hists[1].name == ( "H" * 255 ) - assert hists[0].user == users[0] - assert hists[1].user is None - assert hists[1].datasets[0].metadata.chromCol == 1 + hist0 = [history for history in hists if history.name == "History 1"][0] + hist1 = [history for history in hists if history.name == "H" * 255][0] + assert hist0.name == "History 1" + assert hist1.name == ( "H" * 255 ) + assert hist0.user == user + assert hist1.user is None + assert hist1.datasets[0].metadata.chromCol == 1 # The filename test has moved to objecstore - #id = hists[1].datasets[0].id - #assert hists[1].datasets[0].file_name == os.path.join( "/tmp", *directory_hash_id( id ) ) + ( "/dataset_%d.dat" % id ) + #id = hist1.datasets[0].id + #assert hist1.datasets[0].file_name == os.path.join( "/tmp", *directory_hash_id( id ) ) + ( "/dataset_%d.dat" % id ) # Do an update and check - hists[1].name = "History 2b" - model.session.flush() - model.session.expunge_all() + hist1.name = "History 2b" + self.expunge() hists = model.session.query( model.History ).all() - assert hists[0].name == "History 1" - assert hists[1].name == "History 2b" + hist0 = [history for history in hists if history.name == "History 1"][0] + hist1 = [history for history in hists if history.name == "History 2b"][0] + assert hist0.name == "History 1" + assert hist1.name == "History 2b" # gvk TODO need to ad test for GalaxySessions, but not yet sure what they should look like. + @classmethod + def setUpClass(cls): + # Start the database and connect the mapping + cls.model = mapping.init( "/tmp", "sqlite:///:memory:", create_tables=True ) + assert cls.model.engine is not None + + @classmethod + def query( cls, type ): + return cls.model.session.query( type ) + + @classmethod + def persist(cls, *args): + for arg in args: + cls.model.session.add( arg ) + cls.expunge() + + @classmethod + def expunge(cls): + cls.model.session.flush() + cls.model.session.expunge_all() + def get_suite(): suite = unittest.TestSuite() https://bitbucket.org/galaxy/galaxy-central/commits/ff102054eec1/ Changeset: ff102054eec1 User: martenson Date: 2013-12-11 19:03:02 Summary: merge into default Affected #: 17 files diff -r 206055969b7cedcbfcac36c19428b9547cd5cb5d -r ff102054eec10f7bd1ad9f9663e1884dc7c75223 lib/galaxy/model/__init__.py --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -1827,7 +1827,7 @@ class Library( object, Dictifiable ): permitted_actions = get_permitted_actions( filter='LIBRARY' ) dict_collection_visible_keys = ( 'id', 'name' ) - dict_element_visible_keys = ( 'id', 'deleted', 'name', 'description', 'synopsis' ) + dict_element_visible_keys = ( 'id', 'deleted', 'name', 'description', 'synopsis', 'root_folder_id' ) def __init__( self, name=None, description=None, synopsis=None, root_folder=None ): self.name = name or "Unnamed library" self.description = description @@ -1894,7 +1894,7 @@ return name class LibraryFolder( object, Dictifiable ): - dict_element_visible_keys = ( 'id', 'parent_id', 'name', 'description', 'item_count', 'genome_build' ) + dict_element_visible_keys = ( 'id', 'parent_id', 'name', 'description', 'item_count', 'genome_build', 'update_time' ) def __init__( self, name=None, description=None, item_count=0, order_id=None ): self.name = name or "Unnamed folder" self.description = description @@ -2060,6 +2060,7 @@ genome_build = ldda.dbkey, misc_info = ldda.info, misc_blurb = ldda.blurb, + peek = ( lambda ldda: ldda.display_peek() if ldda.peek and ldda.peek != 'no peek' else None )( ldda ), template_data = template_data ) if ldda.dataset.uuid is None: rval['uuid'] = None diff -r 206055969b7cedcbfcac36c19428b9547cd5cb5d -r ff102054eec10f7bd1ad9f9663e1884dc7c75223 lib/galaxy/model/mapping.py --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -1852,8 +1852,9 @@ table = self.table trans = conn.begin() try: - next_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() - table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid + 1 ) ) + current_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() + next_hid = current_hid + 1 + table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid ) ) trans.commit() return next_hid except: diff -r 206055969b7cedcbfcac36c19428b9547cd5cb5d -r ff102054eec10f7bd1ad9f9663e1884dc7c75223 lib/galaxy/webapps/galaxy/api/datasets.py --- a/lib/galaxy/webapps/galaxy/api/datasets.py +++ b/lib/galaxy/webapps/galaxy/api/datasets.py @@ -1,5 +1,5 @@ """ -API operations on the contents of a dataset. +API operations on the contents of a history dataset. """ from galaxy import web from galaxy.visualization.data_providers.genome import FeatureLocationIndexDataProvider diff -r 206055969b7cedcbfcac36c19428b9547cd5cb5d -r ff102054eec10f7bd1ad9f9663e1884dc7c75223 lib/galaxy/webapps/galaxy/api/folder_contents.py --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -1,5 +1,5 @@ """ -API operations on the contents of a library. +API operations on the contents of a folder. """ import logging, os, string, shutil, urllib, re, socket from cgi import escape, FieldStorage @@ -11,67 +11,122 @@ log = logging.getLogger( __name__ ) class FolderContentsController( BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems ): + """ + Class controls retrieval, creation and updating of folder contents. + """ + + def load_folder_contents( self, trans, folder ): + """ + Loads all contents of the folder (folders and data sets) but only in the first level. + """ + current_user_roles = trans.get_current_user_roles() + is_admin = trans.user_is_admin() + content_items = [] + for subfolder in folder.active_folders: + if not is_admin: + can_access, folder_ids = trans.app.security_agent.check_folder_contents( trans.user, current_user_roles, subfolder ) + if (is_admin or can_access) and not subfolder.deleted: + subfolder.api_type = 'folder' + content_items.append( subfolder ) + for dataset in folder.datasets: + if not is_admin: + can_access = trans.app.security_agent.can_access_dataset( current_user_roles, dataset.library_dataset_dataset_association.dataset ) + if (is_admin or can_access) and not dataset.deleted: + dataset.api_type = 'file' + content_items.append( dataset ) + return content_items @web.expose_api def index( self, trans, folder_id, **kwd ): """ GET /api/folders/{encoded_folder_id}/contents Displays a collection (list) of a folder's contents (files and folders). - The /api/library_contents/{encoded_library_id}/contents - lists everything in a library recursively, which is not what - we want here. We could add a parameter to use the recursive - style, but this is meant to act similar to an "ls" directory listing. + Encoded folder ID is prepended with 'F' if it is a folder as opposed to a data set which does not have it. + Full path is provided as a separate object in response providing data for breadcrumb path building. """ - rval = [] + folder_container = [] current_user_roles = trans.get_current_user_roles() - def traverse( folder ): - admin = trans.user_is_admin() - rval = [] - for subfolder in folder.active_folders: - if not admin: - can_access, folder_ids = trans.app.security_agent.check_folder_contents( trans.user, current_user_roles, subfolder ) - if (admin or can_access) and not subfolder.deleted: - subfolder.api_type = 'folder' - rval.append( subfolder ) - for ld in folder.datasets: - if not admin: - can_access = trans.app.security_agent.can_access_dataset( current_user_roles, ld.library_dataset_dataset_association.dataset ) - if (admin or can_access) and not ld.deleted: - ld.api_type = 'file' - rval.append( ld ) - return rval - - try: - decoded_folder_id = trans.security.decode_id( folder_id[-16:] ) - except TypeError: - trans.response.status = 400 - return "Malformed folder id ( %s ) specified, unable to decode." % str( folder_id ) + if ( folder_id.startswith( 'F' ) ): + try: + decoded_folder_id = trans.security.decode_id( folder_id[1:] ) + except TypeError: + trans.response.status = 400 + return "Malformed folder id ( %s ) specified, unable to decode." % str( folder_id ) try: folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( decoded_folder_id ) - parent_library = folder.parent_library except: folder = None - log.error( "FolderContentsController.index: Unable to retrieve folder %s" - % folder_id ) + log.error( "FolderContentsController.index: Unable to retrieve folder with ID: %s" % folder_id ) - # TODO: Find the API's path to this folder if necessary. - # This was needed in recursive descent, but it's not needed - # for "ls"-style content checking: - if not folder or not ( trans.user_is_admin() or trans.app.security_agent.can_access_library_item( current_user_roles, folder, trans.user ) ): + # We didn't find the folder or user does not have an access to it. + if not folder: trans.response.status = 400 return "Invalid folder id ( %s ) specified." % str( folder_id ) + + if not ( trans.user_is_admin() or trans.app.security_agent.can_access_library_item( current_user_roles, folder, trans.user ) ): + log.warning( "SECURITY: User (id: %s) without proper access rights is trying to load folder with ID of %s" % ( trans.user.id, folder.id ) ) + trans.response.status = 400 + return "Invalid folder id ( %s ) specified." % str( folder_id ) + + path_to_root = [] + def build_path ( folder ): + """ + Search the path upwards recursively and load the whole route of names and ids for breadcrumb purposes. + """ + path_to_root = [] + # We are almost in root + if folder.parent_id is None: + path_to_root.append( ( 'F' + trans.security.encode_id( folder.id ), folder.name ) ) + else: + # We add the current folder and traverse up one folder. + path_to_root.append( ( 'F' + trans.security.encode_id( folder.id ), folder.name ) ) + upper_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( folder.parent_id ) + path_to_root.extend( build_path( upper_folder ) ) + return path_to_root + + # Return the reversed path so it starts with the library node. + full_path = build_path( folder )[::-1] + folder_container.append( dict( full_path = full_path ) ) + + folder_contents = [] + time_updated = '' + time_created = '' + # Go through every item in the folder and include its meta-data. + for content_item in self.load_folder_contents( trans, folder ): +# rval = content_item.to_dict() + return_item = {} + encoded_id = trans.security.encode_id( content_item.id ) + time_updated = content_item.update_time.strftime( "%Y-%m-%d %I:%M %p" ) + time_created = content_item.create_time.strftime( "%Y-%m-%d %I:%M %p" ) + + # For folder return also hierarchy values + if content_item.api_type == 'folder': + encoded_id = 'F' + encoded_id +# time_updated = content_item.update_time.strftime( "%Y-%m-%d %I:%M %p" ) + return_item.update ( dict ( item_count = content_item.item_count ) ) - for content in traverse( folder ): - encoded_id = trans.security.encode_id( content.id ) - if content.api_type == 'folder': - encoded_id = 'F' + encoded_id - rval.append( dict( id = encoded_id, - type = content.api_type, - name = content.name, - url = url_for( 'folder_contents', folder_id=encoded_id ) ) ) - return rval + if content_item.api_type == 'file': + library_dataset_dict = content_item.to_dict() + library_dataset_dict['data_type'] + library_dataset_dict['file_size'] + library_dataset_dict['date_uploaded'] + return_item.update ( dict ( data_type = library_dataset_dict['data_type'], + file_size = library_dataset_dict['file_size'], + date_uploaded = library_dataset_dict['date_uploaded'] ) ) + + # For every item return also the default meta-data + return_item.update( dict( id = encoded_id, + type = content_item.api_type, + name = content_item.name, + time_updated = time_updated, + time_created = time_created + ) ) + folder_contents.append( return_item ) + # Put the data in the container + folder_container.append( dict( folder_contents = folder_contents ) ) + return folder_container @web.expose_api def show( self, trans, id, library_id, **kwd ): diff -r 206055969b7cedcbfcac36c19428b9547cd5cb5d -r ff102054eec10f7bd1ad9f9663e1884dc7c75223 lib/galaxy/webapps/galaxy/api/lda_datasets.py --- /dev/null +++ b/lib/galaxy/webapps/galaxy/api/lda_datasets.py @@ -0,0 +1,241 @@ +""" +API operations on the dataset from library. +""" +import glob +import logging +import operator +import os +import os.path +import string +import sys +import tarfile +import tempfile +import urllib +import urllib2 +import zipfile +from galaxy.security import Action +from galaxy import util, web +from galaxy.util.streamball import StreamBall +from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixinItems + +import logging +log = logging.getLogger( __name__ ) + +# Test for available compression types +# tmpd = tempfile.mkdtemp() +# comptypes = [] +# for comptype in ( 'gz', 'bz2' ): +# tmpf = os.path.join( tmpd, 'compression_test.tar.' + comptype ) +# try: +# archive = tarfile.open( tmpf, 'w:' + comptype ) +# archive.close() +# comptypes.append( comptype ) +# except tarfile.CompressionError: +# log.exception( "Compression error when testing %s compression. This option will be disabled for library downloads." % comptype ) +# try: +# os.unlink( tmpf ) +# except OSError: +# pass +ziptype = '32' +# tmpf = os.path.join( tmpd, 'compression_test.zip' ) +# try: +# archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED, True ) +# archive.close() +# comptypes.append( 'zip' ) +# ziptype = '64' +# except RuntimeError: +# log.exception( "Compression error when testing zip compression. This option will be disabled for library downloads." ) +# except (TypeError, zipfile.LargeZipFile): +# # ZIP64 is only in Python2.5+. Remove TypeError when 2.4 support is dropped +# log.warning( 'Max zip file size is 2GB, ZIP64 not supported' ) +# comptypes.append( 'zip' ) +# try: +# os.unlink( tmpf ) +# except OSError: +# pass +# os.rmdir( tmpd ) + + + +class DatasetsController( BaseAPIController, UsesLibraryMixinItems ): + + @web.expose_api + def show( self, trans, id, **kwd ): + """ + GET /api/libraries/datasets/{encoded_dataset_id} + Displays information about the dataset identified by the lda ID. + """ + # Get dataset. + try: + dataset = self.get_library_dataset( trans, id = id ) + except Exception, e: + return str( e ) + try: + # Default: return dataset as dict. + rval = dataset.to_dict() + except Exception, e: + rval = "Error in dataset API at listing contents: " + str( e ) + log.error( rval + ": %s" % str(e), exc_info=True ) + trans.response.status = 500 + + rval['id'] = trans.security.encode_id(rval['id']); + rval['ldda_id'] = trans.security.encode_id(rval['ldda_id']); + rval['folder_id'] = 'f' + trans.security.encode_id(rval['folder_id']) + + return rval + + @web.expose + def download( self, trans, format, **kwd ): + """ + POST /api/libraries/datasets/download/{format} + POST data: ldda_ids = [] + Downloads dataset(s) in the requested format. + """ + lddas = [] +# is_admin = trans.user_is_admin() +# current_user_roles = trans.get_current_user_roles() + + datasets_to_download = kwd['ldda_ids%5B%5D'] + + if ( datasets_to_download != None ): + datasets_to_download = util.listify( datasets_to_download ) + for dataset_id in datasets_to_download: + try: + ldda = trans.sa_session.query( trans.app.model.LibraryDatasetDatasetAssociation ).get( trans.security.decode_id( dataset_id ) ) + assert not ldda.dataset.purged + lddas.append( ldda ) + except: + ldda = None + message += "Invalid library dataset id (%s) specified. " % str( dataset_id ) + + if format in [ 'zip','tgz','tbz' ]: + error = False + killme = string.punctuation + string.whitespace + trantab = string.maketrans(killme,'_'*len(killme)) + try: + outext = 'zip' + if format == 'zip': + # Can't use mkstemp - the file must not exist first + tmpd = tempfile.mkdtemp() + util.umask_fix_perms( tmpd, trans.app.config.umask, 0777, self.app.config.gid ) + tmpf = os.path.join( tmpd, 'library_download.' + format ) + if ziptype == '64' and trans.app.config.upstream_gzip: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_STORED, True ) + elif ziptype == '64': + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED, True ) + elif trans.app.config.upstream_gzip: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_STORED ) + else: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED ) + archive.add = lambda x, y: archive.write( x, y.encode('CP437') ) + elif format == 'tgz': + if trans.app.config.upstream_gzip: + archive = StreamBall( 'w|' ) + outext = 'tar' + else: + archive = StreamBall( 'w|gz' ) + outext = 'tgz' + elif format == 'tbz': + archive = StreamBall( 'w|bz2' ) + outext = 'tbz2' + except ( OSError, zipfile.BadZipfile ): + error = True + log.exception( "Unable to create archive for download" ) + message = "Unable to create archive for download, please report this error" + status = 'error' + except: + error = True + log.exception( "Unexpected error %s in create archive for download" % sys.exc_info()[0] ) + message = "Unable to create archive for download, please report - %s" % sys.exc_info()[0] + status = 'error' + if not error: + composite_extensions = trans.app.datatypes_registry.get_composite_extensions() + seen = [] + for ldda in lddas: + if ldda.dataset.state in [ 'new', 'upload', 'queued', 'running', 'empty', 'discarded' ]: + continue + ext = ldda.extension + is_composite = ext in composite_extensions + path = "" + parent_folder = ldda.library_dataset.folder + while parent_folder is not None: + # Exclude the now-hidden "root folder" + if parent_folder.parent is None: + path = os.path.join( parent_folder.library_root[0].name, path ) + break + path = os.path.join( parent_folder.name, path ) + parent_folder = parent_folder.parent + path += ldda.name + while path in seen: + path += '_' + seen.append( path ) + zpath = os.path.split(path)[-1] # comes as base_name/fname + outfname,zpathext = os.path.splitext(zpath) + if is_composite: + # need to add all the components from the extra_files_path to the zip + if zpathext == '': + zpath = '%s.html' % zpath # fake the real nature of the html file + try: + archive.add(ldda.dataset.file_name,zpath) # add the primary of a composite set + except IOError: + error = True + log.exception( "Unable to add composite parent %s to temporary library download archive" % ldda.dataset.file_name) + message = "Unable to create archive for download, please report this error" + status = 'error' + continue + flist = glob.glob(os.path.join(ldda.dataset.extra_files_path,'*.*')) # glob returns full paths + for fpath in flist: + efp,fname = os.path.split(fpath) + if fname > '': + fname = fname.translate(trantab) + try: + archive.add( fpath,fname ) + except IOError: + error = True + log.exception( "Unable to add %s to temporary library download archive %s" % (fname,outfname)) + message = "Unable to create archive for download, please report this error" + status = 'error' + continue + else: # simple case + try: + archive.add( ldda.dataset.file_name, path ) + except IOError: + error = True + log.exception( "Unable to write %s to temporary library download archive" % ldda.dataset.file_name) + message = "Unable to create archive for download, please report this error" + status = 'error' + if not error: + lname = 'selected_dataset' + fname = lname.replace( ' ', '_' ) + '_files' + if format == 'zip': + archive.close() + trans.response.set_content_type( "application/octet-stream" ) + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) + archive = util.streamball.ZipBall(tmpf, tmpd) + archive.wsgi_status = trans.response.wsgi_status() + archive.wsgi_headeritems = trans.response.wsgi_headeritems() + return archive.stream + else: + trans.response.set_content_type( "application/x-tar" ) + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) + archive.wsgi_status = trans.response.wsgi_status() + archive.wsgi_headeritems = trans.response.wsgi_headeritems() + return archive.stream + elif format == 'uncompressed': + if len(lddas) != 1: + return 'Wrong request' + else: + single_dataset = lddas[0] + trans.response.set_content_type( single_dataset.get_mime() ) + fStat = os.stat( ldda.file_name ) + trans.response.headers[ 'Content-Length' ] = int( fStat.st_size ) + valid_chars = '.,^_-()[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + fname = ldda.name + fname = ''.join( c in valid_chars and c or '_' for c in fname )[ 0:150 ] + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s"' % fname + try: + return open( single_dataset.file_name ) + except: + return 'This dataset contains no content' + else: + return 'Wrong format'; \ No newline at end of file diff -r 206055969b7cedcbfcac36c19428b9547cd5cb5d -r ff102054eec10f7bd1ad9f9663e1884dc7c75223 lib/galaxy/webapps/galaxy/api/libraries.py --- a/lib/galaxy/webapps/galaxy/api/libraries.py +++ b/lib/galaxy/webapps/galaxy/api/libraries.py @@ -49,9 +49,10 @@ trans.model.Library.table.c.id.in_( accessible_restricted_library_ids ) ) ) rval = [] for library in query: - item = library.to_dict() + item = library.to_dict( view='element' ) item['url'] = url_for( route, id=trans.security.encode_id( library.id ) ) - item['id'] = trans.security.encode_id( item['id'] ) + item['id'] = 'F' + trans.security.encode_id( item['id'] ) + item['root_folder_id'] = 'F' + trans.security.encode_id( item['root_folder_id'] ) rval.append( item ) return rval @@ -131,6 +132,9 @@ rval['name'] = name rval['id'] = encoded_id return rval + + def edit( self, trans, payload, **kwd ): + return "Not implemented yet" @web.expose_api def delete( self, trans, id, **kwd ): diff -r 206055969b7cedcbfcac36c19428b9547cd5cb5d -r ff102054eec10f7bd1ad9f9663e1884dc7c75223 lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -46,12 +46,6 @@ atexit.register( app.shutdown ) # Create the universe WSGI application webapp = GalaxyWebApplication( app, session_cookie='galaxysession', name='galaxy' ) - # Handle displaying tool help images and README file images contained in repositories installed from the tool shed. - webapp.add_route( '/admin_toolshed/static/images/:repository_id/:image_file', - controller='admin_toolshed', - action='display_image_in_repository', - repository_id=None, - image_file=None ) webapp.add_ui_controllers( 'galaxy.webapps.galaxy.controllers', app ) # Force /history to go to /root/history -- needed since the tests assume this webapp.add_route( '/history', controller='root', action='history' ) @@ -75,22 +69,12 @@ webapp.add_route( '/u/:username/v/:slug', controller='visualization', action='display_by_username_and_slug' ) webapp.add_route( '/search', controller='search', action='index' ) - # Add the web API + # ================ + # ===== API ===== + # ================ + webapp.add_api_controllers( 'galaxy.webapps.galaxy.api', app ) - # The /folders section is experimental at this point: - log.debug( "app.config.api_folders: %s" % app.config.api_folders ) - webapp.mapper.resource( 'folder', 'folders', path_prefix='/api' ) - webapp.mapper.resource( 'content', 'contents', - controller='folder_contents', - name_prefix='folder_', - path_prefix='/api/folders/:folder_id', - parent_resources=dict( member_name='folder', collection_name='folders' ) ) - webapp.mapper.resource( 'content', - 'contents', - controller='library_contents', - name_prefix='library_', - path_prefix='/api/libraries/:library_id', - parent_resources=dict( member_name='library', collection_name='libraries' ) ) + webapp.mapper.resource( 'content', 'contents', controller='history_contents', @@ -102,10 +86,6 @@ controller="datasets", action="display", conditions=dict(method=["GET"])) - webapp.mapper.resource( 'permission', - 'permissions', - path_prefix='/api/libraries/:library_id', - parent_resources=dict( member_name='library', collection_name='libraries' ) ) webapp.mapper.resource( 'user', 'users', controller='group_users', @@ -127,11 +107,6 @@ _add_item_tags_controller( webapp, name_prefix="workflow_", path_prefix='/api/workflows/:workflow_id' ) - - _add_item_extended_metadata_controller( webapp, - name_prefix="library_dataset_", - path_prefix='/api/libraries/:library_id/contents/:library_content_id' ) - _add_item_annotation_controller( webapp, name_prefix="history_content_", path_prefix='/api/histories/:history_id/contents/:history_content_id' ) @@ -141,7 +116,6 @@ _add_item_annotation_controller( webapp, name_prefix="workflow_", path_prefix='/api/workflows/:workflow_id' ) - _add_item_provenance_controller( webapp, name_prefix="history_content_", path_prefix='/api/histories/:history_id/contents/:history_content_id' ) @@ -171,6 +145,39 @@ #webapp.mapper.connect( 'run_workflow', '/api/workflow/{workflow_id}/library/{library_id}', controller='workflows', action='run', workflow_id=None, library_id=None, conditions=dict(method=["GET"]) ) webapp.mapper.resource( 'search', 'search', path_prefix='/api' ) + # ======================= + # ===== LIBRARY API ===== + # ======================= + + # The /folders section is experimental at this point: + log.debug( "app.config.api_folders: %s" % app.config.api_folders ) + + webapp.mapper.connect( 'show_lda_item', '/api/libraries/datasets/:id', controller='lda_datasets', action='show', conditions=dict(method=["GET"]) ) + webapp.mapper.connect( 'download_lda_items', '/api/libraries/datasets/download/:format', controller='lda_datasets', action='download', conditions=dict(method=["POST"]) ) + + webapp.mapper.resource_with_deleted( 'library', 'libraries', path_prefix='/api' ) + webapp.mapper.resource( 'folder', 'folders', path_prefix='/api' ) + + webapp.mapper.resource( 'content', 'contents', + controller='folder_contents', + name_prefix='folder_', + path_prefix='/api/folders/:folder_id', + parent_resources=dict( member_name='folder', collection_name='folders' ) ) + webapp.mapper.resource( 'content', + 'contents', + controller='library_contents', + name_prefix='library_', + path_prefix='/api/libraries/:library_id', + parent_resources=dict( member_name='library', collection_name='libraries' ) ) + webapp.mapper.resource( 'permission', + 'permissions', + path_prefix='/api/libraries/:library_id', + parent_resources=dict( member_name='library', collection_name='libraries' ) ) + _add_item_extended_metadata_controller( webapp, + name_prefix="library_dataset_", + path_prefix='/api/libraries/:library_id/contents/:library_content_id' ) + + # add as a non-ATOM API call to support the notion of a 'current/working' history unique to the history resource webapp.mapper.connect( "set_as_current", "/api/histories/{id}/set_as_current", controller="histories", action="set_as_current", conditions=dict( method=["POST"] ) ) @@ -188,6 +195,18 @@ webapp.mapper.connect("workflow_dict", '/api/workflows/{workflow_id}/download', controller='workflows', action='workflow_dict', conditions=dict(method=['GET'])) # Preserve the following download route for now for dependent applications -- deprecate at some point webapp.mapper.connect("workflow_dict", '/api/workflows/download/{workflow_id}', controller='workflows', action='workflow_dict', conditions=dict(method=['GET'])) + + # ==================== + # ===== TOOLSHED ===== + # ==================== + + # Handle displaying tool help images and README file images contained in repositories installed from the tool shed. + webapp.add_route( '/admin_toolshed/static/images/:repository_id/:image_file', + controller='admin_toolshed', + action='display_image_in_repository', + repository_id=None, + image_file=None ) + # Galaxy API for tool shed features. webapp.mapper.resource( 'tool_shed_repository', 'tool_shed_repositories', @@ -201,6 +220,7 @@ path_prefix='/api', new={ 'install_repository_revision' : 'POST' }, parent_resources=dict( member_name='tool_shed_repository', collection_name='tool_shed_repositories' ) ) + # Connect logger from app if app.trace_logger: webapp.trace_logger = app.trace_logger @@ -221,7 +241,7 @@ galaxy.model.mapping.metadata.engine.connection_provider._pool.dispose() except: pass - # Close any pooled database connections before forking + # Close any pooled database connections before forking try: galaxy.model.tool_shed_install.mapping.metadata.engine.connection_provider._pool.dispose() except: diff -r 206055969b7cedcbfcac36c19428b9547cd5cb5d -r ff102054eec10f7bd1ad9f9663e1884dc7c75223 lib/galaxy/webapps/galaxy/controllers/library.py --- a/lib/galaxy/webapps/galaxy/controllers/library.py +++ b/lib/galaxy/webapps/galaxy/controllers/library.py @@ -76,6 +76,17 @@ library_list_grid = LibraryListGrid() + + @web.expose + def list( self, trans, **kwd ): + params = util.Params( kwd ) + # define app configuration for generic mako template + app = { + 'jscript' : "galaxy.library" + } + # fill template + return trans.fill_template('galaxy.panels.mako', config = {'app' : app}) + @web.expose def index( self, trans, **kwd ): params = util.Params( kwd ) diff -r 206055969b7cedcbfcac36c19428b9547cd5cb5d -r ff102054eec10f7bd1ad9f9663e1884dc7c75223 static/scripts/galaxy.library.js --- /dev/null +++ b/static/scripts/galaxy.library.js @@ -0,0 +1,860 @@ +// MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +// === GALAXY LIBRARY MODULE ==== +// MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM + +// global variables +var view = null; +var library_router = null; +var responses = []; + + +// load required libraries +// require([ +// // load js libraries +// 'utils/galaxy.css', +// ], function(css){ +// // load css +// css.load_file("static/style/library.css"); +// }); + +// dependencies +define(["galaxy.modal", "galaxy.masthead", "utils/galaxy.utils"], function(mod_modal, mod_masthead, mod_utils) { + +// MMMMMMMMMMMMMMM +// === Models ==== +// MMMMMMMMMMMMMMM + + // LIBRARY + var Library = Backbone.Model.extend({ + urlRoot: '/api/libraries' + }); + + // LIBRARIES + var Libraries = Backbone.Collection.extend({ + url: '/api/libraries', + model: Library + }); + + // ITEM + var Item = Backbone.Model.extend({ + urlRoot : '/api/libraries/datasets' + }) + + // FOLDER + var Folder = Backbone.Collection.extend({ + model: Item + }) + + // CONTAINER for folder contents (folders, items and metadata). + var FolderContainer = Backbone.Model.extend({ + defaults : { + folder : new Folder(), + full_path : "unknown", + urlRoot : "/api/folders/", + id : "unknown" + }, + parse : function(obj) { + this.full_path = obj[0].full_path; + // update the inner collection + this.get("folder").reset(obj[1].folder_contents); + return obj; + } + }) + + // HISTORY ITEM + var HistoryItem = Backbone.Model.extend({ + urlRoot : '/api/histories/' + }); + + // HISTORY + var GalaxyHistory = Backbone.Model.extend({ + url : '/api/histories/' + }); + + // HISTORIES + var GalaxyHistories = Backbone.Collection.extend({ + url : '/api/histories', + model : GalaxyHistory + }); + + //ROUTER + var LibraryRouter = Backbone.Router.extend({ + routes: { + "" : "libraries", + "folders/:id" : "folder_content", + "folders/:folder_id/download/:format" : "download" + } + }); + + +// MMMMMMMMMMMMMM +// === VIEWS ==== +// MMMMMMMMMMMMMM + +// galaxy folder +var FolderContentView = Backbone.View.extend({ + // main element definition + el : '#center', + // progress percentage + progress: 0, + // progress rate per one item + progressStep: 1, + // last selected history in modal for UX + lastSelectedHistory: '', + // self modal + modal : null, + // loaded folders + folders : null, + + // initialize + initialize : function(){ + this.folders = []; + this.queue = jQuery.Deferred(); + this.queue.resolve(); + }, + +// MMMMMMMMMMMMMMMMMM +// === TEMPLATES ==== +// MMMMMMMMMMMMMMMMMM + + // set up + templateFolder : function (){ + var tmpl_array = []; + + // CONTAINER + tmpl_array.push('<div id="library_container" style="width: 90%; margin: auto; margin-top: 2em; ">'); + tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to <a href="mailto:marten@bx.psu.edu?Subject=DataLibraries_Feedback" target="_blank">Marten</a>.</h3>'); + + // TOOLBAR + tmpl_array.push('<div id="library_folder_toolbar" >'); + tmpl_array.push(' <button title="Create New Folder" id="toolbtn_create_folder" class="btn btn-primary" type="button"><span class="fa fa-icon-plus"></span><span class="fa fa-icon-folder-close"></span> folder</button>'); + tmpl_array.push(' <button id="toolbtn_bulk_import" class="btn btn-primary" style="display: none; margin-left: 0.5em;" type="button"><span class="fa fa-icon-external-link"></span> to history</button>'); + + tmpl_array.push(' <div id="toolbtn_dl" class="btn-group" style="margin-left: 0.5em; display: none; ">'); + tmpl_array.push(' <button id="drop_toggle" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">'); + tmpl_array.push(' <span class="fa fa-icon-download"></span> download <span class="caret"></span>'); + tmpl_array.push(' </button>'); + tmpl_array.push(' <ul class="dropdown-menu" role="menu">'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/tgz">.tar.gz</a></li>'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/tbz">.tar.bz</a></li>'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/zip">.zip</a></li>'); + tmpl_array.push(' </ul>'); + tmpl_array.push(' </div>'); + + tmpl_array.push('</div>'); + + // BREADCRUMBS + tmpl_array.push('<div class="library_breadcrumb">'); + tmpl_array.push('<a title="Return to the list of libraries" href="#">Libraries</a><b>|</b> '); + tmpl_array.push('<% _.each(path, function(path_item) { %>'); + tmpl_array.push('<% if (path_item[0] != id) { %>'); + tmpl_array.push('<a title="Return to this folder" href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a><b>|</b> '); + tmpl_array.push('<% } else { %>'); + tmpl_array.push('<span title="You are in this folder"><%- path_item[1] %></span>'); + tmpl_array.push('<% } %>'); + tmpl_array.push('<% }); %>'); + tmpl_array.push('</div>'); + + // FOLDER CONTENT + tmpl_array.push('<table id="folder_table" class="table table-condensed">'); + tmpl_array.push(' <thead>'); + tmpl_array.push(' <th style="text-align: center; width: 20px; "><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th>'); + tmpl_array.push(' <th class="button_heading">view</th>'); + tmpl_array.push(' <th>name</th>'); + tmpl_array.push(' <th>data type</th>'); + tmpl_array.push(' <th>size</th>'); + tmpl_array.push(' <th>date</th>'); + tmpl_array.push(' </thead>'); + tmpl_array.push(' <tbody>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td><button title="Go to parent folder" type="button" data-id="<%- upper_folder_id %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-arrow-up"></span> .. go up</td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% _.each(items, function(content_item) { %>'); + tmpl_array.push(' <tr class="folder_row light" id="<%- content_item.id %>">'); + tmpl_array.push(' <% if (content_item.get("type") === "folder") { %>'); // folder + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td><button title="Open this folder" type="button" data-id="<%- content_item.id %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-folder-open"></span> browse</td>'); + tmpl_array.push(' <td><%- content_item.get("name") %>'); + tmpl_array.push(' <% if (content_item.get("item_count") === 0) { %>'); // empty folder + tmpl_array.push(' <span class="muted">(empty folder)</span>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' </td>'); + tmpl_array.push(' <td>folder</td>'); // data type + tmpl_array.push(' <td><%= _.escape(content_item.get("item_count")) %> item(s)</td>'); // size + tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <td style="text-align: center; "><input style="margin: 0;" type="checkbox"></td>'); + tmpl_array.push(' <td>'); + tmpl_array.push(' <button title="See details of this dataset" type="button" class="library-dataset btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-eye-open"></span> details'); + tmpl_array.push(' </button>'); + tmpl_array.push(' </td>'); + tmpl_array.push(' <td><%- content_item.get("name") %></td>'); // dataset + tmpl_array.push(' <td><%= _.escape(content_item.get("data_type")) %></td>'); // data type + tmpl_array.push(' <td><%= _.escape(content_item.get("readable_size")) %></td>'); // size + tmpl_array.push(' <% } %> '); + tmpl_array.push(' <td><%= _.escape(content_item.get("time_updated")) %></td>'); // time updated + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push(' '); + tmpl_array.push(' </tbody>'); + tmpl_array.push('</table>'); + + tmpl_array.push('</div>'); + return tmpl_array.join(''); + }, + templateDatasetModal : function(){ + var tmpl_array = []; + + tmpl_array.push('<div id="dataset_info_modal">'); + tmpl_array.push(' <table class="table table-striped table-condensed">'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row" id="id_row" data-id="<%= _.escape(item.get("ldda_id")) %>">Name</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("name")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Data type</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("data_type")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Genome build</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("genome_build")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <th scope="row">Size</th>'); + tmpl_array.push(' <td><%= _.escape(size) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Date uploaded</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("date_uploaded")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Uploaded by</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("uploaded_by")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr scope="row">'); + tmpl_array.push(' <th scope="row">Data Lines</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_data_lines")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <th scope="row">Comment Lines</th>'); + tmpl_array.push(' <% if (item.get("metadata_comment_lines") === "") { %>'); //folder + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_comment_lines")) %></td>'); + tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <td scope="row">unknown</td>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Number of Columns</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_columns")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Column Types</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_column_types")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Miscellaneous information</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("misc_blurb")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' </table>'); + tmpl_array.push(' <pre class="peek">'); + tmpl_array.push(' </pre>'); + tmpl_array.push('</div>'); + + return tmpl_array.join(''); + }, + + templateHistorySelectInModal : function(){ + var tmpl_array = []; + + tmpl_array.push('<span id="history_modal_combo" style="width:90%; margin-left: 1em; margin-right: 1em; ">'); + tmpl_array.push('Select history: '); + tmpl_array.push('<select id="dataset_import_single" name="dataset_import_single" style="width:50%; margin-bottom: 1em; "> '); + tmpl_array.push(' <% _.each(histories, function(history) { %>'); //history select box + tmpl_array.push(' <option value="<%= _.escape(history.get("id")) %>"><%= _.escape(history.get("name")) %></option>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push('</select>'); + tmpl_array.push('</span>'); + + return tmpl_array.join(''); + }, + + templateBulkImportInModal : function(){ + var tmpl_array = []; + + tmpl_array.push('<span id="history_modal_combo_bulk" style="width:90%; margin-left: 1em; margin-right: 1em; ">'); + tmpl_array.push('Select history: '); + tmpl_array.push('<select id="dataset_import_bulk" name="dataset_import_bulk" style="width:50%; margin-bottom: 1em; "> '); + tmpl_array.push(' <% _.each(histories, function(history) { %>'); //history select box + tmpl_array.push(' <option value="<%= _.escape(history.get("id")) %>"><%= _.escape(history.get("name")) %></option>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push('</select>'); + tmpl_array.push('</span>'); + + return tmpl_array.join(''); + }, + + // convert size to nice string + size_to_string : function (size) + { + // identify unit + var unit = ""; + if (size >= 100000000000) { size = size / 100000000000; unit = "TB"; } else + if (size >= 100000000) { size = size / 100000000; unit = "GB"; } else + if (size >= 100000) { size = size / 100000; unit = "MB"; } else + if (size >= 100) { size = size / 100; unit = "KB"; } else + { size = size * 10; unit = "b"; } + // return formatted string + return (Math.round(size) / 10) + unit; + }, + +// MMMMMMMMMMMMMMM +// === EVENTS ==== +// MMMMMMMMMMMMMMM + + // event binding + events: { + 'click #select-all-checkboxes' : 'selectAll', + 'click .folder_row' : 'selectClicked', + 'click #toolbtn_bulk_import' : 'modalBulkImport', + 'click #toolbtn_dl' : 'bulkDownload', + 'click .library-dataset' : 'showDatasetDetails', + 'click #toolbtn_create_folder' : 'createFolderModal', + 'click .btn_open_folder' : 'navigateToFolder' + }, + + //render the folder view + render: function (options) { + //hack to show scrollbars + $("#center").css('overflow','auto'); + + view = this; + var that = this; + + var folderContainer = new FolderContainer({id: options.id}); + folderContainer.url = folderContainer.attributes.urlRoot + options.id + '/contents'; + + folderContainer.fetch({ + success: function (container) { + + // prepare nice size strings + for (var i = 0; i < folderContainer.attributes.folder.models.length; i++) { + var model = folderContainer.attributes.folder.models[i] + if (model.get('type') === 'file'){ + model.set('readable_size', that.size_to_string(model.get('file_size'))) + } + }; + + // find the upper id + var path = folderContainer.full_path; + var upper_folder_id; + if (path.length === 1){ // library is above us + upper_folder_id = 0; + } else { + upper_folder_id = path[path.length-2][0]; + } + + var template = _.template(that.templateFolder(), { path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id, upper_folder_id: upper_folder_id }); + // var template = _.template(that.templateFolder(), { path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id }); + that.$el.html(template); + } + }) + }, + + // handles the click on 'open' and 'upper' folder icons + navigateToFolder : function(event){ + var folder_id = $(event.target).attr('data-id'); + if (typeof folder_id === 'undefined') { + return false; + } else if (folder_id === '0'){ + library_router.navigate('#', {trigger: true, replace: true}); + } else { + library_router.navigate('folders/' + folder_id, {trigger: true, replace: true}); + } + }, + + //show modal with current dataset info + showDatasetDetails : function(event){ + // prevent default + event.preventDefault(); + +//TODO check whether we already have the data + + //load the ID of the row + var id = $(event.target).parent().parent().attr('id'); + + //create new item + var item = new Item(); + var histories = new GalaxyHistories(); + item.id = id; + var self = this; + + //fetch the dataset info + item.fetch({ + success: function (item) { +// TODO can render here already + //fetch user histories for import purposes + histories.fetch({ + success: function (histories){self.renderModalAfterFetch(item, histories)} + }); + } + }); + }, + + // show the current dataset in a modal + renderModalAfterFetch : function(item, histories){ + var size = this.size_to_string(item.get('file_size')); + var template = _.template(this.templateDatasetModal(), { item : item, size : size }); + this.modal = null; + // make modal + var self = this; + this.modal = new mod_modal.GalaxyModal({ + title : 'Dataset Details', + body : template, + buttons : { + 'Import' : function() { self.importCurrentIntoHistory() }, + 'Download' : function() { self.downloadCurrent() }, + 'Close' : function() { self.modal.hide(); $('.modal').remove(); self.modal = null; } // TODO refill nicely modal with data + } + }); + $(".peek").html(item.get("peek")); + var history_footer_tmpl = _.template(this.templateHistorySelectInModal(), {histories : histories.models}); + $(this.modal.elMain).find('.buttons').prepend(history_footer_tmpl); + + // preset last selected history if we know it + if (self.lastSelectedHistory.length > 0) { + $(this.modal.elMain).find('#dataset_import_single').val(self.lastSelectedHistory); + } + + // show the prepared modal + this.modal.show(); + }, + + // download dataset shown currently in modal + downloadCurrent : function(){ + //disable the buttons + this.modal.disableButton('Import'); + this.modal.disableButton('Download'); + + var library_dataset_id = []; + library_dataset_id.push($('#id_row').attr('data-id')); + var url = '/api/libraries/datasets/download/uncompressed'; + var data = {'ldda_ids' : library_dataset_id}; + + // we assume the view is existent + folderContentView.processDownload(url, data); + this.modal.enableButton('Import'); + this.modal.enableButton('Download'); + }, + + // import dataset shown currently in modal into selected history + importCurrentIntoHistory : function(){ + //disable the buttons + this.modal.disableButton('Import'); + this.modal.disableButton('Download'); + + var history_id = $(this.modal.elMain).find('select[name=dataset_import_single] option:selected').val(); + this.lastSelectedHistory = history_id; //save selected history for further use + + var library_dataset_id = $('#id_row').attr('data-id'); + var historyItem = new HistoryItem(); + var self = this; + historyItem.url = historyItem.urlRoot + history_id + '/contents'; + + // save the dataset into selected history + historyItem.save({ content : library_dataset_id, source : 'library' }, { success : function(){ + self.modal.showNotification('Dataset imported', 3000, 'success'); + //enable the buttons + self.modal.enableButton('Import'); + self.modal.enableButton('Download'); + }, error : function(){ + self.modal.showNotification('An error occured! Dataset not imported. Please try again later.', 5000, 'error'); + //enable the buttons + self.modal.enableButton('Import'); + self.modal.enableButton('Download'); + } + }); + }, + + // select all datasets + selectAll : function (event) { + var selected = event.target.checked; + // Iterate each checkbox + $(':checkbox').each(function () { this.checked = selected; }); + this.showTools(); + }, + + // click checkbox on folder click + selectClicked : function (event) { + var checkbox = $("#" + event.target.parentElement.id).find(':checkbox') + if (checkbox[0] != undefined) { + if (checkbox[0].checked){ + checkbox[0].checked = ''; + // $(event.target.parentElement).css('background-color', '').css('color', ''); + $(event.target.parentElement).removeClass('dark'); + $(event.target.parentElement).find('a').removeClass('dark'); + $(event.target.parentElement).addClass('light'); + $(event.target.parentElement).find('a').addClass('light'); + } else { + checkbox[0].checked = 'selected'; + $(event.target.parentElement).removeClass('light'); + $(event.target.parentElement).find('a').removeClass('light'); + $(event.target.parentElement).addClass('dark'); + $(event.target.parentElement).find('a').addClass('dark'); + // $(event.target.parentElement).css('background-color', '#8389a1').css('color', 'white'); + } + } + this.showTools(); + }, + + // show toolbar in case something is selected + showTools : function(){ + var checkedValues = $('#folder_table').find(':checked'); + if(checkedValues.length > 0){ + $('#toolbtn_bulk_import').show(); + $('#toolbtn_dl').show(); + } else { + $('#toolbtn_bulk_import').hide(); + $('#toolbtn_dl').hide(); + } + + }, + + // show bulk import modal + modalBulkImport : function(){ + var self = this; + // fetch histories + var histories = new GalaxyHistories(); + histories.fetch({ + success: function (histories){ + // make modal + var history_modal_tmpl = _.template(self.templateBulkImportInModal(), {histories : histories.models}); + self.modal = new mod_modal.GalaxyModal({ + title : 'Import into History', + body : history_modal_tmpl, + buttons : { + 'Import' : function() {self.importAllIntoHistory()}, + 'Close' : function() {self.modal.hide(); $('.modal').remove(); self.modal = null;} + } + }); + // show the prepared modal + self.modal.show(); + } + }); + }, + + // import all selected datasets into history + importAllIntoHistory : function (){ + //disable the button + this.modal.disableButton('Import'); + + var history_id = $("select[name=dataset_import_bulk] option:selected").val(); + var history_name = $("select[name=dataset_import_bulk] option:selected").text(); + + var dataset_ids = []; + $('#folder_table').find(':checked').each(function(){ + if (this.parentElement.parentElement.id != '') { + dataset_ids.push(this.parentElement.parentElement.id); + } + }); + var progress_bar_tmpl = _.template(this.templateProgressBar(), { history_name : history_name }); + $(this.modal.elMain).find('.modal-body').html(progress_bar_tmpl); + + // init the progress bar + var progressStep = 100 / dataset_ids.length; + this.initProgress(progressStep); + + // prepare the dataset objects to be imported + var datasets_to_import = []; + for (var i = dataset_ids.length - 1; i >= 0; i--) { + library_dataset_id = dataset_ids[i]; + var historyItem = new HistoryItem(); + var self = this; + historyItem.url = historyItem.urlRoot + history_id + '/contents'; + historyItem.content = library_dataset_id; + historyItem.source = 'library'; + datasets_to_import.push(historyItem); + }; + + // call the recursive function to call ajax one after each other + this.chainCall(datasets_to_import); + }, + + chainCall: function(history_item_set){ + var self = this; + var popped_item = history_item_set.pop(); + if (typeof popped_item === "undefined") { + self.modal.showNotification('All datasets imported', 3000, 'success'); + // enable button again + self.modal.enableButton('Import'); + return + } + var promise = $.when(popped_item.save({content: popped_item.content, source: popped_item.source})).done(function(a1){ + self.updateProgress(); + responses.push(a1); + self.chainCall(history_item_set); + }); + }, + + initProgress: function(progressStep){ + this.progress = 0; + this.progressStep = progressStep; + }, + updateProgress: function(){ + this.progress += this.progressStep; + $('.progress-bar').width(Math.round(this.progress) + '%'); + txt_representation = Math.round(this.progress) + '% Complete'; + $('.completion_span').text(txt_representation); + }, + + // progress bar + templateProgressBar : function (){ + var tmpl_array = []; + + tmpl_array.push('<div class="import_text">'); + tmpl_array.push('Importing selected datasets to history <b><%= _.escape(history_name) %></b>'); + tmpl_array.push('</div>'); + tmpl_array.push('<div class="progress">'); + tmpl_array.push(' <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 00%;">'); + tmpl_array.push(' <span class="completion_span">0% Complete</span>'); + tmpl_array.push(' </div>'); + tmpl_array.push('</div>'); + tmpl_array.push(''); + + return tmpl_array.join(''); + }, + + // download selected datasets + download : function(folder_id, format){ + var dataset_ids = []; + $('#folder_table').find(':checked').each(function(){ + if (this.parentElement.parentElement.id != '') { + dataset_ids.push(this.parentElement.parentElement.id); + } + }); + + var url = '/api/libraries/datasets/download/' + format; + var data = {'ldda_ids' : dataset_ids}; + this.processDownload(url, data); + }, + + // create hidden form and submit through POST to initialize download + processDownload: function(url, data, method){ + //url and data options required + if( url && data ){ + //data can be string of parameters or array/object + data = typeof data == 'string' ? data : $.param(data); + //split params into form inputs + var inputs = ''; + $.each(data.split('&'), function(){ + var pair = this.split('='); + inputs+='<input type="hidden" name="'+ pair[0] +'" value="'+ pair[1] +'" />'; + }); + //send request + $('<form action="'+ url +'" method="'+ (method||'post') +'">'+inputs+'</form>') + .appendTo('body').submit().remove(); + }; + }, + + // shows modal for creating folder + createFolderModal: function(){ + alert('creating folder'); + } + + }); + +// galaxy library view +var GalaxyLibraryview = Backbone.View.extend({ + el: '#center', + + events: { + 'click #create_new_library_btn' : 'show_library_modal' + }, + + // initialize + initialize : function(){ + }, + + // template + template_library_list : function (){ + tmpl_array = []; + tmpl_array.push('<div id="library_container" style="width: 90%; margin: auto; margin-top: 2em; overflow: auto !important; ">'); + + tmpl_array.push(''); + tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to <a href="mailto:marten@bx.psu.edu?Subject=DataLibraries_Feedback" target="_blank">Marten</a>.</h3>'); + tmpl_array.push('<a href="" id="create_new_library_btn" class="btn btn-primary icon-file ">New Library</a>'); + tmpl_array.push('<table class="table table-condensed">'); + tmpl_array.push(' <thead>'); + tmpl_array.push(' <th class="button_heading"></th>'); + tmpl_array.push(' <th>name</th>'); + tmpl_array.push(' <th>description</th>'); + tmpl_array.push(' <th>synopsis</th> '); + tmpl_array.push(' <th>model type</th> '); + tmpl_array.push(' </thead>'); + tmpl_array.push(' <tbody>'); + tmpl_array.push(' <% _.each(libraries, function(library) { %>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <td><button title="Open this library" type="button" data-id="<%- library.get("root_folder_id") %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-icon-folder-open"></span> browse</td>'); + tmpl_array.push(' <td><%- library.get("name") %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("description")) %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("synopsis")) %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("model_class")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push(' </tbody>'); + tmpl_array.push('</table>'); + + tmpl_array.push('</div>'); + return tmpl_array.join(''); + }, + + // render + render: function () { + //hack to show scrollbars + $("#center").css('overflow','auto'); + var that = this; + // if (typeof libraries === "undefined") { + libraries = new Libraries(); + // } + libraries.fetch({ + success: function (libraries) { + var template = _.template(that.template_library_list(), { libraries : libraries.models }); + that.$el.html(template); + } + }) + }, + + // own modal + modal : null, + + // show/hide create library modal + show_library_modal : function (event){ + event.preventDefault(); + event.stopPropagation(); + + // create modal + var self = this; + this.modal = new mod_modal.GalaxyModal( + { + title : 'Create New Library', + body : this.template_new_library(), + buttons : { + 'Create' : function() {self.create_new_library_event()}, + 'Close' : function() {self.modal.hide()} + } + }); + + // show prepared modal + this.modal.show(); + }, + + // create the new library from modal + create_new_library_event: function(){ + var libraryDetails = this.serialize_new_library(); + var valid = this.validate_new_library(libraryDetails); + var library = new Library(); + var self = this; + library.save(libraryDetails, { + success: function (library) { + self.modal.hide(); + self.clear_library_modal(); + self.render(); + }, + error: function(){ + self.modal.showNotification('An error occured', 5000, 'error'); + } + }); + return false; + }, + + // clear the library modal once saved + clear_library_modal : function(){ + $("input[name='Name']").val(''); + $("input[name='Description']").val(''); + $("input[name='Synopsis']").val(''); + }, + + // serialize data from the form + serialize_new_library : function(){ + return { + name: $("input[name='Name']").val(), + description: $("input[name='Description']").val(), + synopsis: $("input[name='Synopsis']").val() + }; + }, + + validate_new_library: function(library){ + + }, + + + // template for new library modal + template_new_library: function() + { + tmpl_array = []; + + tmpl_array.push('<div id="new_library_modal">'); + tmpl_array.push('<form>'); + tmpl_array.push('<input type="text" name="Name" value="" placeholder="Name">'); + tmpl_array.push('<input type="text" name="Description" value="" placeholder="Description">'); + tmpl_array.push('<input type="text" name="Synopsis" value="" placeholder="Synopsis">'); + tmpl_array.push('</form>'); + tmpl_array.push('</div>'); + + return tmpl_array.join(''); + } +}); + +// galaxy library wrapper View +var GalaxyLibrary = Backbone.View.extend({ + folderContentView : null, + galaxyLibraryview : null, + initialize : function(){ + + folderContentView = new FolderContentView(); + galaxyLibraryview = new GalaxyLibraryview(); + + library_router = new LibraryRouter(); + + library_router.on('route:libraries', function() { + // render libraries list + galaxyLibraryview.render(); + }); + + library_router.on('route:folder_content', function(id) { + // render folder's contents + folderContentView.render({id: id}); + }); + + library_router.on('route:download', function(folder_id, format) { + // send download stream + if (typeof folderContentView === 'undefined'){ + alert('you cant touch this!'); + // } else if (folderContentView.modal !== null){ + // folderContentView.download(folder_id, format); + } else if ($('#center').find(':checked').length === 0) { // coming from outside of the library app + library_router.navigate('folders/' + folder_id, {trigger: true, replace: true}); + // TODO show message of redirection + } else { + folderContentView.download(folder_id, format); + library_router.navigate('folders/' + folder_id, {trigger: false, replace: true}); + } + + }); + +Backbone.history.start(); + +return this +} +}); + +// return +return { + GalaxyApp: GalaxyLibrary +}; + +}); diff -r 206055969b7cedcbfcac36c19428b9547cd5cb5d -r ff102054eec10f7bd1ad9f9663e1884dc7c75223 static/scripts/galaxy.menu.js --- a/static/scripts/galaxy.menu.js +++ b/static/scripts/galaxy.menu.js @@ -57,7 +57,12 @@ tab_shared.add({ title : "Data Libraries", - content : "library/index", + content : "library/index" + }); + + tab_shared.add({ + title : "New Libraries", + content : "library/list", divider : true }); diff -r 206055969b7cedcbfcac36c19428b9547cd5cb5d -r ff102054eec10f7bd1ad9f9663e1884dc7c75223 static/scripts/galaxy.modal.js --- a/static/scripts/galaxy.modal.js +++ b/static/scripts/galaxy.modal.js @@ -1,6 +1,3 @@ -/* - galaxy modal -*/ // dependencies define([], function() { @@ -25,8 +22,51 @@ // initialize initialize : function(options) { + self = this; if (options) this.create(options); + + this.bindClick(event, self); + }, + + // bind the click-to-hide function + bindClick: function(event, that) { + // bind the ESC key to hide() function + $(document).on('keyup', function(event){ + if (event.keyCode == 27) { self.hide(); } + }) + // bind the click anywhere to hide() function... + $('html').on('click', function(event){ + that.hide(); + }) + // ...but don't hide if the click is on modal content + $('.modal-content').on('click', function(event){ + event.stopPropagation(); + }) + }, + + // unbind the click-to-hide function + unbindClick: function(event, that){ + // bind the ESC key to hide() function + $(document).off('keyup', function(event){ + if (event.keyCode == 27) { that.hide(); } + }) + // unbind the click anywhere to hide() function... + $('html').off('click', function(event){ + that.hide(); + }) + // ...but don't hide if the click is on modal content + $('.modal-content').off('click', function(event){ + event.stopPropagation(); + }) + }, + + + // destroy + destroy : function(){ + this.hide(); + this.unbindClick(); + $('.modal').remove(); }, // adds and displays a new frame/window @@ -87,6 +127,7 @@ this.$footer = (this.$el).find('.modal-footer'); this.$buttons = (this.$el).find('.buttons'); this.$backdrop = (this.$el).find('.modal-backdrop'); + this.$notification = (this.$el).find('.notification-modal'); // append body this.$body.html(this.options.body); @@ -120,6 +161,47 @@ this.$buttons.find('#' + String(name).toLowerCase()).prop('disabled', true); }, + // hide buttons + hideButton: function(name) { + this.$buttons.find('#' + String(name).toLowerCase()).hide(); + }, + // show buttons + showButton: function(name) { + this.$buttons.find('#' + String(name).toLowerCase()).show(); + }, + + // show notification + showNotification : function(message, duration, type) { + // defaults + var duration = typeof duration !== 'undefined' ? duration : 1500; + // var bgColor = typeof bgColor !== 'undefined' ? bgColor : "#F4E0E1"; + // var txtColor = typeof txtColor !== 'undefined' ? txtColor : "#A42732"; + var bgColor; + var txtColor; + + if (type === 'error'){ + bgColor = '#f4e0e1'; + txtColor = '#a42732'; + // } else if (type === 'success'){ + } else { // success is default + bgColor = '#e1f4e0'; + txtColor = '#32a427'; + } + + var HTMLmessage = "<div class='notification-message' style='text-align:center; line-height:16px; '> " + message + " </div>"; + this.$notification.html("<div id='notification-bar' style='display:none; float: right; height: 16px; width:100%; background-color: " + bgColor + "; z-index: 100; color: " + txtColor + ";border-bottom: 1px solid " + txtColor + ";'>" + HTMLmessage + "</div>"); + + var self = this; + + /*animate the bar*/ + $('#notification-bar').slideDown(function() { + setTimeout(function() { + $('#notification-bar').slideUp(function() {self.$notification.html('');}); + }, duration); + }); + + }, + // returns scroll top for body element scrollTop: function() { @@ -139,7 +221,8 @@ '<div class="modal-header">' + '<button type="button" class="close" style="display: none;">×</button>' + '<h4 class="title">' + title + '</h4>' + - '</div>' + + '<span class="notification-modal"></span>' + + '</div>' + '<div class="modal-body"></div>' + '<div class="modal-footer">' + '<div class="buttons" style="float: right;"></div>' + diff -r 206055969b7cedcbfcac36c19428b9547cd5cb5d -r ff102054eec10f7bd1ad9f9663e1884dc7c75223 static/scripts/libs/bootstrap.js --- a/static/scripts/libs/bootstrap.js +++ b/static/scripts/libs/bootstrap.js @@ -575,3 +575,158 @@ } }(window.jQuery); + +/* ======================================================================== + * Bootstrap: dropdown.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#dropdowns + * ======================================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle=dropdown]' + var Dropdown = function (element) { + var $el = $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we we use a backdrop because click events don't delegate + $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus) + } + + $parent.trigger(e = $.Event('show.bs.dropdown')) + + if (e.isDefaultPrevented()) return + + $parent + .toggleClass('open') + .trigger('shown.bs.dropdown') + + $this.focus() + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27)/.test(e.keyCode)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) { + if (e.which == 27) $parent.find(toggle).focus() + return $this.click() + } + + var $items = $('[role=menu] li:not(.divider):visible a', $parent) + + if (!$items.length) return + + var index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index=0 + + $items.eq(index).focus() + } + + function clearMenus() { + $(backdrop).remove() + $(toggle).each(function (e) { + var $parent = getParent($(this)) + if (!$parent.hasClass('open')) return + $parent.trigger(e = $.Event('hide.bs.dropdown')) + if (e.isDefaultPrevented()) return + $parent.removeClass('open').trigger('hidden.bs.dropdown') + }) + } + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + var old = $.fn.dropdown + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('dropdown') + + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== + + $(document) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api' , toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + +}(window.jQuery); \ No newline at end of file diff -r 206055969b7cedcbfcac36c19428b9547cd5cb5d -r ff102054eec10f7bd1ad9f9663e1884dc7c75223 static/style/src/less/library.less --- a/static/style/src/less/library.less +++ b/static/style/src/less/library.less @@ -29,6 +29,50 @@ background-color: @table-bg-accent; } + +tr.light td +{ + background-color: white; + color: black; +} +tr.light:hover td +{ + background-color: #f5f5f5; + color: #8389a1; +} +tr.dark td +{ + background-color: #8389a1; + color: white; +} +tr.dark:hover td +{ + background-color: #bbbfd0; + color: white; +} +a.dark:hover +{ + color: yellow; + // text-decoration: none; +} +a.dark +{ + color: white; + // text-decoration: none; +} +th.button_heading +{ + width: 7em; +} +div.library_breadcrumb{ + padding-top: 0.8em; + padding-bottom: 0.8em; +} +div.library_breadcrumb a:hover{ + color:green; +} + + img.expanderIcon { padding-right: 4px; } https://bitbucket.org/galaxy/galaxy-central/commits/caba1617084b/ Changeset: caba1617084b User: martenson Date: 2013-12-11 20:24:58 Summary: cleaning zip logic, jmchilton pull rq #208 Affected #: 1 file diff -r ff102054eec10f7bd1ad9f9663e1884dc7c75223 -r caba1617084b4a0d38f1ebce970dda675755cd1d lib/galaxy/webapps/galaxy/api/lda_datasets.py --- a/lib/galaxy/webapps/galaxy/api/lda_datasets.py +++ b/lib/galaxy/webapps/galaxy/api/lda_datasets.py @@ -21,42 +21,6 @@ import logging log = logging.getLogger( __name__ ) -# Test for available compression types -# tmpd = tempfile.mkdtemp() -# comptypes = [] -# for comptype in ( 'gz', 'bz2' ): -# tmpf = os.path.join( tmpd, 'compression_test.tar.' + comptype ) -# try: -# archive = tarfile.open( tmpf, 'w:' + comptype ) -# archive.close() -# comptypes.append( comptype ) -# except tarfile.CompressionError: -# log.exception( "Compression error when testing %s compression. This option will be disabled for library downloads." % comptype ) -# try: -# os.unlink( tmpf ) -# except OSError: -# pass -ziptype = '32' -# tmpf = os.path.join( tmpd, 'compression_test.zip' ) -# try: -# archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED, True ) -# archive.close() -# comptypes.append( 'zip' ) -# ziptype = '64' -# except RuntimeError: -# log.exception( "Compression error when testing zip compression. This option will be disabled for library downloads." ) -# except (TypeError, zipfile.LargeZipFile): -# # ZIP64 is only in Python2.5+. Remove TypeError when 2.4 support is dropped -# log.warning( 'Max zip file size is 2GB, ZIP64 not supported' ) -# comptypes.append( 'zip' ) -# try: -# os.unlink( tmpf ) -# except OSError: -# pass -# os.rmdir( tmpd ) - - - class DatasetsController( BaseAPIController, UsesLibraryMixinItems ): @web.expose_api @@ -77,13 +41,13 @@ rval = "Error in dataset API at listing contents: " + str( e ) log.error( rval + ": %s" % str(e), exc_info=True ) trans.response.status = 500 - + rval['id'] = trans.security.encode_id(rval['id']); rval['ldda_id'] = trans.security.encode_id(rval['ldda_id']); rval['folder_id'] = 'f' + trans.security.encode_id(rval['folder_id']) - + return rval - + @web.expose def download( self, trans, format, **kwd ): """ @@ -94,9 +58,9 @@ lddas = [] # is_admin = trans.user_is_admin() # current_user_roles = trans.get_current_user_roles() - + datasets_to_download = kwd['ldda_ids%5B%5D'] - + if ( datasets_to_download != None ): datasets_to_download = util.listify( datasets_to_download ) for dataset_id in datasets_to_download: @@ -107,7 +71,7 @@ except: ldda = None message += "Invalid library dataset id (%s) specified. " % str( dataset_id ) - + if format in [ 'zip','tgz','tbz' ]: error = False killme = string.punctuation + string.whitespace @@ -119,14 +83,10 @@ tmpd = tempfile.mkdtemp() util.umask_fix_perms( tmpd, trans.app.config.umask, 0777, self.app.config.gid ) tmpf = os.path.join( tmpd, 'library_download.' + format ) - if ziptype == '64' and trans.app.config.upstream_gzip: + if trans.app.config.upstream_gzip: archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_STORED, True ) - elif ziptype == '64': + else: archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED, True ) - elif trans.app.config.upstream_gzip: - archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_STORED ) - else: - archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED ) archive.add = lambda x, y: archive.write( x, y.encode('CP437') ) elif format == 'tgz': if trans.app.config.upstream_gzip: @@ -238,4 +198,4 @@ except: return 'This dataset contains no content' else: - return 'Wrong format'; \ No newline at end of file + return 'Wrong format'; https://bitbucket.org/galaxy/galaxy-central/commits/c653272fdbb3/ Changeset: c653272fdbb3 User: martenson Date: 2013-12-11 21:31:20 Summary: trimming whitespace, import library.less into base.less Affected #: 2 files diff -r caba1617084b4a0d38f1ebce970dda675755cd1d -r c653272fdbb3e2fcc8e5dd83b2c71df8bd9b5c2a static/scripts/galaxy.library.js --- a/static/scripts/galaxy.library.js +++ b/static/scripts/galaxy.library.js @@ -7,16 +7,6 @@ var library_router = null; var responses = []; - -// load required libraries -// require([ -// // load js libraries -// 'utils/galaxy.css', -// ], function(css){ -// // load css -// css.load_file("static/style/library.css"); -// }); - // dependencies define(["galaxy.modal", "galaxy.masthead", "utils/galaxy.utils"], function(mod_modal, mod_masthead, mod_utils) { @@ -42,7 +32,7 @@ // FOLDER var Folder = Backbone.Collection.extend({ - model: Item + model: Item }) // CONTAINER for folder contents (folders, items and metadata). @@ -78,7 +68,7 @@ }); //ROUTER - var LibraryRouter = Backbone.Router.extend({ + var LibraryRouter = Backbone.Router.extend({ routes: { "" : "libraries", "folders/:id" : "folder_content", @@ -98,26 +88,26 @@ // progress percentage progress: 0, // progress rate per one item - progressStep: 1, + progressStep: 1, // last selected history in modal for UX lastSelectedHistory: '', // self modal modal : null, // loaded folders folders : null, - + // initialize initialize : function(){ this.folders = []; this.queue = jQuery.Deferred(); this.queue.resolve(); - }, + }, // MMMMMMMMMMMMMMMMMM // === TEMPLATES ==== // MMMMMMMMMMMMMMMMMM - // set up + // set up templateFolder : function (){ var tmpl_array = []; @@ -127,12 +117,12 @@ // TOOLBAR tmpl_array.push('<div id="library_folder_toolbar" >'); - tmpl_array.push(' <button title="Create New Folder" id="toolbtn_create_folder" class="btn btn-primary" type="button"><span class="fa fa-icon-plus"></span><span class="fa fa-icon-folder-close"></span> folder</button>'); - tmpl_array.push(' <button id="toolbtn_bulk_import" class="btn btn-primary" style="display: none; margin-left: 0.5em;" type="button"><span class="fa fa-icon-external-link"></span> to history</button>'); - + tmpl_array.push(' <button title="Create New Folder" id="toolbtn_create_folder" class="btn btn-primary" type="button"><span class="fa fa-plus"></span><span class="fa fa-folder-close"></span> folder</button>'); + tmpl_array.push(' <button id="toolbtn_bulk_import" class="btn btn-primary" style="display: none; margin-left: 0.5em;" type="button"><span class="fa fa-external-link"></span> to history</button>'); + tmpl_array.push(' <div id="toolbtn_dl" class="btn-group" style="margin-left: 0.5em; display: none; ">'); tmpl_array.push(' <button id="drop_toggle" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">'); - tmpl_array.push(' <span class="fa fa-icon-download"></span> download <span class="caret"></span>'); + tmpl_array.push(' <span class="fa fa-download"></span> download <span class="caret"></span>'); tmpl_array.push(' </button>'); tmpl_array.push(' <ul class="dropdown-menu" role="menu">'); tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/tgz">.tar.gz</a></li>'); @@ -166,20 +156,20 @@ tmpl_array.push(' <th>date</th>'); tmpl_array.push(' </thead>'); tmpl_array.push(' <tbody>'); - tmpl_array.push(' <td></td>'); - tmpl_array.push(' <td><button title="Go to parent folder" type="button" data-id="<%- upper_folder_id %>" class="btn_open_folder btn btn-default btn-xs">'); - tmpl_array.push(' <span class="fa fa-icon-arrow-up"></span> .. go up</td>'); - tmpl_array.push(' <td></td>'); - tmpl_array.push(' <td></td>'); - tmpl_array.push(' <td></td>'); - tmpl_array.push(' <td></td>'); - tmpl_array.push(' </tr>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td><button title="Go to parent folder" type="button" data-id="<%- upper_folder_id %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-arrow-up"></span> .. go up</td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' </tr>'); tmpl_array.push(' <% _.each(items, function(content_item) { %>'); tmpl_array.push(' <tr class="folder_row light" id="<%- content_item.id %>">'); tmpl_array.push(' <% if (content_item.get("type") === "folder") { %>'); // folder tmpl_array.push(' <td></td>'); tmpl_array.push(' <td><button title="Open this folder" type="button" data-id="<%- content_item.id %>" class="btn_open_folder btn btn-default btn-xs">'); - tmpl_array.push(' <span class="fa fa-icon-folder-open"></span> browse</td>'); + tmpl_array.push(' <span class="fa fa-folder-open"></span> browse</td>'); tmpl_array.push(' <td><%- content_item.get("name") %>'); tmpl_array.push(' <% if (content_item.get("item_count") === 0) { %>'); // empty folder tmpl_array.push(' <span class="muted">(empty folder)</span>'); @@ -191,7 +181,7 @@ tmpl_array.push(' <td style="text-align: center; "><input style="margin: 0;" type="checkbox"></td>'); tmpl_array.push(' <td>'); tmpl_array.push(' <button title="See details of this dataset" type="button" class="library-dataset btn btn-default btn-xs">'); - tmpl_array.push(' <span class="fa fa-icon-eye-open"></span> details'); + tmpl_array.push(' <span class="fa fa-eye"></span> details'); tmpl_array.push(' </button>'); tmpl_array.push(' </td>'); tmpl_array.push(' <td><%- content_item.get("name") %></td>'); // dataset @@ -241,11 +231,11 @@ tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_data_lines")) %></td>'); tmpl_array.push(' </tr>'); tmpl_array.push(' <th scope="row">Comment Lines</th>'); - tmpl_array.push(' <% if (item.get("metadata_comment_lines") === "") { %>'); //folder + tmpl_array.push(' <% if (item.get("metadata_comment_lines") === "") { %>'); //folder tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_comment_lines")) %></td>'); - tmpl_array.push(' <% } else { %>'); - tmpl_array.push(' <td scope="row">unknown</td>'); - tmpl_array.push(' <% } %>'); + tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <td scope="row">unknown</td>'); + tmpl_array.push(' <% } %>'); tmpl_array.push(' </tr>'); tmpl_array.push(' <tr>'); tmpl_array.push(' <th scope="row">Number of Columns</th>'); @@ -280,7 +270,7 @@ tmpl_array.push('</span>'); return tmpl_array.join(''); - }, + }, templateBulkImportInModal : function(){ var tmpl_array = []; @@ -295,7 +285,7 @@ tmpl_array.push('</span>'); return tmpl_array.join(''); - }, + }, // convert size to nice string size_to_string : function (size) @@ -380,12 +370,12 @@ showDatasetDetails : function(event){ // prevent default event.preventDefault(); - + //TODO check whether we already have the data //load the ID of the row var id = $(event.target).parent().parent().attr('id'); - + //create new item var item = new Item(); var histories = new GalaxyHistories(); @@ -474,12 +464,12 @@ self.modal.showNotification('An error occured! Dataset not imported. Please try again later.', 5000, 'error'); //enable the buttons self.modal.enableButton('Import'); - self.modal.enableButton('Download'); + self.modal.enableButton('Download'); } }); }, - // select all datasets + // select all datasets selectAll : function (event) { var selected = event.target.checked; // Iterate each checkbox @@ -562,7 +552,7 @@ }); var progress_bar_tmpl = _.template(this.templateProgressBar(), { history_name : history_name }); $(this.modal.elMain).find('.modal-body').html(progress_bar_tmpl); - + // init the progress bar var progressStep = 100 / dataset_ids.length; this.initProgress(progressStep); @@ -610,7 +600,7 @@ $('.completion_span').text(txt_representation); }, - // progress bar + // progress bar templateProgressBar : function (){ var tmpl_array = []; @@ -644,14 +634,14 @@ // create hidden form and submit through POST to initialize download processDownload: function(url, data, method){ //url and data options required - if( url && data ){ + if( url && data ){ //data can be string of parameters or array/object data = typeof data == 'string' ? data : $.param(data); //split params into form inputs var inputs = ''; - $.each(data.split('&'), function(){ + $.each(data.split('&'), function(){ var pair = this.split('='); - inputs+='<input type="hidden" name="'+ pair[0] +'" value="'+ pair[1] +'" />'; + inputs+='<input type="hidden" name="'+ pair[0] +'" value="'+ pair[1] +'" />'; }); //send request $('<form action="'+ url +'" method="'+ (method||'post') +'">'+inputs+'</form>') @@ -676,7 +666,7 @@ // initialize initialize : function(){ - }, + }, // template template_library_list : function (){ @@ -685,7 +675,7 @@ tmpl_array.push(''); tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to <a href="mailto:marten@bx.psu.edu?Subject=DataLibraries_Feedback" target="_blank">Marten</a>.</h3>'); - tmpl_array.push('<a href="" id="create_new_library_btn" class="btn btn-primary icon-file ">New Library</a>'); + tmpl_array.push('<a href="" id="create_new_library_btn" class="btn btn-primary file ">New Library</a>'); tmpl_array.push('<table class="table table-condensed">'); tmpl_array.push(' <thead>'); tmpl_array.push(' <th class="button_heading"></th>'); @@ -698,7 +688,7 @@ tmpl_array.push(' <% _.each(libraries, function(library) { %>'); tmpl_array.push(' <tr>'); tmpl_array.push(' <td><button title="Open this library" type="button" data-id="<%- library.get("root_folder_id") %>" class="btn_open_folder btn btn-default btn-xs">'); - tmpl_array.push(' <span class="fa fa-icon-folder-open"></span> browse</td>'); + tmpl_array.push(' <span class="fa fa-folder-open"></span> browse</td>'); tmpl_array.push(' <td><%- library.get("name") %></td>'); tmpl_array.push(' <td><%= _.escape(library.get("description")) %></td>'); tmpl_array.push(' <td><%= _.escape(library.get("synopsis")) %></td>'); @@ -707,7 +697,7 @@ tmpl_array.push(' <% }); %>'); tmpl_array.push(' </tbody>'); tmpl_array.push('</table>'); - + tmpl_array.push('</div>'); return tmpl_array.join(''); }, @@ -735,7 +725,7 @@ show_library_modal : function (event){ event.preventDefault(); event.stopPropagation(); - + // create modal var self = this; this.modal = new mod_modal.GalaxyModal( @@ -747,7 +737,7 @@ 'Close' : function() {self.modal.hide()} } }); - + // show prepared modal this.modal.show(); }, @@ -828,7 +818,7 @@ library_router.on('route:folder_content', function(id) { // render folder's contents folderContentView.render({id: id}); - }); + }); library_router.on('route:download', function(folder_id, format) { // send download stream @@ -844,9 +834,9 @@ library_router.navigate('folders/' + folder_id, {trigger: false, replace: true}); } - }); + }); -Backbone.history.start(); +Backbone.history.start(); return this } diff -r caba1617084b4a0d38f1ebce970dda675755cd1d -r c653272fdbb3e2fcc8e5dd83b2c71df8bd9b5c2a static/style/src/less/base.less --- a/static/style/src/less/base.less +++ b/static/style/src/less/base.less @@ -23,6 +23,7 @@ // galaxy sub-components @import "frame.less"; @import "upload.less"; +@import "library.less"; // Mixins https://bitbucket.org/galaxy/galaxy-central/commits/267dcfd58a60/ Changeset: 267dcfd58a60 User: martenson Date: 2013-12-12 20:21:09 Summary: added library notifications, toaster, can be used outside of libraries as require.js module Affected #: 7 files diff -r c653272fdbb3e2fcc8e5dd83b2c71df8bd9b5c2a -r 267dcfd58a60d2de706d7c00c6fb6ec426eaaf5b static/scripts/galaxy.library.js --- a/static/scripts/galaxy.library.js +++ b/static/scripts/galaxy.library.js @@ -8,7 +8,7 @@ var responses = []; // dependencies -define(["galaxy.modal", "galaxy.masthead", "utils/galaxy.utils"], function(mod_modal, mod_masthead, mod_utils) { +define(["galaxy.modal", "galaxy.masthead", "utils/galaxy.utils", "libs/toastr"], function(mod_modal, mod_masthead, mod_utils, mod_toastr) { // MMMMMMMMMMMMMMM // === Models ==== @@ -706,18 +706,35 @@ render: function () { //hack to show scrollbars $("#center").css('overflow','auto'); + var that = this; - // if (typeof libraries === "undefined") { - libraries = new Libraries(); - // } + libraries = new Libraries(); + libraries.fetch({ success: function (libraries) { var template = _.template(that.template_library_list(), { libraries : libraries.models }); that.$el.html(template); - } + }, + error: function(model, response){ + if (response.statusCode().status === 403){ + //show notification + // var toast = mod_toastr; + mod_toastr.error('Please log in first. Redirecting to login page in 3s.'); + setTimeout(that.redirectToLogin, 3000); + } else { + library_router.navigate('#', {trigger: true, replace: true}); + } + } }) }, + redirectToSplash: function(){ + window.location = '../'; + }, + redirectToLogin: function(){ + window.location = '/user/login'; + }, + // own modal modal : null, diff -r c653272fdbb3e2fcc8e5dd83b2c71df8bd9b5c2a -r 267dcfd58a60d2de706d7c00c6fb6ec426eaaf5b static/scripts/galaxy.modal.js --- a/static/scripts/galaxy.modal.js +++ b/static/scripts/galaxy.modal.js @@ -172,10 +172,7 @@ // show notification showNotification : function(message, duration, type) { - // defaults - var duration = typeof duration !== 'undefined' ? duration : 1500; - // var bgColor = typeof bgColor !== 'undefined' ? bgColor : "#F4E0E1"; - // var txtColor = typeof txtColor !== 'undefined' ? txtColor : "#A42732"; + var duration = typeof duration !== 'undefined' ? duration : 3000; var bgColor; var txtColor; diff -r c653272fdbb3e2fcc8e5dd83b2c71df8bd9b5c2a -r 267dcfd58a60d2de706d7c00c6fb6ec426eaaf5b static/scripts/libs/toastr.js --- /dev/null +++ b/static/scripts/libs/toastr.js @@ -0,0 +1,307 @@ +/* + * Toastr + * Version 2.0.1 + * Copyright 2012 John Papa and Hans Fjällemark. + * All Rights Reserved. + * Use, reproduction, distribution, and modification of this code is subject to the terms and + * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php + * + * Author: John Papa and Hans Fjällemark + * Project: https://github.com/CodeSeven/toastr + */ +; (function (define) { + define([], function () { + var $ = jQuery; + return (function () { + var version = '2.0.1'; + var $container; + var listener; + var toastId = 0; + var toastType = { + error: 'error', + info: 'info', + success: 'success', + warning: 'warning' + }; + + var toastr = { + clear: clear, + error: error, + getContainer: getContainer, + info: info, + options: {}, + subscribe: subscribe, + success: success, + version: version, + warning: warning + }; + + return toastr; + + //#region Accessible Methods + function error(message, title, optionsOverride) { + return notify({ + type: toastType.error, + iconClass: getOptions().iconClasses.error, + message: message, + optionsOverride: optionsOverride, + title: title + }); + } + + function info(message, title, optionsOverride) { + return notify({ + type: toastType.info, + iconClass: getOptions().iconClasses.info, + message: message, + optionsOverride: optionsOverride, + title: title + }); + } + + function subscribe(callback) { + listener = callback; + } + + function success(message, title, optionsOverride) { + return notify({ + type: toastType.success, + iconClass: getOptions().iconClasses.success, + message: message, + optionsOverride: optionsOverride, + title: title + }); + } + + function warning(message, title, optionsOverride) { + return notify({ + type: toastType.warning, + iconClass: getOptions().iconClasses.warning, + message: message, + optionsOverride: optionsOverride, + title: title + }); + } + + function clear($toastElement) { + var options = getOptions(); + if (!$container) { getContainer(options); } + if ($toastElement && $(':focus', $toastElement).length === 0) { + $toastElement[options.hideMethod]({ + duration: options.hideDuration, + easing: options.hideEasing, + complete: function () { removeToast($toastElement); } + }); + return; + } + if ($container.children().length) { + $container[options.hideMethod]({ + duration: options.hideDuration, + easing: options.hideEasing, + complete: function () { $container.remove(); } + }); + } + } + //#endregion + + //#region Internal Methods + + function getDefaults() { + return { + tapToDismiss: true, + toastClass: 'toast', + containerId: 'toast-container', + debug: false, + + showMethod: 'fadeIn', //fadeIn, slideDown, and show are built into jQuery + showDuration: 300, + showEasing: 'swing', //swing and linear are built into jQuery + onShown: undefined, + hideMethod: 'fadeOut', + hideDuration: 1000, + hideEasing: 'swing', + onHidden: undefined, + + extendedTimeOut: 1000, + iconClasses: { + error: 'toast-error', + info: 'toast-info', + success: 'toast-success', + warning: 'toast-warning' + }, + iconClass: 'toast-info', + positionClass: 'toast-top-right', + timeOut: 5000, // Set timeOut and extendedTimeout to 0 to make it sticky + titleClass: 'toast-title', + messageClass: 'toast-message', + target: 'body', + closeHtml: '<button>×</button>', + newestOnTop: true + }; + } + + function publish(args) { + if (!listener) { + return; + } + listener(args); + } + + function notify(map) { + var + options = getOptions(), + iconClass = map.iconClass || options.iconClass; + + if (typeof (map.optionsOverride) !== 'undefined') { + options = $.extend(options, map.optionsOverride); + iconClass = map.optionsOverride.iconClass || iconClass; + } + + toastId++; + + $container = getContainer(options); + var + intervalId = null, + $toastElement = $('<div/>'), + $titleElement = $('<div/>'), + $messageElement = $('<div/>'), + $closeElement = $(options.closeHtml), + response = { + toastId: toastId, + state: 'visible', + startTime: new Date(), + options: options, + map: map + }; + + if (map.iconClass) { + $toastElement.addClass(options.toastClass).addClass(iconClass); + } + + if (map.title) { + $titleElement.append(map.title).addClass(options.titleClass); + $toastElement.append($titleElement); + } + + if (map.message) { + $messageElement.append(map.message).addClass(options.messageClass); + $toastElement.append($messageElement); + } + + if (options.closeButton) { + $closeElement.addClass('toast-close-button'); + $toastElement.prepend($closeElement); + } + + $toastElement.hide(); + if (options.newestOnTop) { + $container.prepend($toastElement); + } else { + $container.append($toastElement); + } + + + $toastElement[options.showMethod]( + { duration: options.showDuration, easing: options.showEasing, complete: options.onShown } + ); + if (options.timeOut > 0) { + intervalId = setTimeout(hideToast, options.timeOut); + } + + $toastElement.hover(stickAround, delayedhideToast); + if (!options.onclick && options.tapToDismiss) { + $toastElement.click(hideToast); + } + if (options.closeButton && $closeElement) { + $closeElement.click(function (event) { + event.stopPropagation(); + hideToast(true); + }); + } + + if (options.onclick) { + $toastElement.click(function () { + options.onclick(); + hideToast(); + }); + } + + publish(response); + + if (options.debug && console) { + console.log(response); + } + + return $toastElement; + + function hideToast(override) { + if ($(':focus', $toastElement).length && !override) { + return; + } + return $toastElement[options.hideMethod]({ + duration: options.hideDuration, + easing: options.hideEasing, + complete: function () { + removeToast($toastElement); + if (options.onHidden) { + options.onHidden(); + } + response.state = 'hidden'; + response.endTime = new Date(), + publish(response); + } + }); + } + + function delayedhideToast() { + if (options.timeOut > 0 || options.extendedTimeOut > 0) { + intervalId = setTimeout(hideToast, options.extendedTimeOut); + } + } + + function stickAround() { + clearTimeout(intervalId); + $toastElement.stop(true, true)[options.showMethod]( + { duration: options.showDuration, easing: options.showEasing } + ); + } + } + function getContainer(options) { + if (!options) { options = getOptions(); } + $container = $('#' + options.containerId); + if ($container.length) { + return $container; + } + $container = $('<div/>') + .attr('id', options.containerId) + .addClass(options.positionClass); + $container.appendTo($(options.target)); + return $container; + } + + function getOptions() { + return $.extend({}, getDefaults(), toastr.options); + } + + function removeToast($toastElement) { + if (!$container) { $container = getContainer(); } + if ($toastElement.is(':visible')) { + return; + } + $toastElement.remove(); + $toastElement = null; + if ($container.children().length === 0) { + $container.remove(); + } + } + //#endregion + + })(); + }); +}(typeof define === 'function' && define.amd ? define : function (deps, factory) { + if (typeof module !== 'undefined' && module.exports) { //Node + module.exports = factory(require(deps[0])); + } else { + window['toastr'] = factory(window['jQuery']); + } +})); diff -r c653272fdbb3e2fcc8e5dd83b2c71df8bd9b5c2a -r 267dcfd58a60d2de706d7c00c6fb6ec426eaaf5b static/scripts/utils/galaxy.uploadbox.js --- a/static/scripts/utils/galaxy.uploadbox.js +++ b/static/scripts/utils/galaxy.uploadbox.js @@ -352,4 +352,4 @@ 'compatible' : compatible }; } -})(jQuery); \ No newline at end of file +})(jQuery); diff -r c653272fdbb3e2fcc8e5dd83b2c71df8bd9b5c2a -r 267dcfd58a60d2de706d7c00c6fb6ec426eaaf5b static/style/Gruntfile.js --- a/static/style/Gruntfile.js +++ b/static/style/Gruntfile.js @@ -6,7 +6,7 @@ var theme = grunt.option( 'theme', 'blue' ); var out = 'blue' - var lessFiles = [ 'base', 'autocomplete_tagging', 'embed_item', 'iphone', 'masthead', 'library', 'trackster' ]; + var lessFiles = [ 'base', 'autocomplete_tagging', 'embed_item', 'iphone', 'masthead', 'library', 'trackster', 'toastr' ]; var _ = grunt.util._; var fmt = _.sprintf; diff -r c653272fdbb3e2fcc8e5dd83b2c71df8bd9b5c2a -r 267dcfd58a60d2de706d7c00c6fb6ec426eaaf5b static/style/src/less/base.less --- a/static/style/src/less/base.less +++ b/static/style/src/less/base.less @@ -24,6 +24,7 @@ @import "frame.less"; @import "upload.less"; @import "library.less"; +@import "toastr.less"; // Mixins diff -r c653272fdbb3e2fcc8e5dd83b2c71df8bd9b5c2a -r 267dcfd58a60d2de706d7c00c6fb6ec426eaaf5b static/style/src/less/toastr.less --- /dev/null +++ b/static/style/src/less/toastr.less @@ -0,0 +1,232 @@ +// Mix-ins +.borderRadius(@radius) { + -moz-border-radius: @radius; + -webkit-border-radius: @radius; + border-radius: @radius; +} + +.boxShadow(@boxShadow) { + -moz-box-shadow: @boxShadow; + -webkit-box-shadow: @boxShadow; + box-shadow: @boxShadow; +} + +.opacity(@opacity) { + @opacityPercent: @opacity * 100; + opacity: @opacity; + -ms-filter: ~"progid:DXImageTransform.Microsoft.Alpha(Opacity=@{opacityPercent})"; + filter: ~"alpha(opacity=@{opacityPercent})"; +} + +.wordWrap(@wordWrap: break-word) { + -ms-word-wrap: @wordWrap; + word-wrap: @wordWrap; +} + +// Variables +@black: #000000; +@grey: #999999; +@light-grey: #CCCCCC; +@white: #FFFFFF; +@near-black: #030303; +@green: #51A351; +@red: #BD362F; +@blue: #2F96B4; +@orange: #F89406; + +// Styles +.toast-title { + font-weight: bold; +} + +.toast-message { + .wordWrap(); + + a, + label { + color: @white; + } + + a:hover { + color: @light-grey; + text-decoration: none; + } +} + +.toast-close-button { + position: relative; + right: -0.3em; + top: -0.3em; + float: right; + font-size: 20px; + font-weight: bold; + color: @white; + -webkit-text-shadow: 0 1px 0 rgba(255,255,255,1); + text-shadow: 0 1px 0 rgba(255,255,255,1); + .opacity(0.8); + + &:hover, + &:focus { + color: @black; + text-decoration: none; + cursor: pointer; + .opacity(0.4); + } +} + +/*Additional properties for button version + iOS requires the button element instead of an anchor tag. + If you want the anchor version, it requires `href="#"`.*/ +button.toast-close-button { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +//#endregion + +.toast-top-full-width { + top: 0; + right: 0; + width: 100%; +} + +.toast-bottom-full-width { + bottom: 0; + right: 0; + width: 100%; +} + +.toast-top-left { + top: 12px; + left: 12px; +} + +.toast-top-right { + top: 12px; + right: 12px; +} + +.toast-bottom-right { + right: 12px; + bottom: 12px; +} + +.toast-bottom-left { + bottom: 12px; + left: 12px; +} + +#toast-container { + position: fixed; + z-index: 999999; + + * { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + } + + > div { + margin: 4em 0 6px; + padding: 15px 15px 15px 50px; + width: 300px; + .borderRadius(3px 3px 3px 3px); + background-position: 15px center; + background-repeat: no-repeat; + .boxShadow(0 0 12px @grey); + color: @white; + .opacity(0.8); + } + + > :hover { + .boxShadow(0 0 12px @black); + .opacity(1); + cursor: pointer; + } + + > .toast-info { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important; + } + + > .toast-error { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important; + } + + > .toast-success { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important; + } + + > .toast-warning { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important; + } + + /*overrides*/ + &.toast-top-full-width > div, + &.toast-bottom-full-width > div { + width: 96%; + margin: auto; + } +} + +.toast { + background-color: @near-black; +} + +.toast-success { + background-color: @green; +} + +.toast-error { + background-color: @red; +} + +.toast-info { + background-color: @blue; +} + +.toast-warning { + background-color: @orange; +} + +/*Responsive Design*/ + +@media all and (max-width: 240px) { + #toast-container { + + > div { + padding: 8px 8px 8px 50px; + width: 11em; + } + + & .toast-close-button { + right: -0.2em; + top: -0.2em; + } + } +} + +@media all and (min-width: 241px) and (max-width: 480px) { + #toast-container { + > div { + padding: 8px 8px 8px 50px; + width: 18em; + } + + & .toast-close-button { + right: -0.2em; + top: -0.2em; + } + } +} + +@media all and (min-width: 481px) and (max-width: 768px) { + #toast-container { + > div { + padding: 15px 15px 15px 50px; + width: 25em; + } + } +} https://bitbucket.org/galaxy/galaxy-central/commits/9e7cd2749bde/ Changeset: 9e7cd2749bde User: martenson Date: 2013-12-13 20:33:06 Summary: rewrite of galaxy.library to use global notifications; removed previous notification engine from galaxy.modal.js; Affected #: 3 files diff -r 267dcfd58a60d2de706d7c00c6fb6ec426eaaf5b -r 9e7cd2749bdeb36bab9a7c883d95a877f9eaa2ca lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -144,38 +144,6 @@ parent_resources=dict( member_name='datatype', collection_name='datatypes' ) ) #webapp.mapper.connect( 'run_workflow', '/api/workflow/{workflow_id}/library/{library_id}', controller='workflows', action='run', workflow_id=None, library_id=None, conditions=dict(method=["GET"]) ) webapp.mapper.resource( 'search', 'search', path_prefix='/api' ) - - # ======================= - # ===== LIBRARY API ===== - # ======================= - - # The /folders section is experimental at this point: - log.debug( "app.config.api_folders: %s" % app.config.api_folders ) - - webapp.mapper.connect( 'show_lda_item', '/api/libraries/datasets/:id', controller='lda_datasets', action='show', conditions=dict(method=["GET"]) ) - webapp.mapper.connect( 'download_lda_items', '/api/libraries/datasets/download/:format', controller='lda_datasets', action='download', conditions=dict(method=["POST"]) ) - - webapp.mapper.resource_with_deleted( 'library', 'libraries', path_prefix='/api' ) - webapp.mapper.resource( 'folder', 'folders', path_prefix='/api' ) - - webapp.mapper.resource( 'content', 'contents', - controller='folder_contents', - name_prefix='folder_', - path_prefix='/api/folders/:folder_id', - parent_resources=dict( member_name='folder', collection_name='folders' ) ) - webapp.mapper.resource( 'content', - 'contents', - controller='library_contents', - name_prefix='library_', - path_prefix='/api/libraries/:library_id', - parent_resources=dict( member_name='library', collection_name='libraries' ) ) - webapp.mapper.resource( 'permission', - 'permissions', - path_prefix='/api/libraries/:library_id', - parent_resources=dict( member_name='library', collection_name='libraries' ) ) - _add_item_extended_metadata_controller( webapp, - name_prefix="library_dataset_", - path_prefix='/api/libraries/:library_id/contents/:library_content_id' ) # add as a non-ATOM API call to support the notion of a 'current/working' history unique to the history resource @@ -195,6 +163,52 @@ webapp.mapper.connect("workflow_dict", '/api/workflows/{workflow_id}/download', controller='workflows', action='workflow_dict', conditions=dict(method=['GET'])) # Preserve the following download route for now for dependent applications -- deprecate at some point webapp.mapper.connect("workflow_dict", '/api/workflows/download/{workflow_id}', controller='workflows', action='workflow_dict', conditions=dict(method=['GET'])) + + # ======================= + # ===== LIBRARY API ===== + # ======================= + + webapp.mapper.connect( 'show_lda_item', + '/api/libraries/datasets/:id', + controller='lda_datasets', + action='show', + conditions=dict( method=[ "GET" ] ) ) + + webapp.mapper.connect( 'download_lda_items', + '/api/libraries/datasets/download/:format', + controller='lda_datasets', + action='download', + conditions=dict( method=[ "POST", "GET" ] ) ) + + webapp.mapper.resource_with_deleted( 'library', + 'libraries', + path_prefix='/api' ) + webapp.mapper.resource( 'folder', + 'folders', + path_prefix='/api' ) + + webapp.mapper.resource( 'content', + 'contents', + controller='folder_contents', + name_prefix='folder_', + path_prefix='/api/folders/:folder_id', + parent_resources=dict( member_name='folder', collection_name='folders' ) ) + + webapp.mapper.resource( 'content', + 'contents', + controller='library_contents', + name_prefix='library_', + path_prefix='/api/libraries/:library_id', + parent_resources=dict( member_name='library', collection_name='libraries' ) ) + + webapp.mapper.resource( 'permission', + 'permissions', + path_prefix='/api/libraries/:library_id', + parent_resources=dict( member_name='library', collection_name='libraries' ) ) + + _add_item_extended_metadata_controller( webapp, + name_prefix="library_dataset_", + path_prefix='/api/libraries/:library_id/contents/:library_content_id' ) # ==================== # ===== TOOLSHED ===== diff -r 267dcfd58a60d2de706d7c00c6fb6ec426eaaf5b -r 9e7cd2749bdeb36bab9a7c883d95a877f9eaa2ca static/scripts/galaxy.library.js --- a/static/scripts/galaxy.library.js +++ b/static/scripts/galaxy.library.js @@ -8,7 +8,11 @@ var responses = []; // dependencies -define(["galaxy.modal", "galaxy.masthead", "utils/galaxy.utils", "libs/toastr"], function(mod_modal, mod_masthead, mod_utils, mod_toastr) { +define([ + "galaxy.modal", + "galaxy.masthead", + "utils/galaxy.utils", + "libs/toastr"], function(mod_modal, mod_masthead, mod_utils, mod_toastr) { // MMMMMMMMMMMMMMM // === Models ==== @@ -308,7 +312,7 @@ // event binding events: { 'click #select-all-checkboxes' : 'selectAll', - 'click .folder_row' : 'selectClicked', + 'click .folder_row' : 'selectClickedRow', 'click #toolbtn_bulk_import' : 'modalBulkImport', 'click #toolbtn_dl' : 'bulkDownload', 'click .library-dataset' : 'showDatasetDetails', @@ -350,6 +354,7 @@ var template = _.template(that.templateFolder(), { path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id, upper_folder_id: upper_folder_id }); // var template = _.template(that.templateFolder(), { path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id }); that.$el.html(template); + } }) }, @@ -456,12 +461,14 @@ // save the dataset into selected history historyItem.save({ content : library_dataset_id, source : 'library' }, { success : function(){ - self.modal.showNotification('Dataset imported', 3000, 'success'); + mod_toastr.success('Dataset imported'); + // self.modal.showNotification('Dataset imported', 3000, 'success'); //enable the buttons self.modal.enableButton('Import'); self.modal.enableButton('Download'); }, error : function(){ - self.modal.showNotification('An error occured! Dataset not imported. Please try again later.', 5000, 'error'); + mod_toastr.error('An error occured! Dataset not imported. Please try again.') + // self.modal.showNotification('An error occured! Dataset not imported. Please try again later.', 5000, 'error'); //enable the buttons self.modal.enableButton('Import'); self.modal.enableButton('Download'); @@ -471,37 +478,69 @@ // select all datasets selectAll : function (event) { - var selected = event.target.checked; - // Iterate each checkbox - $(':checkbox').each(function () { this.checked = selected; }); - this.showTools(); - }, + var selected = event.target.checked; + that = this; + // Iterate each checkbox + $(':checkbox').each(function () { + this.checked = selected; + $row = $(this.parentElement.parentElement); + // Change color of selected/unselected + (selected) ? that.makeDarkRow($row) : that.makeWhiteRow($row); + }); + // Show the tools in menu + this.checkTools(); + }, - // click checkbox on folder click - selectClicked : function (event) { - var checkbox = $("#" + event.target.parentElement.id).find(':checkbox') - if (checkbox[0] != undefined) { - if (checkbox[0].checked){ - checkbox[0].checked = ''; - // $(event.target.parentElement).css('background-color', '').css('color', ''); - $(event.target.parentElement).removeClass('dark'); - $(event.target.parentElement).find('a').removeClass('dark'); - $(event.target.parentElement).addClass('light'); - $(event.target.parentElement).find('a').addClass('light'); + // Check checkbox on row itself or row checkbox click + selectClickedRow : function (event) { + var checkbox = ''; + var $row; + var source; + if (event.target.localName === 'input'){ + checkbox = event.target; + $row = $(event.target.parentElement.parentElement); + source = 'input'; + } else if (event.target.localName === 'td') { + checkbox = $("#" + event.target.parentElement.id).find(':checkbox')[0]; + $row = $(event.target.parentElement); + source = 'td'; + } + if (checkbox === '') {event.stopPropagation(); return;} // button in row was clicked + + if (checkbox.checked){ + if (source==='td'){ + checkbox.checked = ''; + this.makeWhiteRow($row); + } else if (source==='input') { + this.makeDarkRow($row); + } } else { - checkbox[0].checked = 'selected'; - $(event.target.parentElement).removeClass('light'); - $(event.target.parentElement).find('a').removeClass('light'); - $(event.target.parentElement).addClass('dark'); - $(event.target.parentElement).find('a').addClass('dark'); - // $(event.target.parentElement).css('background-color', '#8389a1').css('color', 'white'); + if (source==='td'){ + checkbox.checked = 'selected'; + this.makeDarkRow($row); + } else if (source==='input') { + this.makeWhiteRow($row); + } } - } - this.showTools(); + this.checkTools(); + }, + + makeDarkRow: function($row){ + $row.removeClass('light'); + $row.find('a').removeClass('light'); + $row.addClass('dark'); + $row.find('a').addClass('dark'); + }, + + makeWhiteRow: function($row){ + $row.removeClass('dark'); + $row.find('a').removeClass('dark'); + $row.addClass('light'); + $row.find('a').addClass('light'); }, // show toolbar in case something is selected - showTools : function(){ + checkTools : function(){ var checkedValues = $('#folder_table').find(':checked'); if(checkedValues.length > 0){ $('#toolbtn_bulk_import').show(); @@ -577,7 +616,9 @@ var self = this; var popped_item = history_item_set.pop(); if (typeof popped_item === "undefined") { - self.modal.showNotification('All datasets imported', 3000, 'success'); + mod_toastr.success('All datasets imported'); + this.modal.hide(); + // self.modal.showNotification('All datasets imported', 3000, 'success'); // enable button again self.modal.enableButton('Import'); return @@ -646,12 +687,13 @@ //send request $('<form action="'+ url +'" method="'+ (method||'post') +'">'+inputs+'</form>') .appendTo('body').submit().remove(); + mod_toastr.info('Your download will begin soon'); }; }, // shows modal for creating folder createFolderModal: function(){ - alert('creating folder'); + mod_toastr.info('This will create folder...in the future'); } }); @@ -704,7 +746,7 @@ // render render: function () { - //hack to show scrollbars + //hack to show scrollbars due to #center element inheritance $("#center").css('overflow','auto'); var that = this; @@ -717,18 +759,16 @@ }, error: function(model, response){ if (response.statusCode().status === 403){ - //show notification - // var toast = mod_toastr; mod_toastr.error('Please log in first. Redirecting to login page in 3s.'); setTimeout(that.redirectToLogin, 3000); } else { - library_router.navigate('#', {trigger: true, replace: true}); + mod_toastr.error('An error occured. Please try again.'); } } }) }, - redirectToSplash: function(){ + redirectToHome: function(){ window.location = '../'; }, redirectToLogin: function(){ @@ -762,19 +802,23 @@ // create the new library from modal create_new_library_event: function(){ var libraryDetails = this.serialize_new_library(); - var valid = this.validate_new_library(libraryDetails); - var library = new Library(); - var self = this; - library.save(libraryDetails, { - success: function (library) { - self.modal.hide(); - self.clear_library_modal(); - self.render(); - }, - error: function(){ - self.modal.showNotification('An error occured', 5000, 'error'); - } - }); + if (this.validate_new_library(libraryDetails)){ + var library = new Library(); + var self = this; + library.save(libraryDetails, { + success: function (library) { + self.modal.hide(); + self.clear_library_modal(); + self.render(); + mod_toastr.success('Library created'); + }, + error: function(){ + mod_toastr.error('An error occured :('); + } + }); + } else { + mod_toastr.error('Library\'s name is missing'); + } return false; }, @@ -794,14 +838,13 @@ }; }, - validate_new_library: function(library){ - + // validate new library info + validate_new_library: function(libraryDetails){ + return libraryDetails.name !== ''; }, - // template for new library modal - template_new_library: function() - { + template_new_library: function(){ tmpl_array = []; tmpl_array.push('<div id="new_library_modal">'); @@ -824,34 +867,29 @@ folderContentView = new FolderContentView(); galaxyLibraryview = new GalaxyLibraryview(); - library_router = new LibraryRouter(); library_router.on('route:libraries', function() { // render libraries list galaxyLibraryview.render(); - }); + }); library_router.on('route:folder_content', function(id) { // render folder's contents folderContentView.render({id: id}); - }); + }); library_router.on('route:download', function(folder_id, format) { - // send download stream - if (typeof folderContentView === 'undefined'){ - alert('you cant touch this!'); - // } else if (folderContentView.modal !== null){ - // folderContentView.download(folder_id, format); - } else if ($('#center').find(':checked').length === 0) { // coming from outside of the library app + if ($('#center').find(':checked').length === 0) { + // this happens rarely when there is a server/data error and client gets an actual response instead of an attachment + // we don't know what was selected so we can't download again, we redirect to the folder provided library_router.navigate('folders/' + folder_id, {trigger: true, replace: true}); - // TODO show message of redirection } else { + // send download stream folderContentView.download(folder_id, format); library_router.navigate('folders/' + folder_id, {trigger: false, replace: true}); } - - }); + }); Backbone.history.start(); diff -r 267dcfd58a60d2de706d7c00c6fb6ec426eaaf5b -r 9e7cd2749bdeb36bab9a7c883d95a877f9eaa2ca static/scripts/galaxy.modal.js --- a/static/scripts/galaxy.modal.js +++ b/static/scripts/galaxy.modal.js @@ -26,16 +26,17 @@ if (options) this.create(options); - this.bindClick(event, self); + // Bind the hiding events + this.bindEvents(event, self); }, // bind the click-to-hide function - bindClick: function(event, that) { + bindEvents: function(event, that) { // bind the ESC key to hide() function $(document).on('keyup', function(event){ if (event.keyCode == 27) { self.hide(); } }) - // bind the click anywhere to hide() function... + // bind the 'click anywhere' to hide() function... $('html').on('click', function(event){ that.hide(); }) @@ -46,26 +47,24 @@ }, // unbind the click-to-hide function - unbindClick: function(event, that){ + unbindEvents: function(event, that){ // bind the ESC key to hide() function $(document).off('keyup', function(event){ if (event.keyCode == 27) { that.hide(); } }) - // unbind the click anywhere to hide() function... + // unbind the 'click anywhere' to hide() function... $('html').off('click', function(event){ that.hide(); }) - // ...but don't hide if the click is on modal content $('.modal-content').off('click', function(event){ event.stopPropagation(); }) }, - // destroy destroy : function(){ this.hide(); - this.unbindClick(); + this.unbindEvents(); $('.modal').remove(); }, @@ -103,6 +102,7 @@ // set flag this.visible = false; + this.unbindEvents(); }, // create @@ -127,7 +127,6 @@ this.$footer = (this.$el).find('.modal-footer'); this.$buttons = (this.$el).find('.buttons'); this.$backdrop = (this.$el).find('.modal-backdrop'); - this.$notification = (this.$el).find('.notification-modal'); // append body this.$body.html(this.options.body); @@ -170,35 +169,6 @@ this.$buttons.find('#' + String(name).toLowerCase()).show(); }, - // show notification - showNotification : function(message, duration, type) { - var duration = typeof duration !== 'undefined' ? duration : 3000; - var bgColor; - var txtColor; - - if (type === 'error'){ - bgColor = '#f4e0e1'; - txtColor = '#a42732'; - // } else if (type === 'success'){ - } else { // success is default - bgColor = '#e1f4e0'; - txtColor = '#32a427'; - } - - var HTMLmessage = "<div class='notification-message' style='text-align:center; line-height:16px; '> " + message + " </div>"; - this.$notification.html("<div id='notification-bar' style='display:none; float: right; height: 16px; width:100%; background-color: " + bgColor + "; z-index: 100; color: " + txtColor + ";border-bottom: 1px solid " + txtColor + ";'>" + HTMLmessage + "</div>"); - - var self = this; - - /*animate the bar*/ - $('#notification-bar').slideDown(function() { - setTimeout(function() { - $('#notification-bar').slideUp(function() {self.$notification.html('');}); - }, duration); - }); - - }, - // returns scroll top for body element scrollTop: function() { @@ -218,7 +188,6 @@ '<div class="modal-header">' + '<button type="button" class="close" style="display: none;">×</button>' + '<h4 class="title">' + title + '</h4>' + - '<span class="notification-modal"></span>' + '</div>' + '<div class="modal-body"></div>' + '<div class="modal-footer">' + https://bitbucket.org/galaxy/galaxy-central/commits/0522f7504094/ Changeset: 0522f7504094 User: martenson Date: 2013-12-13 21:05:26 Summary: switch download to GET, removed toaster from grunt file Affected #: 2 files diff -r 9e7cd2749bdeb36bab9a7c883d95a877f9eaa2ca -r 0522f7504094bd5be2be97846e695a0c790bf929 static/scripts/galaxy.library.js --- a/static/scripts/galaxy.library.js +++ b/static/scripts/galaxy.library.js @@ -462,13 +462,11 @@ // save the dataset into selected history historyItem.save({ content : library_dataset_id, source : 'library' }, { success : function(){ mod_toastr.success('Dataset imported'); - // self.modal.showNotification('Dataset imported', 3000, 'success'); //enable the buttons self.modal.enableButton('Import'); self.modal.enableButton('Download'); }, error : function(){ mod_toastr.error('An error occured! Dataset not imported. Please try again.') - // self.modal.showNotification('An error occured! Dataset not imported. Please try again later.', 5000, 'error'); //enable the buttons self.modal.enableButton('Import'); self.modal.enableButton('Download'); @@ -618,7 +616,6 @@ if (typeof popped_item === "undefined") { mod_toastr.success('All datasets imported'); this.modal.hide(); - // self.modal.showNotification('All datasets imported', 3000, 'success'); // enable button again self.modal.enableButton('Import'); return @@ -669,7 +666,7 @@ var url = '/api/libraries/datasets/download/' + format; var data = {'ldda_ids' : dataset_ids}; - this.processDownload(url, data); + this.processDownload(url, data, 'get'); }, // create hidden form and submit through POST to initialize download diff -r 9e7cd2749bdeb36bab9a7c883d95a877f9eaa2ca -r 0522f7504094bd5be2be97846e695a0c790bf929 static/style/Gruntfile.js --- a/static/style/Gruntfile.js +++ b/static/style/Gruntfile.js @@ -6,7 +6,7 @@ var theme = grunt.option( 'theme', 'blue' ); var out = 'blue' - var lessFiles = [ 'base', 'autocomplete_tagging', 'embed_item', 'iphone', 'masthead', 'library', 'trackster', 'toastr' ]; + var lessFiles = [ 'base', 'autocomplete_tagging', 'embed_item', 'iphone', 'masthead', 'library', 'trackster']; var _ = grunt.util._; var fmt = _.sprintf; https://bitbucket.org/galaxy/galaxy-central/commits/5463295a3981/ Changeset: 5463295a3981 User: martenson Date: 2013-12-16 00:26:37 Summary: Merged galaxy/galaxy-central into default Affected #: 139 files diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/app.py --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -1,16 +1,10 @@ from __future__ import absolute_import -import sys, os, atexit +import sys +import os -from galaxy import config, jobs, util, tools, web -import galaxy.tools.search -import galaxy.tools.data -import tool_shed.galaxy_install -import tool_shed.tool_shed_registry -from galaxy.web import security +from galaxy import config, jobs import galaxy.model -import galaxy.datatypes.registry import galaxy.security -from galaxy.objectstore import build_object_store_from_config import galaxy.quota from galaxy.tags.tag_handler import GalaxyTagHandler from galaxy.visualization.genomes import Genomes @@ -27,7 +21,8 @@ import logging log = logging.getLogger( __name__ ) -class UniverseApplication( object ): + +class UniverseApplication( object, config.ConfiguresGalaxyMixin ): """Encapsulates the state of a Universe application""" def __init__( self, **kwargs ): print >> sys.stderr, "python path is: " + ", ".join( sys.path ) @@ -38,92 +33,38 @@ self.config.check() config.configure_logging( self.config ) self.configure_fluent_log() - # Determine the database url - if self.config.database_connection: - db_url = self.config.database_connection - else: - db_url = "sqlite:///%s?isolation_level=IMMEDIATE" % self.config.database - install_db_url = self.config.install_database_connection - # TODO: Consider more aggressive check here that this is not the same - # database file under the hood. - combined_install_database = not( install_db_url and install_db_url != db_url ) - # Set up the tool sheds registry - if os.path.isfile( self.config.tool_sheds_config ): - self.tool_shed_registry = tool_shed.tool_shed_registry.Registry( self.config.root, self.config.tool_sheds_config ) - else: - self.tool_shed_registry = None - # Initialize database / check for appropriate schema version. # If this - # is a new installation, we'll restrict the tool migration messaging. - from galaxy.model.migrate.check import create_or_verify_database - create_or_verify_database( db_url, kwargs.get( 'global_conf', {} ).get( '__file__', None ), self.config.database_engine_options, app=self ) - if not combined_install_database: - from galaxy.model.tool_shed_install.migrate.check import create_or_verify_database as tsi_create_or_verify_database - tsi_create_or_verify_database( install_db_url, self.config.install_database_engine_options, app=self ) - # Alert the Galaxy admin to tools that have been moved from the distribution to the tool shed. - from tool_shed.galaxy_install.migrate.check import verify_tools - verify_tools( self, db_url, kwargs.get( 'global_conf', {} ).get( '__file__', None ), self.config.database_engine_options ) - # Object store manager - self.object_store = build_object_store_from_config(self.config, fsmon=True) + self._configure_tool_shed_registry() + + self._configure_object_store( fsmon=True ) + # Setup the database engine and ORM - from galaxy.model import mapping - self.model = mapping.init( self.config.file_path, - db_url, - self.config.database_engine_options, - map_install_models=combined_install_database, - database_query_profiling_proxy = self.config.database_query_profiling_proxy, - object_store = self.object_store, - trace_logger=self.trace_logger, - use_pbkdf2=self.config.get_bool( 'use_pbkdf2', True ) ) + config_file = kwargs.get( 'global_conf', {} ).get( '__file__', None ) + self._configure_models( check_migrate_databases=True, check_migrate_tools=True, config_file=config_file ) - if combined_install_database: - log.info("Install database targetting Galaxy's database configuration.") - self.install_model = self.model - else: - from galaxy.model.tool_shed_install import mapping as install_mapping - install_db_url = self.config.install_database_connection - log.info("Install database using its own connection %s" % install_db_url) - install_db_engine_options = self.config.install_database_engine_options - self.install_model = install_mapping.init( install_db_url, - install_db_engine_options ) # Manage installed tool shed repositories. - self.installed_repository_manager = tool_shed.galaxy_install.InstalledRepositoryManager( self ) - # Create an empty datatypes registry. - self.datatypes_registry = galaxy.datatypes.registry.Registry() - # Load proprietary datatypes defined in datatypes_conf.xml files in all installed tool shed repositories. We - # load proprietary datatypes before datatypes in the distribution because Galaxy's default sniffers include some - # generic sniffers (eg text,xml) which catch anything, so it's impossible for proprietary sniffers to be used. - # However, if there is a conflict (2 datatypes with the same extension) between a proprietary datatype and a datatype - # in the Galaxy distribution, the datatype in the Galaxy distribution will take precedence. If there is a conflict - # between 2 proprietary datatypes, the datatype from the repository that was installed earliest will take precedence. - self.installed_repository_manager.load_proprietary_datatypes() - # Load the data types in the Galaxy distribution, which are defined in self.config.datatypes_config. - self.datatypes_registry.load_datatypes( self.config.root, self.config.datatypes_config ) + from tool_shed.galaxy_install import installed_repository_manager + self.installed_repository_manager = installed_repository_manager.InstalledRepositoryManager( self ) + + self._configure_datatypes_registry( self.installed_repository_manager ) galaxy.model.set_datatypes_registry( self.datatypes_registry ) + # Security helper - self.security = security.SecurityHelper( id_secret=self.config.id_secret ) + self._configure_security() # Tag handler self.tag_handler = GalaxyTagHandler() # Genomes self.genomes = Genomes( self ) # Data providers registry. self.data_provider_registry = DataProviderRegistry() - # Initialize tool data tables using the config defined by self.config.tool_data_table_config_path. - self.tool_data_tables = galaxy.tools.data.ToolDataTableManager( tool_data_path=self.config.tool_data_path, - config_filename=self.config.tool_data_table_config_path ) - # Load additional entries defined by self.config.shed_tool_data_table_config into tool data tables. - self.tool_data_tables.load_from_config_file( config_filename=self.config.shed_tool_data_table_config, - tool_data_path=self.tool_data_tables.tool_data_path, - from_shed_config=False ) + + self._configure_tool_data_tables( from_shed_config=False ) + # Initialize the job management configuration self.job_config = jobs.JobConfiguration(self) - # Initialize the tools, making sure the list of tool configs includes the reserved migrated_tools_conf.xml file. - tool_configs = self.config.tool_configs - if self.config.migrated_tools_config not in tool_configs: - tool_configs.append( self.config.migrated_tools_config ) - self.toolbox = tools.ToolBox( tool_configs, self.config.tool_path, self ) - # Search support for tools - self.toolbox_search = galaxy.tools.search.ToolBoxSearch( self.toolbox ) + + self._configure_toolbox() + # Load Data Manager self.data_managers = DataManagers( self ) # If enabled, poll respective tool sheds to see if updates are available for any installed tool shed repositories. diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/config.py --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -473,6 +473,7 @@ rval[ key ] = value return rval + def configure_logging( config ): """ Allow some basic logging configuration to be read from ini file. @@ -513,3 +514,110 @@ sentry_handler.setLevel( logging.WARN ) root.addHandler( sentry_handler ) + +class ConfiguresGalaxyMixin: + """ Shared code for configuring Galaxy-like app objects. + """ + + def _configure_toolbox( self ): + # Initialize the tools, making sure the list of tool configs includes the reserved migrated_tools_conf.xml file. + tool_configs = self.config.tool_configs + if self.config.migrated_tools_config not in tool_configs: + tool_configs.append( self.config.migrated_tools_config ) + from galaxy import tools + self.toolbox = tools.ToolBox( tool_configs, self.config.tool_path, self ) + # Search support for tools + import galaxy.tools.search + self.toolbox_search = galaxy.tools.search.ToolBoxSearch( self.toolbox ) + + def _configure_tool_data_tables( self, from_shed_config ): + from galaxy.tools.data import ToolDataTableManager + + # Initialize tool data tables using the config defined by self.config.tool_data_table_config_path. + self.tool_data_tables = ToolDataTableManager( tool_data_path=self.config.tool_data_path, + config_filename=self.config.tool_data_table_config_path ) + # Load additional entries defined by self.config.shed_tool_data_table_config into tool data tables. + self.tool_data_tables.load_from_config_file( config_filename=self.config.shed_tool_data_table_config, + tool_data_path=self.tool_data_tables.tool_data_path, + from_shed_config=from_shed_config ) + + def _configure_datatypes_registry( self, installed_repository_manager=None ): + from galaxy.datatypes import registry + # Create an empty datatypes registry. + self.datatypes_registry = registry.Registry() + if installed_repository_manager: + # Load proprietary datatypes defined in datatypes_conf.xml files in all installed tool shed repositories. We + # load proprietary datatypes before datatypes in the distribution because Galaxy's default sniffers include some + # generic sniffers (eg text,xml) which catch anything, so it's impossible for proprietary sniffers to be used. + # However, if there is a conflict (2 datatypes with the same extension) between a proprietary datatype and a datatype + # in the Galaxy distribution, the datatype in the Galaxy distribution will take precedence. If there is a conflict + # between 2 proprietary datatypes, the datatype from the repository that was installed earliest will take precedence. + installed_repository_manager.load_proprietary_datatypes() + # Load the data types in the Galaxy distribution, which are defined in self.config.datatypes_config. + self.datatypes_registry.load_datatypes( self.config.root, self.config.datatypes_config ) + + def _configure_object_store( self, **kwds ): + from galaxy.objectstore import build_object_store_from_config + self.object_store = build_object_store_from_config( self.config, **kwds ) + + def _configure_security( self ): + from galaxy.web import security + self.security = security.SecurityHelper( id_secret=self.config.id_secret ) + + def _configure_tool_shed_registry( self ): + import tool_shed.tool_shed_registry + + # Set up the tool sheds registry + if os.path.isfile( self.config.tool_sheds_config ): + self.tool_shed_registry = tool_shed.tool_shed_registry.Registry( self.config.root, self.config.tool_sheds_config ) + else: + self.tool_shed_registry = None + + def _configure_models( self, check_migrate_databases=False, check_migrate_tools=False, config_file=None ): + """ + Preconditions: object_store must be set on self. + """ + if self.config.database_connection: + db_url = self.config.database_connection + else: + db_url = "sqlite:///%s?isolation_level=IMMEDIATE" % self.config.database + install_db_url = self.config.install_database_connection + # TODO: Consider more aggressive check here that this is not the same + # database file under the hood. + combined_install_database = not( install_db_url and install_db_url != db_url ) + install_db_url = install_db_url or db_url + + if check_migrate_databases: + # Initialize database / check for appropriate schema version. # If this + # is a new installation, we'll restrict the tool migration messaging. + from galaxy.model.migrate.check import create_or_verify_database + create_or_verify_database( db_url, config_file, self.config.database_engine_options, app=self ) + if not combined_install_database: + from galaxy.model.tool_shed_install.migrate.check import create_or_verify_database as tsi_create_or_verify_database + tsi_create_or_verify_database( install_db_url, self.config.install_database_engine_options, app=self ) + + if check_migrate_tools: + # Alert the Galaxy admin to tools that have been moved from the distribution to the tool shed. + from tool_shed.galaxy_install.migrate.check import verify_tools + verify_tools( self, install_db_url, config_file, self.config.database_engine_options ) + + from galaxy.model import mapping + self.model = mapping.init( self.config.file_path, + db_url, + self.config.database_engine_options, + map_install_models=combined_install_database, + database_query_profiling_proxy=self.config.database_query_profiling_proxy, + object_store=self.object_store, + trace_logger=getattr(self, "trace_logger", None), + use_pbkdf2=self.config.get_bool( 'use_pbkdf2', True ) ) + + if combined_install_database: + log.info("Install database targetting Galaxy's database configuration.") + self.install_model = self.model + else: + from galaxy.model.tool_shed_install import mapping as install_mapping + install_db_url = self.config.install_database_connection + log.info("Install database using its own connection %s" % install_db_url) + install_db_engine_options = self.config.install_database_engine_options + self.install_model = install_mapping.init( install_db_url, + install_db_engine_options ) diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/datatypes/registry.py --- a/lib/galaxy/datatypes/registry.py +++ b/lib/galaxy/datatypes/registry.py @@ -598,6 +598,9 @@ tool_xml_text = """ <tool id="__SET_METADATA__" name="Set External Metadata" version="1.0.1" tool_type="set_metadata"><type class="SetMetadataTool" module="galaxy.tools"/> + <requirements> + <requirement type="package">samtools</requirement> + </requirements><action module="galaxy.tools.actions.metadata" class="SetMetadataToolAction"/><command>$__SET_EXTERNAL_METADATA_COMMAND_LINE__</command><inputs> diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/jobs/__init__.py --- a/lib/galaxy/jobs/__init__.py +++ b/lib/galaxy/jobs/__init__.py @@ -688,10 +688,7 @@ if self.command_line and self.command_line.startswith( 'python' ): self.galaxy_lib_dir = os.path.abspath( "lib" ) # cwd = galaxy root # Shell fragment to inject dependencies - if self.app.config.use_tool_dependencies: - self.dependency_shell_commands = self.tool.build_dependency_shell_commands() - else: - self.dependency_shell_commands = None + self.dependency_shell_commands = self.tool.build_dependency_shell_commands() # We need command_line persisted to the db in order for Galaxy to re-queue the job # if the server was stopped and restarted before the job finished job.command_line = self.command_line @@ -1435,10 +1432,7 @@ if self.command_line and self.command_line.startswith( 'python' ): self.galaxy_lib_dir = os.path.abspath( "lib" ) # cwd = galaxy root # Shell fragment to inject dependencies - if self.app.config.use_tool_dependencies: - self.dependency_shell_commands = self.tool.build_dependency_shell_commands() - else: - self.dependency_shell_commands = None + self.dependency_shell_commands = self.tool.build_dependency_shell_commands() # We need command_line persisted to the db in order for Galaxy to re-queue the job # if the server was stopped and restarted before the job finished task.command_line = self.command_line diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/jobs/actions/post.py --- a/lib/galaxy/jobs/actions/post.py +++ b/lib/galaxy/jobs/actions/post.py @@ -12,7 +12,7 @@ form = """ if (pja.action_type == "%s"){ p_str = "<div class='pjaForm toolForm'><span class='action_tag' style='display:none'>"+ pja.action_type + pja.output_name + "</span><div class='toolFormTitle'> %s <br/> on " + pja.output_name + "\ - <div style='float: right;' class='buttons'><img src='/static/images/delete_icon.png'></div></div><div class='toolFormBody'>"; + <div style='float: right;' class='buttons'><img src='/static/images/history-buttons/delete_icon.png'></div></div><div class='toolFormBody'>"; %s p_str += "</div><div class='toolParamHelp'>%s</div></div>"; }""" % (action_type, title, content, help) @@ -20,7 +20,7 @@ form = """ if (pja.action_type == "%s"){ p_str = "<div class='pjaForm toolForm'><span class='action_tag' style='display:none'>"+ pja.action_type + "</span><div class='toolFormTitle'> %s \ - <div style='float: right;' class='buttons'><img src='/static/images/delete_icon.png'></div></div><div class='toolFormBody'>"; + <div style='float: right;' class='buttons'><img src='/static/images/history-buttons/delete_icon.png'></div></div><div class='toolFormBody'>"; %s p_str += "</div><div class='toolParamHelp'>%s</div></div>"; }""" % (action_type, title, content, help) diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/jobs/runners/__init__.py --- a/lib/galaxy/jobs/runners/__init__.py +++ b/lib/galaxy/jobs/runners/__init__.py @@ -198,7 +198,7 @@ log.exception( "from_work_dir specified a location not in the working directory: %s, %s" % ( source_file, job_wrapper.working_directory ) ) return output_pairs - def _handle_metadata_externally(self, job_wrapper): + def _handle_metadata_externally( self, job_wrapper, resolve_requirements=False ): """ Set metadata externally. Used by the local and lwr job runners where this shouldn't be attached to command-line to execute. @@ -212,6 +212,12 @@ tmp_dir=job_wrapper.working_directory, #we don't want to overwrite metadata that was copied over in init_meta(), as per established behavior kwds={ 'overwrite' : False } ) + if resolve_requirements: + dependency_shell_commands = self.app.datatypes_registry.set_external_metadata_tool.build_dependency_shell_commands() + if dependency_shell_commands: + if isinstance( dependency_shell_commands, list ): + dependency_shell_commands = "&&".join( dependency_shell_commands ) + external_metadata_script = "%s&&%s" % ( dependency_shell_commands, external_metadata_script ) log.debug( 'executing external set_meta script for job %d: %s' % ( job_wrapper.job_id, external_metadata_script ) ) external_metadata_proc = subprocess.Popen( args=external_metadata_script, shell=True, diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/jobs/runners/local.py --- a/lib/galaxy/jobs/runners/local.py +++ b/lib/galaxy/jobs/runners/local.py @@ -110,7 +110,7 @@ job_wrapper.fail( "failure running job", exception=True ) log.exception("failure running job %d" % job_wrapper.job_id) return - self._handle_metadata_externally( job_wrapper ) + self._handle_metadata_externally( job_wrapper, resolve_requirements=True ) # Finish the job! try: job_wrapper.finish( stdout, stderr, exit_code ) diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/jobs/runners/lwr.py --- a/lib/galaxy/jobs/runners/lwr.py +++ b/lib/galaxy/jobs/runners/lwr.py @@ -204,7 +204,7 @@ log.exception("failure running job %d" % job_wrapper.job_id) return if not LwrJobRunner.__remote_metadata( client ): - self._handle_metadata_externally( job_wrapper ) + self._handle_metadata_externally( job_wrapper, resolve_requirements=True ) # Finish the job try: job_wrapper.finish( stdout, stderr ) diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/model/tool_shed_install/__init__.py --- a/lib/galaxy/model/tool_shed_install/__init__.py +++ b/lib/galaxy/model/tool_shed_install/__init__.py @@ -1,11 +1,11 @@ +import logging import os - from galaxy.model.item_attrs import Dictifiable - from galaxy.util.bunch import Bunch from galaxy.util import asbool +from tool_shed.util import common_util -from tool_shed.util import common_util +log = logging.getLogger( __name__ ) class ToolShedRepository( object ): @@ -61,9 +61,53 @@ def can_reset_metadata( self ): return self.status == self.installation_status.INSTALLED - @property - def can_uninstall( self ): - return self.status != self.installation_status.UNINSTALLED + def can_uninstall( self, app ): + # An installed repository cannot be uninstalled if other installed repositories or installed repository + # contents (i.e., tool dependencies) require it. + if self.status == self.installation_status.UNINSTALLED: + return False + this_repository_tup = ( str( self.tool_shed ), + str( self.name ), + str( self.owner ), + str( self.installed_changeset_revision ) ) + irm = app.installed_repository_manager + # See if this repository's current dependencies are restricted to a single circular relationship. This + # means that this repository has a single repository dependency which itself depends upon this repository. + # The repository dependency may have other repository dependencies, but that is not relevant here. + single_repository_dependency_tup = None + installed_repository_dependency_tups = \ + irm.installed_repository_dependencies_of_installed_repositories.get( this_repository_tup, [] ) + # If this repository defines a circular relationship to another repository, then the list of tuples + # defining installed repository dependencies will include itself. + if len( installed_repository_dependency_tups ) == 2: + if this_repository_tup in installed_repository_dependency_tups: + # We have a single circular dependency definition, so get the other repository. + for installed_repository_dependency_tup in installed_repository_dependency_tups: + if installed_repository_dependency_tup != this_repository_tup: + single_repository_dependency_tup = installed_repository_dependency_tup + break + if single_repository_dependency_tup is not None: + installed_repository_dependency_tups = \ + irm.installed_repository_dependencies_of_installed_repositories.get( this_repository_tup, [] ) + if this_repository_tup in installed_repository_dependency_tups: + # This repository is a dependency of the single repository upon which it depends, so we have + # a single circular relationship and this repository can be uninstalled. + return True + # Find other installed repositories that require this repository. + installed_dependent_repositories = \ + irm.installed_dependent_repositories_of_installed_repositories.get( this_repository_tup, [] ) + if installed_dependent_repositories: + # This repository cannot be uninstalled because other installed repositories require it. + return False + # Find installed tool dependencies that require this repository's installed tool dependencies. + installed_tool_dependencies = irm.installed_tool_dependencies_of_installed_repositories.get( this_repository_tup, [] ) + for td_tup in installed_tool_dependencies: + installed_dependent_td_tups = irm.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies.get( td_tup, [] ) + if installed_dependent_td_tups: + # This repository cannot be uninstalled because it contains installed tool dependencies that + # are required at run time by other installed tool dependencies. + return False + return True @property def can_deactivate( self ): @@ -218,7 +262,7 @@ @property def installed_tool_dependencies( self ): - """Return the repository's tool dependencies that are currently installed.""" + """Return the repository's tool dependencies that are currently installed, but possibly in an error state.""" installed_dependencies = [] for tool_dependency in self.tool_dependencies: if tool_dependency.status in [ ToolDependency.installation_status.INSTALLED, @@ -313,9 +357,9 @@ required_repositories_missing_or_being_installed = [] for required_repository in self.repository_dependencies: if required_repository.status in [ self.installation_status.ERROR, - self.installation_status.INSTALLING, - self.installation_status.NEVER_INSTALLED, - self.installation_status.UNINSTALLED ]: + self.installation_status.INSTALLING, + self.installation_status.NEVER_INSTALLED, + self.installation_status.UNINSTALLED ]: required_repositories_missing_or_being_installed.append( required_repository ) return required_repositories_missing_or_being_installed @@ -507,7 +551,9 @@ @property def can_uninstall( self ): - return self.status in [ self.installation_status.ERROR, self.installation_status.INSTALLED ] + # A tool dependency can be uninstalled only if it is currently in an error state. Only the containing + # repository can be uninstalled if a tool dependency is properly installed. + return self.status in [ self.installation_status.ERROR ] @property def can_update( self ): @@ -516,6 +562,13 @@ self.installation_status.ERROR, self.installation_status.UNINSTALLED ] + def get_env_shell_file_path( self, app ): + installation_directory = self.installation_directory( app ) + file_path = os.path.join( installation_directory, 'env.sh' ) + if os.path.exists( file_path ): + return file_path + return None + @property def in_error_state( self ): return self.status == self.installation_status.ERROR @@ -536,6 +589,10 @@ self.tool_shed_repository.name, self.tool_shed_repository.installed_changeset_revision ) + @property + def is_installed( self ): + return self.status == self.installation_status.INSTALLED + class ToolVersion( object, Dictifiable ): dict_element_visible_keys = ( 'id', 'tool_shed_repository' ) @@ -549,23 +606,23 @@ def get_previous_version( self, app ): context = app.install_model.context tva = context.query( app.install_model.ToolVersionAssociation ) \ - .filter( app.install_model.ToolVersionAssociation.table.c.tool_id == self.id ) \ - .first() + .filter( app.install_model.ToolVersionAssociation.table.c.tool_id == self.id ) \ + .first() if tva: return context.query( app.install_model.ToolVersion ) \ - .filter( app.install_model.ToolVersion.table.c.id == tva.parent_id ) \ - .first() + .filter( app.install_model.ToolVersion.table.c.id == tva.parent_id ) \ + .first() return None def get_next_version( self, app ): context = app.install_model.context tva = context.query( app.install_model.ToolVersionAssociation ) \ - .filter( app.install_model.ToolVersionAssociation.table.c.parent_id == self.id ) \ - .first() + .filter( app.install_model.ToolVersionAssociation.table.c.parent_id == self.id ) \ + .first() if tva: return context.query( app.install_model.ToolVersion ) \ - .filter( app.install_model.ToolVersion.table.c.id == tva.tool_id ) \ - .first() + .filter( app.install_model.ToolVersion.table.c.id == tva.tool_id ) \ + .first() return None def get_versions( self, app ): diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/model/tool_shed_install/mapping.py --- a/lib/galaxy/model/tool_shed_install/mapping.py +++ b/lib/galaxy/model/tool_shed_install/mapping.py @@ -7,10 +7,8 @@ from galaxy.model.base import ModelMapping from galaxy.model.orm.engine_factory import build_engine - metadata = MetaData() - install_model.ToolShedRepository.table = Table( "tool_shed_repository", metadata, Column( "id", Integer, primary_key=True ), Column( "create_time", DateTime, default=now ), @@ -72,7 +70,6 @@ Column( "repository_path", TEXT ), Column( "version", Integer ) ) - mapper( install_model.ToolShedRepository, install_model.ToolShedRepository.table, properties=dict( tool_versions=relation( install_model.ToolVersion, primaryjoin=( install_model.ToolShedRepository.table.c.id == install_model.ToolVersion.table.c.tool_shed_repository_id ), @@ -112,17 +109,13 @@ """Connect mappings to the database""" # Load the appropriate db module engine = build_engine( url, engine_options ) - # Connect the metadata to the database. metadata.bind = engine - - result = ModelMapping([install_model], engine=engine) - + result = ModelMapping( [ install_model ], engine=engine ) # Create tables if needed if create_tables: metadata.create_all() # metadata.engine.commit() - result.create_tables = create_tables #load local galaxy security policy return result diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/tools/__init__.py --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -40,7 +40,7 @@ from galaxy.tools.actions import DefaultToolAction from galaxy.tools.actions.data_source import DataSourceToolAction from galaxy.tools.actions.data_manager import DataManagerToolAction -from galaxy.tools.deps import DependencyManager, INDETERMINATE_DEPENDENCY +from galaxy.tools.deps import build_dependency_manager from galaxy.tools.deps.requirements import parse_requirements_from_xml from galaxy.tools.parameters import check_param, params_from_strings, params_to_strings from galaxy.tools.parameters.basic import (BaseURLToolParameter, @@ -700,14 +700,7 @@ return stored.latest_workflow def init_dependency_manager( self ): - if self.app.config.use_tool_dependencies: - dependency_manager_kwds = { - 'default_base_path': self.app.config.tool_dependency_dir, - 'conf_file': self.app.config.dependency_resolvers_config_file, - } - self.dependency_manager = DependencyManager( **dependency_manager_kwds ) - else: - self.dependency_manager = None + self.dependency_manager = build_dependency_manager( self.app.config ) @property def sa_session( self ): @@ -1082,6 +1075,22 @@ :returns: galaxy.jobs.JobDestination -- The destination definition and runner parameters. """ return self.app.job_config.get_destination(self.__get_job_tool_configuration(job_params=job_params).destination) + + def get_panel_section( self ): + for key, item in self.app.toolbox.integrated_tool_panel.items(): + if item: + if key.startswith( 'tool_' ): + if item.id == self.id: + return '', '' + if key.startswith( 'section_' ): + section_id = item.id or '' + section_name = item.name or '' + for section_key, section_item in item.elems.items(): + if section_key.startswith( 'tool_' ): + if section_item: + if section_item.id == self.id: + return section_id, section_name + return None, None def parse( self, root, guid=None ): """ @@ -3017,6 +3026,8 @@ if io_details: tool_dict[ 'inputs' ] = [ input.to_dict( trans ) for input in self.inputs.values() ] tool_dict[ 'outputs' ] = [ output.to_dict() for output in self.outputs.values() ] + + tool_dict[ 'panel_section_id' ], tool_dict[ 'panel_section_name' ] = self.get_panel_section() return tool_dict diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/tools/deps/__init__.py --- a/lib/galaxy/tools/deps/__init__.py +++ b/lib/galaxy/tools/deps/__init__.py @@ -15,6 +15,31 @@ from galaxy.util.submodules import submodules +def build_dependency_manager( config ): + if config.use_tool_dependencies: + dependency_manager_kwds = { + 'default_base_path': config.tool_dependency_dir, + 'conf_file': config.dependency_resolvers_config_file, + } + dependency_manager = DependencyManager( **dependency_manager_kwds ) + else: + dependency_manager = NullDependencyManager() + + return dependency_manager + + +class NullDependencyManager( object ): + + def uses_tool_shed_dependencies(self): + return False + + def dependency_shell_commands( self, requirements, **kwds ): + return [] + + def find_dep( self, name, version=None, type='package', **kwds ): + return INDETERMINATE_DEPENDENCY + + class DependencyManager( object ): """ A DependencyManager attempts to resolve named and versioned dependencies by @@ -76,6 +101,7 @@ return [ ToolShedPackageDependencyResolver(self), GalaxyPackageDependencyResolver(self), + GalaxyPackageDependencyResolver(self, versionless=True), ] def __parse_resolver_conf_xml(self, root): diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py --- a/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py @@ -265,6 +265,9 @@ if remove_from_disk_checked: tool_shed_repository.status = trans.install_model.ToolShedRepository.installation_status.UNINSTALLED tool_shed_repository.error_message = None + # Remove the uninstalled repository and any tool dependencies from the in-memory dictionaries in the + # installed_repository_manager. + trans.app.installed_repository_manager.handle_repository_uninstall( tool_shed_repository ) else: tool_shed_repository.status = trans.install_model.ToolShedRepository.installation_status.DEACTIVATED trans.install_model.context.add( tool_shed_repository ) @@ -1564,7 +1567,7 @@ tool_shed_repository = tool_dependencies[ 0 ].tool_shed_repository if kwd.get( 'uninstall_tool_dependencies_button', False ): errors = False - # Filter tool dependencies to only those that are installed. + # Filter tool dependencies to only those that are installed but in an error state. tool_dependencies_for_uninstallation = [] for tool_dependency in tool_dependencies: if tool_dependency.can_uninstall: diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/webapps/galaxy/controllers/forms.py --- a/lib/galaxy/webapps/galaxy/controllers/forms.py +++ b/lib/galaxy/webapps/galaxy/controllers/forms.py @@ -65,6 +65,9 @@ global_actions = [ grids.GridAction( "Create new form", dict( controller='forms', action='create_form_definition' ) ) ] + + def build_initial_query( self, trans, **kwargs ): + return trans.sa_session.query( self.model_class ).join (model.FormDefinition, self.model_class.latest_form_id == model.FormDefinition.id) class Forms( BaseUIController ): # Empty TextField diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/webapps/galaxy/controllers/history.py --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -838,8 +838,8 @@ # Set imported history to be user's current history. trans.set_history( new_history ) return trans.show_ok_message( - message="""History "%s" has been imported. <br>You can <a href="%s">start using this history</a> or %s.""" - % ( new_history.name, web.url_for( '/' ), referer_message ), use_panels=True ) + message="""History "%s" has been imported. <br>You can <a href="%s" onclick="parent.window.location='%s';">start using this history</a> or %s.""" + % ( new_history.name, web.url_for( '/' ), web.url_for( '/' ), referer_message ), use_panels=True ) elif not user_history or not user_history.datasets or confirm: new_history = import_history.copy() new_history.name = "imported: " + new_history.name diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/galaxy/webapps/tool_shed/controllers/repository.py --- a/lib/galaxy/webapps/tool_shed/controllers/repository.py +++ b/lib/galaxy/webapps/tool_shed/controllers/repository.py @@ -932,7 +932,7 @@ repository_type = kwd.get( 'repository_type', rt_util.UNRESTRICTED ) if kwd.get( 'create_repository_button', False ): error = False - message = repository_maintenance_util.validate_repository_name( name, trans.user ) + message = repository_maintenance_util.validate_repository_name( trans.app, name, trans.user ) if message: error = True if not description: @@ -2085,7 +2085,7 @@ repository.long_description = long_description flush_needed = True if repository.times_downloaded == 0 and repo_name != repository.name: - message = repository_maintenance_util.validate_repository_name( repo_name, user ) + message = repository_maintenance_util.validate_repository_name( trans.app, repo_name, user ) if message: error = True else: diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/tool_shed/galaxy_install/__init__.py --- a/lib/tool_shed/galaxy_install/__init__.py +++ b/lib/tool_shed/galaxy_install/__init__.py @@ -1,54 +0,0 @@ -""" -Classes encapsulating the management of repositories installed from Galaxy tool sheds. -""" -import os, logging -import tool_shed.util.shed_util_common -import tool_shed.util.datatype_util -from galaxy.model.orm import and_ -from tool_shed.util import xml_util - -log = logging.getLogger( __name__ ) - -class InstalledRepositoryManager( object ): - def __init__( self, app ): - self.app = app - self.install_model = self.app.install_model - self.context = self.install_model.context - self.tool_configs = self.app.config.tool_configs - if self.app.config.migrated_tools_config not in self.tool_configs: - self.tool_configs.append( self.app.config.migrated_tools_config ) - self.installed_repository_dicts = [] - def get_repository_install_dir( self, tool_shed_repository ): - for tool_config in self.tool_configs: - tree, error_message = xml_util.parse_xml( tool_config ) - if tree is None: - return None - root = tree.getroot() - tool_path = root.get( 'tool_path', None ) - if tool_path: - ts = tool_shed.util.shed_util_common.clean_tool_shed_url( tool_shed_repository.tool_shed ) - relative_path = os.path.join( tool_path, - ts, - 'repos', - tool_shed_repository.owner, - tool_shed_repository.name, - tool_shed_repository.installed_changeset_revision ) - if os.path.exists( relative_path ): - return relative_path - return None - def load_proprietary_datatypes( self ): - for tool_shed_repository in self.context.query( self.install_model.ToolShedRepository ) \ - .filter( and_( self.install_model.ToolShedRepository.table.c.includes_datatypes==True, - self.install_model.ToolShedRepository.table.c.deleted==False ) ) \ - .order_by( self.install_model.ToolShedRepository.table.c.id ): - relative_install_dir = self.get_repository_install_dir( tool_shed_repository ) - if relative_install_dir: - installed_repository_dict = tool_shed.util.datatype_util.load_installed_datatypes( self.app, tool_shed_repository, relative_install_dir ) - if installed_repository_dict: - self.installed_repository_dicts.append( installed_repository_dict ) - def load_proprietary_converters_and_display_applications( self, deactivate=False ): - for installed_repository_dict in self.installed_repository_dicts: - if installed_repository_dict[ 'converter_path' ]: - tool_shed.util.datatype_util.load_installed_datatype_converters( self.app, installed_repository_dict, deactivate=deactivate ) - if installed_repository_dict[ 'display_path' ]: - tool_shed.util.datatype_util.load_installed_display_applications( self.app, installed_repository_dict, deactivate=deactivate ) diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/tool_shed/galaxy_install/grids/admin_toolshed_grids.py --- a/lib/tool_shed/galaxy_install/grids/admin_toolshed_grids.py +++ b/lib/tool_shed/galaxy_install/grids/admin_toolshed_grids.py @@ -31,6 +31,13 @@ latest_revision_tip_str = '' return '<img src="%s/june_2007_style/blue/ok_small.png" %s/>' % ( url_for( '/static' ), latest_revision_tip_str ) +def generate_repository_can_be_uninstalled_img_str( include_mouse_over=False ): + if include_mouse_over: + can_be_uninstalled_tip_str = 'class="icon-button" title="This repository can be uninstalled"' + else: + can_be_uninstalled_tip_str = '' + return '<img src="%s/images/fugue/toggle-bw.png" %s/>' % ( url_for( '/static' ), can_be_uninstalled_tip_str ) + def generate_revision_updates_img_str( include_mouse_over=False ): if include_mouse_over: revision_updates_tip_str = 'class="icon-button" title="Updates are available in the Tool Shed for this revision"' @@ -73,6 +80,8 @@ tool_shed_status_str += generate_includes_workflows_img_str( include_mouse_over=True ) else: tool_shed_status_str = generate_unknown_img_str( include_mouse_over=True ) + if tool_shed_repository.can_uninstall( trans.app ): + tool_shed_status_str += generate_repository_can_be_uninstalled_img_str( include_mouse_over=True ) return tool_shed_status_str @@ -210,6 +219,7 @@ legend_str += '%s This repository is deprecated in the Tool Shed<br/>' % generate_deprecated_repository_img_str() legend_str += '%s This repository contains exported workflows<br/>' % generate_includes_workflows_img_str() legend_str += '%s Unable to get information from the Tool Shed<br/>' % generate_unknown_img_str() + legend_str += '%s This repository can be uninstalled<br/>' % generate_repository_can_be_uninstalled_img_str() return legend_str diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/tool_shed/galaxy_install/installed_repository_manager.py --- /dev/null +++ b/lib/tool_shed/galaxy_install/installed_repository_manager.py @@ -0,0 +1,368 @@ +""" +Class encapsulating the management of repositories installed from Galaxy tool sheds. +""" +import logging +import os +import tool_shed.util.shed_util_common as suc +from tool_shed.util import datatype_util +from tool_shed.util import repository_dependency_util +from tool_shed.util import tool_dependency_util +from tool_shed.util import xml_util +from galaxy.model.orm import and_ + +log = logging.getLogger( __name__ ) + + +class InstalledRepositoryManager( object ): + + def __init__( self, app ): + """ + Among other things, keep in in-memory sets of tuples defining installed repositories and tool dependencies along with + the relationships between each of them. This will allow for quick discovery of those repositories or components that + can be uninstalled. The feature allowing a Galaxy administrator to uninstall a repository should not be available to + repositories or tool dependency packages that are required by other repositories or their contents (packages). The + uninstall feature should be available only at the repository hierarchy level where every dependency will be uninstalled. + The exception for this is if an item (repository or tool dependency package) is not in an INSTALLED state - in these + cases, the specific item can be uninstalled in order to attempt re-installation. + """ + self.app = app + self.install_model = self.app.install_model + self.context = self.install_model.context + self.tool_configs = self.app.config.tool_configs + if self.app.config.migrated_tools_config not in self.tool_configs: + self.tool_configs.append( self.app.config.migrated_tools_config ) + self.installed_repository_dicts = [] + # Keep an in-memory dictionary whose keys are tuples defining tool_shed_repository objects (whose status is 'Installed') + # and whose values are a list of tuples defining tool_shed_repository objects (whose status can be anything) required by + # the key. The value defines the entire repository dependency tree. + self.repository_dependencies_of_installed_repositories = {} + # Keep an in-memory dictionary whose keys are tuples defining tool_shed_repository objects (whose status is 'Installed') + # and whose values are a list of tuples defining tool_shed_repository objects (whose status is 'Installed') required by + # the key. The value defines the entire repository dependency tree. + self.installed_repository_dependencies_of_installed_repositories = {} + # Keep an in-memory dictionary whose keys are tuples defining tool_shed_repository objects (whose status is 'Installed') + # and whose values are a list of tuples defining tool_shed_repository objects (whose status is 'Installed') that require + # the key. + self.installed_dependent_repositories_of_installed_repositories = {} + # Keep an in-memory dictionary whose keys are tuples defining tool_shed_repository objects (whose status is 'Installed') + # and whose values are a list of tuples defining its immediate tool_dependency objects (whose status can be anything). + # The value defines only the immediate tool dependencies of the repository and does not include any dependencies of the + # tool dependencies. + self.tool_dependencies_of_installed_repositories = {} + # Keep an in-memory dictionary whose keys are tuples defining tool_shed_repository objects (whose status is 'Installed') + # and whose values are a list of tuples defining its immediate tool_dependency objects (whose status is 'Installed'). + # The value defines only the immediate tool dependencies of the repository and does not include any dependencies of the + # tool dependencies. + self.installed_tool_dependencies_of_installed_repositories = {} + # Keep an in-memory dictionary whose keys are tuples defining tool_dependency objects (whose status is 'Installed') and + # whose values are a list of tuples defining tool_dependency objects (whose status can be anything) required by the + # installed tool dependency at runtime. The value defines the entire tool dependency tree. + self.runtime_tool_dependencies_of_installed_tool_dependencies = {} + # Keep an in-memory dictionary whose keys are tuples defining tool_dependency objects (whose status is 'Installed') and + # whose values are a list of tuples defining tool_dependency objects (whose status is 'Installed') that require the key + # at runtime. The value defines the entire tool dependency tree. + self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies = {} + # Load defined dependency relationships for installed tool shed repositories and their contents. + self.load_dependency_relationships() + + def add_entry_to_installed_repository_dependencies_of_installed_repositories( self, repository ): + """ + Add an entry to self.installed_repository_dependencies_of_installed_repositories. A side-effect of this method + is the population of self.installed_dependent_repositories_of_installed_repositories. Since this method discovers + all repositories required by the received repository, it can use the list to add entries to the reverse dictionary. + """ + repository_tup = repository_dependency_util.get_repository_tuple_for_installed_repository_manager( repository ) + tool_shed, name, owner, installed_changeset_revision = repository_tup + # Get the list of repository dependencies for this repository. + status = self.install_model.ToolShedRepository.installation_status.INSTALLED + repository_dependency_tups = \ + repository_dependency_util.get_repository_dependency_tups_for_repository( self.app, repository, status=status ) + # Add an entry to self.installed_repository_dependencies_of_installed_repositories. + if repository_tup not in self.installed_repository_dependencies_of_installed_repositories: + debug_msg = "Adding an entry for revision %s of repository %s owned by %s " % ( installed_changeset_revision, name, owner ) + debug_msg += "to installed_repository_dependencies_of_installed_repositories." + log.debug( debug_msg ) + self.installed_repository_dependencies_of_installed_repositories[ repository_tup ] = repository_dependency_tups + # Use the repository_dependency_tups to add entries to the reverse dictionary + # self.installed_dependent_repositories_of_installed_repositories. + for required_repository_tup in repository_dependency_tups: + debug_msg = "Appending revision %s of repository %s owned by %s " % ( installed_changeset_revision, name, owner ) + debug_msg += "to all dependent repositories in installed_dependent_repositories_of_installed_repositories." + log.debug( debug_msg ) + if required_repository_tup in self.installed_dependent_repositories_of_installed_repositories: + self.installed_dependent_repositories_of_installed_repositories[ required_repository_tup ].append( repository_tup ) + else: + self.installed_dependent_repositories_of_installed_repositories[ required_repository_tup ] = [ repository_tup ] + + def add_entry_to_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies( self, tool_dependency ): + """Add an entry to self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies.""" + tool_dependency_tup = tool_dependency_util.get_tool_dependency_tuple_for_installed_repository_manager( tool_dependency ) + if tool_dependency_tup not in self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies: + tool_shed_repository_id, name, version, type = tool_dependency_tup + debug_msg = "Adding an entry for version %s of %s %s " % ( version, type, name ) + debug_msg += "to installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies." + log.debug( debug_msg ) + status = self.install_model.ToolDependency.installation_status.INSTALLED + installed_runtime_dependent_tool_dependency_tups = \ + tool_dependency_util.get_runtime_dependent_tool_dependency_tuples( self.app, tool_dependency, status=status ) + self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies[ tool_dependency_tup ] = \ + installed_runtime_dependent_tool_dependency_tups + + def add_entry_to_installed_tool_dependencies_of_installed_repositories( self, repository ): + """Add an entry to self.installed_tool_dependencies_of_installed_repositories.""" + repository_tup = repository_dependency_util.get_repository_tuple_for_installed_repository_manager( repository ) + if repository_tup not in self.installed_tool_dependencies_of_installed_repositories: + tool_shed, name, owner, installed_changeset_revision = repository_tup + debug_msg = "Adding an entry for revision %s of repository %s owned by %s " % ( installed_changeset_revision, name, owner ) + debug_msg += "to installed_tool_dependencies_of_installed_repositories." + log.debug( debug_msg ) + installed_tool_dependency_tups = [] + for tool_dependency in repository.tool_dependencies: + if tool_dependency.status == self.app.install_model.ToolDependency.installation_status.INSTALLED: + tool_dependency_tup = tool_dependency_util.get_tool_dependency_tuple_for_installed_repository_manager( tool_dependency ) + installed_tool_dependency_tups.append( tool_dependency_tup ) + self.installed_tool_dependencies_of_installed_repositories[ repository_tup ] = installed_tool_dependency_tups + + def add_entry_to_repository_dependencies_of_installed_repositories( self, repository ): + """Add an entry to self.repository_dependencies_of_installed_repositories.""" + repository_tup = repository_dependency_util.get_repository_tuple_for_installed_repository_manager( repository ) + if repository_tup not in self.repository_dependencies_of_installed_repositories: + tool_shed, name, owner, installed_changeset_revision = repository_tup + debug_msg = "Adding an entry for revision %s of repository %s owned by %s " % ( installed_changeset_revision, name, owner ) + debug_msg += "to repository_dependencies_of_installed_repositories." + log.debug( debug_msg ) + repository_dependency_tups = \ + repository_dependency_util.get_repository_dependency_tups_for_repository( self.app, repository, status=None ) + self.repository_dependencies_of_installed_repositories[ repository_tup ] = repository_dependency_tups + + def add_entry_to_runtime_tool_dependencies_of_installed_tool_dependencies( self, tool_dependency ): + """Add an entry to self.runtime_tool_dependencies_of_installed_tool_dependencies.""" + tool_dependency_tup = tool_dependency_util.get_tool_dependency_tuple_for_installed_repository_manager( tool_dependency ) + if tool_dependency_tup not in self.runtime_tool_dependencies_of_installed_tool_dependencies: + tool_shed_repository_id, name, version, type = tool_dependency_tup + debug_msg = "Adding an entry for version %s of %s %s " % ( version, type, name ) + debug_msg += "to runtime_tool_dependencies_of_installed_tool_dependencies." + log.debug( debug_msg ) + runtime_dependent_tool_dependency_tups = \ + tool_dependency_util.get_runtime_dependent_tool_dependency_tuples( self.app, tool_dependency, status=None ) + self.runtime_tool_dependencies_of_installed_tool_dependencies[ tool_dependency_tup ] = \ + runtime_dependent_tool_dependency_tups + + def add_entry_to_tool_dependencies_of_installed_repositories( self, repository ): + """Add an entry to self.tool_dependencies_of_installed_repositories.""" + repository_tup = repository_dependency_util.get_repository_tuple_for_installed_repository_manager( repository ) + if repository_tup not in self.tool_dependencies_of_installed_repositories: + tool_shed, name, owner, installed_changeset_revision = repository_tup + debug_msg = "Adding an entry for revision %s of repository %s owned by %s " % ( installed_changeset_revision, name, owner ) + debug_msg += "to tool_dependencies_of_installed_repositories." + log.debug( debug_msg ) + tool_dependency_tups = [] + for tool_dependency in repository.tool_dependencies: + tool_dependency_tup = tool_dependency_util.get_tool_dependency_tuple_for_installed_repository_manager( tool_dependency ) + tool_dependency_tups.append( tool_dependency_tup ) + self.tool_dependencies_of_installed_repositories[ repository_tup ] = tool_dependency_tups + + def get_containing_repository_for_tool_dependency( self, tool_dependency_tup ): + tool_shed_repository_id, name, version, type = tool_dependency_tup + return self.app.install_model.context.query( self.app.install_model.ToolShedRepository ).get( tool_shed_repository_id ) + + def get_repository_install_dir( self, tool_shed_repository ): + for tool_config in self.tool_configs: + tree, error_message = xml_util.parse_xml( tool_config ) + if tree is None: + return None + root = tree.getroot() + tool_path = root.get( 'tool_path', None ) + if tool_path: + ts = suc.clean_tool_shed_url( tool_shed_repository.tool_shed ) + relative_path = os.path.join( tool_path, + ts, + 'repos', + str( tool_shed_repository.owner ), + str( tool_shed_repository.name ), + str( tool_shed_repository.installed_changeset_revision ) ) + if os.path.exists( relative_path ): + return relative_path + return None + + def handle_repository_install( self, repository ): + """Load the dependency relationships for a repository that was just installed or reinstalled.""" + # Populate self.repository_dependencies_of_installed_repositories. + self.add_entry_to_repository_dependencies_of_installed_repositories( repository ) + # Populate self.installed_repository_dependencies_of_installed_repositories. + self.add_entry_to_installed_repository_dependencies_of_installed_repositories( repository ) + # Populate self.tool_dependencies_of_installed_repositories. + self.add_entry_to_tool_dependencies_of_installed_repositories( repository ) + # Populate self.installed_tool_dependencies_of_installed_repositories. + self.add_entry_to_installed_tool_dependencies_of_installed_repositories( repository ) + for tool_dependency in repository.tool_dependencies: + # Populate self.runtime_tool_dependencies_of_installed_tool_dependencies. + self.add_entry_to_runtime_tool_dependencies_of_installed_tool_dependencies( tool_dependency ) + # Populate self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies. + self.add_entry_to_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies( tool_dependency ) + + def handle_repository_uninstall( self, repository ): + """Remove the dependency relationships for a repository that was just uninstalled.""" + for tool_dependency in repository.tool_dependencies: + tool_dependency_tup = tool_dependency_util.get_tool_dependency_tuple_for_installed_repository_manager( tool_dependency ) + # Remove this tool_dependency from all values in + # self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies + altered_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies = {} + for td_tup, installed_runtime_dependent_tool_dependency_tups in \ + self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies.items(): + if tool_dependency_tup in installed_runtime_dependent_tool_dependency_tups: + # Remove the tool_dependency from the list. + installed_runtime_dependent_tool_dependency_tups.remove( tool_dependency_tup ) + # Add the possibly altered list to the altered dictionary. + altered_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies[ td_tup ] = \ + installed_runtime_dependent_tool_dependency_tups + self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies = \ + altered_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies + # Remove the entry for this tool_dependency from self.runtime_tool_dependencies_of_installed_tool_dependencies. + self.remove_entry_from_runtime_tool_dependencies_of_installed_tool_dependencies( tool_dependency ) + # Remove the entry for this tool_dependency from + # self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies. + self.remove_entry_from_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies( tool_dependency ) + # Remove this repository's entry from self.installed_tool_dependencies_of_installed_repositories. + self.remove_entry_from_installed_tool_dependencies_of_installed_repositories( repository ) + # Remove this repository's entry from self.tool_dependencies_of_installed_repositories + self.remove_entry_from_tool_dependencies_of_installed_repositories( repository ) + # Remove this repository's entry from self.installed_repository_dependencies_of_installed_repositories. + self.remove_entry_from_installed_repository_dependencies_of_installed_repositories( repository ) + # Remove this repository's entry from self.repository_dependencies_of_installed_repositories. + self.remove_entry_from_repository_dependencies_of_installed_repositories( repository ) + + def handle_tool_dependency_install( self, repository, tool_dependency ): + """Load the dependency relationships for a tool dependency that was just installed independently of its containing repository.""" + # The received repository must have a status of 'Installed'. The value of tool_dependency.status will either be + # 'Installed' or 'Error', but we only need to change the in-memory dictionaries if it is 'Installed'. + if tool_dependency.is_installed: + # Populate self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies. + self.add_entry_to_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies( tool_dependency ) + # Populate self.installed_tool_dependencies_of_installed_repositories. + repository_tup = repository_dependency_util.get_repository_tuple_for_installed_repository_manager( repository ) + tool_dependency_tup = tool_dependency_util.get_tool_dependency_tuple_for_installed_repository_manager( tool_dependency ) + if repository_tup in self.installed_tool_dependencies_of_installed_repositories: + self.installed_tool_dependencies_of_installed_repositories[ repository_tup ].append( tool_dependency_tup ) + else: + self.installed_tool_dependencies_of_installed_repositories[ repository_tup ] = [ tool_dependency_tup ] + + def load_dependency_relationships( self ): + """Load relationships for all installed repositories and tool dependencies into in-memnory dictionaries.""" + # Get the list of installed tool shed repositories. + for repository in self.context.query( self.app.install_model.ToolShedRepository ) \ + .filter( self.app.install_model.ToolShedRepository.table.c.status == + self.app.install_model.ToolShedRepository.installation_status.INSTALLED ): + # Populate self.repository_dependencies_of_installed_repositories. + self.add_entry_to_repository_dependencies_of_installed_repositories( repository ) + # Populate self.installed_repository_dependencies_of_installed_repositories. + self.add_entry_to_installed_repository_dependencies_of_installed_repositories( repository ) + # Populate self.tool_dependencies_of_installed_repositories. + self.add_entry_to_tool_dependencies_of_installed_repositories( repository ) + # Populate self.installed_tool_dependencies_of_installed_repositories. + self.add_entry_to_installed_tool_dependencies_of_installed_repositories( repository ) + # Get the list of installed tool dependencies. + for tool_dependency in self.context.query( self.app.install_model.ToolDependency ) \ + .filter( self.app.install_model.ToolDependency.table.c.status == + self.app.install_model.ToolDependency.installation_status.INSTALLED ): + # Populate self.runtime_tool_dependencies_of_installed_tool_dependencies. + self.add_entry_to_runtime_tool_dependencies_of_installed_tool_dependencies( tool_dependency ) + # Populate self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies. + self.add_entry_to_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies( tool_dependency ) + + def load_proprietary_datatypes( self ): + for tool_shed_repository in self.context.query( self.install_model.ToolShedRepository ) \ + .filter( and_( self.install_model.ToolShedRepository.table.c.includes_datatypes==True, + self.install_model.ToolShedRepository.table.c.deleted==False ) ) \ + .order_by( self.install_model.ToolShedRepository.table.c.id ): + relative_install_dir = self.get_repository_install_dir( tool_shed_repository ) + if relative_install_dir: + installed_repository_dict = datatype_util.load_installed_datatypes( self.app, tool_shed_repository, relative_install_dir ) + if installed_repository_dict: + self.installed_repository_dicts.append( installed_repository_dict ) + + def load_proprietary_converters_and_display_applications( self, deactivate=False ): + for installed_repository_dict in self.installed_repository_dicts: + if installed_repository_dict[ 'converter_path' ]: + datatype_util.load_installed_datatype_converters( self.app, installed_repository_dict, deactivate=deactivate ) + if installed_repository_dict[ 'display_path' ]: + datatype_util.load_installed_display_applications( self.app, installed_repository_dict, deactivate=deactivate ) + + def remove_entry_from_installed_repository_dependencies_of_installed_repositories( self, repository ): + """ + Remove an entry from self.installed_repository_dependencies_of_installed_repositories. A side-effect of this method + is removal of appropriate value items from self.installed_dependent_repositories_of_installed_repositories. + """ + # Remove tuples defining this repository from value lists in self.installed_dependent_repositories_of_installed_repositories. + repository_tup = repository_dependency_util.get_repository_tuple_for_installed_repository_manager( repository ) + tool_shed, name, owner, installed_changeset_revision = repository_tup + altered_installed_dependent_repositories_of_installed_repositories = {} + for r_tup, v_tups in self.installed_dependent_repositories_of_installed_repositories.items(): + if repository_tup in v_tups: + debug_msg = "Removing entry for revision %s of repository %s owned by %s " % \ + ( installed_changeset_revision, name, owner ) + r_tool_shed, r_name, r_owner, r_installed_changeset_revision = r_tup + debug_msg += "from the dependent list for revision %s of repository %s owned by %s " % \ + ( r_installed_changeset_revision, r_name, r_owner ) + debug_msg += "in installed_repository_dependencies_of_installed_repositories." + log.debug( debug_msg ) + v_tups.remove( repository_tup ) + altered_installed_dependent_repositories_of_installed_repositories[ r_tup ] = v_tups + self.installed_dependent_repositories_of_installed_repositories = \ + altered_installed_dependent_repositories_of_installed_repositories + # Remove this repository's entry from self.installed_repository_dependencies_of_installed_repositories. + if repository_tup in self.installed_repository_dependencies_of_installed_repositories: + debug_msg = "Removing entry for revision %s of repository %s owned by %s " % ( installed_changeset_revision, name, owner ) + debug_msg += "from installed_repository_dependencies_of_installed_repositories." + log.debug( debug_msg ) + del self.installed_repository_dependencies_of_installed_repositories[ repository_tup ] + + def remove_entry_from_installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies( self, tool_dependency ): + """Remove an entry from self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies.""" + tool_dependency_tup = tool_dependency_util.get_tool_dependency_tuple_for_installed_repository_manager( tool_dependency ) + if tool_dependency_tup in self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies: + tool_shed_repository_id, name, version, type = tool_dependency_tup + debug_msg = "Removing entry for version %s of %s %s " % ( version, type, name ) + debug_msg += "from installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies." + log.debug( debug_msg ) + del self.installed_runtime_dependent_tool_dependencies_of_installed_tool_dependencies[ tool_dependency_tup ] + + def remove_entry_from_installed_tool_dependencies_of_installed_repositories( self, repository ): + """Remove an entry from self.installed_tool_dependencies_of_installed_repositories.""" + repository_tup = repository_dependency_util.get_repository_tuple_for_installed_repository_manager( repository ) + if repository_tup in self.installed_tool_dependencies_of_installed_repositories: + tool_shed, name, owner, installed_changeset_revision = repository_tup + debug_msg = "Removing entry for revision %s of repository %s owned by %s " % ( installed_changeset_revision, name, owner ) + debug_msg += "from installed_tool_dependencies_of_installed_repositories." + log.debug( debug_msg ) + del self.installed_tool_dependencies_of_installed_repositories[ repository_tup ] + + def remove_entry_from_repository_dependencies_of_installed_repositories( self, repository ): + """Remove an entry from self.repository_dependencies_of_installed_repositories.""" + repository_tup = repository_dependency_util.get_repository_tuple_for_installed_repository_manager( repository ) + if repository_tup in self.repository_dependencies_of_installed_repositories: + tool_shed, name, owner, installed_changeset_revision = repository_tup + debug_msg = "Removing entry for revision %s of repository %s owned by %s " % ( installed_changeset_revision, name, owner ) + debug_msg += "from repository_dependencies_of_installed_repositories." + log.debug( debug_msg ) + del self.repository_dependencies_of_installed_repositories[ repository_tup ] + + def remove_entry_from_runtime_tool_dependencies_of_installed_tool_dependencies( self, tool_dependency ): + """Remove an entry from self.runtime_tool_dependencies_of_installed_tool_dependencies.""" + tool_dependency_tup = tool_dependency_util.get_tool_dependency_tuple_for_installed_repository_manager( tool_dependency ) + if tool_dependency_tup in self.runtime_tool_dependencies_of_installed_tool_dependencies: + tool_shed_repository_id, name, version, type = tool_dependency_tup + debug_msg = "Removing entry for version %s of %s %s from runtime_tool_dependencies_of_installed_tool_dependencies." % \ + ( version, type, name ) + log.debug( debug_msg ) + del self.runtime_tool_dependencies_of_installed_tool_dependencies[ tool_dependency_tup ] + + def remove_entry_from_tool_dependencies_of_installed_repositories( self, repository ): + """Remove an entry from self.tool_dependencies_of_installed_repositories.""" + repository_tup = repository_dependency_util.get_repository_tuple_for_installed_repository_manager( repository ) + if repository_tup in self.tool_dependencies_of_installed_repositories: + tool_shed, name, owner, installed_changeset_revision = repository_tup + debug_msg = "Removing entry for revision %s of repository %s owned by %s from tool_dependencies_of_installed_repositories." % \ + ( installed_changeset_revision, name, owner ) + log.debug( debug_msg ) + del self.tool_dependencies_of_installed_repositories[ repository_tup ] diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/tool_shed/galaxy_install/migrate/common.py --- a/lib/tool_shed/galaxy_install/migrate/common.py +++ b/lib/tool_shed/galaxy_install/migrate/common.py @@ -2,18 +2,10 @@ import os import sys import galaxy.config -import galaxy.datatypes.registry -from galaxy import tools -from galaxy.tools.data import ToolDataTableManager -from galaxy.web import security -import galaxy.model.mapping -import galaxy.tools.search -from galaxy.objectstore import build_object_store_from_config -import tool_shed.tool_shed_registry from tool_shed.galaxy_install import install_manager -class MigrateToolsApplication( object ): +class MigrateToolsApplication( object, galaxy.config.ConfiguresGalaxyMixin ): """Encapsulates the state of a basic Galaxy Universe application in order to initiate the Install Manager""" def __init__( self, tools_migration_config ): @@ -33,41 +25,23 @@ for key, value in config_parser.items( "app:main" ): galaxy_config_dict[ key ] = value self.config = galaxy.config.Configuration( **galaxy_config_dict ) - if not self.config.database_connection: - self.config.database_connection = "sqlite:///%s?isolation_level=IMMEDIATE" % self.config.database + self.config.update_integrated_tool_panel = True - self.object_store = build_object_store_from_config( self.config ) - # Security helper - self.security = security.SecurityHelper( id_secret=self.config.id_secret ) - # Setup the database engine and ORM - self.model = galaxy.model.mapping.init( self.config.file_path, - self.config.database_connection, - engine_options={}, - create_tables=False, - object_store=self.object_store ) - # Create an empty datatypes registry. - self.datatypes_registry = galaxy.datatypes.registry.Registry() - # Load the data types in the Galaxy distribution, which are defined in self.config.datatypes_config. - self.datatypes_registry.load_datatypes( self.config.root, self.config.datatypes_config ) - # Initialize tool data tables using the config defined by self.config.tool_data_table_config_path. - self.tool_data_tables = ToolDataTableManager( tool_data_path=self.config.tool_data_path, - config_filename=self.config.tool_data_table_config_path ) - # Load additional entries defined by self.config.shed_tool_data_table_config into tool data tables. - self.tool_data_tables.load_from_config_file( config_filename=self.config.shed_tool_data_table_config, - tool_data_path=self.tool_data_tables.tool_data_path, - from_shed_config=True ) - # Initialize the tools, making sure the list of tool configs includes the reserved migrated_tools_conf.xml file. - tool_configs = self.config.tool_configs - if self.config.migrated_tools_config not in tool_configs: - tool_configs.append( self.config.migrated_tools_config ) - self.toolbox = tools.ToolBox( tool_configs, self.config.tool_path, self ) - # Search support for tools - self.toolbox_search = galaxy.tools.search.ToolBoxSearch( self.toolbox ) - # Set up the tool sheds registry. - if os.path.isfile( self.config.tool_sheds_config ): - self.tool_shed_registry = tool_shed.tool_shed_registry.Registry( self.config.root, self.config.tool_sheds_config ) - else: - self.tool_shed_registry = None + + self._configure_object_store() + + self._configure_security() + + self._configure_models() + + self._configure_datatypes_registry( ) + + self._configure_tool_data_tables( from_shed_config=True ) + + self._configure_toolbox() + + self._configure_tool_shed_registry() + # Get the latest tool migration script number to send to the Install manager. latest_migration_script_number = int( tools_migration_config.split( '_' )[ 0 ] ) # The value of migrated_tools_config is migrated_tools_conf.xml, and is reserved for containing only those tools that have been diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/tool_shed/galaxy_install/repository_util.py --- a/lib/tool_shed/galaxy_install/repository_util.py +++ b/lib/tool_shed/galaxy_install/repository_util.py @@ -504,8 +504,8 @@ query = trans.install_model.context.query( trans.install_model.ToolShedRepository ).filter( or_( *clause_list ) ) return encoded_kwd, query, tool_shed_repositories, encoded_repository_ids -def install_tool_shed_repository( trans, tool_shed_repository, repo_info_dict, tool_panel_section_key, shed_tool_conf, tool_path, install_tool_dependencies, - reinstalling=False ): +def install_tool_shed_repository( trans, tool_shed_repository, repo_info_dict, tool_panel_section_key, shed_tool_conf, tool_path, + install_tool_dependencies, reinstalling=False ): if tool_panel_section_key: try: tool_section = trans.app.toolbox.tool_panel[ tool_panel_section_key ] @@ -579,7 +579,11 @@ tool_dependencies_config=tool_dependencies_config, tool_dependencies=tool_shed_repository.tool_dependencies ) suc.remove_dir( work_dir ) - suc.update_tool_shed_repository_status( trans.app, tool_shed_repository, trans.install_model.ToolShedRepository.installation_status.INSTALLED ) + suc.update_tool_shed_repository_status( trans.app, + tool_shed_repository, + trans.install_model.ToolShedRepository.installation_status.INSTALLED ) + # Add the installed repository and any tool dependencies to the in-memory dictionaries in the installed_repository_manager. + trans.app.installed_repository_manager.handle_repository_install( tool_shed_repository ) else: # An error occurred while cloning the repository, so reset everything necessary to enable another attempt. set_repository_attributes( trans, diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/tool_shed/galaxy_install/update_manager.py --- a/lib/tool_shed/galaxy_install/update_manager.py +++ b/lib/tool_shed/galaxy_install/update_manager.py @@ -30,7 +30,7 @@ # repository. This information includes items like newer installable repository revisions, current revision updates, whether # the repository revision is the latest installable revision, and whether the repository has been deprecated in the tool shed. for repository in self.context.query( self.app.install_model.ToolShedRepository ) \ - .filter( self.app.install_model.ToolShedRepository.table.c.deleted == False ): + .filter( self.app.install_model.ToolShedRepository.table.c.deleted == False ): tool_shed_status_dict = suc.get_tool_shed_status_for_installed_repository( self.app, repository ) if tool_shed_status_dict: if tool_shed_status_dict != repository.tool_shed_status: diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/tool_shed/scripts/check_repositories_for_functional_tests.py --- a/lib/tool_shed/scripts/check_repositories_for_functional_tests.py +++ b/lib/tool_shed/scripts/check_repositories_for_functional_tests.py @@ -253,6 +253,11 @@ # a test_environment entry. If we use it we need to temporarily eliminate it from the list of tool_test_results_dicts # since it will be re-inserted later. tool_test_results_dict = tool_test_results_dicts.pop( 0 ) + elif len( tool_test_results_dict ) == 2 and \ + 'test_environment' in tool_test_results_dict and 'missing_test_components' in tool_test_results_dict: + # We can re-use tool_test_results_dict if its only entries are "test_environment" and "missing_test_components". + # In this case, some tools are missing tests components while others are not. + tool_test_results_dict = tool_test_results_dicts.pop( 0 ) else: # The latest tool_test_results_dict has been populated with the results of a test run, so it cannot be used. tool_test_results_dict = {} diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/tool_shed/scripts/check_tool_dependency_definition_repositories.py --- a/lib/tool_shed/scripts/check_tool_dependency_definition_repositories.py +++ b/lib/tool_shed/scripts/check_tool_dependency_definition_repositories.py @@ -41,6 +41,7 @@ class RepositoriesApplication( object ): """Encapsulates the state of a Universe application""" + def __init__( self, config ): if config.database_connection is False: config.database_connection = "sqlite:///%s?isolation_level=IMMEDIATE" % config.database @@ -133,11 +134,12 @@ # Do not check metadata records that have an entry in the skip_tool_tests table, since they won't be tested anyway. skip_metadata_ids = select( [ app.model.SkipToolTest.table.c.repository_metadata_id ] ) # Get the list of metadata records to check, restricting it to records that have not been flagged do_not_test. - for repository_metadata in app.sa_session.query( app.model.RepositoryMetadata ) \ - .filter( and_( app.model.RepositoryMetadata.table.c.downloadable == True, - app.model.RepositoryMetadata.table.c.do_not_test == False, - app.model.RepositoryMetadata.table.c.repository_id.in_( tool_dependency_defintion_repository_ids ), - not_( app.model.RepositoryMetadata.table.c.id.in_( skip_metadata_ids ) ) ) ): + for repository_metadata in \ + app.sa_session.query( app.model.RepositoryMetadata ) \ + .filter( and_( app.model.RepositoryMetadata.table.c.downloadable == True, + app.model.RepositoryMetadata.table.c.do_not_test == False, + app.model.RepositoryMetadata.table.c.repository_id.in_( tool_dependency_defintion_repository_ids ), + not_( app.model.RepositoryMetadata.table.c.id.in_( skip_metadata_ids ) ) ) ): records_checked += 1 # Check the next repository revision. changeset_revision = str( repository_metadata.changeset_revision ) @@ -161,9 +163,31 @@ invalid_metadata += 1 if not info_only: # Create the tool_test_results_dict dictionary, using the dictionary from the previous test run if available. - if repository_metadata.tool_test_results: - tool_test_results_dict = repository_metadata.tool_test_results + if repository_metadata.tool_test_results is not None: + # We'll listify the column value in case it uses the old approach of storing the results of only a single test run. + tool_test_results_dicts = listify( repository_metadata.tool_test_results ) else: + tool_test_results_dicts = [] + if tool_test_results_dicts: + # Inspect the tool_test_results_dict for the last test run in case it contains only a test_environment + # entry. This will occur with multiple runs of this script without running the associated + # install_and_test_tool_sed_repositories.sh script which will further populate the tool_test_results_dict. + tool_test_results_dict = tool_test_results_dicts[ 0 ] + if len( tool_test_results_dict ) <= 1: + # We can re-use the mostly empty tool_test_results_dict for this run because it is either empty or it contains only + # a test_environment entry. If we use it we need to temporarily eliminate it from the list of tool_test_results_dicts + # since it will be re-inserted later. + tool_test_results_dict = tool_test_results_dicts.pop( 0 ) + elif len( tool_test_results_dict ) == 2 and \ + 'test_environment' in tool_test_results_dict and 'missing_test_components' in tool_test_results_dict: + # We can re-use tool_test_results_dict if its only entries are "test_environment" and "missing_test_components". + # In this case, some tools are missing tests components while others are not. + tool_test_results_dict = tool_test_results_dicts.pop( 0 ) + else: + # The latest tool_test_results_dict has been populated with the results of a test run, so it cannot be used. + tool_test_results_dict = {} + else: + # Create a new dictionary for the most recent test run. tool_test_results_dict = {} # Initialize the tool_test_results_dict dictionary with the information about the current test environment. test_environment_dict = tool_test_results_dict.get( 'test_environment', {} ) diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/tool_shed/util/common_install_util.py --- a/lib/tool_shed/util/common_install_util.py +++ b/lib/tool_shed/util/common_install_util.py @@ -71,8 +71,8 @@ def get_dependencies_for_repository( trans, tool_shed_url, repo_info_dict, includes_tool_dependencies ): """ - Return dictionaries containing the sets of installed and missing tool dependencies and repository dependencies associated with the repository defined - by the received repo_info_dict. + Return dictionaries containing the sets of installed and missing tool dependencies and repository dependencies associated + with the repository defined by the received repo_info_dict. """ repository = None installed_rd = {} @@ -102,7 +102,8 @@ # Discover all repository dependencies and retrieve information for installing them. all_repo_info_dict = get_required_repo_info_dicts( trans, tool_shed_url, util.listify( repo_info_dict ) ) has_repository_dependencies = all_repo_info_dict.get( 'has_repository_dependencies', False ) - has_repository_dependencies_only_if_compiling_contained_td = all_repo_info_dict.get( 'has_repository_dependencies_only_if_compiling_contained_td', False ) + has_repository_dependencies_only_if_compiling_contained_td = \ + all_repo_info_dict.get( 'has_repository_dependencies_only_if_compiling_contained_td', False ) includes_tools_for_display_in_tool_panel = all_repo_info_dict.get( 'includes_tools_for_display_in_tool_panel', False ) includes_tool_dependencies = all_repo_info_dict.get( 'includes_tool_dependencies', False ) includes_tools = all_repo_info_dict.get( 'includes_tools', False ) @@ -138,7 +139,8 @@ # We have a single repository with (possibly) no defined repository dependencies. all_repo_info_dict = get_required_repo_info_dicts( trans, tool_shed_url, util.listify( repo_info_dict ) ) has_repository_dependencies = all_repo_info_dict.get( 'has_repository_dependencies', False ) - has_repository_dependencies_only_if_compiling_contained_td = all_repo_info_dict.get( 'has_repository_dependencies_only_if_compiling_contained_td', False ) + has_repository_dependencies_only_if_compiling_contained_td = \ + all_repo_info_dict.get( 'has_repository_dependencies_only_if_compiling_contained_td', False ) includes_tools_for_display_in_tool_panel = all_repo_info_dict.get( 'includes_tools_for_display_in_tool_panel', False ) includes_tool_dependencies = all_repo_info_dict.get( 'includes_tool_dependencies', False ) includes_tools = all_repo_info_dict.get( 'includes_tools', False ) @@ -202,8 +204,8 @@ # Get the description from the metadata in case it has a value. repository_dependencies = metadata.get( 'repository_dependencies', {} ) description = repository_dependencies.get( 'description', None ) - # We need to add a root_key entry to one or both of installed_repository_dependencies dictionary and the missing_repository_dependencies - # dictionaries for proper display parsing. + # We need to add a root_key entry to one or both of installed_repository_dependencies dictionary and the + # missing_repository_dependencies dictionaries for proper display parsing. root_key = container_util.generate_repository_dependencies_key_for_repository( repository.tool_shed, repository.name, repository.owner, @@ -460,7 +462,7 @@ if tool_dependency.can_install: # The database record is currently in a state that allows us to install the package on the file system. try: - dependencies_ignored = app.toolbox.dependency_manager and not app.toolbox.dependency_manager.uses_tool_shed_dependencies() + dependencies_ignored = not app.toolbox.dependency_manager.uses_tool_shed_dependencies() if dependencies_ignored: log.debug( "Skipping package %s because tool shed dependency resolver not enabled." % str( package_name ) ) # Tool dependency resolves have been configured and they do not include the tool shed. Do not install package. @@ -489,6 +491,8 @@ if tool_dependency and tool_dependency.status in [ app.install_model.ToolDependency.installation_status.INSTALLED, app.install_model.ToolDependency.installation_status.ERROR ]: installed_tool_dependencies.append( tool_dependency ) + # Add the tool_dependency to the in-memory dictionaries in the installed_repository_manager. + app.installed_repository_manager.handle_tool_dependency_install( tool_shed_repository, tool_dependency ) elif elem.tag == 'set_environment': # <set_environment version="1.0"> # <environment_variable name="R_SCRIPT_PATH"action="set_to">$REPOSITORY_INSTALL_DIR</environment_variable> diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/tool_shed/util/container_util.py --- a/lib/tool_shed/util/container_util.py +++ b/lib/tool_shed/util/container_util.py @@ -5,7 +5,6 @@ from galaxy.web.framework.helpers import time_ago from tool_shed.util import common_util from tool_shed.util import readme_util -from tool_shed.util.tool_dependency_util import tool_dependency_is_orphan import tool_shed.util.shed_util_common as suc log = logging.getLogger( __name__ ) @@ -269,7 +268,7 @@ """Tool dependency object""" def __init__( self, id=None, name=None, version=None, type=None, readme=None, installation_status=None, repository_id=None, - tool_dependency_id=None, is_orphan=None ): + tool_dependency_id=None ): self.id = id self.name = name self.version = version @@ -278,15 +277,6 @@ self.installation_status = installation_status self.repository_id = repository_id self.tool_dependency_id = tool_dependency_id - # The designation of a ToolDependency into the "orphan" category has evolved over time, and is significantly restricted since the - # introduction of the TOOL_DEPENDENCY_DEFINITION repository type. This designation is still critical, however, in that it handles - # the case where a repository contains both tools and a tool_dependencies.xml file, but the definition in the tool_dependencies.xml - # file is in no way related to anything defined by any of the contained tool's requirements tag sets. This is important in that it - # is often a result of a typo (e.g., dependency name or version) that differs between the tool dependency definition within the - # tool_dependencies.xml file and what is defined in the tool config's <requirements> tag sets. In these cases, the user should be - # presented with a warning message, and this warning message is is in fact displayed if the following is_orphan attribute is True. - # This is tricky because in some cases it may be intentional, and tool dependencies that are categorized as "orphan" are in fact valid. - self.is_orphan = is_orphan @property def listify( self ): @@ -318,42 +308,6 @@ self.repository_metadata_id = repository_metadata_id self.repository_id = repository_id -def add_orphan_settings_to_tool_dependencies( tool_dependencies, tools ): - """Inspect all received tool dependencies and label those that are orphans within the repository.""" - #orphan_env_dependencies = orphan_tool_dependencies.get( 'set_environment', None ) - new_tool_dependencies = {} - for td_key, requirements_dict in tool_dependencies.items(): - if td_key in [ 'set_environment' ]: - # "set_environment": [{"name": "R_SCRIPT_PATH", "type": "set_environment"}] - new_set_environment_dict_list = [] - for env_requirements_dict in requirements_dict: - try: - name = env_requirements_dict[ 'name' ] - type = env_requirements_dict[ 'type' ] - if tool_dependency_is_orphan( type, name, None, tools ): - env_requirements_dict[ 'is_orphan' ] = True - except Exception, e: - name = str( e ) - type = 'unknown' - is_orphan = 'unknown' - new_set_environment_dict_list.append( env_requirements_dict ) - new_tool_dependencies[ td_key ] = new_set_environment_dict_list - else: - # {"R/2.15.1": {"name": "R", "readme": "some string", "type": "package", "version": "2.15.1"} - try: - name = requirements_dict[ 'name' ] - type = requirements_dict[ 'type' ] - version = requirements_dict[ 'version'] - if tool_dependency_is_orphan( type, name, version, tools ): - requirements_dict[ 'is_orphan' ] = True - except Exception, e: - name = str( e ) - type = 'unknown' - version = 'unknown' - is_orphan = 'unknown' - new_tool_dependencies[ td_key ] = requirements_dict - return new_tool_dependencies - def build_data_managers_folder( trans, folder_id, data_managers, label=None ): """Return a folder hierarchy containing Data Managers.""" if data_managers: @@ -849,8 +803,6 @@ if 'tools' not in exclude: tools = metadata.get( 'tools', [] ) tools.extend( metadata.get( 'invalid_tools', [] ) ) - if tools: - tool_dependencies = add_orphan_settings_to_tool_dependencies( tool_dependencies, tools ) folder_id, tool_dependencies_root_folder = build_tool_dependencies_folder( trans, folder_id, tool_dependencies, @@ -1055,13 +1007,6 @@ tool_dependency_id += 1 if dependency_key in [ 'set_environment' ]: for set_environment_dict in requirements_dict: - if trans.webapp.name == 'tool_shed': - is_orphan = set_environment_dict.get( 'is_orphan', False ) - else: - # This is probably not necessary to display in Galaxy. - is_orphan = False - if is_orphan: - folder.description = not_used_by_local_tools_description try: name = set_environment_dict.get( 'name', None ) type = set_environment_dict[ 'type' ] @@ -1086,16 +1031,9 @@ readme=None, installation_status=installation_status, repository_id=repository_id, - tool_dependency_id=td_id, - is_orphan=is_orphan ) + tool_dependency_id=td_id ) folder.tool_dependencies.append( tool_dependency ) else: - if trans.webapp.name == 'tool_shed': - is_orphan = requirements_dict.get( 'is_orphan', False ) - else: - is_orphan = False - if is_orphan: - folder.description = not_used_by_local_tools_description try: name = requirements_dict[ 'name' ] version = requirements_dict[ 'version' ] @@ -1122,8 +1060,7 @@ readme=None, installation_status=installation_status, repository_id=repository_id, - tool_dependency_id=td_id, - is_orphan=is_orphan ) + tool_dependency_id=td_id ) folder.tool_dependencies.append( tool_dependency ) else: tool_dependencies_root_folder = None diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/tool_shed/util/metadata_util.py --- a/lib/tool_shed/util/metadata_util.py +++ b/lib/tool_shed/util/metadata_util.py @@ -940,7 +940,12 @@ for required_file in ttb.required_files: value, extra = required_file required_files.append( ( value ) ) - inputs = [ input for input in ttb.inputs ] + inputs = [] + for input_field, values in ttb.inputs.iteritems(): + if len( values ) == 1: + inputs.append( ( input_field, values[0] ) ) + else: + inputs.append( ( input_field, values ) ) outputs = [] for output in ttb.outputs: name, file_name, extra = output diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/tool_shed/util/repository_dependency_util.py --- a/lib/tool_shed/util/repository_dependency_util.py +++ b/lib/tool_shed/util/repository_dependency_util.py @@ -355,14 +355,53 @@ handled_key_rd_dicts, circular_repository_dependencies ) elif key_rd_dicts_to_be_processed: - handle_next_repository_dependency( trans, key_rd_dicts_to_be_processed, all_repository_dependencies, handled_key_rd_dicts, circular_repository_dependencies ) + handle_next_repository_dependency( trans, + key_rd_dicts_to_be_processed, + all_repository_dependencies, + handled_key_rd_dicts, + circular_repository_dependencies ) elif key_rd_dicts_to_be_processed: - handle_next_repository_dependency( trans, key_rd_dicts_to_be_processed, all_repository_dependencies, handled_key_rd_dicts, circular_repository_dependencies ) + handle_next_repository_dependency( trans, + key_rd_dicts_to_be_processed, + all_repository_dependencies, + handled_key_rd_dicts, + circular_repository_dependencies ) elif key_rd_dicts_to_be_processed: - handle_next_repository_dependency( trans, key_rd_dicts_to_be_processed, all_repository_dependencies, handled_key_rd_dicts, circular_repository_dependencies ) + handle_next_repository_dependency( trans, + key_rd_dicts_to_be_processed, + all_repository_dependencies, + handled_key_rd_dicts, + circular_repository_dependencies ) all_repository_dependencies = prune_invalid_repository_dependencies( all_repository_dependencies ) return all_repository_dependencies +def get_repository_dependency_tups_for_repository( app, repository, dependency_tups=None, status=None ): + """ + Return a list of of tuples defining tool_shed_repository objects (whose status can be anything) required by the + received repository. The returned list defines the entire repository dependency tree. + """ + if dependency_tups is None: + dependency_tups = [] + repository_tup = get_repository_tuple_for_installed_repository_manager( repository ) + for rrda in repository.required_repositories: + repository_dependency = rrda.repository_dependency + required_repository = repository_dependency.repository + if status is None or required_repository.status == status: + required_repository_tup = get_repository_tuple_for_installed_repository_manager( required_repository ) + if required_repository_tup == repository_tup: + # We have a circular repository dependency relationship, skip this entry. + continue + if required_repository_tup not in dependency_tups: + dependency_tups.append( required_repository_tup ) + return get_repository_dependency_tups_for_repository( app, required_repository, dependency_tups=dependency_tups ) + return dependency_tups + +def get_repository_tuple_for_installed_repository_manager( repository ): + return ( str( repository.tool_shed ), + str( repository.name ), + str( repository.owner ), + str( repository.installed_changeset_revision ) ) + def get_updated_changeset_revisions_for_repository_dependencies( trans, key_rd_dicts ): updated_key_rd_dicts = [] for key_rd_dict in key_rd_dicts: @@ -373,9 +412,10 @@ if suc.tool_shed_is_this_tool_shed( rd_toolshed ): repository = suc.get_repository_by_name_and_owner( trans.app, rd_name, rd_owner ) if repository: - repository_metadata = metadata_util.get_repository_metadata_by_repository_id_changeset_revision( trans, - trans.security.encode_id( repository.id ), - rd_changeset_revision ) + repository_metadata = \ + metadata_util.get_repository_metadata_by_repository_id_changeset_revision( trans, + trans.security.encode_id( repository.id ), + rd_changeset_revision ) if repository_metadata: # The repository changeset_revision is installable, so no updates are available. new_key_rd_dict = {} diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/tool_shed/util/repository_maintenance_util.py --- a/lib/tool_shed/util/repository_maintenance_util.py +++ b/lib/tool_shed/util/repository_maintenance_util.py @@ -137,7 +137,7 @@ results_message += 'This Tool Shed does not have the category <b>%s</b> so it will not be associated with this repository.' % \ str( category_name ) else: - category_ids.append( trans.security.encode_id( category.id ) ) + category_ids.append( trans.security.encode_id( category.id ) ) # Create the repository record in the database. repository, create_message = create_repository( trans, name, @@ -157,15 +157,18 @@ import_results_tups.append( ( ( str( name ), str( username ) ), results_message ) ) return import_results_tups -def validate_repository_name( name, user ): +def validate_repository_name( app, name, user ): # Repository names must be unique for each user, must be at least four characters # in length and must contain only lower-case letters, numbers, and the '_' character. if name in [ 'None', None, '' ]: return 'Enter the required repository name.' if name in [ 'repos' ]: return "The term <b>%s</b> is a reserved word in the tool shed, so it cannot be used as a repository name." % name - for repository in user.active_repositories: - if repository.name == name: + check_existing = suc.get_repository_by_name_and_owner( app, name, user.username ) + if check_existing is not None: + if check_existing.deleted: + return 'You have a deleted repository named <b>%s</b>, so choose a different name.' % name + else: return "You already have a repository named <b>%s</b>, so choose a different name." % name if len( name ) < 4: return "Repository names must be at least 4 characters in length." diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a lib/tool_shed/util/tool_dependency_util.py --- a/lib/tool_shed/util/tool_dependency_util.py +++ b/lib/tool_shed/util/tool_dependency_util.py @@ -4,6 +4,7 @@ from galaxy import eggs from galaxy import util from galaxy.model.orm import and_ +from galaxy.model.orm import or_ import tool_shed.util.shed_util_common as suc import tool_shed.repository_types.util as rt_util from tool_shed.util import xml_util @@ -13,9 +14,10 @@ def add_installation_directories_to_tool_dependencies( trans, tool_dependencies ): """ - Determine the path to the installation directory for each of the received tool dependencies. This path will be displayed within the tool dependencies - container on the select_tool_panel_section or reselect_tool_panel_section pages when installing or reinstalling repositories that contain tools with the - defined tool dependencies. The list of tool dependencies may be associated with more than a single repository. + Determine the path to the installation directory for each of the received tool dependencies. This path will be + displayed within the tool dependencies container on the select_tool_panel_section or reselect_tool_panel_section + pages when installing or reinstalling repositories that contain tools with the defined tool dependencies. The + list of tool dependencies may be associated with more than a single repository. """ for dependency_key, requirements_dict in tool_dependencies.items(): if dependency_key in [ 'set_environment' ]: @@ -64,7 +66,10 @@ return tool_dependency def create_tool_dependency_objects( app, tool_shed_repository, relative_install_dir, set_status=True ): - """Create or update a ToolDependency for each entry in tool_dependencies_config. This method is called when installing a new tool_shed_repository.""" + """ + Create or update a ToolDependency for each entry in tool_dependencies_config. This method is called when + installing a new tool_shed_repository. + """ tool_dependency_objects = [] shed_config_dict = tool_shed_repository.get_shed_config_dict( app ) if shed_config_dict.get( 'tool_path' ): @@ -82,12 +87,13 @@ name = elem.get( 'name', None ) version = elem.get( 'version', None ) if name and version: + status = app.install_model.ToolDependency.installation_status.NEVER_INSTALLED tool_dependency = create_or_update_tool_dependency( app, tool_shed_repository, name=name, version=version, type=tool_dependency_type, - status=app.install_model.ToolDependency.installation_status.NEVER_INSTALLED, + status=status, set_status=set_status ) tool_dependency_objects.append( tool_dependency ) elif tool_dependency_type == 'set_environment': @@ -96,12 +102,13 @@ name = env_elem.get( 'name', None ) action = env_elem.get( 'action', None ) if name and action: + status = app.install_model.ToolDependency.installation_status.NEVER_INSTALLED tool_dependency = create_or_update_tool_dependency( app, tool_shed_repository, name=name, version=None, type=tool_dependency_type, - status=app.install_model.ToolDependency.installation_status.NEVER_INSTALLED, + status=status, set_status=set_status ) tool_dependency_objects.append( tool_dependency ) return tool_dependency_objects @@ -185,12 +192,14 @@ def generate_message_for_repository_type_change( trans, repository ): message = '' if repository.can_change_type_to( trans.app, rt_util.TOOL_DEPENDENCY_DEFINITION ): - tool_dependency_definition_type_class = trans.app.repository_types_registry.get_class_by_label( rt_util.TOOL_DEPENDENCY_DEFINITION ) - message += "This repository currently contains a single file named <b>%s</b>. If additional files will " % suc.TOOL_DEPENDENCY_DEFINITION_FILENAME - message += "not be added to this repository, then it's type should be set to <b>%s</b>.<br/>" % tool_dependency_definition_type_class.label + tool_dependency_definition_type_class = \ + trans.app.repository_types_registry.get_class_by_label( rt_util.TOOL_DEPENDENCY_DEFINITION ) + message += "This repository currently contains a single file named <b>%s</b>. If additional files will " % \ + suc.TOOL_DEPENDENCY_DEFINITION_FILENAME + message += "not be added to this repository, then it's type should be set to <b>%s</b>.<br/>" % \ + tool_dependency_definition_type_class.label return message - - + def get_download_url_for_platform( url_templates, platform_info_dict ): ''' Compare the dict returned by get_platform_info() with the values specified in the url_template element. Return @@ -289,6 +298,31 @@ env_sh_file_path = os.path.join( env_sh_file_dir, 'env.sh' ) return env_sh_file_path +def get_runtime_dependent_tool_dependency_tuples( app, tool_dependency, status=None ): + """ + Return the list of tool dependency objects that require the received tool dependency at run time. The returned + list will be filtered by the received status if it is not None. This method is called only from Galaxy. + """ + runtime_dependent_tool_dependency_tups = [] + required_env_shell_file_path = tool_dependency.get_env_shell_file_path( app ) + if required_env_shell_file_path: + required_env_shell_file_path = os.path.abspath( required_env_shell_file_path ) + if required_env_shell_file_path is not None: + for td in app.install_model.context.query( app.install_model.ToolDependency ): + if status is None or td.status == status: + env_shell_file_path = td.get_env_shell_file_path( app ) + if env_shell_file_path is not None: + try: + contents = open( env_shell_file_path, 'r' ).read() + except Exception, e: + contents = None + log.debug( 'Error reading file %s, so cannot determine if package %s requires package %s at run time: %s' % \ + ( str( env_shell_file_path ), str( td.name ), str( tool_dependency.name ), str( e ) ) ) + if contents is not None and contents.find( required_env_shell_file_path ) >= 0: + td_tuple = get_tool_dependency_tuple_for_installed_repository_manager( td ) + runtime_dependent_tool_dependency_tups.append( td_tuple ) + return runtime_dependent_tool_dependency_tups + def get_tool_dependency( trans, id ): """Get a tool_dependency from the database via id""" return trans.install_model.context.query( trans.install_model.ToolDependency ).get( trans.security.decode_id( id ) ) @@ -353,6 +387,13 @@ repository_name, repository_changeset_revision ) ) +def get_tool_dependency_tuple_for_installed_repository_manager( tool_dependency ): + if tool_dependency.type is None: + type = None + else: + type = str( tool_dependency.type ) + return ( tool_dependency.tool_shed_repository_id, str( tool_dependency.name ), str( tool_dependency.version ), type ) + def handle_tool_dependency_installation_error( app, tool_dependency, error_message, remove_installation_path=False ): # Since there was an installation error, remove the installation directory because the install_package method uses # this: "if os.path.exists( install_dir ):". Setting remove_installation_path to True should rarely occur. It is @@ -479,6 +520,7 @@ return installed_tool_dependencies, missing_tool_dependencies def remove_tool_dependency( app, tool_dependency ): + """The received tool_dependency must be in an error state.""" context = app.install_model.context dependency_install_dir = tool_dependency.installation_directory( app ) removed, error_message = remove_tool_dependency_installation_directory( dependency_install_dir ) @@ -487,6 +529,9 @@ tool_dependency.error_message = None context.add( tool_dependency ) context.flush() + # Since the received tool_dependency is in an error state, nothing will need to be changed in any + # of the in-memory dictionaries in the installed_repository_manager because changing the state from + # error to uninstalled requires no in-memory changes.. return removed, error_message def remove_tool_dependency_installation_directory( dependency_install_dir ): diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -360,7 +360,7 @@ workflow.has_changes = true; canvas_manager.draw_overview(); }))) - .tooltip({delay:500, title: "Flag this as a workflow output. All non-flagged outputs will be hidden." }); + .tooltip({delay:500, title: "Mark dataset as a workflow output. All unmarked datasets will be hidden." }); callout.css({ top: '50%', margin:'-8px 0px 0px 0px', diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a static/scripts/mvc/base-mvc.js --- a/static/scripts/mvc/base-mvc.js +++ b/static/scripts/mvc/base-mvc.js @@ -173,6 +173,7 @@ //============================================================================== var HiddenUntilActivatedViewMixin = /** @lends hiddenUntilActivatedMixin# */{ +//TODO: since this is a mixin, consider moving toggle, hidden into HUAVOptions /** */ hiddenUntilActivated : function( $activator, options ){ @@ -184,7 +185,9 @@ showSpeed : 'fast' }; _.extend( this.HUAVOptions, options || {}); + /** has this been shown already (and onshowFirstTime called)? */ this.HUAVOptions.hasBeenShown = this.HUAVOptions.$elementShown.is( ':visible' ); + this.hidden = this.isHidden(); if( $activator ){ var mixin = this; @@ -194,21 +197,36 @@ } }, + isHidden : function(){ + return ( this.HUAVOptions.$elementShown.is( ':hidden' ) ); + }, + /** */ toggle : function(){ // can be called manually as well with normal toggle arguments - if( this.HUAVOptions.$elementShown.is( ':hidden' ) ){ + //TODO: better as a callback (when the show/hide is actually done) + // show + if( this.hidden ){ // fire the optional fns on the first/each showing - good for render() if( !this.HUAVOptions.hasBeenShown ){ if( _.isFunction( this.HUAVOptions.onshowFirstTime ) ){ this.HUAVOptions.hasBeenShown = true; this.HUAVOptions.onshowFirstTime.call( this ); } - } else { - if( _.isFunction( this.HUAVOptions.onshow ) ){ - this.HUAVOptions.onshow.call( this ); - } } + if( _.isFunction( this.HUAVOptions.onshow ) ){ + this.HUAVOptions.onshow.call( this ); + this.trigger( 'hiddenUntilActivated:shown', this ); + } + this.hidden = false; + + // hide + } else { + if( _.isFunction( this.HUAVOptions.onhide ) ){ + this.HUAVOptions.onhide.call( this ); + this.trigger( 'hiddenUntilActivated:hidden', this ); + } + this.hidden = true; } return this.HUAVOptions.showFn.apply( this.HUAVOptions.$elementShown, arguments ); } diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 5463295a3981c8487effbbbaa127fe028737491a static/scripts/mvc/dataset/hda-base.js --- a/static/scripts/mvc/dataset/hda-base.js +++ b/static/scripts/mvc/dataset/hda-base.js @@ -46,11 +46,11 @@ /** is the view currently in selection mode? */ this.selectable = attributes.selectable || false; /** is the view currently selected? */ - this.selected = attributes.selected || false; - /** is the body of this hda view expanded/not. */ - this.expanded = attributes.expanded || false; - /** is the body of this hda view expanded/not. */ - this.draggable = attributes.draggable || false; + this.selected = attributes.selected || false; + /** is the body of this hda view expanded/not? */ + this.expanded = attributes.expanded || false; + /** is the body of this hda view expanded/not? */ + this.draggable = attributes.draggable || false; this._setUpListeners(); }, @@ -423,7 +423,7 @@ toggleBodyVisibility : function( event, expand ){ // bail (with propagation) if keydown and not space or enter var KEYCODE_SPACE = 32, KEYCODE_RETURN = 13; - if( ( event.type === 'keydown' ) + if( event && ( event.type === 'keydown' ) && !( event.keyCode === KEYCODE_SPACE || event.keyCode === KEYCODE_RETURN ) ){ return true; } This diff is so big that we needed to truncate the remainder. https://bitbucket.org/galaxy/galaxy-central/commits/70aca9eee9d4/ Changeset: 70aca9eee9d4 User: martenson Date: 2013-12-16 00:22:01 Summary: activation link tweak: show proper message when trying to activate already active account Affected #: 1 file diff -r 0522f7504094bd5be2be97846e695a0c790bf929 -r 70aca9eee9d442a88e49dc99e08ff3b6bbea73d0 lib/galaxy/webapps/galaxy/controllers/user.py --- a/lib/galaxy/webapps/galaxy/controllers/user.py +++ b/lib/galaxy/webapps/galaxy/controllers/user.py @@ -826,19 +826,22 @@ if email is None or activation_token is None: # We don't have the email or activation_token, show error. - return trans.show_error_message( "You are using wrong activation link. Try to log-in and we will send you a new activation email.<br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller="root", action="index" ) + return trans.show_error_message( "You are using wrong activation link. Try to log-in and we will send you a new activation email. <br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller="root", action="index" ) else: # Find the user user = trans.sa_session.query( trans.app.model.User ).filter( trans.app.model.User.table.c.email==email ).first() + # If the user is active already don't try to activate + if user.active == True: + return trans.show_ok_message( "Your account is already active. Nothing has changed. <br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) if user.activation_token == activation_token: user.activation_token = None user.active = True trans.sa_session.add(user) trans.sa_session.flush() - return trans.show_ok_message( "Your account has been successfully activated!<br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) + return trans.show_ok_message( "Your account has been successfully activated! <br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) else: # Tokens don't match. Activation is denied. - return trans.show_error_message( "You are using wrong activation link. Try to log in and we will send you a new activation email.<br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) + return trans.show_error_message( "You are using wrong activation link. Try to log in and we will send you a new activation email. <br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) return def __get_user_type_form_definition( self, trans, user=None, **kwd ): https://bitbucket.org/galaxy/galaxy-central/commits/d5be36722eb3/ Changeset: d5be36722eb3 Branch: data_library User: martenson Date: 2013-12-16 17:14:59 Summary: Closed branch data_library Affected #: 0 files https://bitbucket.org/galaxy/galaxy-central/commits/2b83e7980707/ Changeset: 2b83e7980707 User: martenson Date: 2013-12-16 17:42:28 Summary: closing data_library branch Affected #: 0 files https://bitbucket.org/galaxy/galaxy-central/commits/0b2f53ad1a20/ Changeset: 0b2f53ad1a20 User: martenson Date: 2013-12-16 18:01:35 Summary: Merge own default 2nd head Affected #: 1 file diff -r 2b83e798070736ea7279d5e4e33194f66f8ef7df -r 0b2f53ad1a2025dbfd7c15a76f329fc5e8a859ef lib/galaxy/webapps/galaxy/controllers/user.py --- a/lib/galaxy/webapps/galaxy/controllers/user.py +++ b/lib/galaxy/webapps/galaxy/controllers/user.py @@ -826,19 +826,22 @@ if email is None or activation_token is None: # We don't have the email or activation_token, show error. - return trans.show_error_message( "You are using wrong activation link. Try to log-in and we will send you a new activation email.<br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller="root", action="index" ) + return trans.show_error_message( "You are using wrong activation link. Try to log-in and we will send you a new activation email. <br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller="root", action="index" ) else: # Find the user user = trans.sa_session.query( trans.app.model.User ).filter( trans.app.model.User.table.c.email==email ).first() + # If the user is active already don't try to activate + if user.active == True: + return trans.show_ok_message( "Your account is already active. Nothing has changed. <br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) if user.activation_token == activation_token: user.activation_token = None user.active = True trans.sa_session.add(user) trans.sa_session.flush() - return trans.show_ok_message( "Your account has been successfully activated!<br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) + return trans.show_ok_message( "Your account has been successfully activated! <br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) else: # Tokens don't match. Activation is denied. - return trans.show_error_message( "You are using wrong activation link. Try to log in and we will send you a new activation email.<br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) + return trans.show_error_message( "You are using wrong activation link. Try to log in and we will send you a new activation email. <br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) return def __get_user_type_form_definition( self, trans, user=None, **kwd ): https://bitbucket.org/galaxy/galaxy-central/commits/16d53da082b8/ Changeset: 16d53da082b8 User: martenson Date: 2013-12-17 19:22:55 Summary: added security checks when downloading lddas in archives Affected #: 1 file diff -r 0b2f53ad1a2025dbfd7c15a76f329fc5e8a859ef -r 16d53da082b89db1318a2a688aa8a18470b02f59 lib/galaxy/webapps/galaxy/api/lda_datasets.py --- a/lib/galaxy/webapps/galaxy/api/lda_datasets.py +++ b/lib/galaxy/webapps/galaxy/api/lda_datasets.py @@ -13,15 +13,17 @@ import urllib import urllib2 import zipfile +from paste.httpexceptions import HTTPBadRequest +from galaxy.exceptions import ItemAccessibilityException, MessageException, ItemDeletionException from galaxy.security import Action from galaxy import util, web from galaxy.util.streamball import StreamBall -from galaxy.web.base.controller import BaseAPIController, UsesLibraryMixinItems +from galaxy.web.base.controller import BaseAPIController, UsesVisualizationMixin import logging log = logging.getLogger( __name__ ) -class DatasetsController( BaseAPIController, UsesLibraryMixinItems ): +class LibraryDatasetsController( BaseAPIController, UsesVisualizationMixin ): @web.expose_api def show( self, trans, id, **kwd ): @@ -31,7 +33,7 @@ """ # Get dataset. try: - dataset = self.get_library_dataset( trans, id = id ) + dataset = self.get_library_dataset( trans, id = id, check_ownership=False, check_accessible=True ) except Exception, e: return str( e ) try: @@ -65,12 +67,22 @@ datasets_to_download = util.listify( datasets_to_download ) for dataset_id in datasets_to_download: try: - ldda = trans.sa_session.query( trans.app.model.LibraryDatasetDatasetAssociation ).get( trans.security.decode_id( dataset_id ) ) - assert not ldda.dataset.purged + ldda = self.get_hda_or_ldda( trans, hda_ldda='ldda', dataset_id=dataset_id ) lddas.append( ldda ) - except: - ldda = None - message += "Invalid library dataset id (%s) specified. " % str( dataset_id ) + except ItemAccessibilityException: + trans.response.status = 403 + return 'Insufficient rights to access library dataset with id: (%s)' % str( dataset_id ) + except MessageException: + trans.response.status = 400 + return 'Wrong library dataset id: (%s)' % str( dataset_id ) + except ItemDeletionException: + trans.response.status = 400 + return 'The item with library dataset id: (%s) is deleted' % str( dataset_id ) + except HTTPBadRequest, e: + return 'http bad request' + str( e.err_msg ) + except Exception, e: + trans.response.status = 500 + return 'error of unknown kind' + str( e ) if format in [ 'zip','tgz','tbz' ]: error = False https://bitbucket.org/galaxy/galaxy-central/commits/2b4877fc1024/ Changeset: 2b4877fc1024 User: martenson Date: 2013-12-17 19:35:08 Summary: exception catching rewritten, it is now behaving better, not good but better than before Affected #: 1 file diff -r 16d53da082b89db1318a2a688aa8a18470b02f59 -r 2b4877fc1024180fadb5128187d447c0b84c7cd9 lib/galaxy/webapps/galaxy/api/lda_datasets.py --- a/lib/galaxy/webapps/galaxy/api/lda_datasets.py +++ b/lib/galaxy/webapps/galaxy/api/lda_datasets.py @@ -1,5 +1,5 @@ """ -API operations on the dataset from library. +API operations on the datasets from library. """ import glob import logging @@ -35,6 +35,7 @@ try: dataset = self.get_library_dataset( trans, id = id, check_ownership=False, check_accessible=True ) except Exception, e: + trans.response.status = 500 return str( e ) try: # Default: return dataset as dict. @@ -43,24 +44,22 @@ rval = "Error in dataset API at listing contents: " + str( e ) log.error( rval + ": %s" % str(e), exc_info=True ) trans.response.status = 500 + return "Error in dataset API at listing contents: " + str( e ) rval['id'] = trans.security.encode_id(rval['id']); rval['ldda_id'] = trans.security.encode_id(rval['ldda_id']); rval['folder_id'] = 'f' + trans.security.encode_id(rval['folder_id']) - + trans.response.status = 200 return rval @web.expose def download( self, trans, format, **kwd ): """ - POST /api/libraries/datasets/download/{format} - POST data: ldda_ids = [] - Downloads dataset(s) in the requested format. + GET /api/libraries/datasets/download/{format} + GET multiple params: ldda_ids = [] + Downloads dataset(s) in the requested format or plain. """ lddas = [] -# is_admin = trans.user_is_admin() -# current_user_roles = trans.get_current_user_roles() - datasets_to_download = kwd['ldda_ids%5B%5D'] if ( datasets_to_download != None ): @@ -111,15 +110,13 @@ archive = StreamBall( 'w|bz2' ) outext = 'tbz2' except ( OSError, zipfile.BadZipfile ): - error = True log.exception( "Unable to create archive for download" ) - message = "Unable to create archive for download, please report this error" - status = 'error' + trans.response.status = 500 + return "Unable to create archive for download, please report this error" except: - error = True log.exception( "Unexpected error %s in create archive for download" % sys.exc_info()[0] ) - message = "Unable to create archive for download, please report - %s" % sys.exc_info()[0] - status = 'error' + trans.response.status = 500 + return "Unable to create archive for download, please report - %s" % sys.exc_info()[0] if not error: composite_extensions = trans.app.datatypes_registry.get_composite_extensions() seen = [] @@ -150,11 +147,9 @@ try: archive.add(ldda.dataset.file_name,zpath) # add the primary of a composite set except IOError: - error = True log.exception( "Unable to add composite parent %s to temporary library download archive" % ldda.dataset.file_name) - message = "Unable to create archive for download, please report this error" - status = 'error' - continue + trans.response.status = 500 + return "Unable to create archive for download, please report this error" flist = glob.glob(os.path.join(ldda.dataset.extra_files_path,'*.*')) # glob returns full paths for fpath in flist: efp,fname = os.path.split(fpath) @@ -163,19 +158,16 @@ try: archive.add( fpath,fname ) except IOError: - error = True log.exception( "Unable to add %s to temporary library download archive %s" % (fname,outfname)) - message = "Unable to create archive for download, please report this error" - status = 'error' - continue + trans.response.status = 500 + return "Unable to create archive for download, please report this error" else: # simple case try: archive.add( ldda.dataset.file_name, path ) except IOError: - error = True log.exception( "Unable to write %s to temporary library download archive" % ldda.dataset.file_name) - message = "Unable to create archive for download, please report this error" - status = 'error' + trans.response.status = 500 + return "Unable to create archive for download, please report this error" if not error: lname = 'selected_dataset' fname = lname.replace( ' ', '_' ) + '_files' @@ -186,15 +178,18 @@ archive = util.streamball.ZipBall(tmpf, tmpd) archive.wsgi_status = trans.response.wsgi_status() archive.wsgi_headeritems = trans.response.wsgi_headeritems() + trans.response.status = 200 return archive.stream else: trans.response.set_content_type( "application/x-tar" ) trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) archive.wsgi_status = trans.response.wsgi_status() archive.wsgi_headeritems = trans.response.wsgi_headeritems() + trans.response.status = 200 return archive.stream elif format == 'uncompressed': if len(lddas) != 1: + trans.response.status = 400 return 'Wrong request' else: single_dataset = lddas[0] @@ -206,8 +201,11 @@ fname = ''.join( c in valid_chars and c or '_' for c in fname )[ 0:150 ] trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s"' % fname try: + trans.response.status = 200 return open( single_dataset.file_name ) except: + trans.response.status = 500 return 'This dataset contains no content' else: - return 'Wrong format'; + trans.response.status = 400 + return 'Wrong format parameter specified'; https://bitbucket.org/galaxy/galaxy-central/commits/6f89cbaeb29c/ Changeset: 6f89cbaeb29c User: martenson Date: 2013-12-17 21:58:40 Summary: fix for errors thrown when requested underlying file of dataset does not exist; even better exception handling for ldda-archive downloading Affected #: 2 files diff -r 2b4877fc1024180fadb5128187d447c0b84c7cd9 -r 6f89cbaeb29c33ae8d662d84965bf3bdee7f179d lib/galaxy/util/streamball.py --- a/lib/galaxy/util/streamball.py +++ b/lib/galaxy/util/streamball.py @@ -3,6 +3,7 @@ """ import os import logging, tarfile +from galaxy.exceptions import ObjectNotFound log = logging.getLogger( __name__ ) @@ -14,8 +15,12 @@ self.mode = mode self.wsgi_status = None self.wsgi_headeritems = None - def add( self, file, relpath ): - self.members[file] = relpath + def add( self, file, relpath, check_file=False): + if check_file and len(file)>0: + if not os.path.isfile(file): + raise ObjectNotFound + else: + self.members[file] = relpath def stream( self, environ, start_response ): response_write = start_response( self.wsgi_status, self.wsgi_headeritems ) class tarfileobj: diff -r 2b4877fc1024180fadb5128187d447c0b84c7cd9 -r 6f89cbaeb29c33ae8d662d84965bf3bdee7f179d lib/galaxy/webapps/galaxy/api/lda_datasets.py --- a/lib/galaxy/webapps/galaxy/api/lda_datasets.py +++ b/lib/galaxy/webapps/galaxy/api/lda_datasets.py @@ -14,7 +14,7 @@ import urllib2 import zipfile from paste.httpexceptions import HTTPBadRequest -from galaxy.exceptions import ItemAccessibilityException, MessageException, ItemDeletionException +from galaxy.exceptions import ItemAccessibilityException, MessageException, ItemDeletionException, ObjectNotFound from galaxy.security import Action from galaxy import util, web from galaxy.util.streamball import StreamBall @@ -84,7 +84,7 @@ return 'error of unknown kind' + str( e ) if format in [ 'zip','tgz','tbz' ]: - error = False + # error = False killme = string.punctuation + string.whitespace trantab = string.maketrans(killme,'_'*len(killme)) try: @@ -117,12 +117,12 @@ log.exception( "Unexpected error %s in create archive for download" % sys.exc_info()[0] ) trans.response.status = 500 return "Unable to create archive for download, please report - %s" % sys.exc_info()[0] - if not error: + if True: composite_extensions = trans.app.datatypes_registry.get_composite_extensions() seen = [] for ldda in lddas: - if ldda.dataset.state in [ 'new', 'upload', 'queued', 'running', 'empty', 'discarded' ]: - continue + # if ldda.dataset.state in [ 'new', 'upload', 'queued', 'running', 'empty', 'discarded' ]: + # continue ext = ldda.extension is_composite = ext in composite_extensions path = "" @@ -135,6 +135,7 @@ path = os.path.join( parent_folder.name, path ) parent_folder = parent_folder.parent path += ldda.name + while path in seen: path += '_' seen.append( path ) @@ -145,30 +146,61 @@ if zpathext == '': zpath = '%s.html' % zpath # fake the real nature of the html file try: - archive.add(ldda.dataset.file_name,zpath) # add the primary of a composite set + if format=='zip': + archive.add( ldda.dataset.file_name, zpath ) # add the primary of a composite set + else: + archive.add( ldda.dataset.file_name, zpath, check_file=True ) # add the primary of a composite set except IOError: log.exception( "Unable to add composite parent %s to temporary library download archive" % ldda.dataset.file_name) trans.response.status = 500 return "Unable to create archive for download, please report this error" + except ObjectNotFound: + log.exception( "Requested dataset %s does not exist on the host." % ldda.dataset.file_name ) + trans.response.status = 500 + return "Requested dataset does not exist on the host." + except: + trans.response.status = 500 + return "Unknown error, please report this error" flist = glob.glob(os.path.join(ldda.dataset.extra_files_path,'*.*')) # glob returns full paths for fpath in flist: efp,fname = os.path.split(fpath) if fname > '': fname = fname.translate(trantab) try: - archive.add( fpath,fname ) + if format=='zip': + archive.add( fpath,fname ) + else: + archive.add( fpath,fname, check_file=True ) except IOError: log.exception( "Unable to add %s to temporary library download archive %s" % (fname,outfname)) trans.response.status = 500 return "Unable to create archive for download, please report this error" + except ObjectNotFound: + log.exception( "Requested dataset %s does not exist on the host." % fpath ) + trans.response.status = 500 + return "Requested dataset does not exist on the host." + except: + trans.response.status = 500 + return "Unknown error, please report this error" else: # simple case try: - archive.add( ldda.dataset.file_name, path ) + if format=='zip': + archive.add( ldda.dataset.file_name, path ) + else: + archive.add( ldda.dataset.file_name, path, check_file=True ) except IOError: log.exception( "Unable to write %s to temporary library download archive" % ldda.dataset.file_name) trans.response.status = 500 return "Unable to create archive for download, please report this error" - if not error: + except ObjectNotFound: + log.exception( "Requested dataset %s does not exist on the host." % ldda.dataset.file_name ) + trans.response.status = 500 + return "Requested dataset does not exist on the host." + except: + trans.response.status = 500 + return "Unknown error, please report this error" + + if True: lname = 'selected_dataset' fname = lname.replace( ' ', '_' ) + '_files' if format == 'zip': https://bitbucket.org/galaxy/galaxy-central/commits/8090554de504/ Changeset: 8090554de504 User: martenson Date: 2013-12-18 17:41:24 Summary: minor cosmetic changes Affected #: 2 files diff -r 6f89cbaeb29c33ae8d662d84965bf3bdee7f179d -r 8090554de504ffa05e744fdca051b9350aeae93f static/scripts/utils/galaxy.uploadbox.js --- a/static/scripts/utils/galaxy.uploadbox.js +++ b/static/scripts/utils/galaxy.uploadbox.js @@ -353,3 +353,4 @@ }; } })(jQuery); + diff -r 6f89cbaeb29c33ae8d662d84965bf3bdee7f179d -r 8090554de504ffa05e744fdca051b9350aeae93f static/style/Gruntfile.js --- a/static/style/Gruntfile.js +++ b/static/style/Gruntfile.js @@ -6,7 +6,7 @@ var theme = grunt.option( 'theme', 'blue' ); var out = 'blue' - var lessFiles = [ 'base', 'autocomplete_tagging', 'embed_item', 'iphone', 'masthead', 'library', 'trackster']; + var lessFiles = [ 'base', 'autocomplete_tagging', 'embed_item', 'iphone', 'masthead', 'library', 'trackster' ]; var _ = grunt.util._; var fmt = _.sprintf; https://bitbucket.org/galaxy/galaxy-central/commits/00fb5740d971/ Changeset: 00fb5740d971 User: martenson Date: 2013-12-19 18:44:34 Summary: refactor + docs Affected #: 2 files diff -r 8090554de504ffa05e744fdca051b9350aeae93f -r 00fb5740d971318d4384e69fc2fcc6b2318b2d44 doc/source/lib/galaxy.webapps.galaxy.api.rst --- a/doc/source/lib/galaxy.webapps.galaxy.api.rst +++ b/doc/source/lib/galaxy.webapps.galaxy.api.rst @@ -297,6 +297,14 @@ :undoc-members: :show-inheritance: +:mod:`lda_datasets` Module +-------------------------- + +.. automodule:: galaxy.webapps.galaxy.api.lda_datasets + :members: + :undoc-members: + :show-inheritance: + :mod:`libraries` Module ----------------------- diff -r 8090554de504ffa05e744fdca051b9350aeae93f -r 00fb5740d971318d4384e69fc2fcc6b2318b2d44 lib/galaxy/webapps/galaxy/api/lda_datasets.py --- a/lib/galaxy/webapps/galaxy/api/lda_datasets.py +++ b/lib/galaxy/webapps/galaxy/api/lda_datasets.py @@ -28,8 +28,17 @@ @web.expose_api def show( self, trans, id, **kwd ): """ - GET /api/libraries/datasets/{encoded_dataset_id} - Displays information about the dataset identified by the lda ID. + show( self, trans, id, **kwd ) + * GET /api/libraries/datasets/{encoded_dataset_id}: + Displays information about the dataset identified by the encoded id. + + + :type id: an encoded id string + :param id: the encoded id of the dataset to query + + :rtype: dictionary + :returns: detailed dataset information from + :func:`galaxy.web.base.controller.UsesVisualizationMixin.get_library_dataset.to_dict()` """ # Get dataset. try: @@ -55,9 +64,25 @@ @web.expose def download( self, trans, format, **kwd ): """ - GET /api/libraries/datasets/download/{format} - GET multiple params: ldda_ids = [] - Downloads dataset(s) in the requested format or plain. + download( self, trans, format, **kwd ) + * GET /api/libraries/datasets/download/{format} + + .. code-block:: + example: + GET localhost:8080/api/libraries/datasets/download/tbz?ldda_ids%255B%255D=a0d84b45643a2678&ldda_ids%255B%255D=fe38c84dcd46c828 + + :type format: string + :param format: string representing requested archive format + + .. note:: supported formats are: zip, tgz, tbz, uncompressed + + :type lddas[]: an array + :param lddas[]: an array of encoded ids + + :rtype: file + :returns: either archive with the requested datasets packed inside or a single uncompressed dataset + + :raises: MessageException, ItemDeletionException, ItemAccessibilityException, HTTPBadRequest, OSError, IOError, ObjectNotFound """ lddas = [] datasets_to_download = kwd['ldda_ids%5B%5D'] @@ -117,108 +142,101 @@ log.exception( "Unexpected error %s in create archive for download" % sys.exc_info()[0] ) trans.response.status = 500 return "Unable to create archive for download, please report - %s" % sys.exc_info()[0] - if True: - composite_extensions = trans.app.datatypes_registry.get_composite_extensions() - seen = [] - for ldda in lddas: - # if ldda.dataset.state in [ 'new', 'upload', 'queued', 'running', 'empty', 'discarded' ]: - # continue - ext = ldda.extension - is_composite = ext in composite_extensions - path = "" - parent_folder = ldda.library_dataset.folder - while parent_folder is not None: - # Exclude the now-hidden "root folder" - if parent_folder.parent is None: - path = os.path.join( parent_folder.library_root[0].name, path ) - break - path = os.path.join( parent_folder.name, path ) - parent_folder = parent_folder.parent - path += ldda.name - - while path in seen: - path += '_' - seen.append( path ) - zpath = os.path.split(path)[-1] # comes as base_name/fname - outfname,zpathext = os.path.splitext(zpath) - if is_composite: - # need to add all the components from the extra_files_path to the zip - if zpathext == '': - zpath = '%s.html' % zpath # fake the real nature of the html file + composite_extensions = trans.app.datatypes_registry.get_composite_extensions() + seen = [] + for ldda in lddas: + ext = ldda.extension + is_composite = ext in composite_extensions + path = "" + parent_folder = ldda.library_dataset.folder + while parent_folder is not None: + # Exclude the now-hidden "root folder" + if parent_folder.parent is None: + path = os.path.join( parent_folder.library_root[0].name, path ) + break + path = os.path.join( parent_folder.name, path ) + parent_folder = parent_folder.parent + path += ldda.name + while path in seen: + path += '_' + seen.append( path ) + zpath = os.path.split(path)[-1] # comes as base_name/fname + outfname,zpathext = os.path.splitext(zpath) + if is_composite: # need to add all the components from the extra_files_path to the zip + if zpathext == '': + zpath = '%s.html' % zpath # fake the real nature of the html file + try: + if format=='zip': + archive.add( ldda.dataset.file_name, zpath ) # add the primary of a composite set + else: + archive.add( ldda.dataset.file_name, zpath, check_file=True ) # add the primary of a composite set + except IOError: + log.exception( "Unable to add composite parent %s to temporary library download archive" % ldda.dataset.file_name) + trans.response.status = 500 + return "Unable to create archive for download, please report this error" + except ObjectNotFound: + log.exception( "Requested dataset %s does not exist on the host." % ldda.dataset.file_name ) + trans.response.status = 500 + return "Requested dataset does not exist on the host." + except: + trans.response.status = 500 + return "Unknown error, please report this error" + flist = glob.glob(os.path.join(ldda.dataset.extra_files_path,'*.*')) # glob returns full paths + for fpath in flist: + efp,fname = os.path.split(fpath) + if fname > '': + fname = fname.translate(trantab) try: if format=='zip': - archive.add( ldda.dataset.file_name, zpath ) # add the primary of a composite set - else: - archive.add( ldda.dataset.file_name, zpath, check_file=True ) # add the primary of a composite set + archive.add( fpath,fname ) + else: + archive.add( fpath,fname, check_file=True ) except IOError: - log.exception( "Unable to add composite parent %s to temporary library download archive" % ldda.dataset.file_name) + log.exception( "Unable to add %s to temporary library download archive %s" % (fname,outfname)) trans.response.status = 500 return "Unable to create archive for download, please report this error" except ObjectNotFound: - log.exception( "Requested dataset %s does not exist on the host." % ldda.dataset.file_name ) + log.exception( "Requested dataset %s does not exist on the host." % fpath ) trans.response.status = 500 - return "Requested dataset does not exist on the host." - except: - trans.response.status = 500 - return "Unknown error, please report this error" - flist = glob.glob(os.path.join(ldda.dataset.extra_files_path,'*.*')) # glob returns full paths - for fpath in flist: - efp,fname = os.path.split(fpath) - if fname > '': - fname = fname.translate(trantab) - try: - if format=='zip': - archive.add( fpath,fname ) - else: - archive.add( fpath,fname, check_file=True ) - except IOError: - log.exception( "Unable to add %s to temporary library download archive %s" % (fname,outfname)) - trans.response.status = 500 - return "Unable to create archive for download, please report this error" - except ObjectNotFound: - log.exception( "Requested dataset %s does not exist on the host." % fpath ) - trans.response.status = 500 - return "Requested dataset does not exist on the host." - except: - trans.response.status = 500 - return "Unknown error, please report this error" - else: # simple case - try: - if format=='zip': - archive.add( ldda.dataset.file_name, path ) - else: - archive.add( ldda.dataset.file_name, path, check_file=True ) - except IOError: - log.exception( "Unable to write %s to temporary library download archive" % ldda.dataset.file_name) - trans.response.status = 500 - return "Unable to create archive for download, please report this error" - except ObjectNotFound: - log.exception( "Requested dataset %s does not exist on the host." % ldda.dataset.file_name ) - trans.response.status = 500 - return "Requested dataset does not exist on the host." + return "Requested dataset does not exist on the host." except: trans.response.status = 500 return "Unknown error, please report this error" - - if True: - lname = 'selected_dataset' - fname = lname.replace( ' ', '_' ) + '_files' - if format == 'zip': - archive.close() - trans.response.set_content_type( "application/octet-stream" ) - trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) - archive = util.streamball.ZipBall(tmpf, tmpd) - archive.wsgi_status = trans.response.wsgi_status() - archive.wsgi_headeritems = trans.response.wsgi_headeritems() - trans.response.status = 200 - return archive.stream - else: - trans.response.set_content_type( "application/x-tar" ) - trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) - archive.wsgi_status = trans.response.wsgi_status() - archive.wsgi_headeritems = trans.response.wsgi_headeritems() - trans.response.status = 200 - return archive.stream + else: # simple case + try: + if format=='zip': + archive.add( ldda.dataset.file_name, path ) + else: + archive.add( ldda.dataset.file_name, path, check_file=True ) + except IOError: + log.exception( "Unable to write %s to temporary library download archive" % ldda.dataset.file_name) + trans.response.status = 500 + return "Unable to create archive for download, please report this error" + except ObjectNotFound: + log.exception( "Requested dataset %s does not exist on the host." % ldda.dataset.file_name ) + trans.response.status = 500 + return "Requested dataset does not exist on the host." + except: + trans.response.status = 500 + return "Unknown error, please report this error" + lname = 'selected_dataset' + fname = lname.replace( ' ', '_' ) + '_files' + if format == 'zip': + archive.close() + trans.response.set_content_type( "application/octet-stream" ) + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) + archive = util.streamball.ZipBall(tmpf, tmpd) + archive.wsgi_status = trans.response.wsgi_status() + archive.wsgi_headeritems = trans.response.wsgi_headeritems() + trans.response.status = 200 + return archive.stream + else: + trans.response.set_content_type( "application/x-tar" ) + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) + archive.wsgi_status = trans.response.wsgi_status() + archive.wsgi_headeritems = trans.response.wsgi_headeritems() + trans.response.status = 200 + return archive.stream elif format == 'uncompressed': if len(lddas) != 1: trans.response.status = 400 https://bitbucket.org/galaxy/galaxy-central/commits/c1963ee06cb0/ Changeset: c1963ee06cb0 User: martenson Date: 2013-12-19 18:48:22 Summary: Merged in martenson/galaxy-central-marten (pull request #273) Initial Data Libraries Affected #: 24 files diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b doc/source/lib/galaxy.webapps.galaxy.api.rst --- a/doc/source/lib/galaxy.webapps.galaxy.api.rst +++ b/doc/source/lib/galaxy.webapps.galaxy.api.rst @@ -302,6 +302,14 @@ :undoc-members: :show-inheritance: +:mod:`lda_datasets` Module +-------------------------- + +.. automodule:: galaxy.webapps.galaxy.api.lda_datasets + :members: + :undoc-members: + :show-inheritance: + :mod:`libraries` Module ----------------------- diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b lib/galaxy/model/__init__.py --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -1827,7 +1827,7 @@ class Library( object, Dictifiable ): permitted_actions = get_permitted_actions( filter='LIBRARY' ) dict_collection_visible_keys = ( 'id', 'name' ) - dict_element_visible_keys = ( 'id', 'deleted', 'name', 'description', 'synopsis' ) + dict_element_visible_keys = ( 'id', 'deleted', 'name', 'description', 'synopsis', 'root_folder_id' ) def __init__( self, name=None, description=None, synopsis=None, root_folder=None ): self.name = name or "Unnamed library" self.description = description @@ -1894,7 +1894,7 @@ return name class LibraryFolder( object, Dictifiable ): - dict_element_visible_keys = ( 'id', 'parent_id', 'name', 'description', 'item_count', 'genome_build' ) + dict_element_visible_keys = ( 'id', 'parent_id', 'name', 'description', 'item_count', 'genome_build', 'update_time' ) def __init__( self, name=None, description=None, item_count=0, order_id=None ): self.name = name or "Unnamed folder" self.description = description @@ -2060,6 +2060,7 @@ genome_build = ldda.dbkey, misc_info = ldda.info, misc_blurb = ldda.blurb, + peek = ( lambda ldda: ldda.display_peek() if ldda.peek and ldda.peek != 'no peek' else None )( ldda ), template_data = template_data ) if ldda.dataset.uuid is None: rval['uuid'] = None diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b lib/galaxy/model/mapping.py --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -1852,8 +1852,9 @@ table = self.table trans = conn.begin() try: - next_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() - table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid + 1 ) ) + current_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() + next_hid = current_hid + 1 + table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid ) ) trans.commit() return next_hid except: diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b lib/galaxy/util/streamball.py --- a/lib/galaxy/util/streamball.py +++ b/lib/galaxy/util/streamball.py @@ -3,6 +3,7 @@ """ import os import logging, tarfile +from galaxy.exceptions import ObjectNotFound log = logging.getLogger( __name__ ) @@ -14,8 +15,12 @@ self.mode = mode self.wsgi_status = None self.wsgi_headeritems = None - def add( self, file, relpath ): - self.members[file] = relpath + def add( self, file, relpath, check_file=False): + if check_file and len(file)>0: + if not os.path.isfile(file): + raise ObjectNotFound + else: + self.members[file] = relpath def stream( self, environ, start_response ): response_write = start_response( self.wsgi_status, self.wsgi_headeritems ) class tarfileobj: diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b lib/galaxy/webapps/galaxy/api/datasets.py --- a/lib/galaxy/webapps/galaxy/api/datasets.py +++ b/lib/galaxy/webapps/galaxy/api/datasets.py @@ -1,5 +1,5 @@ """ -API operations on the contents of a dataset. +API operations on the contents of a history dataset. """ from galaxy import web from galaxy.visualization.data_providers.genome import FeatureLocationIndexDataProvider diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b lib/galaxy/webapps/galaxy/api/folder_contents.py --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -1,5 +1,5 @@ """ -API operations on the contents of a library. +API operations on the contents of a folder. """ import logging, os, string, shutil, urllib, re, socket from cgi import escape, FieldStorage @@ -11,67 +11,122 @@ log = logging.getLogger( __name__ ) class FolderContentsController( BaseAPIController, UsesLibraryMixin, UsesLibraryMixinItems ): + """ + Class controls retrieval, creation and updating of folder contents. + """ + + def load_folder_contents( self, trans, folder ): + """ + Loads all contents of the folder (folders and data sets) but only in the first level. + """ + current_user_roles = trans.get_current_user_roles() + is_admin = trans.user_is_admin() + content_items = [] + for subfolder in folder.active_folders: + if not is_admin: + can_access, folder_ids = trans.app.security_agent.check_folder_contents( trans.user, current_user_roles, subfolder ) + if (is_admin or can_access) and not subfolder.deleted: + subfolder.api_type = 'folder' + content_items.append( subfolder ) + for dataset in folder.datasets: + if not is_admin: + can_access = trans.app.security_agent.can_access_dataset( current_user_roles, dataset.library_dataset_dataset_association.dataset ) + if (is_admin or can_access) and not dataset.deleted: + dataset.api_type = 'file' + content_items.append( dataset ) + return content_items @web.expose_api def index( self, trans, folder_id, **kwd ): """ GET /api/folders/{encoded_folder_id}/contents Displays a collection (list) of a folder's contents (files and folders). - The /api/library_contents/{encoded_library_id}/contents - lists everything in a library recursively, which is not what - we want here. We could add a parameter to use the recursive - style, but this is meant to act similar to an "ls" directory listing. + Encoded folder ID is prepended with 'F' if it is a folder as opposed to a data set which does not have it. + Full path is provided as a separate object in response providing data for breadcrumb path building. """ - rval = [] + folder_container = [] current_user_roles = trans.get_current_user_roles() - def traverse( folder ): - admin = trans.user_is_admin() - rval = [] - for subfolder in folder.active_folders: - if not admin: - can_access, folder_ids = trans.app.security_agent.check_folder_contents( trans.user, current_user_roles, subfolder ) - if (admin or can_access) and not subfolder.deleted: - subfolder.api_type = 'folder' - rval.append( subfolder ) - for ld in folder.datasets: - if not admin: - can_access = trans.app.security_agent.can_access_dataset( current_user_roles, ld.library_dataset_dataset_association.dataset ) - if (admin or can_access) and not ld.deleted: - ld.api_type = 'file' - rval.append( ld ) - return rval - - try: - decoded_folder_id = trans.security.decode_id( folder_id[-16:] ) - except TypeError: - trans.response.status = 400 - return "Malformed folder id ( %s ) specified, unable to decode." % str( folder_id ) + if ( folder_id.startswith( 'F' ) ): + try: + decoded_folder_id = trans.security.decode_id( folder_id[1:] ) + except TypeError: + trans.response.status = 400 + return "Malformed folder id ( %s ) specified, unable to decode." % str( folder_id ) try: folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( decoded_folder_id ) - parent_library = folder.parent_library except: folder = None - log.error( "FolderContentsController.index: Unable to retrieve folder %s" - % folder_id ) + log.error( "FolderContentsController.index: Unable to retrieve folder with ID: %s" % folder_id ) - # TODO: Find the API's path to this folder if necessary. - # This was needed in recursive descent, but it's not needed - # for "ls"-style content checking: - if not folder or not ( trans.user_is_admin() or trans.app.security_agent.can_access_library_item( current_user_roles, folder, trans.user ) ): + # We didn't find the folder or user does not have an access to it. + if not folder: trans.response.status = 400 return "Invalid folder id ( %s ) specified." % str( folder_id ) + + if not ( trans.user_is_admin() or trans.app.security_agent.can_access_library_item( current_user_roles, folder, trans.user ) ): + log.warning( "SECURITY: User (id: %s) without proper access rights is trying to load folder with ID of %s" % ( trans.user.id, folder.id ) ) + trans.response.status = 400 + return "Invalid folder id ( %s ) specified." % str( folder_id ) + + path_to_root = [] + def build_path ( folder ): + """ + Search the path upwards recursively and load the whole route of names and ids for breadcrumb purposes. + """ + path_to_root = [] + # We are almost in root + if folder.parent_id is None: + path_to_root.append( ( 'F' + trans.security.encode_id( folder.id ), folder.name ) ) + else: + # We add the current folder and traverse up one folder. + path_to_root.append( ( 'F' + trans.security.encode_id( folder.id ), folder.name ) ) + upper_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( folder.parent_id ) + path_to_root.extend( build_path( upper_folder ) ) + return path_to_root + + # Return the reversed path so it starts with the library node. + full_path = build_path( folder )[::-1] + folder_container.append( dict( full_path = full_path ) ) + + folder_contents = [] + time_updated = '' + time_created = '' + # Go through every item in the folder and include its meta-data. + for content_item in self.load_folder_contents( trans, folder ): +# rval = content_item.to_dict() + return_item = {} + encoded_id = trans.security.encode_id( content_item.id ) + time_updated = content_item.update_time.strftime( "%Y-%m-%d %I:%M %p" ) + time_created = content_item.create_time.strftime( "%Y-%m-%d %I:%M %p" ) + + # For folder return also hierarchy values + if content_item.api_type == 'folder': + encoded_id = 'F' + encoded_id +# time_updated = content_item.update_time.strftime( "%Y-%m-%d %I:%M %p" ) + return_item.update ( dict ( item_count = content_item.item_count ) ) - for content in traverse( folder ): - encoded_id = trans.security.encode_id( content.id ) - if content.api_type == 'folder': - encoded_id = 'F' + encoded_id - rval.append( dict( id = encoded_id, - type = content.api_type, - name = content.name, - url = url_for( 'folder_contents', folder_id=encoded_id ) ) ) - return rval + if content_item.api_type == 'file': + library_dataset_dict = content_item.to_dict() + library_dataset_dict['data_type'] + library_dataset_dict['file_size'] + library_dataset_dict['date_uploaded'] + return_item.update ( dict ( data_type = library_dataset_dict['data_type'], + file_size = library_dataset_dict['file_size'], + date_uploaded = library_dataset_dict['date_uploaded'] ) ) + + # For every item return also the default meta-data + return_item.update( dict( id = encoded_id, + type = content_item.api_type, + name = content_item.name, + time_updated = time_updated, + time_created = time_created + ) ) + folder_contents.append( return_item ) + # Put the data in the container + folder_container.append( dict( folder_contents = folder_contents ) ) + return folder_container @web.expose_api def show( self, trans, id, library_id, **kwd ): diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b lib/galaxy/webapps/galaxy/api/lda_datasets.py --- /dev/null +++ b/lib/galaxy/webapps/galaxy/api/lda_datasets.py @@ -0,0 +1,261 @@ +""" +API operations on the datasets from library. +""" +import glob +import logging +import operator +import os +import os.path +import string +import sys +import tarfile +import tempfile +import urllib +import urllib2 +import zipfile +from paste.httpexceptions import HTTPBadRequest +from galaxy.exceptions import ItemAccessibilityException, MessageException, ItemDeletionException, ObjectNotFound +from galaxy.security import Action +from galaxy import util, web +from galaxy.util.streamball import StreamBall +from galaxy.web.base.controller import BaseAPIController, UsesVisualizationMixin + +import logging +log = logging.getLogger( __name__ ) + +class LibraryDatasetsController( BaseAPIController, UsesVisualizationMixin ): + + @web.expose_api + def show( self, trans, id, **kwd ): + """ + show( self, trans, id, **kwd ) + * GET /api/libraries/datasets/{encoded_dataset_id}: + Displays information about the dataset identified by the encoded id. + + + :type id: an encoded id string + :param id: the encoded id of the dataset to query + + :rtype: dictionary + :returns: detailed dataset information from + :func:`galaxy.web.base.controller.UsesVisualizationMixin.get_library_dataset.to_dict()` + """ + # Get dataset. + try: + dataset = self.get_library_dataset( trans, id = id, check_ownership=False, check_accessible=True ) + except Exception, e: + trans.response.status = 500 + return str( e ) + try: + # Default: return dataset as dict. + rval = dataset.to_dict() + except Exception, e: + rval = "Error in dataset API at listing contents: " + str( e ) + log.error( rval + ": %s" % str(e), exc_info=True ) + trans.response.status = 500 + return "Error in dataset API at listing contents: " + str( e ) + + rval['id'] = trans.security.encode_id(rval['id']); + rval['ldda_id'] = trans.security.encode_id(rval['ldda_id']); + rval['folder_id'] = 'f' + trans.security.encode_id(rval['folder_id']) + trans.response.status = 200 + return rval + + @web.expose + def download( self, trans, format, **kwd ): + """ + download( self, trans, format, **kwd ) + * GET /api/libraries/datasets/download/{format} + + .. code-block:: + example: + GET localhost:8080/api/libraries/datasets/download/tbz?ldda_ids%255B%255D=a0d84b45643a2678&ldda_ids%255B%255D=fe38c84dcd46c828 + + :type format: string + :param format: string representing requested archive format + + .. note:: supported formats are: zip, tgz, tbz, uncompressed + + :type lddas[]: an array + :param lddas[]: an array of encoded ids + + :rtype: file + :returns: either archive with the requested datasets packed inside or a single uncompressed dataset + + :raises: MessageException, ItemDeletionException, ItemAccessibilityException, HTTPBadRequest, OSError, IOError, ObjectNotFound + """ + lddas = [] + datasets_to_download = kwd['ldda_ids%5B%5D'] + + if ( datasets_to_download != None ): + datasets_to_download = util.listify( datasets_to_download ) + for dataset_id in datasets_to_download: + try: + ldda = self.get_hda_or_ldda( trans, hda_ldda='ldda', dataset_id=dataset_id ) + lddas.append( ldda ) + except ItemAccessibilityException: + trans.response.status = 403 + return 'Insufficient rights to access library dataset with id: (%s)' % str( dataset_id ) + except MessageException: + trans.response.status = 400 + return 'Wrong library dataset id: (%s)' % str( dataset_id ) + except ItemDeletionException: + trans.response.status = 400 + return 'The item with library dataset id: (%s) is deleted' % str( dataset_id ) + except HTTPBadRequest, e: + return 'http bad request' + str( e.err_msg ) + except Exception, e: + trans.response.status = 500 + return 'error of unknown kind' + str( e ) + + if format in [ 'zip','tgz','tbz' ]: + # error = False + killme = string.punctuation + string.whitespace + trantab = string.maketrans(killme,'_'*len(killme)) + try: + outext = 'zip' + if format == 'zip': + # Can't use mkstemp - the file must not exist first + tmpd = tempfile.mkdtemp() + util.umask_fix_perms( tmpd, trans.app.config.umask, 0777, self.app.config.gid ) + tmpf = os.path.join( tmpd, 'library_download.' + format ) + if trans.app.config.upstream_gzip: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_STORED, True ) + else: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED, True ) + archive.add = lambda x, y: archive.write( x, y.encode('CP437') ) + elif format == 'tgz': + if trans.app.config.upstream_gzip: + archive = StreamBall( 'w|' ) + outext = 'tar' + else: + archive = StreamBall( 'w|gz' ) + outext = 'tgz' + elif format == 'tbz': + archive = StreamBall( 'w|bz2' ) + outext = 'tbz2' + except ( OSError, zipfile.BadZipfile ): + log.exception( "Unable to create archive for download" ) + trans.response.status = 500 + return "Unable to create archive for download, please report this error" + except: + log.exception( "Unexpected error %s in create archive for download" % sys.exc_info()[0] ) + trans.response.status = 500 + return "Unable to create archive for download, please report - %s" % sys.exc_info()[0] + composite_extensions = trans.app.datatypes_registry.get_composite_extensions() + seen = [] + for ldda in lddas: + ext = ldda.extension + is_composite = ext in composite_extensions + path = "" + parent_folder = ldda.library_dataset.folder + while parent_folder is not None: + # Exclude the now-hidden "root folder" + if parent_folder.parent is None: + path = os.path.join( parent_folder.library_root[0].name, path ) + break + path = os.path.join( parent_folder.name, path ) + parent_folder = parent_folder.parent + path += ldda.name + while path in seen: + path += '_' + seen.append( path ) + zpath = os.path.split(path)[-1] # comes as base_name/fname + outfname,zpathext = os.path.splitext(zpath) + if is_composite: # need to add all the components from the extra_files_path to the zip + if zpathext == '': + zpath = '%s.html' % zpath # fake the real nature of the html file + try: + if format=='zip': + archive.add( ldda.dataset.file_name, zpath ) # add the primary of a composite set + else: + archive.add( ldda.dataset.file_name, zpath, check_file=True ) # add the primary of a composite set + except IOError: + log.exception( "Unable to add composite parent %s to temporary library download archive" % ldda.dataset.file_name) + trans.response.status = 500 + return "Unable to create archive for download, please report this error" + except ObjectNotFound: + log.exception( "Requested dataset %s does not exist on the host." % ldda.dataset.file_name ) + trans.response.status = 500 + return "Requested dataset does not exist on the host." + except: + trans.response.status = 500 + return "Unknown error, please report this error" + flist = glob.glob(os.path.join(ldda.dataset.extra_files_path,'*.*')) # glob returns full paths + for fpath in flist: + efp,fname = os.path.split(fpath) + if fname > '': + fname = fname.translate(trantab) + try: + if format=='zip': + archive.add( fpath,fname ) + else: + archive.add( fpath,fname, check_file=True ) + except IOError: + log.exception( "Unable to add %s to temporary library download archive %s" % (fname,outfname)) + trans.response.status = 500 + return "Unable to create archive for download, please report this error" + except ObjectNotFound: + log.exception( "Requested dataset %s does not exist on the host." % fpath ) + trans.response.status = 500 + return "Requested dataset does not exist on the host." + except: + trans.response.status = 500 + return "Unknown error, please report this error" + else: # simple case + try: + if format=='zip': + archive.add( ldda.dataset.file_name, path ) + else: + archive.add( ldda.dataset.file_name, path, check_file=True ) + except IOError: + log.exception( "Unable to write %s to temporary library download archive" % ldda.dataset.file_name) + trans.response.status = 500 + return "Unable to create archive for download, please report this error" + except ObjectNotFound: + log.exception( "Requested dataset %s does not exist on the host." % ldda.dataset.file_name ) + trans.response.status = 500 + return "Requested dataset does not exist on the host." + except: + trans.response.status = 500 + return "Unknown error, please report this error" + lname = 'selected_dataset' + fname = lname.replace( ' ', '_' ) + '_files' + if format == 'zip': + archive.close() + trans.response.set_content_type( "application/octet-stream" ) + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) + archive = util.streamball.ZipBall(tmpf, tmpd) + archive.wsgi_status = trans.response.wsgi_status() + archive.wsgi_headeritems = trans.response.wsgi_headeritems() + trans.response.status = 200 + return archive.stream + else: + trans.response.set_content_type( "application/x-tar" ) + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s.%s"' % (fname,outext) + archive.wsgi_status = trans.response.wsgi_status() + archive.wsgi_headeritems = trans.response.wsgi_headeritems() + trans.response.status = 200 + return archive.stream + elif format == 'uncompressed': + if len(lddas) != 1: + trans.response.status = 400 + return 'Wrong request' + else: + single_dataset = lddas[0] + trans.response.set_content_type( single_dataset.get_mime() ) + fStat = os.stat( ldda.file_name ) + trans.response.headers[ 'Content-Length' ] = int( fStat.st_size ) + valid_chars = '.,^_-()[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + fname = ldda.name + fname = ''.join( c in valid_chars and c or '_' for c in fname )[ 0:150 ] + trans.response.headers[ "Content-Disposition" ] = 'attachment; filename="%s"' % fname + try: + trans.response.status = 200 + return open( single_dataset.file_name ) + except: + trans.response.status = 500 + return 'This dataset contains no content' + else: + trans.response.status = 400 + return 'Wrong format parameter specified'; diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b lib/galaxy/webapps/galaxy/api/libraries.py --- a/lib/galaxy/webapps/galaxy/api/libraries.py +++ b/lib/galaxy/webapps/galaxy/api/libraries.py @@ -49,9 +49,10 @@ trans.model.Library.table.c.id.in_( accessible_restricted_library_ids ) ) ) rval = [] for library in query: - item = library.to_dict() + item = library.to_dict( view='element' ) item['url'] = url_for( route, id=trans.security.encode_id( library.id ) ) - item['id'] = trans.security.encode_id( item['id'] ) + item['id'] = 'F' + trans.security.encode_id( item['id'] ) + item['root_folder_id'] = 'F' + trans.security.encode_id( item['root_folder_id'] ) rval.append( item ) return rval @@ -131,6 +132,9 @@ rval['name'] = name rval['id'] = encoded_id return rval + + def edit( self, trans, payload, **kwd ): + return "Not implemented yet" @web.expose_api def delete( self, trans, id, **kwd ): diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -46,12 +46,6 @@ atexit.register( app.shutdown ) # Create the universe WSGI application webapp = GalaxyWebApplication( app, session_cookie='galaxysession', name='galaxy' ) - # Handle displaying tool help images and README file images contained in repositories installed from the tool shed. - webapp.add_route( '/admin_toolshed/static/images/:repository_id/:image_file', - controller='admin_toolshed', - action='display_image_in_repository', - repository_id=None, - image_file=None ) webapp.add_ui_controllers( 'galaxy.webapps.galaxy.controllers', app ) # Force /history to go to /root/history -- needed since the tests assume this webapp.add_route( '/history', controller='root', action='history' ) @@ -75,22 +69,12 @@ webapp.add_route( '/u/:username/v/:slug', controller='visualization', action='display_by_username_and_slug' ) webapp.add_route( '/search', controller='search', action='index' ) - # Add the web API + # ================ + # ===== API ===== + # ================ + webapp.add_api_controllers( 'galaxy.webapps.galaxy.api', app ) - # The /folders section is experimental at this point: - log.debug( "app.config.api_folders: %s" % app.config.api_folders ) - webapp.mapper.resource( 'folder', 'folders', path_prefix='/api' ) - webapp.mapper.resource( 'content', 'contents', - controller='folder_contents', - name_prefix='folder_', - path_prefix='/api/folders/:folder_id', - parent_resources=dict( member_name='folder', collection_name='folders' ) ) - webapp.mapper.resource( 'content', - 'contents', - controller='library_contents', - name_prefix='library_', - path_prefix='/api/libraries/:library_id', - parent_resources=dict( member_name='library', collection_name='libraries' ) ) + webapp.mapper.resource( 'content', 'contents', controller='history_contents', @@ -102,10 +86,6 @@ controller="datasets", action="display", conditions=dict(method=["GET"])) - webapp.mapper.resource( 'permission', - 'permissions', - path_prefix='/api/libraries/:library_id', - parent_resources=dict( member_name='library', collection_name='libraries' ) ) webapp.mapper.resource( 'user', 'users', controller='group_users', @@ -127,11 +107,6 @@ _add_item_tags_controller( webapp, name_prefix="workflow_", path_prefix='/api/workflows/:workflow_id' ) - - _add_item_extended_metadata_controller( webapp, - name_prefix="library_dataset_", - path_prefix='/api/libraries/:library_id/contents/:library_content_id' ) - _add_item_annotation_controller( webapp, name_prefix="history_content_", path_prefix='/api/histories/:history_id/contents/:history_content_id' ) @@ -141,7 +116,6 @@ _add_item_annotation_controller( webapp, name_prefix="workflow_", path_prefix='/api/workflows/:workflow_id' ) - _add_item_provenance_controller( webapp, name_prefix="history_content_", path_prefix='/api/histories/:history_id/contents/:history_content_id' ) @@ -170,6 +144,7 @@ parent_resources=dict( member_name='datatype', collection_name='datatypes' ) ) #webapp.mapper.connect( 'run_workflow', '/api/workflow/{workflow_id}/library/{library_id}', controller='workflows', action='run', workflow_id=None, library_id=None, conditions=dict(method=["GET"]) ) webapp.mapper.resource( 'search', 'search', path_prefix='/api' ) + # add as a non-ATOM API call to support the notion of a 'current/working' history unique to the history resource webapp.mapper.connect( "set_as_current", "/api/histories/{id}/set_as_current", @@ -188,6 +163,64 @@ webapp.mapper.connect("workflow_dict", '/api/workflows/{workflow_id}/download', controller='workflows', action='workflow_dict', conditions=dict(method=['GET'])) # Preserve the following download route for now for dependent applications -- deprecate at some point webapp.mapper.connect("workflow_dict", '/api/workflows/download/{workflow_id}', controller='workflows', action='workflow_dict', conditions=dict(method=['GET'])) + + # ======================= + # ===== LIBRARY API ===== + # ======================= + + webapp.mapper.connect( 'show_lda_item', + '/api/libraries/datasets/:id', + controller='lda_datasets', + action='show', + conditions=dict( method=[ "GET" ] ) ) + + webapp.mapper.connect( 'download_lda_items', + '/api/libraries/datasets/download/:format', + controller='lda_datasets', + action='download', + conditions=dict( method=[ "POST", "GET" ] ) ) + + webapp.mapper.resource_with_deleted( 'library', + 'libraries', + path_prefix='/api' ) + webapp.mapper.resource( 'folder', + 'folders', + path_prefix='/api' ) + + webapp.mapper.resource( 'content', + 'contents', + controller='folder_contents', + name_prefix='folder_', + path_prefix='/api/folders/:folder_id', + parent_resources=dict( member_name='folder', collection_name='folders' ) ) + + webapp.mapper.resource( 'content', + 'contents', + controller='library_contents', + name_prefix='library_', + path_prefix='/api/libraries/:library_id', + parent_resources=dict( member_name='library', collection_name='libraries' ) ) + + webapp.mapper.resource( 'permission', + 'permissions', + path_prefix='/api/libraries/:library_id', + parent_resources=dict( member_name='library', collection_name='libraries' ) ) + + _add_item_extended_metadata_controller( webapp, + name_prefix="library_dataset_", + path_prefix='/api/libraries/:library_id/contents/:library_content_id' ) + + # ==================== + # ===== TOOLSHED ===== + # ==================== + + # Handle displaying tool help images and README file images contained in repositories installed from the tool shed. + webapp.add_route( '/admin_toolshed/static/images/:repository_id/:image_file', + controller='admin_toolshed', + action='display_image_in_repository', + repository_id=None, + image_file=None ) + # Galaxy API for tool shed features. webapp.mapper.resource( 'tool_shed_repository', 'tool_shed_repositories', @@ -201,6 +234,7 @@ path_prefix='/api', new={ 'install_repository_revision' : 'POST' }, parent_resources=dict( member_name='tool_shed_repository', collection_name='tool_shed_repositories' ) ) + # Connect logger from app if app.trace_logger: webapp.trace_logger = app.trace_logger @@ -221,7 +255,7 @@ galaxy.model.mapping.metadata.engine.connection_provider._pool.dispose() except: pass - # Close any pooled database connections before forking + # Close any pooled database connections before forking try: galaxy.model.tool_shed_install.mapping.metadata.engine.connection_provider._pool.dispose() except: diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b lib/galaxy/webapps/galaxy/controllers/library.py --- a/lib/galaxy/webapps/galaxy/controllers/library.py +++ b/lib/galaxy/webapps/galaxy/controllers/library.py @@ -76,6 +76,17 @@ library_list_grid = LibraryListGrid() + + @web.expose + def list( self, trans, **kwd ): + params = util.Params( kwd ) + # define app configuration for generic mako template + app = { + 'jscript' : "galaxy.library" + } + # fill template + return trans.fill_template('galaxy.panels.mako', config = {'app' : app}) + @web.expose def index( self, trans, **kwd ): params = util.Params( kwd ) diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b lib/galaxy/webapps/galaxy/controllers/user.py --- a/lib/galaxy/webapps/galaxy/controllers/user.py +++ b/lib/galaxy/webapps/galaxy/controllers/user.py @@ -826,19 +826,22 @@ if email is None or activation_token is None: # We don't have the email or activation_token, show error. - return trans.show_error_message( "You are using wrong activation link. Try to log-in and we will send you a new activation email.<br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller="root", action="index" ) + return trans.show_error_message( "You are using wrong activation link. Try to log-in and we will send you a new activation email. <br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller="root", action="index" ) else: # Find the user user = trans.sa_session.query( trans.app.model.User ).filter( trans.app.model.User.table.c.email==email ).first() + # If the user is active already don't try to activate + if user.active == True: + return trans.show_ok_message( "Your account is already active. Nothing has changed. <br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) if user.activation_token == activation_token: user.activation_token = None user.active = True trans.sa_session.add(user) trans.sa_session.flush() - return trans.show_ok_message( "Your account has been successfully activated!<br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) + return trans.show_ok_message( "Your account has been successfully activated! <br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) else: # Tokens don't match. Activation is denied. - return trans.show_error_message( "You are using wrong activation link. Try to log in and we will send you a new activation email.<br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) + return trans.show_error_message( "You are using wrong activation link. Try to log in and we will send you a new activation email. <br><a href='%s'>Go to login page.</a>" ) % web.url_for( controller='root', action='index' ) return def __get_user_type_form_definition( self, trans, user=None, **kwd ): diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b static/scripts/galaxy.library.js --- /dev/null +++ b/static/scripts/galaxy.library.js @@ -0,0 +1,902 @@ +// MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +// === GALAXY LIBRARY MODULE ==== +// MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM + +// global variables +var view = null; +var library_router = null; +var responses = []; + +// dependencies +define([ + "galaxy.modal", + "galaxy.masthead", + "utils/galaxy.utils", + "libs/toastr"], function(mod_modal, mod_masthead, mod_utils, mod_toastr) { + +// MMMMMMMMMMMMMMM +// === Models ==== +// MMMMMMMMMMMMMMM + + // LIBRARY + var Library = Backbone.Model.extend({ + urlRoot: '/api/libraries' + }); + + // LIBRARIES + var Libraries = Backbone.Collection.extend({ + url: '/api/libraries', + model: Library + }); + + // ITEM + var Item = Backbone.Model.extend({ + urlRoot : '/api/libraries/datasets' + }) + + // FOLDER + var Folder = Backbone.Collection.extend({ + model: Item + }) + + // CONTAINER for folder contents (folders, items and metadata). + var FolderContainer = Backbone.Model.extend({ + defaults : { + folder : new Folder(), + full_path : "unknown", + urlRoot : "/api/folders/", + id : "unknown" + }, + parse : function(obj) { + this.full_path = obj[0].full_path; + // update the inner collection + this.get("folder").reset(obj[1].folder_contents); + return obj; + } + }) + + // HISTORY ITEM + var HistoryItem = Backbone.Model.extend({ + urlRoot : '/api/histories/' + }); + + // HISTORY + var GalaxyHistory = Backbone.Model.extend({ + url : '/api/histories/' + }); + + // HISTORIES + var GalaxyHistories = Backbone.Collection.extend({ + url : '/api/histories', + model : GalaxyHistory + }); + + //ROUTER + var LibraryRouter = Backbone.Router.extend({ + routes: { + "" : "libraries", + "folders/:id" : "folder_content", + "folders/:folder_id/download/:format" : "download" + } + }); + + +// MMMMMMMMMMMMMM +// === VIEWS ==== +// MMMMMMMMMMMMMM + +// galaxy folder +var FolderContentView = Backbone.View.extend({ + // main element definition + el : '#center', + // progress percentage + progress: 0, + // progress rate per one item + progressStep: 1, + // last selected history in modal for UX + lastSelectedHistory: '', + // self modal + modal : null, + // loaded folders + folders : null, + + // initialize + initialize : function(){ + this.folders = []; + this.queue = jQuery.Deferred(); + this.queue.resolve(); + }, + +// MMMMMMMMMMMMMMMMMM +// === TEMPLATES ==== +// MMMMMMMMMMMMMMMMMM + + // set up + templateFolder : function (){ + var tmpl_array = []; + + // CONTAINER + tmpl_array.push('<div id="library_container" style="width: 90%; margin: auto; margin-top: 2em; ">'); + tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to <a href="mailto:marten@bx.psu.edu?Subject=DataLibraries_Feedback" target="_blank">Marten</a>.</h3>'); + + // TOOLBAR + tmpl_array.push('<div id="library_folder_toolbar" >'); + tmpl_array.push(' <button title="Create New Folder" id="toolbtn_create_folder" class="btn btn-primary" type="button"><span class="fa fa-plus"></span><span class="fa fa-folder-close"></span> folder</button>'); + tmpl_array.push(' <button id="toolbtn_bulk_import" class="btn btn-primary" style="display: none; margin-left: 0.5em;" type="button"><span class="fa fa-external-link"></span> to history</button>'); + + tmpl_array.push(' <div id="toolbtn_dl" class="btn-group" style="margin-left: 0.5em; display: none; ">'); + tmpl_array.push(' <button id="drop_toggle" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">'); + tmpl_array.push(' <span class="fa fa-download"></span> download <span class="caret"></span>'); + tmpl_array.push(' </button>'); + tmpl_array.push(' <ul class="dropdown-menu" role="menu">'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/tgz">.tar.gz</a></li>'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/tbz">.tar.bz</a></li>'); + tmpl_array.push(' <li><a href="#/folders/<%= id %>/download/zip">.zip</a></li>'); + tmpl_array.push(' </ul>'); + tmpl_array.push(' </div>'); + + tmpl_array.push('</div>'); + + // BREADCRUMBS + tmpl_array.push('<div class="library_breadcrumb">'); + tmpl_array.push('<a title="Return to the list of libraries" href="#">Libraries</a><b>|</b> '); + tmpl_array.push('<% _.each(path, function(path_item) { %>'); + tmpl_array.push('<% if (path_item[0] != id) { %>'); + tmpl_array.push('<a title="Return to this folder" href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a><b>|</b> '); + tmpl_array.push('<% } else { %>'); + tmpl_array.push('<span title="You are in this folder"><%- path_item[1] %></span>'); + tmpl_array.push('<% } %>'); + tmpl_array.push('<% }); %>'); + tmpl_array.push('</div>'); + + // FOLDER CONTENT + tmpl_array.push('<table id="folder_table" class="table table-condensed">'); + tmpl_array.push(' <thead>'); + tmpl_array.push(' <th style="text-align: center; width: 20px; "><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th>'); + tmpl_array.push(' <th class="button_heading">view</th>'); + tmpl_array.push(' <th>name</th>'); + tmpl_array.push(' <th>data type</th>'); + tmpl_array.push(' <th>size</th>'); + tmpl_array.push(' <th>date</th>'); + tmpl_array.push(' </thead>'); + tmpl_array.push(' <tbody>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td><button title="Go to parent folder" type="button" data-id="<%- upper_folder_id %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-arrow-up"></span> .. go up</td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% _.each(items, function(content_item) { %>'); + tmpl_array.push(' <tr class="folder_row light" id="<%- content_item.id %>">'); + tmpl_array.push(' <% if (content_item.get("type") === "folder") { %>'); // folder + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td><button title="Open this folder" type="button" data-id="<%- content_item.id %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-folder-open"></span> browse</td>'); + tmpl_array.push(' <td><%- content_item.get("name") %>'); + tmpl_array.push(' <% if (content_item.get("item_count") === 0) { %>'); // empty folder + tmpl_array.push(' <span class="muted">(empty folder)</span>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' </td>'); + tmpl_array.push(' <td>folder</td>'); // data type + tmpl_array.push(' <td><%= _.escape(content_item.get("item_count")) %> item(s)</td>'); // size + tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <td style="text-align: center; "><input style="margin: 0;" type="checkbox"></td>'); + tmpl_array.push(' <td>'); + tmpl_array.push(' <button title="See details of this dataset" type="button" class="library-dataset btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-eye"></span> details'); + tmpl_array.push(' </button>'); + tmpl_array.push(' </td>'); + tmpl_array.push(' <td><%- content_item.get("name") %></td>'); // dataset + tmpl_array.push(' <td><%= _.escape(content_item.get("data_type")) %></td>'); // data type + tmpl_array.push(' <td><%= _.escape(content_item.get("readable_size")) %></td>'); // size + tmpl_array.push(' <% } %> '); + tmpl_array.push(' <td><%= _.escape(content_item.get("time_updated")) %></td>'); // time updated + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push(' '); + tmpl_array.push(' </tbody>'); + tmpl_array.push('</table>'); + + tmpl_array.push('</div>'); + return tmpl_array.join(''); + }, + templateDatasetModal : function(){ + var tmpl_array = []; + + tmpl_array.push('<div id="dataset_info_modal">'); + tmpl_array.push(' <table class="table table-striped table-condensed">'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row" id="id_row" data-id="<%= _.escape(item.get("ldda_id")) %>">Name</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("name")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Data type</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("data_type")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Genome build</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("genome_build")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <th scope="row">Size</th>'); + tmpl_array.push(' <td><%= _.escape(size) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Date uploaded</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("date_uploaded")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Uploaded by</th>'); + tmpl_array.push(' <td><%= _.escape(item.get("uploaded_by")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr scope="row">'); + tmpl_array.push(' <th scope="row">Data Lines</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_data_lines")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <th scope="row">Comment Lines</th>'); + tmpl_array.push(' <% if (item.get("metadata_comment_lines") === "") { %>'); //folder + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_comment_lines")) %></td>'); + tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <td scope="row">unknown</td>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Number of Columns</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_columns")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Column Types</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("metadata_column_types")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <th scope="row">Miscellaneous information</th>'); + tmpl_array.push(' <td scope="row"><%= _.escape(item.get("misc_blurb")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' </table>'); + tmpl_array.push(' <pre class="peek">'); + tmpl_array.push(' </pre>'); + tmpl_array.push('</div>'); + + return tmpl_array.join(''); + }, + + templateHistorySelectInModal : function(){ + var tmpl_array = []; + + tmpl_array.push('<span id="history_modal_combo" style="width:90%; margin-left: 1em; margin-right: 1em; ">'); + tmpl_array.push('Select history: '); + tmpl_array.push('<select id="dataset_import_single" name="dataset_import_single" style="width:50%; margin-bottom: 1em; "> '); + tmpl_array.push(' <% _.each(histories, function(history) { %>'); //history select box + tmpl_array.push(' <option value="<%= _.escape(history.get("id")) %>"><%= _.escape(history.get("name")) %></option>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push('</select>'); + tmpl_array.push('</span>'); + + return tmpl_array.join(''); + }, + + templateBulkImportInModal : function(){ + var tmpl_array = []; + + tmpl_array.push('<span id="history_modal_combo_bulk" style="width:90%; margin-left: 1em; margin-right: 1em; ">'); + tmpl_array.push('Select history: '); + tmpl_array.push('<select id="dataset_import_bulk" name="dataset_import_bulk" style="width:50%; margin-bottom: 1em; "> '); + tmpl_array.push(' <% _.each(histories, function(history) { %>'); //history select box + tmpl_array.push(' <option value="<%= _.escape(history.get("id")) %>"><%= _.escape(history.get("name")) %></option>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push('</select>'); + tmpl_array.push('</span>'); + + return tmpl_array.join(''); + }, + + // convert size to nice string + size_to_string : function (size) + { + // identify unit + var unit = ""; + if (size >= 100000000000) { size = size / 100000000000; unit = "TB"; } else + if (size >= 100000000) { size = size / 100000000; unit = "GB"; } else + if (size >= 100000) { size = size / 100000; unit = "MB"; } else + if (size >= 100) { size = size / 100; unit = "KB"; } else + { size = size * 10; unit = "b"; } + // return formatted string + return (Math.round(size) / 10) + unit; + }, + +// MMMMMMMMMMMMMMM +// === EVENTS ==== +// MMMMMMMMMMMMMMM + + // event binding + events: { + 'click #select-all-checkboxes' : 'selectAll', + 'click .folder_row' : 'selectClickedRow', + 'click #toolbtn_bulk_import' : 'modalBulkImport', + 'click #toolbtn_dl' : 'bulkDownload', + 'click .library-dataset' : 'showDatasetDetails', + 'click #toolbtn_create_folder' : 'createFolderModal', + 'click .btn_open_folder' : 'navigateToFolder' + }, + + //render the folder view + render: function (options) { + //hack to show scrollbars + $("#center").css('overflow','auto'); + + view = this; + var that = this; + + var folderContainer = new FolderContainer({id: options.id}); + folderContainer.url = folderContainer.attributes.urlRoot + options.id + '/contents'; + + folderContainer.fetch({ + success: function (container) { + + // prepare nice size strings + for (var i = 0; i < folderContainer.attributes.folder.models.length; i++) { + var model = folderContainer.attributes.folder.models[i] + if (model.get('type') === 'file'){ + model.set('readable_size', that.size_to_string(model.get('file_size'))) + } + }; + + // find the upper id + var path = folderContainer.full_path; + var upper_folder_id; + if (path.length === 1){ // library is above us + upper_folder_id = 0; + } else { + upper_folder_id = path[path.length-2][0]; + } + + var template = _.template(that.templateFolder(), { path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id, upper_folder_id: upper_folder_id }); + // var template = _.template(that.templateFolder(), { path: folderContainer.full_path, items: folderContainer.attributes.folder.models, id: options.id }); + that.$el.html(template); + + } + }) + }, + + // handles the click on 'open' and 'upper' folder icons + navigateToFolder : function(event){ + var folder_id = $(event.target).attr('data-id'); + if (typeof folder_id === 'undefined') { + return false; + } else if (folder_id === '0'){ + library_router.navigate('#', {trigger: true, replace: true}); + } else { + library_router.navigate('folders/' + folder_id, {trigger: true, replace: true}); + } + }, + + //show modal with current dataset info + showDatasetDetails : function(event){ + // prevent default + event.preventDefault(); + +//TODO check whether we already have the data + + //load the ID of the row + var id = $(event.target).parent().parent().attr('id'); + + //create new item + var item = new Item(); + var histories = new GalaxyHistories(); + item.id = id; + var self = this; + + //fetch the dataset info + item.fetch({ + success: function (item) { +// TODO can render here already + //fetch user histories for import purposes + histories.fetch({ + success: function (histories){self.renderModalAfterFetch(item, histories)} + }); + } + }); + }, + + // show the current dataset in a modal + renderModalAfterFetch : function(item, histories){ + var size = this.size_to_string(item.get('file_size')); + var template = _.template(this.templateDatasetModal(), { item : item, size : size }); + this.modal = null; + // make modal + var self = this; + this.modal = new mod_modal.GalaxyModal({ + title : 'Dataset Details', + body : template, + buttons : { + 'Import' : function() { self.importCurrentIntoHistory() }, + 'Download' : function() { self.downloadCurrent() }, + 'Close' : function() { self.modal.hide(); $('.modal').remove(); self.modal = null; } // TODO refill nicely modal with data + } + }); + $(".peek").html(item.get("peek")); + var history_footer_tmpl = _.template(this.templateHistorySelectInModal(), {histories : histories.models}); + $(this.modal.elMain).find('.buttons').prepend(history_footer_tmpl); + + // preset last selected history if we know it + if (self.lastSelectedHistory.length > 0) { + $(this.modal.elMain).find('#dataset_import_single').val(self.lastSelectedHistory); + } + + // show the prepared modal + this.modal.show(); + }, + + // download dataset shown currently in modal + downloadCurrent : function(){ + //disable the buttons + this.modal.disableButton('Import'); + this.modal.disableButton('Download'); + + var library_dataset_id = []; + library_dataset_id.push($('#id_row').attr('data-id')); + var url = '/api/libraries/datasets/download/uncompressed'; + var data = {'ldda_ids' : library_dataset_id}; + + // we assume the view is existent + folderContentView.processDownload(url, data); + this.modal.enableButton('Import'); + this.modal.enableButton('Download'); + }, + + // import dataset shown currently in modal into selected history + importCurrentIntoHistory : function(){ + //disable the buttons + this.modal.disableButton('Import'); + this.modal.disableButton('Download'); + + var history_id = $(this.modal.elMain).find('select[name=dataset_import_single] option:selected').val(); + this.lastSelectedHistory = history_id; //save selected history for further use + + var library_dataset_id = $('#id_row').attr('data-id'); + var historyItem = new HistoryItem(); + var self = this; + historyItem.url = historyItem.urlRoot + history_id + '/contents'; + + // save the dataset into selected history + historyItem.save({ content : library_dataset_id, source : 'library' }, { success : function(){ + mod_toastr.success('Dataset imported'); + //enable the buttons + self.modal.enableButton('Import'); + self.modal.enableButton('Download'); + }, error : function(){ + mod_toastr.error('An error occured! Dataset not imported. Please try again.') + //enable the buttons + self.modal.enableButton('Import'); + self.modal.enableButton('Download'); + } + }); + }, + + // select all datasets + selectAll : function (event) { + var selected = event.target.checked; + that = this; + // Iterate each checkbox + $(':checkbox').each(function () { + this.checked = selected; + $row = $(this.parentElement.parentElement); + // Change color of selected/unselected + (selected) ? that.makeDarkRow($row) : that.makeWhiteRow($row); + }); + // Show the tools in menu + this.checkTools(); + }, + + // Check checkbox on row itself or row checkbox click + selectClickedRow : function (event) { + var checkbox = ''; + var $row; + var source; + if (event.target.localName === 'input'){ + checkbox = event.target; + $row = $(event.target.parentElement.parentElement); + source = 'input'; + } else if (event.target.localName === 'td') { + checkbox = $("#" + event.target.parentElement.id).find(':checkbox')[0]; + $row = $(event.target.parentElement); + source = 'td'; + } + if (checkbox === '') {event.stopPropagation(); return;} // button in row was clicked + + if (checkbox.checked){ + if (source==='td'){ + checkbox.checked = ''; + this.makeWhiteRow($row); + } else if (source==='input') { + this.makeDarkRow($row); + } + } else { + if (source==='td'){ + checkbox.checked = 'selected'; + this.makeDarkRow($row); + } else if (source==='input') { + this.makeWhiteRow($row); + } + } + this.checkTools(); + }, + + makeDarkRow: function($row){ + $row.removeClass('light'); + $row.find('a').removeClass('light'); + $row.addClass('dark'); + $row.find('a').addClass('dark'); + }, + + makeWhiteRow: function($row){ + $row.removeClass('dark'); + $row.find('a').removeClass('dark'); + $row.addClass('light'); + $row.find('a').addClass('light'); + }, + + // show toolbar in case something is selected + checkTools : function(){ + var checkedValues = $('#folder_table').find(':checked'); + if(checkedValues.length > 0){ + $('#toolbtn_bulk_import').show(); + $('#toolbtn_dl').show(); + } else { + $('#toolbtn_bulk_import').hide(); + $('#toolbtn_dl').hide(); + } + + }, + + // show bulk import modal + modalBulkImport : function(){ + var self = this; + // fetch histories + var histories = new GalaxyHistories(); + histories.fetch({ + success: function (histories){ + // make modal + var history_modal_tmpl = _.template(self.templateBulkImportInModal(), {histories : histories.models}); + self.modal = new mod_modal.GalaxyModal({ + title : 'Import into History', + body : history_modal_tmpl, + buttons : { + 'Import' : function() {self.importAllIntoHistory()}, + 'Close' : function() {self.modal.hide(); $('.modal').remove(); self.modal = null;} + } + }); + // show the prepared modal + self.modal.show(); + } + }); + }, + + // import all selected datasets into history + importAllIntoHistory : function (){ + //disable the button + this.modal.disableButton('Import'); + + var history_id = $("select[name=dataset_import_bulk] option:selected").val(); + var history_name = $("select[name=dataset_import_bulk] option:selected").text(); + + var dataset_ids = []; + $('#folder_table').find(':checked').each(function(){ + if (this.parentElement.parentElement.id != '') { + dataset_ids.push(this.parentElement.parentElement.id); + } + }); + var progress_bar_tmpl = _.template(this.templateProgressBar(), { history_name : history_name }); + $(this.modal.elMain).find('.modal-body').html(progress_bar_tmpl); + + // init the progress bar + var progressStep = 100 / dataset_ids.length; + this.initProgress(progressStep); + + // prepare the dataset objects to be imported + var datasets_to_import = []; + for (var i = dataset_ids.length - 1; i >= 0; i--) { + library_dataset_id = dataset_ids[i]; + var historyItem = new HistoryItem(); + var self = this; + historyItem.url = historyItem.urlRoot + history_id + '/contents'; + historyItem.content = library_dataset_id; + historyItem.source = 'library'; + datasets_to_import.push(historyItem); + }; + + // call the recursive function to call ajax one after each other + this.chainCall(datasets_to_import); + }, + + chainCall: function(history_item_set){ + var self = this; + var popped_item = history_item_set.pop(); + if (typeof popped_item === "undefined") { + mod_toastr.success('All datasets imported'); + this.modal.hide(); + // enable button again + self.modal.enableButton('Import'); + return + } + var promise = $.when(popped_item.save({content: popped_item.content, source: popped_item.source})).done(function(a1){ + self.updateProgress(); + responses.push(a1); + self.chainCall(history_item_set); + }); + }, + + initProgress: function(progressStep){ + this.progress = 0; + this.progressStep = progressStep; + }, + updateProgress: function(){ + this.progress += this.progressStep; + $('.progress-bar').width(Math.round(this.progress) + '%'); + txt_representation = Math.round(this.progress) + '% Complete'; + $('.completion_span').text(txt_representation); + }, + + // progress bar + templateProgressBar : function (){ + var tmpl_array = []; + + tmpl_array.push('<div class="import_text">'); + tmpl_array.push('Importing selected datasets to history <b><%= _.escape(history_name) %></b>'); + tmpl_array.push('</div>'); + tmpl_array.push('<div class="progress">'); + tmpl_array.push(' <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 00%;">'); + tmpl_array.push(' <span class="completion_span">0% Complete</span>'); + tmpl_array.push(' </div>'); + tmpl_array.push('</div>'); + tmpl_array.push(''); + + return tmpl_array.join(''); + }, + + // download selected datasets + download : function(folder_id, format){ + var dataset_ids = []; + $('#folder_table').find(':checked').each(function(){ + if (this.parentElement.parentElement.id != '') { + dataset_ids.push(this.parentElement.parentElement.id); + } + }); + + var url = '/api/libraries/datasets/download/' + format; + var data = {'ldda_ids' : dataset_ids}; + this.processDownload(url, data, 'get'); + }, + + // create hidden form and submit through POST to initialize download + processDownload: function(url, data, method){ + //url and data options required + if( url && data ){ + //data can be string of parameters or array/object + data = typeof data == 'string' ? data : $.param(data); + //split params into form inputs + var inputs = ''; + $.each(data.split('&'), function(){ + var pair = this.split('='); + inputs+='<input type="hidden" name="'+ pair[0] +'" value="'+ pair[1] +'" />'; + }); + //send request + $('<form action="'+ url +'" method="'+ (method||'post') +'">'+inputs+'</form>') + .appendTo('body').submit().remove(); + mod_toastr.info('Your download will begin soon'); + }; + }, + + // shows modal for creating folder + createFolderModal: function(){ + mod_toastr.info('This will create folder...in the future'); + } + + }); + +// galaxy library view +var GalaxyLibraryview = Backbone.View.extend({ + el: '#center', + + events: { + 'click #create_new_library_btn' : 'show_library_modal' + }, + + // initialize + initialize : function(){ + }, + + // template + template_library_list : function (){ + tmpl_array = []; + tmpl_array.push('<div id="library_container" style="width: 90%; margin: auto; margin-top: 2em; overflow: auto !important; ">'); + + tmpl_array.push(''); + tmpl_array.push('<h3>New Data Libraries. This is work in progress. Report problems & ideas to <a href="mailto:marten@bx.psu.edu?Subject=DataLibraries_Feedback" target="_blank">Marten</a>.</h3>'); + tmpl_array.push('<a href="" id="create_new_library_btn" class="btn btn-primary file ">New Library</a>'); + tmpl_array.push('<table class="table table-condensed">'); + tmpl_array.push(' <thead>'); + tmpl_array.push(' <th class="button_heading"></th>'); + tmpl_array.push(' <th>name</th>'); + tmpl_array.push(' <th>description</th>'); + tmpl_array.push(' <th>synopsis</th> '); + tmpl_array.push(' <th>model type</th> '); + tmpl_array.push(' </thead>'); + tmpl_array.push(' <tbody>'); + tmpl_array.push(' <% _.each(libraries, function(library) { %>'); + tmpl_array.push(' <tr>'); + tmpl_array.push(' <td><button title="Open this library" type="button" data-id="<%- library.get("root_folder_id") %>" class="btn_open_folder btn btn-default btn-xs">'); + tmpl_array.push(' <span class="fa fa-folder-open"></span> browse</td>'); + tmpl_array.push(' <td><%- library.get("name") %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("description")) %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("synopsis")) %></td>'); + tmpl_array.push(' <td><%= _.escape(library.get("model_class")) %></td>'); + tmpl_array.push(' </tr>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push(' </tbody>'); + tmpl_array.push('</table>'); + + tmpl_array.push('</div>'); + return tmpl_array.join(''); + }, + + // render + render: function () { + //hack to show scrollbars due to #center element inheritance + $("#center").css('overflow','auto'); + + var that = this; + libraries = new Libraries(); + + libraries.fetch({ + success: function (libraries) { + var template = _.template(that.template_library_list(), { libraries : libraries.models }); + that.$el.html(template); + }, + error: function(model, response){ + if (response.statusCode().status === 403){ + mod_toastr.error('Please log in first. Redirecting to login page in 3s.'); + setTimeout(that.redirectToLogin, 3000); + } else { + mod_toastr.error('An error occured. Please try again.'); + } + } + }) + }, + + redirectToHome: function(){ + window.location = '../'; + }, + redirectToLogin: function(){ + window.location = '/user/login'; + }, + + // own modal + modal : null, + + // show/hide create library modal + show_library_modal : function (event){ + event.preventDefault(); + event.stopPropagation(); + + // create modal + var self = this; + this.modal = new mod_modal.GalaxyModal( + { + title : 'Create New Library', + body : this.template_new_library(), + buttons : { + 'Create' : function() {self.create_new_library_event()}, + 'Close' : function() {self.modal.hide()} + } + }); + + // show prepared modal + this.modal.show(); + }, + + // create the new library from modal + create_new_library_event: function(){ + var libraryDetails = this.serialize_new_library(); + if (this.validate_new_library(libraryDetails)){ + var library = new Library(); + var self = this; + library.save(libraryDetails, { + success: function (library) { + self.modal.hide(); + self.clear_library_modal(); + self.render(); + mod_toastr.success('Library created'); + }, + error: function(){ + mod_toastr.error('An error occured :('); + } + }); + } else { + mod_toastr.error('Library\'s name is missing'); + } + return false; + }, + + // clear the library modal once saved + clear_library_modal : function(){ + $("input[name='Name']").val(''); + $("input[name='Description']").val(''); + $("input[name='Synopsis']").val(''); + }, + + // serialize data from the form + serialize_new_library : function(){ + return { + name: $("input[name='Name']").val(), + description: $("input[name='Description']").val(), + synopsis: $("input[name='Synopsis']").val() + }; + }, + + // validate new library info + validate_new_library: function(libraryDetails){ + return libraryDetails.name !== ''; + }, + + // template for new library modal + template_new_library: function(){ + tmpl_array = []; + + tmpl_array.push('<div id="new_library_modal">'); + tmpl_array.push('<form>'); + tmpl_array.push('<input type="text" name="Name" value="" placeholder="Name">'); + tmpl_array.push('<input type="text" name="Description" value="" placeholder="Description">'); + tmpl_array.push('<input type="text" name="Synopsis" value="" placeholder="Synopsis">'); + tmpl_array.push('</form>'); + tmpl_array.push('</div>'); + + return tmpl_array.join(''); + } +}); + +// galaxy library wrapper View +var GalaxyLibrary = Backbone.View.extend({ + folderContentView : null, + galaxyLibraryview : null, + initialize : function(){ + + folderContentView = new FolderContentView(); + galaxyLibraryview = new GalaxyLibraryview(); + library_router = new LibraryRouter(); + + library_router.on('route:libraries', function() { + // render libraries list + galaxyLibraryview.render(); + }); + + library_router.on('route:folder_content', function(id) { + // render folder's contents + folderContentView.render({id: id}); + }); + + library_router.on('route:download', function(folder_id, format) { + if ($('#center').find(':checked').length === 0) { + // this happens rarely when there is a server/data error and client gets an actual response instead of an attachment + // we don't know what was selected so we can't download again, we redirect to the folder provided + library_router.navigate('folders/' + folder_id, {trigger: true, replace: true}); + } else { + // send download stream + folderContentView.download(folder_id, format); + library_router.navigate('folders/' + folder_id, {trigger: false, replace: true}); + } + }); + +Backbone.history.start(); + +return this +} +}); + +// return +return { + GalaxyApp: GalaxyLibrary +}; + +}); diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b static/scripts/galaxy.menu.js --- a/static/scripts/galaxy.menu.js +++ b/static/scripts/galaxy.menu.js @@ -57,7 +57,12 @@ tab_shared.add({ title : "Data Libraries", - content : "library/index", + content : "library/index" + }); + + tab_shared.add({ + title : "New Libraries", + content : "library/list", divider : true }); diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b static/scripts/galaxy.modal.js --- a/static/scripts/galaxy.modal.js +++ b/static/scripts/galaxy.modal.js @@ -1,6 +1,3 @@ -/* - galaxy modal -*/ // dependencies define([], function() { @@ -25,8 +22,50 @@ // initialize initialize : function(options) { + self = this; if (options) this.create(options); + + // Bind the hiding events + this.bindEvents(event, self); + }, + + // bind the click-to-hide function + bindEvents: function(event, that) { + // bind the ESC key to hide() function + $(document).on('keyup', function(event){ + if (event.keyCode == 27) { self.hide(); } + }) + // bind the 'click anywhere' to hide() function... + $('html').on('click', function(event){ + that.hide(); + }) + // ...but don't hide if the click is on modal content + $('.modal-content').on('click', function(event){ + event.stopPropagation(); + }) + }, + + // unbind the click-to-hide function + unbindEvents: function(event, that){ + // bind the ESC key to hide() function + $(document).off('keyup', function(event){ + if (event.keyCode == 27) { that.hide(); } + }) + // unbind the 'click anywhere' to hide() function... + $('html').off('click', function(event){ + that.hide(); + }) + $('.modal-content').off('click', function(event){ + event.stopPropagation(); + }) + }, + + // destroy + destroy : function(){ + this.hide(); + this.unbindEvents(); + $('.modal').remove(); }, // adds and displays a new frame/window @@ -63,6 +102,7 @@ // set flag this.visible = false; + this.unbindEvents(); }, // create @@ -120,6 +160,15 @@ this.$buttons.find('#' + String(name).toLowerCase()).prop('disabled', true); }, + // hide buttons + hideButton: function(name) { + this.$buttons.find('#' + String(name).toLowerCase()).hide(); + }, + // show buttons + showButton: function(name) { + this.$buttons.find('#' + String(name).toLowerCase()).show(); + }, + // returns scroll top for body element scrollTop: function() { @@ -139,7 +188,7 @@ '<div class="modal-header">' + '<button type="button" class="close" style="display: none;">×</button>' + '<h4 class="title">' + title + '</h4>' + - '</div>' + + '</div>' + '<div class="modal-body"></div>' + '<div class="modal-footer">' + '<div class="buttons" style="float: right;"></div>' + diff -r e304da0a99681c7fb8538c6861844f17c375097e -r c1963ee06cb0fc066c111b23d99a3ea49d66f90b static/scripts/libs/bootstrap.js --- a/static/scripts/libs/bootstrap.js +++ b/static/scripts/libs/bootstrap.js @@ -575,3 +575,158 @@ } }(window.jQuery); + +/* ======================================================================== + * Bootstrap: dropdown.js v3.0.0 + * http://twbs.github.com/bootstrap/javascript.html#dropdowns + * ======================================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + + ++function ($) { "use strict"; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle=dropdown]' + var Dropdown = function (element) { + var $el = $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we we use a backdrop because click events don't delegate + $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus) + } + + $parent.trigger(e = $.Event('show.bs.dropdown')) + + if (e.isDefaultPrevented()) return + + $parent + .toggleClass('open') + .trigger('shown.bs.dropdown') + + $this.focus() + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27)/.test(e.keyCode)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) { + if (e.which == 27) $parent.find(toggle).focus() + return $this.click() + } + + var $items = $('[role=menu] li:not(.divider):visible a', $parent) + + if (!$items.length) return + + var index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index=0 + + $items.eq(index).focus() + } + + function clearMenus() { + $(backdrop).remove() + $(toggle).each(function (e) { + var $parent = getParent($(this)) + if (!$parent.hasClass('open')) return + $parent.trigger(e = $.Event('hide.bs.dropdown')) + if (e.isDefaultPrevented()) return + $parent.removeClass('open').trigger('hidden.bs.dropdown') + }) + } + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + var old = $.fn.dropdown + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('dropdown') + + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== + + $(document) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api' , toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + +}(window.jQuery); \ No newline at end of file This diff is so big that we needed to truncate the remainder. Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.