diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e78f10f3715..933b912bd84 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,9 +27,8 @@ the [development mailing list](http://dev.list.galaxyproject.org/). ## How to Contribute * All changes to the [core galaxy project](https://github.com/galaxyproject/galaxy) - should be made through pull requests to this repository except when - serious security vulnerabilities must simulatenously be made to multiple - previously released versions of Galaxy. + should be made through pull requests to this repository (with just two + exceptions outlined below). * If you are new to Git - please check out the very useful tutorials on the [GitHub training hub](https://training.github.com/kit/) @@ -102,3 +101,74 @@ developers. More information about tool development can be found [on the wiki](https://wiki.galaxyproject.org/Develop). + +## Handling Pull Requests + +Everyone is encouraged to express opinions and issue non-binding votes on pull +requests, but only members of the *committers* group may issue binding votes +on pull requests. Information on the *committers* group can be found in the +[organization document](https://github.com/galaxyproject/galaxy/blob/dev/doc/source/project/organization.rst) +describing governance of the core Galaxy code base. + +Votes on pull requests should take the form of +[+1, 0, -1, and fractions](http://www.apache.org/foundation/voting.html) +as outlined by the Apache Foundation. + +Pull requests modifying pre-existing releases should be restricted to bug fixes +and require at least 2 *+1* binding votes from someone other than the author of +the pull request with no *-1* binding votes. + +Pull requests changing or clarifying the procedures governing this repository: + +- Must be made to the ``dev`` branch of this repository. +- Must remain open for at least 192 hours (unless every qualified committer has + voted). +- Require binding *+1* votes from at least 25% of qualified *committers* with no + *-1* binding votes. +- Should be titled with the prefix *[PROCEDURES]* and tagged with + the *procedures* tag in Github. +- Should not be modified once open. If changes are needed, the pull request + should be closed, re-opened with modifications, and votes reset. +- Should be restricted to just modifying the procedures and generally should not + contain code modifications. +- If the pull request adds or removes committers, there must be a separate + pull request for each person added or removed. + +Any other pull request requires at least 1 *+1* binding vote from someone other +than the author of the pull request. + +### Timelines + +Except in the case of pull requests modifying governance procedures, there are +generally no objective guidelines defining how long pull requests must remain +open for comment. Subjectively speaking though - larger and more potentially +controversial pull requests containing enhancements should remain open for a at +least a few days to give everyone the opportunity to weigh in. + +### Vetos + +A note on vetos (*-1* votes) taken verbatim from the +[Apache Foundation](http://www.apache.org/foundation/voting.html): + +>"A code-modification proposal may be stopped dead in its tracks by a -1 vote +by a qualified voter. This constitutes a veto, and it cannot be overruled nor +overridden by anyone. Vetos stand until and unless withdrawn by their casters. +> +>To prevent vetos from being used capriciously, they must be accompanied by a +technical justification showing why the change is bad (opens a security +exposure, negatively affects performance, etc. ). A veto without a +justification is invalid and has no weight." + +For votes regarding non-coding issues such as procedure changes, the requirement +that a veto is accompanied by a *technical* justification is relaxed somewhat, +though a well reasoned justification must still be included. + +### Direct Commit Access + +The Galaxy *committers* group may only commit directly to Galaxy (i.e. outside +of a pull request and not following the procedures described here) the +following two categories of patches: + +* Patches for serious security vulnerabilities. +* Cherry-picking and/or merging of existing approved commits to other +branches. diff --git a/client/GruntFile.js b/client/GruntFile.js index 57ca772887b..acc9fd98f1d 100644 --- a/client/GruntFile.js +++ b/client/GruntFile.js @@ -1,6 +1,25 @@ module.exports = function(grunt) { "use strict"; + var GALAXY_PATHS = { + dist : '../static/scripts', + maps : '../static/maps', + // this symlink allows us to serve uncompressed scripts in DEV_PATH for use with sourcemaps + srcSymlink : '../static/src', + }, + TOOLSHED_PATHS = { + dist : '../static/toolshed/scripts', + maps : '../static/toolshed/maps', + srcSymlink : '../static/toolshed/src', + }; + + grunt.config.set( 'app', 'galaxy' ); + grunt.config.set( 'paths', GALAXY_PATHS ); + if( grunt.option( 'app' ) === 'toolshed' ){ + grunt.config.set( 'app', grunt.option( 'app' ) ); + grunt.config.set( 'paths', TOOLSHED_PATHS ); + } + // see the sub directory grunt-tasks/ for individual task definitions grunt.loadTasks( 'grunt-tasks' ); grunt.registerTask( 'default', [ 'uglify' ] ); diff --git a/client/README.md b/client/README.md index 40fd7d8207e..8aa828dbeb2 100644 --- a/client/README.md +++ b/client/README.md @@ -69,3 +69,13 @@ version, you'll need to use the full, local path when calling it: ./node_modules/.bin/grunt # or ./node_modules/.bin/grunt watch + + +The Toolshed Client Build +========================= + +The commands mentioned above in 'Rebuilding' and 'Grunt watch' also can be applied to toolshed scripts by using the +`--app=toolshed` option: + + grunt watch --app=toolshed + grunt --app=toolshed \ No newline at end of file diff --git a/client/galaxy/scripts/mvc/groups/group-detail-view.js b/client/galaxy/scripts/mvc/groups/group-detail-view.js new file mode 100644 index 00000000000..062ae79274e --- /dev/null +++ b/client/galaxy/scripts/mvc/groups/group-detail-view.js @@ -0,0 +1,134 @@ +define([ + "libs/toastr", + "mvc/groups/group-model" + ], +function( + mod_toastr, + mod_group_model + ) { + +// toolshed group detail view +var GroupDetailView = Backbone.View.extend({ + el: "#groups_element", + options: {}, + app: null, + + initialize : function( options ){ + this.options = _.extend( this.options, options ); + this.app = window.globalTS.groups; + + if ( this.app.collection !== null ){ + this.model = this.app.collection.get( this.options.group_id ); + this.render(); + } else { + this.fetchGroup(); + } + + }, + + fetchGroup: function( options ){ + var that = this; + this.options = _.extend( this.options, options ); + this.model = new mod_group_model.Group( { id: this.options.group_id } ); + this.model.fetch({ + success: function( model ){ + console.log( 'received data: ' ); + console.log( model ); + that.render(); + }, + error: function( model, response ){ + if ( typeof response.responseJSON !== "undefined" ){ + mod_toastr.error( response.responseJSON.err_msg ); + } else { + mod_toastr.error( 'An error ocurred.' ); + } + } + }); + }, + + render: function(){ + var template = this.templateRow(); + this.$el.html( template( { group: this.model } ) ); + $("#center [data-toggle]").tooltip(); + }, + + templateRow: function() { + return _.template([ + '
', + '

<%= group.get("name") %>

', + '

', + 'A group of <%= group.get("members").length %> members with <%= group.get("repositories").length %> repositories and a total of <%= group.get("total_downloads") %> combined tool downloads.

', + + '

Members

', + '', + '', + '', + '', + '', + '', + '', + '<% _.each(group.get("members"), function(member) { %>', + '', + '', + '', + '', + '', + '<% }); %>', + '', + '
NameRepositoriesRegistered
', + '<%= member.username %>', + '', + '<%= member.user_repos_count %>', + '', + '<%= member.time_created %>', + '
', + + '

Repositories

', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '<% _.each(group.get("repositories"), function(repo) { %>', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '<% }); %>', + '', + '
NameDescriptionDownloadsOwnerCreatedRatingVerified
', + '<%= repo.name %>', + '', + '<%= repo.description %>', + '', + '<%= repo.times_downloaded %>', + '', + '<%= repo.owner %>', + '', + '<%= repo.time_created %>', + '', + '<%= repo.ratings_mean %>', + '', + '<%= repo.approved %>', + '
', + '
' + ].join('')); + } +}); +return { + GroupDetailView: GroupDetailView +}; + +}); + diff --git a/client/galaxy/scripts/mvc/groups/group-list-view.js b/client/galaxy/scripts/mvc/groups/group-list-view.js new file mode 100644 index 00000000000..3adeb0feefb --- /dev/null +++ b/client/galaxy/scripts/mvc/groups/group-list-view.js @@ -0,0 +1,148 @@ +define([ + "libs/toastr", + "mvc/groups/group-model", + "mvc/groups/group-listrow-view" + +], function( + mod_toastr, + mod_group_model, + mod_group_listrow_view +){ + +var GroupListView = Backbone.View.extend({ + el: '#groups_element', + defaults: {}, + + /** + * Initialize and fetch the groups from server. + * Async render afterwards. + * @param {object} options an object with options + */ + initialize : function( options ){ + this.options = _.defaults( this.options || {}, this.defaults, options ); + var that = this; + window.globalTS.groups.collection = new mod_group_model.Groups(); + window.globalTS.groups.collection.fetch({ + success: function( model ){ + console.log( 'received data: ' ); + console.log( model ); + that.render(); + }, + error: function( model, response ){ + if ( typeof response.responseJSON !== "undefined" ){ + mod_toastr.error( response.responseJSON.err_msg ); + } else { + mod_toastr.error( 'An error ocurred.' ); + } + } + }); + + }, + + fetch: function(){ + }, + + /** + * Render the groups table from the object's own collection. + */ + render: function ( options ) { + this.options = _.extend( this.options, options ); + $( ".tooltip" ).hide(); + var template = this.templateGroupsList(); + this.$el.html( template( { length: 10, order: 'asc' } ) ); + this.renderRows( window.globalTS.groups.collection.models ); + $( "#center [data-toggle]" ).tooltip(); + $( "#center" ).css( 'overflow','auto' ); + }, + + /** + * Render all given models as rows in the groups list + * @param {array} groups_to_render array of group models to render + */ + renderRows: function( groups_to_render ){ + for ( var i = 0; i < groups_to_render.length; i++ ) { + var group = groups_to_render[i]; + this.renderOne( { group: group } ) + } + }, + + /** + * Create a view for the given model and add it to the groups view. + * @param {Group} model of the view that will be rendered + */ + renderOne: function( options ){ + var rowView = new mod_group_listrow_view.GroupListRowView( options ); + this.$el.find( '#group_list_body' ).append( rowView.el ); + }, + + /** + * Table heading was clicked, update sorting preferences and re-render. + * @return {[type]} [description] + */ + // sort_clicked : function(){ + // if (Galaxy.libraries.preferences.get('sort_order') === 'asc'){ + // Galaxy.libraries.preferences.set({'sort_order': 'desc'}); + // } else { + // Galaxy.libraries.preferences.set({'sort_order': 'asc'}); + // } + // this.render(); + // }, + + /** + * Sort the underlying collection according to the parameters received. + * Currently supports only sorting by name. + */ + // sortLibraries: function(){ + // if (Galaxy.libraries.preferences.get('sort_by') === 'name'){ + // if (Galaxy.libraries.preferences.get('sort_order') === 'asc'){ + // this.collection.sortByNameAsc(); + // } else if (Galaxy.libraries.preferences.get('sort_order') === 'desc'){ + // this.collection.sortByNameDesc(); + // } + // } + // }, + + // redirectToHome: function(){ + // window.location = '../'; + // }, + // redirectToLogin: function(){ + // window.location = '/user/login'; + // }, + +// MMMMMMMMMMMMMMMMMM +// === TEMPLATES ==== +// MMMMMMMMMMMMMMMMMM + + templateGroupsList: function(){ + tmpl_array = []; + + tmpl_array.push('
'); + tmpl_array.push('
'); + tmpl_array.push('
'); + tmpl_array.push('<% if(length === 0) { %>'); + tmpl_array.push('
There are no groups yet.
'); + tmpl_array.push('<% } else{ %>'); + tmpl_array.push(''); + tmpl_array.push(' '); + tmpl_array.push(' '); + tmpl_array.push(' '); + tmpl_array.push(' '); + tmpl_array.push(' '); + tmpl_array.push(' '); + tmpl_array.push(' '); + // group item views will attach here + tmpl_array.push(' '); + tmpl_array.push('
name descriptionmembersrepositories
'); + tmpl_array.push('<% }%>'); + tmpl_array.push('
'); + + return _.template(tmpl_array.join('')); + }, + +}); + +return { + GroupListView: GroupListView +}; + +}); diff --git a/client/galaxy/scripts/mvc/groups/group-listrow-view.js b/client/galaxy/scripts/mvc/groups/group-listrow-view.js new file mode 100644 index 00000000000..47421bbab79 --- /dev/null +++ b/client/galaxy/scripts/mvc/groups/group-listrow-view.js @@ -0,0 +1,36 @@ +define([], +function() { + +// toolshed group row view +var GroupListRowView = Backbone.View.extend({ + events: {}, + + initialize : function( options ){ + this.render( options.group ); + }, + + render: function( group ){ + var tmpl = this.templateRow(); + this.setElement(tmpl( { group:group } )); + this.$el.show(); + return this; + }, + + templateRow: function() { + return _.template([ + '">', + '"><%= group.get("name") %>', + 'description', + '<%= group.get("total_members") %>', + '<%= group.get("total_repos") %>', + '' + ].join('')); + } + +}); + +return { + GroupListRowView: GroupListRowView +}; + +}); diff --git a/client/galaxy/scripts/mvc/groups/group-model.js b/client/galaxy/scripts/mvc/groups/group-model.js new file mode 100644 index 00000000000..e9f810bbbab --- /dev/null +++ b/client/galaxy/scripts/mvc/groups/group-model.js @@ -0,0 +1,22 @@ +define([], function() { + +// ============================================================================ +// LIBRARY RELATED MODELS + + var Group = Backbone.Model.extend({ + urlRoot: '/api/groups', + }); + + var Groups = Backbone.Collection.extend({ + url: '/api/groups', + + model: Group, + + }); + +return { + Group: Group, + Groups: Groups +}; + +}); diff --git a/client/galaxy/scripts/toolshed.groups.js b/client/galaxy/scripts/toolshed.groups.js new file mode 100644 index 00000000000..1785878ed51 --- /dev/null +++ b/client/galaxy/scripts/toolshed.groups.js @@ -0,0 +1,51 @@ +// MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +// === MAIN TOOLSHED GROUP MODULE ==== +// MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM + +define([ + "mvc/groups/group-list-view", + "mvc/groups/group-detail-view", + "mvc/groups/group-model" + ], + function( + mod_group_list, + mod_group_detail, + mod_group_model + ) { + +// ============================================================================ +// ROUTER +var ToolshedRouter = Backbone.Router.extend({ + routes: { + "" : "groups", + ":group_id" : "group_page" + } + +}); + +var ToolshedGroups = Backbone.View.extend({ + groupListView: null, + groupDetailView: null, + collection: null, + + initialize : function(){ + window.globalTS.groups = this; + + this.ts_router = new ToolshedRouter(); + this.ts_router.on( 'route:groups', function() { + window.globalTS.groups.groupListView = new mod_group_list.GroupListView(); + }); + + this.ts_router.on( 'route:group_page', function( group_id ) { + window.globalTS.groups.groupDetailView = new mod_group_detail.GroupDetailView( { group_id: group_id } ); + }); + + Backbone.history.start({pushState: false}); + } +}); + +return { + ToolshedGroups: ToolshedGroups +}; + +}); diff --git a/client/galaxy/style/less/base.less b/client/galaxy/style/less/base.less index e8a6e351351..80e8444b404 100644 --- a/client/galaxy/style/less/base.less +++ b/client/galaxy/style/less/base.less @@ -134,6 +134,14 @@ body { #left-border { left: 250px; } +.reports-panel-container { + position: absolute; + top: @panel_header_height; + bottom: 10px; + overflow: scroll; + width: 100%; + padding: 10px; +} #center { left:250 + @border-default-color_padding; right: 250 + @border-default-color_padding; diff --git a/client/grunt-tasks/scripts.js b/client/grunt-tasks/scripts.js index cd48a99d5dc..0a66ca2ecfc 100644 --- a/client/grunt-tasks/scripts.js +++ b/client/grunt-tasks/scripts.js @@ -6,25 +6,22 @@ module.exports = function( grunt ){ "use strict"; - var DIST_PATH = '../static/scripts', - MAPS_PATH = '../static/maps', - // this symlink allows us to serve uncompressed scripts in DEV_PATH for use with sourcemaps - SRC_SYMLINK = '../static/src'; + var app = grunt.config.get( 'app' ), + paths = grunt.config.get( 'paths' ); - // compress javascript grunt.config( 'uglify', { target : { files: [{ expand : true, - cwd : SRC_SYMLINK, + cwd : paths.srcSymlink, src : '**/*.js', - dest : DIST_PATH + dest : paths.dist }], options : { sourceMap : true, sourceMapName : function( path ){ // rewrite name to have all source maps in 'static/maps' - return path.replace( DIST_PATH, MAPS_PATH ) + '.map'; + return path.replace( paths.dist, paths.maps ) + '.map'; } } }, @@ -53,19 +50,6 @@ module.exports = function( grunt ){ } }); - // use 'grunt watch' (from a new tab in your terminal) to have grunt re-copy changed files automatically - grunt.config( 'watch', { - watch: { - // watch for changes in the src dir - files: [ SRC_SYMLINK + '/**' ], - tasks: [ 'uglify' ], - // tasks: [ 'copy', 'pack' ], - options: { - spawn: false - } - }, - }); - // -------------------------------------------------------------------------- decompress for easier debugging grunt.registerTask( 'decompress', function(){ grunt.log.writeln( "decompressing... (don't forget to call 'grunt' again before committing)" ); @@ -76,10 +60,18 @@ module.exports = function( grunt ){ // alias for symmetry grunt.registerTask( 'compress', [ 'uglify' ] ); - // -------------------------------------------------------------------------- copy,pack only those changed - // adapted from grunt-contrib-watch jslint example - //TODO: a bit hacky and there's prob. a better way - //NOTE: copy will fail silently if a file isn't found + // -------------------------------------------------------------------------- watch & copy,pack only those changed + // use 'grunt watch' (from a new tab in your terminal) to have grunt re-copy changed files automatically + grunt.config( 'watch', { + watch: { + // watch for changes in the src dir + files: [ paths.srcSymlink + '/**' ], + tasks: [ 'uglify' ], + options: { + spawn: false + } + } + }); // outer scope variable for the event handler and onChange fn - begin with empty hash var changedFiles = Object.create(null); @@ -89,23 +81,24 @@ module.exports = function( grunt ){ grunt.log.writeln( 'onChange, changedFiles:', Object.keys( changedFiles ) ); grunt.config( 'uglify.target.files', [{ expand : true, - cwd : SRC_SYMLINK, + cwd : paths.srcSymlink, src : Object.keys( changedFiles ), - dest : DIST_PATH + dest : paths.dist }]); changedFiles = Object.create(null); }, 200); - grunt.event.on( 'watch', function( action, filepath ) { + var addChangedFile = function( action, filepath ) { // store each filepath in a Files obj, the debounced fn above will use it as an aggregate list for copying // we need to take galaxy/scripts out of the filepath or it will be copied to the wrong loc - filepath = filepath.replace( SRC_SYMLINK + '/', '' ); - grunt.log.writeln( 'on.watch, filepath:', filepath ); - changedFiles[filepath] = action; + filepath = filepath.replace( paths.srcSymlink + '/', '' ); + // grunt.log.writeln( 'on.watch, filepath:', filepath ); + changedFiles[ filepath ] = action; onChange(); - }); - + }; + grunt.event.on( 'watch', addChangedFile ); + // -------------------------------------------------------------------------- grunt.loadNpmTasks( 'grunt-contrib-watch' ); grunt.loadNpmTasks( 'grunt-contrib-uglify' ); }; \ No newline at end of file diff --git a/client/toolshed/scripts/test-file.js b/client/toolshed/scripts/test-file.js new file mode 100644 index 00000000000..093dfad294e --- /dev/null +++ b/client/toolshed/scripts/test-file.js @@ -0,0 +1,3 @@ +define( [], function() { + return 'Remove this file'; +}); \ No newline at end of file diff --git a/config/datatypes_conf.xml.sample b/config/datatypes_conf.xml.sample index e24226f0be6..89bbc3ec29f 100644 --- a/config/datatypes_conf.xml.sample +++ b/config/datatypes_conf.xml.sample @@ -272,6 +272,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/project/organization.rst b/doc/source/project/organization.rst new file mode 100644 index 00000000000..54427a5b4bf --- /dev/null +++ b/doc/source/project/organization.rst @@ -0,0 +1,86 @@ +================================== +Galaxy Core Governance +================================== + +This document informally outlines the organizational structure governing the +Galaxy core code base hosted at https://github.com/galaxyproject/galaxy. This +governance extends to code-related activities of this repository such as +releases and packaging. This governance does not include infrastructure such +as Galaxy's Trello board, the Galaxy mailing lists, etc... or other Galaxy- +related projects belonging to the ``galaxyproject`` organization on GitHub. + +Committers +========== + +The `committers` group is the group of trusted developers and advocates who +manage the core Galaxy code base. They assume many roles required to achieve +the project's goals, especially those that require a high level of trust. + +Galaxy Project `committers` are the only individuals who may commit to the +core Galaxy code base. All commits must be made in accordance with procedures +outlined in Galaxy's CONTRIBUTING_ document. In particular, in most cases +direct commit access is not allowed and this access is restricted to merging +pull requests issued by others. + +Committers may participate in formal votes - these votes typically include +votes to modify team membership, merge pull requests, and modify or clarify +the procedures outlined in this document and CONTRIBUTING_. + +Members +---------- + +- Enis Afgan (@afgane) +- Dannon Baker (@dannon) +- Daniel Blankenberg (@blankenberg) +- Dave Bouvier (@davebx) +- Martin Čech (@martenson) +- John Chilton (@jmchilton) +- Dave Clements (@tnabtaf) +- Nate Coraor (@natefoo) +- Carl Eberhard (@carlfeberhard) +- Jeremy Goecks (@jgoecks) +- Björn Grüning (@bgruening) +- Aysam Guerler (@guerler) +- Jennifer Hillman Jackson (@jennaj) +- Ross Lazarus (@fubar2) +- Anton Nekrutenko (@nekrut) +- Eric Rasche (@erasche) +- Nicola Soranzo (@nsoranzo) +- James Taylor (@jxtx) +- Nitesh Turaga (@nitesh1989) + +Membership +---------- + +The `committers` group was seeded with the group of active developers and +advocates with commit access to the repository as of May 2015. This group +subsequently voted in new members. + +Any member of the `committers` group may nominate an individual for membership +to the `committers` group. Such individuals must have demonstrated: + +- Good grasp of the design of Galaxy core project. +- Solid track record of being constructive and helpful. +- Significant contributions to the project. +- Willingness to dedicate some time to improving Galaxy. + +The above list of `committers` is the canonical source used to determine +membership to the `committers` group - as such new members may be added to +this group by opening a pull request adding a qualified person to this list. +Pull requests modifying the membership of this list are subject to the normal +rules for pull requests that modify governance procedures outlined in Galaxy's +CONTRIBUTING_ document - with one exception - a `commmitter` may not vote +against their own removal from the group (for obvious reasons). + +Given the responsibilities and power invested in this group - it is important +that individuals not actively working on Galaxy in some fashion are removed +the group. If individuals in this group intend to change jobs or reallocate +volunteer activities and will no longer be active in the Galaxy community, +they should withdraw from membership of this group. Periodically, active +members may review this group and request inactive members are removed - this +should not be interpreted as a condemnation of these inactive members but +merely as a reflection of a desire to keep this group focused enough to remain +effective. + +.. _LICENSE: https://github.com/galaxyproject/galaxy/blob/dev/LICENSE.txt +.. _CONTRIBUTING: https://github.com/galaxyproject/galaxy/blob/dev/CONTRIBUTING.md diff --git a/lib/galaxy/datatypes/binary.py b/lib/galaxy/datatypes/binary.py index 84b6a3d8738..4fd0caf968b 100644 --- a/lib/galaxy/datatypes/binary.py +++ b/lib/galaxy/datatypes/binary.py @@ -114,6 +114,27 @@ class Ab1( Binary ): Binary.register_unsniffable_binary_ext("ab1") + +class Idat( Binary ): + """Binary data in idat format""" + file_ext = "idat" + + def __init__( self, **kwd ): + Binary.__init__( self, **kwd ) + + def sniff( self, filename ): + try: + header = open( filename ).read(4) + if binascii.b2a_hex( header ) == binascii.hexlify( 'IDAT' ): + return True + return False + except: + return False + +Binary.register_sniffable_binary_format("idat", "idat", Idat) + + + class CompressedArchive( Binary ): """ Class describing an compressed binary file diff --git a/lib/galaxy/datatypes/registry.py b/lib/galaxy/datatypes/registry.py index 4149b893a7c..51009d9c046 100644 --- a/lib/galaxy/datatypes/registry.py +++ b/lib/galaxy/datatypes/registry.py @@ -208,7 +208,7 @@ class Registry( object ): for mod in fields: module = getattr( module, mod ) datatype_class = getattr( module, datatype_class_name ) - self.log.debug( 'Retrieved datatype module %s from the datatype registry.' % str( datatype_module ) ) + self.log.debug( 'Retrieved datatype module %s:%s from the datatype registry.' % ( str( datatype_module ), datatype_class_name ) ) except Exception, e: self.log.exception( 'Error importing datatype module %s: %s' % ( str( datatype_module ), str( e ) ) ) ok = False diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index df24eeed494..d666942bba4 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -2144,7 +2144,7 @@ class Library( object, Dictifiable, HasName ): """ rval = super( Library, self ).to_dict( view=view, value_mapper=value_mapper ) if 'root_folder_id' in rval: - rval[ 'root_folder_id' ] = 'F' + rval[ 'root_folder_id' ] + rval[ 'root_folder_id' ] = 'F' + str(rval[ 'root_folder_id' ]) return rval def get_active_folders( self, folder, folders=None ): # TODO: should we make sure the library is not deleted? diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 5459f4a7db4..9baa171ffb4 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -13,6 +13,8 @@ import threading import types import urllib import copy +import traceback +from datetime import datetime from galaxy import eggs, util @@ -80,6 +82,24 @@ JOB_RESOURCE_CONDITIONAL_XML = """ HELP_UNINITIALIZED = threading.Lock() +class ToolErrorLog: + def __init__(self): + self.error_stack = [] + self.max_errors = 100 + + def add_error(self, file, phase, exception): + self.error_stack.insert(0, { + "file" : file, + "time" : str(datetime.now()), + "phase" : phase, + "error" : str(exception) + } ) + if len(self.error_stack) > self.max_errors: + self.error_stack.pop() + + +global_tool_errors = ToolErrorLog() + class ToolNotFoundException( Exception ): pass @@ -103,7 +123,12 @@ class ToolBox( AbstractToolBox ): return self._tools_by_id def create_tool( self, config_file, repository_id=None, guid=None, **kwds ): - tool_source = get_tool_source( config_file, getattr( self.app.config, "enable_beta_tool_formats", False ) ) + try: + tool_source = get_tool_source( config_file, getattr( self.app.config, "enable_beta_tool_formats", False ) ) + except Exception, e: + #capture and log parsing errors + global_tool_errors.add_error(config_file, "Tool XML parsing", e) + raise e # Allow specifying a different tool subclass to instantiate tool_module = tool_source.parse_tool_module() if tool_module is not None: @@ -460,7 +485,11 @@ class Tool( object, Dictifiable ): #populate toolshed repository info, if available self.populate_tool_shed_info() # Parse XML element containing configuration - self.parse( tool_source, guid=guid ) + try: + self.parse( tool_source, guid=guid ) + except Exception, e: + global_tool_errors.add_error(config_file, "Tool Loading", e) + raise e self.external_runJob_script = app.config.drmaa_external_runjob_script self.history_manager = histories.HistoryManager( app ) diff --git a/lib/galaxy/tools/evaluation.py b/lib/galaxy/tools/evaluation.py index 4903838097d..8dc7b573788 100644 --- a/lib/galaxy/tools/evaluation.py +++ b/lib/galaxy/tools/evaluation.py @@ -21,6 +21,7 @@ from galaxy.tools.parameters.basic import ( SelectToolParameter, ) from galaxy.tools.parameters.grouping import Conditional, Repeat, Section +from galaxy.tools import global_tool_errors from galaxy.jobs.datasets import dataset_path_rewrites import logging @@ -400,10 +401,25 @@ class ToolEvaluator( object ): self.extra_filenames = [] self.command_line = None - self.__build_config_files( ) - self.__build_param_file( ) - self.__build_command_line( ) - + try: + self.__build_config_files( ) + except Exception, e: + #capture and log parsing errors + global_tool_errors.add_error(self.tool.config_file, "Building Config Files", e) + raise e + try: + self.__build_param_file( ) + except Exception, e: + #capture and log parsing errors + global_tool_errors.add_error(self.tool.config_file, "Building Param File", e) + raise e + try: + self.__build_command_line( ) + except Exception, e: + #capture and log parsing errors + global_tool_errors.add_error(self.tool.config_file, "Building Command Line", e) + raise e + return self.command_line, self.extra_filenames def __build_command_line( self ): diff --git a/lib/galaxy/tools/test.py b/lib/galaxy/tools/test.py index 2b8df13d864..e98527c1d0e 100644 --- a/lib/galaxy/tools/test.py +++ b/lib/galaxy/tools/test.py @@ -48,7 +48,7 @@ class ToolTestBuilder( object ): self.name = name self.maxseconds = maxseconds self.required_files = [] - self.inputs = [] + self.inputs = {} self.outputs = [] # By default do not making assertions on number of outputs - but to # test filtering allow explicitly state number of outputs. @@ -134,6 +134,7 @@ class ToolTestBuilder( object ): self.expect_failure = test_dict.get("expect_failure", False) self.md5 = test_dict.get("md5", None) except Exception, e: + self.inputs = {} self.error = True self.exception = e diff --git a/lib/galaxy/web/framework/webapp.py b/lib/galaxy/web/framework/webapp.py index 49f350590bc..e813c29672a 100644 --- a/lib/galaxy/web/framework/webapp.py +++ b/lib/galaxy/web/framework/webapp.py @@ -818,8 +818,7 @@ class GalaxyWebTransaction( base.DefaultWebTransaction, template = template_lookup.get_template( filename ) template.output_encoding = 'utf-8' - data = dict( caller=self, t=self, trans=self, h=helpers, util=util, - request=self.request, response=self.response, app=self.app ) + data = dict( caller=self, t=self, trans=self, h=helpers, util=util, request=self.request, response=self.response, app=self.app ) data.update( self.template_context ) data.update( kwargs ) return template.render( **data ) diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index 001a2192d76..88ceeaee72a 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -2,11 +2,15 @@ Provides factory methods to assemble the Galaxy web application """ -import atexit +import os import sys +import atexit + +try: + import configparser +except: + import ConfigParser as configparser -from paste import httpexceptions -import pkg_resources import galaxy.app from galaxy.config import process_is_uwsgi @@ -19,6 +23,10 @@ from galaxy import util from galaxy.util import asbool from galaxy.util.properties import load_app_properties +from galaxy import eggs +eggs.require('Paste') +from paste import httpexceptions + import logging log = logging.getLogger( __name__ ) @@ -37,6 +45,10 @@ class GalaxyWebApplication( galaxy.web.framework.webapp.WebApplication ): def app_factory( global_conf, **kwargs ): + return paste_app_factory( global_conf, **kwargs ) + + +def paste_app_factory( global_conf, **kwargs ): """ Return a wsgi application serving the root object """ @@ -119,6 +131,22 @@ def app_factory( global_conf, **kwargs ): return webapp +def uwsgi_app_factory(): + import uwsgi + root = os.path.abspath(uwsgi.opt.get('galaxy_root', os.getcwd())) + config_file = uwsgi.opt.get('galaxy_config_file', os.path.join(root, 'config', 'galaxy.ini')) + global_conf = { + '__file__' : config_file if os.path.exists(__file__) else None, + 'here' : root } + parser = configparser.ConfigParser() + parser.read(config_file) + try: + kwargs = dict(parser.items('app:main')) + except configparser.NoSectionError: + kwargs = {} + return app_factory(global_conf, **kwargs) + + @postfork def postfork_setup(): from galaxy.app import app @@ -635,7 +663,7 @@ def wrap_in_middleware( app, global_conf, **local_conf ): # Interactive exception debugging, scary dangerous if publicly # accessible, if not enabled we'll use the regular error printing # middleware. - pkg_resources.require( "WebError" ) + eggs.require( "WebError" ) from weberror import evalexception app = evalexception.EvalException( app, conf, templating_formatters=build_template_error_formatters() ) diff --git a/lib/galaxy/webapps/galaxy/controllers/admin.py b/lib/galaxy/webapps/galaxy/controllers/admin.py index faa613244b0..2dca3bf130b 100644 --- a/lib/galaxy/webapps/galaxy/controllers/admin.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin.py @@ -18,6 +18,7 @@ from galaxy.web.base.controller import BaseUIController, UsesQuotaMixin from galaxy.web.base.controllers.admin import Admin from galaxy.web.framework.helpers import grids, time_ago from galaxy.web.params import QuotaParamParser +from galaxy.tools import global_tool_errors from tool_shed.util import common_util from tool_shed.util import encoding_util from tool_shed.util.web_util import escape @@ -824,6 +825,11 @@ class AdminGalaxy( BaseUIController, Admin, AdminActions, UsesQuotaMixin, QuotaP message=message, status=status ) + @web.expose + @web.require_admin + def tool_errors( self, trans, **kwd ): + return trans.fill_template('admin/tool_errors.mako', tool_errors=global_tool_errors.error_stack) + @web.expose @web.require_admin def view_datatypes_registry( self, trans, **kwd ): diff --git a/lib/galaxy/webapps/tool_shed/api/groups.py b/lib/galaxy/webapps/tool_shed/api/groups.py new file mode 100644 index 00000000000..0f8815ec4da --- /dev/null +++ b/lib/galaxy/webapps/tool_shed/api/groups.py @@ -0,0 +1,132 @@ +import logging +from galaxy import util +from galaxy import web +from galaxy.util import pretty_print_time_interval +from galaxy.exceptions import RequestParameterMissingException +from galaxy.exceptions import AdminRequiredException +from galaxy.exceptions import ObjectNotFound +from galaxy.web import require_admin as require_admin +from galaxy.web import _future_expose_api as expose_api +from galaxy.web import _future_expose_api_anonymous_and_sessionless as expose_api_anonymous_and_sessionless +from galaxy.web.base.controller import BaseAPIController +from tool_shed.managers import groups + +log = logging.getLogger( __name__ ) + + +class GroupsController( BaseAPIController ): + """RESTful controller for interactions with groups in the Tool Shed.""" + + def __init__( self, app ): + super( GroupsController, self ).__init__( app ) + self.group_manager = groups.GroupManager() + + def __get_value_mapper( self, trans ): + value_mapper = { 'id' : trans.security.encode_id } + return value_mapper + + @expose_api_anonymous_and_sessionless + def index( self, trans, deleted=False, **kwd ): + """ + GET /api/groups + Return a list of dictionaries that contain information about each Group. + + :param deleted: flag used to include deleted groups + + Example: GET localhost:9009/api/groups + """ + group_dicts = [] + deleted = util.asbool( deleted ) + if deleted and not trans.user_is_admin(): + raise AdminRequiredException( 'Only administrators can query deleted groups.' ) + for group in self.group_manager.list( trans, deleted ): + group_dicts.append( self._populate( trans, group ) ) + return group_dicts + + @expose_api + @require_admin + def create( self, trans, payload, **kwd ): + """ + POST /api/groups + Return a dictionary of information about the created group. + The following parameters are included in the payload: + + :param name (required): the name of the group + :param description (optional): the description of the group + + Example: POST /api/groups/?key=XXXYYYXXXYYY + Content-Disposition: form-data; name="name" Group_Name + Content-Disposition: form-data; name="description" Group_Description + """ + group_dict = dict( message='', status='ok' ) + name = payload.get( 'name', '' ) + if name: + description = payload.get( 'description', '' ) + if not description: + description = '' + else: + # TODO add description field to the model + group_dict = self.group_manager.create( trans, name=name ).to_dict( view='element', value_mapper=self.__get_value_mapper( trans ) ) + else: + raise RequestParameterMissingException( 'Missing required parameter "name".' ) + return group_dict + + @expose_api_anonymous_and_sessionless + def show( self, trans, encoded_id, **kwd ): + """ + GET /api/groups/{encoded_group_id} + Return a dictionary of information about a group. + + :param id: the encoded id of the Group object + + Example: GET localhost:9009/api/groups/f9cad7b01a472135 + """ + decoded_id = trans.security.decode_id( encoded_id ) + group = self.group_manager.get( trans, decoded_id ) + if group is None: + raise ObjectNotFound( 'Unable to locate group record for id %s.' % ( str( encoded_id ) ) ) + return self._populate( trans, group ) + + def _populate( self, trans, group ): + """ + Turn the given group information from DB into a dict + and add other characteristics like members and repositories. + """ + model = trans.app.model + group_dict = group.to_dict( view='collection', value_mapper=self.__get_value_mapper( trans ) ) + group_members = [] + group_repos = [] + total_downloads = 0 + for uga in group.users: + user = trans.sa_session.query( model.User ).filter( model.User.table.c.id == uga.user_id ).one() + user_repos_count = 0 + for repo in trans.sa_session.query( model.Repository ) \ + .filter( model.Repository.table.c.user_id == uga.user_id ) \ + .join( model.RepositoryMetadata.table ) \ + .join( model.User.table ) \ + .outerjoin( model.RepositoryCategoryAssociation.table ) \ + .outerjoin( model.Category.table ): + time_repo_created = pretty_print_time_interval( repo.create_time, True ) + approved = '' + ratings = [] + for review in repo.reviews: + if review.rating: + ratings.append( review.rating ) + if review.approved == 'yes': + approved = 'yes' + # TODO add user ratings + ratings_mean = str( float( sum( ratings ) ) / len( ratings ) ) if len( ratings ) > 0 else '' + total_downloads += repo.times_downloaded + group_repos.append( { 'name': repo.name, 'times_downloaded': repo.times_downloaded, 'owner': repo.user.username, 'time_created': time_repo_created, 'description': repo.description, 'approved': approved, 'ratings_mean': ratings_mean, 'tools': 'unknown' } ) + user_repos_count += 1 + encoded_user_id = trans.app.security.encode_id( repo.user.id ) + user_repos_url = web.url_for( controller='repository', action='browse_repositories_by_user', user_id=encoded_user_id ) + time_created = pretty_print_time_interval( user.create_time, True ) + member_dict = { 'id': encoded_user_id, 'username': user.username, 'user_repos_url': user_repos_url, 'user_repos_count': user_repos_count, 'user_tools_count': 'unknown', 'time_created': time_created } + group_members.append( member_dict ) + group_dict[ 'members' ] = group_members + group_dict[ 'total_members' ] = len( group_members ) + group_dict[ 'repositories' ] = group_repos + group_dict[ 'total_repos' ] = len( group_repos ) + group_dict[ 'total_downloads' ] = total_downloads + return group_dict diff --git a/lib/galaxy/webapps/tool_shed/buildapp.py b/lib/galaxy/webapps/tool_shed/buildapp.py index a11ffb7e20c..7fa54a6166c 100644 --- a/lib/galaxy/webapps/tool_shed/buildapp.py +++ b/lib/galaxy/webapps/tool_shed/buildapp.py @@ -86,6 +86,21 @@ def app_factory( global_conf, **kwargs ): controller='authenticate', action='get_tool_shed_api_key', conditions=dict( method=[ "GET" ] ) ) + webapp.mapper.connect( 'group', + '/api/groups/', + controller='groups', + action='index', + conditions=dict( method=[ "GET" ] ) ) + webapp.mapper.connect( 'group', + '/api/groups/', + controller='groups', + action='create', + conditions=dict( method=[ "POST" ] ) ) + webapp.mapper.connect( 'group', + '/api/groups/:encoded_id', + controller='groups', + action='show', + conditions=dict( method=[ "GET" ] ) ) webapp.mapper.connect( 'repo_search', '/api/search/', controller='search', diff --git a/lib/galaxy/webapps/tool_shed/controllers/groups.py b/lib/galaxy/webapps/tool_shed/controllers/groups.py new file mode 100644 index 00000000000..1e1e80f1351 --- /dev/null +++ b/lib/galaxy/webapps/tool_shed/controllers/groups.py @@ -0,0 +1,22 @@ +import logging +from galaxy import model, util +from galaxy import web +from galaxy.model.orm import and_, not_, or_ +from galaxy.web.base.controller import BaseUIController +from galaxy.web.framework.helpers import escape, grids + +log = logging.getLogger( __name__ ) + + +class Group( BaseUIController ): + + @web.expose + def index( self, trans, **kwd ): + # define app configuration for generic mako template + app = { + 'jscript' : "toolshed.groups" + } + return trans.fill_template( '/webapps/tool_shed/group/index.mako', + config = { + 'title': 'Tool Shed Groups', + 'app': app } ) diff --git a/lib/tool_shed/dependencies/tool/tag_attribute_handler.py b/lib/tool_shed/dependencies/tool/tag_attribute_handler.py index dfdfd1e87db..90057a272b2 100644 --- a/lib/tool_shed/dependencies/tool/tag_attribute_handler.py +++ b/lib/tool_shed/dependencies/tool/tag_attribute_handler.py @@ -30,7 +30,7 @@ class TagAttributeHandler( object ): elem_index=sub_index, elem=sub_elem, message=message ) - if error_message: + if error_message and error_message not in message: message += error_message if altered: if not self.altered: @@ -68,7 +68,7 @@ class TagAttributeHandler( object ): altered, new_sub_elem, error_message = self.rdd.handle_sub_elem( parent_elem=elem, elem_index=sub_index, elem=sub_elem ) - if error_message: + if error_message and error_message not in message: message += error_message if altered: if not self.altered: @@ -98,7 +98,7 @@ class TagAttributeHandler( object ): altered, new_sub_elem, error_message = \ self.process_actions_tag_set( elem=sub_elem, message=message ) - if error_message: + if error_message and error_message not in message: message += error_message if altered: if not self.altered: @@ -150,7 +150,7 @@ class TagAttributeHandler( object ): ( str( package_version ), str( package_name ) ) error_message += 'the recipe for installing the package is missing either an ' error_message += '<actions> tag set or an <actions_group> tag set.' - if error_message: + if error_message and error_message not in message: message += error_message if altered: if not self.altered: @@ -176,7 +176,7 @@ class TagAttributeHandler( object ): elem_index=sub_index, elem=sub_elem, message=message ) - if error_message: + if error_message and error_message not in message: message += error_message if altered: if not self.altered: @@ -191,7 +191,7 @@ class TagAttributeHandler( object ): altered, new_elem, error_message = self.rdd.handle_complex_dependency_elem( parent_elem=parent_elem, elem_index=elem_index, elem=elem ) - if error_message: + if error_message and error_message not in message: message += error_message if altered: if not self.altered: diff --git a/lib/tool_shed/managers/__init__.py b/lib/tool_shed/managers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/tool_shed/managers/groups.py b/lib/tool_shed/managers/groups.py new file mode 100644 index 00000000000..e8462a09b4b --- /dev/null +++ b/lib/tool_shed/managers/groups.py @@ -0,0 +1,120 @@ +""" +Manager and Serializer for TS groups. +""" +from galaxy.exceptions import InconsistentDatabase +from galaxy.exceptions import RequestParameterInvalidException +from galaxy.exceptions import ObjectNotFound +from galaxy.exceptions import InternalServerError +from galaxy.exceptions import ItemAccessibilityException +from galaxy.exceptions import Conflict +from sqlalchemy.orm.exc import MultipleResultsFound +from sqlalchemy.orm.exc import NoResultFound + +import logging +log = logging.getLogger( __name__ ) + + +# ============================================================================= +class GroupManager( object ): + """ + Interface/service object for interacting with TS groups. + """ + + def __init__( self, *args, **kwargs ): + super( GroupManager, self ).__init__( *args, **kwargs ) + + def get( self, trans, decoded_group_id=None, name=None ): + """ + Get the group from the DB based on its ID or name. + + :param decoded_group_id: decoded group id + :type decoded_group_id: int + + :returns: the requested group + :rtype: Group + """ + if decoded_group_id is None and name is None: + raise RequestParameterInvalidException( 'You must supply either ID or a name of the group.' ) + + name_query = trans.sa_session.query( trans.app.model.Group ).filter( trans.app.model.Group.table.c.name == name ) + id_query = trans.sa_session.query( trans.app.model.Group ).filter( trans.app.model.Group.table.c.id == decoded_group_id ) + + try: + group = id_query.one() if decoded_group_id else name_query.one() + except MultipleResultsFound: + raise InconsistentDatabase( 'Multiple groups found with the same id.' ) + except NoResultFound: + raise ObjectNotFound( 'No group found with the id provided.' ) + except Exception: + raise InternalServerError( 'Error loading from the database.' ) + return group + + def create( self, trans, name, description=''): + """ + Create a new group. + """ + if not trans.user_is_admin: + raise ItemAccessibilityException( 'Only administrators can create groups.' ) + else: + if self.get( trans, name=name ): + raise Conflict( 'Group with the given name already exists. Name: ' + str( name ) ) + # TODO add description field to the model + group = trans.app.model.Group( name=name ) + trans.sa_session.add( group ) + trans.sa_session.flush() + return group + + def update( self, trans, group, name=None, description=None ): + """ + Update the given group + """ + changed = False + if not trans.user_is_admin(): + raise ItemAccessibilityException( 'Only administrators can update groups.' ) + if group.deleted: + raise RequestParameterInvalidException( 'You cannot modify a deleted group. Undelete it first.' ) + if name is not None: + group.name = name + changed = True + if description is not None: + group.description = description + changed = True + if changed: + trans.sa_session.add( group ) + trans.sa_session.flush() + return group + + def delete( self, trans, group, undelete=False ): + """ + Mark given group deleted/undeleted based on the flag. + """ + if not trans.user_is_admin(): + raise ItemAccessibilityException( 'Only administrators can delete and undelete groups.' ) + if undelete: + group.deleted = False + else: + group.deleted = True + trans.sa_session.add( group ) + trans.sa_session.flush() + return group + + def list( self, trans, deleted=False ): + """ + Return a list of groups from the DB. + + :returns: query that will emit all groups + :rtype: sqlalchemy query + """ + is_admin = trans.user_is_admin() + query = trans.sa_session.query( trans.app.model.Group ) + if is_admin: + if deleted is None: + # Flag is not specified, do not filter on it. + pass + elif deleted: + query = query.filter( trans.app.model.Group.table.c.deleted == True ) + else: + query = query.filter( trans.app.model.Group.table.c.deleted == False ) + else: + query = query.filter( trans.app.model.Group.table.c.deleted == False ) + return query diff --git a/lib/tool_shed/managers/repositories.py b/lib/tool_shed/managers/repositories.py new file mode 100644 index 00000000000..e0b79070ee8 --- /dev/null +++ b/lib/tool_shed/managers/repositories.py @@ -0,0 +1,66 @@ +""" +Manager and Serializer for TS repositories. +""" +from galaxy.exceptions import InconsistentDatabase +from galaxy.exceptions import RequestParameterInvalidException +from galaxy.exceptions import InternalServerError +from sqlalchemy.orm.exc import MultipleResultsFound +from sqlalchemy.orm.exc import NoResultFound + +import logging +log = logging.getLogger( __name__ ) + + +# ============================================================================= +class RepoManager( object ): + """ + Interface/service object for interacting with TS repositories. + """ + + def __init__( self, *args, **kwargs ): + super( RepoManager, self ).__init__( *args, **kwargs ) + + def get( self, trans, decoded_repo_id ): + """ + Get the repo from the DB. + + :param decoded_repo_id: decoded repo id + :type decoded_repo_id: int + + :returns: the requested repo + :rtype: Repository + """ + try: + repo = trans.sa_session.query( trans.app.model.Repository ).filter( trans.app.model.Repository.table.c.id == decoded_repo_id ).one() + except MultipleResultsFound: + raise InconsistentDatabase( 'Multiple repositories found with the same id.' ) + except NoResultFound: + raise RequestParameterInvalidException( 'No repository found with the id provided.' ) + except Exception: + raise InternalServerError( 'Error loading from the database.' ) + return repo + + def list_by_owner( self, trans, user_id ): + """ + Return a list of of repositories owned by a given TS user from the DB. + + :returns: query that will emit repositories owned by given user + :rtype: sqlalchemy query + """ + query = trans.sa_session.query( trans.app.model.Repository ).filter( trans.app.model.Repository.table.c.user_id == user_id ) + return query + + def create( self, trans, name, description=''): + """ + Create a new group. + """ + + def update( self, trans, group, name=None, description=None ): + """ + Update the given group + """ + + def delete( self, trans, group, undelete=False ): + """ + Mark given group deleted/undeleted based on the flag. + """ diff --git a/run_tests.sh b/run_tests.sh index a95e08a6bbe..72f7ba1ec67 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -17,6 +17,7 @@ cat <.toolForm:last>.toolFormTitle>.buttons").click(function(){action_to_rem=$(this).closest(".toolForm",".action_tag").children(".action_tag:first").text(),$(this).closest(".toolForm").remove(),delete workflow.active_node.post_job_actions[action_to_rem],workflow.active_form_has_changes=!0})}function display_pja_list(){return pja_list}function display_file_list(a){addlist="",addlist}function new_pja(a,b,c){if(void 0===c.post_job_actions&&(c.post_job_actions={}),void 0===c.post_job_actions[a+b]){var d={};return d.action_type=a,d.output_name=b,c.post_job_actions[a+b]=null,c.post_job_actions[a+b]=d,display_pja(d,c),workflow.active_form_has_changes=!0,!0}return!1}function show_workflow_parameters(){var a=/\$\{.+?\}/g,b=[],c=$("#workflow-parameters-container"),d=$("#workflow-parameters-box"),e="",f=[];$.each(workflow.nodes,function(c,d){var e=d.form_html.match(a);e&&(f=f.concat(e)),d.post_job_actions&&($.each(d.post_job_actions,function(b,c){c.action_arguments&&$.each(c.action_arguments,function(b,c){var d=c.match(a);d&&(f=f.concat(d))})}),f&&$.each(f,function(a,c){-1===$.inArray(c,b)&&b.push(c)}))}),b&&0!==b.length?($.each(b,function(a,b){e+="
"+b.substring(2,b.length-1)+"
"}),c.html(e),d.show()):(c.html(e),d.hide())}function show_tool_form(a,b,c){require(["utils/utils","mvc/tools/tools-form-workflow"],function(d,e){var f="right-content",g=f+"-"+b.id,h=$("#"+f),i=h.find("#"+g);if(i.length>0&&0==i.find(".section-row").length&&i.remove(),0==h.find("#"+g).length){var j=$('
');if("tool"==b.type){var k=JSON.parse(a);k.datatypes=datatypes,j.append(new e.View(k).$el)}else j.append(generic_form_template(a,b));h.append(j)}$("."+f).hide(),h.find("#"+g).show(),h.show(),h.scrollTop(),c&&c()})}function generic_form_template(a,b){var c=$("
").html(a);return b&&"no-node"!=b.id&&(c.find(".toolForm:first").after("

Edit Step Attributes
Add an annotation or notes to this step; annotations are available when a workflow is viewed.
"),c.find("form").ajaxForm({type:"POST",dataType:"json",success:function(a){workflow.active_form_has_changes=!1,b.update_field_data(a),show_workflow_parameters()},beforeSubmit:function(a){a.push({name:"tool_state",value:b.tool_state}),a.push({name:"_",value:"true"})}}).each(function(){var a=this;$(this).find("select[refresh_on_change='true']").change(function(){$(a).submit()}),$(this).find("input[refresh_on_change='true']").change(function(){$(a).submit()}),$(this).find("input,textarea,select").each(function(){$(this).bind("focus click",function(){workflow.active_form_has_changes=!0})})})),c}function show_form_for_tool(a,b){if($(".right-content").hide(),$("#right-content").show().html(a),b&&$("#right-content").find(".toolForm:first").after("

Edit Step Attributes
Add an annotation or notes to this step; annotations are available when a workflow is viewed.
"),b&&"tool"==b.type){pjastr="

Edit Step Actions
"+display_pja_list()+"
"+display_file_list(b)+"
Create
",pjastr+="
Add actions to this step; actions are applied when this workflow step completes.
",$("#right-content").find(".toolForm").after(pjastr);for(var c in b.post_job_actions)"undefined"!=c&&display_pja(b.post_job_actions[c],b);$("#add_pja").click(function(){new_pja($("#new_pja_list").val(),$("#node_data_list").val(),b)})}$("#right-content").find("form").ajaxForm({type:"POST",dataType:"json",success:function(a){workflow.active_form_has_changes=!1,b.update_field_data(a),show_workflow_parameters()},beforeSubmit:function(a){a.push({name:"tool_state",value:b.tool_state}),a.push({name:"_",value:"true"})}}).each(function(){var a=this;$(this).find("select[refresh_on_change='true']").change(function(){$(a).submit()}),$(this).find("input[refresh_on_change='true']").change(function(){$(a).submit()}),$(this).find(".popupmenu").each(function(){var b=$(this).parents("div.form-row").attr("id"),c=$('▼'),d={};$(this).find("button").each(function(){var b=$(this).attr("name"),c=$(this).attr("value");d[$(this).text()]=function(){$(a).append("").submit()}}),c.insertAfter(this),$(this).remove(),make_popupmenu(c,d)}),$(this).find("input,textarea,select").each(function(){$(this).bind("focus click",function(){workflow.active_form_has_changes=!0})})})}$(function(){function a(){workflow.layout(),workflow.fit_canvas_to_nodes(),scroll_to_nodes(),canvas_manager.draw_overview()}function b(){workflow.clear_active_node(function(){$(".right-content").hide(),$("#edit-attributes").show()})}function c(){$.jStorage.set("overview-off",!1),$("#overview-border").css("right","0px"),$("#close-viewport").css("background-position","0px 0px")}function d(){$.jStorage.set("overview-off",!0),$("#overview-border").css("right","20000px"),$("#close-viewport").css("background-position","12px 0px")}if(window.lt_ie_7)return void show_modal("Browser not supported","Sorry, the workflow editor is not supported for IE6 and below.");$("#tool-search-query").click(function(){$(this).focus(),$(this).select()}).keyup(function(){if($(this).css("font-style","normal"),this.value.length<3)reset_tool_search(!1);else if(this.value!=this.lastValue){$(this).addClass("search_active");var a=this.value;this.timer&&clearTimeout(this.timer),$("#search-spinner").show();var b=galaxy_config.root+"api/tools";this.timer=setTimeout(function(){$.get(b,{q:a},function(a){if($("#search-no-results").hide(),$(".toolSectionWrapper").hide(),$(".toolSectionWrapper").find(".toolTitle").hide(),0!=a.length){var b=$.map(a,function(a){return"link-"+a});$(b).each(function(a,b){$("[id='"+b+"']").parent().addClass("search_match"),$("[id='"+b+"']").parent().show().parent().parent().show().parent().show()}),$(".toolPanelLabel").each(function(){for(var a=$(this),b=a.next(),c=!0;0!==b.length&&b.hasClass("toolTitle");){if(b.is(":visible")){c=!1;break}b=b.next()}c&&a.hide()})}else $("#search-no-results").show();$("#search-spinner").hide()},"json")},400)}this.lastValue=this.value}),canvas_manager=new CanvasManager($("#canvas-viewport"),$("#overview")),reset(),datatypes=JSON.parse($.ajax({type:"GET",url:galaxy_config.root+"api/datatypes",async:!1}).responseText),$.ajax({url:get_datatypes_url,dataType:"json",cache:!1,success:function(a){populate_datatype_info(a),$.ajax({url:load_workflow_url,data:{id:workflow_id,_:"true"},dataType:"json",cache:!1,success:function(a){reset(),workflow.from_simple(a),workflow.has_changes=!1,workflow.fit_canvas_to_nodes(),scroll_to_nodes(),canvas_manager.draw_overview(),upgrade_message="",$.each(a.upgrade_messages,function(a,b){upgrade_message+="
  • Step "+(parseInt(a,10)+1)+": "+workflow.nodes[a].name+"
      ",$.each(b,function(a,b){upgrade_message+="
    • "+b+"
    • "}),upgrade_message+="
  • "}),upgrade_message?show_modal("Workflow loaded with changes","Problems were encountered loading this workflow (possibly a result of tool upgrades). Please review the following parameters and then save.
      "+upgrade_message+"
    ",{Continue:hide_modal}):hide_modal(),show_workflow_parameters()},beforeSubmit:function(){show_message("Loading workflow","progress")}})}}),$(document).ajaxStart(function(){active_ajax_call=!0,$(document).bind("ajaxStop.global",function(){active_ajax_call=!1})}),$(document).ajaxError(function(a,b){var c=b.responseText||b.statusText||"Could not connect to server";return show_modal("Server error",c,{"Ignore error":hide_modal}),!1}),make_popupmenu($("#workflow-options-button"),{Save:save_current_workflow,Run:function(){window.location=run_workflow_url},"Edit Attributes":b,"Auto Re-layout":a,Close:close_editor}),overview_size=$.jStorage.get("overview-size"),void 0!==overview_size&&$("#overview-border").css({width:overview_size,height:overview_size}),$.jStorage.get("overview-off")?d():c(),$("#overview-border").bind("dragend",function(a,b){var c=$(this).offsetParent(),d=c.offset(),e=Math.max(c.width()-(b.offsetX-d.left),c.height()-(b.offsetY-d.top));$.jStorage.set("overview-size",e+"px")}),$("#close-viewport").click(function(){"0px"===$("#overview-border").css("right")?d():c()}),window.onbeforeunload=function(){return workflow&&workflow.has_changes?"There are unsaved changes to your workflow which will be lost.":void 0},$("div.toolSectionBody").hide(),$("div.toolSectionTitle > span").wrap("");var e=null;$("div.toolSectionTitle").each(function(){var a=$(this).next("div.toolSectionBody");$(this).click(function(){a.is(":hidden")?(e&&e.slideUp("fast"),e=a,a.slideDown("fast")):(a.slideUp("fast"),e=null)})}),async_save_text("workflow-name","workflow-name",rename_async_url,"new_name"),$("#workflow-tag").click(function(){return $(".tag-area").click(),!1}),async_save_text("workflow-annotation","workflow-annotation",annotate_async_url,"new_annotation",25,!0,4)});var close_editor=function(){workflow.check_changes_in_active_form(),workflow&&workflow.has_changes?(do_close=function(){window.onbeforeunload=void 0,window.document.location=workflow_index_url},show_modal("Close workflow editor","There are unsaved changes to your workflow which will be lost.",{Cancel:hide_modal,"Save Changes":function(){save_current_workflow(null,do_close)}},{"Don't Save":do_close})):window.document.location=workflow_index_url},save_current_workflow=function(a,b){if(show_message("Saving workflow","progress"),workflow.check_changes_in_active_form(),!workflow.has_changes)return hide_modal(),void(b&&b());workflow.rectify_workflow_outputs();var c=function(a){$.ajax({url:save_workflow_url,type:"POST",data:{id:workflow_id,workflow_data:function(){return JSON.stringify(workflow.to_simple())},_:"true"},dataType:"json",success:function(b){var c=$("
    ").text(b.message);if(b.errors){c.addClass("warningmark");var d=$("
      ");$.each(b.errors,function(a,b){$("
    • ").text(b).appendTo(d)}),c.append(d)}else c.addClass("donemark");workflow.name=b.name,workflow.has_changes=!1,workflow.stored=!0,show_workflow_parameters(),b.errors?show_modal("Saving workflow",c,{Ok:hide_modal}):(a&&a(),hide_modal())}})};active_ajax_call?$(document).bind("ajaxStop.save_workflow",function(){$(document).unbind("ajaxStop.save_workflow"),c(),$(document).unbind("ajaxStop.save_workflow"),active_ajax_call=!1}):c(b)}; +//# sourceMappingURL=../maps/galaxy.workflows.js.map \ No newline at end of file diff --git a/static/scripts/mvc/groups/group-detail-view.js b/static/scripts/mvc/groups/group-detail-view.js new file mode 100644 index 00000000000..fcee488d131 --- /dev/null +++ b/static/scripts/mvc/groups/group-detail-view.js @@ -0,0 +1,2 @@ +define(["libs/toastr","mvc/groups/group-model"],function(a,b){var c=Backbone.View.extend({el:"#groups_element",options:{},app:null,initialize:function(a){this.options=_.extend(this.options,a),this.app=window.globalTS.groups,null!==this.app.collection?(this.model=this.app.collection.get(this.options.group_id),this.render()):this.fetchGroup()},fetchGroup:function(c){var d=this;this.options=_.extend(this.options,c),this.model=new b.Group({id:this.options.group_id}),this.model.fetch({success:function(a){console.log("received data: "),console.log(a),d.render()},error:function(b,c){a.error("undefined"!=typeof c.responseJSON?c.responseJSON.err_msg:"An error ocurred.")}})},render:function(){var a=this.templateRow();this.$el.html(a({group:this.model})),$("#center [data-toggle]").tooltip()},templateRow:function(){return _.template(["
      ",'

      <%= group.get("name") %>

      ','

      ','A group of <%= group.get("members").length %> members with <%= group.get("repositories").length %> repositories and a total of <%= group.get("total_downloads") %> combined tool downloads.

      ',"

      Members

      ",'',"","","","","","",'<% _.each(group.get("members"), function(member) { %>',"","","","","","<% }); %>","","
      NameRepositoriesRegistered
      ","<%= member.username %>","",'<%= member.user_repos_count %>',"","<%= member.time_created %>","
      ","

      Repositories

      ",'',"","","","","","","","","","",'<% _.each(group.get("repositories"), function(repo) { %>',"","","","","","","","","","<% }); %>","","
      NameDescriptionDownloadsOwnerCreatedRatingVerified
      ",'<%= repo.name %>',"","<%= repo.description %>","","<%= repo.times_downloaded %>","","<%= repo.owner %>","","<%= repo.time_created %>","","<%= repo.ratings_mean %>","","<%= repo.approved %>","
      ","
      "].join(""))}});return{GroupDetailView:c}}); +//# sourceMappingURL=../../../maps/mvc/groups/group-detail-view.js.map \ No newline at end of file diff --git a/static/scripts/mvc/groups/group-groupdetail-view.js b/static/scripts/mvc/groups/group-groupdetail-view.js new file mode 100644 index 00000000000..ac9d5f5c776 --- /dev/null +++ b/static/scripts/mvc/groups/group-groupdetail-view.js @@ -0,0 +1,2 @@ +define([],function(){var a=Backbone.View.extend({events:{},initialize:function(a){this.render(a)},render:function(a){var b=this.templateRow();return this.setElement(b({group:a})),this.$el.show(),this},templateRow:function(){return _.template(["
      TEST GROUP PAGE
      "].join(""))}});return{GroupDetailView:a}}); +//# sourceMappingURL=../../../maps/mvc/groups/group-groupdetail-view.js.map \ No newline at end of file diff --git a/static/scripts/mvc/groups/group-grouprow-view.js b/static/scripts/mvc/groups/group-grouprow-view.js new file mode 100644 index 00000000000..49178b680f1 --- /dev/null +++ b/static/scripts/mvc/groups/group-grouprow-view.js @@ -0,0 +1,2 @@ +define([],function(){var a=Backbone.View.extend({events:{},initialize:function(a){this.render(a)},render:function(a){var b=this.templateRow();return this.setElement(b({group:a})),this.$el.show(),this},templateRow:function(){return _.template(['">','"><%= group.get("name") %>',"description",'<%= group.get("total_members") %>','<%= group.get("total_repos") %>',""].join(""))}});return{GroupRowView:a}}); +//# sourceMappingURL=../../../maps/mvc/groups/group-grouprow-view.js.map \ No newline at end of file diff --git a/static/scripts/mvc/groups/group-list-view.js b/static/scripts/mvc/groups/group-list-view.js new file mode 100644 index 00000000000..d14a196bd3f --- /dev/null +++ b/static/scripts/mvc/groups/group-list-view.js @@ -0,0 +1,2 @@ +define(["libs/toastr","mvc/groups/group-model","mvc/groups/group-listrow-view"],function(a,b,c){var d=Backbone.View.extend({el:"#groups_element",defaults:{},initialize:function(c){this.options=_.defaults(this.options||{},this.defaults,c);var d=this;window.globalTS.groups.collection=new b.Groups,window.globalTS.groups.collection.fetch({success:function(a){console.log("received data: "),console.log(a),d.render()},error:function(b,c){a.error("undefined"!=typeof c.responseJSON?c.responseJSON.err_msg:"An error ocurred.")}})},fetch:function(){},render:function(a){this.options=_.extend(this.options,a),$(".tooltip").hide();var b=this.templateGroupsList();this.$el.html(b({length:10,order:"asc"})),this.renderRows(window.globalTS.groups.collection.models),$("#center [data-toggle]").tooltip(),$("#center").css("overflow","auto")},renderRows:function(a){for(var b=0;b'),tmpl_array.push("
    "),tmpl_array.push('
    '),tmpl_array.push("<% if(length === 0) { %>"),tmpl_array.push("
    There are no groups yet.
    "),tmpl_array.push("<% } else{ %>"),tmpl_array.push(''),tmpl_array.push(" "),tmpl_array.push(' '),tmpl_array.push(' '),tmpl_array.push(' '),tmpl_array.push(' '),tmpl_array.push(" "),tmpl_array.push(' '),tmpl_array.push(" "),tmpl_array.push("
    name descriptionmembersrepositories
    "),tmpl_array.push("<% }%>"),tmpl_array.push("
    "),_.template(tmpl_array.join(""))}});return{GroupListView:d}}); +//# sourceMappingURL=../../../maps/mvc/groups/group-list-view.js.map \ No newline at end of file diff --git a/static/scripts/mvc/groups/group-listrow-view.js b/static/scripts/mvc/groups/group-listrow-view.js new file mode 100644 index 00000000000..865816453d9 --- /dev/null +++ b/static/scripts/mvc/groups/group-listrow-view.js @@ -0,0 +1,2 @@ +define([],function(){var a=Backbone.View.extend({events:{},initialize:function(a){this.render(a.group)},render:function(a){var b=this.templateRow();return this.setElement(b({group:a})),this.$el.show(),this},templateRow:function(){return _.template(['">','"><%= group.get("name") %>',"description",'<%= group.get("total_members") %>','<%= group.get("total_repos") %>',""].join(""))}});return{GroupListRowView:a}}); +//# sourceMappingURL=../../../maps/mvc/groups/group-listrow-view.js.map \ No newline at end of file diff --git a/static/scripts/mvc/groups/group-model.js b/static/scripts/mvc/groups/group-model.js new file mode 100644 index 00000000000..ab732aa056b --- /dev/null +++ b/static/scripts/mvc/groups/group-model.js @@ -0,0 +1,2 @@ +define([],function(){var a=Backbone.Model.extend({urlRoot:"/api/groups"}),b=Backbone.Collection.extend({url:"/api/groups",model:a});return{Group:a,Groups:b}}); +//# sourceMappingURL=../../../maps/mvc/groups/group-model.js.map \ No newline at end of file diff --git a/static/scripts/mvc/groups/group.model.js b/static/scripts/mvc/groups/group.model.js new file mode 100644 index 00000000000..b050bdad096 --- /dev/null +++ b/static/scripts/mvc/groups/group.model.js @@ -0,0 +1,2 @@ +define([],function(){var a=Backbone.Model.extend({urlRoot:"/api/groups/"}),b=Backbone.Collection.extend({url:"/api/groups",model:a});return{Group:a,Groups:b}}); +//# sourceMappingURL=../../../maps/mvc/groups/group.model.js.map \ No newline at end of file diff --git a/static/scripts/toolshed.groups.js b/static/scripts/toolshed.groups.js new file mode 100644 index 00000000000..4c0663b9afb --- /dev/null +++ b/static/scripts/toolshed.groups.js @@ -0,0 +1,2 @@ +define(["mvc/groups/group-list-view","mvc/groups/group-detail-view","mvc/groups/group-model"],function(a,b){var c=Backbone.Router.extend({routes:{"":"groups",":group_id":"group_page"}}),d=Backbone.View.extend({groupListView:null,groupDetailView:null,collection:null,initialize:function(){window.globalTS.groups=this,this.ts_router=new c,this.ts_router.on("route:groups",function(){window.globalTS.groups.groupListView=new a.GroupListView}),this.ts_router.on("route:group_page",function(a){window.globalTS.groups.groupDetailView=new b.GroupDetailView({group_id:a})}),Backbone.history.start({pushState:!1})}});return{ToolshedGroups:d}}); +//# sourceMappingURL=../maps/toolshed.groups.js.map \ No newline at end of file diff --git a/static/style/blue/base.css b/static/style/blue/base.css index 215bd179b41..f1d6e219277 100644 --- a/static/style/blue/base.css +++ b/static/style/blue/base.css @@ -1761,6 +1761,7 @@ body{background:#fff;color:#000;margin:10px}body.full-content{overflow:hidden;ma #left,#left-border,#center,#right-border,#right{position:absolute;top:34px;bottom:0px;overflow:hidden;background:#fff} #left{left:0px;width:250px;z-index:200;border-right:solid #999 1px} #left-border{left:250px} +.reports-panel-container{position:absolute;top:30px;bottom:10px;overflow:scroll;width:100%;padding:10px} #center{left:250px;right:250px;overflow:hidden;z-index:1} #right-border{right:250px} #right{width:250px;right:0px;z-index:200;border-left:solid #999 1px} diff --git a/static/toolshed/src b/static/toolshed/src new file mode 120000 index 00000000000..c92cf98f11b --- /dev/null +++ b/static/toolshed/src @@ -0,0 +1 @@ +../../client/toolshed/scripts/ \ No newline at end of file diff --git a/templates/admin/tool_errors.mako b/templates/admin/tool_errors.mako new file mode 100644 index 00000000000..7843f0034ff --- /dev/null +++ b/templates/admin/tool_errors.mako @@ -0,0 +1,27 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Tool Errors + +

    Tool Errors

    + +

    +Internal Tool Error log +

    + + + + + + + + +%for error in tool_errors: + + + + + + +%endfor +
    TimePhaseFileError
    ${error['time']}${error['phase']}${error['file']}${error['error']}
    + diff --git a/templates/grid_base.mako b/templates/grid_base.mako index 02cac200788..80a92c91a12 100644 --- a/templates/grid_base.mako +++ b/templates/grid_base.mako @@ -6,7 +6,6 @@ if kwargs.get( 'embedded', False ): # No inheritance - using only embeddable content (self.body) return None - if context.get('use_panels'): if context.get('webapp'): app_name = context.get('webapp') diff --git a/templates/webapps/galaxy/admin/index.mako b/templates/webapps/galaxy/admin/index.mako index 1d3fb604c5b..9737953277d 100644 --- a/templates/webapps/galaxy/admin/index.mako +++ b/templates/webapps/galaxy/admin/index.mako @@ -73,6 +73,7 @@ +
    diff --git a/templates/webapps/reports/index.mako b/templates/webapps/reports/index.mako index 04d5e96a91b..877ff819ad5 100644 --- a/templates/webapps/reports/index.mako +++ b/templates/webapps/reports/index.mako @@ -36,7 +36,7 @@
    Reports
    -
    +
    diff --git a/templates/webapps/tool_shed/base_panels.mako b/templates/webapps/tool_shed/base_panels.mako index 97b980d7090..48149204112 100644 --- a/templates/webapps/tool_shed/base_panels.mako +++ b/templates/webapps/tool_shed/base_panels.mako @@ -80,6 +80,9 @@ ## Repositories tab. ${tab( "repositories", "Repositories", h.url_for( controller='/repository', action='index' ) )} + ## Groups tab. + ${tab( "groups", "Groups", h.url_for( controller='/groups', action='index' ) )} + ## Admin tab. ${tab( "admin", "Admin", h.url_for( controller='/admin', action='index' ), extra_class="admin-only", visible=( trans.user and app.config.is_admin_user( trans.user ) ) )} diff --git a/templates/webapps/tool_shed/group/index.mako b/templates/webapps/tool_shed/group/index.mako new file mode 100644 index 00000000000..e070faec5ee --- /dev/null +++ b/templates/webapps/tool_shed/group/index.mako @@ -0,0 +1,50 @@ +<%inherit file="/webapps/tool_shed/base_panels.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +<%def name="stylesheets()"> + ## Include "base.css" for styling tool menu and forms (details) + ${h.css( "base", "autocomplete_tagging", "tool_menu" )} + + ## But make sure styles for the layout take precedence + ${parent.stylesheets()} + + + + + +<%def name="javascripts()"> + ${parent.javascripts()} + + +<%def name="init()"> + %if trans.app.config.require_login and not trans.user: + + %endif + + +<%def name="center_panel()"> + +
    +