$scroller = $(window);
$scroller.
addClass('infinite-scroller').
- on('scroll', { container: this }, maybe_load_more_content);
+ on('scroll resize', { container: this }, maybe_load_more_content).
+ trigger('scroll');
});
});
end
def load_filters_and_paging_params
+ @order = params[:order] || 'created_at desc'
+ @order = [@order] unless @order.is_a? Array
+
@limit ||= 200
if params[:limit]
@limit = params[:limit].to_i
respond_to do |f|
f.json { render json: @objects }
f.html {
- if params['tab_pane']
- comparable = self.respond_to? :compare
- render(partial: 'show_' + params['tab_pane'].downcase,
- locals: { comparable: comparable, objects: @objects })
+ if params[:tab_pane]
+ render_pane params[:tab_pane]
else
render
end
end
end
+ helper_method :render_pane
+ def render_pane tab_pane, opts={}
+ render_opts = {
+ partial: 'show_' + tab_pane.downcase,
+ locals: {
+ comparable: self.respond_to?(:compare),
+ objects: @objects,
+ tab_pane: tab_pane
+ }.merge(opts[:locals] || {})
+ }
+ if opts[:to_string]
+ render_to_string render_opts
+ else
+ render render_opts
+ end
+ end
+
def index
find_objects_for_index if !@objects
render_index
f.json { render json: @object.attributes.merge(href: url_for(@object)) }
f.html {
if params['tab_pane']
- comparable = self.respond_to? :compare
- render(partial: 'show_' + params['tab_pane'].downcase,
- locals: { comparable: comparable, objects: @objects })
+ render_pane params['tab_pane']
elsif request.method.in? ['GET', 'HEAD']
render
else
super
end
+ def load_contents_objects kinds=[]
+ kind_filters = @filters.select do |attr,op,val|
+ op == 'is_a' and val.is_a? Array and val.count > 1
+ end
+ if /^created_at\b/ =~ @order[0] and kind_filters.count == 1
+ # If filtering on multiple types and sorting by date: Get the
+ # first page of each type, sort the entire set, truncate to one
+ # page, and use the last item on this page as a filter for
+ # retrieving the next page. Ideally the API would do this for
+ # us, but it doesn't (yet).
+ nextpage_operator = /\bdesc$/i =~ @order[0] ? '<' : '>'
+ @objects = []
+ @name_link_for = {}
+ kind_filters.each do |attr,op,val|
+ (val.is_a?(Array) ? val : [val]).each do |type|
+ objects = @object.contents(order: @order,
+ limit: @limit,
+ include_linked: true,
+ filters: (@filters - kind_filters + [['uuid', 'is_a', type]]),
+ offset: @offset)
+ objects.each do |object|
+ @name_link_for[object.andand.uuid] = objects.links_for(object, 'name').first
+ end
+ @objects += objects
+ end
+ end
+ @objects = @objects.to_a.sort_by(&:created_at)
+ @objects.reverse! if nextpage_operator == '<'
+ @objects = @objects[0..@limit-1]
+ @next_page_filters = @filters.reject do |attr,op,val|
+ attr == 'created_at' and op == nextpage_operator
+ end
+ if @objects.any?
+ @next_page_filters += [['created_at',
+ nextpage_operator,
+ @objects.last.created_at]]
+ @next_page_href = url_for(partial: :contents_rows,
+ filters: @next_page_filters.to_json)
+ else
+ @next_page_href = nil
+ end
+ else
+ @objects = @object.contents(order: @order,
+ limit: @limit,
+ include_linked: true,
+ filters: @filters,
+ offset: @offset)
+ @next_page_href = next_page_href(partial: :contents_rows)
+ end
+ end
+
def show
if !@object
return render_not_found("object not found")
end
- @objects = @object.contents(limit: 50,
- include_linked: true,
- filters: params[:filters],
- offset: params[:offset] || 0)
- @logs = Log.limit(10).filter([['object_uuid', '=', @object.uuid]])
- @users = User.limit(10000).
- select(["uuid", "is_active", "first_name", "last_name"]).
- filter([['is_active', '=', 'true']])
- @groups = Group.limit(10000).
- select(["uuid", "name", "description"])
@user_is_manager = false
@share_links = []
end
end
- @objects_and_names = get_objects_and_names @objects
-
if params[:partial]
+ load_contents_objects
respond_to do |f|
f.json {
render json: {
content: render_to_string(partial: 'show_contents_rows.html',
- formats: [:html],
- locals: {
- objects_and_names: @objects_and_names,
- project: @object
- }),
- next_page_href: (next_page_offset and
- url_for(offset: next_page_offset, filters: params[:filters], partial: true))
+ formats: [:html]),
+ next_page_href: @next_page_href
}
}
end
else
+ @objects = []
super
end
end
end
helper_method :get_objects_and_names
- def get_objects_and_names(objects)
+ def get_objects_and_names(objects=nil)
+ objects = @objects if objects.nil?
objects_and_names = []
objects.each do |object|
- if !(name_links = objects.links_for(object, 'name')).empty?
+ if objects.respond_to? :links_for and
+ !(name_links = objects.links_for(object, 'name')).empty?
name_links.each do |name_link|
objects_and_names << [object, name_link]
end
+ elsif @name_link_for.andand[object.uuid]
+ objects_and_names << [object, @name_link_for[object.uuid]]
elsif object.respond_to? :name
objects_and_names << [object, object]
else
<div id="<%= pane %>-scroll" style="margin-top:0.5em;">
<div class="pane-content">
<% if i == 0 %>
- <%= render(partial: 'show_' + pane.downcase,
- locals: { comparable: comparable, objects: @objects }) %>
+ <%= render_pane pane, to_string: true %>
<% else %>
<div class="spinner spinner-32px spinner-h-center"></div>
<% end %>
<%= content_for :content_top %>
- <% if @object and @object.is_a?(Group) and @object.group_class == 'project' %>
- <div class="pull-right">
- <%= content_for :tab_line_buttons %>
- </div>
- <br clear="all" />
-<% else %>
- <br clear="all" />
- <div class="pull-right">
- <%= content_for :tab_line_buttons %>
- </div>
-<% end %>
+<div class="pull-right">
+ <%= content_for :tab_line_buttons %>
+</div>
+<br clear="all" />
<%= content_for :tab_panes %>
<% end %>
<%= render partial: 'content', layout: 'content_layout', locals: {pane_list: controller.show_pane_list }%>
-
-<% content_for :tab_line_buttons do %>
-<span style="padding-left: 1em">Collection storage status:</span>
-<%= render partial: 'toggle_persist', locals: { uuid: @object.uuid, current_state: (@is_persistent ? 'persistent' : 'cache') } %>
-<% end %>
-
<% file_tree = @object.andand.files_tree %>
<% if file_tree.nil? or file_tree.empty? %>
<p>This collection is empty.</p>
-<% content_for :tab_line_buttons do %>
- <%= form_tag collections_path, method: 'get', remote: true, class: 'form-search' do %>
- <div class="input-group">
- <%= text_field_tag :search, params[:search], class: 'form-control', placeholder: 'Search collections' %>
- <span class="input-group-btn">
- <%= button_tag(class: 'btn btn-info') do %>
- <span class="glyphicon glyphicon-search"></span>
- <% end %>
- </span>
- </div>
- <% end %>
-<% end %>
-
<%= render partial: "paging", locals: {results: @collections, object: @object} %>
<div style="padding-right: 1em">
--- /dev/null
+<% content_for :tab_line_buttons do %>
+ <%= form_tag collections_path, method: 'get', remote: true, class: 'form-search' do %>
+ <div class="input-group">
+ <%= text_field_tag :search, params[:search], class: 'form-control', placeholder: 'Search collections' %>
+ <span class="input-group-btn">
+ <%= button_tag(class: 'btn btn-info') do %>
+ <span class="glyphicon glyphicon-search"></span>
+ <% end %>
+ </span>
+ </div>
+ <% end %>
+<% end %>
+
+<%= render file: 'application/index.html.erb', locals: local_assigns %>
</div>
</div>
-<%= render file: 'application/show.html.erb' %>
+<% content_for :tab_line_buttons do %>
+ <span style="padding-left: 1em">Collection storage status:</span>
+ <%= render partial: 'toggle_persist', locals: { uuid: @object.uuid, current_state: (@is_persistent ? 'persistent' : 'cache') } %>
+<% end %>
+
+<%= render file: 'application/show.html.erb', locals: local_assigns %>
<% end %>
<% end %>
<%= content_for :content_top %>
-<%= content_for :tab_line_buttons %>
+<div class="pull-right">
+ <%= content_for :tab_line_buttons %>
+</div>
<%= content_for :tab_panes %>
-<%= content_for :tab_line_buttons do %>
-<%= form_tag({action: 'compare', controller: params[:controller], method: 'get'}, {method: 'get', id: 'compare', class: 'pull-right small-form-margin'}) do |f| %>
- <%= submit_tag 'Compare 2 or 3 selected', {class: 'btn btn-primary', disabled: true, style: 'display: none'} %>
-
-<% end rescue nil %>
-<% end %>
-
<%= render partial: "paging", locals: {results: @objects, object: @object} %>
<%= form_tag do |f| %>
--- /dev/null
+<% content_for :tab_line_buttons do %>
+<%= form_tag({action: 'compare', controller: params[:controller], method: 'get'}, {method: 'get', id: 'compare', class: 'pull-right small-form-margin'}) do |f| %>
+ <%= submit_tag 'Compare 2 or 3 selected', {class: 'btn btn-primary', disabled: true, style: 'display: none'} %>
+
+<% end rescue nil %>
+<% end %>
+
+<%= render file: 'application/index.html.erb', locals: local_assigns %>
-<% content_for :tab_line_buttons do %>
- <%= button_to(choose_projects_path(id: "run-pipeline-button",
- title: 'Choose project',
- editable: true,
- action_name: 'Choose',
- action_href: pipeline_instances_path,
- action_method: 'post',
- action_data: {selection_param: 'pipeline_instance[owner_uuid]',
- 'pipeline_instance[pipeline_template_uuid]' => @object.uuid,
- 'success' => 'redirect-to-created-object'
- }.to_json),
- { class: "btn btn-primary btn-sm", remote: true, method: 'get' }
- ) do %>
- Run this pipeline
- <% end %>
-<% end %>
-
<%= render_pipeline_components("editable", :json, editable: false) %>
--- /dev/null
+<% content_for :tab_line_buttons do %>
+ <%= button_to(choose_projects_path(id: "run-pipeline-button",
+ title: 'Choose project',
+ editable: true,
+ action_name: 'Choose',
+ action_href: pipeline_instances_path,
+ action_method: 'post',
+ action_data: {selection_param: 'pipeline_instance[owner_uuid]',
+ 'pipeline_instance[pipeline_template_uuid]' => @object.uuid,
+ 'success' => 'redirect-to-created-object'
+ }.to_json),
+ { class: "btn btn-primary btn-sm", remote: true, method: 'get' }
+ ) do %>
+ Run this pipeline
+ <% end %>
+<% end %>
+
+<%= render file: 'application/show.html.erb', locals: local_assigns %>
-<% objects_and_names.each do |object, name_link| %>
+<% get_objects_and_names.each do |object, name_link| %>
<% name_object = (object.respond_to?(:name) || !name_link) ? object : name_link %>
<tr class="filterable"
data-object-uuid="<%= name_object.uuid %>"
data-kind="<%= object.kind %>"
+ data-object-created-at="<%= object.created_at %>"
>
<td>
- <%= render partial: 'selection_checkbox', locals: {object: name_object, friendly_name: ((name_object.name rescue '') || '')} %>
+ <div style="width:1em; display:inline-block;">
+ <%= render partial: 'selection_checkbox', locals: {object: name_object, friendly_name: ((name_object.name rescue '') || '')} %>
+ </div>
- <% if project.editable? %>
- <%= link_to({action: 'remove_item', id: project.uuid, item_uuid: ((name_link && name_link.uuid) || object.uuid)}, method: :delete, remote: true, data: {confirm: "Remove #{object.class_for_display.downcase} #{name_object.name rescue object.uuid} from this project?", toggle: 'tooltip', placement: 'top'}, class: 'btn btn-sm btn-default btn-nodecorate', title: 'remove') do %>
+ <% 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: "Remove #{object.class_for_display.downcase} #{name_object.name rescue object.uuid} from this project?", toggle: 'tooltip', placement: 'top'}, class: 'btn btn-sm btn-default btn-nodecorate', title: 'remove') do %>
<i class="fa fa-fw fa-trash-o"></i>
<% end %>
<% else %>
-<% if @object.uuid != current_user.uuid # Not the "Home" project %>
-<% content_for :content_top do %>
-
-<h2>
- <%= render_editable_attribute @object, 'name', nil, { 'data-emptytext' => "New project" } %>
-</h2>
-
-<div class="arv-description-as-subtitle">
- <%= render_editable_attribute @object, 'description', nil, { 'data-emptytext' => "(No description provided)", 'data-toggle' => 'manual' } %>
-</div>
-
-<% end %>
-<% end %>
-
-<% content_for :tab_line_buttons do %>
- <% if @object.editable? %>
- <%= link_to(
- choose_collections_path(
- title: 'Add data to project:',
- multiple: true,
- action_name: 'Add',
- action_href: actions_path(id: @object.uuid),
- action_method: 'post',
- action_data: {selection_param: 'selection[]', copy_selections_into_project: @object.uuid, success: 'page-refresh'}.to_json),
- { class: "btn btn-primary btn-sm", remote: true, method: 'get', data: {'event-after-select' => 'page-refresh'} }) do %>
- <i class="fa fa-fw fa-plus"></i> Add data...
- <% end %>
- <%= link_to(
- choose_pipeline_templates_path(
- title: 'Choose a pipeline to run:',
- action_name: 'Next: choose inputs <i class="fa fa-fw fa-arrow-circle-right"></i>',
- action_href: pipeline_instances_path,
- action_method: 'post',
- action_data: {'selection_param' => 'pipeline_instance[pipeline_template_uuid]', 'pipeline_instance[owner_uuid]' => @object.uuid, 'success' => 'redirect-to-created-object'}.to_json),
- { class: "btn btn-primary btn-sm", remote: true, method: 'get' }) do %>
- <i class="fa fa-fw fa-gear"></i> Run a pipeline...
- <% end %>
- <%= link_to projects_path('project[owner_uuid]' => @object.uuid), method: 'post', class: 'btn btn-sm btn-primary' do %>
- <i class="fa fa-fw fa-plus"></i>
- Add a subproject
- <% end %>
- <% if @object.uuid != current_user.uuid # Not the "Home" project %>
- <%= link_to(
- choose_projects_path(
- title: 'Move this project to...',
- editable: true,
- my_root_selectable: true,
- action_name: 'Move',
- action_href: project_path(@object.uuid),
- action_method: 'put',
- action_data: {selection_param: 'project[owner_uuid]', success: 'page-refresh'}.to_json),
- { class: "btn btn-sm btn-primary arv-move-to-project", remote: true, method: 'get' }) do %>
- <i class="fa fa-fw fa-truck"></i> Move project...
- <% end %>
- <%= link_to(project_path(id: @object.uuid), method: 'delete', class: 'btn btn-sm btn-primary', data: {confirm: "Really delete project '#{@object.name}'?"}) do %>
- <i class="fa fa-fw fa-trash-o"></i> Delete project
- <% end %>
- <% end %>
- <% end %>
-<% end %>
-
-<%
- filters = [['uuid', 'is_a', "arvados#collection"]]
- @objects = @object.contents({limit: 50, include_linked: true, :filters => filters})
- objects_and_names = get_objects_and_names @objects
- page_offset = next_page_offset @objects
-%>
-
-<%= render partial: 'show_tab_contents', locals: {project: @object, objects_and_names: objects_and_names, filters: filters, page_offset: page_offset, tab_name: 'Data_collections'} %>
+<%= render_pane 'tab_contents', to_string: true, locals: {
+ filters: [['uuid', 'is_a', "arvados#collection"]]
+ }.merge(local_assigns) %>
-<%
- filters = [['uuid', 'is_a', ["arvados#pipelineInstance","arvados#job"]]]
- @objects = @object.contents({limit: 50, include_linked: true, :filters => filters})
- objects_and_names = get_objects_and_names @objects
- page_offset = next_page_offset @objects
-%>
-
-<%= render partial: 'show_tab_contents', locals: {project: @object, objects_and_names: objects_and_names, filters: filters, page_offset: page_offset, tab_name: 'Jobs_and_pipelines'} %>
+<%= render_pane 'tab_contents', to_string: true, locals: {
+ filters: [['uuid', 'is_a', ["arvados#job", "arvados#pipelineInstance"]]]
+ }.merge(local_assigns) %>
-<%
- filters = [['uuid', 'is_a', ["arvados#human","arvados#specimen","arvados#trait"]]]
- @objects = @object.contents({limit: 50, include_linked: true, :filters => filters})
- objects_and_names = get_objects_and_names @objects
- page_offset = next_page_offset @objects
-%>
-
-<%= render partial: 'show_tab_contents', locals: {project: @object, objects_and_names: objects_and_names, filters: filters, page_offset: page_offset, tab_name: 'Other_objects'} %>
+<%= render_pane 'tab_contents', to_string: true, locals: {
+ filters: [['uuid', 'is_a', ["arvados#human", "arvados#specimen", "arvados#trait"]]]
+ }.merge(local_assigns) %>
-<%
- filters = [['uuid', 'is_a', "arvados#pipelineTemplate"]]
- @objects = @object.contents({limit: 50, include_linked: true, :filters => filters})
- objects_and_names = get_objects_and_names @objects
- page_offset = next_page_offset @objects
-%>
-
-<%= render partial: 'show_tab_contents', locals: {project: @object, objects_and_names: objects_and_names, filters: filters, page_offset: page_offset, tab_name: 'Pipeline_templates'} %>
+<%= render_pane 'tab_contents', to_string: true, locals: {
+ filters: [['uuid', 'is_a', ["arvados#pipelineTemplate"]]]
+ }.merge(local_assigns) %>
<%
uuid_map = {}
- [@users, @groups].each do |obj_list|
- obj_list.each { |o| uuid_map[o.uuid] = o }
+ if @share_links
+ [User, Group].each do |type|
+ type.limit(10000)
+ .filter([['uuid','in',@share_links.collect(&:tail_uuid)]])
+ .each do |o|
+ uuid_map[o.uuid] = o
+ end
+ end
end
perm_name_desc_map = {}
perm_desc_name_map = {}
-<%
- filters = [['uuid', 'is_a', "arvados#group"]]
- @objects = @object.contents({limit: 50, include_linked: true, :filters => filters})
- objects_and_names = get_objects_and_names @objects
- page_offset = next_page_offset @objects
-%>
-
-<%= render partial: 'show_tab_contents', locals: {project: @object, objects_and_names: objects_and_names, filters: filters, page_offset: page_offset, tab_name: 'Subprojects'} %>
+<%= render_pane 'tab_contents', to_string: true, locals: {
+ filters: [['uuid', 'is_a', ["arvados#group"]]]
+ }.merge(local_assigns) %>
<div class="row">
<div class="col-sm-5">
<div class="btn-group btn-group-sm">
- <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">Selection... <i class="fa fa-fw fa-long-arrow-down "></i></button>
+ <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">Selection... <span class="caret"></span></button>
<ul class="dropdown-menu" role="menu">
<li><%= link_to "Compare selected", '#',
'data-href' => compare_pipeline_instances_path,
</div>
</div>
<div class="col-sm-4 pull-right">
- <input type="text" class="form-control filterable-control" placeholder="Search project contents" data-filterable-target="table.arv-index.arv-project-<%= tab_name %> tbody"/>
+ <input type="text" class="form-control filterable-control" placeholder="Search project contents" data-filterable-target="table.arv-index.arv-project-<%= tab_pane %> tbody"/>
</div>
</div>
- <table class="table table-condensed table-fixedlayout arv-index arv-project-<%= tab_name %>" style="overflow-x: hidden">
+ <table class="table table-condensed table-fixedlayout arv-index arv-project-<%= tab_pane %>" style="overflow-x: hidden">
<colgroup>
<col width="40%" />
<col width="60%" />
</colgroup>
- <tbody data-infinite-scroller="#<%= tab_name %>-scroll" data-infinite-content-href="<%= url_for(format: :json, partial: :contents_rows, offset: page_offset, filters: "#{filters}") if page_offset %>">
- <%= render partial: 'show_contents_rows', locals: {project: @object, objects_and_names: objects_and_names} %>
+ <tbody data-infinite-scroller="#<%= tab_pane %>-scroll" data-infinite-content-href="<%= url_for partial: :contents_rows, filters: filters.to_json %>">
</tbody>
<thead>
<tr>
</tr>
</thead>
</table>
-
</div>
--- /dev/null
+<% if @object.uuid != current_user.uuid # Not the "Home" project %>
+<% content_for :content_top do %>
+
+<h2>
+ <%= render_editable_attribute @object, 'name', nil, { 'data-emptytext' => "New project" } %>
+</h2>
+
+<div class="arv-description-as-subtitle">
+ <%= render_editable_attribute @object, 'description', nil, { 'data-emptytext' => "(No description provided)", 'data-toggle' => 'manual' } %>
+</div>
+
+<% end %>
+<% end %>
+
+<% content_for :tab_line_buttons do %>
+ <% if @object.editable? %>
+ <%= link_to(
+ choose_collections_path(
+ title: 'Add data to project:',
+ multiple: true,
+ action_name: 'Add',
+ action_href: actions_path(id: @object.uuid),
+ action_method: 'post',
+ action_data: {selection_param: 'selection[]', copy_selections_into_project: @object.uuid, success: 'page-refresh'}.to_json),
+ { class: "btn btn-primary btn-sm", remote: true, method: 'get', data: {'event-after-select' => 'page-refresh'} }) do %>
+ <i class="fa fa-fw fa-plus"></i> Add data...
+ <% end %>
+ <%= link_to(
+ choose_pipeline_templates_path(
+ title: 'Choose a pipeline to run:',
+ action_name: 'Next: choose inputs <i class="fa fa-fw fa-arrow-circle-right"></i>',
+ action_href: pipeline_instances_path,
+ action_method: 'post',
+ action_data: {'selection_param' => 'pipeline_instance[pipeline_template_uuid]', 'pipeline_instance[owner_uuid]' => @object.uuid, 'success' => 'redirect-to-created-object'}.to_json),
+ { class: "btn btn-primary btn-sm", remote: true, method: 'get' }) do %>
+ <i class="fa fa-fw fa-gear"></i> Run a pipeline...
+ <% end %>
+ <%= link_to projects_path('project[owner_uuid]' => @object.uuid), method: 'post', class: 'btn btn-sm btn-primary' do %>
+ <i class="fa fa-fw fa-plus"></i>
+ Add a subproject
+ <% end %>
+ <% if @object.uuid != current_user.uuid # Not the "Home" project %>
+ <%= link_to(
+ choose_projects_path(
+ title: 'Move this project to...',
+ editable: true,
+ my_root_selectable: true,
+ action_name: 'Move',
+ action_href: project_path(@object.uuid),
+ action_method: 'put',
+ action_data: {selection_param: 'project[owner_uuid]', success: 'page-refresh'}.to_json),
+ { class: "btn btn-sm btn-primary arv-move-to-project", remote: true, method: 'get' }) do %>
+ <i class="fa fa-fw fa-truck"></i> Move project...
+ <% end %>
+ <%= link_to(project_path(id: @object.uuid), method: 'delete', class: 'btn btn-sm btn-primary', data: {confirm: "Really delete project '#{@object.name}'?"}) do %>
+ <i class="fa fa-fw fa-trash-o"></i> Delete project
+ <% end %>
+ <% end %>
+ <% end %>
+<% end %>
+
+<%= render file: 'application/show.html.erb', locals: local_assigns %>
format: "json"},
session_for(:active))
assert_response :success
- json_response = Oj.load(@response.body)
assert_equal(uuid_list, json_response["success"])
end
format: "json"},
session_for(:project_viewer))
assert_response 422
- json_response = Oj.load(@response.body)
assert(json_response["errors"].andand.
any? { |msg| msg.start_with?("#{share_uuid}: ") },
"JSON response missing properly formatted sharing error")
test "viewer can't manage asubproject" do
refute user_can_manage(:project_viewer, "asubproject")
end
+
+ test 'projects#show tab infinite scroll partial obeys limit' do
+ get_contents_rows(limit: 1, filters: [['uuid','is_a',['arvados#job']]])
+ assert_response :success
+ assert_equal(1, json_response['content'].scan('<tr').count,
+ "Did not get exactly one row")
+ end
+
+ ['', ' asc', ' desc'].each do |direction|
+ test "projects#show tab partial orders correctly by #{direction}" do
+ _test_tab_content_order direction
+ end
+ end
+
+ def _test_tab_content_order direction
+ get_contents_rows(limit: 100,
+ order: "created_at#{direction}",
+ filters: [['uuid','is_a',['arvados#job',
+ 'arvados#pipelineInstance']]])
+ assert_response :success
+ not_grouped_by_kind = nil
+ last_timestamp = nil
+ last_kind = nil
+ found_kind = {}
+ json_response['content'].scan /<tr[^>]+>/ do |tr_tag|
+ found_timestamps = 0
+ tr_tag.scan(/\ data-object-created-at=\"(.*?)\"/).each do |t,|
+ if last_timestamp
+ correct_operator = / desc$/ =~ direction ? :>= : :<=
+ assert_operator(last_timestamp, correct_operator, t,
+ "Rows are not sorted by created_at#{direction}")
+ end
+ last_timestamp = t
+ found_timestamps += 1
+ end
+ assert_equal(1, found_timestamps,
+ "Content row did not have exactly one timestamp")
+
+ # Confirm that the test for timestamp ordering couldn't have
+ # passed merely because the test fixtures have convenient
+ # timestamps (e.g., there is only one pipeline and one job in
+ # the project being tested, or there are no pipelines at all in
+ # the project being tested):
+ tr_tag.scan /\ data-kind=\"(.*?)\"/ do |kind|
+ if last_kind and last_kind != kind and found_kind[kind]
+ # We saw this kind before, then a different kind, then
+ # this kind again. That means objects are not grouped by
+ # kind.
+ not_grouped_by_kind = true
+ end
+ found_kind[kind] ||= 0
+ found_kind[kind] += 1
+ last_kind = kind
+ end
+ end
+ assert_equal(true, not_grouped_by_kind,
+ "Could not confirm that results are not grouped by kind")
+ end
+
+ def get_contents_rows params
+ params = {
+ id: api_fixture('users')['active']['uuid'],
+ partial: :contents_rows,
+ format: :json,
+ }.merge(params)
+ encoded_params = Hash[params.map { |k,v|
+ [k, (v.is_a?(Array) || v.is_a?(Hash)) ? v.to_json : v]
+ }]
+ get :show, encoded_params, session_for(:active)
+ end
end
specimen_uuid = api_fixture('specimens')['owned_by_aproject_with_no_name_link']['uuid']
visit page_with_token 'active', '/projects/' + project_uuid
click_link 'Other objects'
- within(".selection-action-container") do
- within (first('tr', text: 'Specimen')) do
+ within '.selection-action-container' do
+ # Wait for the tab to load:
+ assert_selector 'tr[data-kind="arvados#specimen"]'
+ within first('tr', text: 'Specimen') do
find(".fa-pencil").click
find('.editable-input input').set('Now I have a name.')
find('.glyphicon-ok').click
- find('.editable', text: 'Now I have a name.').click
+ assert_selector '.editable', text: 'Now I have a name.'
find(".fa-pencil").click
find('.editable-input input').set('Now I have a new name.')
find('.glyphicon-ok').click
- end
+ end
wait_for_ajax
- find('.editable', text: 'Now I have a new name.')
+ assert_selector '.editable', text: 'Now I have a new name.'
end
visit current_path
click_link 'Other objects'
arvados_api_token: api_fixture('api_client_authorizations')[api_client_auth_name.to_s]['api_token']
}
end
+ def json_response
+ Oj.load(@response.body)
+ end
end
class ApiServerForTests
#!/usr/bin/env python
+# collection-merge
+#
+# Merge two or more collections together. Can also be used to extract specific
+# files from a collection to produce a new collection.
+#
+# input:
+# An array of collections or collection/file paths in script_parameter["input"]
+#
+# output:
+# A manifest with the collections merged. Duplicate file names will
+# have their contents concatenated in the order that they appear in the input
+# array.
+
import arvados
import md5
import subst
if fn in s.files():
merged += s.files()[fn].as_manifest()
-crm = arvados.CollectionReader(merged)
-
-combined = crm.manifest_text(strip=True)
-
-m = hashlib.new('md5')
-m.update(combined)
-
-uuid = "{}+{}".format(m.hexdigest(), len(combined))
-
-collection = arvados.api().collections().create(
- body={
- 'uuid': uuid,
- 'manifest_text': crm.manifest_text(),
- }).execute()
-
-for s in src:
- l = arvados.api().links().create(body={
- "link": {
- "tail_uuid": s,
- "head_uuid": uuid,
- "link_class": "provenance",
- "name": "provided"
- }}).execute()
-
-arvados.current_task().set_output(uuid)
+arvados.current_task().set_output(merged)
--- /dev/null
+#!/usr/bin/env python
+
+#
+# decompress-all.py
+#
+# Decompress all compressed files in the collection using the "dtrx" tool and
+# produce a new collection with the contents. Uncompressed files
+# are passed through.
+#
+# input:
+# A collection at script_parameters["input"]
+#
+# output:
+# A manifest of the uncompressed contents of the input collection.
+
+import arvados
+import re
+import subprocess
+import os
+import sys
+
+arvados.job_setup.one_task_per_input_file(if_sequence=0, and_end_task=True,
+ input_as_path=True)
+
+task = arvados.current_task()
+
+input_file = task['parameters']['input']
+
+infile_parts = re.match(r"(^[a-f0-9]{32}\+\d+)(\+\S+)*(/.*)?(/[^/]+)$", input_file)
+
+outdir = os.path.join(task.tmpdir, "output")
+os.makedirs(outdir)
+os.chdir(outdir)
+
+if infile_parts == None:
+ print >>sys.stderr, "Failed to parse input filename '%s' as a Keep file\n" % input_file
+ sys.exit(1)
+
+cr = arvados.CollectionReader(infile_parts.group(1))
+streamname = infile_parts.group(3)[1:]
+filename = infile_parts.group(4)[1:]
+
+if streamname != None:
+ subprocess.call(["mkdir", "-p", streamname])
+ os.chdir(streamname)
+else:
+ streamname = '.'
+
+m = re.match(r'.*\.(gz|Z|bz2|tgz|tbz|zip|rar|7z|cab|deb|rpm|cpio|gem)$', arvados.get_task_param_mount('input'), re.IGNORECASE)
+
+if m != None:
+ rc = subprocess.call(["dtrx", "-r", "-n", "-q", arvados.get_task_param_mount('input')])
+ if rc == 0:
+ out = arvados.CollectionWriter()
+ out.write_directory_tree(outdir, max_manifest_depth=0)
+ task.set_output(out.finish())
+ else:
+ sys.exit(rc)
+else:
+ streamreader = filter(lambda s: s.name() == streamname, cr.all_streams())[0]
+ filereader = streamreader.files()[filename]
+ task.set_output(streamname + filereader.as_manifest()[1:])
import time
import arvados.commands.put as put
import signal
+import stat
+import copy
+import traceback
+import pprint
+import multiprocessing
+import logging
os.umask(0077)
+logging.basicConfig(format="run-command: %(message)s")
t = arvados.current_task().tmpdir
os.chdir("output")
+outdir = os.getcwd()
+
+taskp = None
+jobp = arvados.current_job()['script_parameters']
if len(arvados.current_task()['parameters']) > 0:
- p = arvados.current_task()['parameters']
-else:
- p = arvados.current_job()['script_parameters']
+ taskp = arvados.current_task()['parameters']
links = []
def sub_link(v):
- r = os.path.basename(v)
- os.symlink(os.path.join(os.environ['TASK_KEEPMOUNT'], v) , r)
+ r = os.path.join(outdir, os.path.basename(v))
+ os.symlink(v, r)
links.append(r)
return r
def sub_tmpdir(v):
return os.path.join(arvados.current_task().tmpdir, 'tmpdir')
+def sub_outdir(v):
+ return outdir
+
def sub_cores(v):
- return os.environ['CRUNCH_NODE_SLOTS']
+ return str(multiprocessing.cpu_count())
+
+def sub_jobid(v):
+ return os.environ['JOB_UUID']
+
+def sub_taskid(v):
+ return os.environ['TASK_UUID']
+
+def sub_jobsrc(v):
+ return os.environ['CRUNCH_SRC']
subst.default_subs["link "] = sub_link
-subst.default_subs["tmpdir"] = sub_tmpdir
+subst.default_subs["task.tmpdir"] = sub_tmpdir
+subst.default_subs["task.outdir"] = sub_outdir
+subst.default_subs["job.srcdir"] = sub_jobsrc
subst.default_subs["node.cores"] = sub_cores
-
-rcode = 1
+subst.default_subs["job.uuid"] = sub_jobid
+subst.default_subs["task.uuid"] = sub_taskid
def machine_progress(bytes_written, bytes_expected):
return "run-command: wrote {} total {}\n".format(
sp.send_signal(signum)
self.sig = signum
+def expand_item(p, c):
+ if isinstance(c, dict):
+ if "foreach" in c and "command" in c:
+ var = c["foreach"]
+ items = get_items(p, p[var])
+ r = []
+ for i in items:
+ params = copy.copy(p)
+ params[var] = i
+ r.extend(expand_list(params, c["command"]))
+ return r
+ elif isinstance(c, list):
+ return expand_list(p, c)
+ elif isinstance(c, str) or isinstance(c, unicode):
+ return [subst.do_substitution(p, c)]
+
+ return []
+
+def expand_list(p, l):
+ return [exp for arg in l for exp in expand_item(p, arg)]
+
+def get_items(p, value):
+ if isinstance(value, list):
+ return expand_list(p, value)
+
+ fn = subst.do_substitution(p, value)
+ mode = os.stat(fn).st_mode
+ prefix = fn[len(os.environ['TASK_KEEPMOUNT'])+1:]
+ if mode != None:
+ if stat.S_ISDIR(mode):
+ items = ["$(dir %s/%s/)" % (prefix, l) for l in os.listdir(fn)]
+ elif stat.S_ISREG(mode):
+ with open(fn) as f:
+ items = [line for line in f]
+ return items
+ else:
+ return None
+
+stdoutname = None
+stdoutfile = None
+rcode = 1
+
try:
- cmd = []
- for c in p["command"]:
- cmd.append(subst.do_substitution(p, c))
-
- stdoutname = None
- stdoutfile = None
- if "stdout" in p:
- stdoutname = subst.do_substitution(p, p["stdout"])
+ if "task.foreach" in jobp:
+ if arvados.current_task()['sequence'] == 0:
+ var = jobp["task.foreach"]
+ items = get_items(jobp, jobp[var])
+ logging.info("parallelizing on %s with items %s" % (var, items))
+ if items != None:
+ for i in items:
+ params = copy.copy(jobp)
+ params[var] = i
+ arvados.api().job_tasks().create(body={
+ 'job_uuid': arvados.current_job()['uuid'],
+ 'created_by_job_task_uuid': arvados.current_task()['uuid'],
+ 'sequence': 1,
+ 'parameters': params
+ }
+ ).execute()
+ arvados.current_task().set_output(None)
+ sys.exit(0)
+ else:
+ sys.exit(1)
+ else:
+ taskp = jobp
+
+ cmd = expand_list(taskp, taskp["command"])
+
+ if "save.stdout" in taskp:
+ stdoutname = subst.do_substitution(taskp, taskp["save.stdout"])
stdoutfile = open(stdoutname, "wb")
- print("run-command: {}{}".format(' '.join(cmd), (" > " + stdoutname) if stdoutname != None else ""))
+ logging.info("{}{}".format(' '.join(cmd), (" > " + stdoutname) if stdoutname != None else ""))
+
+except Exception as e:
+ logging.exception("caught exception")
+ logging.error("task parameters was:")
+ logging.error(pprint.pformat(taskp))
+ sys.exit(1)
+try:
sp = subprocess.Popen(cmd, shell=False, stdout=stdoutfile)
sig = SigHandler()
rcode = sp.wait()
if sig.sig != None:
- print("run-command: terminating on signal %s" % sig.sig)
+ logging.critical("terminating on signal %s" % sig.sig)
sys.exit(2)
else:
- print("run-command: completed with exit code %i (%s)" % (rcode, "success" if rcode == 0 else "failed"))
+ logging.info("completed with exit code %i (%s)" % (rcode, "success" if rcode == 0 else "failed"))
except Exception as e:
- print("run-command: caught exception: {}".format(e))
+ logging.exception("caught exception")
# restore default signal handlers.
signal.signal(signal.SIGINT, signal.SIG_DFL)
for l in links:
os.unlink(l)
-print("run-command: the follow output files will be saved to keep:")
+logging.info("the following output files will be saved to keep:")
-subprocess.call(["find", ".", "-type", "f", "-printf", "run-command: %12.12s %h/%f\\n"])
+subprocess.call(["find", ".", "-type", "f", "-printf", "run-command: %12.12s %h/%f\\n"], stdout=sys.stderr)
-print("run-command: start writing output to keep")
+logging.info("start writing output to keep")
done = False
resume_cache = put.ResumeCache(os.path.join(arvados.current_task().tmpdir, "upload-output-checkpoint"))
}).execute()
done = True
except KeyboardInterrupt:
- print("run-command: terminating on signal 2")
+ logging.critical("terminating on signal 2")
sys.exit(2)
except Exception as e:
- print("run-command: caught exception: {}".format(e))
+ logging.exception("caught exception:")
time.sleep(5)
sys.exit(rcode)
--- /dev/null
+#!/usr/bin/python
+
+import arvados
+import re
+import hashlib
+import string
+
+api = arvados.api('v1')
+
+piece = 0
+manifest_text = ""
+
+# Look for paired reads
+
+inp = arvados.CollectionReader(arvados.getjobparam('reads'))
+
+manifest_list = []
+
+chunking = False #arvados.getjobparam('chunking')
+
+def nextline(reader, start):
+ n = -1
+ while True:
+ r = reader.readfrom(start, 128)
+ if r == '':
+ break
+ n = string.find(r, "\n")
+ if n > -1:
+ break
+ else:
+ start += 128
+ return n
+
+# Chunk a fastq into approximately 64 MiB chunks. Requires that the input data
+# be decompressed ahead of time, such as using decompress-all.py. Generates a
+# new manifest, but doesn't actually move any data around. Handles paired
+# reads by ensuring that each chunk of a pair gets the same number of records.
+#
+# This works, but in practice is so slow that potential gains in alignment
+# performance are lost in the prep time, which is why it is currently disabled.
+#
+# A better algorithm would seek to a file position a bit less than the desired
+# chunk size and then scan ahead for the next record, making sure that record
+# was matched by the read pair.
+def splitfastq(p):
+ for i in xrange(0, len(p)):
+ p[i]["start"] = 0
+ p[i]["end"] = 0
+
+ count = 0
+ recordsize = [0, 0]
+
+ global piece
+ finish = False
+ while not finish:
+ for i in xrange(0, len(p)):
+ recordsize[i] = 0
+
+ # read next 4 lines
+ for i in xrange(0, len(p)):
+ for ln in xrange(0, 4):
+ r = nextline(p[i]["reader"], p[i]["end"]+recordsize[i])
+ if r == -1:
+ finish = True
+ break
+ recordsize[i] += (r+1)
+
+ splitnow = finish
+ for i in xrange(0, len(p)):
+ if ((p[i]["end"] - p[i]["start"]) + recordsize[i]) >= (64*1024*1024):
+ splitnow = True
+
+ if splitnow:
+ for i in xrange(0, len(p)):
+ global manifest_list
+ print >>sys.stderr, "Finish piece ./_%s/%s (%s %s)" % (piece, p[i]["reader"].name(), p[i]["start"], p[i]["end"])
+ manifest = []
+ manifest.extend(["./_" + str(piece)])
+ manifest.extend([d[arvados.LOCATOR] for d in p[i]["reader"]._stream._data_locators])
+ manifest.extend(["{}:{}:{}".format(seg[arvados.LOCATOR]+seg[arvados.OFFSET], seg[arvados.SEGMENTSIZE], p[i]["reader"].name().replace(' ', '\\040')) for seg in arvados.locators_and_ranges(p[i]["reader"].segments, p[i]["start"], p[i]["end"] - p[i]["start"])])
+ manifest_list.append(manifest)
+ p[i]["start"] = p[i]["end"]
+ piece += 1
+ else:
+ for i in xrange(0, len(p)):
+ p[i]["end"] += recordsize[i]
+ count += 1
+ if count % 10000 == 0:
+ print >>sys.stderr, "Record %s at %s" % (count, p[i]["end"])
+
+prog = re.compile(r'(.*?)(_[12])?\.fastq(\.gz)?$')
+
+# Look for fastq files
+for s in inp.all_streams():
+ for f in s.all_files():
+ name_pieces = prog.match(f.name())
+ if name_pieces != None:
+ if s.name() != ".":
+ # The downstream tool (run-command) only iterates over the top
+ # level of directories so if there are fastq files in
+ # directories in the input, the choice is either to forget
+ # there are directories (which might lead to name conflicts) or
+ # just fail.
+ print >>sys.stderr, "fastq must be at the root of the collection"
+ sys.exit(1)
+
+ p = None
+ if name_pieces.group(2) != None:
+ if name_pieces.group(2) == "_1":
+ p = [{}, {}]
+ p[0]["reader"] = s.files()[name_pieces.group(0)]
+ p[1]["reader"] = s.files()[name_pieces.group(1) + "_2.fastq" + (name_pieces.group(3) if name_pieces.group(3) else '')]
+ else:
+ p = [{}]
+ p[0]["reader"] = s.files()[name_pieces.group(0)]
+
+ if p != None:
+ if chunking:
+ splitfastq(p)
+ else:
+ for i in xrange(0, len(p)):
+ m = p[i]["reader"].as_manifest()[1:]
+ manifest_list.append(["./_" + str(piece), m[:-1]])
+ piece += 1
+
+manifest_text = "\n".join(" ".join(m) for m in manifest_list)
+
+arvados.current_task().set_output(manifest_text)
return os.path.splitext(os.path.basename(v))[0]
def sub_glob(v):
- return glob.glob(v)[0]
+ l = glob.glob(v)
+ if len(l) == 0:
+ raise Exception("$(glob): No match on '%s'" % v)
+ else:
+ return l[0]
default_subs = {"file ": sub_file,
"dir ": sub_dir,
# Install dependencies and set up system.
# The FUSE packages help ensure that we can install the Python SDK (arv-mount).
RUN /usr/bin/apt-get install -q -y python-dev python-llfuse python-pip \
- libio-socket-ssl-perl libjson-perl liburi-perl libwww-perl \
+ libio-socket-ssl-perl libjson-perl liburi-perl libwww-perl dtrx \
fuse libattr1-dev libfuse-dev && \
/usr/sbin/adduser --disabled-password \
--gecos 'Crunch execution user' crunch && \
# Install Arvados packages.
RUN (find /usr/src/arvados/sdk -name '*.gem' -print0 | \
- xargs -0rn 1 gem install) && \
+ xargs -0rn 1 /usr/local/rvm/bin/rvm-exec default gem install) && \
cd /usr/src/arvados/services/fuse && \
python setup.py install && \
cd /usr/src/arvados/sdk/python && \
end
def self.create(attributes)
result = $client.execute(:api_method => $arvados.pipeline_instances.create,
- :body => {
- :pipeline_instance => attributes.to_json
+ :body_object => {
+ :pipeline_instance => attributes
},
:authenticated => false,
:headers => {
:parameters => {
:uuid => @pi[:uuid]
},
- :body => {
- :pipeline_instance => @attributes_to_update.to_json
+ :body_object => {
+ :pipeline_instance => @attributes_to_update
},
:authenticated => false,
:headers => {
body = {job: no_nil_values(job)}.merge(no_nil_values(create_params))
result = $client.execute(:api_method => $arvados.jobs.create,
- :body => body,
+ :body_object => body,
:authenticated => false,
:headers => {
authorization: 'OAuth2 '+ENV['ARVADOS_API_TOKEN']
msg += "Job submission was: #{body.to_json}"
$client.execute(:api_method => $arvados.logs.create,
- :body => {
+ :body_object => {
:log => {
:object_uuid => pipeline[:uuid],
:event_type => 'stderr',
if ArvadosModel::resource_class_for_uuid(perm.tail_uuid) == Group
@users.each do |user_uuid, user|
user.group_permissions.each do |group_uuid, perm_mask|
- if perm_mask[:write]
+ if perm_mask[:manage]
+ perms << {name: 'can_manage', user_uuid: user_uuid}
+ elsif perm_mask[:write]
perms << {name: 'can_write', user_uuid: user_uuid}
elsif perm_mask[:read]
perms << {name: 'can_read', user_uuid: user_uuid}
end
@repo_info.values.each do |repo_users|
repo_users[:user_permissions].each do |user_uuid,perms|
- if perms['can_write']
+ if perms['can_manage']
+ perms[:gitolite_permissions] = 'RW'
+ perms['can_write'] = true
+ perms['can_read'] = true
+ elsif perms['can_write']
perms[:gitolite_permissions] = 'RW'
perms['can_read'] = true
elsif perms['can_read']
if self.cancelled_at and not self.cancelled_at_was
self.cancelled_at = Time.now
self.cancelled_by_user_uuid = current_user.uuid
- self.cancelled_by_client_uuid = current_api_client.uuid
+ self.cancelled_by_client_uuid = current_api_client.andand.uuid
@need_crunch_dispatch_trigger = true
else
self.cancelled_at = self.cancelled_at_was
cancelled_at: ~
cancelled_by_user_uuid: ~
cancelled_by_client_uuid: ~
+ created_at: <%= 3.minute.ago.to_s(:db) %>
started_at: <%= 3.minute.ago.to_s(:db) %>
finished_at: ~
script_version: 1de84a854e2b440dc53bf42f8548afa4c17da332
cancelled_at: <%= 1.minute.ago.to_s(:db) %>
cancelled_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
cancelled_by_client_uuid: zzzzz-ozdt8-obw7foaks3qjyej
+ created_at: <%= 4.minute.ago.to_s(:db) %>
started_at: <%= 3.minute.ago.to_s(:db) %>
finished_at: ~
script_version: 1de84a854e2b440dc53bf42f8548afa4c17da332
cancelled_by_user_uuid: ~
cancelled_by_client_uuid: ~
script_version: 7def43a4d3f20789dda4700f703b5514cc3ed250
+ created_at: <%= 5.minute.ago.to_s(:db) %>
started_at: <%= 3.minute.ago.to_s(:db) %>
finished_at: <%= 2.minute.ago.to_s(:db) %>
running: false
script_version: 7def43a4d3f20789dda4700f703b5514cc3ed250
script_parameters:
input: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
+ created_at: <%= 4.minute.ago.to_s(:db) %>
started_at: <%= 3.minute.ago.to_s(:db) %>
finished_at: <%= 2.minute.ago.to_s(:db) %>
running: false
script_parameters:
input: fa7aeb5140e2848d39b416daeef4ffc5+45
an_integer: 1
+ created_at: <%= 4.minute.ago.to_s(:db) %>
started_at: <%= 3.minute.ago.to_s(:db) %>
finished_at: <%= 2.minute.ago.to_s(:db) %>
running: false
previous_job_run:
uuid: zzzzz-8i9sb-cjs4pklxxjykqqq
+ created_at: <%= 14.minute.ago.to_s(:db) %>
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
repository: foo
script: hash
previous_docker_job_run:
uuid: zzzzz-8i9sb-k6emstgk4kw4yhi
+ created_at: <%= 14.minute.ago.to_s(:db) %>
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
repository: foo
script: hash
previous_job_run_no_output:
uuid: zzzzz-8i9sb-cjs4pklxxjykppp
+ created_at: <%= 14.minute.ago.to_s(:db) %>
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
repository: foo
script: hash
nondeterminisic_job_run:
uuid: zzzzz-8i9sb-cjs4pklxxjykyyy
+ created_at: <%= 14.minute.ago.to_s(:db) %>
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
repository: foo
script: hash2
nearly_finished_job:
uuid: zzzzz-8i9sb-2gx6rz0pjl033w3
+ created_at: <%= 14.minute.ago.to_s(:db) %>
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
repository: arvados
script: doesnotexist
queued:
uuid: zzzzz-8i9sb-grx15v5mjnsyxk7
+ created_at: <%= 1.minute.ago.to_s(:db) %>
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
cancelled_at: ~
cancelled_by_user_uuid: ~
state: New
uuid: zzzzz-d1hrv-f4gneyn6br1xize
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ created_at: <%= 1.minute.ago.to_s(:db) %>
has_component_with_no_script_parameters:
state: Ready
uuid: zzzzz-d1hrv-1xfj6xkicf2muk2
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ created_at: <%= 10.minute.ago.to_s(:db) %>
components:
foo:
script: foo
state: Ready
uuid: zzzzz-d1hrv-jq16l10gcsnyumo
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ created_at: <%= 3.minute.ago.to_s(:db) %>
components:
foo:
script: foo
state: Ready
uuid: zzzzz-d1hrv-1yfj6xkidf2muk3
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ created_at: <%= 3.1.minute.ago.to_s(:db) %>
components:
foo:
script: foo
# Helps test that clients cope with funny-shaped components.
# For an example, see #3321.
uuid: zzzzz-d1hrv-jobspeccomponts
+ created_at: <%= 30.minute.ago.to_s(:db) %>
owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
created_at: 2014-04-14 12:35:04 -0400
updated_at: 2014-04-14 12:35:04 -0400