From f541a6e4493916404507b0e25cc7eed172453e81 Mon Sep 17 00:00:00 2001 From: GriffinJ Date: Fri, 17 Jan 2020 11:38:57 -0500 Subject: [PATCH 1/3] Adding Ruby 2.7, adding Rails 6.0.2, and updating the existing Ruby and Rails releases on the CircleCI config. --- .circleci/config.yml | 40 +++++++++++----- Gemfile | 11 ++--- browse-everything.gemspec | 2 +- .../test_compiling_stylesheets_spec.rb | 1 + spec/lib/browse_everything/driver_spec.rb | 46 +++++++++++++++++-- 5 files changed, 77 insertions(+), 23 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e1d173d2..cc544294 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -45,26 +45,42 @@ jobs: workflows: ci: jobs: + - build: + name: "ruby2-7_rails6-0" + ruby_version: 2.7.0 + rails_version: 6.0.2 + - build: + name: "ruby2-6_rails6-0" + ruby_version: 2.6.5 + rails_version: 6.0.2 + - build: + name: "ruby2-5_rails6-0" + ruby_version: 2.5.7 + rails_version: 6.0.2 + - build: + name: "ruby2-7_rails5-2" + ruby_version: 2.7.0 + rails_version: 5.2.4 - build: name: "ruby2-6_rails5-2" - ruby_version: 2.6.3 - rails_version: 5.2.3 + ruby_version: 2.6.5 + rails_version: 5.2.4 + - build: + name: "ruby2-5_rails5-2" + ruby_version: 2.5.7 + rails_version: 5.2.4 + - build: + name: "ruby2-4_rails5-2" + ruby_version: 2.4.6 + rails_version: 5.2.4 - build: name: "ruby2-6_rails5-1" - ruby_version: 2.6.3 + ruby_version: 2.6.5 rails_version: 5.1.7 - - build: - name: "ruby2-5_rails5-2" - ruby_version: 2.5.5 - rails_version: 5.2.3 - build: name: "ruby2-5_rails5-1" - ruby_version: 2.5.5 + ruby_version: 2.5.7 rails_version: 5.1.7 - - build: - name: "ruby2-4_rails5-2" - ruby_version: 2.4.6 - rails_version: 5.2.3 - build: name: "ruby2-4_rails5-1" ruby_version: 2.4.6 diff --git a/Gemfile b/Gemfile index 4e133ddb..910aba05 100644 --- a/Gemfile +++ b/Gemfile @@ -33,15 +33,12 @@ else end end - case ENV['RAILS_VERSION'] + case ENV['RAILS_VERSION'] + when /^6\./ + gem 'puma', '~> 4.1' when /^5\./ gem 'capybara', '~> 2.18.0' - when /^4\.2/ - gem 'coffee-rails', '~> 4.1.0' - gem 'json', '~> 1.8' - gem 'railties', '~> 4.2' - gem 'responders', '~> 2.0' - gem 'sass-rails', '>= 5.0' + gem 'puma', '~> 3.11' end end # END ENGINE_CART BLOCK diff --git a/browse-everything.gemspec b/browse-everything.gemspec index 0fb0f002..c3b00587 100644 --- a/browse-everything.gemspec +++ b/browse-everything.gemspec @@ -25,7 +25,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'google-api-client', '~> 0.23' spec.add_dependency 'google_drive', '>= 2.1', "< 4" spec.add_dependency 'googleauth', '>= 0.6.6', '< 1.0' - spec.add_dependency 'rails', '>= 4.2', '< 6.0' + spec.add_dependency 'rails', '>= 4.2', '< 7.0' spec.add_dependency 'ruby-box' spec.add_dependency 'signet', '~> 0.8' spec.add_dependency 'sprockets', '~> 3.7' diff --git a/spec/features/test_compiling_stylesheets_spec.rb b/spec/features/test_compiling_stylesheets_spec.rb index a1172e9e..af7b5eb5 100644 --- a/spec/features/test_compiling_stylesheets_spec.rb +++ b/spec/features/test_compiling_stylesheets_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true describe 'Compiling the stylesheets', type: :feature do + routes { Rails.application.class.routes } it 'does not raise errors' do visit '/' expect(page).not_to have_content 'Sass::SyntaxError' diff --git a/spec/lib/browse_everything/driver_spec.rb b/spec/lib/browse_everything/driver_spec.rb index c57ed129..67ca799e 100644 --- a/spec/lib/browse_everything/driver_spec.rb +++ b/spec/lib/browse_everything/driver_spec.rb @@ -15,9 +15,49 @@ def get_sorter # rubocop:disable Naming/AccessorMethodName end end - describe '#sorter' do - it 'defaults to nil' do - expect(described_class.sorter).to be nil + after do + Object.send(:remove_const, :MyDriver) + end + + describe '#default_sorter' do + let(:file1) do + instance_double(BrowseEverything::FileEntry) + end + let(:file2) do + instance_double(BrowseEverything::FileEntry) + end + let(:file3) do + instance_double(BrowseEverything::FileEntry) + end + let(:files) do + [ + file1, + file2, + file3 + ] + end + + before do + class MyNewDriver < BrowseEverything::Driver::Base; end + + allow(file1).to receive(:container?).and_return(false) + allow(file1).to receive(:name).and_return('test file1') + allow(file2).to receive(:container?).and_return(false) + allow(file2).to receive(:name).and_return('test file2') + allow(file3).to receive(:container?).and_return(true) + allow(file3).to receive(:name).and_return('test container1') + end + after do + Object.send(:remove_const, :MyNewDriver) + end + + it 'defaults to proc which sorts by containers' do + expect(MyNewDriver.default_sorter).to be_a(Proc) + + results = MyNewDriver.default_sorter.call(files) + expect(results.first.name).to eq file3.name + expect(results[1].name).to eq file1.name + expect(results.last.name).to eq file2.name end end From 9c483d19dc0adef6343b6cef12ddce6b63ff91e9 Mon Sep 17 00:00:00 2001 From: "James R. Griffin III" Date: Mon, 8 Jun 2020 11:54:39 -0400 Subject: [PATCH 2/3] Removing CircleCI builds testing Ruby 4.y.z releases with Rails 6.y.z releases; Updating the CircleCI cache key; Updating bixby to release 3.0.0 --- .circleci/config.yml | 44 +++--- .rubocop.yml | 73 ++++----- Gemfile | 2 +- .../browse_everything_controller.rb | 120 +++++++-------- browse-everything.gemspec | 2 +- .../auth/google/credentials.rb | 10 +- .../auth/google/request_parameters.rb | 76 +++++----- lib/browse_everything/browser.rb | 12 +- lib/browse_everything/driver/base.rb | 28 ++-- lib/browse_everything/driver/box.rb | 112 +++++++------- lib/browse_everything/driver/dropbox.rb | 78 +++++----- lib/browse_everything/driver/file_system.rb | 34 ++--- lib/browse_everything/driver/google_drive.rb | 76 +++++----- lib/browse_everything/driver/s3.rb | 122 ++++++++-------- lib/browse_everything/file_entry.rb | 2 +- lib/browse_everything/retriever.rb | 138 +++++++++--------- .../test_compiling_stylesheets_spec.rb | 7 +- 17 files changed, 472 insertions(+), 464 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index cc544294..4f0c4fba 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,7 +12,7 @@ jobs: type: string bundler_version: type: string - default: 2.0.1 + default: 2.1.4 executor: name: 'samvera/ruby' ruby_version: << parameters.ruby_version >> @@ -27,7 +27,7 @@ jobs: project: 'browse-everything' - samvera/engine_cart_generate: - cache_key: v1-internal-test-app-{{ checksum "browse-everything.gemspec" }}-{{ checksum "spec/test_app_templates/lib/generators/test_app_generator.rb" }}-{{ checksum "lib/generators/browse_everything/install_generator.rb" }}-{{ checksum "lib/generators/browse_everything/config_generator.rb" }}--<< parameters.rails_version >>-<< parameters.ruby_version >> + cache_key: v1-internal-test-app-{{ checksum "browse-everything.gemspec" }}-{{ checksum "Gemfile" }}-{{ checksum "spec/test_app_templates/lib/generators/test_app_generator.rb" }}-{{ checksum "lib/generators/browse_everything/install_generator.rb" }}-{{ checksum "lib/generators/browse_everything/config_generator.rb" }}--<< parameters.rails_version >>-<< parameters.ruby_version >> - samvera/bundle_for_gem: ruby_version: << parameters.ruby_version >> @@ -47,41 +47,47 @@ workflows: jobs: - build: name: "ruby2-7_rails6-0" - ruby_version: 2.7.0 - rails_version: 6.0.2 + ruby_version: 2.7.1 + rails_version: 6.0.3.1 - build: name: "ruby2-6_rails6-0" - ruby_version: 2.6.5 - rails_version: 6.0.2 + ruby_version: 2.6.6 + rails_version: 6.0.3.1 - build: name: "ruby2-5_rails6-0" - ruby_version: 2.5.7 - rails_version: 6.0.2 + ruby_version: 2.5.8 + rails_version: 6.0.3.1 + - build: name: "ruby2-7_rails5-2" - ruby_version: 2.7.0 - rails_version: 5.2.4 + ruby_version: 2.7.1 + rails_version: 5.2.4.3 - build: name: "ruby2-6_rails5-2" - ruby_version: 2.6.5 - rails_version: 5.2.4 + ruby_version: 2.6.6 + rails_version: 5.2.4.3 - build: name: "ruby2-5_rails5-2" - ruby_version: 2.5.7 - rails_version: 5.2.4 + ruby_version: 2.5.8 + rails_version: 5.2.4.3 - build: name: "ruby2-4_rails5-2" - ruby_version: 2.4.6 - rails_version: 5.2.4 + ruby_version: 2.4.10 + rails_version: 5.2.4.3 + + - build: + name: "ruby2-7_rails5-1" + ruby_version: 2.7.1 + rails_version: 5.1.7 - build: name: "ruby2-6_rails5-1" - ruby_version: 2.6.5 + ruby_version: 2.6.6 rails_version: 5.1.7 - build: name: "ruby2-5_rails5-1" - ruby_version: 2.5.7 + ruby_version: 2.5.8 rails_version: 5.1.7 - build: name: "ruby2-4_rails5-1" - ruby_version: 2.4.6 + ruby_version: 2.4.10 rails_version: 5.1.7 diff --git a/.rubocop.yml b/.rubocop.yml index 26875d0d..cb1468bb 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -3,71 +3,72 @@ inherit_gem: bixby: bixby_default.yml AllCops: - TargetRubyVersion: 2.3 + TargetRubyVersion: 2.6 DisplayCopNames: true Exclude: - 'vendor/**/*' -Rails: - Enabled: true -Rails/FilePath: +Bundler/DuplicatedGem: Exclude: - - 'lib/generators/browse_everything/config_generator.rb' + - 'Gemfile' -Metrics/ClassLength: - Max: 130 - Exclude: - - 'lib/browse_everything/driver/google_drive.rb' -Metrics/LineLength: +Layout/LineLength: Max: 400 Exclude: - 'spec/lib/browse_everything/driver/box_spec.rb' - 'spec/lib/browse_everything/driver/dropbox_spec.rb' + Metrics/BlockLength: Exclude: - '*.gemspec' - 'spec/**/*' -RSpec/NestedGroups: - Enabled: false +Metrics/ClassLength: + Max: 130 + Exclude: + - 'lib/browse_everything/driver/google_drive.rb' + +Naming/FileName: + Exclude: + - 'browse-everything.gemspec' + - 'Gemfile' + - 'lib/browse-everything.rb' + +Rails: + Enabled: true + +Rails/FilePath: + Exclude: + - 'lib/generators/browse_everything/config_generator.rb' RSpec/DescribeClass: Exclude: - 'spec/javascripts/**/*' - -RSpec/LeadingSubject: - Enabled: false RSpec/ExampleLength: Enabled: false -RSpec/MultipleExpectations: +RSpec/LeadingSubject: Enabled: false -Style/NumericLiterals: - MinDigits: 7 - -Layout/IndentationConsistency: - EnforcedStyle: rails +RSpec/MultipleExpectations: + Enabled: false -Naming/FileName: - Exclude: - - 'browse-everything.gemspec' - - 'Gemfile' - - 'lib/browse-everything.rb' +RSpec/NestedGroups: + Enabled: false Style/MixinUsage: Exclude: - - 'spec/lib/browse_everything/driver/s3_spec.rb' - - 'spec/lib/browse_everything/driver/google_drive_spec.rb' - - 'spec/lib/browse_everything/driver/file_system_spec.rb' - - 'spec/lib/browse_everything/driver/dropbox_spec.rb' - - 'spec/lib/browse_everything/driver/box_spec.rb' - - 'spec/lib/browse_everything/driver/base_spec.rb' + - 'spec/helper/browse_everything_controller_helper_spec.rb' - 'spec/lib/browse_everything/browser_spec.rb' + - 'spec/lib/browse_everything/driver/base_spec.rb' + - 'spec/lib/browse_everything/driver/box_spec.rb' + - 'spec/lib/browse_everything/driver/dropbox_spec.rb' + - 'spec/lib/browse_everything/driver/file_system_spec.rb' + - 'spec/lib/browse_everything/driver/google_drive_spec.rb' + - 'spec/lib/browse_everything/driver/s3_spec.rb' - 'spec/services/browser_factory_spec.rb' - - 'spec/helper/browse_everything_controller_helper_spec.rb' -Bundler/DuplicatedGem: - Exclude: - - 'Gemfile' +Style/NumericLiterals: + MinDigits: 7 + diff --git a/Gemfile b/Gemfile index 910aba05..a0022499 100644 --- a/Gemfile +++ b/Gemfile @@ -33,7 +33,7 @@ else end end - case ENV['RAILS_VERSION'] + case ENV['RAILS_VERSION'] when /^6\./ gem 'puma', '~> 4.1' when /^5\./ diff --git a/app/controllers/browse_everything_controller.rb b/app/controllers/browse_everything_controller.rb index c42d8819..0a3cb3a3 100644 --- a/app/controllers/browse_everything_controller.rb +++ b/app/controllers/browse_everything_controller.rb @@ -2,7 +2,7 @@ require File.expand_path('../helpers/browse_everything_helper', __dir__) -class BrowseEverythingController < ActionController::Base +class BrowseEverythingController < ApplicationController layout 'browse_everything' helper BrowseEverythingHelper @@ -61,72 +61,72 @@ def resolve private - # Constructs or accesses an existing session manager Object - # @return [BrowseEverythingSession::ProviderSession] the session manager - def provider_session - BrowseEverythingSession::ProviderSession.new(session: session, name: provider_name) - end + # Constructs or accesses an existing session manager Object + # @return [BrowseEverythingSession::ProviderSession] the session manager + def provider_session + BrowseEverythingSession::ProviderSession.new(session: session, name: provider_name) + end - # Clears all authentication tokens, codes, and other data from the Rails session - def reset_provider_session! - return unless @provider_session - @provider_session.token = nil - @provider_session.code = nil - @provider_session.data = nil - @provider_session = nil - end + # Clears all authentication tokens, codes, and other data from the Rails session + def reset_provider_session! + return unless @provider_session + @provider_session.token = nil + @provider_session.code = nil + @provider_session.data = nil + @provider_session = nil + end - def connector_response_url_options - { protocol: request.protocol, host: request.host, port: request.port } - end + def connector_response_url_options + { protocol: request.protocol, host: request.host, port: request.port } + end - # Generates the authentication link for a given provider service - # @return [String] the authentication link - def auth_link - @auth_link ||= if provider.present? - link, data = provider.auth_link(connector_response_url_options) - provider_session.data = data - link = "#{link}&state=#{provider.key}" unless link.to_s.include?('state') - link - end - end + # Generates the authentication link for a given provider service + # @return [String] the authentication link + def auth_link + @auth_link ||= if provider.present? + link, data = provider.auth_link(connector_response_url_options) + provider_session.data = data + link = "#{link}&state=#{provider.key}" unless link.to_s.include?('state') + link + end + end - # Accesses the relative path for browsing from the Rails session - # @return [String] - def browse_path - params[:path] || '' - end + # Accesses the relative path for browsing from the Rails session + # @return [String] + def browse_path + params[:path] || '' + end - # Generate the provider name from the Rails session state value - # @return [String] - def provider_name_from_state - params[:state].to_s.split(/\|/).last - end + # Generate the provider name from the Rails session state value + # @return [String] + def provider_name_from_state + params[:state].to_s.split(/\|/).last + end - # Generates the name of the provider using Rails session values - # @return [String] - def provider_name - params[:provider] || provider_name_from_state || browser.providers.each_key.to_a.first - end + # Generates the name of the provider using Rails session values + # @return [String] + def provider_name + params[:provider] || provider_name_from_state || browser.providers.each_key.to_a.first + end - # Retrieve the Driver for each request - # @return [BrowseEverything::Driver::Base] - def provider - browser.providers[provider_name.to_sym] || browser.first_provider - end + # Retrieve the Driver for each request + # @return [BrowseEverything::Driver::Base] + def provider + browser.providers[provider_name.to_sym] || browser.first_provider + end - # Constructs a browser manager Object - # Browser state cannot persist between requests to the Controller - # Hence, a Browser must be reinstantiated for each request using the state provided in the Rails session - # @return [BrowseEverything::Browser] - def browser - BrowserFactory.build(session: session, url_options: url_options) - end + # Constructs a browser manager Object + # Browser state cannot persist between requests to the Controller + # Hence, a Browser must be reinstantiated for each request using the state provided in the Rails session + # @return [BrowseEverything::Browser] + def browser + BrowserFactory.build(session: session, url_options: url_options) + end - helper_method :auth_link - helper_method :browser - helper_method :browse_path - helper_method :provider - helper_method :provider_name - helper_method :provider_contents + helper_method :auth_link + helper_method :browser + helper_method :browse_path + helper_method :provider + helper_method :provider_name + helper_method :provider_contents end diff --git a/browse-everything.gemspec b/browse-everything.gemspec index c3b00587..908b852f 100644 --- a/browse-everything.gemspec +++ b/browse-everything.gemspec @@ -31,7 +31,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'sprockets', '~> 3.7' spec.add_dependency 'typhoeus' - spec.add_development_dependency 'bixby', '>= 1.0' + spec.add_development_dependency 'bixby', '~> 3.0' spec.add_development_dependency 'bundler', '>= 1.3' spec.add_development_dependency 'capybara' spec.add_development_dependency 'coveralls' diff --git a/lib/browse_everything/auth/google/credentials.rb b/lib/browse_everything/auth/google/credentials.rb index 843cc019..23c59e7e 100644 --- a/lib/browse_everything/auth/google/credentials.rb +++ b/lib/browse_everything/auth/google/credentials.rb @@ -17,11 +17,11 @@ def fetch_access_token(options = {}) private - # Structure a hash from existing access token values (usually cached within a Cookie) - # @return [Hash] - def build_token_hash - { 'access_token' => access_token } - end + # Structure a hash from existing access token values (usually cached within a Cookie) + # @return [Hash] + def build_token_hash + { 'access_token' => access_token } + end end end end diff --git a/lib/browse_everything/auth/google/request_parameters.rb b/lib/browse_everything/auth/google/request_parameters.rb index 15edf9dd..374c0445 100644 --- a/lib/browse_everything/auth/google/request_parameters.rb +++ b/lib/browse_everything/auth/google/request_parameters.rb @@ -14,47 +14,47 @@ def initialize(params = {}) private - # The default query parameters for the Google Drive API - # @return [Hash] - def default_params - { - q: default_query, - order_by: 'modifiedTime desc,folder,name', - fields: 'nextPageToken,files(name,id,mimeType,size,modifiedTime,parents,web_content_link)', - supports_team_drives: true, - include_team_drive_items: true, - corpora: 'user,allTeamDrives', - page_size: 1000 - } - end + # The default query parameters for the Google Drive API + # @return [Hash] + def default_params + { + q: default_query, + order_by: 'modifiedTime desc,folder,name', + fields: 'nextPageToken,files(name,id,mimeType,size,modifiedTime,parents,web_content_link)', + supports_team_drives: true, + include_team_drive_items: true, + corpora: 'user,allTeamDrives', + page_size: 1000 + } + end - def default_query - field_queries = [] - contraints.each_pair do |field, constraints| - field_constraint = constraints.join(" and #{field} ") - field_queries << "#{field} #{field_constraint}" - end - field_queries.join(' ') + def default_query + field_queries = [] + contraints.each_pair do |field, constraints| + field_constraint = constraints.join(" and #{field} ") + field_queries << "#{field} #{field_constraint}" end + field_queries.join(' ') + end - def contraints - { - 'mimeType' => [ - '!= \'application/vnd.google-apps.audio\'', - '!= \'application/vnd.google-apps.document\'', - '!= \'application/vnd.google-apps.drawing\'', - '!= \'application/vnd.google-apps.form\'', - '!= \'application/vnd.google-apps.fusiontable\'', - '!= \'application/vnd.google-apps.map\'', - '!= \'application/vnd.google-apps.photo\'', - '!= \'application/vnd.google-apps.presentation\'', - '!= \'application/vnd.google-apps.script\'', - '!= \'application/vnd.google-apps.site\'', - '!= \'application/vnd.google-apps.spreadsheet\'', - '!= \'application/vnd.google-apps.video\'' - ] - } - end + def contraints + { + 'mimeType' => [ + '!= \'application/vnd.google-apps.audio\'', + '!= \'application/vnd.google-apps.document\'', + '!= \'application/vnd.google-apps.drawing\'', + '!= \'application/vnd.google-apps.form\'', + '!= \'application/vnd.google-apps.fusiontable\'', + '!= \'application/vnd.google-apps.map\'', + '!= \'application/vnd.google-apps.photo\'', + '!= \'application/vnd.google-apps.presentation\'', + '!= \'application/vnd.google-apps.script\'', + '!= \'application/vnd.google-apps.site\'', + '!= \'application/vnd.google-apps.spreadsheet\'', + '!= \'application/vnd.google-apps.video\'' + ] + } + end end end end diff --git a/lib/browse_everything/browser.rb b/lib/browse_everything/browser.rb index 24cf4314..621450ab 100644 --- a/lib/browse_everything/browser.rb +++ b/lib/browse_everything/browser.rb @@ -15,13 +15,11 @@ def initialize(opts = {}) @providers = ActiveSupport::HashWithIndifferentAccess.new opts.each_pair do |driver_key, config| - begin - driver = driver_key.to_s - driver_klass = BrowseEverything::Driver.const_get((config[:driver] || driver).camelize.to_sym) - @providers[driver_key] = driver_klass.new(config.merge(url_options: url_options)) - rescue NameError - Rails.logger.warn "Unknown provider: #{driver}" - end + driver = driver_key.to_s + driver_klass = BrowseEverything::Driver.const_get((config[:driver] || driver).camelize.to_sym) + @providers[driver_key] = driver_klass.new(config.merge(url_options: url_options)) + rescue NameError + Rails.logger.warn "Unknown provider: #{driver}" end end diff --git a/lib/browse_everything/driver/base.rb b/lib/browse_everything/driver/base.rb index 480fc1a3..5a5478a5 100644 --- a/lib/browse_everything/driver/base.rb +++ b/lib/browse_everything/driver/base.rb @@ -100,21 +100,21 @@ def connect(*_args) private - # Generate the options for the Rails URL generation for API callbacks - # remove the script_name parameter from the url_options since that is causing issues - # with the route not containing the engine path in rails 4.2.0 - # @return [Hash] - def callback_options - options = config.to_hash - options.deep_symbolize_keys! - options[:url_options].reject { |k, _v| k == :script_name } - end + # Generate the options for the Rails URL generation for API callbacks + # remove the script_name parameter from the url_options since that is causing issues + # with the route not containing the engine path in rails 4.2.0 + # @return [Hash] + def callback_options + options = config.to_hash + options.deep_symbolize_keys! + options[:url_options].reject { |k, _v| k == :script_name } + end - # Generate the URL for the API callback - # @return [String] - def callback - connector_response_url(callback_options) - end + # Generate the URL for the API callback + # @return [String] + def callback + connector_response_url(callback_options) + end end end end diff --git a/lib/browse_everything/driver/box.rb b/lib/browse_everything/driver/box.rb index 44b7e1e9..a57f0312 100644 --- a/lib/browse_everything/driver/box.rb +++ b/lib/browse_everything/driver/box.rb @@ -77,72 +77,72 @@ def connect(params, _data, _url_options) private - def token_expired? - return true if expiration_time.nil? - Time.now.to_i > expiration_time - end + def token_expired? + return true if expiration_time.nil? + Time.now.to_i > expiration_time + end - def box_client - if token_expired? - session = box_session - register_access_token(session.refresh_token(box_refresh_token)) - end - RubyBox::Client.new(box_session) + def box_client + if token_expired? + session = box_session + register_access_token(session.refresh_token(box_refresh_token)) end + RubyBox::Client.new(box_session) + end - def session - AuthenticationFactory.new( - self.class.authentication_klass, - client_id: config[:client_id], - client_secret: config[:client_secret], - access_token: box_token, - refresh_token: box_refresh_token - ) - end + def session + AuthenticationFactory.new( + self.class.authentication_klass, + client_id: config[:client_id], + client_secret: config[:client_secret], + access_token: box_token, + refresh_token: box_refresh_token + ) + end - def authenticate - session.authenticate - end + def authenticate + session.authenticate + end - def box_session - authenticate - end + def box_session + authenticate + end - # If there is an active session, {@token} will be set by {BrowseEverythingController} using data stored in the - # session. However, if there is no prior session, or the token has expired, we reset it here using # a new - # access_token received from {#box_session}. - # - # @param [OAuth2::AccessToken] access_token - def register_access_token(access_token) - @token = { - 'token' => access_token.token, - 'refresh_token' => access_token.refresh_token, - 'expires_at' => access_token.expires_at - } - end + # If there is an active session, {@token} will be set by {BrowseEverythingController} using data stored in the + # session. However, if there is no prior session, or the token has expired, we reset it here using # a new + # access_token received from {#box_session}. + # + # @param [OAuth2::AccessToken] access_token + def register_access_token(access_token) + @token = { + 'token' => access_token.token, + 'refresh_token' => access_token.refresh_token, + 'expires_at' => access_token.expires_at + } + end - def box_token - return unless @token - @token.fetch('token', nil) - end + def box_token + return unless @token + @token.fetch('token', nil) + end - def box_refresh_token - return unless @token - @token.fetch('refresh_token', nil) - end + def box_refresh_token + return unless @token + @token.fetch('refresh_token', nil) + end - def expiration_time - return unless @token - @token.fetch('expires_at', nil).to_i - end + def expiration_time + return unless @token + @token.fetch('expires_at', nil).to_i + end - # Constructs a BrowseEverything::FileEntry object for a Box file - # resource - # @param file [String] ID to the file resource - # @return [BrowseEverything::File] - def directory_entry(file) - BrowseEverything::FileEntry.new(file.id, "#{key}:#{file.id}", file.name, file.size, file.created_at, file.type == 'folder') - end + # Constructs a BrowseEverything::FileEntry object for a Box file + # resource + # @param file [String] ID to the file resource + # @return [BrowseEverything::File] + def directory_entry(file) + BrowseEverything::FileEntry.new(file.id, "#{key}:#{file.id}", file.name, file.size, file.created_at, file.type == 'folder') + end end end end diff --git a/lib/browse_everything/driver/dropbox.rb b/lib/browse_everything/driver/dropbox.rb index e7d41469..2b645984 100644 --- a/lib/browse_everything/driver/dropbox.rb +++ b/lib/browse_everything/driver/dropbox.rb @@ -16,14 +16,14 @@ def self.build(metadata:, key:) class << self private - def klass_for(metadata) - case metadata - when DropboxApi::Metadata::File - FileFactory - else - ResourceFactory - end + def klass_for(metadata) + case metadata + when DropboxApi::Metadata::File + FileFactory + else + ResourceFactory end + end end end @@ -138,44 +138,44 @@ def authorized? private - def session - AuthenticationFactory.new( - self.class.authentication_klass, - config[:client_id], - config[:client_secret] - ) - end + def session + AuthenticationFactory.new( + self.class.authentication_klass, + config[:client_id], + config[:client_secret] + ) + end - def authenticate - session.authenticate - end + def authenticate + session.authenticate + end - def authenticator - @authenticator ||= authenticate - end + def authenticator + @authenticator ||= authenticate + end - def client - DropboxApi::Client.new(token) - end + def client + DropboxApi::Client.new(token) + end - def redirect_uri(url_options) - connector_response_url(**url_options) - end + def redirect_uri(url_options) + connector_response_url(**url_options) + end - # Ensures that the "tmp" directory is used if there is no default download - # directory specified in the configuration - # @return [String] - def default_download_directory - Rails.root.join('tmp') - end + # Ensures that the "tmp" directory is used if there is no default download + # directory specified in the configuration + # @return [String] + def default_download_directory + Rails.root.join('tmp') + end - # Retrieves the directory path for downloads used when retrieving the - # resource from Dropbox - # @return [String] - def download_directory_path - dir_path = config[:download_directory] || default_download_directory - File.expand_path(dir_path) - end + # Retrieves the directory path for downloads used when retrieving the + # resource from Dropbox + # @return [String] + def download_directory_path + dir_path = config[:download_directory] || default_download_directory + File.expand_path(dir_path) + end end end end diff --git a/lib/browse_everything/driver/file_system.rb b/lib/browse_everything/driver/file_system.rb index c563e075..8a3c728b 100644 --- a/lib/browse_everything/driver/file_system.rb +++ b/lib/browse_everything/driver/file_system.rb @@ -55,25 +55,25 @@ def details(path, display = File.basename(path)) private - # Construct an array of FileEntry objects for the contents of a - # directory - # @param real_path [String] path to the file system directory - # @return [Array] - def make_directory_entry(real_path) - entries = [] - entries + Dir[File.join(real_path, '*')].collect { |f| details(f) } - end + # Construct an array of FileEntry objects for the contents of a + # directory + # @param real_path [String] path to the file system directory + # @return [Array] + def make_directory_entry(real_path) + entries = [] + entries + Dir[File.join(real_path, '*')].collect { |f| details(f) } + end - def make_pathname(path) - Pathname.new(File.expand_path(path)).relative_path_from(Pathname.new(config[:home])) - end + def make_pathname(path) + Pathname.new(File.expand_path(path)).relative_path_from(Pathname.new(config[:home])) + end - def file_size(path) - File.size(path).to_i - rescue StandardError => error - Rails.logger.error "Failed to find the file size for #{path}: #{error}" - 0 - end + def file_size(path) + File.size(path).to_i + rescue StandardError => error + Rails.logger.error "Failed to find the file size for #{path}: #{error}" + 0 + end end end end diff --git a/lib/browse_everything/driver/google_drive.rb b/lib/browse_everything/driver/google_drive.rb index a2053fb4..46025041 100644 --- a/lib/browse_everything/driver/google_drive.rb +++ b/lib/browse_everything/driver/google_drive.rb @@ -191,50 +191,50 @@ def drive_service private - def client_secrets - { - Google::Auth::ClientId::WEB_APP => { - Google::Auth::ClientId::CLIENT_ID => config[:client_id], - Google::Auth::ClientId::CLIENT_SECRET => config[:client_secret] - } + def client_secrets + { + Google::Auth::ClientId::WEB_APP => { + Google::Auth::ClientId::CLIENT_ID => config[:client_id], + Google::Auth::ClientId::CLIENT_SECRET => config[:client_secret] } - end + } + end - # This is required for using the googleauth Gem - # @see http://www.rubydoc.info/gems/googleauth/Google/Auth/Stores/FileTokenStore FileTokenStore for googleauth - # @return [Tempfile] temporary file within which to cache credentials - def file_token_store_path - Tempfile.new('gdrive.yaml') - end + # This is required for using the googleauth Gem + # @see http://www.rubydoc.info/gems/googleauth/Google/Auth/Stores/FileTokenStore FileTokenStore for googleauth + # @return [Tempfile] temporary file within which to cache credentials + def file_token_store_path + Tempfile.new('gdrive.yaml') + end - def scope - Google::Apis::DriveV3::AUTH_DRIVE - end + def scope + Google::Apis::DriveV3::AUTH_DRIVE + end - # Provides the user ID for caching access tokens - # (This is a hack which attempts to anonymize the access tokens) - # @return [String] the ID for the user - def user_id - 'current_user' - end + # Provides the user ID for caching access tokens + # (This is a hack which attempts to anonymize the access tokens) + # @return [String] the ID for the user + def user_id + 'current_user' + end - # Please see https://developers.google.com/drive/v3/web/manage-downloads - # @param id [String] the ID for the Google Drive File - # @return [String] the URL for the file download - def download_url(id) - "https://www.googleapis.com/drive/v3/files/#{id}?alt=media" - end + # Please see https://developers.google.com/drive/v3/web/manage-downloads + # @param id [String] the ID for the Google Drive File + # @return [String] the URL for the file download + def download_url(id) + "https://www.googleapis.com/drive/v3/files/#{id}?alt=media" + end - # Restore the credentials for the Google API - # @param access_token [String] the access token redeemed using an authorization code - # @return Credentials credentials restored from a cached access token - def restore_credentials(access_token) - client = Auth::Google::Credentials.new - client.client_id = client_id.id - client.client_secret = client_id.secret - client.update_token!('access_token' => access_token) - @credentials = client - end + # Restore the credentials for the Google API + # @param access_token [String] the access token redeemed using an authorization code + # @return Credentials credentials restored from a cached access token + def restore_credentials(access_token) + client = Auth::Google::Credentials.new + client.client_id = client_id.id + client.client_secret = client_id.secret + client.update_token!('access_token' => access_token) + @credentials = client + end end end end diff --git a/lib/browse_everything/driver/s3.rb b/lib/browse_everything/driver/s3.rb index ad998017..8af7e76d 100644 --- a/lib/browse_everything/driver/s3.rb +++ b/lib/browse_everything/driver/s3.rb @@ -79,79 +79,79 @@ def bucket private - def strip(path) - path.sub %r{^/?(.+?)/?$}, '\1' - end + def strip(path) + path.sub %r{^/?(.+?)/?$}, '\1' + end - def from_base(key) - Pathname.new(key).relative_path_from(Pathname.new(config[:base].to_s)).to_s - end + def from_base(key) + Pathname.new(key).relative_path_from(Pathname.new(config[:base].to_s)).to_s + end - def full_path(path) - config[:base].present? ? File.join(config[:base], path) : path - end + def full_path(path) + config[:base].present? ? File.join(config[:base], path) : path + end - def aws_config - result = {} - result[:credentials] = Aws::Credentials.new(config[:app_key], config[:app_secret]) if config[:app_key].present? - result[:region] = config[:region] if config.key?(:region) - result - end + def aws_config + result = {} + result[:credentials] = Aws::Credentials.new(config[:app_key], config[:app_secret]) if config[:app_key].present? + result[:region] = config[:region] if config.key?(:region) + result + end - def session - AuthenticationFactory.new( - self.class.authentication_klass, - aws_config - ) - end + def session + AuthenticationFactory.new( + self.class.authentication_klass, + aws_config + ) + end - def authenticate - session.authenticate - end + def authenticate + session.authenticate + end - def client - @client ||= authenticate - end + def client + @client ||= authenticate + end - # Construct a BrowseEverything::FileEntry object - # @param name [String] - # @param size [String] - # @param date [DateTime] - # @param dir [String] - # @return [BrowseEverything::FileEntry] - def entry_for(name, size, date, dir) - BrowseEverything::FileEntry.new(name, [key, name].join(':'), File.basename(name), size, date, dir) - end + # Construct a BrowseEverything::FileEntry object + # @param name [String] + # @param size [String] + # @param date [DateTime] + # @param dir [String] + # @return [BrowseEverything::FileEntry] + def entry_for(name, size, date, dir) + BrowseEverything::FileEntry.new(name, [key, name].join(':'), File.basename(name), size, date, dir) + end - # Populate the entries with FileEntry objects from an S3 listing - # @param listing [Seahorse::Client::Response] - def add_directories(listing) - listing.common_prefixes.each do |prefix| - new_entry = entry_for(from_base(prefix.prefix), 0, Time.current, true) - @entries << new_entry unless new_entry.nil? - end + # Populate the entries with FileEntry objects from an S3 listing + # @param listing [Seahorse::Client::Response] + def add_directories(listing) + listing.common_prefixes.each do |prefix| + new_entry = entry_for(from_base(prefix.prefix), 0, Time.current, true) + @entries << new_entry unless new_entry.nil? end + end - # Given a listing and a S3 listing and path, populate the entries - # @param listing [Seahorse::Client::Response] - # @param path [String] - def add_files(listing, path) - listing.contents.each do |entry| - key = from_base(entry.key) - new_entry = entry_for(key, entry.size, entry.last_modified, false) - @entries << new_entry unless strip(key) == strip(path) || new_entry.nil? - end + # Given a listing and a S3 listing and path, populate the entries + # @param listing [Seahorse::Client::Response] + # @param path [String] + def add_files(listing, path) + listing.contents.each do |entry| + key = from_base(entry.key) + new_entry = entry_for(key, entry.size, entry.last_modified, false) + @entries << new_entry unless strip(key) == strip(path) || new_entry.nil? end + end - # For a given path to a S3 resource, retrieve the listing object and - # construct the file entries - # @param path [String] - def generate_listing(path) - client - listing = client.list_objects(bucket: config[:bucket], delimiter: '/', prefix: full_path(path)) - add_directories(listing) - add_files(listing, path) - end + # For a given path to a S3 resource, retrieve the listing object and + # construct the file entries + # @param path [String] + def generate_listing(path) + client + listing = client.list_objects(bucket: config[:bucket], delimiter: '/', prefix: full_path(path)) + add_directories(listing) + add_files(listing, path) + end end end end diff --git a/lib/browse_everything/file_entry.rb b/lib/browse_everything/file_entry.rb index 2ab6c192..2006227f 100644 --- a/lib/browse_everything/file_entry.rb +++ b/lib/browse_everything/file_entry.rb @@ -15,7 +15,7 @@ def initialize(id, location, name, size, mtime, container, type = nil) end def relative_parent_path? - name =~ /^\.\.?$/ ? true : false + name.match?(/^\.\.?$/) end def container? diff --git a/lib/browse_everything/retriever.rb b/lib/browse_everything/retriever.rb index b80f4a6f..6f5750f4 100644 --- a/lib/browse_everything/retriever.rb +++ b/lib/browse_everything/retriever.rb @@ -89,81 +89,81 @@ def retrieve(options, &block) private - # Extract and parse options used to download a file or resource from an HTTP API - # @param options [Hash] - # @return [Hash] - def extract_download_options(options) - url = options.fetch('url') - - # This avoids the potential for a KeyError - headers = options.fetch('headers', {}) || {} - - file_size_value = options.fetch('file_size', 0) - file_size = file_size_value.to_i - - output = { - url: ::Addressable::URI.parse(url), - headers: headers, - file_size: file_size - } - - output[:file_size] = get_file_size(output) if output[:file_size] < 1 - output - end + # Extract and parse options used to download a file or resource from an HTTP API + # @param options [Hash] + # @return [Hash] + def extract_download_options(options) + url = options.fetch('url') - # Retrieve the file from the file system - # @param options [Hash] - def retrieve_file(options) - file_uri = options.fetch(:url) - file_size = options.fetch(:file_size) - - retrieved = 0 - File.open(file_uri.path, 'rb') do |f| - until f.eof? - chunk = f.read(chunk_size) - retrieved += chunk.length - yield(chunk, retrieved, file_size) - end - end - end + # This avoids the potential for a KeyError + headers = options.fetch('headers', {}) || {} - # Retrieve a resource over the HTTP - # @param options [Hash] - def retrieve_http(options) - file_size = options.fetch(:file_size) - headers = options.fetch(:headers) - url = options.fetch(:url) - retrieved = 0 - - request = Typhoeus::Request.new(url.to_s, method: :get, headers: headers) - request.on_headers do |response| - raise DownloadError.new("#{self.class}: Failed to download #{url}: Status Code: #{response.code}", response) unless response.code == 200 - end - request.on_body do |chunk| - retrieved += chunk.bytesize + file_size_value = options.fetch('file_size', 0) + file_size = file_size_value.to_i + + output = { + url: ::Addressable::URI.parse(url), + headers: headers, + file_size: file_size + } + + output[:file_size] = get_file_size(output) if output[:file_size] < 1 + output + end + + # Retrieve the file from the file system + # @param options [Hash] + def retrieve_file(options) + file_uri = options.fetch(:url) + file_size = options.fetch(:file_size) + + retrieved = 0 + File.open(file_uri.path, 'rb') do |f| + until f.eof? + chunk = f.read(chunk_size) + retrieved += chunk.length yield(chunk, retrieved, file_size) end - request.run end + end - # Retrieve the file size - # @param options [Hash] - # @return [Integer] the size of the requested file - def get_file_size(options) - url = options.fetch(:url) - headers = options.fetch(:headers) - file_size = options.fetch(:file_size) - - case url.scheme - when 'file' - File.size(url.path) - when /https?/ - response = Typhoeus.head(url.to_s, headers: headers) - length_value = response.headers['Content-Length'] || file_size - length_value.to_i - else - raise URI::BadURIError, "Unknown URI scheme: #{url.scheme}" - end + # Retrieve a resource over the HTTP + # @param options [Hash] + def retrieve_http(options) + file_size = options.fetch(:file_size) + headers = options.fetch(:headers) + url = options.fetch(:url) + retrieved = 0 + + request = Typhoeus::Request.new(url.to_s, method: :get, headers: headers) + request.on_headers do |response| + raise DownloadError.new("#{self.class}: Failed to download #{url}: Status Code: #{response.code}", response) unless response.code == 200 end + request.on_body do |chunk| + retrieved += chunk.bytesize + yield(chunk, retrieved, file_size) + end + request.run + end + + # Retrieve the file size + # @param options [Hash] + # @return [Integer] the size of the requested file + def get_file_size(options) + url = options.fetch(:url) + headers = options.fetch(:headers) + file_size = options.fetch(:file_size) + + case url.scheme + when 'file' + File.size(url.path) + when /https?/ + response = Typhoeus.head(url.to_s, headers: headers) + length_value = response.headers['Content-Length'] || file_size + length_value.to_i + else + raise URI::BadURIError, "Unknown URI scheme: #{url.scheme}" + end + end end end diff --git a/spec/features/test_compiling_stylesheets_spec.rb b/spec/features/test_compiling_stylesheets_spec.rb index af7b5eb5..b625e7af 100644 --- a/spec/features/test_compiling_stylesheets_spec.rb +++ b/spec/features/test_compiling_stylesheets_spec.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true -describe 'Compiling the stylesheets', type: :feature do - routes { Rails.application.class.routes } +describe 'Compiling the stylesheets', type: :feature, js: true do + #routes do + # Rails.application.class.routes + #end + it 'does not raise errors' do visit '/' expect(page).not_to have_content 'Sass::SyntaxError' From dbedb5df098fe411b33092c09a7e8f680547679a Mon Sep 17 00:00:00 2001 From: "James R. Griffin III" Date: Thu, 11 Jun 2020 10:39:40 -0400 Subject: [PATCH 3/3] Updating the support for Webpacker integration for the JavaScript dependencies; Porting the TreeTable plugin into a separate file for usage in Webpack (for Rails 6.x support); Fixing a Bootstrap Modal bug for the initial rendering of the dialogue box; Ensuring that a valid AWS region is used for the test suites --- .rubocop.yml | 7 + .../javascripts/browse_everything/behavior.js | 24 +- app/assets/javascripts/treetable.webpack.js | 687 ++++++++++++++++++ app/helpers/browse_everything_helper.rb | 4 + app/views/browse_everything/_files.html.erb | 5 +- lib/browse_everything/browser.rb | 12 +- .../test_compiling_stylesheets_spec.rb | 4 - spec/spec_helper.rb | 2 +- spec/test_app_templates/Gemfile.extra | 9 + .../lib/generators/test_app_generator.rb | 57 +- 10 files changed, 788 insertions(+), 23 deletions(-) create mode 100644 app/assets/javascripts/treetable.webpack.js diff --git a/.rubocop.yml b/.rubocop.yml index cb1468bb..4e83743b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -28,6 +28,10 @@ Metrics/ClassLength: Exclude: - 'lib/browse_everything/driver/google_drive.rb' +Metrics/MethodLength: + Exclude: + - 'spec/test_app_templates/lib/generators/test_app_generator.rb' + Naming/FileName: Exclude: - 'browse-everything.gemspec' @@ -72,3 +76,6 @@ Style/MixinUsage: Style/NumericLiterals: MinDigits: 7 +Style/RedundantBegin: + Exclude: + - 'lib/browse_everything/browser.rb' diff --git a/app/assets/javascripts/browse_everything/behavior.js b/app/assets/javascripts/browse_everything/behavior.js index c2dba718..cf0f0fe2 100644 --- a/app/assets/javascripts/browse_everything/behavior.js +++ b/app/assets/javascripts/browse_everything/behavior.js @@ -262,21 +262,29 @@ $(function () { $.fn.browseEverything = function (options) { var ctx = $(this).data('ev-state'); + + // Try and load the options from the HTML data attributes if (ctx == null && options == null) { options = $(this).data(); } + if (options != null) { ctx = initialize(this[0], options); - $(this).click(function () { - dialog.data('ev-state', ctx); - return dialog.load(ctx.opts.route, function () { - setTimeout(refreshFiles, 500); - ctx.callbacks.show.fire(); - return dialog.modal('show'); - }); - }); } + $(this).click(function () { + dialog.data('ev-state', ctx); + return dialog.load(ctx.opts.route, function () { + setTimeout(refreshFiles, 50); + ctx.callbacks.show.fire(); + dialog.removeClass('fade') + .removeClass('in') + .addClass('show'); + + return dialog.modal('show'); + }); + }); + if (ctx) { return ctx.callback_proxy; } else { diff --git a/app/assets/javascripts/treetable.webpack.js b/app/assets/javascripts/treetable.webpack.js new file mode 100644 index 00000000..8baa2de2 --- /dev/null +++ b/app/assets/javascripts/treetable.webpack.js @@ -0,0 +1,687 @@ +/* + * jQuery treetable Plugin 3.2.0 + * http://ludo.cubicphuse.nl/jquery-treetable + * + * Copyright 2013, Ludo van den Boom + * Dual licensed under the MIT or GPL Version 2 licenses. + */ + +/** + * Callback defining the Node Class + */ +var defineNode = function() { + + function Node(row, tree, settings) { + var parentId; + + this.row = row; + this.tree = tree; + this.settings = settings; + + // TODO Ensure id/parentId is always a string (not int) + this.id = this.row.data(this.settings.nodeIdAttr); + + // TODO Move this to a setParentId function? + parentId = this.row.data(this.settings.parentIdAttr); + if (parentId != null && parentId !== "") { + this.parentId = parentId; + } + + this.treeCell = jQuery(this.row.children(this.settings.columnElType)[this.settings.column]); + this.expander = jQuery(this.settings.expanderTemplate); + this.indenter = jQuery(this.settings.indenterTemplate); + this.children = []; + this.initialized = false; + this.treeCell.prepend(this.indenter); + } + + Node.prototype.addChild = function(child) { + return this.children.push(child); + }; + + Node.prototype.ancestors = function() { + var ancestors, node; + node = this; + ancestors = []; + while (node = node.parentNode()) { + ancestors.push(node); + } + return ancestors; + }; + + Node.prototype.collapse = function() { + if (this.collapsed()) { + return this; + } + + this.row.removeClass("expanded").addClass("collapsed"); + + this._hideChildren(); + this.expander.attr("title", this.settings.stringExpand); + + if (this.initialized && this.settings.onNodeCollapse != null) { + this.settings.onNodeCollapse.apply(this); + } + + return this; + }; + + Node.prototype.collapsed = function() { + return this.row.hasClass("collapsed"); + }; + + // TODO destroy: remove event handlers, expander, indenter, etc. + + Node.prototype.expand = function() { + if (this.expanded()) { + return this; + } + + this.row.removeClass("collapsed").addClass("expanded"); + + if (this.initialized && this.settings.onNodeExpand != null) { + this.settings.onNodeExpand.apply(this); + } + + if (jQuery(this.row).is(":visible")) { + this._showChildren(); + } + + this.expander.attr("title", this.settings.stringCollapse); + + return this; + }; + + Node.prototype.expanded = function() { + return this.row.hasClass("expanded"); + }; + + Node.prototype.hide = function() { + this._hideChildren(); + this.row.hide(); + return this; + }; + + Node.prototype.isBranchNode = function() { + if(this.children.length > 0 || this.row.data(this.settings.branchAttr) === true) { + return true; + } else { + return false; + } + }; + + Node.prototype.updateBranchLeafClass = function(){ + this.row.removeClass('branch'); + this.row.removeClass('leaf'); + this.row.addClass(this.isBranchNode() ? 'branch' : 'leaf'); + }; + + Node.prototype.level = function() { + return this.ancestors().length; + }; + + Node.prototype.parentNode = function() { + if (this.parentId != null) { + return this.tree[this.parentId]; + } else { + return null; + } + }; + + Node.prototype.removeChild = function(child) { + var i = jQuery.inArray(child, this.children); + return this.children.splice(i, 1) + }; + + Node.prototype.render = function() { + var handler, + settings = this.settings, + target; + + if (settings.expandable === true && this.isBranchNode()) { + handler = function(e) { + jQuery(this).parents("table").treetable("node", jQuery(this).parents("tr").data(settings.nodeIdAttr)).toggle(); + return e.preventDefault(); + }; + + this.indenter.html(this.expander); + target = settings.clickableNodeNames === true ? this.treeCell : this.expander; + + target.off("click.treetable").on("click.treetable", handler); + target.off("keydown.treetable").on("keydown.treetable", function(e) { + if (e.keyCode == 13) { + handler.apply(this, [e]); + } + }); + } + + this.indenter[0].style.paddingLeft = "" + (this.level() * settings.indent) + "px"; + + return this; + }; + + Node.prototype.reveal = function() { + if (this.parentId != null) { + this.parentNode().reveal(); + } + return this.expand(); + }; + + Node.prototype.setParent = function(node) { + if (this.parentId != null) { + this.tree[this.parentId].removeChild(this); + } + this.parentId = node.id; + this.row.data(this.settings.parentIdAttr, node.id); + return node.addChild(this); + }; + + Node.prototype.show = function() { + if (!this.initialized) { + this._initialize(); + } + this.row.show(); + if (this.expanded()) { + this._showChildren(); + } + return this; + }; + + Node.prototype.toggle = function() { + if (this.expanded()) { + this.collapse(); + } else { + this.expand(); + } + return this; + }; + + Node.prototype._hideChildren = function() { + var child, _i, _len, _ref, _results; + _ref = this.children; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + _results.push(child.hide()); + } + return _results; + }; + + Node.prototype._initialize = function() { + var settings = this.settings; + + this.render(); + + if (settings.expandable === true && settings.initialState === "collapsed") { + this.collapse(); + } else { + this.expand(); + } + + if (settings.onNodeInitialized != null) { + settings.onNodeInitialized.apply(this); + } + + return this.initialized = true; + }; + + Node.prototype._showChildren = function() { + var child, _i, _len, _ref, _results; + _ref = this.children; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + _results.push(child.show()); + } + return _results; + }; + + return Node; +} + +var Node = defineNode() + +/** + * Callback defining the Tree Class + */ +var defineTree = function() { + /** + * Class modeling trees of nodes + */ + + function Tree(table, settings) { + this.table = table; + this.settings = settings; + this.tree = {}; + + // Cache the nodes and roots in simple arrays for quick access/iteration + this.nodes = []; + this.roots = []; + } + + Tree.prototype.collapseAll = function() { + var node, _i, _len, _ref, _results; + _ref = this.nodes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + _results.push(node.collapse()); + } + return _results; + }; + + Tree.prototype.expandAll = function() { + var node, _i, _len, _ref, _results; + _ref = this.nodes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + _results.push(node.expand()); + } + return _results; + }; + + Tree.prototype.findLastNode = function (node) { + if (node.children.length > 0) { + return this.findLastNode(node.children[node.children.length - 1]); + } else { + return node; + } + }; + + Tree.prototype.loadRows = function(rows) { + var node, row, i; + + if (rows != null) { + for (i = 0; i < rows.length; i++) { + row = jQuery(rows[i]); + + if (row.data(this.settings.nodeIdAttr) != null) { + node = new Node(row, this.tree, this.settings); + this.nodes.push(node); + this.tree[node.id] = node; + + if (node.parentId != null && this.tree[node.parentId]) { + this.tree[node.parentId].addChild(node); + } else { + this.roots.push(node); + } + } + } + } + + for (i = 0; i < this.nodes.length; i++) { + node = this.nodes[i].updateBranchLeafClass(); + } + + return this; + }; + + Tree.prototype.move = function(node, destination) { + // Conditions: + // 1: +node+ should not be inserted as a child of +node+ itself. + // 2: +destination+ should not be the same as +node+'s current parent (this + // prevents +node+ from being moved to the same location where it already + // is). + // 3: +node+ should not be inserted in a location in a branch if this would + // result in +node+ being an ancestor of itself. + var nodeParent = node.parentNode(); + if (node !== destination && destination.id !== node.parentId && jQuery.inArray(node, destination.ancestors()) === -1) { + node.setParent(destination); + this._moveRows(node, destination); + + // Re-render parentNode if this is its first child node, and therefore + // doesn't have the expander yet. + if (node.parentNode().children.length === 1) { + node.parentNode().render(); + } + } + + if(nodeParent){ + nodeParent.updateBranchLeafClass(); + } + if(node.parentNode()){ + node.parentNode().updateBranchLeafClass(); + } + node.updateBranchLeafClass(); + return this; + }; + + Tree.prototype.removeNode = function(node) { + // Recursively remove all descendants of +node+ + this.unloadBranch(node); + + // Remove node from DOM () + node.row.remove(); + + // Remove node from parent children list + if (node.parentId != null) { + node.parentNode().removeChild(node); + } + + // Clean up Tree object (so Node objects are GC-ed) + delete this.tree[node.id]; + this.nodes.splice(jQuery.inArray(node, this.nodes), 1); + + return this; + } + + Tree.prototype.render = function() { + var root, _i, _len, _ref; + _ref = this.roots; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + root = _ref[_i]; + + // Naming is confusing (show/render). I do not call render on node from + // here. + root.show(); + } + return this; + }; + + Tree.prototype.sortBranch = function(node, sortFun) { + // First sort internal array of children + node.children.sort(sortFun); + + // Next render rows in correct order on page + this._sortChildRows(node); + + return this; + }; + + Tree.prototype.unloadBranch = function(node) { + // Use a copy of the children array to not have other functions interfere + // with this function if they manipulate the children array + // (eg removeNode). + var children = node.children.slice(0), + i; + + for (i = 0; i < children.length; i++) { + this.removeNode(children[i]); + } + + // Reset node's collection of children + node.children = []; + + node.updateBranchLeafClass(); + + return this; + }; + + Tree.prototype._moveRows = function(node, destination) { + var children = node.children, i; + + node.row.insertAfter(destination.row); + node.render(); + + // Loop backwards through children to have them end up on UI in correct + // order (see #112) + for (i = children.length - 1; i >= 0; i--) { + this._moveRows(children[i], node); + } + }; + + // Special _moveRows case, move children to itself to force sorting + Tree.prototype._sortChildRows = function(parentNode) { + return this._moveRows(parentNode, parentNode); + }; + + return Tree; +}; + +var Tree = defineTree() + +/** + * Define the methods for the jQuery Plugin + */ +var methods = { + /** + * Constructor + */ + init: function(options, force) { + var settings; + + settings = jQuery.extend({ + branchAttr: "ttBranch", + clickableNodeNames: false, + column: 0, + columnElType: "td", // i.e. 'td', 'th' or 'td,th' + expandable: false, + expanderTemplate: " ", + indent: 19, + indenterTemplate: "", + initialState: "collapsed", + nodeIdAttr: "ttId", // maps to data-tt-id + parentIdAttr: "ttParentId", // maps to data-tt-parent-id + stringExpand: "Expand", + stringCollapse: "Collapse", + + // Events + onInitialized: null, + onNodeCollapse: null, + onNodeExpand: null, + onNodeInitialized: null + }, options); + + return this.each(function() { + var el = jQuery(this), tree; + + if (force || el.data("treetable") === undefined) { + tree = new Tree(this, settings); + tree.loadRows(this.rows).render(); + + el.addClass("treetable").data("treetable", tree); + + if (settings.onInitialized != null) { + settings.onInitialized.apply(tree); + } + } + + return el; + }); + }, + + /** + * Destructor + */ + destroy: function() { + return this.each(function() { + return jQuery(this).removeData("treetable").removeClass("treetable"); + }); + }, + + /** + * Collapsing all tree branches + */ + collapseAll: function() { + this.data("treetable").collapseAll(); + return this; + }, + + /** + * Collapsing a single tree branch + */ + collapseNode: function(id) { + var node = this.data("treetable").tree[id]; + + if (node) { + node.collapse(); + } else { + throw new Error("Unknown node '" + id + "'"); + } + + return this; + }, + + /** + * Expanding all tree branches + */ + expandAll: function() { + this.data("treetable").expandAll(); + return this; + }, + + /** + * Expanding a single tree branch + */ + expandNode: function(id) { + var node = this.data("treetable").tree[id]; + + if (node) { + if (!node.initialized) { + node._initialize(); + } + + node.expand(); + } else { + throw new Error("Unknown node '" + id + "'"); + } + + return this; + }, + + /** + * Load the markup for a single tree branch + */ + loadBranch: function(node, rows) { + var settings = this.data("treetable").settings, + tree = this.data("treetable").tree; + + // TODO Switch to $.parseHTML + rows = jQuery(rows); + + if (node == null) { // Inserting new root nodes + this.append(rows); + } else { + var lastNode = this.data("treetable").findLastNode(node); + rows.insertAfter(lastNode.row); + } + + this.data("treetable").loadRows(rows); + + // Make sure nodes are properly initialized + rows.filter("tr").each(function() { + tree[jQuery(this).data(settings.nodeIdAttr)].show(); + }); + + if (node != null) { + // Re-render parent to ensure expander icon is shown (#79) + node.render().expand(); + } + + return this; + }, + + /** + * + */ + move: function(nodeId, destinationId) { + var destination, node; + + node = this.data("treetable").tree[nodeId]; + destination = this.data("treetable").tree[destinationId]; + this.data("treetable").move(node, destination); + + return this; + }, + + /** + * + */ + node: function(id) { + return this.data("treetable").tree[id]; + }, + + /** + * + */ + removeNode: function(id) { + var node = this.data("treetable").tree[id]; + + if (node) { + this.data("treetable").removeNode(node); + } else { + throw new Error("Unknown node '" + id + "'"); + } + + return this; + }, + + /** + * + */ + reveal: function(id) { + var node = this.data("treetable").tree[id]; + + if (node) { + node.reveal(); + } else { + throw new Error("Unknown node '" + id + "'"); + } + + return this; + }, + + /** + * + */ + sortBranch: function(node, columnOrFunction) { + var settings = this.data("treetable").settings, + prepValue, + sortFun; + + columnOrFunction = columnOrFunction || settings.column; + sortFun = columnOrFunction; + + if (jQuery.isNumeric(columnOrFunction)) { + sortFun = function(a, b) { + var extractValue, valA, valB; + + extractValue = function(node) { + var val = node.row.find("td:eq(" + columnOrFunction + ")").text(); + // Ignore trailing/leading whitespace and use uppercase values for + // case insensitive ordering + return jQuery.trim(val).toUpperCase(); + } + + valA = extractValue(a); + valB = extractValue(b); + + if (valA < valB) return -1; + if (valA > valB) return 1; + return 0; + }; + } + + this.data("treetable").sortBranch(node, sortFun); + return this; + }, + + /** + * + */ + unloadBranch: function(node) { + this.data("treetable").unloadBranch(node); + return this; + } +}; + +/** + * Define the jQuery Plugin + * + */ +jQuery.fn.treetable = function(method) { + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } else { + return jQuery.error("Method " + method + " does not exist on jQuery.treetable"); + } +}; + +// Expose classes to world +document.TreeTable || (document.TreeTable = {}); +document.TreeTable.Node = Node; +document.TreeTable.Tree = Tree; + diff --git a/app/helpers/browse_everything_helper.rb b/app/helpers/browse_everything_helper.rb index 4d0b4459..cd1fa3f2 100644 --- a/app/helpers/browse_everything_helper.rb +++ b/app/helpers/browse_everything_helper.rb @@ -20,4 +20,8 @@ def is_acceptable?(file) acceptable_types << 'application/x-directory' acceptable_types.any? { |type| mime_match?(file.type, type) } end + + def file_size_to_human_size(file_size) + "#{file_size} bytes" + end end diff --git a/app/views/browse_everything/_files.html.erb b/app/views/browse_everything/_files.html.erb index 084b9f22..6daefe18 100644 --- a/app/views/browse_everything/_files.html.erb +++ b/app/views/browse_everything/_files.html.erb @@ -16,6 +16,7 @@ <% disabled = false %> <% else %> <% max_size = provider.config[:max_upload_file_size].to_i %> + <% max_human_size = file_size_to_human_size(max_size) %> <% disabled = file.size > max_size %> <% end %> @@ -28,7 +29,7 @@ <% if disabled %> - @@ -53,7 +54,7 @@ <% if file.size %> - <%= number_to_human_size(file.size).sub(/Bytes/,'bytes') %> + <%= file_size_to_human_size(file.size) %> <% else %> Unknown diff --git a/lib/browse_everything/browser.rb b/lib/browse_everything/browser.rb index 621450ab..24cf4314 100644 --- a/lib/browse_everything/browser.rb +++ b/lib/browse_everything/browser.rb @@ -15,11 +15,13 @@ def initialize(opts = {}) @providers = ActiveSupport::HashWithIndifferentAccess.new opts.each_pair do |driver_key, config| - driver = driver_key.to_s - driver_klass = BrowseEverything::Driver.const_get((config[:driver] || driver).camelize.to_sym) - @providers[driver_key] = driver_klass.new(config.merge(url_options: url_options)) - rescue NameError - Rails.logger.warn "Unknown provider: #{driver}" + begin + driver = driver_key.to_s + driver_klass = BrowseEverything::Driver.const_get((config[:driver] || driver).camelize.to_sym) + @providers[driver_key] = driver_klass.new(config.merge(url_options: url_options)) + rescue NameError + Rails.logger.warn "Unknown provider: #{driver}" + end end end diff --git a/spec/features/test_compiling_stylesheets_spec.rb b/spec/features/test_compiling_stylesheets_spec.rb index b625e7af..200b71fc 100644 --- a/spec/features/test_compiling_stylesheets_spec.rb +++ b/spec/features/test_compiling_stylesheets_spec.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true describe 'Compiling the stylesheets', type: :feature, js: true do - #routes do - # Rails.application.class.routes - #end - it 'does not raise errors' do visit '/' expect(page).not_to have_content 'Sass::SyntaxError' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5b421067..9b8f9109 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -87,7 +87,7 @@ def stub_configuration app_key: 'S3AppKey', app_secret: 'S3AppSecret', bucket: 's3.bucket', - region: 's3.region' + region: 'us-east-1' }) end diff --git a/spec/test_app_templates/Gemfile.extra b/spec/test_app_templates/Gemfile.extra index 8ac40553..fcf7bd46 100644 --- a/spec/test_app_templates/Gemfile.extra +++ b/spec/test_app_templates/Gemfile.extra @@ -1,6 +1,15 @@ gem 'jquery-rails' gem 'puma-rails' +if ENV['RAILS_VERSION'] + case ENV['RAILS_VERSION'] + when /6\./ + gem 'webpacker', '~> 4.0' + when /5\./ + gem 'webpacker', '~> 4.0' + end +end + if ENV['TEST_BOOTSTRAP'] == "3" gem "bootstrap-sass" else diff --git a/spec/test_app_templates/lib/generators/test_app_generator.rb b/spec/test_app_templates/lib/generators/test_app_generator.rb index 3daf36c1..8ec63419 100644 --- a/spec/test_app_templates/lib/generators/test_app_generator.rb +++ b/spec/test_app_templates/lib/generators/test_app_generator.rb @@ -28,11 +28,62 @@ def inject_css end def inject_javascript - insert_into_file 'app/assets/javascripts/application.js', after: '//= require_tree .' do - %( + if /^6\./.match?(Rails.version) + # Both jQuery and Twitter Bootstrap 3.x need to be added for legacy JavaScript + system('yarn add jquery@3.3.1') + system('yarn add bootstrap@3.4.1') + system('yarn install') + + # Adding the JavaScript module dependencies + insert_into_file 'app/javascript/packs/application.js', after: 'require("channels")' do + %( + require("jquery") + require("bootstrap") + ) + end + + # Here the JavaScript needs to be injected for Webpacker + insert_into_file 'config/webpack/environment.js', after: "const { environment } = require('@rails/webpacker')" do + %( + const webpack = require('webpack') + + environment.plugins.prepend('Provide', + new webpack.ProvidePlugin({ + $: 'jquery/src/jquery', + jQuery: 'jquery/src/jquery' + }) + ) + ) + end + + # Copy the TreeTable JavaScript + copy_file('../../app/assets/javascripts/treetable.webpack.js', 'app/javascript/packs/treetable.js') + + # Copy the browse_everything JavaScript + copy_file('../../app/assets/javascripts/browse_everything/behavior.js', 'app/javascript/packs/browse-everything.js') + + # Adding the JavaScript module dependencies + insert_into_file 'app/javascript/packs/browse-everything.js', before: "'use strict';" do + %( + require("bootstrap") + require("./treetable") + ) + end + + # Load the new packs into the view + insert_into_file 'app/views/layouts/application.html.erb', before: '' do + %( + <%= javascript_pack_tag 'treetable', 'data-turbolinks-track': 'reload' %> + <%= javascript_pack_tag 'browse-everything', 'data-turbolinks-track': 'reload' %> + ) + end + else + insert_into_file 'app/assets/javascripts/application.js', after: '//= require_tree .' do + %( //= require jquery //= require browse_everything - ) + ) + end end end