From: Tom Clegg
Date: Sat, 16 Aug 2014 06:06:20 +0000 (-0400)
Subject: Merge branch '2800-python-global-state' into 2800-pgs
X-Git-Tag: 1.1.0~2316^2~5
X-Git-Url: https://git.arvados.org/arvados.git/commitdiff_plain/0bd1c28bed9a0756c61037947d5a9dccd5066f00?hp=c6db15d0529212126920002665c9c99a4f864529
Merge branch '2800-python-global-state' into 2800-pgs
Conflicts:
sdk/python/arvados/api.py
---
diff --git a/.gitignore b/.gitignore
index 0cddee596c..8cc6b89324 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,11 +11,9 @@ sdk/perl/Makefile
sdk/perl/blib
sdk/perl/pm_to_blib
*/vendor/bundle
-services/keep/bin
-services/keep/pkg
-services/keep/src/github.com
sdk/java/target
*.class
apps/workbench/vendor/bundle
services/api/vendor/bundle
sdk/java/log
+sdk/cli/vendor
diff --git a/apps/workbench/Gemfile b/apps/workbench/Gemfile
index 754d5c6043..a289303e4d 100644
--- a/apps/workbench/Gemfile
+++ b/apps/workbench/Gemfile
@@ -49,6 +49,8 @@ gem 'bootstrap-x-editable-rails'
gem 'less'
gem 'less-rails'
+gem 'wiselinks'
+gem 'sshkey'
# To use ActiveModel has_secure_password
# gem 'bcrypt-ruby', '~> 3.0.0'
diff --git a/apps/workbench/Gemfile.lock b/apps/workbench/Gemfile.lock
index 173be13cc3..3daa501ca5 100644
--- a/apps/workbench/Gemfile.lock
+++ b/apps/workbench/Gemfile.lock
@@ -167,6 +167,7 @@ GEM
activesupport (>= 3.0)
sprockets (~> 2.8)
sqlite3 (1.3.8)
+ sshkey (1.6.1)
therubyracer (0.12.0)
libv8 (~> 3.16.14.0)
ref
@@ -183,6 +184,7 @@ GEM
json (>= 1.8.0)
websocket (1.0.7)
websocket-driver (0.3.2)
+ wiselinks (1.2.1)
xpath (2.0.0)
nokogiri (~> 1.3)
@@ -216,6 +218,8 @@ DEPENDENCIES
simplecov (~> 0.7.1)
simplecov-rcov
sqlite3
+ sshkey
themes_for_rails!
therubyracer
uglifier (>= 1.0.3)
+ wiselinks
diff --git a/apps/workbench/app/assets/javascripts/application.js b/apps/workbench/app/assets/javascripts/application.js
index d21c4b5625..c4cb68322f 100644
--- a/apps/workbench/app/assets/javascripts/application.js
+++ b/apps/workbench/app/assets/javascripts/application.js
@@ -21,6 +21,7 @@
//= require bootstrap/modal
//= require bootstrap/button
//= require bootstrap3-editable/bootstrap-editable
+//= require wiselinks
//= require_tree .
jQuery(function($){
@@ -135,11 +136,13 @@ jQuery(function($){
on('ajax:complete ready', function() {
// See http://getbootstrap.com/javascript/#buttons
$('.btn').button();
- });
-
- $(document).
+ }).
on('ready ajax:complete', function() {
$('[data-toggle~=tooltip]').tooltip({container:'body'});
+ }).
+ on('ready ajax:complete', function() {
+ // This makes the dialog close on Esc key, obviously.
+ $('.modal').attr('tabindex', '-1')
});
HeaderRowFixer = function(selector) {
@@ -177,4 +180,29 @@ jQuery(function($){
fixer.duplicateTheadTr();
fixer.fixThead();
});
+
+ $(document).ready(function() {
+ /* When wiselinks is initialized, selection.js is not working. Since we want to stop
+ using selection.js in the near future, let's not initialize wiselinks for now. */
+
+ // window.wiselinks = new Wiselinks();
+
+ $(document).off('page:loading').on('page:loading', function(event, $target, render, url){
+ $("#page-wrapper").fadeOut(200);
+ });
+
+ $(document).off('page:redirected').on('page:redirected', function(event, $target, render, url){
+ });
+
+ $(document).off('page:always').on('page:always', function(event, xhr, settings){
+ $("#page-wrapper").fadeIn(200);
+ });
+
+ $(document).off('page:done').on('page:done', function(event, $target, status, url, data){
+ });
+
+ $(document).off('page:fail').on('page:fail', function(event, $target, status, url, error, code){
+ });
+ });
+
});
diff --git a/apps/workbench/app/assets/javascripts/filterable.js b/apps/workbench/app/assets/javascripts/filterable.js
index 76c5ac3ca1..7da461ec44 100644
--- a/apps/workbench/app/assets/javascripts/filterable.js
+++ b/apps/workbench/app/assets/javascripts/filterable.js
@@ -1,11 +1,12 @@
$(document).
- on('paste keyup change', 'input[type=text].filterable-control', function() {
+ on('paste keyup input', 'input[type=text].filterable-control', function() {
var q = new RegExp($(this).val(), 'i');
$($(this).attr('data-filterable-target')).
addClass('filterable-container').
data('q', q).
trigger('refresh');
}).on('refresh', '.filterable-container', function() {
+ var $container = $(this);
var q = $(this).data('q');
var filters = $(this).data('filters');
$('.filterable', this).hide().filter(function() {
@@ -26,6 +27,16 @@ $(document).
}
return pass;
}).show();
+
+ // Show/hide each section heading depending on whether any
+ // content rows are visible in that section.
+ $('.row[data-section-heading]', this).each(function(){
+ $(this).toggle($('.row.filterable[data-section-name="' +
+ $(this).attr('data-section-name') +
+ '"]:visible').length > 0);
+ });
+
+ // Load more content if the last result is showing.
$('.infinite-scroller').add(window).trigger('scroll');
}).on('change', 'select.filterable-control', function() {
var val = $(this).val();
@@ -38,5 +49,5 @@ $(document).
data('filters', filters).
trigger('refresh');
}).on('ajax:complete', function() {
- $('.filterable-control').trigger('change');
+ $('.filterable-control').trigger('input');
});
diff --git a/apps/workbench/app/assets/javascripts/infinite_scroll.js b/apps/workbench/app/assets/javascripts/infinite_scroll.js
index 1f9997efa0..e70ca259e4 100644
--- a/apps/workbench/app/assets/javascripts/infinite_scroll.js
+++ b/apps/workbench/app/assets/javascripts/infinite_scroll.js
@@ -1,25 +1,31 @@
-function maybe_load_more_content() {
+function maybe_load_more_content(event) {
var scroller = this; // element with scroll bars
- var container; // element that receives new content
+ var $container; // element that receives new content
var src; // url for retrieving content
var scrollHeight;
var spinner, colspan;
+ var serial = Date.now();
scrollHeight = scroller.scrollHeight || $('body')[0].scrollHeight;
- var num_scrollers = $(window).data("arv-num-scrollers");
if ($(scroller).scrollTop() + $(scroller).height()
>
scrollHeight - 50)
{
- for (var i = 0; i < num_scrollers; i++) {
- $container = $($(this).data('infinite-container'+i));
+ $container = $(event.data.container);
+ if (!$container.attr('data-infinite-content-href0')) {
+ // Remember the first page source url, so we can refresh
+ // from page 1 later.
+ $container.attr('data-infinite-content-href0',
+ $container.attr('data-infinite-content-href'));
+ }
src = $container.attr('data-infinite-content-href');
if (!src || !$container.is(':visible'))
- continue;
+ // Finished
+ return;
// Don't start another request until this one finishes
$container.attr('data-infinite-content-href', null);
spinner = '';
- if ($(container).is('table,tbody,thead,tfoot')) {
+ if ($container.is('table,tbody,thead,tfoot')) {
// Hack to determine how many columns a new tr should have
// in order to reach full width.
colspan = $container.closest('table').
@@ -30,16 +36,21 @@ function maybe_load_more_content() {
spinner +
'');
}
+ $container.find(".spinner").detach();
$container.append(spinner);
+ $container.attr('data-infinite-serial', serial);
$.ajax(src,
{dataType: 'json',
type: 'GET',
- data: {},
- context: {container: $container, src: src}}).
- always(function() {
- $(this.container).find(".spinner").detach();
- }).
+ data: ($container.data('infinite-content-params') || {}),
+ context: {container: $container, src: src, serial: serial}}).
fail(function(jqxhr, status, error) {
+ var $faildiv;
+ var $container = this.container;
+ if ($container.attr('data-infinite-serial') != this.serial) {
+ // A newer request is already in progress.
+ return;
+ }
if (jqxhr.readyState == 0 || jqxhr.status == 0) {
message = "Cancelled."
} else if (jqxhr.responseJSON && jqxhr.responseJSON.errors) {
@@ -47,32 +58,60 @@ function maybe_load_more_content() {
} else {
message = "Request failed.";
}
- // TODO: report this to the user.
+ // TODO: report the message to the user.
console.log(message);
- $(this.container).attr('data-infinite-content-href', this.src);
+ $faildiv = $('').
+ attr('data-infinite-content-href', this.src).
+ addClass('infinite-retry').
+ append(' Oops, request failed. ');
+ $container.find('div.spinner').replaceWith($faildiv);
}).
done(function(data, status, jqxhr) {
- $(this.container).append(data.content);
- $(this.container).attr('data-infinite-content-href', data.next_page_href);
+ if ($container.attr('data-infinite-serial') != this.serial) {
+ // A newer request is already in progress.
+ return;
+ }
+ $container.find(".spinner").detach();
+ $container.append(data.content);
+ $container.attr('data-infinite-content-href', data.next_page_href);
});
- break;
}
- }
}
$(document).
+ on('click', 'div.infinite-retry button', function() {
+ var $retry_div = $(this).closest('.infinite-retry');
+ var $scroller = $(this).closest('.infinite-scroller')
+ $scroller.attr('data-infinite-content-href',
+ $retry_div.attr('data-infinite-content-href'));
+ $retry_div.replaceWith('');
+ $scroller.trigger('scroll');
+ }).
+ on('refresh-content', '[data-infinite-scroller]', function() {
+ // Clear all rows, reset source href to initial state, and
+ // (if the container is visible) start loading content.
+ var first_page_href = $(this).attr('data-infinite-content-href0');
+ if (!first_page_href)
+ first_page_href = $(this).attr('data-infinite-content-href');
+ $(this).
+ html('').
+ attr('data-infinite-content-href', first_page_href);
+ $('.infinite-scroller').
+ trigger('scroll');
+ }).
on('ready ajax:complete', function() {
- var num_scrollers = 0;
$('[data-infinite-scroller]').each(function() {
+ if ($(this).hasClass('infinite-scroller-ready'))
+ return;
+ $(this).addClass('infinite-scroller-ready');
+
var $scroller = $($(this).attr('data-infinite-scroller'));
if (!$scroller.hasClass('smart-scroll') &&
'scroll' != $scroller.css('overflow-y'))
$scroller = $(window);
$scroller.
addClass('infinite-scroller').
- data('infinite-container'+num_scrollers, this).
- on('scroll', maybe_load_more_content);
- num_scrollers++;
+ on('scroll resize', { container: this }, maybe_load_more_content).
+ trigger('scroll');
});
- $(window).data("arv-num-scrollers", num_scrollers);
});
diff --git a/apps/workbench/app/assets/javascripts/select_modal.js b/apps/workbench/app/assets/javascripts/select_modal.js
index cd23556a4a..1fec3dcb95 100644
--- a/apps/workbench/app/assets/javascripts/select_modal.js
+++ b/apps/workbench/app/assets/javascripts/select_modal.js
@@ -1,14 +1,14 @@
$(document).on('click', '.selectable', function() {
var any;
var $this = $(this);
- if (!$this.hasClass('multiple')) {
- $this.closest('.selectable-container').
+ var $container = $(this).closest('.selectable-container');
+ if (!$container.hasClass('multiple')) {
+ $container.
find('.selectable').
removeClass('active');
}
$this.toggleClass('active');
- any = ($this.
- closest('.selectable-container').
+ any = ($container.
find('.selectable.active').length > 0)
$this.
closest('.modal').
@@ -16,14 +16,19 @@ $(document).on('click', '.selectable', function() {
prop('disabled', !any);
if ($this.hasClass('active')) {
+ var no_preview_available = '(No preview available)
';
+ if (!$this.attr('data-preview-href')) {
+ $(".modal-dialog-preview-pane").html(no_preview_available);
+ return;
+ }
$(".modal-dialog-preview-pane").html('');
$.ajax($this.attr('data-preview-href'),
{dataType: "html"}).
- done(function(data, status, jqxhr) {
+ done(function(data, status, jqxhr) {
$(".modal-dialog-preview-pane").html(data);
}).
fail(function(data, status, jqxhr) {
- $(".modal-dialog-preview-pane").text('Preview load failed.');
+ $(".modal-dialog-preview-pane").html(no_preview_available);
});
}
@@ -65,12 +70,72 @@ $(document).on('click', '.selectable', function() {
$(document).trigger(event_name!=null ? event_name : 'page-refresh',
[data, status, jqxhr, this.action_data]);
});
-});
-$(document).on('page-refresh', function(event, data, status, jqxhr, action_data) {
+}).on('click', '.chooser-show-project', function() {
+ var params = {};
+ var project_uuid = $(this).attr('data-project-uuid');
+ $(this).attr('href', '#'); // Skip normal click handler
+ if (project_uuid) {
+ params = {'filters[]': JSON.stringify(['owner_uuid',
+ '=',
+ project_uuid]),
+ project_uuid: project_uuid
+ };
+ }
+ // Use current selection as dropdown button label
+ $(this).
+ closest('.dropdown-menu').
+ prev('button').
+ html($(this).text() + ' ');
+ // Set (or unset) filter params and refresh filterable rows
+ $($(this).closest('[data-filterable-target]').attr('data-filterable-target')).
+ data('infinite-content-params', params).
+ trigger('refresh-content');
+}).on('ready', function() {
+ $('form[data-search-modal] a').on('click', function() {
+ $(this).closest('form').submit();
+ return false;
+ });
+ $('form[data-search-modal]').on('submit', function() {
+ // Ask the server for a Search modal. When it arrives, copy
+ // the search string from the top nav input into the modal's
+ // search query field.
+ var $form = $(this);
+ var searchq = $form.find('input').val();
+ var is_a_uuid = /^([0-9a-f]{32}(\+\S+)?|[0-9a-z]{5}-[0-9a-z]{5}-[0-9a-z]{15})$/;
+ if (searchq.trim().match(is_a_uuid)) {
+ window.location = '/actions?uuid=' + encodeURIComponent(searchq.trim());
+ // Show the "loading" indicator. TODO: better page transition hook
+ $(document).trigger('ajax:send');
+ return false;
+ }
+ if ($form.find('a[data-remote]').length > 0) {
+ // A search dialog is already loading.
+ return false;
+ }
+ $('').
+ attr('href', $form.attr('data-search-modal')).
+ attr('data-remote', 'true').
+ attr('data-method', 'GET').
+ hide().
+ appendTo($form).
+ on('ajax:success', function(data, status, xhr) {
+ $('body > .modal-container input[type=text]').
+ val($form.find('input').val()).
+ focus();
+ $form.find('input').val('');
+ }).on('ajax:complete', function() {
+ $(this).detach();
+ }).
+ click();
+ return false;
+ });
+}).on('page-refresh', function(event, data, status, jqxhr, action_data) {
window.location.reload();
}).on('tab-refresh', function(event, data, status, jqxhr, action_data) {
$(document).trigger('arv:pane:reload:all');
$('body > .modal-container .modal').modal('hide');
}).on('redirect-to-created-object', function(event, data, status, jqxhr, action_data) {
window.location.href = data.href.replace(/^[^\/]*\/\/[^\/]*/, '');
+}).on('shown.bs.modal', 'body > .modal-container .modal', function() {
+ $('.focus-on-display', this).focus();
});
diff --git a/apps/workbench/app/assets/stylesheets/application.css.scss b/apps/workbench/app/assets/stylesheets/application.css.scss
index 75d58ed517..6b91783e60 100644
--- a/apps/workbench/app/assets/stylesheets/application.css.scss
+++ b/apps/workbench/app/assets/stylesheets/application.css.scss
@@ -146,6 +146,10 @@ nav.navbar-fixed-top .navbar-nav.navbar-right > li > a:hover {
margin-bottom: -15px;
}
+.infinite-scroller .fa-warning {
+ color: #800;
+}
+
.inline-progress-container div.progress {
margin-bottom: 0;
}
@@ -240,3 +244,7 @@ div.pane-content iframe {
width: 100%;
border: none;
}
+
+div.rounded {
+ border-radius: 3px;
+}
diff --git a/apps/workbench/app/assets/stylesheets/select_modal.css.scss b/apps/workbench/app/assets/stylesheets/select_modal.css.scss
index 09017697b6..4295f30ece 100644
--- a/apps/workbench/app/assets/stylesheets/select_modal.css.scss
+++ b/apps/workbench/app/assets/stylesheets/select_modal.css.scss
@@ -14,3 +14,6 @@
background: #428bca;
color: #fff;
}
+.selectable-container > .row.class-separator {
+ background: #ddd;
+}
diff --git a/apps/workbench/app/controllers/actions_controller.rb b/apps/workbench/app/controllers/actions_controller.rb
index 9a76e9aed4..d1dc0fca35 100644
--- a/apps/workbench/app/controllers/actions_controller.rb
+++ b/apps/workbench/app/controllers/actions_controller.rb
@@ -10,6 +10,19 @@ class ActionsController < ApplicationController
ArvadosBase::resource_class_for_uuid(params[:uuid])
end
+ def show
+ @object = model_class.andand.find(params[:uuid])
+ if @object.is_a? Link and
+ @object.link_class == 'name' and
+ ArvadosBase::resource_class_for_uuid(@object.head_uuid) == Collection
+ redirect_to collection_path(id: @object.uuid)
+ elsif @object
+ redirect_to @object
+ else
+ raise ActiveRecord::RecordNotFound
+ end
+ end
+
def post
params.keys.collect(&:to_sym).each do |param|
if @@exposed_actions[param]
diff --git a/apps/workbench/app/controllers/application_controller.rb b/apps/workbench/app/controllers/application_controller.rb
index f739ee1046..222888085d 100644
--- a/apps/workbench/app/controllers/application_controller.rb
+++ b/apps/workbench/app/controllers/application_controller.rb
@@ -12,8 +12,11 @@ class ApplicationController < ActionController::Base
# Methods that don't require login should
# skip_around_filter :require_thread_api_token
around_filter :require_thread_api_token, except: ERROR_ACTIONS
+ before_filter :accept_uuid_as_id_param, except: ERROR_ACTIONS
before_filter :check_user_agreements, except: ERROR_ACTIONS
+ before_filter :check_user_profile, except: [:update_profile] + ERROR_ACTIONS
before_filter :check_user_notifications, except: ERROR_ACTIONS
+ before_filter :load_filters_and_paging_params, except: ERROR_ACTIONS
before_filter :find_object_by_uuid, except: [:index, :choose] + ERROR_ACTIONS
theme :select_theme
@@ -86,7 +89,10 @@ class ApplicationController < ActionController::Base
end
end
- def find_objects_for_index
+ def load_filters_and_paging_params
+ @order = params[:order] || 'created_at desc'
+ @order = [@order] unless @order.is_a? Array
+
@limit ||= 200
if params[:limit]
@limit = params[:limit].to_i
@@ -102,10 +108,22 @@ class ApplicationController < ActionController::Base
filters = params[:filters]
if filters.is_a? String
filters = Oj.load filters
+ elsif filters.is_a? Array
+ filters = filters.collect do |filter|
+ if filter.is_a? String
+ # Accept filters[]=["foo","=","bar"]
+ Oj.load filter
+ else
+ # Accept filters=[["foo","=","bar"]]
+ filter
+ end
+ end
end
@filters += filters
end
+ end
+ def find_objects_for_index
@objects ||= model_class
@objects = @objects.filter(@filters).limit(@limit).offset(@offset)
end
@@ -114,10 +132,8 @@ class ApplicationController < ActionController::Base
respond_to do |f|
f.json { render json: @objects }
f.html {
- if params['tab_pane']
- comparable = self.respond_to? :compare
- render(partial: 'show_' + params['tab_pane'].downcase,
- locals: { comparable: comparable, objects: @objects })
+ if params[:tab_pane]
+ render_pane params[:tab_pane]
else
render
end
@@ -126,6 +142,23 @@ class ApplicationController < ActionController::Base
end
end
+ helper_method :render_pane
+ def render_pane tab_pane, opts={}
+ render_opts = {
+ partial: 'show_' + tab_pane.downcase,
+ locals: {
+ comparable: self.respond_to?(:compare),
+ objects: @objects,
+ tab_pane: tab_pane
+ }.merge(opts[:locals] || {})
+ }
+ if opts[:to_string]
+ render_to_string render_opts
+ else
+ render render_opts
+ end
+ end
+
def index
find_objects_for_index if !@objects
render_index
@@ -148,6 +181,13 @@ class ApplicationController < ActionController::Base
end
end
+ helper_method :next_page_href
+ def next_page_href with_params={}
+ if next_page_offset
+ url_for with_params.merge(offset: next_page_offset)
+ end
+ end
+
def show
if !@object
return render_not_found("object not found")
@@ -156,9 +196,7 @@ class ApplicationController < ActionController::Base
f.json { render json: @object.attributes.merge(href: url_for(@object)) }
f.html {
if params['tab_pane']
- comparable = self.respond_to? :compare
- render(partial: 'show_' + params['tab_pane'].downcase,
- locals: { comparable: comparable, objects: @objects })
+ render_pane params['tab_pane']
elsif request.method.in? ['GET', 'HEAD']
render
else
@@ -171,26 +209,14 @@ class ApplicationController < ActionController::Base
def choose
params[:limit] ||= 40
- if !@objects
- if params[:project_uuid] and !params[:project_uuid].empty?
- # We want the chooser to show objects of the controllers's model_class
- # type within a specific project specified by project_uuid, so fetch the
- # project and request the contents of the project filtered on the
- # controllers's model_class kind.
- @objects = Group.find(params[:project_uuid]).contents({:filters => [['uuid', 'is_a', "arvados\##{ArvadosApiClient.class_kind(model_class)}"]]})
- end
- find_objects_for_index if !@objects
- end
+ find_objects_for_index if !@objects
respond_to do |f|
if params[:partial]
f.json {
render json: {
content: render_to_string(partial: "choose_rows.html",
- formats: [:html],
- locals: {
- multiple: params[:multiple]
- }),
- next_page_href: @next_page_href
+ formats: [:html]),
+ next_page_href: next_page_href(partial: params[:partial])
}
}
end
@@ -341,10 +367,14 @@ class ApplicationController < ActionController::Base
end
end
- def find_object_by_uuid
+
+ def accept_uuid_as_id_param
if params[:id] and params[:id].match /\D/
params[:uuid] = params.delete :id
end
+ end
+
+ def find_object_by_uuid
begin
if not model_class
@object = nil
@@ -380,9 +410,6 @@ class ApplicationController < ActionController::Base
Thread.current[:arvados_api_token] = new_token
if new_token.nil?
Thread.current[:user] = nil
- elsif (new_token == session[:arvados_api_token]) and
- session[:user].andand[:is_active]
- Thread.current[:user] = User.new(session[:user])
else
Thread.current[:user] = User.current
end
@@ -400,15 +427,7 @@ class ApplicationController < ActionController::Base
false # We may redirect to login, or not, based on the current action.
else
session[:arvados_api_token] = params[:api_token]
- session[:user] = {
- uuid: user.uuid,
- email: user.email,
- first_name: user.first_name,
- last_name: user.last_name,
- is_active: user.is_active,
- is_admin: user.is_admin,
- prefs: user.prefs
- }
+
if !request.format.json? and request.method.in? ['GET', 'HEAD']
# Repeat this request with api_token in the (new) session
# cookie instead of the query string. This prevents API
@@ -502,6 +521,41 @@ class ApplicationController < ActionController::Base
true
end
+ def check_user_profile
+ if request.method.downcase != 'get' || params[:partial] ||
+ params[:tab_pane] || params[:action_method] ||
+ params[:action] == 'setup_popup'
+ return true
+ end
+
+ if missing_required_profile?
+ render 'users/profile'
+ end
+ true
+ end
+
+ helper_method :missing_required_profile?
+ def missing_required_profile?
+ missing_required = false
+
+ profile_config = Rails.configuration.user_profile_form_fields
+ if current_user && profile_config
+ current_user_profile = current_user.prefs[:profile]
+ profile_config.kind_of?(Array) && profile_config.andand.each do |entry|
+ if entry['required']
+ if !current_user_profile ||
+ !current_user_profile[entry['key'].to_sym] ||
+ current_user_profile[entry['key'].to_sym].empty?
+ missing_required = true
+ break
+ end
+ end
+ end
+ end
+
+ missing_required
+ end
+
def select_theme
return Rails.configuration.arvados_theme
end
@@ -569,7 +623,7 @@ class ApplicationController < ActionController::Base
helper_method :all_projects
def all_projects
@all_projects ||= Group.
- filter([['group_class','in',['project','folder']]]).order('name')
+ filter([['group_class','=','project']]).order('name')
end
helper_method :my_projects
@@ -609,8 +663,8 @@ class ApplicationController < ActionController::Base
(Job.limit(10) |
PipelineInstance.limit(10)).
sort_by do |x|
- x.finished_at || x.started_at || x.created_at rescue x.created_at
- end
+ (x.finished_at || x.started_at rescue nil) || x.modified_at || x.created_at
+ end.reverse
end
helper_method :my_project_tree
@@ -680,7 +734,7 @@ class ApplicationController < ActionController::Base
crumbs = []
current = @name_link || @object
while current
- if current.is_a?(Group) and current.group_class.in?(['project','folder'])
+ if current.is_a?(Group) and current.group_class == 'project'
crumbs.prepend current
end
if current.is_a? Link
@@ -694,7 +748,7 @@ class ApplicationController < ActionController::Base
helper_method :current_project_uuid
def current_project_uuid
- if @object.is_a? Group and @object.group_class.in?(['project','folder'])
+ if @object.is_a? Group and @object.group_class == 'project'
@object.uuid
elsif @name_link.andand.tail_uuid
@name_link.tail_uuid
@@ -872,4 +926,7 @@ class ApplicationController < ActionController::Base
@objects_for
end
+ def wiselinks_layout
+ 'body'
+ end
end
diff --git a/apps/workbench/app/controllers/collections_controller.rb b/apps/workbench/app/controllers/collections_controller.rb
index 5a7a52207b..bea72a4274 100644
--- a/apps/workbench/app/controllers/collections_controller.rb
+++ b/apps/workbench/app/controllers/collections_controller.rb
@@ -45,22 +45,17 @@ class CollectionsController < ApplicationController
def choose
params[:limit] ||= 40
- filter = [['link_class','=','name'],
- ['head_uuid','is_a','arvados#collection']]
-
- if params[:project_uuid] and !params[:project_uuid].empty?
- filter << ['tail_uuid', '=', params[:project_uuid]]
- end
-
- @objects = Link.filter(filter)
+ @filters += [['link_class','=','name'],
+ ['head_uuid','is_a','arvados#collection']]
+ @objects = Link
find_objects_for_index
- @next_page_href = (next_page_offset and
- url_for(offset: next_page_offset, partial: true))
+
@name_links = @objects
@objects = Collection.
filter([['uuid','in',@name_links.collect(&:head_uuid)]])
+ preload_links_for_objects @objects.to_a
super
end
diff --git a/apps/workbench/app/controllers/groups_controller.rb b/apps/workbench/app/controllers/groups_controller.rb
index 7698fdba93..080386ea5c 100644
--- a/apps/workbench/app/controllers/groups_controller.rb
+++ b/apps/workbench/app/controllers/groups_controller.rb
@@ -1,6 +1,6 @@
class GroupsController < ApplicationController
def index
- @groups = Group.filter [['group_class', 'not in', ['folder', 'project']]]
+ @groups = Group.filter [['group_class', '!=', 'project']]
@group_uuids = @groups.collect &:uuid
@links_from = Link.where link_class: 'permission', tail_uuid: @group_uuids
@links_to = Link.where link_class: 'permission', head_uuid: @group_uuids
@@ -8,7 +8,7 @@ class GroupsController < ApplicationController
end
def show
- if @object.group_class.in?(['project','folder'])
+ if @object.group_class == 'project'
redirect_to(project_path(@object))
else
super
diff --git a/apps/workbench/app/controllers/projects_controller.rb b/apps/workbench/app/controllers/projects_controller.rb
index f32f356d0c..6287793f76 100644
--- a/apps/workbench/app/controllers/projects_controller.rb
+++ b/apps/workbench/app/controllers/projects_controller.rb
@@ -3,6 +3,31 @@ class ProjectsController < ApplicationController
Group
end
+ def find_object_by_uuid
+ if current_user and params[:uuid] == current_user.uuid
+ @object = current_user.dup
+ @object.uuid = current_user.uuid
+ class << @object
+ def name
+ 'Home'
+ end
+ def description
+ ''
+ end
+ def attribute_editable? attr, *args
+ case attr
+ when 'description', 'name'
+ false
+ else
+ super
+ end
+ end
+ end
+ else
+ super
+ end
+ end
+
def index_pane_list
%w(Projects)
end
@@ -52,6 +77,32 @@ class ProjectsController < ApplicationController
end
end
+ def move_items
+ target_uuid = params['target']
+ uuids_to_add = session[:selected_move_items]
+
+ uuids_to_add.
+ collect { |x| ArvadosBase::resource_class_for_uuid(x) }.
+ uniq.
+ each do |resource_class|
+ resource_class.filter([['uuid','in',uuids_to_add]]).each do |dst|
+ if resource_class == Collection
+ dst = Link.new(owner_uuid: target_uuid,
+ tail_uuid: target_uuid,
+ head_uuid: dst.uuid,
+ link_class: 'name',
+ name: target_uuid)
+ else
+ dst.owner_uuid = target_uuid
+ dst.tail_uuid = target_uuid if dst.class == Link
+ end
+ dst.save!
+ end
+ end
+ session[:selected_move_items] = nil
+ redirect_to @object
+ end
+
def destroy
while (objects = Link.filter([['owner_uuid','=',@object.uuid],
['tail_uuid','=',@object.uuid]])).any?
@@ -77,48 +128,86 @@ class ProjectsController < ApplicationController
super
end
+ def load_contents_objects kinds=[]
+ kind_filters = @filters.select do |attr,op,val|
+ op == 'is_a' and val.is_a? Array and val.count > 1
+ end
+ if /^created_at\b/ =~ @order[0] and kind_filters.count == 1
+ # If filtering on multiple types and sorting by date: Get the
+ # first page of each type, sort the entire set, truncate to one
+ # page, and use the last item on this page as a filter for
+ # retrieving the next page. Ideally the API would do this for
+ # us, but it doesn't (yet).
+ nextpage_operator = /\bdesc$/i =~ @order[0] ? '<' : '>'
+ @objects = []
+ @name_link_for = {}
+ kind_filters.each do |attr,op,val|
+ (val.is_a?(Array) ? val : [val]).each do |type|
+ objects = @object.contents(order: @order,
+ limit: @limit,
+ include_linked: true,
+ filters: (@filters - kind_filters + [['uuid', 'is_a', type]]),
+ offset: @offset)
+ objects.each do |object|
+ @name_link_for[object.andand.uuid] = objects.links_for(object, 'name').first
+ end
+ @objects += objects
+ end
+ end
+ @objects = @objects.to_a.sort_by(&:created_at)
+ @objects.reverse! if nextpage_operator == '<'
+ @objects = @objects[0..@limit-1]
+ @next_page_filters = @filters.reject do |attr,op,val|
+ attr == 'created_at' and op == nextpage_operator
+ end
+ if @objects.any?
+ @next_page_filters += [['created_at',
+ nextpage_operator,
+ @objects.last.created_at]]
+ @next_page_href = url_for(partial: :contents_rows,
+ filters: @next_page_filters.to_json)
+ else
+ @next_page_href = nil
+ end
+ else
+ @objects = @object.contents(order: @order,
+ limit: @limit,
+ include_linked: true,
+ filters: @filters,
+ offset: @offset)
+ @next_page_href = next_page_href(partial: :contents_rows)
+ end
+ end
+
def show
if !@object
return render_not_found("object not found")
end
- @objects = @object.contents(limit: 50,
- include_linked: true,
- filters: params[:filters],
- offset: params[:offset] || 0)
- @logs = Log.limit(10).filter([['object_uuid', '=', @object.uuid]])
- @users = User.limit(10000).
- select(["uuid", "is_active", "first_name", "last_name"]).
- filter([['is_active', '=', 'true']])
- @groups = Group.limit(10000).
- select(["uuid", "name", "description"])
- begin
- @share_links = Link.permissions_for(@object)
- @user_is_manager = true
- rescue ArvadosApiClient::AccessForbiddenException,
- ArvadosApiClient::NotFoundException
- @share_links = []
- @user_is_manager = false
+ @user_is_manager = false
+ @share_links = []
+ if @object.uuid != current_user.uuid
+ begin
+ @share_links = Link.permissions_for(@object)
+ @user_is_manager = true
+ rescue ArvadosApiClient::AccessForbiddenException,
+ ArvadosApiClient::NotFoundException
+ end
end
- @objects_and_names = get_objects_and_names @objects
-
if params[:partial]
+ load_contents_objects
respond_to do |f|
f.json {
render json: {
content: render_to_string(partial: 'show_contents_rows.html',
- formats: [:html],
- locals: {
- objects_and_names: @objects_and_names,
- project: @object
- }),
- next_page_href: (next_page_offset and
- url_for(offset: next_page_offset, filters: params[:filters], partial: true))
+ formats: [:html]),
+ next_page_href: @next_page_href
}
}
end
else
+ @objects = []
super
end
end
@@ -135,13 +224,17 @@ class ProjectsController < ApplicationController
end
helper_method :get_objects_and_names
- def get_objects_and_names(objects)
+ def get_objects_and_names(objects=nil)
+ objects = @objects if objects.nil?
objects_and_names = []
objects.each do |object|
- if !(name_links = objects.links_for(object, 'name')).empty?
+ if objects.respond_to? :links_for and
+ !(name_links = objects.links_for(object, 'name')).empty?
name_links.each do |name_link|
objects_and_names << [object, name_link]
end
+ elsif @name_link_for.andand[object.uuid]
+ objects_and_names << [object, @name_link_for[object.uuid]]
elsif object.respond_to? :name
objects_and_names << [object, object]
else
diff --git a/apps/workbench/app/controllers/search_controller.rb b/apps/workbench/app/controllers/search_controller.rb
new file mode 100644
index 0000000000..6f209a5a9e
--- /dev/null
+++ b/apps/workbench/app/controllers/search_controller.rb
@@ -0,0 +1,26 @@
+class SearchController < ApplicationController
+ def find_objects_for_index
+ search_what = Group
+ if params[:project_uuid]
+ # Special case for "search all things in project":
+ @filters = @filters.select do |attr, operator, operand|
+ not (attr == 'owner_uuid' and operator == '=')
+ end
+ # Special case for project_uuid is a user uuid:
+ if ArvadosBase::resource_class_for_uuid(params[:project_uuid]) == User
+ search_what = User.find params[:project_uuid]
+ else
+ search_what = Group.find params[:project_uuid]
+ end
+ end
+ @objects = search_what.contents(limit: @limit,
+ offset: @offset,
+ filters: @filters,
+ include_linked: true)
+ super
+ end
+
+ def next_page_href with_params={}
+ super with_params.merge(last_object_class: @objects.last.class.to_s)
+ end
+end
diff --git a/apps/workbench/app/controllers/user_agreements_controller.rb b/apps/workbench/app/controllers/user_agreements_controller.rb
index 6ab8ae215f..924bf44bae 100644
--- a/apps/workbench/app/controllers/user_agreements_controller.rb
+++ b/apps/workbench/app/controllers/user_agreements_controller.rb
@@ -1,6 +1,7 @@
class UserAgreementsController < ApplicationController
skip_before_filter :check_user_agreements
skip_before_filter :find_object_by_uuid
+ skip_before_filter :check_user_profile
def model_class
Collection
diff --git a/apps/workbench/app/controllers/users_controller.rb b/apps/workbench/app/controllers/users_controller.rb
index 0313de5aa2..67b51a9bc9 100644
--- a/apps/workbench/app/controllers/users_controller.rb
+++ b/apps/workbench/app/controllers/users_controller.rb
@@ -210,6 +210,73 @@ class UsersController < ApplicationController
end
end
+ def manage_account
+ # repositories current user can read / write
+ repo_links = []
+ Link.filter([['head_uuid', 'is_a', 'arvados#repository'],
+ ['tail_uuid', '=', current_user.uuid],
+ ['link_class', '=', 'permission'],
+ ['name', 'in', ['can_write', 'can_read']],
+ ]).
+ each do |perm_link|
+ repo_links << perm_link[:head_uuid]
+ end
+ @my_repositories = Repository.where(uuid: repo_links)
+
+ # virtual machines the current user can login into
+ @my_vm_logins = {}
+ Link.where(tail_uuid: current_user.uuid,
+ link_class: 'permission',
+ name: 'can_login').
+ each do |perm_link|
+ if perm_link.properties.andand[:username]
+ @my_vm_logins[perm_link.head_uuid] ||= []
+ @my_vm_logins[perm_link.head_uuid] << perm_link.properties[:username]
+ end
+ end
+ @my_virtual_machines = VirtualMachine.where(uuid: @my_vm_logins.keys)
+
+ # current user's ssh keys
+ @my_ssh_keys = AuthorizedKey.where(key_type: 'SSH', owner_uuid: current_user.uuid)
+
+ respond_to do |f|
+ f.html { render template: 'users/manage_account' }
+ end
+ end
+
+ def add_ssh_key_popup
+ respond_to do |format|
+ format.html
+ format.js
+ end
+ end
+
+ def add_ssh_key
+ respond_to do |format|
+ key_params = {'key_type' => 'SSH'}
+ key_params['authorized_user_uuid'] = current_user.uuid
+
+ if params['name'] && params['name'].size>0
+ key_params['name'] = params['name'].strip
+ end
+ if params['public_key'] && params['public_key'].size>0
+ key_params['public_key'] = params['public_key'].strip
+ end
+
+ if !key_params['name'] && params['public_key'].andand.size>0
+ split_key = key_params['public_key'].split
+ key_params['name'] = split_key[-1] if (split_key.size == 3)
+ end
+
+ new_key = AuthorizedKey.create! key_params
+ if new_key
+ format.js
+ else
+ self.render_error status: 422
+ end
+ end
+ end
+
protected
def find_current_links user
diff --git a/apps/workbench/app/helpers/application_helper.rb b/apps/workbench/app/helpers/application_helper.rb
index c3856c2f29..428c14f828 100644
--- a/apps/workbench/app/helpers/application_helper.rb
+++ b/apps/workbench/app/helpers/application_helper.rb
@@ -260,12 +260,18 @@ module ApplicationHelper
display_value = value_info[:link_name]
end
end
+ if (attr == :components) and (subattr.size > 2)
+ chooser_title = "Choose a dataset for #{object.component_input_title(subattr[0], subattr[2])}:"
+ else
+ chooser_title = "Choose a dataset:"
+ end
modal_path = choose_collections_path \
- ({ title: 'Choose a dataset:',
- filters: [['tail_uuid', '=', object.owner_uuid]].to_json,
+ ({ title: chooser_title,
+ filters: [['owner_uuid', '=', object.owner_uuid]].to_json,
action_name: 'OK',
action_href: pipeline_instance_path(id: object.uuid),
action_method: 'patch',
+ preconfigured_search_str: "#{value_info[:search_for]}",
action_data: {
merge: true,
selection_param: selection_param,
@@ -428,4 +434,13 @@ module ApplicationHelper
RESOURCE_CLASS_ICONS.fetch(class_name, default)
end
end
+
+ def chooser_preview_url_for object
+ case object.class.to_s
+ when 'Collection'
+ polymorphic_path(object, tab_pane: 'chooser_preview')
+ else
+ nil
+ end
+ end
end
diff --git a/apps/workbench/app/models/group.rb b/apps/workbench/app/models/group.rb
index 9e627bf66e..558c587a1c 100644
--- a/apps/workbench/app/models/group.rb
+++ b/apps/workbench/app/models/group.rb
@@ -3,6 +3,15 @@ class Group < ArvadosBase
true
end
+ def self.contents params={}
+ res = arvados_api_client.api self, "/contents", {
+ _method: 'GET'
+ }.merge(params)
+ ret = ArvadosResourceList.new
+ ret.results = arvados_api_client.unpack_api_response(res)
+ ret
+ end
+
def contents params={}
res = arvados_api_client.api self.class, "/#{self.uuid}/contents", {
_method: 'GET'
@@ -13,7 +22,7 @@ class Group < ArvadosBase
end
def class_for_display
- group_class.in?(['folder', 'project']) ? 'Project' : super
+ group_class == 'project' ? 'Project' : super
end
def editable?
diff --git a/apps/workbench/app/models/pipeline_instance.rb b/apps/workbench/app/models/pipeline_instance.rb
index c3b14755f3..fa9fab68eb 100644
--- a/apps/workbench/app/models/pipeline_instance.rb
+++ b/apps/workbench/app/models/pipeline_instance.rb
@@ -28,7 +28,7 @@ class PipelineInstance < ArvadosBase
end
end
end
-
+
def attribute_editable? attr, *args
super && (attr.to_sym == :name ||
(attr.to_sym == :components and
@@ -42,4 +42,11 @@ class PipelineInstance < ArvadosBase
def self.creatable?
false
end
+
+ def component_input_title(component_name, input_name)
+ component = components[component_name]
+ return nil if component.nil?
+ component[:script_parameters].andand[input_name.to_sym].andand[:title] ||
+ "\"#{input_name.to_s}\" parameter for #{component[:script]} script in #{component_name} component"
+ end
end
diff --git a/apps/workbench/app/models/user.rb b/apps/workbench/app/models/user.rb
index 9c914776a2..87ea5faefa 100644
--- a/apps/workbench/app/models/user.rb
+++ b/apps/workbench/app/models/user.rb
@@ -27,11 +27,15 @@ class User < ArvadosBase
{}))
end
+ def contents params={}
+ Group.contents params.merge(uuid: self.uuid)
+ end
+
def attributes_for_display
super.reject { |k,v| %w(owner_uuid default_owner_uuid identity_url prefs).index k }
end
- def attribute_editable? attr, *args
+ def attribute_editable? attr, *args
(not (self.uuid.andand.match(/000000000000000$/) and self.is_admin)) and super
end
@@ -49,4 +53,10 @@ class User < ArvadosBase
arvados_api_client.api(self, "/setup", params)
end
+ def update_profile params
+ self.private_reload(arvados_api_client.api(self.class,
+ "/#{self.uuid}/profile",
+ params))
+ end
+
end
diff --git a/apps/workbench/app/views/application/404.html.erb b/apps/workbench/app/views/application/404.html.erb
index 40d73b9a73..fd97295956 100644
--- a/apps/workbench/app/views/application/404.html.erb
+++ b/apps/workbench/app/views/application/404.html.erb
@@ -1,7 +1,8 @@
<%
if (controller.andand.action_name == 'show') and params[:uuid]
- class_name = controller.model_class.to_s.underscore.humanize(capitalize: false)
- req_item = safe_join([class_name, " with UUID ",
+ class_name = controller.model_class.to_s.underscore
+ class_name_h = class_name.humanize(capitalize: false)
+ req_item = safe_join([class_name_h, " with UUID ",
raw(""), params[:uuid], raw("
")], "")
else
req_item = "page you requested"
@@ -14,7 +15,7 @@
<% if class_name %>
Perhaps you'd like to
-<%= link_to("browse all #{class_name.pluralize}", action: :index) %>?
+<%= link_to("browse all #{class_name_h.pluralize}", action: :index, controller: class_name.tableize) %>?
<% end %>
diff --git a/apps/workbench/app/views/application/_choose.html.erb b/apps/workbench/app/views/application/_choose.html.erb
index 3c1aaadf7d..68351a9499 100644
--- a/apps/workbench/app/views/application/_choose.html.erb
+++ b/apps/workbench/app/views/application/_choose.html.erb
@@ -7,48 +7,59 @@