# This can be a symlink to ../../../doc/.site in dev setups
/public/doc
+
+# SimpleCov reports
+/coverage
gem 'capybara'
gem 'poltergeist'
gem 'headless'
+ # Note: "require: false" here tells bunder not to automatically
+ # 'require' the packages during application startup. Installation is
+ # still mandatory.
+ gem 'simplecov', '~> 0.7.1', require: false
+ gem 'simplecov-rcov', require: false
end
gem 'jquery-rails'
multi_json (~> 1.0)
rubyzip (~> 1.0)
websocket (~> 1.0.4)
+ simplecov (0.7.1)
+ multi_json (~> 1.0)
+ simplecov-html (~> 0.7.1)
+ simplecov-html (0.7.1)
+ simplecov-rcov (0.2.3)
+ simplecov (>= 0.4.1)
sprockets (2.2.2)
hike (~> 1.2)
multi_json (~> 1.0)
sass
sass-rails (~> 3.2.0)
selenium-webdriver
+ simplecov (~> 0.7.1)
+ simplecov-rcov
sqlite3
themes_for_rails
therubyracer
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
}
});
- $('.editable').editable();
$('[data-toggle=tooltip]').tooltip();
$('.expand-collapse-row').on('click', function(event) {
-$.fn.editable.defaults.ajaxOptions = {type: 'put', dataType: 'json'};
+$.fn.editable.defaults.ajaxOptions = {type: 'post', dataType: 'json'};
$.fn.editable.defaults.send = 'always';
// Default for editing is popup. I experimented with inline which is a little
$.fn.editable.defaults.params = function (params) {
var a = {};
var key = params.pk.key;
- a.id = params.pk.id;
- a[key] = {};
+ a.id = $(this).attr('data-object-uuid') || params.pk.id;
+ a[key] = params.pk.defaults || {};
+ // Remove null values. Otherwise they get transmitted as empty
+ // strings in request params.
+ for (i in a[key]) {
+ if (a[key][i] == null)
+ delete a[key][i];
+ }
a[key][params.name] = params.value;
+ if (!a.id) {
+ a['_method'] = 'post';
+ } else {
+ a['_method'] = 'put';
+ }
return a;
};
}
}
+$(document).
+ on('ready ajax:complete', function() {
+ $('#editable-submit').click(function() {
+ console.log($(this));
+ });
+ $('.editable').
+ editable({
+ success: function(response, newValue) {
+ // If we just created a new object, stash its UUID
+ // so we edit it next time instead of creating
+ // another new object.
+ if (!$(this).attr('data-object-uuid') && response.uuid) {
+ $(this).attr('data-object-uuid', response.uuid);
+ }
+ if (response.href) {
+ $(this).editable('option', 'url', response.href);
+ }
+ return;
+ }
+ }).
+ on('hidden', function(e, reason) {
+ // After saving a new attribute, update the same
+ // information if it appears elsewhere on the page.
+ if (reason != 'save') return;
+ var html = $(this).html();
+ var uuid = $(this).attr('data-object-uuid');
+ var attr = $(this).attr('data-name');
+ var edited = this;
+ if (uuid && attr) {
+ $("[data-object-uuid='" + uuid + "']" +
+ "[data-name='" + attr + "']").each(function() {
+ if (this != edited)
+ $(this).html(html);
+ });
+ }
+ });
+ });
+
$.fn.editabletypes.text.defaults.tpl = '<input type="text" name="editable-text">'
$.fn.editableform.buttons = '\
--- /dev/null
+$(document).
+ on('ready ajax:complete', function() {
+ $("[data-toggle='x-editable']").click(function(e) {
+ e.stopPropagation();
+ $($(this).attr('data-toggle-selector')).editable('toggle');
+ });
+ }).on('paste keyup change', 'input.search-folder-contents', function() {
+ var q = new RegExp($(this).val(), 'i');
+ $(this).closest('div.panel').find('tbody tr').each(function() {
+ $(this).toggle(!!$(this).text().match(q));
+ });
+ });
--- /dev/null
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
}
var update_count = function(e) {
+ var html;
+ var this_object_uuid = $('#selection-form-content').
+ closest('form').
+ find('input[name=uuid]').val();
var lst = get_selection_list();
$("#persistent-selection-count").text(lst.length);
if (lst.length > 0) {
- $('#selection-form-content').html(
- '<li><a href="#" id="clear_selections_button">Clear selections</a></li>'
- + '<li><input type="submit" name="combine_selected_files_into_collection" '
- + ' id="combine_selected_files_into_collection" '
- + ' value="Combine selected collections and files into a new collection" /></li>'
- + '<li class="notification"><table style="width: 100%"></table></li>');
+ html = '<li><a href="#" class="btn btn-xs btn-info" id="clear_selections_button"><i class="fa fa-fw fa-ban"></i> Clear selections</a></li>';
+ if (this_object_uuid.match('-j7d0g-'))
+ html += '<li><button class="btn btn-xs btn-info" type="submit" name="copy_selections_into_folder" id="copy_selections_into_folder"><i class="fa fa-fw fa-folder-open"></i> Copy selections into this folder</button></li>';
+ html += '<li><button class="btn btn-xs btn-info" type="submit" name="combine_selected_files_into_collection" '
+ + ' id="combine_selected_files_into_collection">'
+ + '<i class="fa fa-fw fa-archive"></i> Combine selected collections and files into a new collection</button></li>'
+ + '<li class="notification"><table style="width: 100%"></table></li>';
+ $('#selection-form-content').html(html);
for (var i = 0; i < lst.length; i++) {
$('#selection-form-content > li > table').append("<tr>"
table.table-justforlayout {
margin-bottom: 0;
}
+.smaller-text {
+ font-size: .8em;
+}
.deemphasize {
font-size: .8em;
color: #888;
}
+.arvados-uuid {
+ font-size: .8em;
+ font-family: monospace;
+}
table .data-size, .table .data-size {
text-align: right;
}
text-decoration: none;
text-shadow: 0 1px 0 #ffffff;
}
-/*.navbar .nav .dropdown .dropdown-menu li a {
- padding: 2px 20px;
-}*/
-
-ul.arvados-nav {
- list-style: none;
- padding-left: 0em;
- margin-left: 0em;
-}
-
-ul.arvados-nav li ul {
- list-style: none;
- padding-left: 0;
-}
-
-ul.arvados-nav li ul li {
- list-style: none;
- padding-left: 1em;
-}
.dax {
max-width: 10%;
li.notification {
padding: 10px;
}
-.arvados-nav-container {
- top: 70px;
- height: calc(100% - 70px);
- overflow: auto;
- z-index: 2;
-}
-
-.arvados-nav-active {
- background: rgb(66, 139, 202);
-}
-
-.arvados-nav-active a, .arvados-nav-active a:hover {
- color: white;
-}
// See HeaderRowFixer in application.js
table.table-fixed-header-row {
overflow-y: auto;
}
+.row-fill-height, .row-fill-height>div[class*='col-'] {
+ display: flex;
+}
+.row-fill-height>div[class*='col-']>div {
+ width: 100%;
+}
+
+/* Show editable popover above side-nav */
+.editable-popup.popover {
+ z-index:1055;
+}
+
+.navbar-nav.side-nav {
+ box-shadow: inset -1px 0 #e7e7e7;
+}
+.navbar-nav.side-nav > li:first-child {
+ margin-top: 5px; /* keep "hover" bg below top nav bottom border */
+}
+.navbar-nav.side-nav > li > a {
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+.navbar-nav.side-nav > li.dropdown > ul.dropdown-menu > li > a {
+ padding-top: 5px;
+ padding-bottom: 5px;
+}
+.navbar-nav.side-nav a.active,
+.navbar-nav.side-nav a:hover,
+.navbar-nav.side-nav a:focus {
+ border-right: 1px solid #ffffff;
+ background: #ffffff;
+}
--- /dev/null
+.card {
+ padding-top: 20px;
+ margin: 10px 0 20px 0;
+ background-color: #ffffff;
+ border: 1px solid #d8d8d8;
+ border-top-width: 0;
+ border-bottom-width: 2px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.card.arvados-object {
+ position: relative;
+ display: inline-block;
+ width: 170px;
+ height: 175px;
+ padding-top: 0;
+ margin-left: 20px;
+ overflow: hidden;
+ vertical-align: top;
+}
+.card.arvados-object .card-top.green {
+ background-color: #53a93f;
+}
+.card.arvados-object .card-top.blue {
+ background-color: #427fed;
+}
+.card.arvados-object .card-top {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: inline-block;
+ width: 170px;
+ height: 25px;
+ background-color: #ffffff;
+}
+.card.arvados-object .card-info {
+ position: absolute;
+ top: 25px;
+ display: inline-block;
+ width: 100%;
+ height: 101px;
+ overflow: hidden;
+ background: #ffffff;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.card.arvados-object .card-info .title {
+ display: block;
+ margin: 8px 14px 0 14px;
+ overflow: hidden;
+ font-size: 16px;
+ font-weight: bold;
+ line-height: 18px;
+ color: #404040;
+}
+.card.arvados-object .card-info .desc {
+ display: block;
+ margin: 8px 14px 0 14px;
+ overflow: hidden;
+ font-size: 12px;
+ line-height: 16px;
+ color: #737373;
+ text-overflow: ellipsis;
+}
+.card.arvados-object .card-bottom {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ display: inline-block;
+ width: 100%;
+ padding: 10px 20px;
+ line-height: 29px;
+ text-align: center;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
--- /dev/null
+// Place all the styles related to the folders controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
--- /dev/null
+/*
+Author: Start Bootstrap - http://startbootstrap.com
+'SB Admin' HTML Template by Start Bootstrap
+
+All Start Bootstrap themes are licensed under Apache 2.0.
+For more info and more free Bootstrap 3 HTML themes, visit http://startbootstrap.com!
+*/
+
+/* ATTN: This is mobile first CSS - to update 786px and up screen width use the media query near the bottom of the document! */
+
+/* Global Styles */
+
+body {
+ margin-top: 50px;
+}
+
+#wrapper {
+ padding-left: 0;
+}
+
+#page-wrapper {
+ width: 100%;
+ padding: 5px 15px;
+}
+
+/* Nav Messages */
+
+.messages-dropdown .dropdown-menu .message-preview .avatar,
+.messages-dropdown .dropdown-menu .message-preview .name,
+.messages-dropdown .dropdown-menu .message-preview .message,
+.messages-dropdown .dropdown-menu .message-preview .time {
+ display: block;
+}
+
+.messages-dropdown .dropdown-menu .message-preview .avatar {
+ float: left;
+ margin-right: 15px;
+}
+
+.messages-dropdown .dropdown-menu .message-preview .name {
+ font-weight: bold;
+}
+
+.messages-dropdown .dropdown-menu .message-preview .message {
+ font-size: 12px;
+}
+
+.messages-dropdown .dropdown-menu .message-preview .time {
+ font-size: 12px;
+}
+
+
+/* Nav Announcements */
+
+.announcement-heading {
+ font-size: 50px;
+ margin: 0;
+}
+
+.announcement-text {
+ margin: 0;
+}
+
+/* Table Headers */
+
+table.tablesorter thead {
+ cursor: pointer;
+}
+
+table.tablesorter thead tr th:hover {
+ background-color: #f5f5f5;
+}
+
+/* Flot Chart Containers */
+
+.flot-chart {
+ display: block;
+ height: 400px;
+}
+
+.flot-chart-content {
+ width: 100%;
+ height: 100%;
+}
+
+/* Edit Below to Customize Widths > 768px */
+@media (min-width:768px) {
+
+ /* Wrappers */
+
+ #wrapper {
+ padding-left: 225px;
+ }
+
+ #page-wrapper {
+ padding: 15px 25px;
+ }
+
+ /* Side Nav */
+
+ .side-nav {
+ margin-left: -225px;
+ left: 225px;
+ width: 225px;
+ position: fixed;
+ top: 50px;
+ height: calc(100% - 50px);
+ border-radius: 0;
+ border: none;
+ background-color: #f8f8f8;
+ overflow-y: auto;
+ overflow-x: hidden; /* no left nav scroll bar */
+ }
+
+ /* Bootstrap Default Overrides - Customized Dropdowns for the Side Nav */
+
+ .side-nav>li.dropdown>ul.dropdown-menu {
+ position: relative;
+ min-width: 225px;
+ margin: 0;
+ padding: 0;
+ border: none;
+ border-radius: 0;
+ background-color: transparent;
+ box-shadow: none;
+ -webkit-box-shadow: none;
+ }
+
+ .side-nav>li.dropdown>ul.dropdown-menu>li>a {
+ color: #777777;
+ padding: 15px 15px 15px 25px;
+ }
+
+ .side-nav>li.dropdown>ul.dropdown-menu>li>a:hover,
+ .side-nav>li.dropdown>ul.dropdown-menu>li>a.active,
+ .side-nav>li.dropdown>ul.dropdown-menu>li>a:focus {
+ background-color: #ffffff;
+ }
+
+ .side-nav>li>a {
+ width: 225px;
+ }
+
+ .navbar-default .navbar-nav.side-nav>li>a:hover,
+ .navbar-default .navbar-nav.side-nav>li>a:focus {
+ background-color: #ffffff;
+ }
+
+ /* Nav Messages */
+
+ .messages-dropdown .dropdown-menu {
+ min-width: 300px;
+ }
+
+ .messages-dropdown .dropdown-menu li a {
+ white-space: normal;
+ }
+
+ .navbar-collapse {
+ padding-left: 15px !important;
+ padding-right: 15px !important;
+ }
+
+}
width: 500px;
}
-#selection-form-content > li > a, #selection-form-content > li > input {
- display: block;
- padding: 3px 20px;
- clear: both;
- font-weight: normal;
- line-height: 1.42857;
- color: rgb(51, 51, 51);
- white-space: nowrap;
- border: none;
- background: transparent;
- width: 100%;
- text-align: left;
+#selection-form-content > li > a, #selection-form-content > li > button {
+ margin: 3px 20px;
}
#selection-form-content li table tr {
border-top: 1px solid rgb(221, 221, 221);
}
-#selection-form-content a:hover, #selection-form-content a:focus, #selection-form-content input:hover, #selection-form-content input:focus, #selection-form-content tr:hover {
- text-decoration: none;
- color: rgb(38, 38, 38);
- background-color: whitesmoke;
-}
\ No newline at end of file
+#selection-form-content li table tr:last-child {
+ border-bottom: 1px solid rgb(221, 221, 221);
+}
class ActionsController < ApplicationController
- skip_before_filter :find_object_by_uuid, only: :post
+ @@exposed_actions = {}
+ def self.expose_action method, &block
+ @@exposed_actions[method] = true
+ define_method method, block
+ end
+
+ def model_class
+ ArvadosBase::resource_class_for_uuid(params[:uuid])
+ end
+
+ def post
+ params.keys.collect(&:to_sym).each do |param|
+ if @@exposed_actions[param]
+ return self.send(param)
+ end
+ end
+ redirect_to :back
+ end
+
+ expose_action :copy_selections_into_folder do
+ already_named = Link.
+ filter([['tail_uuid','=',@object.uuid],
+ ['head_uuid','in',params["selection"]]]).
+ collect(&:head_uuid)
+ (params["selection"] - already_named).each do |s|
+ Link.create(tail_uuid: @object.uuid,
+ head_uuid: s,
+ link_class: 'name',
+ name: "#{s} added #{Time.now}")
+ end
+ redirect_to @object
+ end
- def combine_selected_files_into_collection
+ expose_action :combine_selected_files_into_collection do
lst = []
files = []
params["selection"].each do |s|
redirect_to controller: 'collections', action: :show, id: newc.uuid
end
- def post
- if params["combine_selected_files_into_collection"]
- combine_selected_files_into_collection
- else
- redirect_to :back
- end
- end
end
filtered = m.to_ary.reject do |x|
x.api_client_id == 0 or (x.expires_at and x.expires_at < Time.now) rescue false
end
- ArvadosApiClient::patch_paging_vars(filtered, items_available, offset, limit)
+ ArvadosApiClient::patch_paging_vars(filtered, items_available, offset, limit, nil)
@objects = ArvadosResourceList.new(ApiClientAuthorization)
@objects.results= filtered
super
before_filter :check_user_notifications, except: ERROR_ACTIONS
around_filter :using_reader_tokens, only: [:index, :show]
before_filter :find_object_by_uuid, except: [:index] + ERROR_ACTIONS
+ before_filter :check_my_folders, :except => ERROR_ACTIONS
theme :select_theme
begin
offset = 0
end
- @objects ||= model_class.limit(limit).offset(offset).all
+ if params[:filters]
+ filters = params[:filters]
+ if filters.is_a? String
+ filters = Oj.load filters
+ end
+ else
+ filters = []
+ end
+
+ @objects ||= model_class.filter(filters).limit(limit).offset(offset).all
respond_to do |f|
f.json { render json: @objects }
f.html { render }
return render_not_found("object not found")
end
respond_to do |f|
- f.json { render json: @object }
+ f.json { render json: @object.attributes.merge(href: url_for(@object)) }
f.html {
if request.method == 'GET'
render
end
def create
- @object ||= model_class.new params[model_class.to_s.underscore.singularize]
+ @new_resource_attrs ||= params[model_class.to_s.underscore.singularize]
+ @new_resource_attrs ||= {}
+ @new_resource_attrs.reject! { |k,v| k.to_s == 'uuid' }
+ @object ||= model_class.new @new_resource_attrs
@object.save!
-
- respond_to do |f|
- f.json { render json: @object }
- f.html {
- redirect_to(params[:return_to] || @object)
- }
- f.js { render }
- end
+ show
end
def destroy
if params[:id] and params[:id].match /\D/
params[:uuid] = params.delete :id
end
- if params[:uuid].is_a? String
- @object = model_class.find(params[:uuid])
+ if not model_class
+ @object = nil
+ elsif params[:uuid].is_a? String
+ if params[:uuid].empty?
+ @object = nil
+ else
+ @object = model_class.find(params[:uuid])
+ end
else
@object = model_class.where(uuid: params[:uuid]).first
end
}
}
+ def check_my_folders
+ @my_top_level_folders = lambda do
+ @top_level_folders ||= Group.
+ filter([['group_class','=','folder'],
+ ['owner_uuid','=',current_user.uuid]]).
+ sort_by { |x| x.name || '' }
+ end
+ end
+
def check_user_notifications
@notification_count = 0
@notifications = []
end
Collection.where(uuid: @object.uuid).each do |u|
- puts request
@prov_svg = ProvenanceHelper::create_provenance_graph(u.provenance, "provenance_svg",
{:request => request,
:direction => :bottom_up,
--- /dev/null
+class FoldersController < ApplicationController
+ def model_class
+ Group
+ end
+
+ def index_pane_list
+ %w(My_folders Shared_with_me)
+ end
+
+ def remove_item
+ @removed_uuids = []
+ links = []
+ item = ArvadosBase.find params[:item_uuid]
+ if (item.class == Link and
+ item.link_class == 'name' and
+ item.tail_uuid = @object.uuid)
+ # Given uuid is a name link, linking an object to this
+ # folder. First follow the link to find the item we're removing,
+ # then delete the link.
+ links << item
+ item = ArvadosBase.find item.head_uuid
+ else
+ # Given uuid is an object. Delete all names.
+ links += Link.where(tail_uuid: @object.uuid,
+ head_uuid: item.uuid,
+ link_class: 'name')
+ end
+ links.each do |link|
+ @removed_uuids << link.uuid
+ link.destroy
+ end
+ if item.owner_uuid == @object.uuid
+ # Object is owned by this folder. Remove it from the folder by
+ # changing owner to the current user.
+ item.update_attributes owner_uuid: current_user
+ @removed_uuids << item.uuid
+ end
+ end
+
+ def index
+ @my_folders = []
+ @shared_with_me = []
+ @objects = Group.where(group_class: 'folder').order('name')
+ owner_of = {}
+ moretodo = true
+ while moretodo
+ moretodo = false
+ @objects.each do |folder|
+ if !owner_of[folder.uuid]
+ moretodo = true
+ owner_of[folder.uuid] = folder.owner_uuid
+ end
+ if owner_of[folder.owner_uuid]
+ if owner_of[folder.uuid] != owner_of[folder.owner_uuid]
+ owner_of[folder.uuid] = owner_of[folder.owner_uuid]
+ moretodo = true
+ end
+ end
+ end
+ end
+ @objects.each do |folder|
+ if owner_of[folder.uuid] == current_user.uuid
+ @my_folders << folder
+ else
+ @shared_with_me << folder
+ end
+ end
+ end
+
+ def show
+ @objects = @object.contents include_linked: true
+ @share_links = Link.filter([['head_uuid', '=', @object.uuid],
+ ['link_class', '=', 'permission']])
+ @logs = Log.limit(10).filter([['object_uuid', '=', @object.uuid]])
+
+ @objects_and_names = []
+ @objects.each do |object|
+ if !(name_links = @objects.links_for(object, 'name')).empty?
+ name_links.each do |name_link|
+ @objects_and_names << [object, name_link]
+ end
+ else
+ @objects_and_names << [object,
+ Link.new(tail_uuid: @object.uuid,
+ head_uuid: object.uuid,
+ link_class: "name",
+ name: "")]
+ end
+ end
+
+ super
+ end
+
+ def create
+ @new_resource_attrs = (params['folder'] || {}).merge(group_class: 'folder')
+ @new_resource_attrs[:name] ||= 'New folder'
+ super
+ end
+end
class GroupsController < ApplicationController
def index
- @groups = Group.all
+ @groups = Group.filter [['group_class', 'not in', ['folder']]]
@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
end
+
+ def show
+ return redirect_to(folder_path(@object)) if @object.group_class == 'folder'
+ super
+ end
end
attrvalue = attrvalue.to_json if attrvalue.is_a? Hash or attrvalue.is_a? Array
- link_to attrvalue.to_s, '#', {
+ ajax_options = {
+ "data-pk" => {
+ id: object.uuid,
+ key: object.class.to_s.underscore
+ }
+ }
+ if object.uuid
+ ajax_options['data-url'] = url_for(action: "update", id: object.uuid, controller: object.class.to_s.pluralize.underscore)
+ else
+ ajax_options['data-url'] = url_for(action: "create", controller: object.class.to_s.pluralize.underscore)
+ ajax_options['data-pk'][:defaults] = object.attributes
+ end
+ ajax_options['data-pk'] = ajax_options['data-pk'].to_json
+
+ content_tag 'span', attrvalue.to_s, {
"data-emptytext" => "none",
"data-placement" => "bottom",
"data-type" => input_type,
- "data-url" => url_for(action: "update", id: object.uuid, controller: object.class.to_s.pluralize.underscore),
"data-title" => "Update #{attr.gsub '_', ' '}",
"data-name" => attr,
- "data-pk" => "{id: \"#{object.uuid}\", key: \"#{object.class.to_s.underscore}\"}",
+ "data-object-uuid" => object.uuid,
:class => "editable"
- }.merge(htmloptions)
+ }.merge(htmloptions).merge(ajax_options)
end
def render_pipeline_component_attribute(object, attr, subattr, value_info, htmloptions={})
--- /dev/null
+module FoldersHelper
+end
i = -1
object.components.each do |cname, c|
- puts cname, c
i += 1
pj = {index: i, name: cname}
pj[:job] = c[:job].is_a?(Hash) ? c[:job] : {}
resp
end
- def self.patch_paging_vars(ary, items_available, offset, limit)
+ def self.patch_paging_vars(ary, items_available, offset, limit, links=nil)
if items_available
(class << ary; self; end).class_eval { attr_accessor :items_available }
ary.items_available = items_available
(class << ary; self; end).class_eval { attr_accessor :limit }
ary.limit = limit
end
+ if links
+ (class << ary; self; end).class_eval { attr_accessor :links }
+ ary.links = links
+ end
ary
end
def unpack_api_response(j, kind=nil)
if j.is_a? Hash and j[:items].is_a? Array and j[:kind].match(/(_list|List)$/)
ary = j[:items].collect { |x| unpack_api_response x, x[:kind] }
- self.class.patch_paging_vars(ary, j[:items_available], j[:offset], j[:limit])
+ links = ArvadosResourceList.new Link
+ links.results = (j[:links] || []).collect do |x|
+ unpack_api_response x, x[:kind]
+ end
+ self.class.patch_paging_vars(ary, j[:items_available], j[:offset], j[:limit], links)
elsif j.is_a? Hash and (kind || j[:kind])
oclass = self.kind_class(kind || j[:kind])
if oclass
super(*args)
@attribute_sortkey ||= {
'id' => nil,
- 'uuid' => '000',
- 'owner_uuid' => '001',
- 'created_at' => '002',
- 'modified_at' => '003',
- 'modified_by_user_uuid' => '004',
- 'modified_by_client_uuid' => '005',
- 'name' => '050',
- 'tail_uuid' => '100',
- 'head_uuid' => '101',
- 'info' => 'zzz-000',
- 'updated_at' => 'zzz-999'
+ 'name' => '000',
+ 'owner_uuid' => '002',
+ 'event_type' => '100',
+ 'link_class' => '100',
+ 'group_class' => '100',
+ 'tail_uuid' => '101',
+ 'head_uuid' => '102',
+ 'object_uuid' => '102',
+ 'summary' => '104',
+ 'description' => '104',
+ 'properties' => '150',
+ 'info' => '150',
+ 'created_at' => '200',
+ 'modified_at' => '201',
+ 'modified_by_user_uuid' => '202',
+ 'modified_by_client_uuid' => '203',
+ 'uuid' => '999',
}
end
raise 'argument to find() must be a uuid string. Acceptable formats: warehouse locator or string with format xxxxx-xxxxx-xxxxxxxxxxxxxxx'
end
+ if self == ArvadosBase
+ # Determine type from uuid and defer to the appropriate subclass.
+ return resource_class_for_uuid(uuid).find(uuid, opts)
+ end
+
# Only do one lookup on the API side per {class, uuid, workbench
# request} unless {cache: false} is given via opts.
cache_key = "request_#{Thread.current.object_id}_#{self.to_s}_#{uuid}"
}
end
+ def class_for_display
+ self.class.to_s
+ end
+
def self.creatable?
current_user
end
def editable?
(current_user and current_user.is_active and
(current_user.is_admin or
- current_user.uuid == self.owner_uuid))
+ current_user.uuid == self.owner_uuid or
+ new_record?))
end
def attribute_editable?(attr)
elsif "uuid owner_uuid".index(attr.to_s) or current_user.is_admin
current_user.is_admin
else
- current_user.uuid == self.owner_uuid or current_user.uuid == self.uuid
+ current_user.uuid == self.owner_uuid or
+ current_user.uuid == self.uuid or
+ new_record?
end
end
(name if self.respond_to? :name) || uuid
end
+ def content_summary
+ self.class_for_display
+ end
+
def selection_label
friendly_link_name
end
class ArvadosResourceList
include Enumerable
- def initialize(resource_class)
+ def initialize resource_class=nil
@resource_class = resource_class
end
self
end
+ def collect
+ results.collect do |m|
+ yield m
+ end
+ end
+
def first
results.first
end
results.offset if results.respond_to? :offset
end
+ def result_links
+ results.links if results.respond_to? :links
+ end
+
+ # Return links provided with API response that point to the
+ # specified object, and have the specified link_class. If link_class
+ # is false or omitted, return all links pointing to the specified
+ # object.
+ def links_for item_or_uuid, link_class=false
+ return [] if !result_links
+ unless @links_for_uuid
+ @links_for_uuid = {}
+ result_links.each do |link|
+ if link.respond_to? :head_uuid
+ @links_for_uuid[link.head_uuid] ||= []
+ @links_for_uuid[link.head_uuid] << link
+ end
+ end
+ end
+ if item_or_uuid.respond_to? :uuid
+ uuid = item_or_uuid.uuid
+ else
+ uuid = item_or_uuid
+ end
+ (@links_for_uuid[uuid] || []).select do |link|
+ link_class == false or link.link_class == link_class
+ end
+ end
+
+ # Note: this arbitrarily chooses one of (possibly) multiple names.
+ def name_for item_or_uuid
+ links_for(item_or_uuid, 'name').first.andand.name
+ end
+
end
class Collection < ArvadosBase
+ include ApplicationHelper
MD5_EMPTY = 'd41d8cd98f00b204e9800998ecf8427e'
!!locator.to_s.match("^#{MD5_EMPTY}(\\+.*)?\$")
end
+ def content_summary
+ human_readable_bytes_html(total_bytes) + " " + super
+ end
+
def total_bytes
if files
tot = 0
class Group < ArvadosBase
- def self.owned_items
- res = $arvados_api_client.api self, "/#{self.uuid}/owned_items", {}
- $arvados_api_client.unpack_api_response(res)
+ def contents params={}
+ res = $arvados_api_client.api self.class, "/#{self.uuid}/contents", {
+ _method: 'GET'
+ }.merge(params)
+ ret = ArvadosResourceList.new
+ ret.results = $arvados_api_client.unpack_api_response(res)
+ ret
+ end
+
+ def class_for_display
+ group_class == 'folder' ? 'Folder' : super
+ end
+
+ def editable?
+ respond_to?(:writable_by) and
+ writable_by and
+ writable_by.index(current_user.uuid)
end
end
class Log < ArvadosBase
attr_accessor :object
+ def self.creatable?
+ # Technically yes, but not worth offering: it will be empty, and
+ # you won't be able to edit it.
+ false
+ end
end
end
end
- def owned_items
- res = $arvados_api_client.api self.class, "/#{self.uuid}/owned_items"
- $arvados_api_client.unpack_api_response(res)
- end
-
def full_name
(self.first_name || "") + " " + (self.last_name || "")
end
<% pane_list ||= %w(recent) %>
<% panes = Hash[pane_list.map { |pane|
[pane, render(partial: 'show_' + pane.downcase,
- locals: { comparable: comparable })]
+ locals: { comparable: comparable, objects: @objects })]
}.compact] %>
<ul class="nav nav-tabs">
<% if object.editable? %>
- <%= link_to({action: 'destroy', id: object.uuid}, method: :delete, remote: true, data: {confirm: "You are about to delete #{object.class} #{object.uuid}.\n\nAre you sure?"}) do %>
+ <%= link_to({action: 'destroy', id: object.uuid}, method: :delete, remote: true, data: {confirm: "You are about to delete #{object.class_for_display.downcase} '#{object.friendly_link_name}' (#{object.uuid}).\n\nAre you sure?"}) do %>
<i class="glyphicon glyphicon-trash"></i>
<% end %>
<% end %>
--- /dev/null
+<% htmloptions = {class: ''}.merge(htmloptions || {})
+ htmloptions[:class] += " btn-#{size}" rescue nil %>
+<%= link_to_if_arvados_object object, { link_text: raw('Show <i class="fa fa-fw fa-arrow-circle-right"></i>') }, { class: 'btn btn-default ' + htmloptions[:class] } %>
-<% if @objects.empty? %>
+<% if objects.empty? %>
<br/>
<p style="text-align: center">
- No <%= controller.model_class.to_s.pluralize.underscore.gsub '_', ' ' %> to display.
+ No <%= controller.controller_name.humanize.downcase %> to display.
</p>
<% else %>
-<% attr_blacklist = ' created_at modified_at modified_by_user_uuid modified_by_client_uuid updated_at' %>
+<% attr_blacklist = ' created_at modified_at modified_by_user_uuid modified_by_client_uuid updated_at owner_uuid group_class' %>
-<%= render partial: "paging", locals: {results: @objects, object: @object} %>
+<%= render partial: "paging", locals: {results: objects, object: @object} %>
<%= form_tag do |f| %>
<thead>
<tr>
<th></th>
- <% @objects.first.attributes_for_display.each do |attr, attrvalue| %>
+ <th></th>
+ <% objects.first.attributes_for_display.each do |attr, attrvalue| %>
<% next if attr_blacklist.index(" "+attr) %>
<th class="arv-attr-<%= attr %>">
<%= controller.model_class.attribute_info[attr.to_sym].andand[:column_heading] or attr.sub /_uuid/, '' %>
</thead>
<tbody>
- <% @objects.each do |object| %>
+ <% objects.each do |object| %>
<tr data-object-uuid="<%= object.uuid %>">
<td>
<%= render :partial => "selection_checkbox", :locals => {:object => object} %>
</td>
+ <td>
+ <%= render :partial => "show_object_button", :locals => {object: object, size: 'xs'} %>
+ </td>
<% object.attributes_for_display.each do |attr, attrvalue| %>
<% next if attr_blacklist.index(" "+attr) %>
<td class="arv-object-<%= object.class.to_s %> arv-attr-<%= attr %>">
<% if attr == 'uuid' %>
- <%= link_to_if_arvados_object object %>
- <%= link_to_if_arvados_object(object, { link_text: raw('<i class="icon-hand-right"></i>') }) %>
+ <span class="arvados-uuid"><%= attrvalue %></span>
<% else %>
<% if object.attribute_editable? attr %>
<%= render_editable_attribute object, attr %>
<% end %>
-<%= render partial: "paging", locals: {results: @objects, object: @object} %>
+<%= render partial: "paging", locals: {results: objects, object: @object} %>
<% end %>
<% content_for :page_title do %>
-<%= controller.model_class.to_s.pluralize.underscore.capitalize.gsub('_', ' ') %>
+<%= controller.controller_name.humanize.capitalize %>
<% end %>
<% content_for :tab_line_buttons do %>
'data-target' => '#user-setup-modal-window', return_to: request.url} %>
<div id="user-setup-modal-window" class="modal fade" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"></div>
<% else %>
- <%= button_to "Add a new #{controller.model_class.to_s.underscore.gsub '_', ' '}",
- { action: 'create', return_to: request.url },
+ <%= button_to "Add a new #{controller.controller_name.singularize.humanize.downcase}",
+ { action: 'create' },
{ class: 'btn btn-primary pull-right' } %>
<% end %>
--- /dev/null
+<%= render(partial: 'show_recent',
+ locals: { comparable: comparable, objects: @my_folders }) %>
--- /dev/null
+<%= render(partial: 'show_recent',
+ locals: { comparable: comparable, objects: @shared_with_me }) %>
--- /dev/null
+<% @removed_uuids.each do |uuid| %>
+$('[data-object-uuid=<%= uuid %>]').hide('slow', function() {
+ $(this).remove();
+});
+<% end %>
--- /dev/null
+<div class="row row-fill-height">
+ <div class="col-md-6">
+ <div class="panel panel-info">
+ <div class="panel-heading">
+ <h3 class="panel-title">
+ <%= render_editable_attribute @object, 'name', nil, {data: {emptytext: "New folder"}} %>
+ </h3>
+ </div>
+ <div class="panel-body">
+ <img src="/favicon.ico" class="pull-right" alt="" style="opacity: 0.3"/>
+ <%= render_editable_attribute @object, 'description', nil, { 'data-emptytext' => "Created: #{@object.created_at.to_s(:long)}", 'data-toggle' => 'manual', 'id' => "#{@object.uuid}-description" } %>
+ <% if @object.attribute_editable? 'description' %>
+ <div style="margin-top: 1em;">
+ <a href="#" class="btn btn-xs btn-default" data-toggle="x-editable" data-toggle-selector="#<%= @object.uuid %>-description"><i class="fa fa-fw fa-pencil"></i> Edit description</a>
+ </div>
+ <% end %>
+ </div>
+ </div>
+ </div>
+ <div class="col-md-3">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">
+ Activity
+ </h3>
+ </div>
+ <div class="panel-body smaller-text">
+ <!--
+ <input type="text" class="form-control" placeholder="Search"/>
+ -->
+ <div style="height:0.5em;"></div>
+ <% @logs[0..2].each do |log| %>
+ <p>
+ <%= time_ago_in_words(log.event_at) %> ago: <%= log.summary %>
+ <% if log.object_uuid %>
+ <%= link_to_if_arvados_object log.object_uuid, link_text: raw('<i class="fa fa-hand-o-right"></i>') %>
+ <% end %>
+ </p>
+ <% end %>
+ <% if @logs.any? %>
+ <%= link_to raw('Show all activity <i class="fa fa-fw fa-arrow-circle-right"></i>'),
+ logs_path(filters: [['object_uuid','=',@object.uuid]].to_json),
+ class: 'btn btn-xs btn-default' %>
+ <% else %>
+ <p>
+ Created: <%= @object.created_at.to_s(:long) %>
+ </p>
+ <p>
+ Last modified: <%= @object.modified_at.to_s(:long) %> by <%= link_to_if_arvados_object @object.modified_by_user_uuid, friendly_name: true %>
+ </p>
+ <% end %>
+ </div>
+ </div>
+ </div>
+ <div class="col-md-3">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">
+ Sharing and permissions
+ </h3>
+ </div>
+ <div class="panel-body">
+ <!--
+ <input type="text" class="form-control" placeholder="Search"/>
+ -->
+ <div style="height:0.5em;"></div>
+ <p>Owner: <%= link_to_if_arvados_object @object.owner_uuid, friendly_name: true %></p>
+ <% if @share_links.any? %>
+ <p>Shared with:
+ <% @share_links.andand.each do |link| %>
+ <br /><%= link_to_if_arvados_object link.tail_uuid, friendly_name: true %>
+ <% end %>
+ </p>
+ <% end %>
+ </div>
+ </div>
+ </div>
+</div>
+
+<% if @show_cards %>
+<!-- disable cards section until we have bookmarks -->
+<div class="row">
+ <% @objects[0..3].each do |object| %>
+ <div class="card arvados-object">
+ <div class="card-top blue">
+ <a href="#">
+ <img src="/favicon.ico" alt=""/>
+ </a>
+ </div>
+ <div class="card-info">
+ <span class="title"><%= @objects.name_for(object) || object.class_for_display %></span>
+ <div class="desc"><%= object.respond_to?(:description) ? object.description : object.uuid %></div>
+ </div>
+ <div class="card-bottom">
+ <%= render :partial => "show_object_button", :locals => {object: object, htmloptions: {class: 'btn-default btn-block'}} %>
+ </div>
+ </div>
+ <% end %>
+</div>
+<!-- end disabled cards section -->
+<% end %>
+
+<div class="row">
+ <div class="col-md-12">
+ <div class="panel panel-info">
+ <div class="panel-heading">
+ <div class="row">
+ <div class="col-md-6">
+ <h3 class="panel-title" style="vertical-align:middle;">
+ Contents
+ </h3>
+ </div>
+ <div class="col-md-6">
+ <div class="input-group input-group-sm pull-right">
+ <input type="text" class="form-control search-folder-contents" placeholder="Search folder contents"/>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="panel-body">
+ <p>
+ </p>
+ <table class="table table-condensed arv-index">
+ <tbody>
+ <colgroup>
+ <col width="3%" />
+ <col width="8%" />
+ <col width="30%" />
+ <col width="15%" />
+ <col width="15%" />
+ <col width="20%" />
+ <col width="8%" />
+ </colgroup>
+ <% @objects_and_names.each do |object, name_link| %>
+ <tr data-object-uuid="<%= (name_link && name_link.uuid) || object.uuid %>">
+ <td>
+ <%= render :partial => "selection_checkbox", :locals => {object: object} %>
+ </td>
+ <td>
+ <%= render :partial => "show_object_button", :locals => {object: object, size: 'xs'} %>
+ </td>
+ <td>
+ <%= render_editable_attribute name_link, 'name', nil, {data: {emptytext: "Unnamed #{object.class_for_display}"}} %>
+ </td>
+ <td>
+ <%= object.content_summary %>
+ </td>
+ <td title="<%= object.modified_at %>">
+ <span>
+ <%= raw distance_of_time_in_words(object.modified_at, Time.now).sub('about ','~').sub(' ',' ') + ' ago' rescue object.modified_at %>
+ </span>
+ </td>
+ <td class="arvados-uuid">
+ <%= object.uuid %>
+ </td>
+ <td>
+ <% if @object.editable? %>
+ <%= link_to({action: 'remove_item', id: @object.uuid, item_uuid: ((name_link && name_link.uuid) || object.uuid)}, method: :delete, remote: true, data: {confirm: "You are about to remove #{object.class_for_display} #{object.uuid} from this folder.\n\nAre you sure?"}, class: 'btn btn-xs btn-default') do %>
+ Remove <i class="fa fa-fw fa-ban"></i>
+ <% end %>
+ <% end %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+ <thead>
+ <tr>
+ <th>
+ </th>
+ <th>
+ </th>
+ <th>
+ name
+ </th>
+ <th>
+ type
+ </th>
+ <th>
+ modified
+ </th>
+ <th>
+ uuid
+ </th>
+ <th>
+ </th>
+ </tr>
+ </thead>
+ </table>
+ <p></p>
+ </div>
+ </div>
+ </div>
+</div>
padding-top: 70px; /* 70px to make the container go all the way to the bottom of the navbar */
}
- body > div.container-fluid > div.col-sm-9.col-sm-offset-3 {
- overflow: auto;
- }
-
@media (max-width: 979px) { body { padding-top: 0; } }
.navbar .nav li.nav-separator > span.glyphicon.glyphicon-arrow-right {
padding-top: 1.25em;
}
- @media (min-width: 768px) {
- .left-nav {
- position: fixed;
- }
- }
@media (max-width: 767px) {
.breadcrumbs {
display: none;
}
}
</style>
+ <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
</head>
<body>
-
- <div class="navbar navbar-default navbar-fixed-top">
- <div class="container-fluid">
+ <div id="wrapper">
+ <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="navbar-header">
- <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#workbench-navbar.navbar-collapse">
+ <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<a class="navbar-brand" href="/"><%= Rails.configuration.site_name rescue Rails.application.class.parent_name %></a>
</div>
- <div class="collapse navbar-collapse" id="workbench-navbar">
- <ul class="nav navbar-nav navbar-left breadcrumbs">
- <% if current_user %>
- <% if content_for?(:breadcrumbs) %>
- <%= yield(:breadcrumbs) %>
- <% else %>
- <li class="nav-separator"><span class="glyphicon glyphicon-arrow-right"></span></li>
- <li>
- <%= link_to(
- controller.model_class.to_s.pluralize.underscore.gsub('_', ' '),
- url_for({controller: params[:controller]})) %>
- </li>
- <% if params[:action] != 'index' %>
- <li class="nav-separator">
- <span class="glyphicon glyphicon-arrow-right"></span>
+ <div class="collapse navbar-collapse">
+ <% if current_user.andand.is_active %>
+ <ul class="nav navbar-nav side-nav">
+
+ <li class="<%= 'arvados-nav-active' if params[:action] == 'home' %>">
+ <a href="/"><i class="fa fa-lg fa-dashboard fa-fw"></i> Dashboard</a>
</li>
- <li>
- <%= link_to_if_arvados_object @object %>
+
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-lg fa-hand-o-up fa-fw"></i> Help <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> Tutorials and User guide'), "#{Rails.configuration.arvados_docsite}/user", target: "_blank" %></li>
+ <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> API Reference'), "#{Rails.configuration.arvados_docsite}/api", target: "_blank" %></li>
+ <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> SDK Reference'), "#{Rails.configuration.arvados_docsite}/sdk", target: "_blank" %></li>
+ </ul>
</li>
- <li style="padding: 14px 0 14px">
- <%= form_tag do |f| %>
- <%= render :partial => "selection_checkbox", :locals => {:object => @object} %>
- <% end %>
+
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-lg fa-folder-o fa-fw"></i> Folders <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><%= link_to raw('<i class="fa fa-plus fa-fw"></i> Create new folder'), folders_path, method: :post %></li>
+ <% @my_top_level_folders.call[0..7].each do |folder| %>
+ <li><%= link_to raw('<i class="fa fa-folder-open fa-fw"></i> ') + folder.name, folder_path(folder) %></li>
+ <% end %>
+ <li><a href="/folders">
+ <i class="fa fa-ellipsis-h fa-fw"></i> Show all folders
+ </a></li>
+ </ul>
</li>
- <% end %>
- <% end %>
- <% end %>
- </ul>
-
- <ul class="nav navbar-nav navbar-right">
-
- <li>
- <a><i class="rotating loading glyphicon glyphicon-refresh"></i></a>
- </li>
-
- <% if current_user %>
- <!-- XXX placeholder for this when search is implemented
- <li>
- <form class="navbar-form" role="search">
- <div class="input-group" style="width: 220px">
- <input type="text" class="form-control" placeholder="search">
- <span class="input-group-addon"><span class="glyphicon glyphicon-search"></span></span>
- </div>
- </form>
- </li>
- -->
-
- <li class="dropdown notification-menu">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="collections-menu">
- <span class="glyphicon glyphicon-paperclip"></span>
- <span class="badge" id="persistent-selection-count"></span>
- <span class="caret"></span>
- </a>
- <ul class="dropdown-menu" role="menu" id="persistent-selection-list">
- <%= form_tag '/actions' do %>
- <div id="selection-form-content"></div>
- <% end %>
- </ul>
- </li>
-
- <% if current_user.is_active %>
- <li class="dropdown notification-menu">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="notifications-menu">
- <span class="glyphicon glyphicon-envelope"></span>
- <span class="badge badge-alert notification-count"><%= @notification_count %></span>
- <span class="caret"></span>
- </a>
- <ul class="dropdown-menu" role="menu">
- <% if (@notifications || []).length > 0 %>
- <% @notifications.each_with_index do |n, i| %>
- <% if i > 0 %><li class="divider"></li><% end %>
- <li class="notification"><%= n.call(self) %></li>
- <% end %>
- <% else %>
- <li class="notification empty">No notifications.</li>
+ <li><a href="/collections">
+ <i class="fa fa-lg fa-briefcase fa-fw"></i> Collections (data files)
+ </a></li>
+ <li><a href="/pipeline_instances">
+ <i class="fa fa-lg fa-tasks fa-fw"></i> Pipeline instances
+ </a></li>
+ <li><a href="/pipeline_templates">
+ <i class="fa fa-lg fa-gears fa-fw"></i> Pipeline templates
+ </a></li>
+ <li> </li>
+ <li><a href="/repositories">
+ <i class="fa fa-lg fa-code-fork fa-fw"></i> Repositories
+ </a></li>
+ <li><a href="/virtual_machines">
+ <i class="fa fa-lg fa-terminal fa-fw"></i> Virtual machines
+ </a></li>
+ <li><a href="/humans">
+ <i class="fa fa-lg fa-male fa-fw"></i> Humans
+ </a></li>
+ <li><a href="/specimens">
+ <i class="fa fa-lg fa-flask fa-fw"></i> Specimens
+ </a></li>
+ <li><a href="/traits">
+ <i class="fa fa-lg fa-clipboard fa-fw"></i> Traits
+ </a></li>
+ <li><a href="/links">
+ <i class="fa fa-lg fa-arrows-h fa-fw"></i> Links
+ </a></li>
+ <% if current_user.andand.is_admin %>
+ <li><a href="/users">
+ <i class="fa fa-lg fa-user fa-fw"></i> Users
+ </a></li>
<% end %>
+ <li><a href="/groups">
+ <i class="fa fa-lg fa-users fa-fw"></i> Groups
+ </a></li>
+ <li><a href="/nodes">
+ <i class="fa fa-lg fa-cogs fa-fw"></i> Compute nodes
+ </a></li>
+ <li><a href="/keep_disks">
+ <i class="fa fa-lg fa-hdd-o fa-fw"></i> Keep disks
+ </a></li>
</ul>
- </li>
<% end %>
- <li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="user-menu">
- <span class="glyphicon glyphicon-user"></span><span class="caret"></span>
- </a>
- <ul class="dropdown-menu" role="menu">
- <li role="presentation" class="dropdown-header"><%= current_user.email %></li>
- <% if current_user.is_active %>
- <li role="presentation" class="divider"></li>
- <li role="presentation"><a href="/authorized_keys" role="menuitem">Manage ssh keys</a></li>
- <li role="presentation"><a href="/api_client_authorizations" role="menuitem">Manage API tokens</a></li>
- <li role="presentation" class="divider"></li>
+ <ul class="nav navbar-nav navbar-left breadcrumbs">
+ <% if current_user %>
+ <% if content_for?(:breadcrumbs) %>
+ <%= yield(:breadcrumbs) %>
+ <% else %>
+ <li class="nav-separator"><span class="glyphicon glyphicon-arrow-right"></span></li>
+ <li>
+ <%= link_to(
+ controller.controller_name.humanize.downcase,
+ url_for({controller: params[:controller]})) %>
+ </li>
+ <% if params[:action] != 'index' %>
+ <li class="nav-separator">
+ <span class="glyphicon glyphicon-arrow-right"></span>
+ </li>
+ <li>
+ <%= link_to_if_arvados_object @object, {friendly_name: true}, {data: {object_uuid: @object.andand.uuid, name: 'name'}} %>
+ </li>
+ <li style="padding: 14px 0 14px">
+ <%= form_tag do |f| %>
+ <%= render :partial => "selection_checkbox", :locals => {:object => @object} %>
+ <% end %>
+ </li>
+ <% end %>
<% end %>
- <li role="presentation"><a href="<%= logout_path %>" role="menuitem">Log out</a></li>
- </ul>
- </li>
- <% else -%>
- <li><a href="<%= $arvados_api_client.arvados_login_url(return_to: root_url) %>">Log in</a></li>
- <% end -%>
- </ul>
- </div><!-- /.navbar-collapse -->
- </div><!-- /.container-fluid -->
- </div>
+ <% end %>
+ </ul>
- <div class="container-fluid">
- <div class="col-sm-9 col-sm-offset-3">
- <div id="content" class="body-content">
- <%= yield %>
- </div>
- </div>
- <div class="col-sm-3 left-nav">
- <div class="arvados-nav-container">
- <% if current_user.andand.is_active %>
- <div class="well">
- <ul class="arvados-nav">
- <li class="<%= 'arvados-nav-active' if params[:action] == 'home' %>">
- <a href="/">Dashboard</a>
+ <ul class="nav navbar-nav navbar-right">
+
+ <li>
+ <a><i class="rotating loading glyphicon glyphicon-refresh"></i></a>
</li>
- <% [['Data', [['collections', 'Collections (data files)'],
- ['humans'],
- ['traits'],
- ['specimens'],
- ['links']]],
- ['Activity', [['pipeline_instances', 'Recent pipeline instances'],
- ['jobs', 'Recent jobs']]],
- ['Compute', [['pipeline_templates'],
- ['repositories', 'Code repositories'],
- ['virtual_machines']]],
- ['System', [['users'],
- ['groups'],
- ['nodes', 'Compute nodes'],
- ['keep_disks']]]].each do |j| %>
- <li><%= j[0] %>
- <ul>
- <% j[1].each do |k| %>
- <% unless k[0] == 'users' and !current_user.andand.is_admin %>
- <li class="<%= 'arvados-nav-active' if (params[:controller] == k[0] && params[:action] != 'home') %>">
- <a href="/<%= k[0] %>">
- <%= if k[1] then k[1] else k[0].capitalize.gsub('_', ' ') end %>
- </a>
- </li>
+ <% if current_user %>
+ <!-- XXX placeholder for this when search is implemented
+ <li>
+ <form class="navbar-form" role="search">
+ <div class="input-group" style="width: 220px">
+ <input type="text" class="form-control" placeholder="search">
+ <span class="input-group-addon"><span class="glyphicon glyphicon-search"></span></span>
+ </div>
+ </form>
+ </li>
+ -->
+
+ <li class="dropdown notification-menu">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="collections-menu">
+ <span class="glyphicon glyphicon-paperclip"></span>
+ <span class="badge" id="persistent-selection-count"></span>
+ <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu" role="menu" id="persistent-selection-list">
+ <%= form_tag '/actions' do %>
+ <%= hidden_field_tag 'uuid', @object.andand.uuid %>
+ <div id="selection-form-content"></div>
<% end %>
+ </ul>
+ </li>
+
+ <% if current_user.is_active %>
+ <li class="dropdown notification-menu">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="notifications-menu">
+ <span class="glyphicon glyphicon-envelope"></span>
+ <span class="badge badge-alert notification-count"><%= @notification_count %></span>
+ <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu" role="menu">
+ <% if (@notifications || []).length > 0 %>
+ <% @notifications.each_with_index do |n, i| %>
+ <% if i > 0 %><li class="divider"></li><% end %>
+ <li class="notification"><%= n.call(self) %></li>
+ <% end %>
+ <% else %>
+ <li class="notification empty">No notifications.</li>
<% end %>
- </ul>
- </li>
+ </ul>
+ </li>
<% end %>
- <li>Help
- <ul>
- <li><%= link_to 'Tutorials and User guide', "#{Rails.configuration.arvados_docsite}/user", target: "_blank" %></li>
- <li><%= link_to 'API Reference', "#{Rails.configuration.arvados_docsite}/api", target: "_blank" %></li>
- <li><%= link_to 'SDK Reference', "#{Rails.configuration.arvados_docsite}/sdk", target: "_blank" %></li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="user-menu">
+ <span class="glyphicon glyphicon-user"></span><span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu" role="menu">
+ <li role="presentation" class="dropdown-header"><%= current_user.email %></li>
+ <% if current_user.is_active %>
+ <li role="presentation" class="divider"></li>
+ <li role="presentation"><a href="/authorized_keys" role="menuitem"><i class="fa fa-key fa-fw"></i> Manage ssh keys</a></li>
+ <li role="presentation"><a href="/api_client_authorizations" role="menuitem"><i class="fa fa-ticket fa-fw"></i> Manage API tokens</a></li>
+ <li role="presentation" class="divider"></li>
+ <% end %>
+ <li role="presentation"><a href="<%= logout_path %>" role="menuitem"><i class="fa fa-sign-out fa-fw"></i> Log out</a></li>
</ul>
</li>
+ <% else %>
+ <li><a href="<%= $arvados_api_client.arvados_login_url(return_to: root_url) %>">Log in</a></li>
+ <% end %>
</ul>
- </div>
- <% end %>
- </div>
- </div>
+ </div><!-- /.navbar-collapse -->
+ </nav>
+
+ <div id="page-wrapper">
+ <%= yield %>
+ </div>
</div>
+</div>
+
<%= yield :footer_html %>
<%= piwik_tracking_tag %>
<%= javascript_tag do %>
+++ /dev/null
-<%= render :partial => 'application/arvados_object' %>
post 'set_persistent', on: :member
end
get '/collections/:uuid/*file' => 'collections#show_file', :format => false
+ resources :folders do
+ match 'remove/:item_uuid', on: :member, via: :delete, action: :remove_item
+ end
post 'actions' => 'actions#post'
get 'websockets' => 'websocket#index'
--- /dev/null
+require 'test_helper'
+
+class FoldersControllerTest < ActionController::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
--- /dev/null
+require 'integration_helper'
+require 'selenium-webdriver'
+require 'headless'
+
+class FoldersTest < ActionDispatch::IntegrationTest
+
+ test 'Find a folder and edit its description' do
+ Capybara.current_driver = Capybara.javascript_driver
+ visit page_with_token 'active', '/'
+ find('nav a', text: 'Folders').click
+ find('.side-nav a,button', text: 'A Folder').
+ click
+ within('.panel', text: api_fixture('groups')['afolder']['name']) do
+ find('span', text: api_fixture('groups')['afolder']['name']).click
+ find('.glyphicon-ok').click
+ find('.btn', text: 'Edit description').click
+ find('.editable-input textarea').set('I just edited this.')
+ find('.editable-submit').click
+ wait_for_ajax
+ end
+ visit current_path
+ assert(find?('.panel', text: 'I just edited this.'),
+ "Description update did not survive page refresh")
+ end
+
+ test 'Add a new name, then edit it, without creating a duplicate' do
+ Capybara.current_driver = Capybara.javascript_driver
+ folder_uuid = api_fixture('groups')['afolder']['uuid']
+ specimen_uuid = api_fixture('specimens')['owned_by_afolder_with_no_name_link']['uuid']
+ visit page_with_token 'active', '/folders/' + folder_uuid
+ within('.panel tr', text: specimen_uuid) do
+ find(".editable[data-name='name']").click
+ find('.editable-input input').set('Now I have a name.')
+ find('.glyphicon-ok').click
+ find('.editable', text: 'Now I have a name.').click
+ find('.editable-input input').set('Now I have a new name.')
+ find('.glyphicon-ok').click
+ wait_for_ajax
+ find('.editable', text: 'Now I have a new name.')
+ end
+ visit current_path
+ within '.panel', text: 'Contents' do
+ find '.editable', text: 'Now I have a new name.'
+ page.assert_no_selector '.editable', text: 'Now I have a name.'
+ end
+ end
+
+end
visit page_with_token('active_trustedclient', '/')
assert_visit_success
click_link 'user-menu'
- urls = [all_links_in('.arvados-nav'),
+ urls = [all_links_in('nav'),
all_links_in('.navbar', /^Manage /)].flatten
seen_urls = ['/']
while not (url = urls.shift).nil?
assert (text.include? 'true false'), 'Expected is_active'
end
- click_link 'zzzzz-tpzed-xurymjxw79nv3jz'
+ find('tr', text: 'zzzzz-tpzed-xurymjxw79nv3jz').
+ find('a,button', text: 'Show').
+ click
assert page.has_text? 'Attributes'
assert page.has_text? 'Metadata'
assert page.has_text? 'Admin'
# go to the Attributes tab
click_link 'Attributes'
assert page.has_text? 'modified_by_user_uuid'
- page.within(:xpath, '//a[@data-name="is_active"]') do
+ page.within(:xpath, '//span[@data-name="is_active"]') do
assert_equal "true", text, "Expected user's is_active to be true"
end
- page.within(:xpath, '//a[@data-name="is_admin"]') do
+ page.within(:xpath, '//span[@data-name="is_admin"]') do
assert_equal "false", text, "Expected user's is_admin to be false"
end
click_button "Submit"
end
- sleep(0.1)
-
- # verify that the new user showed up in the users page
- assert page.has_text? 'foo@example.com'
-
- new_user_uuid = nil
- all("tr").each do |elem|
- if elem.text.include? 'foo@example.com'
- new_user_uuid = elem.text.split[0]
- break
- end
- end
+ visit '/users'
+ # verify that the new user showed up in the users page and find
+ # the new user's UUID
+ new_user_uuid =
+ find('tr[data-object-uuid]', text: 'foo@example.com').
+ find('td', text: '-tpzed-').
+ text
assert new_user_uuid, "Expected new user uuid not found"
# go to the new user's page
- click_link new_user_uuid
+ find('tr', text: new_user_uuid).
+ find('a,button', text: 'Show').
+ click
assert page.has_text? 'modified_by_user_uuid'
- page.within(:xpath, '//a[@data-name="is_active"]') do
+ page.within(:xpath, '//span[@data-name="is_active"]') do
assert_equal "false", text, "Expected new user's is_active to be false"
end
click_link 'Users'
- assert page.has_link? 'zzzzz-tpzed-xurymjxw79nv3jz'
-
# click on active user
- click_link 'zzzzz-tpzed-xurymjxw79nv3jz'
+ find('tr', text: 'zzzzz-tpzed-xurymjxw79nv3jz').
+ find('a,button', text: 'Show').
+ click
# Setup user
click_link 'Admin'
click_link 'Users'
- assert page.has_link? 'zzzzz-tpzed-xurymjxw79nv3jz'
-
# click on active user
- click_link 'zzzzz-tpzed-xurymjxw79nv3jz'
+ find('tr', text: 'zzzzz-tpzed-xurymjxw79nv3jz').
+ find('a,button', text: 'Show').
+ click
# Verify that is_active is set
- click_link 'Attributes'
+ find('a,button', text: 'Attributes').click
assert page.has_text? 'modified_by_user_uuid'
- page.within(:xpath, '//a[@data-name="is_active"]') do
+ page.within(:xpath, '//span[@data-name="is_active"]') do
assert_equal "true", text, "Expected user's is_active to be true"
end
# Should now be back in the Attributes tab for the user
page.driver.browser.switch_to.alert.accept
assert page.has_text? 'modified_by_user_uuid'
- page.within(:xpath, '//a[@data-name="is_active"]') do
+ page.within(:xpath, '//span[@data-name="is_active"]') do
assert_equal "false", text, "Expected user's is_active to be false after unsetup"
end
click_link 'Virtual machines'
assert page.has_text? 'testvm.shell'
click_on 'Add a new virtual machine'
- assert page.has_text? 'none'
- click_link 'none'
+ find('tr', text: 'hostname').
+ find('span', text: 'none').click
assert page.has_text? 'Update hostname'
fill_in 'editable-text', with: 'testname'
click_button 'editable-submit'
require 'uri'
require 'yaml'
+module WaitForAjax
+ Capybara.default_wait_time = 5
+ def wait_for_ajax
+ Timeout.timeout(Capybara.default_wait_time) do
+ loop until finished_all_ajax_requests?
+ end
+ end
+
+ def finished_all_ajax_requests?
+ page.evaluate_script('jQuery.active').zero?
+ end
+end
+
class ActionDispatch::IntegrationTest
# Make the Capybara DSL available in all integration tests
include Capybara::DSL
include ApiFixtureLoader
+ include WaitForAjax
@@API_AUTHS = self.api_fixture('api_client_authorizations')
q_string = URI.encode_www_form('api_token' => api_token)
"#{path}#{sep}#{q_string}"
end
+
+ # Find a page element, but return false instead of raising an
+ # exception if not found. Use this with assertions to explain that
+ # the error signifies a failed test rather than an unexpected error
+ # during a testing procedure.
+ def find? *args
+ begin
+ find *args
+ rescue Capybara::ElementNotFound
+ false
+ end
+ end
end
ENV["RAILS_ENV"] = "test"
+unless ENV["NO_COVERAGE_TEST"]
+ begin
+ require 'simplecov'
+ require 'simplecov-rcov'
+ class SimpleCov::Formatter::MergedFormatter
+ def format(result)
+ SimpleCov::Formatter::HTMLFormatter.new.format(result)
+ SimpleCov::Formatter::RcovFormatter.new.format(result)
+ end
+ end
+ SimpleCov.formatter = SimpleCov::Formatter::MergedFormatter
+ SimpleCov.start do
+ add_filter '/test/'
+ add_filter 'initializers/secret_token'
+ end
+ rescue Exception => e
+ $stderr.puts "SimpleCov unavailable (#{e}). Proceeding without."
+ end
+end
+
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
def _run(args=[])
Capybara.javascript_driver = :poltergeist
server_pid = Dir.chdir($ARV_API_SERVER_DIR) do |apidir|
+ ENV["NO_COVERAGE_TEST"] = "1"
_system('bundle', 'exec', 'rake', 'db:test:load')
_system('bundle', 'exec', 'rake', 'db:fixtures:load')
_system('bundle', 'exec', 'rails', 'server', '-d')
--- /dev/null
+require 'test_helper'
+
+class ResourceListTest < ActiveSupport::TestCase
+
+ test 'links_for on a resource list that does not return links' do
+ use_token :active
+ results = Specimen.all
+ assert_equal [], results.links_for(api_fixture('users')['active']['uuid'])
+ end
+
+ test 'links_for on non-empty resource list' do
+ use_token :active
+ results = Group.find(api_fixture('groups')['afolder']['uuid']).contents(include_linked: true)
+ assert_equal [], results.links_for(api_fixture('users')['active']['uuid'])
+ assert_equal [], results.links_for(api_fixture('jobs')['running_cancelled']['uuid'])
+ assert_equal [], results.links_for(api_fixture('jobs')['running']['uuid'], 'bogus-link-class')
+ assert_equal true, results.links_for(api_fixture('jobs')['running']['uuid'], 'name').any?
+ end
+
+ test 'links_for returns all link classes (simulated results)' do
+ folder_uuid = api_fixture('groups')['afolder']['uuid']
+ specimen_uuid = api_fixture('specimens')['in_afolder']['uuid']
+ api_response = {
+ kind: 'arvados#specimenList',
+ links: [{kind: 'arvados#link',
+ uuid: 'zzzzz-o0j2j-asdfasdfasdfas0',
+ tail_uuid: folder_uuid,
+ head_uuid: specimen_uuid,
+ link_class: 'name',
+ name: 'Alice'},
+ {kind: 'arvados#link',
+ uuid: 'zzzzz-o0j2j-asdfasdfasdfas1',
+ tail_uuid: folder_uuid,
+ head_uuid: specimen_uuid,
+ link_class: 'foo',
+ name: 'Bob'},
+ {kind: 'arvados#link',
+ uuid: 'zzzzz-o0j2j-asdfasdfasdfas2',
+ tail_uuid: folder_uuid,
+ head_uuid: specimen_uuid,
+ link_class: nil,
+ name: 'Clydesdale'}],
+ items: [{kind: 'arvados#specimen',
+ uuid: specimen_uuid}]
+ }
+ arl = ArvadosResourceList.new
+ arl.results = ArvadosApiClient.new.unpack_api_response(api_response)
+ assert_equal(['name', 'foo', nil],
+ (arl.
+ links_for(specimen_uuid).
+ collect { |x| x.link_class }),
+ "Expected links_for to return all link_classes")
+ end
+
+end
require 'test_helper'
-class ProjectTest < ActiveSupport::TestCase
- # test "the truth" do
- # assert true
- # end
+class GroupTest < ActiveSupport::TestCase
+ test "get contents with names" do
+ use_token :active
+ oi = Group.
+ find(api_fixture('groups')['asubfolder']['uuid']).
+ contents(include_linked: true)
+ assert_operator(0, :<, oi.count,
+ "Expected to find some items belonging to :active user")
+ assert_operator(0, :<, oi.items_available,
+ "Expected contents response to have items_available > 0")
+ assert_operator(0, :<, oi.result_links.count,
+ "Expected to receive name links with contents response")
+ oi_uuids = oi.collect { |i| i['uuid'] }
+
+ expect_uuid = api_fixture('specimens')['in_asubfolder']['uuid']
+ assert_includes(oi_uuids, expect_uuid,
+ "Expected '#{expect_uuid}' in asubfolder's contents")
+
+ expect_uuid = api_fixture('specimens')['in_afolder_linked_from_asubfolder']['uuid']
+ expect_name = api_fixture('links')['specimen_is_in_two_folders']['name']
+ assert_includes(oi_uuids, expect_uuid,
+ "Expected '#{expect_uuid}' in asubfolder's contents")
+ assert_equal(expect_name, oi.name_for(expect_uuid),
+ "Expected name_for '#{expect_uuid}' to be '#{expect_name}'")
+ end
end
--- /dev/null
+require 'test_helper'
+
+class FoldersHelperTest < ActionView::TestCase
+end
require 'test_helper'
class UserTest < ActiveSupport::TestCase
- test "get owned_items" do
- use_token :active
- oi = User.find(api_fixture('users')['active']['uuid']).owned_items
- assert_operator(0, :<, oi.count,
- "Expected to find some items belonging to :active user")
- assert_operator(0, :<, oi.items_available,
- "Expected owned_items response to have items_available > 0")
- oi_uuids = oi.collect { |i| i['uuid'] }
- expect = api_fixture('specimens')['owned_by_active_user']['uuid']
- assert_includes(oi_uuids, expect,
- "Expected active user's owned_items to include #{expect}")
- end
end
These resources govern the Arvados infrastructure itself: Git repositories, Keep disks, active nodes, etc.
-* "CommitAncestor":schema/CommitAncestor.html
-* "Commit":schema/Commit.html
* "KeepDisk":schema/KeepDisk.html
* "Node":schema/Node.html
* "Repository":schema/Repository.html
Required arguments are displayed in %{background:#ccffcc}green%.
+h2. contents
+
+Retrieve a list of items which are associated with the given group by ownership (i.e., the group owns the item) or a "name" link (i.e., a "name" link referencing the item).
+
+Arguments:
+
+table(table table-bordered table-condensed).
+|_. Argument |_. Type |_. Description |_. Location |_. Example |
+{background:#ccffcc}.|uuid|string|The UUID of the group in question.|path||
+|include_linked|boolean|If false, results will only include items whose @owner_uuid@ attribute is the specified group. If true, results will additionally include items for which a "name" link exists.|path|{white-space:nowrap}. @false@ (default)
+@true@|
+
+If @include_linked@ is @true@, the @"links"@ field in the response will contain the "name" links referencing the objects in the @"items"@ field.
+
h2. create
Create a new Group.
|order|string|Order in which to return matching groups.|query||
|filters|array|Conditions for filtering groups.|query||
-h2. owned_items
-
-Retrieve a list of items which are owned by the given group.
-
-Arguments:
-
-table(table table-bordered table-condensed).
-|_. Argument |_. Type |_. Description |_. Location |_. Example |
-{background:#ccffcc}.|uuid|string|The UUID of the group in question.|path||
-|include_linked|boolean|If true, results will also include items on which the given group has _can_manage_ permission, even if they are owned by different users/groups.|path|{white-space:nowrap}. @false@ (default)
-@true@|
-
h2. show
show groups
|order|string|Order in which to return matching users.|query||
|filters|array|Conditions for filtering users.|query||
-h2. owned_items
-
-Retrieve a list of items which are owned by the given user.
-
-Arguments:
-
-table(table table-bordered table-condensed).
-|_. Argument |_. Type |_. Description |_. Location |_. Example |
-{background:#ccffcc}.|uuid|string|The UUID of the user in question.|path||
-|include_linked|boolean|If true, results will also include items on which the given user has _can_manage_ permission, even if they are owned by different users/groups.|path|{white-space:nowrap}. @false@ (default)
-@true@|
-
h2. show
show users
h3. Side effects of creating a Collection
-Referenced data can be protected from garbage collection. See the section about "resources" links on the "Links":Links.html page.
+Referenced data can be protected from garbage collection. See the section about "resources" links on the "Links":Link.html page.
Data can be shared with other users via the Arvados permission model.
...
-
-
-h3. Python
-
-{% include 'notebox_begin' %}
-The Python package includes the Python API client library module and the CLI utilities @arv-get@ and @arv-put@.
-{% include 'notebox_end' %}
-
-Get the arvados source tree.
-
-notextile. <pre><code>$ <span class="userinput">git clone https://github.com/curoverse/arvados.git</span></code></pre>
-
-Build and install the python package.
-
-<notextile>
-<pre><code>$ <span class="userinput">cd arvados/sdk/python</span>
-$ <span class="userinput">sudo python setup.py install</span>
-</code></pre>
-</notextile>
-
-Alternatively, build the package (without sudo) using @python setup.py bdist_egg@ and copy the @.egg@ package from @dist/@ to the target system.
-
-h3. Ruby
-
-{% include 'notebox_begin' %}
-The arvados package includes the Ruby client library module. The arvados-cli package includes the CLI utilities @arv@, @arv-run-pipeline-instance@, and @crunch-job@.
-{% include 'notebox_end' %}
-
-notextile. <pre><code>$ <span class="userinput">sudo gem install arvados arvados-cli</span></code></pre>
-
-h3. Perl
-
-{% include 'notebox_begin' %}
-The Perl client library includes the @Arvados.pm@ module and submodules.
-{% include 'notebox_end' %}
-
-<notextile>
-<pre><code>$ <span class="userinput">cd arvados/sdk/perl</span>
-$ <span class="userinput">perl Makefile.PL</span>
-$ <span class="userinput">sudo make install</span>
-</code></pre>
-</notextile>
+The "SDK Reference":{{site.baseurl}}/sdk/index.html page has installation instructions for each of the SDKs.
To use the Python SDK elsewhere, you can either install the Python SDK via PyPI or build and install the package using the arvados source tree.
+{% include 'notebox_begin' %}
+The Python SDK requires Python 2.7
+{% include 'notebox_end' %}
+
h4. Option 1: install with PyPI
<notextile>
<notextile>
<pre>
$ <code class="userinput">git clone https://github.com/curoverse/arvados.git</code>
-$ <code class="userinput">cd arvados/sdk/cli</code>
+$ <code class="userinput">cd arvados/sdk/ruby</code>
$ <code class="userinput">gem build arvados.gemspec</code>
$ <code class="userinput">sudo gem install arvados-*.gem</code>
</pre>
import arvados
import pprint
import arvados.events
+import re
+import apiclient
+import json
from time import time
from llfuse import FUSEError
-class Directory(object):
- '''Generic directory object, backed by a dict.
- Consists of a set of entries with the key representing the filename
- and the value referencing a File or Directory object.
- '''
-
- def __init__(self, parent_inode):
- '''parent_inode is the integer inode number'''
- self.inode = None
- if not isinstance(parent_inode, int):
- raise Exception("parent_inode should be an int")
- self.parent_inode = parent_inode
- self._entries = {}
+class FreshBase(object):
+ '''Base class for maintaining fresh/stale state to determine when to update.'''
+ def __init__(self):
self._stale = True
self._poll = False
self._last_update = time()
self._poll_time = 60
- # Overriden by subclasses to implement logic to update the entries dict
- # when the directory is stale
- def update(self):
- pass
-
- # Mark the entries dict as stale
+ # Mark the value as stale
def invalidate(self):
self._stale = True
self._stale = False
self._last_update = time()
+
+class File(FreshBase):
+ '''Base for file objects.'''
+
+ def __init__(self, parent_inode):
+ super(File, self).__init__()
+ self.inode = None
+ self.parent_inode = parent_inode
+
+ def size(self):
+ return 0
+
+ def readfrom(self, off, size):
+ return ''
+
+
+class StreamReaderFile(File):
+ '''Wraps a StreamFileReader as a file.'''
+
+ def __init__(self, parent_inode, reader):
+ super(StreamReaderFile, self).__init__(parent_inode)
+ self.reader = reader
+
+ def size(self):
+ return self.reader.size()
+
+ def readfrom(self, off, size):
+ return self.reader.readfrom(off, size)
+
+ def stale(self):
+ return False
+
+
+class ObjectFile(File):
+ '''Wraps a dict as a serialized json object.'''
+
+ def __init__(self, parent_inode, contents):
+ super(ObjectFile, self).__init__(parent_inode)
+ self.contentsdict = contents
+ self.uuid = self.contentsdict['uuid']
+ self.contents = json.dumps(self.contentsdict, indent=4, sort_keys=True)
+
+ def size(self):
+ return len(self.contents)
+
+ def readfrom(self, off, size):
+ return self.contents[off:(off+size)]
+
+
+class Directory(FreshBase):
+ '''Generic directory object, backed by a dict.
+ Consists of a set of entries with the key representing the filename
+ and the value referencing a File or Directory object.
+ '''
+
+ def __init__(self, parent_inode):
+ super(Directory, self).__init__()
+
+ '''parent_inode is the integer inode number'''
+ self.inode = None
+ if not isinstance(parent_inode, int):
+ raise Exception("parent_inode should be an int")
+ self.parent_inode = parent_inode
+ self._entries = {}
+
+ # Overriden by subclasses to implement logic to update the entries dict
+ # when the directory is stale
+ def update(self):
+ pass
+
# Only used when computing the size of the disk footprint of the directory
# (stub)
def size(self):
return 0
- def __getitem__(self, item):
+ def checkupdate(self):
if self.stale():
- self.update()
+ try:
+ self.update()
+ except apiclient.errors.HttpError as e:
+ print e
+
+ def __getitem__(self, item):
+ self.checkupdate()
return self._entries[item]
def items(self):
- if self.stale():
- self.update()
+ self.checkupdate()
return self._entries.items()
def __iter__(self):
- if self.stale():
- self.update()
+ self.checkupdate()
return self._entries.iterkeys()
def __contains__(self, k):
- if self.stale():
- self.update()
+ self.checkupdate()
return k in self._entries
+ def merge(self, items, fn, same, new_entry):
+ '''Helper method for updating the contents of the directory.
+
+ items: array with new directory contents
+
+ fn: function to take an entry in 'items' and return the desired file or
+ directory name
+
+ same: function to compare an existing entry with an entry in the items
+ list to determine whether to keep the existing entry.
+
+ new_entry: function to create a new directory entry from array entry.
+ '''
+
+ oldentries = self._entries
+ self._entries = {}
+ for i in items:
+ n = fn(i)
+ if n in oldentries and same(oldentries[n], i):
+ self._entries[n] = oldentries[n]
+ del oldentries[n]
+ else:
+ self._entries[n] = self.inodes.add_entry(new_entry(i))
+ for n in oldentries:
+ llfuse.invalidate_entry(self.inode, str(n))
+ self.inodes.del_entry(oldentries[n])
+ self.fresh()
+
class CollectionDirectory(Directory):
'''Represents the root of a directory tree holding a collection.'''
self.inodes = inodes
self.collection_locator = collection_locator
+ def same(self, i):
+ return i['uuid'] == self.collection_locator
+
def update(self):
collection = arvados.CollectionReader(arvados.Keep.get(self.collection_locator))
for s in collection.all_streams():
cwd._entries[part] = self.inodes.add_entry(Directory(cwd.inode))
cwd = cwd._entries[part]
for k, v in s.files().items():
- cwd._entries[k] = self.inodes.add_entry(File(cwd.inode, v))
+ cwd._entries[k] = self.inodes.add_entry(StreamReaderFile(cwd.inode, v))
self.fresh()
+
class MagicDirectory(Directory):
'''A special directory that logically contains the set of all extant keep
locators. When a file is referenced by lookup(), it is tested to see if it
self._entries[a].invalidate()
def update(self):
- tags = self.api.links().list(filters=[['link_class', '=', 'tag']], select=['name'], distinct = 'name').execute()
- oldentries = self._entries
- self._entries = {}
- for n in tags['items']:
- n = n['name']
- if n in oldentries:
- self._entries[n] = oldentries[n]
- else:
- self._entries[n] = self.inodes.add_entry(TagDirectory(self.inode, self.inodes, self.api, n, poll=self._poll, poll_time=self._poll_time))
- self.fresh()
-
+ tags = self.api.links().list(filters=[['link_class', '=', 'tag']], select=['name'], distinct = True).execute()
+ self.merge(tags['items'],
+ lambda i: i['name'],
+ lambda a, i: a.tag == i,
+ lambda i: TagDirectory(self.inode, self.inodes, self.api, i['name'], poll=self._poll, poll_time=self._poll_time))
class TagDirectory(Directory):
'''A special directory that contains as subdirectories all collections visible
self._poll_time = poll_time
def update(self):
- collections = self.api.links().list(filters=[['link_class', '=', 'tag'],
+ taggedcollections = self.api.links().list(filters=[['link_class', '=', 'tag'],
['name', '=', self.tag],
['head_uuid', 'is_a', 'arvados#collection']],
select=['head_uuid']).execute()
- oldentries = self._entries
- self._entries = {}
- for c in collections['items']:
- n = c['head_uuid']
- if n in oldentries:
- self._entries[n] = oldentries[n]
- else:
- self._entries[n] = self.inodes.add_entry(CollectionDirectory(self.inode, self.inodes, n))
- self.fresh()
+ self.merge(taggedcollections['items'],
+ lambda i: i['head_uuid'],
+ lambda a, i: a.collection_locator == i['head_uuid'],
+ lambda i: CollectionDirectory(self.inode, self.inodes, i['head_uuid']))
-class File(object):
- '''Wraps a StreamFileReader for use by Directory.'''
- def __init__(self, parent_inode, reader):
- self.inode = None
- self.parent_inode = parent_inode
- self.reader = reader
+class GroupsDirectory(Directory):
+ '''A special directory that contains as subdirectories all groups visible to the user.'''
- def size(self):
- return self.reader.size()
+ def __init__(self, parent_inode, inodes, api, poll_time=60):
+ super(GroupsDirectory, self).__init__(parent_inode)
+ self.inodes = inodes
+ self.api = api
+ try:
+ arvados.events.subscribe(self.api, [], lambda ev: self.invalidate())
+ except:
+ self._poll = True
+ self._poll_time = poll_time
+
+ def invalidate(self):
+ with llfuse.lock:
+ super(GroupsDirectory, self).invalidate()
+ for a in self._entries:
+ self._entries[a].invalidate()
+
+ def update(self):
+ groups = self.api.groups().list().execute()
+ self.merge(groups['items'],
+ lambda i: i['uuid'],
+ lambda a, i: a.uuid == i['uuid'],
+ lambda i: GroupDirectory(self.inode, self.inodes, self.api, i, poll=self._poll, poll_time=self._poll_time))
+
+
+class GroupDirectory(Directory):
+ '''A special directory that contains the contents of a group.'''
+
+ def __init__(self, parent_inode, inodes, api, uuid, poll=False, poll_time=60):
+ super(GroupDirectory, self).__init__(parent_inode)
+ self.inodes = inodes
+ self.api = api
+ self.uuid = uuid['uuid']
+ self._poll = poll
+ self._poll_time = poll_time
+
+ def invalidate(self):
+ with llfuse.lock:
+ super(GroupDirectory, self).invalidate()
+ for a in self._entries:
+ self._entries[a].invalidate()
+
+ def createDirectory(self, i):
+ if re.match(r'[0-9a-f]{32}\+\d+', i['uuid']):
+ return CollectionDirectory(self.inode, self.inodes, i['uuid'])
+ elif re.match(r'[a-z0-9]{5}-j7d0g-[a-z0-9]{15}', i['uuid']):
+ return GroupDirectory(self.parent_inode, self.inodes, self.api, i, self._poll, self._poll_time)
+ elif re.match(r'[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}', i['uuid']):
+ return ObjectFile(self.parent_inode, i)
+ return None
+
+ def update(self):
+ contents = self.api.groups().contents(uuid=self.uuid, include_linked=True).execute()
+ links = {}
+ for a in contents['links']:
+ links[a['head_uuid']] = a['name']
+
+ def choose_name(i):
+ if i['uuid'] in links:
+ return links[i['uuid']]
+ else:
+ return i['uuid']
+
+ def same(a, i):
+ if isinstance(a, CollectionDirectory):
+ return a.collection_locator == i['uuid']
+ elif isinstance(a, GroupDirectory):
+ return a.uuid == i['uuid']
+ elif isinstance(a, ObjectFile):
+ return a.uuid == i['uuid'] and not a.stale()
+ return False
+
+ self.merge(contents['items'],
+ choose_name,
+ same,
+ self.createDirectory)
class FileHandle(object):
self._counter += 1
return entry
+ def del_entry(self, entry):
+ llfuse.invalidate_inode(entry.inode)
+ del self._entries[entry.inode]
+
class Operations(llfuse.Operations):
'''This is the main interface with llfuse. The methods on this object are
called by llfuse threads to service FUSE events to query and read from
return True
def getattr(self, inode):
+ if inode not in self.inodes:
+ raise llfuse.FUSEError(errno.ENOENT)
+
e = self.inodes[inode]
entry = llfuse.EntryAttributes()
try:
with llfuse.lock_released:
- return handle.entry.reader.readfrom(off, size)
+ return handle.entry.readfrom(off, size)
except:
raise llfuse.FUSEError(errno.EIO)
e = off
while e < len(handle.entry):
- yield (handle.entry[e][0], self.getattr(handle.entry[e][1].inode), e+1)
+ if handle.entry[e][1].inode in self.inodes:
+ yield (handle.entry[e][0], self.getattr(handle.entry[e][1].inode), e+1)
e += 1
def releasedir(self, fh):
import arvados
import subprocess
import argparse
+import daemon
if __name__ == '__main__':
# Handle command line parameters
parser.add_argument('--collection', type=str, help="""Mount only the specified collection at the mount point.""")
parser.add_argument('--tags', action='store_true', help="""Mount as a virtual directory consisting of subdirectories representing tagged
collections on the server.""")
+ parser.add_argument('--groups', action='store_true', help="""Mount as a virtual directory consisting of subdirectories representing groups on the server.""")
parser.add_argument('--debug', action='store_true', help="""Debug mode""")
+ parser.add_argument('--foreground', action='store_true', help="""Run in foreground (default is to daemonize unless --exec specified)""", default=False)
parser.add_argument('--exec', type=str, nargs=argparse.REMAINDER,
dest="exec_args", metavar=('command', 'args', '...', '--'),
help="""Mount, run a command, then unmount and exit""")
# Create the request handler
operations = Operations(os.getuid(), os.getgid())
- if args.tags:
+ if args.groups:
+ api = arvados.api('v1')
+ e = operations.inodes.add_entry(GroupsDirectory(llfuse.ROOT_INODE, operations.inodes, api))
+ elif args.tags:
api = arvados.api('v1')
e = operations.inodes.add_entry(TagsDirectory(llfuse.ROOT_INODE, operations.inodes, api))
elif args.collection != None:
if args.debug:
opts += ['debug']
- # Initialize the fuse connection
- llfuse.init(operations, args.mountpoint, opts)
-
if args.exec_args:
+ # Initialize the fuse connection
+ llfuse.init(operations, args.mountpoint, opts)
+
t = threading.Thread(None, lambda: llfuse.main())
t.start()
exit(rc)
else:
- llfuse.main()
+ if args.foreground:
+ # Initialize the fuse connection
+ llfuse.init(operations, args.mountpoint, opts)
+ llfuse.main()
+ else:
+ with daemon.DaemonContext():
+ # Initialize the fuse connection
+ llfuse.init(operations, args.mountpoint, opts)
+ llfuse.main()
urllib3==1.7.1
llfuse==0.40
ws4py==0.3.4
+PyYAML==3.11
+python-daemon==1.6
],
install_requires=[
'arvados-python-client',
- 'llfuse'
+ 'llfuse',
+ 'python-daemon'
],
zip_safe=False)
import subprocess
import glob
import run_test_server
-
+import json
class MountTestBase(unittest.TestCase):
def setUp(self):
d3.sort()
self.assertEqual(['fa7aeb5140e2848d39b416daeef4ffc5+45'], d3)
- api.links().create(body={'link': {
+ l = api.links().create(body={'link': {
'head_uuid': 'ea10d51bcf88862dbcc36eb292017dfd+45',
'link_class': 'tag',
'name': 'bar_tag'
d4.sort()
self.assertEqual(['ea10d51bcf88862dbcc36eb292017dfd+45', 'fa7aeb5140e2848d39b416daeef4ffc5+45'], d4)
+ api.links().delete(uuid=l['uuid']).execute()
+
+ time.sleep(1)
+
+ d5 = os.listdir(os.path.join(self.mounttmp, 'bar_tag'))
+ d5.sort()
+ self.assertEqual(['fa7aeb5140e2848d39b416daeef4ffc5+45'], d5)
+
class FuseTagsUpdateTestWebsockets(FuseTagsUpdateTestBase):
def setUp(self):
def tearDown(self):
run_test_server.stop(False)
super(FuseTagsUpdateTestPoll, self).tearDown()
+
+
+class FuseGroupsTest(MountTestBase):
+ def setUp(self):
+ super(FuseGroupsTest, self).setUp()
+ run_test_server.run()
+
+ def runTest(self):
+ run_test_server.authorize_with("admin")
+ api = arvados.api('v1')
+
+ operations = fuse.Operations(os.getuid(), os.getgid())
+ e = operations.inodes.add_entry(fuse.GroupsDirectory(llfuse.ROOT_INODE, operations.inodes, api))
+
+ llfuse.init(operations, self.mounttmp, [])
+ t = threading.Thread(None, lambda: llfuse.main())
+ t.start()
+
+ # wait until the driver is finished initializing
+ operations.initlock.wait()
+
+ d1 = os.listdir(self.mounttmp)
+ d1.sort()
+ self.assertIn('zzzzz-j7d0g-v955i6s2oi1cbso', d1)
+
+ d2 = os.listdir(os.path.join(self.mounttmp, 'zzzzz-j7d0g-v955i6s2oi1cbso'))
+ d2.sort()
+ self.assertEqual(["I'm a job in a folder",
+ "I'm a template in a folder",
+ "zzzzz-j58dm-5gid26432uujf79",
+ "zzzzz-j58dm-7r18rnd5nzhg5yk",
+ "zzzzz-j7d0g-axqo7eu9pwvna1x"
+ ], d2)
+
+ d3 = os.listdir(os.path.join(self.mounttmp, 'zzzzz-j7d0g-v955i6s2oi1cbso', 'zzzzz-j7d0g-axqo7eu9pwvna1x'))
+ d3.sort()
+ self.assertEqual(["I'm in a subfolder, too",
+ "zzzzz-j58dm-c40lddwcqqr1ffs",
+ "zzzzz-o0j2j-ryhm1bn83ni03sn"
+ ], d3)
+
+ with open(os.path.join(self.mounttmp, 'zzzzz-j7d0g-v955i6s2oi1cbso', "I'm a template in a folder")) as f:
+ j = json.load(f)
+ self.assertEqual("Two Part Pipeline Template", j['name'])
+
+ def tearDown(self):
+ run_test_server.stop()
+ super(FuseGroupsTest, self).tearDown()
+Gemfile.lock
arvados*gem
+++ /dev/null
-PATH
- remote: .
- specs:
- arvados (0.1.20140228213600)
- activesupport (>= 3.2.13)
- andand
- google-api-client (~> 0.6.3)
- json (>= 1.7.7)
-
-GEM
- remote: https://rubygems.org/
- specs:
- activesupport (3.2.17)
- i18n (~> 0.6, >= 0.6.4)
- multi_json (~> 1.0)
- addressable (2.3.5)
- andand (1.3.3)
- autoparse (0.3.3)
- addressable (>= 2.3.1)
- extlib (>= 0.9.15)
- multi_json (>= 1.0.0)
- extlib (0.9.16)
- faraday (0.8.9)
- multipart-post (~> 1.2.0)
- google-api-client (0.6.4)
- addressable (>= 2.3.2)
- autoparse (>= 0.3.3)
- extlib (>= 0.9.15)
- faraday (~> 0.8.4)
- jwt (>= 0.1.5)
- launchy (>= 2.1.1)
- multi_json (>= 1.0.0)
- signet (~> 0.4.5)
- uuidtools (>= 2.1.0)
- i18n (0.6.9)
- json (1.8.1)
- jwt (0.1.11)
- multi_json (>= 1.5)
- launchy (2.4.2)
- addressable (~> 2.3)
- minitest (5.2.2)
- multi_json (1.8.4)
- multipart-post (1.2.0)
- rake (10.1.1)
- signet (0.4.5)
- addressable (>= 2.2.3)
- faraday (~> 0.8.1)
- jwt (>= 0.1.5)
- multi_json (>= 1.0.0)
- uuidtools (2.1.4)
-
-PLATFORMS
- ruby
-
-DEPENDENCIES
- arvados!
- minitest (>= 5.0.0)
- rake
/Capfile*
/config/deploy*
+# SimpleCov reports
+/coverage
# gem 'rails', :git => 'git://github.com/rails/rails.git'
group :test, :development do
- gem 'sqlite3'
+ # Note: "require: false" here tells bunder not to automatically
+ # 'require' the packages during application startup. Installation is
+ # still mandatory.
+ gem 'simplecov', '~> 0.7.1', require: false
+ gem 'simplecov-rcov', require: false
end
# This might not be needed in :test and :development, but we load it
gem 'themes_for_rails'
gem 'arvados-cli', '>= 0.1.20140328152103'
+
+# pg_power lets us use partial indexes in schema.rb in Rails 3
+gem 'pg_power'
rack
rake (>= 0.8.1)
pg (0.17.1)
+ pg_power (1.6.4)
+ pg
+ rails (~> 3.1)
polyglot (0.3.4)
rack (1.4.5)
rack-cache (1.2)
faraday (~> 0.8.1)
jwt (>= 0.1.5)
multi_json (>= 1.0.0)
+ simplecov (0.7.1)
+ multi_json (~> 1.0)
+ simplecov-html (~> 0.7.1)
+ simplecov-html (0.7.1)
+ simplecov-rcov (0.2.3)
+ simplecov (>= 0.4.1)
sprockets (2.2.2)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
- sqlite3 (1.3.9)
test_after_commit (0.2.3)
themes_for_rails (0.5.1)
rails (>= 3.0.0)
omniauth-oauth2 (= 1.1.1)
passenger
pg
+ pg_power
rails (~> 3.2.0)
redis
rvm-capistrano
sass-rails (>= 3.2.0)
- sqlite3
+ simplecov (~> 0.7.1)
+ simplecov-rcov
test_after_commit
themes_for_rails
therubyracer
require File.expand_path('../config/application', __FILE__)
+begin
+ ok = PgPower
+rescue
+ abort "Hm, pg_power is missing. Make sure you use 'bundle exec rake ...'"
+end
+
Server::Application.load_tasks
before_filter :catch_redirect_hint
before_filter(:find_object_by_uuid,
except: [:index, :create] + ERROR_ACTIONS)
- before_filter :load_limit_offset_order_params, only: [:index, :owned_items]
- before_filter :load_where_param, only: [:index, :owned_items]
- before_filter :load_filters_param, only: [:index, :owned_items]
+ before_filter :load_limit_offset_order_params, only: [:index, :contents]
+ before_filter :load_where_param, only: [:index, :contents]
+ before_filter :load_filters_param, only: [:index, :contents]
before_filter :find_objects_for_index, :only => :index
before_filter :reload_object_before_update, :only => :update
before_filter(:render_404_if_no_object,
show
end
- def self._owned_items_requires_parameters
- _index_requires_parameters.
- merge({
- include_linked: {
- type: 'boolean', required: false, default: false
- },
- })
- end
-
- def owned_items
- all_objects = []
- all_available = 0
-
- # Trick apply_where_limit_order_params into applying suitable
- # per-table values. *_all are the real ones we'll apply to the
- # aggregate set.
- limit_all = @limit
- offset_all = @offset
- @orders = []
-
- ArvadosModel.descendants.
- reject(&:abstract_class?).
- sort_by(&:to_s).
- each do |klass|
- case klass.to_s
- # We might expect klass==Link etc. here, but we would be
- # disappointed: when Rails reloads model classes, we get two
- # distinct classes called Link which do not equal each
- # other. But we can still rely on klass.to_s to be "Link".
- when 'ApiClientAuthorization'
- # Do not want.
- else
- @objects = klass.readable_by(*@read_users)
- cond_sql = "#{klass.table_name}.owner_uuid = ?"
- cond_params = [@object.uuid]
- if params[:include_linked]
- @objects = @objects.
- joins("LEFT JOIN links mng_links"\
- " ON mng_links.link_class=#{klass.sanitize 'permission'}"\
- " AND mng_links.name=#{klass.sanitize 'can_manage'}"\
- " AND mng_links.tail_uuid=#{klass.sanitize @object.uuid}"\
- " AND mng_links.head_uuid=#{klass.table_name}.uuid")
- cond_sql += " OR mng_links.uuid IS NOT NULL"
- end
- @objects = @objects.where(cond_sql, *cond_params).order(:uuid)
- @limit = limit_all - all_objects.count
- apply_where_limit_order_params
- items_available = @objects.
- except(:limit).except(:offset).
- count(:id, distinct: true)
- all_available += items_available
- @offset = [@offset - items_available, 0].max
-
- all_objects += @objects.to_a
- end
- end
- @objects = all_objects || []
- @object_list = {
- :kind => "arvados#objectList",
- :etag => "",
- :self_link => "",
- :offset => offset_all,
- :limit => limit_all,
- :items_available => all_available,
- :items => @objects.as_api_response(nil)
- }
- render json: @object_list
- end
-
def catch_redirect_hint
if !current_user
if params.has_key?('redirect_to') then
class Arvados::V1::GroupsController < ApplicationController
+
+ def self._contents_requires_parameters
+ _index_requires_parameters.
+ merge({
+ include_linked: {
+ type: 'boolean', required: false, default: false
+ },
+ })
+ end
+
+ def contents
+ all_objects = []
+ all_available = 0
+
+ # Trick apply_where_limit_order_params into applying suitable
+ # per-table values. *_all are the real ones we'll apply to the
+ # aggregate set.
+ limit_all = @limit
+ offset_all = @offset
+ @orders = []
+
+ ArvadosModel.descendants.reject(&:abstract_class?).sort_by(&:to_s).
+ each do |klass|
+ case klass.to_s
+ # We might expect klass==Link etc. here, but we would be
+ # disappointed: when Rails reloads model classes, we get two
+ # distinct classes called Link which do not equal each
+ # other. But we can still rely on klass.to_s to be "Link".
+ when 'ApiClientAuthorization', 'UserAgreement', 'Link'
+ # Do not want.
+ else
+ @objects = klass.readable_by(*@read_users)
+ cond_sql = "#{klass.table_name}.owner_uuid = ?"
+ cond_params = [@object.uuid]
+ if params[:include_linked]
+ cond_sql += " OR #{klass.table_name}.uuid IN (SELECT head_uuid FROM links WHERE link_class=#{klass.sanitize 'name'} AND links.tail_uuid=#{klass.sanitize @object.uuid})"
+ end
+ @objects = @objects.where(cond_sql, *cond_params).order("#{klass.table_name}.uuid")
+ @limit = limit_all - all_objects.count
+ apply_where_limit_order_params
+ items_available = @objects.
+ except(:limit).except(:offset).
+ count(:id, distinct: true)
+ all_available += items_available
+ @offset = [@offset - items_available, 0].max
+
+ all_objects += @objects.to_a
+ end
+ end
+ @objects = all_objects || []
+ @links = Link.where('link_class=? and tail_uuid=?'\
+ ' and head_uuid in (?)',
+ 'name',
+ @object.uuid,
+ @objects.collect(&:uuid))
+ @object_list = {
+ :kind => "arvados#objectList",
+ :etag => "",
+ :self_link => "",
+ :links => @links.as_api_response(nil),
+ :offset => offset_all,
+ :limit => limit_all,
+ :items_available => all_available,
+ :items => @objects.as_api_response(nil)
+ }
+ render json: @object_list
+ end
+
end
# omniauth callback method
def create
omniauth = env['omniauth.auth']
- #logger.debug "+++ #{omniauth}"
identity_url_ok = (omniauth['info']['identity_url'].length > 0) rescue false
unless identity_url_ok
# "unauthorized":
Thread.current[:user] = user
- user.save!
+ user.save or raise Exception.new(user.errors.messages)
omniauth.delete('extra')
attr_protected :modified_by_client_uuid
attr_protected :modified_at
after_initialize :log_start_state
- before_create :ensure_permission_to_create
- before_update :ensure_permission_to_update
+ before_save :ensure_permission_to_save
+ before_save :ensure_owner_uuid_is_permitted
+ before_save :ensure_ownership_path_leads_to_user
+ before_destroy :ensure_owner_uuid_is_permitted
before_destroy :ensure_permission_to_destroy
before_create :update_modified_by_fields
self.columns.select { |col| col.name == attr.to_s }.first
end
+ # Return nil if current user is not allowed to see the list of
+ # writers. Otherwise, return a list of user_ and group_uuids with
+ # write permission. (If not returning nil, current_user is always in
+ # the list because can_manage permission is needed to see the list
+ # of writers.)
+ def writable_by
+ unless (owner_uuid == current_user.uuid or
+ current_user.is_admin or
+ current_user.groups_i_can(:manage).index(owner_uuid))
+ return nil
+ end
+ [owner_uuid, current_user.uuid] + permissions.collect do |p|
+ if ['can_write', 'can_manage'].index p.name
+ p.tail_uuid
+ end
+ end.compact.uniq
+ end
+
# Return a query with read permissions restricted to the union of of the
# permissions of the members of users_list, i.e. if something is readable by
# any user in users_list, it will be readable in the query returned by this
# A permission link exists ('write' and 'manage' implicitly include
# 'read') from a member of users_list, or a group readable by users_list,
# to this row, or to the owner of this row (see join() below).
+ permitted_uuids = "(SELECT head_uuid FROM links WHERE link_class='permission' AND tail_uuid IN (#{sanitized_uuid_list}))"
+
sql_conds += ["#{table_name}.owner_uuid in (?)",
"#{table_name}.uuid in (?)",
- "permissions.head_uuid IS NOT NULL"]
+ "#{table_name}.uuid IN #{permitted_uuids}"]
sql_params += [uuid_list, user_uuids]
if self == Link and users_list.any?
if self == Log and users_list.any?
# Link head points to the object described by this row
- or_object_uuid = ", #{table_name}.object_uuid"
+ sql_conds += ["#{table_name}.object_uuid IN #{permitted_uuids}"]
# This object described by this row is owned by this user, or owned by a group readable by this user
sql_conds += ["#{table_name}.object_owner_uuid in (?)"]
# user (the identity with authorization to read)
#
# Link class is 'permission' ('write' and 'manage' implicitly include 'read')
-
- joins("LEFT JOIN links permissions ON permissions.head_uuid in (#{table_name}.owner_uuid, #{table_name}.uuid #{or_object_uuid}) AND permissions.tail_uuid in (#{sanitized_uuid_list}) AND permissions.link_class='permission'")
- .where(sql_conds.join(' OR '), *sql_params).uniq
-
+ where(sql_conds.join(' OR '), *sql_params)
else
# At least one user is admin, so don't bother to apply any restrictions.
self
end
-
end
def logged_attributes
protected
- def ensure_permission_to_create
- raise PermissionDeniedError unless permission_to_create
+ def ensure_ownership_path_leads_to_user
+ if new_record? or owner_uuid_changed?
+ uuid_in_path = {owner_uuid => true, uuid => true}
+ x = owner_uuid
+ while (owner_class = self.class.resource_class_for_uuid(x)) != User
+ begin
+ if x == uuid
+ # Test for cycles with the new version, not the DB contents
+ x = owner_uuid
+ elsif !owner_class.respond_to? :find_by_uuid
+ raise ActiveRecord::RecordNotFound.new
+ else
+ x = owner_class.find_by_uuid(x).owner_uuid
+ end
+ rescue ActiveRecord::RecordNotFound => e
+ errors.add :owner_uuid, "is not owned by any user: #{e}"
+ return false
+ end
+ if uuid_in_path[x]
+ if x == owner_uuid
+ errors.add :owner_uuid, "would create an ownership cycle"
+ else
+ errors.add :owner_uuid, "has an ownership cycle"
+ end
+ return false
+ end
+ uuid_in_path[x] = true
+ end
+ end
+ true
end
- def permission_to_create
- current_user.andand.is_active
+ def ensure_owner_uuid_is_permitted
+ raise PermissionDeniedError if !current_user
+ self.owner_uuid ||= current_user.uuid
+ if self.owner_uuid_changed?
+ if current_user.uuid == self.owner_uuid or
+ current_user.can? write: self.owner_uuid
+ # current_user is, or has :write permission on, the new owner
+ else
+ logger.warn "User #{current_user.uuid} tried to change owner_uuid of #{self.class.to_s} #{self.uuid} to #{self.owner_uuid} but does not have permission to write to #{self.owner_uuid}"
+ raise PermissionDeniedError
+ end
+ end
+ if new_record?
+ return true
+ elsif current_user.uuid == self.owner_uuid_was or
+ current_user.uuid == self.uuid or
+ current_user.can? write: self.owner_uuid_was
+ # current user is, or has :write permission on, the previous owner
+ return true
+ else
+ logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} but does not have permission to write #{self.owner_uuid_was}"
+ raise PermissionDeniedError
+ end
+ end
+
+ def ensure_permission_to_save
+ unless (new_record? ? permission_to_create : permission_to_update)
+ raise PermissionDeniedError
+ end
end
- def ensure_permission_to_update
- raise PermissionDeniedError unless permission_to_update
+ def permission_to_create
+ current_user.andand.is_active
end
def permission_to_update
logger.warn "User #{current_user.uuid} tried to change uuid of #{self.class.to_s} #{self.uuid_was} to #{self.uuid}"
return false
end
- if self.owner_uuid_changed?
- if current_user.uuid == self.owner_uuid or
- current_user.can? write: self.owner_uuid
- # current_user is, or has :write permission on, the new owner
- else
- logger.warn "User #{current_user.uuid} tried to change owner_uuid of #{self.class.to_s} #{self.uuid} to #{self.owner_uuid} but does not have permission to write to #{self.owner_uuid}"
- return false
- end
- end
- if current_user.uuid == self.owner_uuid_was or
- current_user.uuid == self.uuid or
- current_user.can? write: self.owner_uuid_was
- # current user is, or has :write permission on, the previous owner
- return true
- else
- logger.warn "User #{current_user.uuid} tried to modify #{self.class.to_s} #{self.uuid} but does not have permission to write #{self.owner_uuid_was}"
- return false
- end
+ return true
end
def ensure_permission_to_destroy
def maybe_update_modified_by_fields
update_modified_by_fields if self.changed? or self.new_record?
+ true
end
def update_modified_by_fields
self.modified_at = Time.now
self.modified_by_user_uuid = current_user ? current_user.uuid : nil
self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
+ true
end
def ensure_serialized_attribute_type
t.add :name
t.add :group_class
t.add :description
+ t.add :writable_by
end
end
after_create :maybe_invalidate_permissions_cache
after_destroy :maybe_invalidate_permissions_cache
attr_accessor :head_kind, :tail_kind
+ validate :name_link_has_valid_name
api_accessible :user, extend: :common do |t|
t.add :tail_uuid
User.invalidate_permissions_cache
end
end
+
+ def name_link_has_valid_name
+ if link_class == 'name'
+ unless name.is_a? String and !name.empty?
+ errors.add('name', 'must be a non-empty string')
+ end
+ else
+ true
+ end
+ end
end
end
def start!(ping_url_method)
- ensure_permission_to_update
+ ensure_permission_to_save
ping_url = ping_url_method.call({ id: self.uuid, ping_secret: self.info[:ping_secret] })
if (Rails.configuration.compute_node_ec2run_args and
Rails.configuration.compute_node_ami)
protected
+ def ensure_ownership_path_leads_to_user
+ true
+ end
+
def permission_to_update
# users must be able to update themselves (even if they are
# inactive) in order to create sessions
get 'used_by', on: :member
end
resources :groups do
- get 'owned_items', on: :member
+ get 'contents', on: :member
end
resources :humans
resources :job_tasks
post 'activate', on: :member
post 'setup', on: :collection
post 'unsetup', on: :member
- get 'owned_items', on: :member
end
resources :virtual_machines do
get 'logins', on: :member
--- /dev/null
+class AddUniqueNameIndexToLinks < ActiveRecord::Migration
+ def change
+ # Make sure PgPower is here. Otherwise the "where" will be ignored
+ # and we'll end up with a far too restrictive unique
+ # constraint. (Rails4 should work without PgPower, but that isn't
+ # tested.)
+ if not PgPower then raise "No partial column support" end
+
+ add_index(:links, [:tail_uuid, :name], unique: true,
+ where: "link_class='name'",
+ name: 'links_tail_name_unique_if_link_class_name')
+ end
+end
#
# It's strongly recommended to check this file into your version control system.
-ActiveRecord::Schema.define(:version => 20140423133559) do
+ActiveRecord::Schema.define(:version => 20140501165548) do
+
+
create_table "api_client_authorizations", :force => true do |t|
t.string "api_token", :null => false
add_index "links", ["created_at"], :name => "index_links_on_created_at"
add_index "links", ["head_uuid"], :name => "index_links_on_head_uuid"
add_index "links", ["modified_at"], :name => "index_links_on_modified_at"
+ add_index "links", ["tail_uuid", "name"], :name => "links_tail_name_unique_if_link_class_name", :unique => true, :where => "((link_class)::text = 'name'::text)"
add_index "links", ["tail_uuid"], :name => "index_links_on_tail_uuid"
add_index "links", ["uuid"], :name => "index_links_on_uuid", :unique => true
add_index "virtual_machines", ["hostname"], :name => "index_virtual_machines_on_hostname"
add_index "virtual_machines", ["uuid"], :name => "index_virtual_machines_on_uuid", :unique => true
+
end
def system_user
if not $system_user
real_current_user = Thread.current[:user]
- Thread.current[:user] = User.new(is_admin: true, is_active: true)
+ Thread.current[:user] = User.new(is_admin: true,
+ is_active: true,
+ uuid: system_user_uuid)
$system_user = User.where('uuid=?', system_user_uuid).first
if !$system_user
$system_user = User.new(uuid: system_user_uuid,
when String
begin
@select = Oj.load params[:select]
- raise unless @select.is_a? Array
+ raise unless @select.is_a? Array or @select.nil?
rescue
raise ArgumentError.new("Could not parse \"select\" param as an array")
end
trusted_workbench:
uuid: zzzzz-ozdt8-teyxzyd8qllg11h
+ owner_uuid: zzzzz-tpzed-000000000000000
name: Official Workbench
url_prefix: https://official-workbench.local/
is_trusted: true
untrusted:
uuid: zzzzz-ozdt8-obw7foaks3qjyej
+ owner_uuid: zzzzz-tpzed-000000000000000
name: Untrusted
url_prefix: https://untrusted.local/
is_trusted: false
modified_at: 2014-04-21 15:37:48 -0400
updated_at: 2014-04-21 15:37:48 -0400
name: A Subfolder
- description: Test folder belonging to active user's first test folder
+ description: "Test folder belonging to active user's first test folder"
group_class: folder
+
+bad_group_has_ownership_cycle_a:
+ uuid: zzzzz-j7d0g-cx2al9cqkmsf1hs
+ owner_uuid: zzzzz-j7d0g-0077nzts8c178lw
+ modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+ modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ created_at: 2014-05-03 18:50:08 -0400
+ modified_at: 2014-05-03 18:50:08 -0400
+ updated_at: 2014-05-03 18:50:08 -0400
+ name: Owned by bad group b
+
+bad_group_has_ownership_cycle_b:
+ uuid: zzzzz-j7d0g-0077nzts8c178lw
+ owner_uuid: zzzzz-j7d0g-cx2al9cqkmsf1hs
+ modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+ modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ created_at: 2014-05-03 18:50:08 -0400
+ modified_at: 2014-05-03 18:50:08 -0400
+ updated_at: 2014-05-03 18:50:08 -0400
+ name: Owned by bad group a
tail_uuid: zzzzz-tpzed-l1s2piq4t4mps8r
link_class: permission
name: can_read
- head_uuid: zzzzz-2x53u-382brsig8rp3666
+ head_uuid: zzzzz-s0uqq-382brsig8rp3666
properties: {}
foo_repository_writable_by_active:
specimen_is_in_two_folders:
uuid: zzzzz-o0j2j-ryhm1bn83ni03sn
- owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
created_at: 2014-04-21 15:37:48 -0400
modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
modified_at: 2014-04-21 15:37:48 -0400
updated_at: 2014-04-21 15:37:48 -0400
tail_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
- head_uuid: zzzzz-2x53u-5gid26432uujf79
- link_class: permission
- name: can_manage
+ head_uuid: zzzzz-j58dm-5gid26432uujf79
+ link_class: name
+ name: "I'm in a subfolder, too"
+ properties: {}
+
+template_name_in_afolder:
+ uuid: zzzzz-o0j2j-4kpwf3d6rwkeqhl
+ owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ created_at: 2014-04-29 16:47:26 -0400
+ modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+ modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ modified_at: 2014-04-29 16:47:26 -0400
+ updated_at: 2014-04-29 16:47:26 -0400
+ tail_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
+ head_uuid: zzzzz-p5p6p-aox0k0ofxrystgw
+ link_class: name
+ name: "I'm a template in a folder"
+ properties: {}
+
+job_name_in_afolder:
+ uuid: zzzzz-o0j2j-1kt6dppqcxbl1yt
+ owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ created_at: 2014-04-29 16:47:26 -0400
+ modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+ modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ modified_at: 2014-04-29 16:47:26 -0400
+ updated_at: 2014-04-29 16:47:26 -0400
+ tail_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
+ head_uuid: zzzzz-8i9sb-pshmckwoma9plh7
+ link_class: name
+ name: "I'm a job in a folder"
properties: {}
foo_collection_tag:
link_class: tag
name: foo_tag
properties: {}
+
+active_user_can_manage_bad_group_cx2al9cqkmsf1hs:
+ uuid: zzzzz-o0j2j-ezv55ahzc9lvjwe
+ owner_uuid: zzzzz-tpzed-000000000000000
+ created_at: 2014-05-03 18:50:08 -0400
+ modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+ modified_by_user_uuid: zzzzz-tpzed-000000000000000
+ modified_at: 2014-05-03 18:50:08 -0400
+ updated_at: 2014-05-03 18:50:08 -0400
+ tail_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ link_class: permission
+ name: can_manage
+ head_uuid: zzzzz-j7d0g-cx2al9cqkmsf1hs
+ properties: {}
foo:
- uuid: zzzzz-2x53u-382brsig8rp3666
+ uuid: zzzzz-s0uqq-382brsig8rp3666
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz # active user
name: foo
repository2:
- uuid: zzzzz-2x53u-382brsig8rp3667
+ uuid: zzzzz-s0uqq-382brsig8rp3667
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz # active user
name: foo2
owned_by_active_user:
- uuid: zzzzz-2x53u-3zx463qyo0k4xrn
+ uuid: zzzzz-j58dm-3zx463qyo0k4xrn
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ created_at: 2014-04-21 15:37:48 -0400
+ modified_at: 2014-04-21 15:37:48 -0400
owned_by_private_group:
- uuid: zzzzz-2x53u-5m3qwg45g3nlpu6
+ uuid: zzzzz-j58dm-5m3qwg45g3nlpu6
owner_uuid: zzzzz-j7d0g-rew6elm53kancon
+ created_at: 2014-04-21 15:37:48 -0400
+ modified_at: 2014-04-21 15:37:48 -0400
owned_by_spectator:
- uuid: zzzzz-2x53u-3b0xxwzlbzxq5yr
+ uuid: zzzzz-j58dm-3b0xxwzlbzxq5yr
owner_uuid: zzzzz-tpzed-l1s2piq4t4mps8r
+ created_at: 2014-04-21 15:37:48 -0400
+ modified_at: 2014-04-21 15:37:48 -0400
in_afolder:
- uuid: zzzzz-2x53u-7r18rnd5nzhg5yk
+ uuid: zzzzz-j58dm-7r18rnd5nzhg5yk
owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
+ created_at: 2014-04-21 15:37:48 -0400
+ modified_at: 2014-04-21 15:37:48 -0400
in_asubfolder:
- uuid: zzzzz-2x53u-c40lddwcqqr1ffs
+ uuid: zzzzz-j58dm-c40lddwcqqr1ffs
owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
+ created_at: 2014-04-21 15:37:48 -0400
+ modified_at: 2014-04-21 15:37:48 -0400
in_afolder_linked_from_asubfolder:
- uuid: zzzzz-2x53u-5gid26432uujf79
+ uuid: zzzzz-j58dm-5gid26432uujf79
owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
+ created_at: 2014-04-21 15:37:48 -0400
+ modified_at: 2014-04-21 15:37:48 -0400
+
+owned_by_afolder_with_no_name_link:
+ uuid: zzzzz-j58dm-ypsjlol9dofwijz
+ owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
+ created_at: 2014-05-05 04:11:52 -0400
+ modified_at: 2014-05-05 04:11:52 -0400
test 'get group-owned objects' do
authorize_with :active
- get :owned_items, {
+ get :contents, {
id: groups(:afolder).uuid,
format: :json,
+ include_linked: true,
}
assert_response :success
assert_operator 2, :<=, json_response['items_available']
assert_operator 2, :<=, json_response['items'].count
+ kinds = json_response['items'].collect { |i| i['kind'] }.uniq
+ expect_kinds = %w'arvados#group arvados#specimen arvados#pipelineTemplate arvados#job'
+ assert_equal expect_kinds, (expect_kinds & kinds)
end
test 'get group-owned objects with limit' do
authorize_with :active
- get :owned_items, {
+ get :contents, {
id: groups(:afolder).uuid,
limit: 1,
format: :json,
test 'get group-owned objects with limit and offset' do
authorize_with :active
- get :owned_items, {
+ get :contents, {
id: groups(:afolder).uuid,
limit: 1,
offset: 12345,
test 'get group-owned objects with additional filter matching nothing' do
authorize_with :active
- get :owned_items, {
+ get :contents, {
id: groups(:afolder).uuid,
filters: [['uuid', 'in', ['foo_not_a_uuid','bar_not_a_uuid']]],
format: :json,
test 'get group-owned objects without include_linked' do
unexpected_uuid = specimens(:in_afolder_linked_from_asubfolder).uuid
authorize_with :active
- get :owned_items, {
+ get :contents, {
id: groups(:asubfolder).uuid,
format: :json,
}
test 'get group-owned objects with include_linked' do
expected_uuid = specimens(:in_afolder_linked_from_asubfolder).uuid
authorize_with :active
- get :owned_items, {
+ get :contents, {
id: groups(:asubfolder).uuid,
include_linked: true,
format: :json,
assert_response :success
uuids = json_response['items'].collect { |i| i['uuid'] }
assert_includes uuids, expected_uuid, "Did not get #{expected_uuid}"
+
+ expected_name = links(:specimen_is_in_two_folders).name
+ found_specimen_name = false
+ assert(json_response['links'].any?,
+ "Expected a non-empty array of links in response")
+ json_response['links'].each do |link|
+ if link['head_uuid'] == expected_uuid
+ if link['name'] == expected_name
+ found_specimen_name = true
+ end
+ end
+ end
+ assert(found_specimen_name,
+ "Expected to find name '#{expected_name}' in response")
+ end
+
+ [false, true].each do |inc_ind|
+ test "get all pages of group-owned #{'and -linked ' if inc_ind}objects" do
+ authorize_with :active
+ limit = 5
+ offset = 0
+ items_available = nil
+ uuid_received = {}
+ owner_received = {}
+ while true
+ # Behaving badly here, using the same controller multiple
+ # times within a test.
+ @json_response = nil
+ get :contents, {
+ id: groups(:afolder).uuid,
+ include_linked: inc_ind,
+ limit: limit,
+ offset: offset,
+ format: :json,
+ }
+ assert_response :success
+ assert_operator(0, :<, json_response['items'].count,
+ "items_available=#{items_available} but received 0 "\
+ "items with offset=#{offset}")
+ items_available ||= json_response['items_available']
+ assert_equal(items_available, json_response['items_available'],
+ "items_available changed between page #{offset/limit} "\
+ "and page #{1+offset/limit}")
+ json_response['items'].each do |item|
+ uuid = item['uuid']
+ assert_equal(nil, uuid_received[uuid],
+ "Received '#{uuid}' again on page #{1+offset/limit}")
+ uuid_received[uuid] = true
+ owner_received[item['owner_uuid']] = true
+ offset += 1
+ if not inc_ind
+ assert_equal groups(:afolder).uuid, item['owner_uuid']
+ end
+ end
+ break if offset >= items_available
+ end
+ if inc_ind
+ assert_operator 0, :<, (json_response.keys - [users(:active).uuid]).count,
+ "Set include_linked=true but did not receive any non-owned items"
+ end
+ end
+ end
+
+ %w(offset limit).each do |arg|
+ ['foo', '', '1234five', '0x10', '-8'].each do |val|
+ test "Raise error on bogus #{arg} parameter #{val.inspect}" do
+ authorize_with :active
+ get :contents, {
+ :id => groups(:afolder).uuid,
+ :format => :json,
+ arg => val,
+ }
+ assert_response 422
+ end
+ end
+ end
+
+ test 'get writable_by list for owned group' do
+ authorize_with :active
+ get :show, {
+ id: groups(:afolder).uuid,
+ format: :json
+ }
+ assert_response :success
+ assert_not_nil(json_response['writable_by'],
+ "Should receive uuid list in 'writable_by' field")
+ assert_includes(json_response['writable_by'], users(:active).uuid,
+ "owner should be included in writable_by list")
+ end
+
+ test 'no writable_by list for group with read-only access' do
+ authorize_with :rominiadmin
+ get :show, {
+ id: groups(:testusergroup_admins).uuid,
+ format: :json
+ }
+ assert_response :success
+ assert_nil(json_response['writable_by'],
+ "Should not receive uuid list in 'writable_by' field")
+ end
+
+ test 'get writable_by list by admin user' do
+ authorize_with :admin
+ get :show, {
+ id: groups(:testusergroup_admins).uuid,
+ format: :json
+ }
+ assert_response :success
+ assert_not_nil(json_response['writable_by'],
+ "Should receive uuid list in 'writable_by' field")
+ assert_includes(json_response['writable_by'],
+ users(:admin).uuid,
+ "Current user should be included in 'writable_by' field")
end
end
assert_response :success
end
+ test "refuse duplicate name" do
+ the_name = links(:job_name_in_afolder).name
+ the_folder = links(:job_name_in_afolder).tail_uuid
+ authorize_with :active
+ post :create, link: {
+ tail_uuid: the_folder,
+ head_uuid: specimens(:owned_by_active_user).uuid,
+ link_class: 'name',
+ name: the_name,
+ properties: {this_s: "a duplicate name"}
+ }
+ assert_response 422
+ end
end
tail_uuid: system_group_uuid,
head_uuid: user_uuid).count
end
-
- test 'get user-owned objects' do
- authorize_with :active
- get :owned_items, {
- id: users(:active).uuid,
- limit: 500,
- format: :json,
- }
- assert_response :success
- assert_operator 2, :<=, json_response['items_available']
- assert_operator 2, :<=, json_response['items'].count
- kinds = json_response['items'].collect { |i| i['kind'] }.uniq
- expect_kinds = %w'arvados#group arvados#specimen arvados#pipelineTemplate arvados#job'
- assert_equal expect_kinds, (expect_kinds & kinds)
- end
-
- [false, true].each do |inc_ind|
- test "get all pages of user-owned #{'and -linked ' if inc_ind}objects" do
- authorize_with :active
- limit = 5
- offset = 0
- items_available = nil
- uuid_received = {}
- owner_received = {}
- while true
- # Behaving badly here, using the same controller multiple
- # times within a test.
- @json_response = nil
- get :owned_items, {
- id: users(:active).uuid,
- include_linked: inc_ind,
- limit: limit,
- offset: offset,
- format: :json,
- }
- assert_response :success
- assert_operator(0, :<, json_response['items'].count,
- "items_available=#{items_available} but received 0 "\
- "items with offset=#{offset}")
- items_available ||= json_response['items_available']
- assert_equal(items_available, json_response['items_available'],
- "items_available changed between page #{offset/limit} "\
- "and page #{1+offset/limit}")
- json_response['items'].each do |item|
- uuid = item['uuid']
- assert_equal(nil, uuid_received[uuid],
- "Received '#{uuid}' again on page #{1+offset/limit}")
- uuid_received[uuid] = true
- owner_received[item['owner_uuid']] = true
- offset += 1
- if not inc_ind
- assert_equal users(:active).uuid, item['owner_uuid']
- end
- end
- break if offset >= items_available
- end
- if inc_ind
- assert_operator 0, :<, (json_response.keys - [users(:active).uuid]).count,
- "Set include_linked=true but did not receive any non-owned items"
- end
- end
- end
-
- %w(offset limit).each do |arg|
- ['foo', '', '1234five', '0x10', '-8'].each do |val|
- test "Raise error on bogus #{arg} parameter #{val.inspect}" do
- authorize_with :active
- get :owned_items, {
- :id => users(:active).uuid,
- :format => :json,
- arg => val,
- }
- assert_response 422
- end
- end
- end
end
--- /dev/null
+require 'test_helper'
+
+class UserSessionsApiTest < ActionDispatch::IntegrationTest
+ test 'create new user during omniauth callback' do
+ mock = {
+ 'provider' => 'josh_id',
+ 'uid' => 'https://edward.example.com',
+ 'info' => {
+ 'identity_url' => 'https://edward.example.com',
+ 'name' => 'Edward Example',
+ 'first_name' => 'Edward',
+ 'last_name' => 'Example',
+ 'email' => 'edward@example.com',
+ },
+ }
+ client_url = 'https://wb.example.com'
+ post('/auth/josh_id/callback',
+ {return_to: client_url},
+ {'omniauth.auth' => mock})
+ assert_response :redirect, 'Did not redirect to client with token'
+ assert_equal(0, @response.redirect_url.index(client_url),
+ 'Redirected to wrong address after succesful login: was ' +
+ @response.redirect_url + ', expected ' + client_url + '[...]')
+ assert_not_nil(@response.redirect_url.index('api_token='),
+ 'Expected api_token in query string of redirect url ' +
+ @response.redirect_url)
+ end
+end
ENV["RAILS_ENV"] = "test"
+unless ENV["NO_COVERAGE_TEST"]
+ begin
+ require 'simplecov'
+ require 'simplecov-rcov'
+ class SimpleCov::Formatter::MergedFormatter
+ def format(result)
+ SimpleCov::Formatter::HTMLFormatter.new.format(result)
+ SimpleCov::Formatter::RcovFormatter.new.format(result)
+ end
+ end
+ SimpleCov.formatter = SimpleCov::Formatter::MergedFormatter
+ SimpleCov.start do
+ add_filter '/test/'
+ add_filter 'initializers/secret_token'
+ add_filter 'initializers/omniauth'
+ end
+ rescue Exception => e
+ $stderr.puts "SimpleCov unavailable (#{e}). Proceeding without."
+ end
+end
+
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require 'test_helper'
class GroupTest < ActiveSupport::TestCase
- # test "the truth" do
- # assert true
- # end
+
+ test "cannot set owner_uuid to object with existing ownership cycle" do
+ set_user_from_auth :active_trustedclient
+
+ # First make sure we have lots of permission on the bad group by
+ # renaming it to "{current name} is mine all mine"
+ g = groups(:bad_group_has_ownership_cycle_b)
+ g.name += " is mine all mine"
+ assert g.save, "active user should be able to modify group #{g.uuid}"
+
+ # Use the group as the owner of a new object
+ s = Specimen.
+ create(owner_uuid: groups(:bad_group_has_ownership_cycle_b).uuid)
+ assert s.valid?, "ownership should pass validation"
+ assert_equal false, s.save, "should not save object with #{g.uuid} as owner"
+
+ # Use the group as the new owner of an existing object
+ s = specimens(:in_afolder)
+ s.owner_uuid = groups(:bad_group_has_ownership_cycle_b).uuid
+ assert s.valid?, "ownership should pass validation"
+ assert_equal false, s.save, "should not save object with #{g.uuid} as owner"
+ end
+
+ test "cannot create a new ownership cycle" do
+ set_user_from_auth :active_trustedclient
+
+ g_foo = Group.create(name: "foo")
+ g_foo.save!
+
+ g_bar = Group.create(name: "bar")
+ g_bar.save!
+
+ g_foo.owner_uuid = g_bar.uuid
+ assert g_foo.save, lambda { g_foo.errors.messages }
+ g_bar.owner_uuid = g_foo.uuid
+ assert g_bar.valid?, "ownership cycle should not prevent validation"
+ assert_equal false, g_bar.save, "should not create an ownership loop"
+ assert g_bar.errors.messages[:owner_uuid].join(" ").match(/ownership cycle/)
+ end
+
+ test "cannot create a single-object ownership cycle" do
+ set_user_from_auth :active_trustedclient
+
+ g_foo = Group.create(name: "foo")
+ assert g_foo.save
+
+ # Ensure I have permission to manage this group even when its owner changes
+ perm_link = Link.create(tail_uuid: users(:active).uuid,
+ head_uuid: g_foo.uuid,
+ link_class: 'permission',
+ name: 'can_manage')
+ assert perm_link.save
+
+ g_foo.owner_uuid = g_foo.uuid
+ assert_equal false, g_foo.save, "should not create an ownership loop"
+ assert g_foo.errors.messages[:owner_uuid].join(" ").match(/ownership cycle/)
+ end
+
end
require 'test_helper'
class LinkTest < ActiveSupport::TestCase
- # test "the truth" do
- # assert true
- # end
+ fixtures :all
+
+ setup do
+ Thread.current[:user] = users(:active)
+ end
+
+ test 'name links with the same tail_uuid must be unique' do
+ a = Link.create!(tail_uuid: groups(:afolder).uuid,
+ head_uuid: specimens(:owned_by_active_user).uuid,
+ link_class: 'name',
+ name: 'foo')
+ assert a.valid?, a.errors.to_s
+ assert_raises ActiveRecord::RecordNotUnique do
+ b = Link.create!(tail_uuid: groups(:afolder).uuid,
+ head_uuid: specimens(:owned_by_active_user).uuid,
+ link_class: 'name',
+ name: 'foo')
+ end
+ end
+
+ test 'name links with different tail_uuid need not be unique' do
+ a = Link.create!(tail_uuid: groups(:afolder).uuid,
+ head_uuid: specimens(:owned_by_active_user).uuid,
+ link_class: 'name',
+ name: 'foo')
+ assert a.valid?, a.errors.to_s
+ b = Link.create!(tail_uuid: groups(:asubfolder).uuid,
+ head_uuid: specimens(:owned_by_active_user).uuid,
+ link_class: 'name',
+ name: 'foo')
+ assert b.valid?, b.errors.to_s
+ assert_not_equal(a.uuid, b.uuid,
+ "created two links and got the same uuid back.")
+ end
+
+ [nil, '', false].each do |name|
+ test "name links cannot have name=#{name.inspect}" do
+ a = Link.create(tail_uuid: groups(:afolder).uuid,
+ head_uuid: specimens(:owned_by_active_user).uuid,
+ link_class: 'name',
+ name: name)
+ assert a.invalid?, "invalid name was accepted as valid?"
+ end
+ end
end