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/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/tool_shed/api/groups.py b/lib/galaxy/webapps/tool_shed/api/groups.py
index 8ec4bc1f316..16ea547fb7e 100644
--- a/lib/galaxy/webapps/tool_shed/api/groups.py
+++ b/lib/galaxy/webapps/tool_shed/api/groups.py
@@ -3,8 +3,10 @@ import os
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 as expose_api_anonymous
@@ -37,29 +39,12 @@ class GroupsController( BaseAPIController ):
Example: GET localhost:9009/api/groups
"""
- model = trans.app.model
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 ):
- group_dict = group.to_dict( view='collection', value_mapper=self.__get_value_mapper( trans ) )
- group_members = []
- for uga in group.users:
- member = ( trans.sa_session.query( model.User ).filter( model.User.table.c.id == uga.user_id ).one() )
- member_repositories = []
- 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 ):
- member_repositories.append( { 'name': repo.name, 'times_downloaded': repo.times_downloaded } )
-
- member_dict = { 'username' : member.username, 'repositories': member_repositories }
- group_members.append( member_dict )
- group_dict[ 'members' ] = group_members
- group_dicts.append( group_dict )
+ for group in self.group_manager.list( trans, deleted ):
+ group_dicts.append( self._populate( trans, group ) )
return group_dicts
@expose_api
@@ -83,8 +68,7 @@ class GroupsController( BaseAPIController ):
description = payload.get( 'description', '' )
if not description:
description = ''
- # TODO check existence
- # if suc.get_category_by_name( trans.app, name ):
+ # TODO check group existence
# raise exceptions.Conflict( 'A category with that name already exists.' )
else:
# Create the group
@@ -98,7 +82,7 @@ class GroupsController( BaseAPIController ):
return group_dict
@expose_api_anonymous_and_sessionless
- def show( self, trans, id, **kwd ):
+ def show( self, trans, encoded_id, **kwd ):
"""
GET /api/groups/{encoded_group_id}
Return a dictionary of information about a group.
@@ -107,12 +91,52 @@ class GroupsController( BaseAPIController ):
Example: GET localhost:9009/api/groups/f9cad7b01a472135
"""
- # TODO write get group utility function
- group = suc.get_group( trans.app, id )
+ decoded_id = trans.security.decode_id( encoded_id )
+ group = self.group_manager.get( trans, decoded_id )
if group is None:
- group_dict = dict( message = 'Unable to locate group record for id %s.' % ( str( id ) ), status = 'error' )
- return group_dict
- group_dict = group.to_dict( view='element', value_mapper=self.__get_value_mapper( trans ) )
+ 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 df92168e881..2ffc2f153e6 100644
--- a/lib/galaxy/webapps/tool_shed/buildapp.py
+++ b/lib/galaxy/webapps/tool_shed/buildapp.py
@@ -91,6 +91,11 @@ def app_factory( global_conf, **kwargs ):
controller='groups',
action='index',
conditions=dict( method=[ "GET" ] ) )
+ 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/static/maps/mvc/groups/group-detail-view.js.map b/static/maps/mvc/groups/group-detail-view.js.map
new file mode 100644
index 00000000000..ef653f0f771
--- /dev/null
+++ b/static/maps/mvc/groups/group-detail-view.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"group-detail-view.js","sources":["../../../src/mvc/groups/group-detail-view.js"],"names":["define","mod_toastr","mod_group_model","GroupDetailView","Backbone","View","extend","el","options","app","initialize","this","_","window","globalTS","groups","collection","model","get","group_id","render","fetchGroup","that","Group","id","fetch","success","console","log","error","response","responseJSON","err_msg","template","templateRow","$el","html","group","$","tooltip","join"],"mappings":"AAAAA,QACI,cACA,0BAEJ,SACIC,EACAC,GAIJ,GAAIC,GAAkBC,SAASC,KAAKC,QAChCC,GAAI,kBACJC,WACAC,IAAK,KAELC,WAAa,SAAUF,GACnBG,KAAKH,QAAUI,EAAEN,OAAQK,KAAKH,QAASA,GACvCG,KAAKF,IAAMI,OAAOC,SAASC,OAEE,OAAxBJ,KAAKF,IAAIO,YACVL,KAAKM,MAAQN,KAAKF,IAAIO,WAAWE,IAAKP,KAAKH,QAAQW,UACnDR,KAAKS,UAELT,KAAKU,cAKbA,WAAY,SAAUb,GAClB,GAAIc,GAAOX,IACXA,MAAKH,QAAUI,EAAEN,OAAQK,KAAKH,QAASA,GACvCG,KAAKM,MAAQ,GAAIf,GAAgBqB,OAASC,GAAIb,KAAKH,QAAQW,WAC3DR,KAAKM,MAAMQ,OACTC,QAAS,SAAUT,GACjBU,QAAQC,IAAK,mBACbD,QAAQC,IAAKX,GACbK,EAAKF,UAEPS,MAAO,SAAUZ,EAAOa,GAElB7B,EAAW4B,MADyB,mBAA1BC,GAASC,aACDD,EAASC,aAAaC,QAEtB,yBAM9BZ,OAAQ,WACJ,GAAIa,GAAWtB,KAAKuB,aACpBvB,MAAKwB,IAAIC,KAAMH,GAAYI,MAAO1B,KAAKM,SACvCqB,EAAE,yBAAyBC,WAG/BL,YAAa,WACT,MAAOtB,GAAEqB,UACD,QACI,oCACA,mCACA,kMAEA,mBACA,6CACI,UACI,gBACA,wBACA,sBACJ,WACA,UACI,wDACA,OACI,OACI,yBACJ,QACA,OACI,gPACJ,QACA,OACI,6BACJ,QACJ,QACA,YACJ,WACJ,WAEA,wBACA,6CACI,UACI,gBACA,uBACA,qBACA,iBACA,mBACA,kBACA,oBACJ,WACA,UACI,2DACA,OACI,OACI,6KACJ,QACA,OACI,0BACJ,QACA,OACI,+BACJ,QACA,OACI,oBACJ,QACA,OACI,2BACJ,QACA,OACI,2BACJ,QACA,OACI,uBACJ,QACJ,QACA,YACJ,WACJ,WACJ,UACNO,KAAK,OAGf,QACIrC,gBAAiBA"}
\ No newline at end of file
diff --git a/static/maps/mvc/groups/group-groupdetail-view.js.map b/static/maps/mvc/groups/group-groupdetail-view.js.map
new file mode 100644
index 00000000000..4977e80ac92
--- /dev/null
+++ b/static/maps/mvc/groups/group-groupdetail-view.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"group-groupdetail-view.js","sources":["../../../src/mvc/groups/group-groupdetail-view.js"],"names":["define","GroupDetailView","Backbone","View","extend","events","initialize","group","this","render","tmpl","templateRow","setElement","$el","show","_","template","join"],"mappings":"AAAAA,UACA,WAGA,GAAIC,GAAkBC,SAASC,KAAKC,QAChCC,UAGAC,WAAa,SAAUC,GACnBC,KAAKC,OAAQF,IAGjBE,OAAQ,SAAUF,GACd,GAAIG,GAAOF,KAAKG,aAGhB,OAFAH,MAAKI,WAAWF,GAAQH,MAAMA,KAC9BC,KAAKK,IAAIC,OACFN,MAGXG,YAAa,WACT,MAAOI,GAAEC,UACD,8BACNC,KAAK,OAKf,QACIhB,gBAAiBA"}
\ No newline at end of file
diff --git a/static/maps/mvc/groups/group-grouprow-view.js.map b/static/maps/mvc/groups/group-grouprow-view.js.map
new file mode 100644
index 00000000000..29804f64894
--- /dev/null
+++ b/static/maps/mvc/groups/group-grouprow-view.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"group-grouprow-view.js","sources":["../../../src/mvc/groups/group-grouprow-view.js"],"names":["define","GroupRowView","Backbone","View","extend","events","initialize","group","this","render","tmpl","templateRow","setElement","$el","show","_","template","join"],"mappings":"AAAAA,UACA,WAGA,GAAIC,GAAeC,SAASC,KAAKC,QAC7BC,UAGAC,WAAa,SAAUC,GACnBC,KAAKC,OAAOF,IAGhBE,OAAQ,SAAUF,GACd,GAAIG,GAAOF,KAAKG,aAIhB,OAFAH,MAAKI,WAAWF,GAAQH,MAAMA,KAC9BC,KAAKK,IAAIC,OACFN,MAaXG,YAAa,WACT,MAAOI,GAAEC,UACD,iDACA,iFACA,uBACA,6CACA,2CACA,SACNC,KAAK,OAKf,QACIhB,aAAcA"}
\ No newline at end of file
diff --git a/static/maps/mvc/groups/group-list-view.js.map b/static/maps/mvc/groups/group-list-view.js.map
new file mode 100644
index 00000000000..3fce756f66a
--- /dev/null
+++ b/static/maps/mvc/groups/group-list-view.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"group-list-view.js","sources":["../../../src/mvc/groups/group-list-view.js"],"names":["define","mod_toastr","mod_group_model","mod_group_listrow_view","GroupListView","Backbone","View","extend","el","defaults","initialize","options","this","_","that","window","globalTS","groups","collection","Groups","fetch","success","model","console","log","render","error","response","responseJSON","err_msg","$","hide","template","templateGroupsList","$el","html","length","order","renderRows","models","tooltip","css","groups_to_render","i","group","renderOne","rowView","GroupListRowView","find","append","tmpl_array","push","join"],"mappings":"AAAAA,QACI,cACA,yBACA,iCAED,SACCC,EACAC,EACAC,GAGJ,GAAIC,GAAgBC,SAASC,KAAKC,QAC9BC,GAAI,kBACJC,YAOAC,WAAa,SAAUC,GACnBC,KAAKD,QAAUE,EAAEJ,SAAUG,KAAKD,YAAeC,KAAKH,SAAUE,EAC9D,IAAIG,GAAOF,IACXG,QAAOC,SAASC,OAAOC,WAAa,GAAIhB,GAAgBiB,OACxDJ,OAAOC,SAASC,OAAOC,WAAWE,OAChCC,QAAS,SAAUC,GACjBC,QAAQC,IAAK,mBACbD,QAAQC,IAAKF,GACbR,EAAKW,UAEPC,MAAO,SAAUJ,EAAOK,GAElB1B,EAAWyB,MADyB,mBAA1BC,GAASC,aACDD,EAASC,aAAaC,QAEtB,yBAO9BT,MAAO,aAMPK,OAAQ,SAAWd,GACfC,KAAKD,QAAUE,EAAEN,OAAQK,KAAKD,QAASA,GACvCmB,EAAG,YAAaC,MAChB,IAAIC,GAAWpB,KAAKqB,oBACpBrB,MAAKsB,IAAIC,KAAMH,GAAYI,OAAQ,GAAIC,MAAO,SAC9CzB,KAAK0B,WAAYvB,OAAOC,SAASC,OAAOC,WAAWqB,QACnDT,EAAG,yBAA0BU,UAC7BV,EAAG,WAAYW,IAAK,WAAW,SAOnCH,WAAY,SAAUI,GAClB,IAAM,GAAIC,GAAI,EAAGA,EAAID,EAAiBN,OAAQO,IAAM,CAChD,GAAIC,GAAQF,EAAiBC,EAC7B/B,MAAKiC,WAAaD,MAAOA,MAQjCC,UAAW,SAAUlC,GACjB,GAAImC,GAAU,GAAI3C,GAAuB4C,iBAAkBpC,EAC3DC,MAAKsB,IAAIc,KAAM,oBAAqBC,OAAQH,EAAQtC,KAyCxDyB,mBAAoB,WAuBhB,MAtBAiB,eAEAA,WAAWC,KAAK,qBAChBD,WAAWC,KAAK,UAChBD,WAAWC,KAAK,mDAChBD,WAAWC,KAAK,4BAChBD,WAAWC,KAAK,uCAChBD,WAAWC,KAAK,iBAChBD,WAAWC,KAAK,8CAChBD,WAAWC,KAAK,cAChBD,WAAWC,KAAK,mMAChBD,WAAWC,KAAK,gDAChBD,WAAWC,KAAK,6CAChBD,WAAWC,KAAK,iDAChBD,WAAWC,KAAK,eAChBD,WAAWC,KAAK,mCAEhBD,WAAWC,KAAK,eAChBD,WAAWC,KAAK,YAChBD,WAAWC,KAAK,UAChBD,WAAWC,KAAK,UAETtC,EAAEmB,SAASkB,WAAWE,KAAK,OAK1C,QACIhD,cAAeA"}
\ No newline at end of file
diff --git a/static/maps/mvc/groups/group-listrow-view.js.map b/static/maps/mvc/groups/group-listrow-view.js.map
new file mode 100644
index 00000000000..a3a50073eab
--- /dev/null
+++ b/static/maps/mvc/groups/group-listrow-view.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"group-listrow-view.js","sources":["../../../src/mvc/groups/group-listrow-view.js"],"names":["define","GroupListRowView","Backbone","View","extend","events","initialize","options","this","render","group","tmpl","templateRow","setElement","$el","show","_","template","join"],"mappings":"AAAAA,UACA,WAGA,GAAIC,GAAmBC,SAASC,KAAKC,QACjCC,UAEAC,WAAa,SAAUC,GACnBC,KAAKC,OAAQF,EAAQG,QAGzBD,OAAQ,SAAUC,GACd,GAAIC,GAAOH,KAAKI,aAGhB,OAFAJ,MAAKK,WAAWF,GAAQD,MAAMA,KAC9BF,KAAKM,IAAIC,OACFP,MAGXI,YAAa,WACT,MAAOI,GAAEC,UACD,iDACA,iFACA,uBACA,6CACA,2CACA,SACNC,KAAK,OAKf,QACIjB,iBAAkBA"}
\ No newline at end of file
diff --git a/static/maps/mvc/groups/group-model.js.map b/static/maps/mvc/groups/group-model.js.map
new file mode 100644
index 00000000000..7cf16222049
--- /dev/null
+++ b/static/maps/mvc/groups/group-model.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"group-model.js","sources":["../../../src/mvc/groups/group-model.js"],"names":["define","Group","Backbone","Model","extend","urlRoot","Groups","Collection","url","model"],"mappings":"AAAAA,UAAW,WAKP,GAAIC,GAAQC,SAASC,MAAMC,QACzBC,QAAS,gBAGPC,EAASJ,SAASK,WAAWH,QAC/BI,IAAK,cAELC,MAAOR,GAIb,QACIA,MAAOA,EACPK,OAAQA"}
\ No newline at end of file
diff --git a/static/maps/mvc/groups/group.model.js.map b/static/maps/mvc/groups/group.model.js.map
new file mode 100644
index 00000000000..8b65022f69d
--- /dev/null
+++ b/static/maps/mvc/groups/group.model.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"group.model.js","sources":["../../../src/mvc/groups/group.model.js"],"names":["define","Group","Backbone","Model","extend","urlRoot","Groups","Collection","url","model"],"mappings":"AAAAA,UAAW,WAKP,GAAIC,GAAQC,SAASC,MAAMC,QACzBC,QAAS,iBAGPC,EAASJ,SAASK,WAAWH,QAC/BI,IAAK,cAELC,MAAOR,GAIb,QACIA,MAAOA,EACPK,OAAQA"}
\ No newline at end of file
diff --git a/static/maps/toolshed.groups.js.map b/static/maps/toolshed.groups.js.map
new file mode 100644
index 00000000000..2ef66e19331
--- /dev/null
+++ b/static/maps/toolshed.groups.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"toolshed.groups.js","sources":["../src/toolshed.groups.js"],"names":["define","mod_group_list","mod_group_detail","ToolshedRouter","Backbone","Router","extend","routes",":group_id","ToolshedGroups","View","groupListView","groupDetailView","collection","initialize","window","globalTS","groups","this","ts_router","on","GroupListView","group_id","GroupDetailView","history","start","pushState"],"mappings":"AAIAA,QACQ,6BACA,+BACA,0BAEJ,SACIC,EACAC,GAMR,GAAIC,GAAiBC,SAASC,OAAOC,QACjCC,QACI,GAA4B,SAC5BC,YAA4B,gBAKhCC,EAAiBL,SAASM,KAAKJ,QAC/BK,cAAe,KACfC,gBAAiB,KACjBC,WAAY,KAEZC,WAAa,WACTC,OAAOC,SAASC,OAASC,KAEzBA,KAAKC,UAAY,GAAIhB,GACrBe,KAAKC,UAAUC,GAAI,eAAgB,WAC/BL,OAAOC,SAASC,OAAON,cAAgB,GAAIV,GAAeoB,gBAG9DH,KAAKC,UAAUC,GAAI,mBAAoB,SAAUE,GAC7CP,OAAOC,SAASC,OAAOL,gBAAkB,GAAIV,GAAiBqB,iBAAmBD,SAAUA,MAG/FlB,SAASoB,QAAQC,OAAOC,WAAW,MAI3C,QACIjB,eAAgBA"}
\ 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/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/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>
+
+
+<%def name="javascripts()">
+ ${parent.javascripts()}
+%def>
+
+<%def name="init()">
+ %if trans.app.config.require_login and not trans.user:
+
+ %endif
+%def>
+
+<%def name="center_panel()">
+
+
+%def>