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 ',
+ '
',
+ '',
+ 'Name ',
+ 'Repositories ',
+ 'Registered ',
+ ' ',
+ '',
+ '<% _.each(group.get("members"), function(member) { %>',
+ '',
+ '',
+ '<%= member.username %>',
+ ' ',
+ '',
+ '<%= member.user_repos_count %> ',
+ ' ',
+ '',
+ '<%= member.time_created %>',
+ ' ',
+ ' ',
+ '<% }); %>',
+ ' ',
+ '
',
+
+ '
Repositories ',
+ '
',
+ '',
+ 'Name ',
+ 'Description ',
+ 'Downloads ',
+ 'Owner ',
+ 'Created ',
+ 'Rating ',
+ 'Verified ',
+ ' ',
+ '',
+ '<% _.each(group.get("repositories"), function(repo) { %>',
+ '',
+ '',
+ '<%= 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(' name ');
+ tmpl_array.push(' description ');
+ tmpl_array.push(' members ');
+ tmpl_array.push(' repositories ');
+ tmpl_array.push(' ');
+ tmpl_array.push(' ');
+ // group item views will attach here
+ tmpl_array.push(' ');
+ tmpl_array.push('
');
+ 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="";for(var b in a.output_terminals)addlist+=""+b+" ";return 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("
"),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("
"),b&&"tool"==b.type){pjastr="
",$("#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.",{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 ",'
',"","Name ","Repositories ","Registered "," ","",'<% _.each(group.get("members"), function(member) { %>',"","","<%= member.username %>"," ","",'<%= member.user_repos_count %> '," ","","<%= member.time_created %>"," "," ","<% }); %>"," ","
","
Repositories ",'
',"","Name ","Description ","Downloads ","Owner ","Created ","Rating ","Verified "," ","",'<% _.each(group.get("repositories"), function(repo) { %>',"","",'<%= 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(' name '),tmpl_array.push(' description '),tmpl_array.push(' members '),tmpl_array.push(' repositories '),tmpl_array.push(" "),tmpl_array.push(' '),tmpl_array.push(" "),tmpl_array.push("
"),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%def>
+
+Tool Errors
+
+
+Internal Tool Error log
+
+
+
+
+%for error in tool_errors:
+
+ ${error['time']}
+ ${error['phase']}
+ ${error['file']}
+ ${error['error']}
+
+%endfor
+
+
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 @@
-