display: none;
}
+/* Hide Angular content until Angular is ready */
[ng\:cloak], [ng-cloak], .ng-cloak {
display: none !important;
}
+
+/* tabs */
+ul.nav.nav-tabs {
+ font-size: 90%
+}
def move_or_copy action
uuids_to_add = params["selection"]
uuids_to_add = [ uuids_to_add ] unless uuids_to_add.is_a? Array
- uuids_to_add.
+ resource_classes = uuids_to_add.
collect { |x| ArvadosBase::resource_class_for_uuid(x) }.
- uniq.
- each do |resource_class|
+ uniq
+ resource_classes.each do |resource_class|
resource_class.filter([['uuid','in',uuids_to_add]]).each do |src|
if resource_class == Collection and not Collection.attribute_info.include?(:name)
dst = Link.new(owner_uuid: @object.uuid,
end
end
end
- redirect_to @object
+ if (resource_classes == [Collection] and
+ @object.is_a? Group and
+ @object.group_class == 'project')
+ # In the common case where only collections are copied/moved
+ # into a project, it's polite to land on the collections tab on
+ # the destination project.
+ redirect_to project_url(@object.uuid, anchor: 'Data_collections')
+ else
+ # Otherwise just land on the default (Description) tab.
+ redirect_to @object
+ end
end
def arv_normalize mt, *opts
end
end
+ def redirect_to uri, *args
+ if request.xhr?
+ if not uri.is_a? String
+ uri = polymorphic_url(uri)
+ end
+ render json: {href: uri}
+ else
+ super
+ end
+ end
+
def choose
params[:limit] ||= 40
respond_to do |f|
# It also seems to me that something like these could be used to configure the contents of the panes.
def show_pane_list
pane_list = []
+ if @object.uuid != current_user.andand.uuid
+ pane_list << 'Description'
+ end
pane_list <<
{
:name => 'Data_collections',
"id" => span_id,
:class => "editable #{is_textile?( object, attr ) ? 'editable-textile' : ''}"
}.merge(htmloptions).merge(ajax_options)
- edit_button = raw('<a href="#" class="btn btn-xs btn-default btn-nodecorate" data-toggle="x-editable tooltip" data-toggle-selector="#' + span_id + '" data-placement="top" title="' + (htmloptions[:tiptitle] || 'edit') + '"><i class="fa fa-fw fa-pencil"></i></a>')
+ edit_tiptitle = 'edit'
+ edit_tiptitle = 'Warning: do not use hyphens in the repository name as they will be stripped' if (object.class.to_s == 'Repository' and attr == 'name')
+ edit_button = raw('<a href="#" class="btn btn-xs btn-default btn-nodecorate" data-toggle="x-editable tooltip" data-toggle-selector="#' + span_id + '" data-placement="top" title="' + (htmloptions[:tiptitle] || edit_tiptitle) + '"><i class="fa fa-fw fa-pencil"></i></a>')
if htmloptions[:btnplacement] == :left
edit_button + ' ' + span_tag
else
<%= render_editable_attribute @object, 'description', nil, { 'data-emptytext' => "(No description provided)", 'data-toggle' => 'manual' } %>
</div>
<% end %>
-
</div>
<div class="collection_files_name">
- <% if !defined? no_checkboxes or !no_checkboxes %>
+ <% if (!defined? no_checkboxes or !no_checkboxes) and current_user %>
<%= check_box_tag 'uuids[]', "#{@object.uuid}/#{file_path}", false, {
:class => "persistent-selection",
:friendly_type => "File",
--- /dev/null
+<% if @object.respond_to? :description %>
+ <div class="arv-description-as-subtitle">
+ <%= render_editable_attribute @object, 'description', nil, { 'data-emptytext' => "(No description provided)", 'data-toggle' => 'manual' } %>
+ </div>
+<% end %>
-<% if @object.uuid != current_user.andand.uuid # Not the "Home" project %>
<% content_for :content_top do %>
- <%= render partial: 'name_and_description' %>
-<% end %>
+ <h2>
+ <% if @object.uuid == current_user.andand.uuid %>
+ Home
+ <% else %>
+ <%= render_editable_attribute @object, 'name', nil, { 'data-emptytext' => "New project" } %>
+ <% end %>
+ </h2>
<% end %>
<% content_for :tab_line_buttons do %>
test "selection actions when anonymous user accesses shared project" do
visit PUBLIC_PROJECT
+ assert_selector 'a', text: 'Description'
assert_selector 'a', text: 'Data collections'
assert_selector 'a', text: 'Jobs and pipelines'
assert_selector 'a', text: 'Pipeline templates'
assert_no_selector 'a', text: 'Other objects'
assert_no_selector 'button', text: 'Add data'
+ click_link 'Data collections'
click_button 'Selection'
within('.selection-action-container') do
assert_selector 'li', text: 'Compare selected'
test "anonymous user accesses data collections tab in shared project" do
visit PUBLIC_PROJECT
+ click_link 'Data collections'
collection = api_fixture('collections')['user_agreement_in_anonymously_accessible_project']
assert_text 'GNU General Public License'
assert_no_selector 'button', 'Selection'
within '#collection_files tr,li', text: 'GNU_General_Public_License,_version_3.pdf' do
+ assert page.has_no_selector?('[value*="GNU_General_Public_License"]')
find 'a[title~=View]'
find 'a[title~=Download]'
end
].each do |type|
test "anonymous user accesses jobs and pipelines tab in shared project and clicks on #{type}" do
visit PUBLIC_PROJECT
+ click_link 'Data collections'
assert_text 'GNU General Public License'
click_link 'Jobs and pipelines'
test "anonymous user accesses pipeline templates tab in shared project" do
visit PUBLIC_PROJECT
+ click_link 'Data collections'
assert_text 'GNU General Public License'
assert_selector 'a', text: 'Pipeline templates'
click_link 'Copy to project...'
find('.selectable', text: project_name).click
find('.modal-footer a,button', text: 'Copy').click
- wait_for_ajax
- # It should navigate to the project after copying...
- assert(page.has_text?(project_name))
- assert(page.has_text?("Copy of #{collection_name}"))
+ # Should navigate to the Data collections tab of the project after copying
+ assert_text project_name
+ assert_text "Copy of #{collection_name}"
end
test "Collection page renders name" do
need_javascript
end
+ def parse_browser_timestamp t
+ # Timestamps are displayed in the browser's time zone (which can
+ # differ from ours) and they come from toLocaleTimeString (which
+ # means they don't necessarily tell us which time zone they're
+ # using). In order to make sense of them, we need to ask the
+ # browser to parse them and generate a timestamp that can be
+ # parsed reliably.
+ #
+ # Note: Even with all this help, phantomjs seem to behave badly
+ # when parsing timestamps on the other side of a DST transition.
+ # See skipped tests below.
+ if /(\d+:\d+ [AP]M) (\d+\/\d+\/\d+)/ =~ t
+ # Currently dates.js renders timestamps as
+ # '{t.toLocaleTimeString()} {t.toLocaleDateString()}' which even
+ # browsers can't make sense of. First we need to flip it around
+ # so it looks like what toLocaleString() would have made.
+ t = $~[2] + ', ' + $~[1]
+ end
+ DateTime.parse(page.evaluate_script "new Date('#{t}').toUTCString()").to_time
+ end
+
+ if false
+ # No need to test (or mention) these all the time. If they start
+ # working (without need_selenium) then some real tests might not
+ # need_selenium any more.
+
+ test 'phantomjs DST' do
+ skip '^^'
+ t0s = '3/8/2015, 01:59 AM'
+ t1s = '3/8/2015, 03:01 AM'
+ t0 = parse_browser_timestamp t0s
+ t1 = parse_browser_timestamp t1s
+ assert_equal 120, t1-t0, "'#{t0s}' to '#{t1s}' was reported as #{t1-t0} seconds, should be 120"
+ end
+
+ test 'phantomjs DST 2' do
+ skip '^^'
+ t0s = '2015-03-08T10:43:00Z'
+ t1s = '2015-03-09T03:43:00Z'
+ t0 = parse_browser_timestamp page.evaluate_script("new Date('#{t0s}').toLocaleString()")
+ t1 = parse_browser_timestamp page.evaluate_script("new Date('#{t1s}').toLocaleString()")
+ assert_equal 17*3600, t1-t0, "'#{t0s}' to '#{t1s}' was reported as #{t1-t0} seconds, should be #{17*3600} (17 hours)"
+ end
+ end
+
test 'Create and run a pipeline' do
visit page_with_token('active_trustedclient', '/pipeline_templates')
within('tr', text: 'Two Part Pipeline Template') do
['active', 'zzzzz-d1hrv-runningpipeline', nil], # state = running
].each do |user, uuid, run_time|
test "pipeline start and finish time display for #{uuid}" do
+ need_selenium 'to parse timestamps correctly across DST boundaries'
visit page_with_token(user, "/pipeline_instances/#{uuid}")
assert page.has_text? 'This pipeline started at'
start_at = match[1]
assert_not_nil(start_at, 'Did not find start_at time')
- # start and finished time display is of the format '2:20 PM 10/20/2014'
- start_time = DateTime.strptime(start_at, '%H:%M %p %m/%d/%Y').to_time
+ start_time = parse_browser_timestamp start_at
if run_time
finished_at = match[3]
assert_not_nil(finished_at, 'Did not find finished_at time')
- finished_time = DateTime.strptime(finished_at, '%H:%M %p %m/%d/%Y').to_time
+ finished_time = parse_browser_timestamp finished_at
assert_equal(run_time, finished_time-start_time,
"Time difference did not match for start_at #{start_at}, finished_at #{finished_at}, ran_for #{match[2]}")
else
test 'Check collection count for A Project in the tab pane titles' do
project_uuid = api_fixture('groups')['aproject']['uuid']
visit page_with_token 'active', '/projects/' + project_uuid
+ click_link 'Data collections'
wait_for_ajax
collection_count = page.all("[data-pk*='collection']").count
assert_selector '#Data_collections-tab span', text: "(#{collection_count})"
visit page_with_token 'active', '/'
find("#projects-menu").click
find(".dropdown-menu a", text: dest['name']).click
+ click_link 'Data collections'
assert page.has_text?(my_collection['name']), 'Collection not found in dest project after copy'
when 'Move'
visit page_with_token 'active', '/'
find("#projects-menu").click
find(".dropdown-menu a", text: dest['name']).click
+ click_link 'Data collections'
assert page.has_text?(my_collection['name']), 'Collection not found in dest project after move'
when 'Remove'
visit page_with_token 'active', '/'
find("#projects-menu").click
find(".dropdown-menu a", text: src['name']).click
+ click_link 'Data collections'
assert page.has_text?(item['name']), 'Collection not found in src project'
within('tr', text: item['name']) do
find("#projects-menu").click
find(".dropdown-menu a", text: my_project['name']).click
+ click_link 'Data collections'
click_button 'Selection'
within('.selection-action-container') do
assert_selector 'li.disabled', text: 'Create new collection with selected collections'
visit page_with_token 'active', '/'
find("#projects-menu").click
find(".dropdown-menu a", text: my_project['name']).click
+ click_link 'Data collections'
assert page.has_text?(my_collection['name']), 'Collection not found in project'
within('tr', text: my_collection['name']) do
end
end
- # "Move selected" and "Remove selected" options should not be available when current user cannot write to the project
+ # "Move selected" and "Remove selected" options should not be
+ # available when current user cannot write to the project
test "move selected and remove selected actions not available when current user cannot write to project" do
my_project = api_fixture('groups')['anonymously_accessible_project']
visit page_with_token 'active', "/projects/#{my_project['uuid']}"
+ click_link 'Data collections'
click_button 'Selection'
within('.selection-action-container') do
assert_selector 'li', text: 'Create new collection with selected collections'
visit page_with_token user, '/'
find("#projects-menu").click
find(".dropdown-menu a", text: my_project['name']).click
+ click_link 'Data collections'
assert page.has_text?(my_collection['name']), 'Collection not found in project'
within('tr', text: my_collection['name']) do
# As of 2014-12-19, the first tab of project#show uses infinite scrolling.
# Make sure that it loads data even if we visit another tab directly.
need_selenium 'to land on specified tab using {url}#Advanced'
- project = api_fixture("groups", "aproject")
+ user = api_fixture("users", "active")
visit(page_with_token("active_trustedclient",
- "/projects/#{project['uuid']}#Advanced"))
+ "/projects/#{user['uuid']}#Advanced"))
assert_text("API response")
find("#page-wrapper .nav-tabs :first-child a").click
assert_text("Collection modified at")
--- /dev/null
+require 'has_uuid'
+require 'kind_and_etag'
+
+class FixCollectionPortableDataHashWithHintedManifest < ActiveRecord::Migration
+ include CurrentApiClient
+
+ class ArvadosModel < ActiveRecord::Base
+ self.abstract_class = true
+ extend HasUuid::ClassMethods
+ include CurrentApiClient
+ include KindAndEtag
+ before_create do |record|
+ record.uuid ||= record.class.generate_uuid
+ record.owner_uuid ||= system_user_uuid
+ end
+ serialize :properties, Hash
+
+ def self.to_s
+ # Clean up the name of the stub model class so we generate correct UUIDs.
+ super.sub("FixCollectionPortableDataHashWithHintedManifest::", "")
+ end
+ end
+
+ class Collection < ArvadosModel
+ end
+
+ class Log < ArvadosModel
+ def self.log_for(thing, age="old")
+ { "#{age}_etag" => thing.etag,
+ "#{age}_attributes" => thing.attributes,
+ }
+ end
+
+ def self.log_create(thing)
+ new_log("create", thing, log_for(thing, "new"))
+ end
+
+ def self.log_update(thing, start_state)
+ new_log("update", thing, start_state.merge(log_for(thing, "new")))
+ end
+
+ def self.log_destroy(thing)
+ new_log("destroy", thing, log_for(thing, "old"))
+ end
+
+ private
+
+ def self.new_log(event_type, thing, properties)
+ create!(event_type: event_type,
+ event_at: Time.now,
+ object_uuid: thing.uuid,
+ object_owner_uuid: thing.owner_uuid,
+ properties: properties)
+ end
+ end
+
+ def each_bad_collection
+ Collection.find_each do |coll|
+ next unless (coll.manifest_text =~ /\+[A-Z]/)
+ stripped_manifest = coll.manifest_text.
+ gsub(/( [0-9a-f]{32}(\+\d+)?)(\+\S+)/, '\1')
+ stripped_pdh = sprintf("%s+%i",
+ Digest::MD5.hexdigest(stripped_manifest),
+ stripped_manifest.bytesize)
+ yield [coll, stripped_pdh] if (coll.portable_data_hash != stripped_pdh)
+ end
+ end
+
+ def up
+ Collection.reset_column_information
+ Log.reset_column_information
+ copied_attr_names =
+ [:owner_uuid, :created_at, :modified_by_client_uuid, :manifest_text,
+ :modified_by_user_uuid, :modified_at, :updated_at, :name,
+ :description, :portable_data_hash, :replication_desired,
+ :replication_confirmed, :replication_confirmed_at, :expires_at]
+ new_expiry = Date.new(2038, 1, 31)
+
+ each_bad_collection do |coll, stripped_pdh|
+ # Create a copy of the collection including bad portable data hash,
+ # with an expiration. This makes it possible to resolve the bad
+ # portable data hash, but the expiration can hide the Collection
+ # from more user-friendly interfaces like Workbench.
+ start_log = Log.log_for(coll)
+ attributes = Hash[copied_attr_names.map { |key| [key, coll.send(key)] }]
+ attributes[:expires_at] ||= new_expiry
+ attributes[:properties] = (coll.properties.dup rescue {})
+ attributes[:properties]["migrated_from"] ||= coll.uuid
+ coll_copy = Collection.create!(attributes)
+ Log.log_create(coll_copy)
+ coll.update_attributes(portable_data_hash: stripped_pdh)
+ Log.log_update(coll, start_log)
+ end
+ end
+
+ def down
+ Collection.reset_column_information
+ Log.reset_column_information
+ each_bad_collection do |coll, stripped_pdh|
+ if ((src_uuid = coll.properties["migrated_from"]) and
+ (src_coll = Collection.where(uuid: src_uuid).first) and
+ (src_coll.portable_data_hash == stripped_pdh))
+ start_log = Log.log_for(src_coll)
+ src_coll.portable_data_hash = coll.portable_data_hash
+ src_coll.save!
+ Log.log_update(src_coll, start_log)
+ coll.destroy or raise Exception.new("failed to destroy old collection")
+ Log.log_destroy(coll)
+ end
+ end
+ end
+end
INSERT INTO schema_migrations (version) VALUES ('20150206230342');
-INSERT INTO schema_migrations (version) VALUES ('20150216193428');
\ No newline at end of file
+INSERT INTO schema_migrations (version) VALUES ('20150216193428');
+
+INSERT INTO schema_migrations (version) VALUES ('20150303210106');
\ No newline at end of file
created_at: 2015-02-07 00:19:28.596506247 Z
modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
modified_at: 2015-02-07 00:19:28.596338465 Z
- portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
+ portable_data_hash: ec53808e4c23e6aeebea24d998ae5346+88
replication_desired: 2
replication_confirmed_at: 2015-02-07 00:24:52.983381227 Z
replication_confirmed: 2