diff --git a/groups/static/groups/bower_components/mocha/media/logo.svg b/groups/static/groups/bower_components/mocha/media/logo.svg
new file mode 100644
index 00000000..8770b08d
--- /dev/null
+++ b/groups/static/groups/bower_components/mocha/media/logo.svg
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/safesite/static/safesite/easyui/datagrid-scrollview.js b/safesite/static/safesite/easyui/datagrid-scrollview.js
new file mode 100644
index 00000000..c10f84b2
--- /dev/null
+++ b/safesite/static/safesite/easyui/datagrid-scrollview.js
@@ -0,0 +1,460 @@
+$.extend($.fn.datagrid.defaults, {
+ onBeforeFetch: function(page){},
+ onFetch: function(page, rows){}
+});
+
+var scrollview = $.extend({}, $.fn.datagrid.defaults.view, {
+ render: function(target, container, frozen){
+ var state = $.data(target, 'datagrid');
+ var opts = state.options;
+ var rows = this.rows || [];
+ if (!rows.length) {
+ return;
+ }
+ var fields = $(target).datagrid('getColumnFields', frozen);
+
+ if (frozen){
+ if (!(opts.rownumbers || (opts.frozenColumns && opts.frozenColumns.length))){
+ return;
+ }
+ }
+
+ var index = this.index;
+ var table = ['
'];
+ for(var i=0; i');
+ table.push(this.renderRow.call(this, target, fields, frozen, index, rows[i]));
+ table.push('');
+
+ // render the detail row
+ if (opts.detailFormatter){
+ table.push('');
+ if (frozen){
+ table.push('');
+ } else {
+ table.push(' | ');
+ }
+ table.push(' ');
+ if (frozen){
+ table.push(' ');
+ } else {
+ table.push(opts.detailFormatter.call(target, i, rows[i]));
+ }
+ table.push(' ');
+ table.push(' | ');
+ table.push('
');
+ }
+
+ index++;
+ }
+ table.push('
');
+
+ $(container).html(table.join(''));
+ },
+
+ renderRow: function(target, fields, frozen, rowIndex, rowData){
+ var opts = $.data(target, 'datagrid').options;
+
+ var cc = [];
+ if (frozen && opts.rownumbers){
+ var rownumber = rowIndex + 1;
+ if (opts.pagination){
+ rownumber += (opts.pageNumber-1)*opts.pageSize;
+ }
+ cc.push(''+rownumber+' | ');
+ }
+ for(var i=0; i');
+
+ if (col.checkbox){
+ style = '';
+ } else if (col.expander){
+ style = "text-align:center;height:16px;";
+ } else {
+ style = styleValue;
+ if (col.align){style += ';text-align:' + col.align + ';'}
+ if (!opts.nowrap){
+ style += ';white-space:normal;height:auto;';
+ } else if (opts.autoRowHeight){
+ style += ';height:auto;';
+ }
+ }
+
+ cc.push('');
+
+ if (col.checkbox){
+ cc.push('
');
+ } else if (col.expander) {
+ //cc.push('
');
+ cc.push('');
+ //cc.push('
');
+ } else if (col.formatter){
+ cc.push(col.formatter(value, rowData, rowIndex));
+ } else {
+ cc.push(value);
+ }
+
+ cc.push('
');
+ cc.push('');
+ }
+ }
+ return cc.join('');
+ },
+
+ bindEvents: function(target){
+ var state = $.data(target, 'datagrid');
+ var dc = state.dc;
+ var opts = state.options;
+ var body = dc.body1.add(dc.body2);
+ var clickHandler = ($.data(body[0],'events')||$._data(body[0],'events')).click[0].handler;
+ body.unbind('click').bind('click', function(e){
+ var tt = $(e.target);
+ var tr = tt.closest('tr.datagrid-row');
+ if (!tr.length){return}
+ if (tt.hasClass('datagrid-row-expander')){
+ var rowIndex = parseInt(tr.attr('datagrid-row-index'));
+ if (tt.hasClass('datagrid-row-expand')){
+ $(target).datagrid('expandRow', rowIndex);
+ } else {
+ $(target).datagrid('collapseRow', rowIndex);
+ }
+ $(target).datagrid('fixRowHeight');
+
+ } else {
+ clickHandler(e);
+ }
+ e.stopPropagation();
+ });
+ },
+
+ onBeforeRender: function(target){
+ var state = $.data(target, 'datagrid');
+ var opts = state.options;
+ var dc = state.dc;
+ var view = this;
+
+ state.data.firstRows = state.data.rows;
+
+ opts.finder = $.extend({}, $.fn.datagrid.defaults.finder, {
+ getRow: function(t, p){
+ var index = (typeof p == 'object') ? p.attr('datagrid-row-index') : p;
+ var row = $.data(t, 'datagrid').data.rows[index];
+ if (!row){
+ var v = $(t).datagrid('options').view;
+ row = v.rows[index - v.index];
+ }
+ return row;
+ }
+ });
+
+ dc.body1.add(dc.body2).empty();
+ this.rows = undefined; // the rows to be rendered
+ this.r1 = this.r2 = []; // the first part and last part of rows
+
+ init();
+ createHeaderExpander();
+
+ function init(){
+ // erase the onLoadSuccess event, make sure it can't be triggered
+ state.onLoadSuccess = opts.onLoadSuccess;
+ opts.onLoadSuccess = function(){};
+ setTimeout(function(){
+ dc.body2.unbind('.datagrid').bind('scroll.datagrid', function(e){
+ if (state.onLoadSuccess){
+ opts.onLoadSuccess = state.onLoadSuccess; // restore the onLoadSuccess event
+ state.onLoadSuccess = undefined;
+ }
+ if (view.scrollTimer){
+ clearTimeout(view.scrollTimer);
+ }
+ view.scrollTimer = setTimeout(function(){
+ scrolling.call(view);
+ }, 50);
+ });
+ dc.body2.triggerHandler('scroll.datagrid');
+ }, 0);
+ }
+ function scrolling(){
+ if (dc.body2.is(':empty')){
+ reload.call(this);
+ } else {
+ var firstTr = opts.finder.getTr(target, this.index, 'body', 2);
+ var lastTr = opts.finder.getTr(target, 0, 'last', 2);
+ var headerHeight = dc.view2.children('div.datagrid-header').outerHeight();
+ var top = firstTr.position().top - headerHeight;
+ var bottom = lastTr.position().top + lastTr.outerHeight() - headerHeight;
+
+ if (top > dc.body2.height() || bottom < 0){
+ reload.call(this);
+ } else if (top > 0){
+ var page = Math.floor(this.index/opts.pageSize);
+ this.getRows.call(this, target, page, function(rows){
+ this.r2 = this.r1;
+ this.r1 = rows;
+ this.index = (page-1)*opts.pageSize;
+ this.rows = this.r1.concat(this.r2);
+ this.populate.call(this, target);
+ });
+ } else if (bottom < dc.body2.height()){
+ var page = Math.floor(this.index/opts.pageSize)+2;
+ if (this.r2.length){
+ page++;
+ }
+ this.getRows.call(this, target, page, function(rows){
+ if (!this.r2.length){
+ this.r2 = rows;
+ } else {
+ this.r1 = this.r2;
+ this.r2 = rows;
+ this.index += opts.pageSize;
+ }
+ this.rows = this.r1.concat(this.r2);
+ this.populate.call(this, target);
+ });
+ }
+ }
+
+ function reload(){
+ var top = $(dc.body2).scrollTop();
+ var index = Math.floor(top/25);
+ var page = Math.floor(index/opts.pageSize) + 1;
+
+ this.getRows.call(this, target, page, function(rows){
+ this.index = (page-1)*opts.pageSize;
+ this.rows = rows;
+ this.r1 = rows;
+ this.r2 = [];
+ this.populate.call(this, target);
+ dc.body2.triggerHandler('scroll.datagrid');
+ });
+ }
+ }
+ function createHeaderExpander(){
+ if (!opts.detailFormatter){return}
+
+ var t = $(target);
+ var hasExpander = false;
+ var fields = t.datagrid('getColumnFields',true).concat(t.datagrid('getColumnFields'));
+ for(var i=0; i');
+ if ($('tr',t).length == 0){
+ td.wrap('
').parent().appendTo($('tbody',t));
+ } else if (opts.rownumbers){
+ td.insertAfter(t.find('td:has(div.datagrid-header-rownumber)'));
+ } else {
+ td.prependTo(t.find('tr:first'));
+ }
+ }
+
+ setTimeout(function(){
+ view.bindEvents(target);
+ },0);
+ }
+ },
+
+ onAfterRender: function(target){
+ $.fn.datagrid.defaults.view.onAfterRender.call(this, target);
+ var dc = $.data(target, 'datagrid').dc;
+ var footer = dc.footer1.add(dc.footer2);
+ footer.find('span.datagrid-row-expander').css('visibility', 'hidden');
+ },
+
+ getRows: function(target, page, callback){
+ var state = $.data(target, 'datagrid');
+ var opts = state.options;
+ var index = (page-1)*opts.pageSize;
+
+ if (opts.onBeforeFetch.call(target, page) == false){return}
+
+ var rows = state.data.firstRows.slice(index, index+opts.pageSize);
+ if (rows.length){
+ opts.onFetch.call(target, page, rows);
+ callback.call(this, rows);
+ } else {
+ var param = $.extend({}, opts.queryParams, {
+ page: page,
+ rows: opts.pageSize
+ });
+ if (opts.sortName){
+ $.extend(param, {
+ sort: opts.sortName,
+ order: opts.sortOrder
+ });
+ }
+ if (opts.onBeforeLoad.call(target, param) == false) return;
+
+ $(target).datagrid('loading');
+ var result = opts.loader.call(target, param, function(data){
+ $(target).datagrid('loaded');
+ var data = opts.loadFilter.call(target, data);
+ opts.onFetch.call(target, page, data.rows);
+ if (data.rows && data.rows.length){
+ callback.call(opts.view, data.rows);
+ } else {
+ opts.onLoadSuccess.call(target, data);
+ }
+ }, function(){
+ $(target).datagrid('loaded');
+ opts.onLoadError.apply(target, arguments);
+ });
+ if (result == false){
+ $(target).datagrid('loaded');
+ if (!state.data.firstRows.length){
+ opts.onFetch.call(target, page, state.data.firstRows);
+ opts.onLoadSuccess.call(target, state.data);
+ }
+ }
+ }
+ },
+
+ populate: function(target){
+ var state = $.data(target, 'datagrid');
+ var opts = state.options;
+ var dc = state.dc;
+ var rowHeight = 25;
+
+ if (this.rows.length){
+ opts.view.render.call(opts.view, target, dc.body2, false);
+ opts.view.render.call(opts.view, target, dc.body1, true);
+// dc.body1.add(dc.body2).children('table.datagrid-btable').css({
+// paddingTop: this.index*rowHeight,
+// paddingBottom: state.data.total*rowHeight - this.rows.length*rowHeight - this.index*rowHeight
+// });
+ dc.body1.add(dc.body2).children('table.datagrid-btable').css({
+ marginTop: this.index*rowHeight,
+ marginBottom: state.data.total*rowHeight - this.rows.length*rowHeight - this.index*rowHeight
+ });
+
+ var r = [];
+ for(var i=0; i