From 18de43171e8275f16faec78f51e8e09a5d456a12 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Mon, 15 Apr 2024 20:19:30 +0000 Subject: [PATCH 01/41] Upgrade from Bulkrax 2.3.0 to 8.0.0, no configuration just yet --- Gemfile | 2 +- Gemfile.lock | 17 ++-- app/models/ability.rb | 9 ++ bin/importer | 146 +++++++++++++++++++++++++++++++++ config/initializers/bulkrax.rb | 49 +++++------ 5 files changed, 193 insertions(+), 30 deletions(-) create mode 100644 bin/importer diff --git a/Gemfile b/Gemfile index 576944c0..7b903a80 100644 --- a/Gemfile +++ b/Gemfile @@ -65,7 +65,7 @@ gem 'riiif', '~> 2.0' gem 'cookies_eu' #gem 'bulkrax', git: 'https://github.com/samvera-labs/bulkrax.git' -gem 'bulkrax', '2.3.0' +gem 'bulkrax', '8.0.0' gem 'willow_sword', github: 'notch8/willow_sword' diff --git a/Gemfile.lock b/Gemfile.lock index 24c7f5d3..73b17921 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -104,7 +104,7 @@ GEM babel-transpiler (0.7.0) babel-source (>= 4.0, < 6) execjs (~> 2.0) - bagit (0.4.5) + bagit (0.4.6) docopt (~> 0.5.0) validatable (~> 1.6) base64 (0.2.0) @@ -162,18 +162,21 @@ GEM signet (~> 0.8) typhoeus builder (3.2.4) - bulkrax (2.3.0) - bagit (~> 0.4) + bulkrax (8.0.0) + bagit (~> 0.4.6) coderay + denormalize_fields iso8601 (~> 0.9.0) kaminari language_list (~> 1.2, >= 1.2.1) - libxml-ruby (~> 3.1.0) + libxml-ruby (~> 3.2.4) loofah (>= 2.2.3) + marcel oai (>= 0.4, < 2.x) rack (>= 2.0.6) rails (>= 5.1.6) rdf (>= 2.0.2, < 4.0) + rubyzip simple_form byebug (11.1.3) cancancan (1.17.0) @@ -221,6 +224,8 @@ GEM declarative-builder (0.1.0) declarative-option (< 0.2.0) declarative-option (0.1.0) + denormalize_fields (1.3.0) + activerecord (>= 4.1.14, < 8.0.0) deprecation (1.1.0) activesupport devise (4.9.2) @@ -577,7 +582,7 @@ GEM multi_json libv8-node (16.19.0.1-x86_64-darwin) libv8-node (16.19.0.1-x86_64-linux) - libxml-ruby (3.1.0) + libxml-ruby (3.2.4) link_header (0.0.8) linkeddata (3.1.6) equivalent-xml (~> 0.6) @@ -1062,7 +1067,7 @@ DEPENDENCIES blacklight_range_limit bootsnap (>= 1.1.0) bootstrap-sass (~> 3.0) - bulkrax (= 2.3.0) + bulkrax (= 8.0.0) byebug capybara (>= 2.15) chosen-rails diff --git a/app/models/ability.rb b/app/models/ability.rb index 783958ee..083b4771 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -42,4 +42,13 @@ def contentadmins_can_create_curation_concerns can :index, Hydra::AccessControls::Embargo can :index, Hydra::AccessControls::Lease end + + # Added for Bulkrax 5.0.0+ + def can_import_works? + can_create_any_work? + end + + def can_export_works? + can_create_any_work? + end end diff --git a/bin/importer b/bin/importer new file mode 100644 index 00000000..996d805d --- /dev/null +++ b/bin/importer @@ -0,0 +1,146 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative '../config/environment' + +require 'slop' + +def main(opts = {}) + check_required_params + + update = opts[:importer_id].present? + port = opts[:port].presence + url = build_url(opts.delete(:importer_id), opts.delete(:url), port) + + headers = { 'Content-Type' => 'application/json' } + headers['Authorization'] = "Token: #{opts.delete(:auth_token)}" + params = build_params(opts) + + logger.info("POST to #{url} - PARAMS #{params}") + + conn = Faraday.new( + url: url, + headers: headers + ) + + response = if update + conn.put do |request| + request.body = params.to_json + end + else + conn.post do |request| + request.body = params.to_json + end + end + + puts "#{response.status} - #{response.body.truncate(200)}" +end + +def check_required_params + if opts[:importer_id].blank? && invalid?(opts) + puts 'Missing required parameters' + help + end + + if opts[:auth_token].blank? # rubocop:disable Style/GuardClause + puts 'Missing Authentication Token --auth_token' + exit + end +end + +def invalid?(opts) + required_params.each do |p| + return true if opts[p.to_sym].blank? + end + return false +end + +def required_params + Bulkrax.api_definition['bulkrax']['importer'].map { |key, value| key if value['required'] == true }.compact +end + +def build_params(opts = {}) + params = {} + params[:commit] = opts.delete(:commit) + parser_fields = { + metadata_file_name: opts.delete(:metadata_file_name), + metadata_format: opts.delete(:metadata_format), + rights_statement: opts.delete(:rights_statement), + override_rights_statement: opts.delete(:override_rights_statement), + import_file_path: opts.delete(:import_file_path), + metadata_prefix: opts.delete(:metadata_prefix), + set: opts.delete(:set), + collection_name: opts.delete(:collection_name) + }.compact + params[:importer] = opts.compact + params[:importer][:user_id] = opts.delete(:user_id) + params[:importer][:admin_set_id] = opts.delete(:admin_set_id) + params[:importer][:parser_fields] = parser_fields || {} + return params.compact +end + +def build_url(importer_id, url, port = nil) + if url.nil? + protocol = Rails.application.config.force_ssl ? 'https://' : 'http://' + host = Rails.application.config.action_mailer.default_url_options[:host] + url = "#{protocol}#{host}" + url = "#{url}:#{port}" if port + end + path = Bulkrax::Engine.routes.url_helpers.polymorphic_path(Bulkrax::Importer) + url = File.join(url, path) + url = File.join(url, importer_id) if importer_id + return url +end + +def logger + Rails.logger +end + +def version + puts "Bulkrax #{Bulkrax::VERSION}" + puts "Slop #{Slop::VERSION}" +end + +# Format the help for the CLI +def help + puts 'CREATE:' + puts ' bin/importer --name "My Import" --parser_klass Bulkrax::CsvParser --commit "Create and Import" --import_file_path /data/tmp/import.csv --auth_token 12345' + puts 'UPDATE:' + puts ' bin/importer --importer_id 1 --commit "Update and Re-Import (update metadata only)" --import_file_path /data/tmp/import.csv --auth_token 12345' + puts 'PARAMETERS:' + Bulkrax.api_definition['bulkrax']['importer'].each_pair do |key, value| + next if key == 'parser_fields' + puts " --#{key}" + value.each_pair do |k, v| + next if k == 'contained_in' + puts " #{k}: #{v}" + end + end + puts ' --url' + puts " Repository URL" + exit +end + +# Setup the options +options = Slop.parse do |o| + o.on '--version', 'Print the version' do + version + exit + end + + o.on '--help', 'Print help' do + help + exit + end + + Bulkrax.api_definition['bulkrax']['importer'].each_pair do |key, value| + if value['required'].blank? + o.string "--#{key}", value['definition'], default: nil + else + o.string "--#{key}", value['definition'] + end + end + o.string '--url', 'Repository URL' +end + +main(options.to_hash) diff --git a/config/initializers/bulkrax.rb b/config/initializers/bulkrax.rb index ca5d0d08..731a35c5 100644 --- a/config/initializers/bulkrax.rb +++ b/config/initializers/bulkrax.rb @@ -7,8 +7,13 @@ # ] # WorkType to use as the default if none is specified in the import - # Default is the first returned by Hyrax.config.curation_concerns - config.default_work_type = 'GwWork' + # Default is the first returned by Hyrax.config.curation_concerns, stringified + # config.default_work_type = "MyWork" + + # Factory Class to use when generating and saving objects + config.object_factory = Bulkrax::ObjectFactory + # Use this for a Postgres-backed Valkyrized Hyrax + # config.object_factory = Bulkrax::ValkyrieObjectFactory # Path to store pending imports # config.import_path = 'tmp/imports' @@ -33,24 +38,6 @@ # config.field_mappings = { # "Bulkrax::OaiDcParser" => { **individual field mappings go here*** } # } - config.field_mappings['Bulkrax::CsvParser'] = { - "contributor" => { from: ["contributor", split: ';' ] }, - "creator" => { from: ["creator"], split: "; " }, - "date_created" => { from: ["date_created"], split: ';' }, - "description" => { from: ["description"] }, - "identifier" => { from: ["identifier"], split: ';' }, - "related_url" => { from: ["related_url"] }, - "rights_statement" => { from: ["rights_statement"] }, - "license" => { from: ["license"], split: ';' }, - "source_identifier" => { from: ["source_identifier"] }, - "keyword" => { from: ["keyword"], split: ';' }, - "title" => { from: ["title"] }, - "doi" => {from: ["doi"], split: ';'}, - "resource_type" => { from: ["resource_type"], split: ';' }, - "gw_affiliation" => { from: ["gw_affiliation"], split: ';' }, - 'parents' => { from: ['parents'], related_parents_field_mapping: true }, - 'children' => { from: ['children'], related_children_field_mapping: true } - } # Add to, or change existing mappings as follows # e.g. to exclude date @@ -62,7 +49,7 @@ # (For more info on importing relationships, see Bulkrax Wiki: https://github.com/samvera-labs/bulkrax/wiki/Configuring-Bulkrax#parent-child-relationship-field-mappings) # # # e.g. to add the required source_identifier field - # # config.field_mappings["Bulkrax::CsvParser"]["source_id"] = { from: ["old_source_id"], source_identifier: true } + # # config.field_mappings["Bulkrax::CsvParser"]["source_id"] = { from: ["old_source_id"], source_identifier: true, search_field: 'source_id_sim' } # If you want Bulkrax to fill in source_identifiers for you, see below # To duplicate a set of mappings from one parser to another @@ -75,11 +62,27 @@ # It is given two aruguments, self at the time of call and the index of the reocrd # config.fill_in_blank_source_identifiers = ->(parser, index) { "b-#{parser.importer.id}-#{index}"} # or use a uuid - #config.fill_in_blank_source_identifiers = ->(parser, index) { SecureRandom.uuid } + # config.fill_in_blank_source_identifiers = ->(parser, index) { SecureRandom.uuid } # Properties that should not be used in imports/exports. They are reserved for use by Hyrax. # config.reserved_properties += ['my_field'] + + # List of Questioning Authority properties that are controlled via YAML files in + # the config/authorities/ directory. For example, the :rights_statement property + # is controlled by the active terms in config/authorities/rights_statements.yml + # Defaults: 'rights_statement' and 'license' + # config.qa_controlled_properties += ['my_field'] + + # Specify the delimiter regular expression for splitting an attribute's values into a multi-value array. + # config.multi_value_element_split_on = /\s*[:;|]\s*/.freeze + + # Specify the delimiter for joining an attribute's multi-value array into a string. Note: the + # specific delimeter should likely be present in the multi_value_element_split_on expression. + # config.multi_value_element_join_on = ' | ' end # Sidebar for hyrax 3+ support -#Hyrax::DashboardController.sidebar_partials[:repository_content] << "hyrax/dashboard/sidebar/bulkrax_sidebar_additions" if Object.const_defined?(:Hyrax) && ::Hyrax::DashboardController&.respond_to?(:sidebar_partials) \ No newline at end of file +# rubocop:disable Style/IfUnlessModifier +if Object.const_defined?(:Hyrax) && ::Hyrax::DashboardController&.respond_to?(:sidebar_partials) + Hyrax::DashboardController.sidebar_partials[:repository_content] << "hyrax/dashboard/sidebar/bulkrax_sidebar_additions" +end From c9a3b2ba838078b9ab164ed7c39baa86f2c86f16 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Sun, 21 Apr 2024 05:17:11 +0000 Subject: [PATCH 02/41] Fixes uploads-with-files issue by pointing to bulkrax branch --- Gemfile | 3 +-- Gemfile.lock | 40 +++++++++++++++++++--------------- config/initializers/bulkrax.rb | 18 +++++++++++---- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/Gemfile b/Gemfile index 7b903a80..bcc36815 100644 --- a/Gemfile +++ b/Gemfile @@ -64,8 +64,7 @@ gem 'riiif', '~> 2.0' gem 'cookies_eu' -#gem 'bulkrax', git: 'https://github.com/samvera-labs/bulkrax.git' -gem 'bulkrax', '8.0.0' +gem 'bulkrax', git: 'https://github.com/samvera/bulkrax.git', branch: 'i951-false-object-bug' gem 'willow_sword', github: 'notch8/willow_sword' diff --git a/Gemfile.lock b/Gemfile.lock index 73b17921..733ba0e0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,6 +7,28 @@ GIT rails (>= 5.1.6) rubyzip (>= 1.0.0) +GIT + remote: https://github.com/samvera/bulkrax.git + revision: 091a8b9f1fdfb8101b1910d8977deb247b2d3213 + branch: i951-false-object-bug + specs: + bulkrax (8.0.0) + bagit (~> 0.4.6) + coderay + denormalize_fields + iso8601 (~> 0.9.0) + kaminari + language_list (~> 1.2, >= 1.2.1) + libxml-ruby (~> 3.2.4) + loofah (>= 2.2.3) + marcel + oai (>= 0.4, < 2.x) + rack (>= 2.0.6) + rails (>= 5.1.6) + rdf (>= 2.0.2, < 4.0) + rubyzip + simple_form + GEM remote: https://rubygems.org/ specs: @@ -162,22 +184,6 @@ GEM signet (~> 0.8) typhoeus builder (3.2.4) - bulkrax (8.0.0) - bagit (~> 0.4.6) - coderay - denormalize_fields - iso8601 (~> 0.9.0) - kaminari - language_list (~> 1.2, >= 1.2.1) - libxml-ruby (~> 3.2.4) - loofah (>= 2.2.3) - marcel - oai (>= 0.4, < 2.x) - rack (>= 2.0.6) - rails (>= 5.1.6) - rdf (>= 2.0.2, < 4.0) - rubyzip - simple_form byebug (11.1.3) cancancan (1.17.0) capybara (3.39.2) @@ -1067,7 +1073,7 @@ DEPENDENCIES blacklight_range_limit bootsnap (>= 1.1.0) bootstrap-sass (~> 3.0) - bulkrax (= 8.0.0) + bulkrax! byebug capybara (>= 2.15) chosen-rails diff --git a/config/initializers/bulkrax.rb b/config/initializers/bulkrax.rb index 731a35c5..3ddfc5b1 100644 --- a/config/initializers/bulkrax.rb +++ b/config/initializers/bulkrax.rb @@ -8,7 +8,7 @@ # WorkType to use as the default if none is specified in the import # Default is the first returned by Hyrax.config.curation_concerns, stringified - # config.default_work_type = "MyWork" + config.default_work_type = "GwWork" # Factory Class to use when generating and saving objects config.object_factory = Bulkrax::ObjectFactory @@ -39,6 +39,15 @@ # "Bulkrax::OaiDcParser" => { **individual field mappings go here*** } # } + # This config may seem redundant, but (as of bulkrax 6.0.1) including it + # seems to prevent the object from being created with a visible metadata + # field of Source with a value that's a big ugly uuid + config.field_mappings['Bulkrax::CsvParser'] = { + 'source_identifier' => { from: ['source_identifier'], source_identifier: true, search_field: 'source_id_sim' }, + 'keyword' => { from: ['keyword'], split: true }, + 'file' => { from: ['file'], split: '\;' } + } + # Add to, or change existing mappings as follows # e.g. to exclude date # config.field_mappings["Bulkrax::OaiDcParser"]["date"] = { from: ["date"], excluded: true } @@ -62,7 +71,7 @@ # It is given two aruguments, self at the time of call and the index of the reocrd # config.fill_in_blank_source_identifiers = ->(parser, index) { "b-#{parser.importer.id}-#{index}"} # or use a uuid - # config.fill_in_blank_source_identifiers = ->(parser, index) { SecureRandom.uuid } + config.fill_in_blank_source_identifiers = ->(parser, index) { SecureRandom.uuid } # Properties that should not be used in imports/exports. They are reserved for use by Hyrax. # config.reserved_properties += ['my_field'] @@ -74,11 +83,12 @@ # config.qa_controlled_properties += ['my_field'] # Specify the delimiter regular expression for splitting an attribute's values into a multi-value array. - # config.multi_value_element_split_on = /\s*[:;|]\s*/.freeze + #config.multi_value_element_split_on = /\s*[:;|]\s*/.freeze + config.multi_value_element_split_on = ';'.freeze # Specify the delimiter for joining an attribute's multi-value array into a string. Note: the # specific delimeter should likely be present in the multi_value_element_split_on expression. - # config.multi_value_element_join_on = ' | ' + config.multi_value_element_join_on = ' | ' end # Sidebar for hyrax 3+ support From b73746c0039dc9756614d44ea425212d4f84d88b Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Sun, 28 Apr 2024 22:45:23 +0000 Subject: [PATCH 03/41] Work in Progress - tasks to ingest ProQuest ETD zips --- lib/tasks/ingest_etd_new.rake | 101 ++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 lib/tasks/ingest_etd_new.rake diff --git a/lib/tasks/ingest_etd_new.rake b/lib/tasks/ingest_etd_new.rake new file mode 100644 index 00000000..2d909222 --- /dev/null +++ b/lib/tasks/ingest_etd_new.rake @@ -0,0 +1,101 @@ +require 'nokogiri' +require 'rake' +require 'zip' + +namespace :gwss do + desc "Creates a bulkrax zip for all of the ProQuest ETD zip files in a folder" + task :ingest_pq_etds, [:filepath] do |t, args| + # create folder for metadata.csv and files folder + Dir.mkdir('bulkrax_zip') unless File.exists?('bulkrax_zip') + + # get all ETD zip files in the args.filepath folder + path_to_zips = args.filepath + + works_metadata = [] + filesets_metadata = [] + + zip_paths = Dir.glob("#{path_to_zips}/etdadmin*.zip") + puts("zip_paths: #{zip_paths}") + zip_paths.each do |zip_path| + # for each ETD zip file: + puts("Processing #{zip_path}") + zip_file = Zip::File.open(zip_path) + zip_file_basename = File.basename(zip_path, '.zip') + Dir.mkdir("/tmp/#{zip_file_basename}") unless File.exists?(zip_file_basename) + zip_file.each do |component_file| + puts(" Extracting #{component_file.name}") + zip_file.extract(component_file, "/tmp/#{zip_file_basename}/#{component_file.name}") + end + # 1. extract the work metdata and add to the works metadata array + # 2. extract the embargo info + # 3. add files info (w/embargo info) to the filesets array + # 4. copy the file attachments to the 'files' folder + end + + # create metadata CSV from the works metadata array and the filesets array + # zip up the working folder + end + + desc "Ingests ProQuest XML metadata for a single ETD" + task :ingest_etd_new, [:filepath] do |t, args| + + # attr_accessor :etd_doc, :repo_metadata + + def extract_zip(zip_file_path) + puts("filepath is #{zip_file_path}") + zip_file = Zip::File.open(zip_file_path) + zip_file_basename = File.basename(zip_file_path, '.zip') + Dir.mkdir(zip_file_basename) unless File.exists?(zip_file_basename) + + zip_file.each do |component_file| + puts "Extracting #{component_file.name}" + zip_file.extract(component_file, "#{zip_file_basename}/#{component_file.name}") + end + + # return path to files + zip_file_basename + end + + def get_metadata_doc_path(pq_files_dir) + xml_paths = Dir.glob("#{pq_files_dir}/*.xml") + pq_xml_file_path = xml_paths.first + pq_xml_file_path + end + + def get_etd_doc(xml_file_path) + File.open(xml_file_path) { |f| Nokogiri::XML(f) } + end + + def extract_metadata(doc) + repo_metadata = Hash.new + end + + def get_title(doc) + doc.at_xpath("//DISS_description/DISS_title").text + end + + def get_language(doc) + doc.at_xpath("//DISS_description/DISS_categorization/DISS_language").text + end + + def get_abstract(doc) + # TODO: + abstract_text_array = [] + doc.xpath("//DISS_content/DISS_abstract/DISS_para").each do |p| + abstract_text_array << p.text + end + abstract_text = Nokogiri::HTML(abstract_text_array.join("\n")).text + end + + files_dir = extract_zip(args.filepath) + xml_doc_path = get_metadata_doc_path(files_dir) + etd_doc = get_etd_doc(xml_doc_path) + + repo_metadata = Hash.new + repo_metadata['title'] = get_title(etd_doc) + repo_metadata['language'] = get_language(etd_doc) + repo_metadata['description'] = get_abstract(etd_doc) + + puts repo_metadata + end +end From c9b1c42916c924643079535550a86e8dd6749d8f Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Wed, 1 May 2024 15:15:37 +0000 Subject: [PATCH 04/41] WIP - next need to create CSV from array of metadata hashes --- lib/tasks/ingest_etd_new.rake | 78 +++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/lib/tasks/ingest_etd_new.rake b/lib/tasks/ingest_etd_new.rake index 2d909222..0483c9f9 100644 --- a/lib/tasks/ingest_etd_new.rake +++ b/lib/tasks/ingest_etd_new.rake @@ -5,8 +5,57 @@ require 'zip' namespace :gwss do desc "Creates a bulkrax zip for all of the ProQuest ETD zip files in a folder" task :ingest_pq_etds, [:filepath] do |t, args| + + def get_metadata_doc_path(pq_files_dir) + xml_paths = Dir.glob("#{pq_files_dir}/*_DATA.xml") + pq_xml_file_path = xml_paths.first + pq_xml_file_path + end + + def get_etd_doc(xml_file_path) + File.open(xml_file_path) { |f| Nokogiri::XML(f) } + end + + def get_attachment_file_paths(pq_files_dir) + Dir.glob("#{pq_files_dir}/**/*") + end + + def get_title(doc) + doc.at_xpath("//DISS_description/DISS_title").text + end + + def get_language(doc) + doc.at_xpath("//DISS_description/DISS_categorization/DISS_language").text + end + + def get_abstract(doc) + # TODO: + abstract_text_array = [] + doc.xpath("//DISS_content/DISS_abstract/DISS_para").each do |p| + abstract_text_array << p.text + end + abstract_text = Nokogiri::HTML(abstract_text_array.join("\n")).text + end + + def extract_metadata(doc) + repo_metadata = Hash.new + repo_metadata['model'] = 'GwEtd' + repo_metadata['title'] = get_title(doc) + repo_metadata['language'] = get_language(doc) + repo_metadata['description'] = get_abstract(doc) + repo_metadata + end + + def all_keys(hash_array) + keys_set = Set[] + hash_array.each |h| + h.keys.each { |k| keys_set << k } + end + keys_set.to_a + end + # create folder for metadata.csv and files folder - Dir.mkdir('bulkrax_zip') unless File.exists?('bulkrax_zip') + Dir.mkdir("#{ENV['TEMP_FILE_BASE']}/bulkrax_zip") unless File.exists?("#{ENV['TEMP_FILE_BASE']}/bulkrax_zip") # get all ETD zip files in the args.filepath folder path_to_zips = args.filepath @@ -21,12 +70,35 @@ namespace :gwss do puts("Processing #{zip_path}") zip_file = Zip::File.open(zip_path) zip_file_basename = File.basename(zip_path, '.zip') - Dir.mkdir("/tmp/#{zip_file_basename}") unless File.exists?(zip_file_basename) + Dir.mkdir("#{ENV['TEMP_FILE_BASE']}/etds") unless File.exists?("#{ENV['TEMP_FILE_BASE']}/etds") + zip_file_dir = "#{ENV['TEMP_FILE_BASE']}/etds/#{zip_file_basename}" + Dir.mkdir(zip_file_dir) unless File.exists?(zip_file_dir) zip_file.each do |component_file| puts(" Extracting #{component_file.name}") - zip_file.extract(component_file, "/tmp/#{zip_file_basename}/#{component_file.name}") + zip_file.extract(component_file, "#{zip_file_dir}/#{component_file.name}") end + # 1. extract the work metdata and add to the works metadata array + xml_file_path = get_metadata_doc_path(zip_file_dir) + etd_doc = get_etd_doc(xml_file_path) + puts "xml is at: #{xml_file_path}" + etd_md = extract_metadata(etd_doc) + works_metadata << etd_md + + attachment_file_paths = get_attachment_file_paths(zip_file_dir) + attachment_file_paths.delete(xml_file_path) + attachment_file_paths.each do |fp| + fp_basename = File.basename(fp) + puts "path = #{fp}, basename = #{fp_basename}" + file_md = {'model': 'FileSet', 'file': fp, 'title': fp_basename, 'parent': 'TBD-parentWorkID'} + filesets_metadata << file_md + end + + all_md = works_metadata + filesets_metadata + + csv_header = all_keys(all_md) + # Next, get the rows + # 2. extract the embargo info # 3. add files info (w/embargo info) to the filesets array # 4. copy the file attachments to the 'files' folder From 3018d1925129b7c6f1da13bc16e87f92067e4a31 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Thu, 2 May 2024 03:09:51 +0000 Subject: [PATCH 05/41] WIP - fixed problem creating header row --- lib/tasks/ingest_etd_new.rake | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/tasks/ingest_etd_new.rake b/lib/tasks/ingest_etd_new.rake index 0483c9f9..558e22e6 100644 --- a/lib/tasks/ingest_etd_new.rake +++ b/lib/tasks/ingest_etd_new.rake @@ -48,8 +48,10 @@ namespace :gwss do def all_keys(hash_array) keys_set = Set[] - hash_array.each |h| - h.keys.each { |k| keys_set << k } + hash_array.each do |h| + h.keys.each do |k| + keys_set << k + end end keys_set.to_a end From 8c69d97b18908376872f97279507713f6024a299 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Mon, 6 May 2024 21:23:35 +0000 Subject: [PATCH 06/41] Fixed embargo logic; fixed CSV structure --- ..._etd_new.rake => ingest_bulkrax_prep.rake} | 84 +++++++++++++++---- 1 file changed, 67 insertions(+), 17 deletions(-) rename lib/tasks/{ingest_etd_new.rake => ingest_bulkrax_prep.rake} (66%) diff --git a/lib/tasks/ingest_etd_new.rake b/lib/tasks/ingest_bulkrax_prep.rake similarity index 66% rename from lib/tasks/ingest_etd_new.rake rename to lib/tasks/ingest_bulkrax_prep.rake index 558e22e6..30c83a95 100644 --- a/lib/tasks/ingest_etd_new.rake +++ b/lib/tasks/ingest_bulkrax_prep.rake @@ -46,18 +46,47 @@ namespace :gwss do repo_metadata end - def all_keys(hash_array) - keys_set = Set[] - hash_array.each do |h| - h.keys.each do |k| - keys_set << k + def is_embargoed?(doc) + sales_restric = doc.xpath("//DISS_restriction/DISS_sales_restriction") + return false if sales_restric.empty? + rmv = sales_restric.attribute('remove') + return false if rmv.nil? + # else + true + end + + def get_embargo_date(doc) + sales_restric = doc.xpath("//DISS_restriction/DISS_sales_restriction") + return nil if sales_restric.empty? + sales_restric.attribute('remove').text + end + + def convert_to_iso(date_str) + date = Date.strptime(date_str, '%m/%d/%Y') + date.strftime('%Y-%m-%dT00:00:00') + end + + def hash_array_to_csv_array(hash_array) + hash_keys = hash_array.flat_map(&:keys).uniq + # header row + csv_array = [hash_keys] + hash_array.each do |row| + csv_array << hash_keys.map {|key| row[key]} + end + csv_array + end + + def write_csv(csv_array, csv_path) + CSV.open(csv_path, 'w') do |csv| + csv_array.each do |row| + csv << row end end - keys_set.to_a end # create folder for metadata.csv and files folder - Dir.mkdir("#{ENV['TEMP_FILE_BASE']}/bulkrax_zip") unless File.exists?("#{ENV['TEMP_FILE_BASE']}/bulkrax_zip") + bulkrax_zip_path = "#{ENV['TEMP_FILE_BASE']}/bulkrax_zip" + Dir.mkdir(bulkrax_zip_path) unless File.exists?(bulkrax_zip_path) # get all ETD zip files in the args.filepath folder path_to_zips = args.filepath @@ -87,24 +116,45 @@ namespace :gwss do etd_md = extract_metadata(etd_doc) works_metadata << etd_md + # 2. extract the attachment files paths and add to the filesets metadata array attachment_file_paths = get_attachment_file_paths(zip_file_dir) attachment_file_paths.delete(xml_file_path) attachment_file_paths.each do |fp| fp_basename = File.basename(fp) puts "path = #{fp}, basename = #{fp_basename}" - file_md = {'model': 'FileSet', 'file': fp, 'title': fp_basename, 'parent': 'TBD-parentWorkID'} + file_md = Hash.new + file_md['model'] = 'FileSet' + file_md['file'] = fp + file_md['title'] = fp_basename + file_md['parent'] = 'TBD-parentWorkID' + # Add embargo info to file_md + if !is_embargoed?(etd_doc) + # Get embargo info + embargo_date = get_embargo_date(etd_doc) + # TODO: Convert to isoformat as per Python DateTime.isoformat() + file_md['visibility'] = 'embargo' + file_md['visibility_during_embargo'] = 'restricted' + file_md['visibility_after_embargo'] = 'open' + if !embargo_date.nil? + file_md['embargo_release_date'] = convert_to_iso(embargo_date) + else + file_md['embargo_release_date'] = nil + end + end filesets_metadata << file_md end - - all_md = works_metadata + filesets_metadata - - csv_header = all_keys(all_md) - # Next, get the rows - - # 2. extract the embargo info - # 3. add files info (w/embargo info) to the filesets array - # 4. copy the file attachments to the 'files' folder end + + puts("works_metadata: #{works_metadata}") + puts("files_metadata: #{filesets_metadata}") + all_md = works_metadata + filesets_metadata + puts("all_md: #{all_md}") + + csv_rows = hash_array_to_csv_array(all_md) + #bulkrax_zip_spec_path = "#{bulkrax_zip_path}/#{zip_file_basename}" + #Dir.mkdir(bulkrax_zip_spec_path) unless File.exists?(bulkrax_zip_spec_path) + bulkrax_csv_filepath = "#{bulkrax_zip_path}/metadata.csv" + write_csv(csv_rows, bulkrax_csv_filepath) # create metadata CSV from the works metadata array and the filesets array # zip up the working folder From 7d7b47e2f67b317404e16c304dcd26011a5507ba Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Wed, 8 May 2024 03:04:27 +0000 Subject: [PATCH 07/41] Eliminated folder names from metadata csv FileSet entries; copy files to bulkrax zip staging directory, but will need to segregate files from each ETD into separate directoroes --- lib/tasks/ingest_bulkrax_prep.rake | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/tasks/ingest_bulkrax_prep.rake b/lib/tasks/ingest_bulkrax_prep.rake index 30c83a95..aeb3c175 100644 --- a/lib/tasks/ingest_bulkrax_prep.rake +++ b/lib/tasks/ingest_bulkrax_prep.rake @@ -1,3 +1,4 @@ +require 'fileutils' require 'nokogiri' require 'rake' require 'zip' @@ -16,10 +17,6 @@ namespace :gwss do File.open(xml_file_path) { |f| Nokogiri::XML(f) } end - def get_attachment_file_paths(pq_files_dir) - Dir.glob("#{pq_files_dir}/**/*") - end - def get_title(doc) doc.at_xpath("//DISS_description/DISS_title").text end @@ -104,9 +101,12 @@ namespace :gwss do Dir.mkdir("#{ENV['TEMP_FILE_BASE']}/etds") unless File.exists?("#{ENV['TEMP_FILE_BASE']}/etds") zip_file_dir = "#{ENV['TEMP_FILE_BASE']}/etds/#{zip_file_basename}" Dir.mkdir(zip_file_dir) unless File.exists?(zip_file_dir) - zip_file.each do |component_file| - puts(" Extracting #{component_file.name}") - zip_file.extract(component_file, "#{zip_file_dir}/#{component_file.name}") + + attachment_file_paths = [] + zip_file.each do |entry| + puts(" Extracting #{entry.name}") + zip_file.extract(entry, "#{zip_file_dir}/#{entry.name}") + attachment_file_paths << "#{zip_file_dir}/#{entry.name}" if !entry.name_is_directory? end # 1. extract the work metdata and add to the works metadata array @@ -117,7 +117,6 @@ namespace :gwss do works_metadata << etd_md # 2. extract the attachment files paths and add to the filesets metadata array - attachment_file_paths = get_attachment_file_paths(zip_file_dir) attachment_file_paths.delete(xml_file_path) attachment_file_paths.each do |fp| fp_basename = File.basename(fp) @@ -142,15 +141,19 @@ namespace :gwss do end end filesets_metadata << file_md + + FileUtils::copy_file(fp, "#{bulkrax_zip_path}/#{fp_basename}") end end - puts("works_metadata: #{works_metadata}") - puts("files_metadata: #{filesets_metadata}") + # puts("works_metadata: #{works_metadata}") + # puts("files_metadata: #{filesets_metadata}") all_md = works_metadata + filesets_metadata - puts("all_md: #{all_md}") + # puts("all_md: #{all_md}") csv_rows = hash_array_to_csv_array(all_md) + # Don't delete this: We need to resurrect it in order to put each ETD's files in a separate directory + # to avoid name collisions #bulkrax_zip_spec_path = "#{bulkrax_zip_path}/#{zip_file_basename}" #Dir.mkdir(bulkrax_zip_spec_path) unless File.exists?(bulkrax_zip_spec_path) bulkrax_csv_filepath = "#{bulkrax_zip_path}/metadata.csv" From dd9eb7c3e7b840041a8db492153038cf759d3fca Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Mon, 20 May 2024 21:29:54 +0000 Subject: [PATCH 08/41] Adds 'bulkrax_identifier' metadata; fixes imports of works w/files, using prerelease of next bulkrax release. --- Gemfile | 4 +++- Gemfile.lock | 4 ++-- app/models/collection.rb | 4 ++++ app/models/file_set.rb | 6 ++++++ app/models/gw_etd.rb | 4 ++++ app/models/gw_journal_issue.rb | 4 ++++ app/models/gw_work.rb | 6 +++++- config/initializers/bulkrax.rb | 12 +++++++++--- 8 files changed, 37 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index bcc36815..d8c76f31 100644 --- a/Gemfile +++ b/Gemfile @@ -64,7 +64,9 @@ gem 'riiif', '~> 2.0' gem 'cookies_eu' -gem 'bulkrax', git: 'https://github.com/samvera/bulkrax.git', branch: 'i951-false-object-bug' +#gem 'bulkrax', git: 'https://github.com/samvera/bulkrax.git', branch: 'i951-false-object-bug' +#gem 'bulkrax', git: 'https://github.com/samvera/bulkrax.git', branch: 'main' +gem 'bulkrax', git: 'https://github.com/samvera/bulkrax.git', ref: '0de8ee06115ff9e6e89177d93c407826ae892a7f' gem 'willow_sword', github: 'notch8/willow_sword' diff --git a/Gemfile.lock b/Gemfile.lock index 733ba0e0..9c909c19 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,8 +9,8 @@ GIT GIT remote: https://github.com/samvera/bulkrax.git - revision: 091a8b9f1fdfb8101b1910d8977deb247b2d3213 - branch: i951-false-object-bug + revision: 0de8ee06115ff9e6e89177d93c407826ae892a7f + branch: main specs: bulkrax (8.0.0) bagit (~> 0.4.6) diff --git a/app/models/collection.rb b/app/models/collection.rb index 87e71166..f31d4a17 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -4,4 +4,8 @@ class Collection < ActiveFedora::Base # You can replace these metadata if they're not suitable include Hyrax::BasicMetadata self.indexer = Hyrax::CollectionWithBasicMetadataIndexer + + property :bulkrax_identifier, predicate: ::RDF::URI("https://iro.bl.uk/resource#bulkraxIdentifier"), multiple: false do |index| + index.as :stored_searchable, :facetable + end end diff --git a/app/models/file_set.rb b/app/models/file_set.rb index 393de0ee..0c503479 100644 --- a/app/models/file_set.rb +++ b/app/models/file_set.rb @@ -1,4 +1,10 @@ # Generated by hyrax:models:install class FileSet < ActiveFedora::Base +# include ::Hyrax::FileSetBehavior + + property :bulkrax_identifier, predicate: ::RDF::URI("https://iro.bl.uk/resource#bulkraxIdentifier"), multiple: false do |index| + index.as :stored_searchable, :facetable + end + include ::Hyrax::FileSetBehavior end diff --git a/app/models/gw_etd.rb b/app/models/gw_etd.rb index d2c72a78..435cad1b 100644 --- a/app/models/gw_etd.rb +++ b/app/models/gw_etd.rb @@ -28,5 +28,9 @@ class GwEtd < ActiveFedora::Base index.as :stored_searchable, :facetable end + property :bulkrax_identifier, predicate: ::RDF::URI("https://iro.bl.uk/resource#bulkraxIdentifier"), multiple: false do |index| + index.as :stored_searchable, :facetable + end + include ::Hyrax::BasicMetadata end diff --git a/app/models/gw_journal_issue.rb b/app/models/gw_journal_issue.rb index eb61c5fd..51eb987a 100644 --- a/app/models/gw_journal_issue.rb +++ b/app/models/gw_journal_issue.rb @@ -24,6 +24,10 @@ class GwJournalIssue < ActiveFedora::Base index.as :stored_searchable end + property :bulkrax_identifier, predicate: ::RDF::URI("https://iro.bl.uk/resource#bulkraxIdentifier"), multiple: false do |index| + index.as :stored_searchable, :facetable + end + # This must be included at the end, because it finalizes the metadata # schema (by adding accepts_nested_attributes) include ::Hyrax::BasicMetadata diff --git a/app/models/gw_work.rb b/app/models/gw_work.rb index 2c85a7ca..addc060b 100644 --- a/app/models/gw_work.rb +++ b/app/models/gw_work.rb @@ -16,5 +16,9 @@ class GwWork < ActiveFedora::Base index.as :stored_searchable end + property :bulkrax_identifier, predicate: ::RDF::URI("https://iro.bl.uk/resource#bulkraxIdentifier"), multiple: false do |index| + index.as :stored_searchable, :facetable + end + include ::Hyrax::BasicMetadata -end \ No newline at end of file +end diff --git a/config/initializers/bulkrax.rb b/config/initializers/bulkrax.rb index 3ddfc5b1..58dd9a05 100644 --- a/config/initializers/bulkrax.rb +++ b/config/initializers/bulkrax.rb @@ -43,9 +43,15 @@ # seems to prevent the object from being created with a visible metadata # field of Source with a value that's a big ugly uuid config.field_mappings['Bulkrax::CsvParser'] = { - 'source_identifier' => { from: ['source_identifier'], source_identifier: true, search_field: 'source_id_sim' }, + # 'source_identifier' => { from: ['source_identifier'], source_identifier: true, search_field: 'source_id_sim' }, + 'bulkrax_identifier' => { from: ['bulkrax_identifier'], source_identifier: true }, 'keyword' => { from: ['keyword'], split: true }, - 'file' => { from: ['file'], split: '\;' } + 'advisor' => { from: ['advisor'], split: true }, + 'doi' => { from: ['doi'], split: true }, + 'committee_member' => { from: ['committee_member'], split: true }, + 'gw_affiliation' => { from: ['gw_affiliation'], split: true }, + 'file' => { from: ['file'], split: '\;' }, + 'parents' => { from: ['parents'], split: '\;', related_parents_field_mapping: true }, } # Add to, or change existing mappings as follows @@ -71,7 +77,7 @@ # It is given two aruguments, self at the time of call and the index of the reocrd # config.fill_in_blank_source_identifiers = ->(parser, index) { "b-#{parser.importer.id}-#{index}"} # or use a uuid - config.fill_in_blank_source_identifiers = ->(parser, index) { SecureRandom.uuid } + # config.fill_in_blank_source_identifiers = ->(parser, index) { SecureRandom.uuid } # Properties that should not be used in imports/exports. They are reserved for use by Hyrax. # config.reserved_properties += ['my_field'] From c13eb436835a34c2df6730b89a6281de2396250e Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Tue, 21 May 2024 01:15:27 +0000 Subject: [PATCH 09/41] implemented parent work/child FileSet bulkrax_identifier, repaired embargo attributes --- lib/tasks/ingest_bulkrax_prep.rake | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/tasks/ingest_bulkrax_prep.rake b/lib/tasks/ingest_bulkrax_prep.rake index aeb3c175..55f9666e 100644 --- a/lib/tasks/ingest_bulkrax_prep.rake +++ b/lib/tasks/ingest_bulkrax_prep.rake @@ -55,6 +55,7 @@ namespace :gwss do def get_embargo_date(doc) sales_restric = doc.xpath("//DISS_restriction/DISS_sales_restriction") return nil if sales_restric.empty? + return nil if sales_restric.attribute('remove').text.empty? sales_restric.attribute('remove').text end @@ -114,10 +115,13 @@ namespace :gwss do etd_doc = get_etd_doc(xml_file_path) puts "xml is at: #{xml_file_path}" etd_md = extract_metadata(etd_doc) + parent_work_identifier = SecureRandom.uuid + etd_md['bulkrax_identifier'] = parent_work_identifier works_metadata << etd_md # 2. extract the attachment files paths and add to the filesets metadata array attachment_file_paths.delete(xml_file_path) + Dir.mkdir("#{bulkrax_zip_path}/#{zip_file_basename}") if !attachment_file_paths.empty? attachment_file_paths.each do |fp| fp_basename = File.basename(fp) puts "path = #{fp}, basename = #{fp_basename}" @@ -125,9 +129,9 @@ namespace :gwss do file_md['model'] = 'FileSet' file_md['file'] = fp file_md['title'] = fp_basename - file_md['parent'] = 'TBD-parentWorkID' + file_md['parent'] = parent_work_identifier # Add embargo info to file_md - if !is_embargoed?(etd_doc) + if is_embargoed?(etd_doc) # Get embargo info embargo_date = get_embargo_date(etd_doc) # TODO: Convert to isoformat as per Python DateTime.isoformat() @@ -142,7 +146,7 @@ namespace :gwss do end filesets_metadata << file_md - FileUtils::copy_file(fp, "#{bulkrax_zip_path}/#{fp_basename}") + FileUtils::copy_file(fp, "#{bulkrax_zip_path}/#{zip_file_basename}/#{fp_basename}") end end @@ -161,6 +165,7 @@ namespace :gwss do # create metadata CSV from the works metadata array and the filesets array # zip up the working folder + # Consider a system command here? Not so simple with rubyzip end desc "Ingests ProQuest XML metadata for a single ETD" From 5fdf6a751542beb3625a0322ba0249ed1fc4d4bb Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Fri, 24 May 2024 16:10:57 +0000 Subject: [PATCH 10/41] refactor file paths for extracted zip; parse creator/contributors --- lib/tasks/ingest_bulkrax_prep.rake | 64 +++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/lib/tasks/ingest_bulkrax_prep.rake b/lib/tasks/ingest_bulkrax_prep.rake index 55f9666e..353173f4 100644 --- a/lib/tasks/ingest_bulkrax_prep.rake +++ b/lib/tasks/ingest_bulkrax_prep.rake @@ -26,7 +26,6 @@ namespace :gwss do end def get_abstract(doc) - # TODO: abstract_text_array = [] doc.xpath("//DISS_content/DISS_abstract/DISS_para").each do |p| abstract_text_array << p.text @@ -34,12 +33,46 @@ namespace :gwss do abstract_text = Nokogiri::HTML(abstract_text_array.join("\n")).text end + def get_creators(doc) + creators_array = [] + contributors_array = [] + doc.xpath("//DISS_authorship/DISS_author").each do |a| + author_type = a.attribute('type').type + lastname = a.xpath("DISS_name/DISS_surname").text + firstname = a.xpath("DISS_name/DISS_fname").text + middlename = a.xpath("DISS_name/DISS_middle").text + + fullname = lastname + ", " + firstname + fullname = fullname + " " + middlename unless middlename.empty? + + if author_type == 'primary' + creators_array << fullname + else + contributors_array << fullname + end + end + + {'creators' => creators_array, 'contributors' => contributors_array} + end + + def get_keywords(doc) + keyword_array = [] + doc.xpath("//DISS_description/DISS_categorization/DISS_keyword").text.split(',') do |k| + keyword_array << k.strip() + end + keyword_array + end + def extract_metadata(doc) repo_metadata = Hash.new repo_metadata['model'] = 'GwEtd' repo_metadata['title'] = get_title(doc) + creators = get_creators(doc) + repo_metadata['creator'] = creators['creators'].join(';') + repo_metadata['contributor'] = creators['contributors'].join(';') repo_metadata['language'] = get_language(doc) repo_metadata['description'] = get_abstract(doc) + repo_metadata['keyword'] = get_keywords(doc).join(';') repo_metadata end @@ -84,7 +117,9 @@ namespace :gwss do # create folder for metadata.csv and files folder bulkrax_zip_path = "#{ENV['TEMP_FILE_BASE']}/bulkrax_zip" - Dir.mkdir(bulkrax_zip_path) unless File.exists?(bulkrax_zip_path) + bulkrax_files_path = "#{ENV['TEMP_FILE_BASE']}/bulkrax_zip/files" + puts "File.exists?(bulkrax_zip_path) = #{File.exists?(bulkrax_zip_path)}" + FileUtils.makedirs("#{bulkrax_files_path}") unless File.exists?(bulkrax_zip_path) # get all ETD zip files in the args.filepath folder path_to_zips = args.filepath @@ -98,16 +133,18 @@ namespace :gwss do # for each ETD zip file: puts("Processing #{zip_path}") zip_file = Zip::File.open(zip_path) - zip_file_basename = File.basename(zip_path, '.zip') - Dir.mkdir("#{ENV['TEMP_FILE_BASE']}/etds") unless File.exists?("#{ENV['TEMP_FILE_BASE']}/etds") - zip_file_dir = "#{ENV['TEMP_FILE_BASE']}/etds/#{zip_file_basename}" + zip_file_basename = File.basename(zip_path, '.zip') # e.g. etdadmin_upload_353614 + # Dir.mkdir("#{ENV['TEMP_FILE_BASE']}/etds") unless File.exists?("#{ENV['TEMP_FILE_BASE']}/etds") + # zip_file_dir = "#{ENV['TEMP_FILE_BASE']}/etds/#{zip_file_basename}" + zip_file_dir = "#{bulkrax_files_path}/#{zip_file_basename}" # e.g. bulkrax_zip/files/etdadmin_upload_353614 Dir.mkdir(zip_file_dir) unless File.exists?(zip_file_dir) attachment_file_paths = [] zip_file.each do |entry| puts(" Extracting #{entry.name}") zip_file.extract(entry, "#{zip_file_dir}/#{entry.name}") - attachment_file_paths << "#{zip_file_dir}/#{entry.name}" if !entry.name_is_directory? + # attachment_file_paths << "#{zip_file_dir}/#{entry.name}" if !entry.name_is_directory? + attachment_file_paths << "#{entry.name}" if !entry.name_is_directory? end # 1. extract the work metdata and add to the works metadata array @@ -117,19 +154,23 @@ namespace :gwss do etd_md = extract_metadata(etd_doc) parent_work_identifier = SecureRandom.uuid etd_md['bulkrax_identifier'] = parent_work_identifier + puts etd_md works_metadata << etd_md # 2. extract the attachment files paths and add to the filesets metadata array - attachment_file_paths.delete(xml_file_path) - Dir.mkdir("#{bulkrax_zip_path}/#{zip_file_basename}") if !attachment_file_paths.empty? + attachment_file_paths.delete(File.basename(xml_file_path)) + # Dir.mkdir("#{bulkrax_zip_path}/#{zip_file_basename}") if !attachment_file_paths.empty? attachment_file_paths.each do |fp| fp_basename = File.basename(fp) puts "path = #{fp}, basename = #{fp_basename}" file_md = Hash.new file_md['model'] = 'FileSet' - file_md['file'] = fp + # safe_fp = File.dirname("#{zip_file_basename}/#{fp}") + '/"' + File.basename(fp) + '"' + safe_fp = "#{zip_file_basename}/#{fp}" + file_md['file'] = safe_fp file_md['title'] = fp_basename - file_md['parent'] = parent_work_identifier + file_md['bulkrax_identifier'] = SecureRandom.uuid + file_md['parents'] = parent_work_identifier # Add embargo info to file_md if is_embargoed?(etd_doc) # Get embargo info @@ -146,7 +187,7 @@ namespace :gwss do end filesets_metadata << file_md - FileUtils::copy_file(fp, "#{bulkrax_zip_path}/#{zip_file_basename}/#{fp_basename}") + # FileUtils::copy_file(fp, "#{bulkrax_zip_path}/#{zip_file_basename}/#{fp_basename}") end end @@ -231,3 +272,4 @@ namespace :gwss do puts repo_metadata end end + From e97b03237a95bde7756bf8b4121b7565f8f37b86 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Fri, 24 May 2024 17:23:54 +0000 Subject: [PATCH 11/41] Repair attachment filenames with spaces (or else bulkrax will); fix author parsing --- lib/tasks/ingest_bulkrax_prep.rake | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/tasks/ingest_bulkrax_prep.rake b/lib/tasks/ingest_bulkrax_prep.rake index 353173f4..5d9291c8 100644 --- a/lib/tasks/ingest_bulkrax_prep.rake +++ b/lib/tasks/ingest_bulkrax_prep.rake @@ -37,7 +37,7 @@ namespace :gwss do creators_array = [] contributors_array = [] doc.xpath("//DISS_authorship/DISS_author").each do |a| - author_type = a.attribute('type').type + author_type = a.attribute('type').text lastname = a.xpath("DISS_name/DISS_surname").text firstname = a.xpath("DISS_name/DISS_fname").text middlename = a.xpath("DISS_name/DISS_middle").text @@ -114,6 +114,15 @@ namespace :gwss do end end end + + def repair_filename(filepath) + # translate spaces in the filename portion to _ + if File.dirname(filepath) == '.' + File.basename(filepath).tr(' ', '_') + else + File.join(File.dirname(filepath), File.basename(filepath).tr(' ', '_')) + end + end # create folder for metadata.csv and files folder bulkrax_zip_path = "#{ENV['TEMP_FILE_BASE']}/bulkrax_zip" @@ -142,9 +151,10 @@ namespace :gwss do attachment_file_paths = [] zip_file.each do |entry| puts(" Extracting #{entry.name}") - zip_file.extract(entry, "#{zip_file_dir}/#{entry.name}") + entry_name_clean = repair_filename(entry.name) + zip_file.extract(entry, "#{zip_file_dir}/#{entry_name_clean}") # attachment_file_paths << "#{zip_file_dir}/#{entry.name}" if !entry.name_is_directory? - attachment_file_paths << "#{entry.name}" if !entry.name_is_directory? + attachment_file_paths << "#{entry_name_clean}" if !entry.name_is_directory? end # 1. extract the work metdata and add to the works metadata array @@ -154,7 +164,6 @@ namespace :gwss do etd_md = extract_metadata(etd_doc) parent_work_identifier = SecureRandom.uuid etd_md['bulkrax_identifier'] = parent_work_identifier - puts etd_md works_metadata << etd_md # 2. extract the attachment files paths and add to the filesets metadata array From d5342b1e05b0dd54c3314f9cfa9cfff4cc5b52bc Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Fri, 24 May 2024 18:03:30 +0000 Subject: [PATCH 12/41] Add degree, advisors, committee members --- lib/tasks/ingest_bulkrax_prep.rake | 47 +++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/lib/tasks/ingest_bulkrax_prep.rake b/lib/tasks/ingest_bulkrax_prep.rake index 5d9291c8..c4e1f6fc 100644 --- a/lib/tasks/ingest_bulkrax_prep.rake +++ b/lib/tasks/ingest_bulkrax_prep.rake @@ -33,22 +33,26 @@ namespace :gwss do abstract_text = Nokogiri::HTML(abstract_text_array.join("\n")).text end + def fullname(person_node) + lastname = person_node.xpath("DISS_name/DISS_surname").text + firstname = person_node.xpath("DISS_name/DISS_fname").text + middlename = person_node.xpath("DISS_name/DISS_middle").text + + fullname = lastname + ", " + firstname + fullname = fullname + " " + middlename unless middlename.empty? + fullname + end + def get_creators(doc) creators_array = [] contributors_array = [] - doc.xpath("//DISS_authorship/DISS_author").each do |a| - author_type = a.attribute('type').text - lastname = a.xpath("DISS_name/DISS_surname").text - firstname = a.xpath("DISS_name/DISS_fname").text - middlename = a.xpath("DISS_name/DISS_middle").text - - fullname = lastname + ", " + firstname - fullname = fullname + " " + middlename unless middlename.empty? + doc.xpath("//DISS_authorship/DISS_author").each do |author_node| + author_type = author_node.attribute('type').text if author_type == 'primary' - creators_array << fullname + creators_array << fullname(author_node) else - contributors_array << fullname + contributors_array << fullname(author_node) end end @@ -73,6 +77,9 @@ namespace :gwss do repo_metadata['language'] = get_language(doc) repo_metadata['description'] = get_abstract(doc) repo_metadata['keyword'] = get_keywords(doc).join(';') + repo_metadata['degree'] = get_degree(doc) + repo_metadata['advisor'] = get_advisors(doc).join(';') + repo_metadata['committee_member'] = get_committee_members(doc).join(';') repo_metadata end @@ -92,6 +99,26 @@ namespace :gwss do sales_restric.attribute('remove').text end + def get_degree(doc) + doc.xpath("//DISS_description/DISS_degree").text + end + + def get_advisors(doc) + advisors = [] + doc.xpath("//DISS_description/DISS_advisor").each do |advisor_node| + advisors << fullname(advisor_node) + end + advisors + end + + def get_committee_members(doc) + committee_members = [] + doc.xpath("//DISS_description/DISS_cmte_member").each do |committee_member_node| + committee_members << fullname(committee_member_node) + end + committee_members + end + def convert_to_iso(date_str) date = Date.strptime(date_str, '%m/%d/%Y') date.strftime('%Y-%m-%dT00:00:00') From d6a515d126ae52b21a8928a2b45d4c224f8f4a68 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Fri, 24 May 2024 19:45:29 +0000 Subject: [PATCH 13/41] Add gw_affiliation, date_created --- lib/tasks/ingest_bulkrax_prep.rake | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/tasks/ingest_bulkrax_prep.rake b/lib/tasks/ingest_bulkrax_prep.rake index c4e1f6fc..cd2f14ac 100644 --- a/lib/tasks/ingest_bulkrax_prep.rake +++ b/lib/tasks/ingest_bulkrax_prep.rake @@ -59,6 +59,10 @@ namespace :gwss do {'creators' => creators_array, 'contributors' => contributors_array} end + def get_department(doc) + doc.xpath("//DISS_description/DISS_institution/DISS_inst_contact").text + end + def get_keywords(doc) keyword_array = [] doc.xpath("//DISS_description/DISS_categorization/DISS_keyword").text.split(',') do |k| @@ -67,6 +71,15 @@ namespace :gwss do keyword_array end + def get_date_created(doc) + comp_date = doc.xpath("//DISS_description/DISS_dates/DISS_comp_date").text + if !comp_date.empty? and comp_date.length >= 4 + comp_date[0..3] + else + nil + end + end + def extract_metadata(doc) repo_metadata = Hash.new repo_metadata['model'] = 'GwEtd' @@ -79,6 +92,9 @@ namespace :gwss do repo_metadata['keyword'] = get_keywords(doc).join(';') repo_metadata['degree'] = get_degree(doc) repo_metadata['advisor'] = get_advisors(doc).join(';') + repo_metadata['gw_affiliation'] = get_department(doc) + etd_date_created = get_date_created(doc) + repo_metadata['date_created'] = etd_date_created unless etd_date_created.nil? repo_metadata['committee_member'] = get_committee_members(doc).join(';') repo_metadata end From c47c8eca708669956735ae7f2b922d35224a6038 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Mon, 27 May 2024 16:33:00 +0000 Subject: [PATCH 14/41] Simplify embargo date; add rights statement; clean up --- Gemfile.lock | 2 +- lib/tasks/ingest_bulkrax_prep.rake | 156 ++++++----------------------- 2 files changed, 29 insertions(+), 129 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9c909c19..03d52f59 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,7 +10,7 @@ GIT GIT remote: https://github.com/samvera/bulkrax.git revision: 0de8ee06115ff9e6e89177d93c407826ae892a7f - branch: main + ref: 0de8ee06115ff9e6e89177d93c407826ae892a7f specs: bulkrax (8.0.0) bagit (~> 0.4.6) diff --git a/lib/tasks/ingest_bulkrax_prep.rake b/lib/tasks/ingest_bulkrax_prep.rake index cd2f14ac..fe104612 100644 --- a/lib/tasks/ingest_bulkrax_prep.rake +++ b/lib/tasks/ingest_bulkrax_prep.rake @@ -17,14 +17,6 @@ namespace :gwss do File.open(xml_file_path) { |f| Nokogiri::XML(f) } end - def get_title(doc) - doc.at_xpath("//DISS_description/DISS_title").text - end - - def get_language(doc) - doc.at_xpath("//DISS_description/DISS_categorization/DISS_language").text - end - def get_abstract(doc) abstract_text_array = [] doc.xpath("//DISS_content/DISS_abstract/DISS_para").each do |p| @@ -59,8 +51,8 @@ namespace :gwss do {'creators' => creators_array, 'contributors' => contributors_array} end - def get_department(doc) - doc.xpath("//DISS_description/DISS_institution/DISS_inst_contact").text + def get_node_value(doc, xpath) + doc.xpath(xpath).text end def get_keywords(doc) @@ -80,25 +72,6 @@ namespace :gwss do end end - def extract_metadata(doc) - repo_metadata = Hash.new - repo_metadata['model'] = 'GwEtd' - repo_metadata['title'] = get_title(doc) - creators = get_creators(doc) - repo_metadata['creator'] = creators['creators'].join(';') - repo_metadata['contributor'] = creators['contributors'].join(';') - repo_metadata['language'] = get_language(doc) - repo_metadata['description'] = get_abstract(doc) - repo_metadata['keyword'] = get_keywords(doc).join(';') - repo_metadata['degree'] = get_degree(doc) - repo_metadata['advisor'] = get_advisors(doc).join(';') - repo_metadata['gw_affiliation'] = get_department(doc) - etd_date_created = get_date_created(doc) - repo_metadata['date_created'] = etd_date_created unless etd_date_created.nil? - repo_metadata['committee_member'] = get_committee_members(doc).join(';') - repo_metadata - end - def is_embargoed?(doc) sales_restric = doc.xpath("//DISS_restriction/DISS_sales_restriction") return false if sales_restric.empty? @@ -115,10 +88,6 @@ namespace :gwss do sales_restric.attribute('remove').text end - def get_degree(doc) - doc.xpath("//DISS_description/DISS_degree").text - end - def get_advisors(doc) advisors = [] doc.xpath("//DISS_description/DISS_advisor").each do |advisor_node| @@ -137,7 +106,27 @@ namespace :gwss do def convert_to_iso(date_str) date = Date.strptime(date_str, '%m/%d/%Y') - date.strftime('%Y-%m-%dT00:00:00') + date.strftime('%Y-%m-%d') #T00:00:00') + end + + def extract_metadata(doc) + work_metadata = Hash.new + work_metadata['model'] = 'GwEtd' + work_metadata['title'] = get_node_value(doc, "//DISS_description/DISS_title") + creators = get_creators(doc) + work_metadata['creator'] = creators['creators'].join(';') + work_metadata['contributor'] = creators['contributors'].join(';') + work_metadata['language'] = get_node_value(doc, "//DISS_description/DISS_categorization/DISS_language") + work_metadata['description'] = get_abstract(doc) + work_metadata['keyword'] = get_keywords(doc).join(';') + work_metadata['degree'] = get_node_value(doc, "//DISS_description/DISS_degree") + work_metadata['advisor'] = get_advisors(doc).join(';') + work_metadata['gw_affiliation'] = get_node_value(doc, "//DISS_description/DISS_institution/DISS_inst_contact") + etd_date_created = get_date_created(doc) + work_metadata['date_created'] = etd_date_created unless etd_date_created.nil? + work_metadata['committee_member'] = get_committee_members(doc).join(';') + work_metadata['rights_statement'] = 'http://rightsstatements.org/vocab/InC/1.0/' + work_metadata end def hash_array_to_csv_array(hash_array) @@ -186,8 +175,6 @@ namespace :gwss do puts("Processing #{zip_path}") zip_file = Zip::File.open(zip_path) zip_file_basename = File.basename(zip_path, '.zip') # e.g. etdadmin_upload_353614 - # Dir.mkdir("#{ENV['TEMP_FILE_BASE']}/etds") unless File.exists?("#{ENV['TEMP_FILE_BASE']}/etds") - # zip_file_dir = "#{ENV['TEMP_FILE_BASE']}/etds/#{zip_file_basename}" zip_file_dir = "#{bulkrax_files_path}/#{zip_file_basename}" # e.g. bulkrax_zip/files/etdadmin_upload_353614 Dir.mkdir(zip_file_dir) unless File.exists?(zip_file_dir) @@ -196,33 +183,33 @@ namespace :gwss do puts(" Extracting #{entry.name}") entry_name_clean = repair_filename(entry.name) zip_file.extract(entry, "#{zip_file_dir}/#{entry_name_clean}") - # attachment_file_paths << "#{zip_file_dir}/#{entry.name}" if !entry.name_is_directory? + # skip directories - these get their own entries in a zip file attachment_file_paths << "#{entry_name_clean}" if !entry.name_is_directory? end # 1. extract the work metdata and add to the works metadata array xml_file_path = get_metadata_doc_path(zip_file_dir) etd_doc = get_etd_doc(xml_file_path) - puts "xml is at: #{xml_file_path}" + puts "xml is located at: #{xml_file_path}" etd_md = extract_metadata(etd_doc) parent_work_identifier = SecureRandom.uuid etd_md['bulkrax_identifier'] = parent_work_identifier works_metadata << etd_md # 2. extract the attachment files paths and add to the filesets metadata array + # Remove the metadata xml file so we don't go and attach it to thw work attachment_file_paths.delete(File.basename(xml_file_path)) - # Dir.mkdir("#{bulkrax_zip_path}/#{zip_file_basename}") if !attachment_file_paths.empty? attachment_file_paths.each do |fp| fp_basename = File.basename(fp) puts "path = #{fp}, basename = #{fp_basename}" file_md = Hash.new file_md['model'] = 'FileSet' - # safe_fp = File.dirname("#{zip_file_basename}/#{fp}") + '/"' + File.basename(fp) + '"' safe_fp = "#{zip_file_basename}/#{fp}" file_md['file'] = safe_fp file_md['title'] = fp_basename file_md['bulkrax_identifier'] = SecureRandom.uuid file_md['parents'] = parent_work_identifier + # Add embargo info to file_md if is_embargoed?(etd_doc) # Get embargo info @@ -237,91 +224,4 @@ namespace :gwss do file_md['embargo_release_date'] = nil end end - filesets_metadata << file_md - - # FileUtils::copy_file(fp, "#{bulkrax_zip_path}/#{zip_file_basename}/#{fp_basename}") - end - end - - # puts("works_metadata: #{works_metadata}") - # puts("files_metadata: #{filesets_metadata}") - all_md = works_metadata + filesets_metadata - # puts("all_md: #{all_md}") - - csv_rows = hash_array_to_csv_array(all_md) - # Don't delete this: We need to resurrect it in order to put each ETD's files in a separate directory - # to avoid name collisions - #bulkrax_zip_spec_path = "#{bulkrax_zip_path}/#{zip_file_basename}" - #Dir.mkdir(bulkrax_zip_spec_path) unless File.exists?(bulkrax_zip_spec_path) - bulkrax_csv_filepath = "#{bulkrax_zip_path}/metadata.csv" - write_csv(csv_rows, bulkrax_csv_filepath) - - # create metadata CSV from the works metadata array and the filesets array - # zip up the working folder - # Consider a system command here? Not so simple with rubyzip - end - - desc "Ingests ProQuest XML metadata for a single ETD" - task :ingest_etd_new, [:filepath] do |t, args| - - # attr_accessor :etd_doc, :repo_metadata - - def extract_zip(zip_file_path) - puts("filepath is #{zip_file_path}") - zip_file = Zip::File.open(zip_file_path) - zip_file_basename = File.basename(zip_file_path, '.zip') - Dir.mkdir(zip_file_basename) unless File.exists?(zip_file_basename) - - zip_file.each do |component_file| - puts "Extracting #{component_file.name}" - zip_file.extract(component_file, "#{zip_file_basename}/#{component_file.name}") - end - - # return path to files - zip_file_basename - end - - def get_metadata_doc_path(pq_files_dir) - xml_paths = Dir.glob("#{pq_files_dir}/*.xml") - pq_xml_file_path = xml_paths.first - pq_xml_file_path - end - - def get_etd_doc(xml_file_path) - File.open(xml_file_path) { |f| Nokogiri::XML(f) } - end - - def extract_metadata(doc) - repo_metadata = Hash.new - end - - def get_title(doc) - doc.at_xpath("//DISS_description/DISS_title").text - end - - def get_language(doc) - doc.at_xpath("//DISS_description/DISS_categorization/DISS_language").text - end - - def get_abstract(doc) - # TODO: - abstract_text_array = [] - doc.xpath("//DISS_content/DISS_abstract/DISS_para").each do |p| - abstract_text_array << p.text - end - abstract_text = Nokogiri::HTML(abstract_text_array.join("\n")).text - end - - files_dir = extract_zip(args.filepath) - xml_doc_path = get_metadata_doc_path(files_dir) - etd_doc = get_etd_doc(xml_doc_path) - - repo_metadata = Hash.new - repo_metadata['title'] = get_title(etd_doc) - repo_metadata['language'] = get_language(etd_doc) - repo_metadata['description'] = get_abstract(etd_doc) - - puts repo_metadata - end -end - + filesets_metad \ No newline at end of file From 4419701b684458013a8584fb8e1ccc189a696ec0 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Mon, 27 May 2024 19:07:18 +0000 Subject: [PATCH 15/41] Fix truncated file; clarify configs, set default rights --- Gemfile | 5 +++-- config/initializers/bulkrax.rb | 9 +++++---- lib/tasks/ingest_bulkrax_prep.rake | 19 ++++++++++++++++++- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index d8c76f31..8a6ff98c 100644 --- a/Gemfile +++ b/Gemfile @@ -64,8 +64,9 @@ gem 'riiif', '~> 2.0' gem 'cookies_eu' -#gem 'bulkrax', git: 'https://github.com/samvera/bulkrax.git', branch: 'i951-false-object-bug' -#gem 'bulkrax', git: 'https://github.com/samvera/bulkrax.git', branch: 'main' +# When the next Bulkrax release *after* 8.0.0 is available, change this to: +# gem 'bulkrax', '8.0.1' # or whatever the new version is +# and re-test. gem 'bulkrax', git: 'https://github.com/samvera/bulkrax.git', ref: '0de8ee06115ff9e6e89177d93c407826ae892a7f' gem 'willow_sword', github: 'notch8/willow_sword' diff --git a/config/initializers/bulkrax.rb b/config/initializers/bulkrax.rb index 58dd9a05..c926236a 100644 --- a/config/initializers/bulkrax.rb +++ b/config/initializers/bulkrax.rb @@ -14,6 +14,9 @@ config.object_factory = Bulkrax::ObjectFactory # Use this for a Postgres-backed Valkyrized Hyrax # config.object_factory = Bulkrax::ValkyrieObjectFactory + + # Queue name for imports + config.ingest_queue_name = :import # Path to store pending imports # config.import_path = 'tmp/imports' @@ -39,11 +42,9 @@ # "Bulkrax::OaiDcParser" => { **individual field mappings go here*** } # } - # This config may seem redundant, but (as of bulkrax 6.0.1) including it - # seems to prevent the object from being created with a visible metadata - # field of Source with a value that's a big ugly uuid config.field_mappings['Bulkrax::CsvParser'] = { - # 'source_identifier' => { from: ['source_identifier'], source_identifier: true, search_field: 'source_id_sim' }, + # Setting source_identifier: true makes bulkrax_identifier a mandatory field, + # so it MUST be present in the CSV row for EVERY item (regardless of type, so this includes FileSets as well) 'bulkrax_identifier' => { from: ['bulkrax_identifier'], source_identifier: true }, 'keyword' => { from: ['keyword'], split: true }, 'advisor' => { from: ['advisor'], split: true }, diff --git a/lib/tasks/ingest_bulkrax_prep.rake b/lib/tasks/ingest_bulkrax_prep.rake index fe104612..01e95a25 100644 --- a/lib/tasks/ingest_bulkrax_prep.rake +++ b/lib/tasks/ingest_bulkrax_prep.rake @@ -224,4 +224,21 @@ namespace :gwss do file_md['embargo_release_date'] = nil end end - filesets_metad \ No newline at end of file + filesets_metadata << file_md + end + end + + # puts("works_metadata: #{works_metadata}") + # puts("files_metadata: #{filesets_metadata}") + all_md = works_metadata + filesets_metadata + # puts("all_md: #{all_md}") + + csv_rows = hash_array_to_csv_array(all_md) + bulkrax_csv_filepath = "#{bulkrax_zip_path}/metadata.csv" + write_csv(csv_rows, bulkrax_csv_filepath) + + # create metadata CSV from the works metadata array and the filesets array + # zip up the working folder + # Consider a system command here? Not so simple with rubyzip + end +end From caff6dcaeb425c856ae8b8d88b0c66378940f7f3 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Tue, 28 May 2024 13:12:44 +0000 Subject: [PATCH 16/41] Update bulkrax hash, now contains db migration fix --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 8a6ff98c..443dcf47 100644 --- a/Gemfile +++ b/Gemfile @@ -67,7 +67,7 @@ gem 'cookies_eu' # When the next Bulkrax release *after* 8.0.0 is available, change this to: # gem 'bulkrax', '8.0.1' # or whatever the new version is # and re-test. -gem 'bulkrax', git: 'https://github.com/samvera/bulkrax.git', ref: '0de8ee06115ff9e6e89177d93c407826ae892a7f' +gem 'bulkrax', git: 'https://github.com/samvera/bulkrax.git', ref: 'd8f9b85a9064b352a252c5deb590a430ca4a738e' gem 'willow_sword', github: 'notch8/willow_sword' diff --git a/Gemfile.lock b/Gemfile.lock index 03d52f59..1f6ca0c0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,8 +9,8 @@ GIT GIT remote: https://github.com/samvera/bulkrax.git - revision: 0de8ee06115ff9e6e89177d93c407826ae892a7f - ref: 0de8ee06115ff9e6e89177d93c407826ae892a7f + revision: d8f9b85a9064b352a252c5deb590a430ca4a738e + ref: d8f9b85a9064b352a252c5deb590a430ca4a738e specs: bulkrax (8.0.0) bagit (~> 0.4.6) From d3e87e57b73575e130aa70462754a8b5b9c71064 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Mon, 3 Jun 2024 15:17:52 +0000 Subject: [PATCH 17/41] Code cleanup for PR --- lib/tasks/ingest_bulkrax_prep.rake | 33 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/lib/tasks/ingest_bulkrax_prep.rake b/lib/tasks/ingest_bulkrax_prep.rake index 01e95a25..271973d5 100644 --- a/lib/tasks/ingest_bulkrax_prep.rake +++ b/lib/tasks/ingest_bulkrax_prep.rake @@ -106,7 +106,7 @@ namespace :gwss do def convert_to_iso(date_str) date = Date.strptime(date_str, '%m/%d/%Y') - date.strftime('%Y-%m-%d') #T00:00:00') + date.strftime('%Y-%m-%d') end def extract_metadata(doc) @@ -158,7 +158,7 @@ namespace :gwss do # create folder for metadata.csv and files folder bulkrax_zip_path = "#{ENV['TEMP_FILE_BASE']}/bulkrax_zip" - bulkrax_files_path = "#{ENV['TEMP_FILE_BASE']}/bulkrax_zip/files" + bulkrax_files_path = "#{bulkrax_zip_path}/files" puts "File.exists?(bulkrax_zip_path) = #{File.exists?(bulkrax_zip_path)}" FileUtils.makedirs("#{bulkrax_files_path}") unless File.exists?(bulkrax_zip_path) @@ -196,6 +196,17 @@ namespace :gwss do etd_md['bulkrax_identifier'] = parent_work_identifier works_metadata << etd_md + # Set up embargo info that will be applied below to all file attachments + etd_is_embargoed = is_embargoed?(etd_doc) + if etd_is_embargoed + embargo_date = get_embargo_date(etd_doc) + if !embargo_date.nil? + embargo_release_date = convert_to_iso(embargo_date) + else + embargo_release_date = nil + end + end + # 2. extract the attachment files paths and add to the filesets metadata array # Remove the metadata xml file so we don't go and attach it to thw work attachment_file_paths.delete(File.basename(xml_file_path)) @@ -210,34 +221,24 @@ namespace :gwss do file_md['bulkrax_identifier'] = SecureRandom.uuid file_md['parents'] = parent_work_identifier - # Add embargo info to file_md - if is_embargoed?(etd_doc) - # Get embargo info - embargo_date = get_embargo_date(etd_doc) - # TODO: Convert to isoformat as per Python DateTime.isoformat() + # Add embargo info and + if etd_is_embargoed file_md['visibility'] = 'embargo' file_md['visibility_during_embargo'] = 'restricted' file_md['visibility_after_embargo'] = 'open' - if !embargo_date.nil? - file_md['embargo_release_date'] = convert_to_iso(embargo_date) - else - file_md['embargo_release_date'] = nil - end + file_md['embargo_release_date'] = embargo_release_date end filesets_metadata << file_md end end - # puts("works_metadata: #{works_metadata}") - # puts("files_metadata: #{filesets_metadata}") all_md = works_metadata + filesets_metadata - # puts("all_md: #{all_md}") csv_rows = hash_array_to_csv_array(all_md) bulkrax_csv_filepath = "#{bulkrax_zip_path}/metadata.csv" write_csv(csv_rows, bulkrax_csv_filepath) - # create metadata CSV from the works metadata array and the filesets array + # FUTURE EXPANSION: Zip up the bulkrax ingest manifest and files # zip up the working folder # Consider a system command here? Not so simple with rubyzip end From 01d91df0cbc5d8b151b87b4b140e246b7bcfd3f3 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Mon, 3 Jun 2024 20:00:21 +0000 Subject: [PATCH 18/41] Add scholarspace-ingest directory and volume mapping --- Dockerfile | 1 + docker-compose.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 1f3a58ec..c4302691 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,6 +41,7 @@ RUN mkdir -p /opt/scholarspace/scholarspace-hyrax \ && mkdir -p /opt/scholarspace/scholarspace-tmp \ && mkdir -p /opt/scholarspace/scholarspace-minter \ && mkdir -p /opt/scholarspace/scholarspace-derivatives \ + && mkdir -p /opt/scholarspace/scholarspace-ingest \ && chmod 775 -R /opt/scholarspace/scholarspace-derivatives WORKDIR /opt/scholarspace/scholarspace-hyrax diff --git a/docker-compose.yml b/docker-compose.yml index db72b3d7..cadac627 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -116,6 +116,7 @@ services: - ${NGINX_CERT_DIR}:/opt/scholarspace/certs - ${NGINX_KEY_DIR}:/opt/scholarspace/keys - /opt/scholarspace/scholarspace-derivatives:/opt/scholarspace/scholarspace-derivatives + - /opt/scholarspace/scholarspace-ingest:/opt/scholarspace/scholarspace-ingest - app-hyrax:/opt/scholarspace # Uncomment for development # - /opt/scholarspace/scholarspace-hyrax:/opt/scholarspace/scholarspace-hyrax @@ -149,6 +150,7 @@ services: - ${NGINX_CERT_DIR}:/opt/scholarspace/certs - ${NGINX_KEY_DIR}:/opt/scholarspace/keys - /opt/scholarspace/scholarspace-derivatives:/opt/scholarspace/scholarspace-derivatives + - /opt/scholarspace/scholarspace-ingest:/opt/scholarspace/scholarspace-ingest - app-hyrax:/opt/scholarspace # Uncomment for development # - /opt/scholarspace/scholarspace-hyrax:/opt/scholarspace/scholarspace-hyrax From 020ad50a7e04c28fe730a1be9aab030a6018ca83 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Tue, 4 Jun 2024 18:15:31 +0000 Subject: [PATCH 19/41] Add mapping for scholarspace-ingest directory --- .github/workflows/ci-cache.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-cache.yml b/.github/workflows/ci-cache.yml index 647a5f5a..b06d5088 100644 --- a/.github/workflows/ci-cache.yml +++ b/.github/workflows/ci-cache.yml @@ -33,6 +33,7 @@ jobs: mkdir /opt/scholarspace-minter mkdir /opt/scholarspace/fedora-data mkdir /opt/scholarspace/solr-data + mkdir /opt/scholarspace/scholarspace-ingest cd /opt/scholarspace # Checkout the repository code - name: Check out repository code From 99cbb751083d00721e910ac15dddf8e379281f41 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Tue, 4 Jun 2024 18:27:14 +0000 Subject: [PATCH 20/41] Add CI directive to create ingest folder --- README.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/README.md b/README.md index b69b8b76..60a451ad 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ a separate user for the app, but it is not necessary. That user will need to ow /opt/scholarspace/certs /opt/scholarspace/scholarspace-tmp /opt/scholarspace/scholarspace-minter + /opt/scholarspace/scholarspace-ingest ``` 6. In `/opt/scholarspace/scholarspace-hyrax` run `cp example.env .env` to create the local environment file. 7. Edit `.env` to add the following values: @@ -174,16 +175,6 @@ echo $CR_PAT | docker login ghcr.io -u [USERNAME] --password-stdin ## Setting up a new production instance -### (Optional) Install etd-loader - -* Install the **etd-loader** application in `/opt/etd-loader` as per instructions at https://github.com/gwu-libraries/etd-loader - -* When configuring `config.py`, ensure that it contains the following values: - ``` - ingest_path = "/opt/scholarspace/scholarspace-hyrax" - ingest_command = "rake RAILS_ENV=production gwss:ingest_etd" - ``` - ### Migrating Production Database In the app-server container (i.e. through `docker exec -it scholarspace-hyrax_app-server_1 /bin/sh`, followed by `su scholarspace`), run: From 9fd14dd88218779a29deab19c349b60d626838b9 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Tue, 4 Jun 2024 18:45:28 +0000 Subject: [PATCH 21/41] Upgrade Bulkrax to 8.1.0 --- Gemfile | 5 +---- Gemfile.lock | 40 +++++++++++++++++----------------------- db/schema.rb | 26 +++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/Gemfile b/Gemfile index 443dcf47..f8581083 100644 --- a/Gemfile +++ b/Gemfile @@ -64,10 +64,7 @@ gem 'riiif', '~> 2.0' gem 'cookies_eu' -# When the next Bulkrax release *after* 8.0.0 is available, change this to: -# gem 'bulkrax', '8.0.1' # or whatever the new version is -# and re-test. -gem 'bulkrax', git: 'https://github.com/samvera/bulkrax.git', ref: 'd8f9b85a9064b352a252c5deb590a430ca4a738e' +gem 'bulkrax', '8.1.0' gem 'willow_sword', github: 'notch8/willow_sword' diff --git a/Gemfile.lock b/Gemfile.lock index 1f6ca0c0..79d861e0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,28 +7,6 @@ GIT rails (>= 5.1.6) rubyzip (>= 1.0.0) -GIT - remote: https://github.com/samvera/bulkrax.git - revision: d8f9b85a9064b352a252c5deb590a430ca4a738e - ref: d8f9b85a9064b352a252c5deb590a430ca4a738e - specs: - bulkrax (8.0.0) - bagit (~> 0.4.6) - coderay - denormalize_fields - iso8601 (~> 0.9.0) - kaminari - language_list (~> 1.2, >= 1.2.1) - libxml-ruby (~> 3.2.4) - loofah (>= 2.2.3) - marcel - oai (>= 0.4, < 2.x) - rack (>= 2.0.6) - rails (>= 5.1.6) - rdf (>= 2.0.2, < 4.0) - rubyzip - simple_form - GEM remote: https://rubygems.org/ specs: @@ -184,6 +162,22 @@ GEM signet (~> 0.8) typhoeus builder (3.2.4) + bulkrax (8.1.0) + bagit (~> 0.4.6) + coderay + denormalize_fields + iso8601 (~> 0.9.0) + kaminari + language_list (~> 1.2, >= 1.2.1) + libxml-ruby (~> 3.2.4) + loofah (>= 2.2.3) + marcel + oai (>= 0.4, < 2.x) + rack (>= 2.0.6) + rails (>= 5.1.6) + rdf (>= 2.0.2, < 4.0) + rubyzip + simple_form byebug (11.1.3) cancancan (1.17.0) capybara (3.39.2) @@ -1073,7 +1067,7 @@ DEPENDENCIES blacklight_range_limit bootsnap (>= 1.1.0) bootstrap-sass (~> 3.0) - bulkrax! + bulkrax (= 8.1.0) byebug capybara (>= 2.15) chosen-rails diff --git a/db/schema.rb b/db/schema.rb index e963d30e..304acc64 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_02_08_142942) do +ActiveRecord::Schema.define(version: 2024_03_07_053156) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -40,6 +40,10 @@ t.datetime "last_succeeded_at" t.string "importerexporter_type", default: "Bulkrax::Importer" t.integer "import_attempts", default: 0 + t.string "status_message", default: "Pending" + t.index ["identifier", "importerexporter_id", "importerexporter_type"], name: "bulkrax_identifier_idx" + t.index ["importerexporter_id", "importerexporter_type"], name: "bulkrax_entries_importerexporter_idx" + t.index ["type"], name: "index_bulkrax_entries_on_type" end create_table "bulkrax_exporter_runs", force: :cascade do |t| @@ -70,6 +74,9 @@ t.date "finish_date" t.string "work_visibility" t.string "workflow_status" + t.boolean "include_thumbnails", default: false + t.boolean "generated_metadata", default: false + t.string "status_message", default: "Pending" t.index ["user_id"], name: "index_bulkrax_exporters_on_user_id" end @@ -110,9 +117,22 @@ t.boolean "validate_only" t.datetime "last_error_at" t.datetime "last_succeeded_at" + t.string "status_message", default: "Pending" t.index ["user_id"], name: "index_bulkrax_importers_on_user_id" end + create_table "bulkrax_pending_relationships", force: :cascade do |t| + t.bigint "importer_run_id", null: false + t.string "parent_id", null: false + t.string "child_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "order", default: 0 + t.index ["child_id"], name: "index_bulkrax_pending_relationships_on_child_id" + t.index ["importer_run_id"], name: "index_bulkrax_pending_relationships_on_importer_run_id" + t.index ["parent_id"], name: "index_bulkrax_pending_relationships_on_parent_id" + end + create_table "bulkrax_statuses", force: :cascade do |t| t.string "status_message" t.string "error_class" @@ -124,6 +144,9 @@ t.string "runnable_type" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["error_class"], name: "index_bulkrax_statuses_on_error_class" + t.index ["runnable_id", "runnable_type"], name: "bulkrax_statuses_runnable_idx" + t.index ["statusable_id", "statusable_type"], name: "bulkrax_statuses_statusable_idx" end create_table "checksum_audit_logs", id: :serial, force: :cascade do |t| @@ -682,6 +705,7 @@ add_foreign_key "bulkrax_exporter_runs", "bulkrax_exporters", column: "exporter_id" add_foreign_key "bulkrax_importer_runs", "bulkrax_importers", column: "importer_id" + add_foreign_key "bulkrax_pending_relationships", "bulkrax_importer_runs", column: "importer_run_id" add_foreign_key "collection_type_participants", "hyrax_collection_types" add_foreign_key "curation_concerns_operations", "users" add_foreign_key "mailboxer_conversation_opt_outs", "mailboxer_conversations", column: "conversation_id", name: "mb_opt_outs_on_conversations_id" From c764271f4112dfe7fe1191d2a1e1b81ef2be9447 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Fri, 7 Jun 2024 05:44:31 +0000 Subject: [PATCH 22/41] Allow admin user to visit /importers and /exporters even when there isn't yet an admin set for admin to deposit to --- app/models/ability.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 083b4771..4cd1b013 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -45,10 +45,12 @@ def contentadmins_can_create_curation_concerns # Added for Bulkrax 5.0.0+ def can_import_works? - can_create_any_work? + # can_create_any_work? + admin? or contentadmin_user? end def can_export_works? - can_create_any_work? + # can_create_any_work? + admin? or contentadmin_user? end end From 22ea9744c771ba052531e839bd6873f08dbf1f45 Mon Sep 17 00:00:00 2001 From: Alex Boyd Date: Tue, 11 Jun 2024 21:53:44 +0000 Subject: [PATCH 23/41] Add fixture zips for bulkrax rspec testing --- spec/fixtures/etd_zips/etdadmin_upload_1.zip | Bin 0 -> 65669 bytes spec/fixtures/etd_zips/etdadmin_upload_2.zip | Bin 0 -> 13781 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 spec/fixtures/etd_zips/etdadmin_upload_1.zip create mode 100644 spec/fixtures/etd_zips/etdadmin_upload_2.zip diff --git a/spec/fixtures/etd_zips/etdadmin_upload_1.zip b/spec/fixtures/etd_zips/etdadmin_upload_1.zip new file mode 100644 index 0000000000000000000000000000000000000000..ec0e4ecfe14fa579fbea9c4f3beda66e882547cd GIT binary patch literal 65669 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 Date: Tue, 11 Jun 2024 21:54:27 +0000 Subject: [PATCH 24/41] Add sidekiq inline testing setting --- spec/rails_helper.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index dcb6421c..22a21aad 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -9,6 +9,9 @@ require_relative "../spec/support/fedora_cleaner" require_relative "../spec/support/solr_cleaner" +require 'sidekiq/testing' +Sidekiq::Testing.inline! + # Add additional requires below this line. Rails is not loaded until this point! # Requires supporting ruby files with custom matchers and macros, etc, in From b3aa9c1527b8b82d93c6e00d90e595cb7d9f671a Mon Sep 17 00:00:00 2001 From: Alex Boyd Date: Tue, 11 Jun 2024 21:55:04 +0000 Subject: [PATCH 25/41] Set testing queue for inline sidekiq --- config/environments/test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/test.rb b/config/environments/test.rb index c284a0f7..13aac196 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -41,5 +41,5 @@ # config.action_view.raise_on_missing_translations = true config.permanent_url_base = "https://scholarspace-etds.library.gwu.edu/" - config.active_job.queue_adapter = :test + config.active_job.queue_adapter = :sidekiq end From 86762e0ef7a3a35db4c0741db3dd8691f5efce6d Mon Sep 17 00:00:00 2001 From: Alex Boyd Date: Tue, 11 Jun 2024 21:55:58 +0000 Subject: [PATCH 26/41] Modify ingest_bulkrax_prep when in test mode --- lib/tasks/ingest_bulkrax_prep.rake | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/tasks/ingest_bulkrax_prep.rake b/lib/tasks/ingest_bulkrax_prep.rake index 271973d5..7913c5c0 100644 --- a/lib/tasks/ingest_bulkrax_prep.rake +++ b/lib/tasks/ingest_bulkrax_prep.rake @@ -156,8 +156,15 @@ namespace :gwss do end end - # create folder for metadata.csv and files folder - bulkrax_zip_path = "#{ENV['TEMP_FILE_BASE']}/bulkrax_zip" + # create folder for metadata.csv and files folder + + # if running spec tests, add /test/ to the tmp file path to prevent filling /tmp/bulkrax_zip when tests are run + if Rails.env.test? + bulkrax_zip_path = "#{ENV['TEMP_FILE_BASE']}/test/bulkrax_zip" + else + bulkrax_zip_path = "#{ENV['TEMP_FILE_BASE']}/bulkrax_zip" + end + bulkrax_files_path = "#{bulkrax_zip_path}/files" puts "File.exists?(bulkrax_zip_path) = #{File.exists?(bulkrax_zip_path)}" FileUtils.makedirs("#{bulkrax_files_path}") unless File.exists?(bulkrax_zip_path) From 906e6229816da7afbcd66468cf6ed1e8d744315b Mon Sep 17 00:00:00 2001 From: Alex Boyd Date: Tue, 11 Jun 2024 21:57:25 +0000 Subject: [PATCH 27/41] Add bulkrax importer tests --- spec/features/bulkrax_upload_spec.rb | 108 +++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 spec/features/bulkrax_upload_spec.rb diff --git a/spec/features/bulkrax_upload_spec.rb b/spec/features/bulkrax_upload_spec.rb new file mode 100644 index 00000000..55f8dfb7 --- /dev/null +++ b/spec/features/bulkrax_upload_spec.rb @@ -0,0 +1,108 @@ +require 'rails_helper' +require 'csv' + +Rails.application.load_tasks + +RSpec.describe "Deposit files through Bulkrax" do + + before :all do + # remove the folder so it doesn't repeatedly add new works when ingest task is run + FileUtils.rm_rf("#{Rails.root}/tmp/test/bulkrax_zip") + + Rake::Task["gwss:ingest_pq_etds"].invoke("#{Rails.root}/spec/fixtures/etd_zips") + end + + it 'generates deposit file structure via gwss:ingest_pq_etds task' do + expect(File.directory?("#{Rails.root}/tmp/test/bulkrax_zip")).to be true + expect(File.directory?("#{Rails.root}/tmp/test/bulkrax_zip/files")).to be true + + expect(File.directory?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_1")).to be true + expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_1/Ab_gwu_0075A_16593_DATA.xml")).to be true + expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_1/Ab_gwu_0075A_16593.pdf")).to be true + + expect(File.directory?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_2")).to be true + expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_2/Ab_gwu_0076A_12345_DATA.xml")).to be true + expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_2/Ab_gwu_0076A_12345.pdf")).to be true + + expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/metadata.csv")).to be true + end + + it 'generates accurate CSV file for import' do + csv_rows = CSV.read("#{Rails.root}/tmp/test/bulkrax_zip/metadata.csv") + + headers_arr = csv_rows[0] + + expect(headers_arr).to eq(["model", "title", "creator", "contributor", "language", + "description", "keyword", "degree", "advisor", "gw_affiliation", + "date_created", "committee_member", "rights_statement", "bulkrax_identifier", + "file", "parents", "visibility", "visibility_during_embargo", + "visibility_after_embargo", "embargo_release_date"]) + + first_work_metadata = csv_rows[1] + first_work_metadata[13] = "a-replacement-uuid" + # this and similar lines replace the uuids generated with placeholders, which is messy but testable + # could probably do something with a regex or some other way to test + + expect(first_work_metadata).to eq(["GwEtd", "A False Work For Testing Purposes", "Boyd, Alex L", + "","en","","testing;rspec;ruby;scholarspace;beefaroni","Ph.D.","Kitty, Sandwich", + "English","2024","Kerchner, Dan;Smith, Dolsy","http://rightsstatements.org/vocab/InC/1.0/", + "a-replacement-uuid",nil,nil,nil,nil,nil,nil]) + + second_work_metadata = csv_rows[2] + second_work_metadata[13] = "a-replacement-uuid" + + expect(second_work_metadata).to eq(["GwEtd","Another False Work For Bulkrax Testing Purposes","Boyd, Alex L", + "","en","","testing;rspec;ruby;scholarspace;beefaroni","Ph.D.","Kitty, Sandwich", + "English","2024","Kerchner, Dan;Smith, Dolsy","http://rightsstatements.org/vocab/InC/1.0/", + "a-replacement-uuid",nil,nil,nil,nil,nil,nil]) + + first_work_file_data = csv_rows[3] + first_work_file_data[13] = "a-replacement-uuid" + first_work_file_data[15] = "another-replacement-uuid" + + expect(first_work_file_data).to eq(["FileSet","Ab_gwu_0075A_16593.pdf",nil,nil,nil,nil,nil,nil, + nil,nil,nil,nil,nil,"a-replacement-uuid","etdadmin_upload_1/Ab_gwu_0075A_16593.pdf", + "another-replacement-uuid","embargo","restricted","open","2026-01-05"]) + + second_work_file_data = csv_rows[4] + second_work_file_data[13] = "a-replacement-uuid" + second_work_file_data[15] = "another-replacement-uuid" + + expect(second_work_file_data).to eq(["FileSet","Ab_gwu_0076A_12345.pdf",nil,nil,nil,nil,nil,nil, + nil,nil,nil,nil,nil,"a-replacement-uuid","etdadmin_upload_2/Ab_gwu_0076A_12345.pdf", + "another-replacement-uuid","embargo","restricted","open","2026-01-05"]) + end + + it 'can deposit works via bulkrax import' do + admin_user = FactoryBot.create(:admin_user) + etds_admin_set = Hyrax::AdministrativeSet.new(title: ['ETDs']) + etds_admin_set = Hyrax.persister.save(resource: etds_admin_set) + Hyrax::AdminSetCreateService.call!(admin_set: etds_admin_set, creating_user: admin_user) + + sign_in_user(admin_user) + + visit '/importers/new' + + fill_in('importer_name', with: "Test Bulkrax Import") + select('ETDs', from: 'importer_admin_set_id') + select('CSV - Comma Separated Values', from: 'importer_parser_klass') + + import_parser_radio_button_elements = page.all('//*[@id="importer_parser_fields_file_style_specify_a_path_on_the_server"]') + import_parser_radio_button_elements.last.click + + import_parser_file_path_elements = page.all('//*[@id="importer_parser_fields_import_file_path"]') + import_parser_file_path_elements.last.fill_in with: "#{Rails.root}/tmp/test/bulkrax_zip/metadata.csv" + + click_on("Create and Import") + + # check if both works are created + work_1 = GwEtd.where(title: "A False Work For Testing Purposes").first + work_2 = GwEtd.where(title: "Another False Work For Bulkrax Testing Purposes").first + + # check if both works get an embargo ID + expect(work_1.embargo_id.present?).to be true + expect(work_2.embargo_id.present?).to be true + end +end + +# bundle exec rspec spec/features/bulkrax_upload_spec.rb \ No newline at end of file From d33a54acb8339461081ca30e892e117734b5059b Mon Sep 17 00:00:00 2001 From: Alex Boyd Date: Tue, 11 Jun 2024 22:16:49 +0000 Subject: [PATCH 28/41] Simplify bulkrax tests --- spec/features/bulkrax_upload_spec.rb | 46 +++++++++------------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/spec/features/bulkrax_upload_spec.rb b/spec/features/bulkrax_upload_spec.rb index 55f8dfb7..eee827a0 100644 --- a/spec/features/bulkrax_upload_spec.rb +++ b/spec/features/bulkrax_upload_spec.rb @@ -32,45 +32,29 @@ headers_arr = csv_rows[0] + # check that header row generated is correct expect(headers_arr).to eq(["model", "title", "creator", "contributor", "language", "description", "keyword", "degree", "advisor", "gw_affiliation", "date_created", "committee_member", "rights_statement", "bulkrax_identifier", "file", "parents", "visibility", "visibility_during_embargo", "visibility_after_embargo", "embargo_release_date"]) - first_work_metadata = csv_rows[1] - first_work_metadata[13] = "a-replacement-uuid" - # this and similar lines replace the uuids generated with placeholders, which is messy but testable - # could probably do something with a regex or some other way to test - - expect(first_work_metadata).to eq(["GwEtd", "A False Work For Testing Purposes", "Boyd, Alex L", - "","en","","testing;rspec;ruby;scholarspace;beefaroni","Ph.D.","Kitty, Sandwich", - "English","2024","Kerchner, Dan;Smith, Dolsy","http://rightsstatements.org/vocab/InC/1.0/", - "a-replacement-uuid",nil,nil,nil,nil,nil,nil]) + # check that there are five rows - one for header, one for each of the etds, one for each of the files + expect(csv_rows.count).to eq(5) + first_work_metadata = csv_rows[1] second_work_metadata = csv_rows[2] - second_work_metadata[13] = "a-replacement-uuid" + first_file_data = csv_rows[3] + second_file_data = csv_rows[4] - expect(second_work_metadata).to eq(["GwEtd","Another False Work For Bulkrax Testing Purposes","Boyd, Alex L", - "","en","","testing;rspec;ruby;scholarspace;beefaroni","Ph.D.","Kitty, Sandwich", - "English","2024","Kerchner, Dan;Smith, Dolsy","http://rightsstatements.org/vocab/InC/1.0/", - "a-replacement-uuid",nil,nil,nil,nil,nil,nil]) - - first_work_file_data = csv_rows[3] - first_work_file_data[13] = "a-replacement-uuid" - first_work_file_data[15] = "another-replacement-uuid" + expect(first_work_metadata.include?("GwEtd")).to be true + expect(second_work_metadata.include?("GwEtd")).to be true - expect(first_work_file_data).to eq(["FileSet","Ab_gwu_0075A_16593.pdf",nil,nil,nil,nil,nil,nil, - nil,nil,nil,nil,nil,"a-replacement-uuid","etdadmin_upload_1/Ab_gwu_0075A_16593.pdf", - "another-replacement-uuid","embargo","restricted","open","2026-01-05"]) + expect(first_file_data.include?("embargo")).to be true + expect(second_file_data.include?("embargo")).to be true - second_work_file_data = csv_rows[4] - second_work_file_data[13] = "a-replacement-uuid" - second_work_file_data[15] = "another-replacement-uuid" - - expect(second_work_file_data).to eq(["FileSet","Ab_gwu_0076A_12345.pdf",nil,nil,nil,nil,nil,nil, - nil,nil,nil,nil,nil,"a-replacement-uuid","etdadmin_upload_2/Ab_gwu_0076A_12345.pdf", - "another-replacement-uuid","embargo","restricted","open","2026-01-05"]) + expect(first_file_data.include?("restricted")).to be true + expect(second_file_data.include?("restricted")).to be true end it 'can deposit works via bulkrax import' do @@ -94,6 +78,8 @@ import_parser_file_path_elements.last.fill_in with: "#{Rails.root}/tmp/test/bulkrax_zip/metadata.csv" click_on("Create and Import") + + # the 'expect' statements below are not super specific, but the test will fail if any step in the deposit fails, so feels robust enough # check if both works are created work_1 = GwEtd.where(title: "A False Work For Testing Purposes").first @@ -103,6 +89,4 @@ expect(work_1.embargo_id.present?).to be true expect(work_2.embargo_id.present?).to be true end -end - -# bundle exec rspec spec/features/bulkrax_upload_spec.rb \ No newline at end of file +end \ No newline at end of file From 9030ee8e95a9047e785e43962560af964ce13506 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Fri, 14 Jun 2024 19:26:39 +0000 Subject: [PATCH 29/41] Populates degree and resource_type. License is still WIP, pending input from ScholComm --- lib/tasks/ingest_bulkrax_prep.rake | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/tasks/ingest_bulkrax_prep.rake b/lib/tasks/ingest_bulkrax_prep.rake index 7913c5c0..b53082e5 100644 --- a/lib/tasks/ingest_bulkrax_prep.rake +++ b/lib/tasks/ingest_bulkrax_prep.rake @@ -7,6 +7,8 @@ namespace :gwss do desc "Creates a bulkrax zip for all of the ProQuest ETD zip files in a folder" task :ingest_pq_etds, [:filepath] do |t, args| + @degree_etd_map = {} + def get_metadata_doc_path(pq_files_dir) xml_paths = Dir.glob("#{pq_files_dir}/*_DATA.xml") pq_xml_file_path = xml_paths.first @@ -109,6 +111,20 @@ namespace :gwss do date.strftime('%Y-%m-%d') end + def build_resource_type_degree_mapping + etd_degree_map = YAML.load_file('config/etd_degree_map.yml') + @degree_etd_map = {} + degree_categories = etd_degree_map.keys + # Flip etd_degree_map to create degree_etd_map + # So that for any given degree, we can get back whether it's a masters or a doctorate + degree_categories.each do |degree_category| + etd_degree_map[degree_category].each do |degree_name| + # upcase each degree (just in case) and ignore "."s + @degree_etd_map[degree_name.upcase.delete('.')] = degree_category + end + end + end + def extract_metadata(doc) work_metadata = Hash.new work_metadata['model'] = 'GwEtd' @@ -119,13 +135,20 @@ namespace :gwss do work_metadata['language'] = get_node_value(doc, "//DISS_description/DISS_categorization/DISS_language") work_metadata['description'] = get_abstract(doc) work_metadata['keyword'] = get_keywords(doc).join(';') - work_metadata['degree'] = get_node_value(doc, "//DISS_description/DISS_degree") + degree = get_node_value(doc, "//DISS_description/DISS_degree") + work_metadata['degree'] = degree + work_metadata['resource_type'] = @degree_etd_map[degree.upcase.delete('.')] work_metadata['advisor'] = get_advisors(doc).join(';') work_metadata['gw_affiliation'] = get_node_value(doc, "//DISS_description/DISS_institution/DISS_inst_contact") etd_date_created = get_date_created(doc) work_metadata['date_created'] = etd_date_created unless etd_date_created.nil? work_metadata['committee_member'] = get_committee_members(doc).join(';') work_metadata['rights_statement'] = 'http://rightsstatements.org/vocab/InC/1.0/' + # Can't currently load this license because this Bulkrax code + # https://github.com/samvera/bulkrax/blob/v8.1.0/app/models/concerns/bulkrax/import_behavior.rb#L145-L146 + # will try to match it with http://www.europeana.eu/portal/rights/rr-r.html/ (slash-terminated) + # -- not finding a match, Bulkrax will throw an error. + # work_metadata['license'] = 'http://www.europeana.eu/portal/rights/rr-r.html' work_metadata end @@ -155,6 +178,10 @@ namespace :gwss do File.join(File.dirname(filepath), File.basename(filepath).tr(' ', '_')) end end + + build_resource_type_degree_mapping + puts "build_resource_type_degree_mapping: " + puts @degree_etd_map # create folder for metadata.csv and files folder From bf8c139e42392c361a0794aabcdf4ff2d5dbf07b Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Fri, 14 Jun 2024 20:13:56 +0000 Subject: [PATCH 30/41] Added resource_type field --- spec/features/bulkrax_upload_spec.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/features/bulkrax_upload_spec.rb b/spec/features/bulkrax_upload_spec.rb index eee827a0..792871f1 100644 --- a/spec/features/bulkrax_upload_spec.rb +++ b/spec/features/bulkrax_upload_spec.rb @@ -32,9 +32,8 @@ headers_arr = csv_rows[0] - # check that header row generated is correct expect(headers_arr).to eq(["model", "title", "creator", "contributor", "language", - "description", "keyword", "degree", "advisor", "gw_affiliation", + "description", "keyword", "degree", "resource_type", "advisor", "gw_affiliation", "date_created", "committee_member", "rights_statement", "bulkrax_identifier", "file", "parents", "visibility", "visibility_during_embargo", "visibility_after_embargo", "embargo_release_date"]) @@ -89,4 +88,4 @@ expect(work_1.embargo_id.present?).to be true expect(work_2.embargo_id.present?).to be true end -end \ No newline at end of file +end From 602dc8a50e8c33e3df74477642c9a75d6d4fa14e Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Thu, 14 Mar 2024 15:20:11 +0000 Subject: [PATCH 31/41] updated Gemfile.lock --- Gemfile.lock | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index 79d861e0..7a50fad7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -162,8 +162,13 @@ GEM signet (~> 0.8) typhoeus builder (3.2.4) +<<<<<<< HEAD bulkrax (8.1.0) bagit (~> 0.4.6) +======= + bulkrax (5.5.1) + bagit (~> 0.4) +>>>>>>> updated Gemfile.lock coderay denormalize_fields iso8601 (~> 0.9.0) @@ -1067,7 +1072,11 @@ DEPENDENCIES blacklight_range_limit bootsnap (>= 1.1.0) bootstrap-sass (~> 3.0) +<<<<<<< HEAD bulkrax (= 8.1.0) +======= + bulkrax (= 5.5.1) +>>>>>>> updated Gemfile.lock byebug capybara (>= 2.15) chosen-rails From edafac1a5f7a8867218020d38c93d7c56328957b Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Thu, 14 Mar 2024 15:46:56 +0000 Subject: [PATCH 32/41] Re-apply bulkrax 5.5.1 changes --- Gemfile.lock | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7a50fad7..79d861e0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -162,13 +162,8 @@ GEM signet (~> 0.8) typhoeus builder (3.2.4) -<<<<<<< HEAD bulkrax (8.1.0) bagit (~> 0.4.6) -======= - bulkrax (5.5.1) - bagit (~> 0.4) ->>>>>>> updated Gemfile.lock coderay denormalize_fields iso8601 (~> 0.9.0) @@ -1072,11 +1067,7 @@ DEPENDENCIES blacklight_range_limit bootsnap (>= 1.1.0) bootstrap-sass (~> 3.0) -<<<<<<< HEAD bulkrax (= 8.1.0) -======= - bulkrax (= 5.5.1) ->>>>>>> updated Gemfile.lock byebug capybara (>= 2.15) chosen-rails From 38c97e023912c6d0c817bcae7bc59f72a3b2cae8 Mon Sep 17 00:00:00 2001 From: Dan Kerchner Date: Thu, 14 Mar 2024 16:40:30 +0000 Subject: [PATCH 33/41] Upgrade bulkrax to 6.0.1 --- Gemfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Gemfile b/Gemfile index f8581083..09a9a386 100644 --- a/Gemfile +++ b/Gemfile @@ -64,7 +64,12 @@ gem 'riiif', '~> 2.0' gem 'cookies_eu' +<<<<<<< HEAD gem 'bulkrax', '8.1.0' +======= +#gem 'bulkrax', git: 'https://github.com/samvera-labs/bulkrax.git' +gem 'bulkrax', '6.0.1' +>>>>>>> Upgrade bulkrax to 6.0.1 gem 'willow_sword', github: 'notch8/willow_sword' From 8172e658ebc619b7b44f7fd0f3e7010acb173043 Mon Sep 17 00:00:00 2001 From: Dolsy Smith Date: Tue, 6 Aug 2024 18:27:03 +0000 Subject: [PATCH 34/41] Added display of license field; deprecated previous field; added new field --- app/views/hyrax/gw_etds/_attribute_rows.erb | 1 + app/views/hyrax/gw_works/_attribute_rows.erb | 1 + config/authorities/licenses.yml | 4 ++++ config/initializers/qa_authorities_patch.rb | 12 ++++++++++++ 4 files changed, 18 insertions(+) create mode 100644 config/initializers/qa_authorities_patch.rb diff --git a/app/views/hyrax/gw_etds/_attribute_rows.erb b/app/views/hyrax/gw_etds/_attribute_rows.erb index a7dd78b5..c485958f 100644 --- a/app/views/hyrax/gw_etds/_attribute_rows.erb +++ b/app/views/hyrax/gw_etds/_attribute_rows.erb @@ -11,6 +11,7 @@ <%= presenter.attribute_to_html(:related_url, render_as: :external_link) %> <%= presenter.attribute_to_html(:resource_type, render_as: :faceted) %> <%= presenter.attribute_to_html(:source) %> +<%= presenter.attribute_to_html(:license, render_as: :license) %> <%= presenter.attribute_to_html(:rights_statement, render_as: :rights_statement) %> <%= presenter.attribute_to_html(:gw_affiliation, render_as: :faceted) %> <%= presenter.attribute_to_html(:degree, render_as: :faceted) %> diff --git a/app/views/hyrax/gw_works/_attribute_rows.erb b/app/views/hyrax/gw_works/_attribute_rows.erb index a7dd78b5..c485958f 100644 --- a/app/views/hyrax/gw_works/_attribute_rows.erb +++ b/app/views/hyrax/gw_works/_attribute_rows.erb @@ -11,6 +11,7 @@ <%= presenter.attribute_to_html(:related_url, render_as: :external_link) %> <%= presenter.attribute_to_html(:resource_type, render_as: :faceted) %> <%= presenter.attribute_to_html(:source) %> +<%= presenter.attribute_to_html(:license, render_as: :license) %> <%= presenter.attribute_to_html(:rights_statement, render_as: :rights_statement) %> <%= presenter.attribute_to_html(:gw_affiliation, render_as: :faceted) %> <%= presenter.attribute_to_html(:degree, render_as: :faceted) %> diff --git a/config/authorities/licenses.yml b/config/authorities/licenses.yml index 8b7ef40c..40c4c917 100644 --- a/config/authorities/licenses.yml +++ b/config/authorities/licenses.yml @@ -42,5 +42,9 @@ terms: term: CC0 1.0 Universal active: true - id: http://www.europeana.eu/portal/rights/rr-r.html + term: All rights reserved (deprecated) + active: false + - id: All rights reserved term: All rights reserved active: true + diff --git a/config/initializers/qa_authorities_patch.rb b/config/initializers/qa_authorities_patch.rb new file mode 100644 index 00000000..f70c3a94 --- /dev/null +++ b/config/initializers/qa_authorities_patch.rb @@ -0,0 +1,12 @@ +module Hyrax + module QaSelectServicePatch + def include_current_value(value, _index, render_options, html_options) + unless value.blank? || active?(value) + html_options[:class][-1] += ' force-select' + render_options += [[label(value) { value }, value]] + end + [render_options, html_options] + end + end +end +Hyrax::QaSelectService.prepend Hyrax::QaSelectServicePatch \ No newline at end of file From 65e662aa40602c48c77a52d0f3770cdaa5722799 Mon Sep 17 00:00:00 2001 From: Dolsy Smith Date: Tue, 6 Aug 2024 18:40:00 +0000 Subject: [PATCH 35/41] Fixed merge error in Gemfile --- Gemfile | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Gemfile b/Gemfile index 09a9a386..f8581083 100644 --- a/Gemfile +++ b/Gemfile @@ -64,12 +64,7 @@ gem 'riiif', '~> 2.0' gem 'cookies_eu' -<<<<<<< HEAD gem 'bulkrax', '8.1.0' -======= -#gem 'bulkrax', git: 'https://github.com/samvera-labs/bulkrax.git' -gem 'bulkrax', '6.0.1' ->>>>>>> Upgrade bulkrax to 6.0.1 gem 'willow_sword', github: 'notch8/willow_sword' From 644d5535ec0d983e723d560166f745706e3ef488 Mon Sep 17 00:00:00 2001 From: Dolsy Smith Date: Wed, 7 Aug 2024 12:29:58 +0000 Subject: [PATCH 36/41] moved patch to folder that mirrors Hyrax repo location --- .../initializers => app/services/hyrax}/qa_authorities_patch.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {config/initializers => app/services/hyrax}/qa_authorities_patch.rb (100%) diff --git a/config/initializers/qa_authorities_patch.rb b/app/services/hyrax/qa_authorities_patch.rb similarity index 100% rename from config/initializers/qa_authorities_patch.rb rename to app/services/hyrax/qa_authorities_patch.rb From 0804b38fba2c50cacea829e5136f1f55517e96d8 Mon Sep 17 00:00:00 2001 From: Dolsy Smith Date: Wed, 7 Aug 2024 16:29:43 +0000 Subject: [PATCH 37/41] Added rake task to update license field --- lib/tasks/replace_license_value.rake | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 lib/tasks/replace_license_value.rake diff --git a/lib/tasks/replace_license_value.rake b/lib/tasks/replace_license_value.rake new file mode 100644 index 00000000..774e8b52 --- /dev/null +++ b/lib/tasks/replace_license_value.rake @@ -0,0 +1,29 @@ +require 'rake' + +namespace :gwss do + desc 'Replace license value for deprecated Europeana license' + + task :replace_license_value => :environment do + old_value = 'http://www.europeana.eu/portal/rights/rr-r.html' + new_value = 'All rights reserved' + + # Since the text field (tesim) doesn't allow exact searching we do an additional filter just to be safe + solr_results(old_value).each do |doc| + work = ActiveFedora::Base.find(doc['id']) # Retrieve the Fedora object + work.license = work.license.map { |license_value| license_value == old_value ? new_value : license_value } # Update the Fedora object + work.save + end + end +end + +def solr_results(old_value) + # Retrieve paginated results if necessary for records with the old value in the license field + params = {:q => "license_tesim:#{old_value}", fl:'id,license_tesim'} + docs = ActiveFedora::SolrService.instance.conn.paginate(1, 1000, 'select', :params => params).dig('response', 'docs') + p = 2 + while (next_page = ActiveFedora::SolrService.instance.conn.paginate(p, 1000, 'select', :params => params).dig('response', 'docs')) != [] + docs += next_page + p += 1 + end + docs +end From 8f2d78ea0c52fe4636be7d3182de89ed657a1987 Mon Sep 17 00:00:00 2001 From: Dolsy Smith Date: Wed, 7 Aug 2024 18:05:20 +0000 Subject: [PATCH 38/41] Revised bulkrax spec test to use ENV variable for temp file path --- spec/features/bulkrax_upload_spec.rb | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/spec/features/bulkrax_upload_spec.rb b/spec/features/bulkrax_upload_spec.rb index 792871f1..3ee51dd1 100644 --- a/spec/features/bulkrax_upload_spec.rb +++ b/spec/features/bulkrax_upload_spec.rb @@ -5,30 +5,32 @@ RSpec.describe "Deposit files through Bulkrax" do + temp_base = ENV.fetch('TEMP_FILE_BASE', "#{Rails.root}/tmp") + before :all do # remove the folder so it doesn't repeatedly add new works when ingest task is run - FileUtils.rm_rf("#{Rails.root}/tmp/test/bulkrax_zip") + FileUtils.rm_rf("#{temp_base}/test/bulkrax_zip") Rake::Task["gwss:ingest_pq_etds"].invoke("#{Rails.root}/spec/fixtures/etd_zips") end it 'generates deposit file structure via gwss:ingest_pq_etds task' do - expect(File.directory?("#{Rails.root}/tmp/test/bulkrax_zip")).to be true - expect(File.directory?("#{Rails.root}/tmp/test/bulkrax_zip/files")).to be true + expect(File.directory?("#{temp_base}/test/bulkrax_zip")).to be true + expect(File.directory?("#{temp_base}/test/bulkrax_zip/files")).to be true - expect(File.directory?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_1")).to be true - expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_1/Ab_gwu_0075A_16593_DATA.xml")).to be true - expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_1/Ab_gwu_0075A_16593.pdf")).to be true + expect(File.directory?("#{temp_base}/test/bulkrax_zip/files/etdadmin_upload_1")).to be true + expect(File.file?("#{temp_base}/test/bulkrax_zip/files/etdadmin_upload_1/Ab_gwu_0075A_16593_DATA.xml")).to be true + expect(File.file?("#{temp_base}/test/bulkrax_zip/files/etdadmin_upload_1/Ab_gwu_0075A_16593.pdf")).to be true - expect(File.directory?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_2")).to be true - expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_2/Ab_gwu_0076A_12345_DATA.xml")).to be true - expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_2/Ab_gwu_0076A_12345.pdf")).to be true + expect(File.directory?("#{temp_base}/test/bulkrax_zip/files/etdadmin_upload_2")).to be true + expect(File.file?("#{temp_base}/test/bulkrax_zip/files/etdadmin_upload_2/Ab_gwu_0076A_12345_DATA.xml")).to be true + expect(File.file?("#{temp_base}/test/bulkrax_zip/files/etdadmin_upload_2/Ab_gwu_0076A_12345.pdf")).to be true - expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/metadata.csv")).to be true + expect(File.file?("#{temp_base}/test/bulkrax_zip/metadata.csv")).to be true end it 'generates accurate CSV file for import' do - csv_rows = CSV.read("#{Rails.root}/tmp/test/bulkrax_zip/metadata.csv") + csv_rows = CSV.read("#{temp_base}/test/bulkrax_zip/metadata.csv") headers_arr = csv_rows[0] @@ -74,7 +76,7 @@ import_parser_radio_button_elements.last.click import_parser_file_path_elements = page.all('//*[@id="importer_parser_fields_import_file_path"]') - import_parser_file_path_elements.last.fill_in with: "#{Rails.root}/tmp/test/bulkrax_zip/metadata.csv" + import_parser_file_path_elements.last.fill_in with: "#{temp_base}/test/bulkrax_zip/metadata.csv" click_on("Create and Import") From 37c3bdb3e476a91b7ec1c40115e126340a7a0382 Mon Sep 17 00:00:00 2001 From: Dolsy Smith Date: Thu, 8 Aug 2024 14:09:19 +0000 Subject: [PATCH 39/41] Added tests --- spec/factories/gw_works.rb | 9 ++++++ spec/features/gw_indexer_spec.rb | 9 ++++-- spec/features/license_spec.rb | 55 ++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 spec/features/license_spec.rb diff --git a/spec/factories/gw_works.rb b/spec/factories/gw_works.rb index 3f0dc88b..c9988eaf 100644 --- a/spec/factories/gw_works.rb +++ b/spec/factories/gw_works.rb @@ -2,6 +2,10 @@ factory :gw_work do id { Noid::Rails::Service.new.mint } title { [Faker::Book.title] } + creator { [Faker::Book.author] } + resource_type { [Hyrax::QaSelectService.new('resource_types').select_active_options.first.second] } + license { [Hyrax::QaSelectService.new('licenses').select_active_options.last.second] } + rights_statement { [Hyrax::QaSelectService.new('rights_statements').select_active_options.first.second] } transient do visibility { "public" } @@ -12,6 +16,11 @@ work.visibility = Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PUBLIC if options.visibility == "public" work.visibility = Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PRIVATE if options.visibility == "private" work.visibility = Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_AUTHENTICATED if options.visibility == "authenticated" + + # Add the user's ability to the work -> necessary for enabling access to Edit pages + actor = Hyrax::CurationConcern.actor + actor_environment = Hyrax::Actors::Environment.new(work, Ability.new(options.user), {}) + actor.create(actor_environment) end end end \ No newline at end of file diff --git a/spec/features/gw_indexer_spec.rb b/spec/features/gw_indexer_spec.rb index 1d7f0c72..9980de9c 100644 --- a/spec/features/gw_indexer_spec.rb +++ b/spec/features/gw_indexer_spec.rb @@ -61,7 +61,8 @@ gw_work_good_year = FactoryBot.create(:gw_work, admin_set: admin_set, visibility: "public", - date_created: ["2001"]) + date_created: ["2001"], + user: admin_user) expect(gw_work_good_year.to_solr['date_created_isim']).to eq(2001) @@ -72,7 +73,8 @@ gw_work_multiple_date_created_values = FactoryBot.create(:gw_work, admin_set: admin_set, visibility: "public", - date_created: ["august", "4", "2009", "2005", "1999"]) + date_created: ["august", "4", "2009", "2005", "1999"], + user: admin_user) expect(gw_work_multiple_date_created_values.to_solr['date_created_isim']).to eq(1999) @@ -83,7 +85,8 @@ gw_work_no_good_values = FactoryBot.create(:gw_work, admin_set: admin_set, visibility: "public", - date_created: ["august", "4", "garbanzo"]) + date_created: ["august", "4", "garbanzo"], + user: admin_user) expect(gw_work_no_good_values.to_solr['date_created_isim']).to eq(nil) end diff --git a/spec/features/license_spec.rb b/spec/features/license_spec.rb new file mode 100644 index 00000000..f0599b90 --- /dev/null +++ b/spec/features/license_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +RSpec.describe "View and edit license field" do + + let(:solr) { Blacklight.default_index.connection } + let(:admin_set) { FactoryBot.create(:admin_set) } + + let(:content_admin_user) { FactoryBot.create(:content_admin_user) } + + let(:work_with_license) { FactoryBot.create(:gw_work, + admin_set: admin_set, + visibility: "public", + user: content_admin_user) } + + let(:another_license_value) { Hyrax::QaSelectService.new('licenses').select_active_options.first.first } + + before do + ActiveFedora::Cleaner.clean! + solr.delete_by_query("*:*") + + solr.add(work_with_license.to_solr) + + solr.commit + end + + after do + ActiveFedora::Cleaner.clean! + solr.delete_by_query("*:*") + solr.commit + end + + context 'as a user looking at a work bearing a license' do + it 'can view the license field' do + visit "/concern/gw_works/#{work_with_license.id}" + expect(page).to have_content(work_with_license.license.first) + end + end +=begin context 'as a content-admin user' do + before :each do + sign_in_user(content_admin_user) + end + + it 'can edit the work containing the license field' do + visit "/concern/gw_works/#{work_with_license.id}/edit" + page.select another_license_value, :from => "gw_work_license" + page.click_on("Save changes") + end + + it 'can see the changes' do + visit "/concern/gw_works/#{work_with_license.id}" + expect(page).to have_content(another_license_value) + end + end +=end +end From c0e90ab97cb8cf7a7ae101f6855bcbf01e286f45 Mon Sep 17 00:00:00 2001 From: Dolsy Smith Date: Thu, 5 Sep 2024 12:52:36 +0000 Subject: [PATCH 40/41] Fixed tests to use temp directory from environment --- lib/tasks/ingest_bulkrax_prep.rake | 1 - spec/features/bulkrax_upload_spec.rb | 29 +++++++++++++++------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/tasks/ingest_bulkrax_prep.rake b/lib/tasks/ingest_bulkrax_prep.rake index b53082e5..d52de8ba 100644 --- a/lib/tasks/ingest_bulkrax_prep.rake +++ b/lib/tasks/ingest_bulkrax_prep.rake @@ -193,7 +193,6 @@ namespace :gwss do end bulkrax_files_path = "#{bulkrax_zip_path}/files" - puts "File.exists?(bulkrax_zip_path) = #{File.exists?(bulkrax_zip_path)}" FileUtils.makedirs("#{bulkrax_files_path}") unless File.exists?(bulkrax_zip_path) # get all ETD zip files in the args.filepath folder diff --git a/spec/features/bulkrax_upload_spec.rb b/spec/features/bulkrax_upload_spec.rb index 792871f1..b7e78668 100644 --- a/spec/features/bulkrax_upload_spec.rb +++ b/spec/features/bulkrax_upload_spec.rb @@ -7,28 +7,31 @@ before :all do # remove the folder so it doesn't repeatedly add new works when ingest task is run - FileUtils.rm_rf("#{Rails.root}/tmp/test/bulkrax_zip") + @temp_dir = "#{ENV['TEMP_FILE_BASE']}/test/bulkrax_zip" + if File.directory?(@temp_dir) + FileUtils.rm_rf(@temp_dir) + end Rake::Task["gwss:ingest_pq_etds"].invoke("#{Rails.root}/spec/fixtures/etd_zips") end it 'generates deposit file structure via gwss:ingest_pq_etds task' do - expect(File.directory?("#{Rails.root}/tmp/test/bulkrax_zip")).to be true - expect(File.directory?("#{Rails.root}/tmp/test/bulkrax_zip/files")).to be true + expect(File.directory?(@temp_dir)).to be true + expect(File.directory?("#{@temp_dir}/files")).to be true - expect(File.directory?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_1")).to be true - expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_1/Ab_gwu_0075A_16593_DATA.xml")).to be true - expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_1/Ab_gwu_0075A_16593.pdf")).to be true + expect(File.directory?("#{@temp_dir}/files/etdadmin_upload_1")).to be true + expect(File.file?("#{@temp_dir}/files/etdadmin_upload_1/Ab_gwu_0075A_16593_DATA.xml")).to be true + expect(File.file?("#{@temp_dir}/files/etdadmin_upload_1/Ab_gwu_0075A_16593.pdf")).to be true - expect(File.directory?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_2")).to be true - expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_2/Ab_gwu_0076A_12345_DATA.xml")).to be true - expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/files/etdadmin_upload_2/Ab_gwu_0076A_12345.pdf")).to be true + expect(File.directory?("#{@temp_dir}/files/etdadmin_upload_2")).to be true + expect(File.file?("#{@temp_dir}/files/etdadmin_upload_2/Ab_gwu_0076A_12345_DATA.xml")).to be true + expect(File.file?("#{@temp_dir}/files/etdadmin_upload_2/Ab_gwu_0076A_12345.pdf")).to be true - expect(File.file?("#{Rails.root}/tmp/test/bulkrax_zip/metadata.csv")).to be true + expect(File.file?("#{@temp_dir}/metadata.csv")).to be true end it 'generates accurate CSV file for import' do - csv_rows = CSV.read("#{Rails.root}/tmp/test/bulkrax_zip/metadata.csv") + csv_rows = CSV.read("#{@temp_dir}/metadata.csv") headers_arr = csv_rows[0] @@ -57,7 +60,7 @@ end it 'can deposit works via bulkrax import' do - admin_user = FactoryBot.create(:admin_user) + admin_user = FactoryBot.create(:admin) etds_admin_set = Hyrax::AdministrativeSet.new(title: ['ETDs']) etds_admin_set = Hyrax.persister.save(resource: etds_admin_set) Hyrax::AdminSetCreateService.call!(admin_set: etds_admin_set, creating_user: admin_user) @@ -74,7 +77,7 @@ import_parser_radio_button_elements.last.click import_parser_file_path_elements = page.all('//*[@id="importer_parser_fields_import_file_path"]') - import_parser_file_path_elements.last.fill_in with: "#{Rails.root}/tmp/test/bulkrax_zip/metadata.csv" + import_parser_file_path_elements.last.fill_in with: "#{@temp_dir}/metadata.csv" click_on("Create and Import") From 399f53a934eb2c81409f5ceaea563e869ad2558b Mon Sep 17 00:00:00 2001 From: Dolsy Smith Date: Thu, 5 Sep 2024 14:46:22 +0000 Subject: [PATCH 41/41] Fixed license & bulkrax tests* --- lib/tasks/ingest_bulkrax_prep.rake | 6 +----- spec/factories/gw_works.rb | 6 +++++- spec/features/bulkrax_upload_spec.rb | 4 ++-- spec/features/license_spec.rb | 13 ++++--------- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/lib/tasks/ingest_bulkrax_prep.rake b/lib/tasks/ingest_bulkrax_prep.rake index d52de8ba..7e1ffe03 100644 --- a/lib/tasks/ingest_bulkrax_prep.rake +++ b/lib/tasks/ingest_bulkrax_prep.rake @@ -144,11 +144,7 @@ namespace :gwss do work_metadata['date_created'] = etd_date_created unless etd_date_created.nil? work_metadata['committee_member'] = get_committee_members(doc).join(';') work_metadata['rights_statement'] = 'http://rightsstatements.org/vocab/InC/1.0/' - # Can't currently load this license because this Bulkrax code - # https://github.com/samvera/bulkrax/blob/v8.1.0/app/models/concerns/bulkrax/import_behavior.rb#L145-L146 - # will try to match it with http://www.europeana.eu/portal/rights/rr-r.html/ (slash-terminated) - # -- not finding a match, Bulkrax will throw an error. - # work_metadata['license'] = 'http://www.europeana.eu/portal/rights/rr-r.html' + work_metadata['license'] = 'All rights reserved' work_metadata end diff --git a/spec/factories/gw_works.rb b/spec/factories/gw_works.rb index 92e5aad0..8127d592 100644 --- a/spec/factories/gw_works.rb +++ b/spec/factories/gw_works.rb @@ -20,7 +20,11 @@ end end - after(:create) do |work, _evaluator| + after(:create) do |work, options| + # Add the user's ability to the work -> necessary for enabling access to Edit pages + actor = Hyrax::CurationConcern.actor + actor_environment = Hyrax::Actors::Environment.new(work, Ability.new(options.user), {}) + actor.create(actor_environment) work.save! if work.try(:member_of_collections) && work.member_of_collections.present? end diff --git a/spec/features/bulkrax_upload_spec.rb b/spec/features/bulkrax_upload_spec.rb index b7e78668..17b48b41 100644 --- a/spec/features/bulkrax_upload_spec.rb +++ b/spec/features/bulkrax_upload_spec.rb @@ -34,10 +34,10 @@ csv_rows = CSV.read("#{@temp_dir}/metadata.csv") headers_arr = csv_rows[0] - + print headers_arr expect(headers_arr).to eq(["model", "title", "creator", "contributor", "language", "description", "keyword", "degree", "resource_type", "advisor", "gw_affiliation", - "date_created", "committee_member", "rights_statement", "bulkrax_identifier", + "date_created", "committee_member", "rights_statement", "license", "bulkrax_identifier", "file", "parents", "visibility", "visibility_during_embargo", "visibility_after_embargo", "embargo_release_date"]) diff --git a/spec/features/license_spec.rb b/spec/features/license_spec.rb index f0599b90..ffbd4968 100644 --- a/spec/features/license_spec.rb +++ b/spec/features/license_spec.rb @@ -5,11 +5,10 @@ let(:solr) { Blacklight.default_index.connection } let(:admin_set) { FactoryBot.create(:admin_set) } - let(:content_admin_user) { FactoryBot.create(:content_admin_user) } + let(:content_admin_user) { FactoryBot.create(:content_admin) } - let(:work_with_license) { FactoryBot.create(:gw_work, + let(:work_with_license) { FactoryBot.create(:public_work, admin_set: admin_set, - visibility: "public", user: content_admin_user) } let(:another_license_value) { Hyrax::QaSelectService.new('licenses').select_active_options.first.first } @@ -35,7 +34,7 @@ expect(page).to have_content(work_with_license.license.first) end end -=begin context 'as a content-admin user' do + context 'as a content-admin user' do before :each do sign_in_user(content_admin_user) end @@ -44,12 +43,8 @@ visit "/concern/gw_works/#{work_with_license.id}/edit" page.select another_license_value, :from => "gw_work_license" page.click_on("Save changes") - end - - it 'can see the changes' do - visit "/concern/gw_works/#{work_with_license.id}" expect(page).to have_content(another_license_value) end end -=end + end