├── VERSION ├── user_documentation ├── user_doc.txt ├── after_copy.png ├── after_load.png ├── examples │ ├── images │ │ ├── readme.txt │ │ ├── load_popup.png │ │ ├── results_do.png │ │ ├── added_first_ss.png │ │ ├── first_ss_copied.png │ │ ├── first_ss_report.png │ │ ├── add_ss_load_report.png │ │ ├── tree_after_add_load.png │ │ ├── add_load_report_in_xls.png │ │ ├── empty_collection_edit.png │ │ ├── empty_collection_view.png │ │ ├── new_load_spreadsheet.png │ │ ├── select_archival_object.png │ │ ├── empty_collection_results.png │ │ ├── empty_test_file_selection.png │ │ └── empty_collection_finished_popup.png │ ├── empty_test_collection.xlsx │ ├── results │ │ ├── first_ss_report.xlsx │ │ ├── add_ss_load_report.xlsx │ │ └── readme.txt │ ├── add_to_hl_test_ingest_collection.xlsx │ └── example.md ├── EmptyResource.png ├── file_selected.png ├── load_report.png ├── copied_into_ss.png ├── load_completion.png ├── load_on_object.png ├── selecting_file.png ├── load_on_resource.png ├── OpenLoadSpreadsheet.png ├── descriptionLevelDropDown.png ├── digital_objects_instructions.md ├── USER_DOCUMENTATION.md └── archival_objects_instructions.md ├── Gemfile ├── templates ├── aspace_import_excel_template.xlsx ├── aspace_import_excel_DO_template.xlsx └── extended_aspace_import_excel_template.xlsx ├── frontend ├── views │ ├── layout_head.html.erb │ └── resources │ │ ├── _bulk_file_form.html.erb │ │ └── _bulk_response.html.erb ├── controllers │ ├── concerns │ │ ├── updates_utils.rb │ │ └── linked_objects.rb │ └── resources_updates_controller.rb ├── routes.rb ├── models │ ├── enum_list.rb │ ├── digital_object_handler.rb │ ├── ingest_report.rb │ ├── handler.rb │ ├── subject_handler.rb │ ├── container_instance_handler.rb │ └── agent_handler.rb ├── plugin_init.rb ├── locales │ └── en.yml └── assets │ ├── tree_extensions.js │ ├── clipboard.js │ └── javascripts │ └── utils.js ├── extras ├── README.md └── modified_initialize-plugin.bat ├── .gitignore ├── README.md └── LICENSE /VERSION: -------------------------------------------------------------------------------- 1 | v3.0.3 2 | -------------------------------------------------------------------------------- /user_documentation/user_doc.txt: -------------------------------------------------------------------------------- 1 | placeholder 2 | -------------------------------------------------------------------------------- /user_documentation/after_copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/after_copy.png -------------------------------------------------------------------------------- /user_documentation/after_load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/after_load.png -------------------------------------------------------------------------------- /user_documentation/examples/images/readme.txt: -------------------------------------------------------------------------------- 1 | This files contains images that are used in the /user_documentation/examples/readme.md 2 | -------------------------------------------------------------------------------- /user_documentation/EmptyResource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/EmptyResource.png -------------------------------------------------------------------------------- /user_documentation/file_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/file_selected.png -------------------------------------------------------------------------------- /user_documentation/load_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/load_report.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | ASpaceGems.setup if defined? ASpaceGems 2 | 3 | source 'http://rubygems.org' 4 | 5 | gem 'rubyXL', "3.3.29", :require => false 6 | 7 | -------------------------------------------------------------------------------- /user_documentation/copied_into_ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/copied_into_ss.png -------------------------------------------------------------------------------- /user_documentation/load_completion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/load_completion.png -------------------------------------------------------------------------------- /user_documentation/load_on_object.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/load_on_object.png -------------------------------------------------------------------------------- /user_documentation/selecting_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/selecting_file.png -------------------------------------------------------------------------------- /user_documentation/load_on_resource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/load_on_resource.png -------------------------------------------------------------------------------- /templates/aspace_import_excel_template.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/templates/aspace_import_excel_template.xlsx -------------------------------------------------------------------------------- /user_documentation/OpenLoadSpreadsheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/OpenLoadSpreadsheet.png -------------------------------------------------------------------------------- /templates/aspace_import_excel_DO_template.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/templates/aspace_import_excel_DO_template.xlsx -------------------------------------------------------------------------------- /user_documentation/descriptionLevelDropDown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/descriptionLevelDropDown.png -------------------------------------------------------------------------------- /user_documentation/examples/images/load_popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/load_popup.png -------------------------------------------------------------------------------- /user_documentation/examples/images/results_do.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/results_do.png -------------------------------------------------------------------------------- /templates/extended_aspace_import_excel_template.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/templates/extended_aspace_import_excel_template.xlsx -------------------------------------------------------------------------------- /user_documentation/examples/images/added_first_ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/added_first_ss.png -------------------------------------------------------------------------------- /user_documentation/examples/empty_test_collection.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/empty_test_collection.xlsx -------------------------------------------------------------------------------- /user_documentation/examples/images/first_ss_copied.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/first_ss_copied.png -------------------------------------------------------------------------------- /user_documentation/examples/images/first_ss_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/first_ss_report.png -------------------------------------------------------------------------------- /user_documentation/examples/results/first_ss_report.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/results/first_ss_report.xlsx -------------------------------------------------------------------------------- /user_documentation/examples/images/add_ss_load_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/add_ss_load_report.png -------------------------------------------------------------------------------- /user_documentation/examples/images/tree_after_add_load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/tree_after_add_load.png -------------------------------------------------------------------------------- /user_documentation/examples/images/add_load_report_in_xls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/add_load_report_in_xls.png -------------------------------------------------------------------------------- /user_documentation/examples/images/empty_collection_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/empty_collection_edit.png -------------------------------------------------------------------------------- /user_documentation/examples/images/empty_collection_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/empty_collection_view.png -------------------------------------------------------------------------------- /user_documentation/examples/images/new_load_spreadsheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/new_load_spreadsheet.png -------------------------------------------------------------------------------- /user_documentation/examples/images/select_archival_object.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/select_archival_object.png -------------------------------------------------------------------------------- /user_documentation/examples/results/add_ss_load_report.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/results/add_ss_load_report.xlsx -------------------------------------------------------------------------------- /user_documentation/examples/results/readme.txt: -------------------------------------------------------------------------------- 1 | This directory contains Excel Spreadsheets that have been created by pasting the "copied to clipboard" results of example spreadsheet loads 2 | -------------------------------------------------------------------------------- /user_documentation/examples/images/empty_collection_results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/empty_collection_results.png -------------------------------------------------------------------------------- /user_documentation/examples/add_to_hl_test_ingest_collection.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/add_to_hl_test_ingest_collection.xlsx -------------------------------------------------------------------------------- /user_documentation/examples/images/empty_test_file_selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/empty_test_file_selection.png -------------------------------------------------------------------------------- /user_documentation/examples/images/empty_collection_finished_popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harvard-library/aspace-import-excel/HEAD/user_documentation/examples/images/empty_collection_finished_popup.png -------------------------------------------------------------------------------- /frontend/views/layout_head.html.erb: -------------------------------------------------------------------------------- 1 | <% if controller.controller_name == 'resources' && controller.action_name == 'edit' %> 2 | <%= javascript_include_tag "#{@base_url}/assets/clipboard.js" %> 3 | <%= javascript_include_tag "#{@base_url}/assets/tree_extensions.js" %> 4 | 5 | <% end %> 6 | -------------------------------------------------------------------------------- /extras/README.md: -------------------------------------------------------------------------------- 1 | # The extras/ Directory 2 | This directory contains files that may need to be copied to another directory upon installation. 3 | 4 | ## modified_initialize-plugin.bat 5 | 6 | This DOS batch file is used to overcome a problem with **\scripts\initialize-plugin.bat**, which is currently downloading into the **\plugins\aspace-import-excel** the latest version of [Bundler](https://bundler.io/) that is incompatible with the version of Bundler that the core ArchivesSpace uses. -------------------------------------------------------------------------------- /extras/modified_initialize-plugin.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | SETLOCAL ENABLEDELAYEDEXPANSION 4 | 5 | cd /d %~dp0..\plugins\%1 6 | 7 | for /d %%a in (..\..\gems\gems\bundler-*) do set bnm=%%a 8 | for /f "tokens=1* delims=-" %%a in ("%bnm%") do set vers=%%b 9 | echo %vers% 10 | 11 | set JRUBY= 12 | FOR /D %%c IN (..\..\gems\gems\jruby-*) DO ( 13 | set JRUBY=!JRUBY!;%%c\lib\* 14 | ) 15 | 16 | set GEM_HOME=gems 17 | java %JAVA_OPTS% -cp "..\..\lib\*!JRUBY!" org.jruby.Main -S gem install bundler -v "%vers%" 18 | java %JAVA_OPTS% -cp "..\..\lib\*!JRUBY!" org.jruby.Main -S ..\..\gems\bin\bundle install --gemfile=Gemfile 19 | -------------------------------------------------------------------------------- /frontend/controllers/concerns/updates_utils.rb: -------------------------------------------------------------------------------- 1 | module UpdatesUtils 2 | extend ActiveSupport::Concern 3 | 4 | # contains methods needed to support validation and other processing of various classes 5 | 6 | 7 | # returns true if the input object validates, otherwise raises an erro 8 | def self.test_exceptions(obj, what = '') 9 | # Pry::ColorPrinter.pp "TESTING #{what}: #{obj.jsonmodel_type}" 10 | ret_val = false 11 | begin 12 | obj._exceptions 13 | true 14 | rescue Exception => e 15 | # Pry::ColorPrinter.pp e.message 16 | # Pry::ColorPrinter.pp ASUtils.jsonmodels_to_hashes(obj) 17 | # Pry::ColorPrinter.pp e.backtrace[1..2] 18 | raise ExcelImportException.new("editable?") if e.message.include?("editable?") 19 | raise e 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /frontend/routes.rb: -------------------------------------------------------------------------------- 1 | ArchivesSpace::Application.routes.draw do 2 | scope AppConfig[:frontend_proxy_prefix] do 3 | match 'resources/:rid/getfile' => 'resources_updates#get_file', :via => [:post] 4 | match 'resources/:rid/getfile' => 'resources_updates#get_file', :via => [:get] 5 | # match 'resources/ssload' => 'resources_updates#load_ss', :via => [:post] 6 | match 'resources/:id/ssload' => 'resources_updates#load_ss', :via => [:post] 7 | match 'resources/:id/ssload' => 'resources_updates#load_ss', :via => [:get] 8 | match 'resources/:id/getdofile' => 'resources_updates#get_do_file', :via => [:get] 9 | match 'resources/:id/getdofile' => 'resources_updates#get_do_file', :via => [:post] 10 | match 'resources/:id/digital_load' => 'resources_updates#load_dos', :via => [:get] 11 | match 'resources/:id/digital_load' => 'resources_updates#load_dos', :via => [:post] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | *.bridgesupport 21 | build-iPhoneOS/ 22 | build-iPhoneSimulator/ 23 | 24 | ## Specific to RubyMotion (use of CocoaPods): 25 | # 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 29 | # 30 | # vendor/Pods/ 31 | 32 | ## Documentation cache and generated files: 33 | /.yardoc/ 34 | /_yardoc/ 35 | /doc/ 36 | /rdoc/ 37 | 38 | ## Environment normalization: 39 | /.bundle/ 40 | /vendor/bundle 41 | /lib/bundler/man/ 42 | 43 | # for a library or gem, you might want to ignore these files since the code is 44 | # intended to run in multiple environments; otherwise, check them in: 45 | # Gemfile.lock 46 | # .ruby-version 47 | # .ruby-gemset 48 | 49 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 50 | .rvmrc 51 | -------------------------------------------------------------------------------- /frontend/models/enum_list.rb: -------------------------------------------------------------------------------- 1 | class EnumList 2 | require 'pp' 3 | @list = [] 4 | @list_hash = {} 5 | @which = '' 6 | 7 | def initialize(which) 8 | @which = which 9 | renew 10 | end 11 | 12 | def value(label) 13 | if @list_hash[label] 14 | v = @list_hash[label] 15 | elsif @list.index(label) 16 | v = label 17 | end 18 | raise Exception.new(I18n.t('plugins.aspace-import-excel.error.enum',:label =>label,:which => @which)) if !v 19 | v 20 | end 21 | 22 | def length 23 | @list.length 24 | end 25 | 26 | def renew 27 | @list = [] 28 | list_hash = {} 29 | enums = JSONModel(:enumeration).all 30 | enums_list = ASUtils.jsonmodels_to_hashes(enums) 31 | enums_list.each do |enum| 32 | if enum['name'] == @which 33 | enum['values'].each do |v| 34 | if v 35 | trans = I18n.t("enumerations.#{@which}.#{v}", default: v) 36 | if !list_hash[trans] 37 | list_hash[trans] = v 38 | @list.push v 39 | else 40 | Rails.logger.warn(I18n.t('plugins.aspace-import-excel.warn.dup', :which => @which, :trans => trans, :used => list_hash[trans])) 41 | end 42 | end 43 | end 44 | break 45 | end 46 | end 47 | @list_hash = list_hash 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /frontend/plugin_init.rb: -------------------------------------------------------------------------------- 1 | # set this to true if you don't want to allow the digital object loading functionality 2 | AppConfig[:hide_do_load] = false 3 | 4 | # handle the spreadsheet load 5 | my_routes = File.join(File.dirname(__FILE__), "routes.rb") 6 | # ArchivesSpace::Application.config.paths['config/routes'].concat(my_routes) 7 | if ArchivesSpace::Application.respond_to?(:extend_aspace_routes) 8 | ArchivesSpace::Application.extend_aspace_routes(my_routes) 9 | else 10 | ArchivesSpace::Application.config.paths['config/routes'].concat([my_routes]) 11 | end 12 | # create a special exception for this import 13 | 14 | class ExcelImportException < Exception 15 | end 16 | 17 | # create a "stop everything" exception 18 | 19 | class StopExcelImportException < Exception 20 | end 21 | 22 | # override the editable? method so errors end up rescued as ValidationExceptions 23 | Rails.application.config.after_initialize do 24 | class ClientEnumSource 25 | def editable?(name) 26 | begin 27 | MemoryLeak::Resources.get(:enumerations).fetch(name).editable? 28 | rescue Exception => e 29 | Rails.logger.error("Blowup for #{name}! #{e.message}") 30 | end 31 | end 32 | end 33 | end 34 | 35 | 36 | # Work around small difference in rubyzip API (from https://github.com/hudmol/nla_staff_spreadsheet_importer/blob/2a28e6379a6748877ab433735153bba96be09b12/backend/plugin_init.rb) 37 | module Zip 38 | if !defined?(Error) 39 | class Error < StandardError 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /frontend/controllers/concerns/linked_objects.rb: -------------------------------------------------------------------------------- 1 | module LinkedObjects 2 | extend ActiveSupport::Concern 3 | 4 | 5 | # This module originally incorporated all the classes needed to handle objects that must be linked to 6 | # Archival Objects, such as Subjects, Top Containers, etc. These classes have be refactored out, and 7 | # can be found in aspace-import-excel/frontend/models 8 | # a lot of this is adapted from Hudson Mlonglo's Arrearage plugin: 9 | # https://github.com/hudmol/nla_staff_spreadsheet_importer/blob/master/backend/converters/arrearage_converter.rb 10 | 11 | # ParentTracker, used to keep track of hierarchy, remains in this module 12 | 13 | 14 | #shamelessly stolen (and adapted from HM's nla_staff_spreadsheet plugin :-) 15 | class ParentTracker 16 | require 'pp' 17 | def set_uri(hier, uri) 18 | @current_hierarchy ||= {} 19 | @current_hierarchy = Hash[@current_hierarchy.map {|k, v| 20 | if k < hier 21 | [k, v] 22 | end 23 | }.compact] 24 | 25 | # Record the URI of the current record 26 | @current_hierarchy[hier] = uri 27 | end 28 | def parent_for(hier) 29 | # Level 1 parent may be a resource record and therefore nil, 30 | if hier > 0 31 | parent_level = hier - 1 32 | @current_hierarchy.fetch(parent_level) 33 | else 34 | nil 35 | end 36 | end 37 | end #of ParentTracker 38 | 39 | end 40 | -------------------------------------------------------------------------------- /frontend/models/digital_object_handler.rb: -------------------------------------------------------------------------------- 1 | class DigitalObjectHandler < Handler 2 | @@digital_object_types ||= EnumList.new('digital_object_digital_object_type') 3 | 4 | def self.create(row, archival_object, report) 5 | dig_o = nil 6 | dig_instance = nil 7 | thumb = row['thumbnail'] || row['Thumbnail'] 8 | unless !thumb && !row['digital_object_link'] 9 | files = [] 10 | if !row['digital_object_link'].blank? && row['digital_object_link'].start_with?('http') 11 | fv = JSONModel(:file_version).new._always_valid! 12 | fv.file_uri = row['digital_object_link'] 13 | fv.publish = row['publish'] 14 | fv.xlink_actuate_attribute = 'onRequest' 15 | fv.xlink_show_attribute = 'new' 16 | files.push fv 17 | end 18 | if !thumb.blank? && thumb.start_with?('http') 19 | fv = JSONModel(:file_version).new._always_valid! 20 | fv.file_uri = thumb 21 | fv.publish = row['publish'] 22 | fv.xlink_actuate_attribute = 'onLoad' 23 | fv.xlink_show_attribute = 'embed' 24 | fv.is_representative = true 25 | files.push fv 26 | end 27 | osn = row['digital_object_id'].blank? ? (archival_object.ref_id + 'd') : row['digital_object_id'] 28 | dig_o = JSONModel(:digital_object).new._always_valid! 29 | dig_o.title = row['digital_object_title'].blank? ? archival_object.display_string : row['digital_object_title'] 30 | dig_o.digital_object_id = osn 31 | dig_o.file_versions = files 32 | dig_o.publish = row['publish'] 33 | begin 34 | dig_o.save 35 | rescue ValidationException => ve 36 | report.add_errors(I18n.t('plugins.aspace-import-excel.error.dig_validation', :err => ve.errors)) 37 | return nil 38 | rescue Exception => e 39 | raise e 40 | end 41 | report.add_info(I18n.t('plugins.aspace-import-excel.created', :what =>I18n.t('plugins.aspace-import-excel.dig'), :id => "'#{dig_o.title}' #{dig_o.uri} [#{dig_o.digital_object_id}]")) 42 | dig_instance = JSONModel(:instance).new._always_valid! 43 | dig_instance.instance_type = 'digital_object' 44 | dig_instance.digital_object = {"ref" => dig_o.uri} 45 | end 46 | dig_instance 47 | end 48 | 49 | def self.renew 50 | clear(@@digital_object_types) 51 | end 52 | end # DigitalObjectHandler 53 | 54 | -------------------------------------------------------------------------------- /frontend/views/resources/_bulk_file_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | <%= form_tag url_for("#{AppConfig[:frontend_proxy_prefix]}resources/ssload"), :method => "post", multipart: true, :id => 'bulk_ingest_form' do %> 5 | 6 | 35 | 36 | 41 | <% end %> 42 | 43 |
44 |
45 | -------------------------------------------------------------------------------- /frontend/models/ingest_report.rb: -------------------------------------------------------------------------------- 1 | # In order to have robust reporting, I'm separating out the report 2 | 3 | class IngestReport 4 | require 'pp' 5 | 6 | def initialize 7 | @rows = [] 8 | @current_row = nil 9 | @terminal_error = '' 10 | @file_name = nil 11 | @error_rows = 0 12 | end 13 | 14 | def add_errors(errors) 15 | @error_rows += 1 if @current_row.errors.blank? 16 | @current_row.add_errors(errors) 17 | end 18 | 19 | def add_info(info) 20 | @current_row.add_info(info) 21 | end 22 | 23 | def add_archival_object(ao) 24 | @current_row.archival_object(ao)if ao 25 | end 26 | 27 | # If we stop processing before getting to the end of the spreadsheet, we want that reported out special 28 | def add_terminal_error(error, counter) 29 | if counter 30 | @terminal_error = I18n.t('plugins.aspace-import-excel.error.stopped', :row => counter, :msg => error) 31 | else 32 | @terminal_error = I18n.t('plugins.aspace-import-excel.error.initialize', :msg => error) 33 | end 34 | end 35 | 36 | def row_count 37 | @rows.length 38 | end 39 | 40 | def end_row 41 | @rows.push @current_row if @current_row 42 | @current_row = nil 43 | end 44 | 45 | def file_name 46 | @file_name 47 | end 48 | 49 | def new_row(row_number) 50 | @rows.push @current_row if @current_row 51 | @current_row = Row.new(row_number) 52 | end 53 | 54 | 55 | def set_file_name(file_name) 56 | @file_name = file_name || I18n.t('plugins.aspace-import-excel.error.file_name') 57 | end 58 | 59 | 60 | def rows 61 | @rows 62 | end 63 | 64 | def terminal_error 65 | @terminal_error 66 | end 67 | 68 | Row = Struct.new(:archival_object_id,:archival_object_display,:ref_id, :row, :errors, :info) do 69 | 70 | def initialize(row_number) 71 | self.row = I18n.t('plugins.aspace-import-excel.row', :row => row_number) 72 | self.errors = [] 73 | self.info = [] 74 | self.archival_object_id = nil 75 | self.archival_object_display = nil 76 | self.ref_id = nil 77 | end 78 | 79 | # if other structures (top_container, agent, etc.) were created along the way 80 | def add_info(info) 81 | self.info.push info 82 | end 83 | 84 | def add_errors(errors) 85 | if errors.is_a? Array 86 | self.errors.concat(errors) 87 | else 88 | self.errors.push errors 89 | end 90 | end 91 | 92 | def archival_object(ao) 93 | self.archival_object_id = ao.uri 94 | self.archival_object_display = ao.display_string 95 | self.ref_id = ao.ref_id 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /frontend/views/resources/_bulk_response.html.erb: -------------------------------------------------------------------------------- 1 | <% unless report.terminal_error.blank? %> 2 |
<%= report.terminal_error %>
3 | <% end %> 4 | <% if report.row_count > 0 %> 5 | 34 | <% end %> <%# of rows %> 35 | 36 | 64 | 65 | -------------------------------------------------------------------------------- /frontend/models/handler.rb: -------------------------------------------------------------------------------- 1 | # this is the base class for handling objects that must be linked to 2 | # Archival Objects, such as Subjects, Top Containers, etc. 3 | 4 | # a lot of this is adapted from Hudson Mlonglo's Arrearage plugin: 5 | #https://github.com/hudmol/nla_staff_spreadsheet_importer/blob/master/backend/converters/arrearage_converter.rb 6 | 7 | # One of the main differences is that we do lookups against the database for objects (such as agent, subject) that 8 | # might already be in the database 9 | 10 | class Handler 11 | require 'enum_list' 12 | require 'pp' 13 | 14 | DISAMB_STR = ' DISAMBIGUATE ME!' 15 | 16 | # centralize the checking for an already-found object 17 | def self.stored(hash, id, key) 18 | ret_obj = hash.fetch(id, nil) || hash.fetch(key, nil) 19 | end 20 | 21 | 22 | # returns nil, a hash of a jason model (if 1 found), or throws a multiples found error 23 | # if repo_id is nil, do a global search (subject and agent) 24 | # this is using archivesspace/frontend/app/models/search.rb 25 | def self.search(repo_id,params,jmsym, type = '', match = '') 26 | obj = nil 27 | search = nil 28 | matches = match.split(':') 29 | if repo_id 30 | search = Search.all(repo_id, params) 31 | else 32 | begin 33 | search = Search.global(params,type) 34 | rescue Exception => e 35 | s = JSONModel::HTTP::get_json("/search/#{type}", params) 36 | raise e if !e.message.match('

Not Found

') # global search doesn't handle this gracefully :-( 37 | search = {'total_hits' => 0} 38 | end 39 | end 40 | total_hits = search['total_hits'] || 0 41 | if total_hits == 1 && !search['results'].blank? # for some reason, you get a hit of '1' but still have empty results?? 42 | obj = JSONModel(jmsym).find_by_uri(search['results'][0]['id']) 43 | elsif total_hits > 1 44 | if matches.length == 2 45 | match_ct = 0 46 | disam = matches[1] + DISAMB_STR 47 | disam_obj = nil 48 | search['results'].each do |result| 49 | # if we have a disambiguate result get it 50 | if result[matches[0]] == disam 51 | disam_obj = JSONModel(jmsym).find_by_uri(result['id']) 52 | elsif result[matches[0]] == matches[1] 53 | match_ct += 1 54 | obj = JSONModel(jmsym).find_by_uri(result['id']) 55 | end 56 | end 57 | # if we have more than one exact match, then return disam_obj if we have one, or bail! 58 | if match_ct > 1 59 | return disam_obj if disam_obj 60 | raise Exception.new(I18n.t('plugins.aspace-import-excel.error.too_many')) 61 | end 62 | else 63 | raise Exception.new(I18n.t('plugins.aspace-import-excel.error.too_many')) 64 | end 65 | elsif total_hits == 0 66 | # Rails.logger.info("No hits found") 67 | end 68 | obj 69 | end 70 | 71 | def self.clear(enum_list) 72 | enum_list.renew 73 | end 74 | 75 | 76 | end 77 | -------------------------------------------------------------------------------- /user_documentation/digital_objects_instructions.md: -------------------------------------------------------------------------------- 1 | # Add Digital Objects to Archival Objects 2 | 3 | This functionality supports the creation of Digital Objects, associating them with already-existing Archival Objects. 4 | 5 | ## Constraints 6 | With this plugin: 7 | + Only one Digital Object can be associated with a single Archival Object 8 | + The Digital Object can have up to two files associated with it: 9 | + a File with an *Xlink Actuate Attribute* of **onLoad** and an *Xlink Show Attribute* of **embed** 10 | + a File with an Xlink Actuate Attribute of **onRequest** and an *Xlink Show Attribute* of **new** 11 | + If the Archival Object *already* has a Digital Object associated with it, that row is skipped. 12 | 13 | ## Using the Template to Create a Spreadsheet 14 | 15 | The Excel Spreadsheet template is at https://github.com/harvard-library/aspace-import-excel/blob/master/templates/aspace_import_excel_DO_template.xlsx . 16 | 17 | Use **Save as** *(your new filename}*.xlsx to begin creating your spreadsheet. 18 | 19 | 20 | The template is designed to be flexible enough to accommodate different workflows. The first row is the place where you can put identifying information, such as "Foo Collection". 21 | 22 | As long as you **don't edit** the **row** marked *"ArchivesSpace field code"*, you may hide, delete, or rearrange **columns** to suit your workflow. Indeed, you will see that there are a few already-hidden columns; these are not currently used, but may be used in future enhancements. 23 | 24 | **Note** that the **Publish Digital Record** column already has in-column drop down data validation defined. 25 | 26 | ### Required Columns 27 | 28 | The following columns __must__ be filled in: 29 | 30 | * EAD ID -- of the Resource to which you're adding Digital Objects.This will be used to confirm that you are trying to add your spreadsheet information to the correct resource. 31 | * REF ID -- of the Archival Object that you want to associate the new Digital Object with 32 | 33 | ## Column Definitions 34 | 35 | Below is a discussion of each used column in the spreadsheet. 36 | 37 | Column | Value | Default | Comment 38 | -------|-------|---------|--------- 39 | EAD ID | String || **REQUIRED** 40 | REF ID | String || **REQUIRED** 41 | Digital Object ID | String|| Leave blank to get a automatically-assigned Digital Object ID based on the REF ID. You can override this, but make sure it's unique 42 | Digital Object Title | String|| If blank, the Archival Object's Title will be assigned 43 | Publish Digital Object Record|TRUE or FALSE|FALSE|This value will be inherited by each File, as well as the Digital Object 44 | File URL of Linked-to digital object|URL String||This will be assigned an Xlink Actuate Attribute of **onRequest** and an *Xlink Show Attribute* of **new** 45 | File URL of Thumbnail|URL String||This will be assigned an *Xlink Actuate Attribute* of **onLoad** and an *Xlink Show Attribute* of **embed**; the "is representative" flag is set to TRUE 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /frontend/models/subject_handler.rb: -------------------------------------------------------------------------------- 1 | class SubjectHandler < Handler 2 | @@subjects = {} # will track both confirmed ids, and newly created ones. 3 | @@subject_term_types ||= EnumList.new('subject_term_type') 4 | @@subject_sources ||= EnumList.new('subject_source') 5 | 6 | def self.renew 7 | clear(@@subject_term_types) 8 | clear(@@subject_sources) 9 | @@subjects = {} 10 | end 11 | 12 | def self.key_for(subject) 13 | key = "#{subject[:term]} #{subject[:source]}: #{subject[:type]}" 14 | key 15 | end 16 | def self.build(row, num) 17 | id = row.fetch("subject_#{num}_record_id", nil) 18 | input_term = row.fetch("subject_#{num}_term", nil) 19 | { 20 | :id => id, 21 | :term => input_term || (id ? I18n.t('plugins.aspace-import-excel.unfound_id', :id => id, :type => 'subject') : nil), 22 | :type => @@subject_term_types.value(row.fetch("subject_#{num}_type") || 'topical'), 23 | :source => @@subject_sources.value( row.fetch("subject_#{num}_source") || 'ingest'), 24 | :id_but_no_term => id && !input_term 25 | } 26 | end 27 | 28 | def self.get_or_create(row, num, repo_id, report) 29 | subject = build(row, num) 30 | subject_key = key_for(subject) 31 | if !(subj = stored(@@subjects, subject[:id], subject_key)) 32 | unless subject[:id].blank? 33 | begin 34 | subj = JSONModel(:subject).find( subject[:id]) 35 | rescue Exception => e 36 | if e.message != 'RecordNotFound' 37 | raise ExcelImportException.new( I18n.t('plugins.aspace-import-excel.error.no_subject',:num => num, :why => e.message)) 38 | end 39 | end 40 | end 41 | begin 42 | if !subj 43 | begin 44 | subj = get_db_subj(subject) 45 | rescue Exception => e 46 | if e.message == 'More than one match found in the database' 47 | subject[:term] = subject[:term] + DISAMB_STR 48 | report.add_info(I18n.t('plugins.aspace-import-excel.warn.disam', :name => subject[:term])) 49 | else 50 | raise e 51 | end 52 | end 53 | end 54 | if !subj 55 | subj = create_subj(subject, num) 56 | report.add_info(I18n.t('plugins.aspace-import-excel.created', :what =>"#{I18n.t('plugins.aspace-import-excel.subj')}[#{subject[:term]}]", :id => subj.uri)) 57 | end 58 | rescue Exception => e 59 | Rails.logger.error(e.backtrace) 60 | raise ExcelImportException.new( I18n.t('plugins.aspace-import-excel.error.no_subject',:num => num, :why => e.message)) 61 | end 62 | if subj 63 | if subj[:id_but_no_term] 64 | @@subjects[subject[:id].to_s] = subj 65 | else 66 | @@subjects[subj.id.to_s] = subj 67 | end 68 | @@subjects[subject_key] = subj 69 | end 70 | end 71 | subj 72 | end 73 | 74 | def self.create_subj(subject, num) 75 | begin 76 | term = JSONModel(:term).new._always_valid! 77 | term.term = subject[:term] 78 | term.term_type = subject[:type] 79 | term.vocabulary = '/vocabularies/1' # we're making a gross assumption here 80 | subj = JSONModel(:subject).new._always_valid! 81 | subj.terms.push term 82 | subj.source = subject[:source] 83 | subj.vocabulary = '/vocabularies/1' # we're making a gross assumption here 84 | subj.save 85 | rescue Exception => e 86 | raise ExcelImportException.new(I18n.t('plugins.aspace-import-excel.error.no_subject',:num => num, :why => e.message)) 87 | end 88 | subj 89 | end 90 | 91 | def self.get_db_subj(subject) 92 | s_params = {} 93 | s_params["q"] = "title:\"#{subject[:term]}\" AND first_term_type:#{subject[:type]}" 94 | ret_subj = search(nil, s_params, :subject, 'subjects',"title:#{subject[:term]}" ) 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /user_documentation/USER_DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # Using This Plugin to Ingest Spreadsheets 2 | 3 | As of version V2.1.0, the *aspace-import-excel* plugin supports both the **Import Archival Objects** (original) spreadsheet ingest and the new **Add Digital Objects to Archival Objects** ingest method. 4 | 5 | ## Common Workflow: 6 | 7 | 1. Make sure the plug-in has been installed! See the [Installation instructions](../README.md#installation) in the main README document. 8 | 1. Download the appropriate Excel Spreadsheet template. 9 | + For **Import Archival Objects**, use [aspace_import_excel_template.xlsx](../templates/aspace_import_excel_template.xlsx) or _(new for v3.0)_ [extended_aspace_import_excel_template.xlsx](../templates/extended_aspace_import_excel_template.xlsx) 10 | + For **Add Digital Objects to Archival Objects**, use [aspace_import_excel_DO_template.xlsx](https://github.com/harvard-library/aspace-import-excel/blob/master/templates/aspace_import_excel_DO_template.xlsx) 11 | It's recommended that you make a copy of this template, renaming it to something identifiable, e.g.: ead_foo234.xslx) 12 | 13 | 1. Identify your Resource EAD ID. For importing Archival Objects, this may represent a Resource with no Archival Objects. Obviously, if you're adding Digital Objects to Archival Objects, the Resource must have the corresponding Archival Objects! 14 | 1. Fill in your spreadsheet. Use either the [Instructions for Importing Archival Objects](archival_objects_instructions.md) or [Instructions for Adding Digital Objects to Archival Objects](digital_objects_instructions.md), as appropriate 15 | 1. Use the plugin to ingest the spreadsheet. 16 | 17 | **Note** *The Resource must already be defined, with an EAD ID, in ArchivesSpace before initiating the ingest* 18 | 19 | ## Initiating the ingest 20 | 1. In ArchivesSpace, where you invoke the ingest depends on what you are trying to do. 21 | + Locate the desired Resource record **if**: 22 | + the resource has no Archival Objects; or 23 | + you want to create Archival Objects to be appended to the end of the list of first-level Archival Objects; or 24 | + you want to add Digital Objects to Archival Objects. 25 | 26 | + **Otherwise**, if you want the first Archival Object in your list to be inserted as a sibling/child (see Hierarchical Relationship) of an *already-existing* Archival Object, locate the Archival Object where you want to begin your insertion. You can either search for it or select it from the tree that displays on the Resource record. In the latter case, the page will reload to that Archival Object. 27 | 28 | 2. When you have displayed the Resource or the Archival Object as appropriate, make sure you are in *edit* mode. A "Load via Spreadsheet" button will appear. Finding the Load via Spreadsheet button on an empty resource 29 | 30 | 3. Click on the button. You will see a Load Spreadsheet modal window, with the rest of the page "greyed out". 31 | If you are at the Resource level, the modal window will look like this: modal window with two ingest choices 32 | If you are at the Archival Object level, the modal window will look like this: modal window with only Add Archival Object 33 | 34 | 4. Click on "Select File" to browse and locate a file on your system. Select the Excel File. 35 | 5. Click on either the **Import Archival Objects** or **Add Digital Objects to Archival Objects** button, if you are given the choice. 36 | 5. Click on **"Import from SpreadSheet"**. The Ingester will start; the rest of the page will continue to be "greyed out". 37 | 6. When the ingest is finished, there will be an alert pop-up. 38 | 7. Click to close the popup, and you will be presented with a report of the processing. 39 | 8. You can click on "Copy to clipboard" to get a tabbed version of the report to examine and/or save. 40 | 41 | *back to Workflow* 42 | -------------------------------------------------------------------------------- /frontend/models/container_instance_handler.rb: -------------------------------------------------------------------------------- 1 | # Supporting multiple containers in the row 2 | 3 | class ContainerInstanceHandler < Handler 4 | @@top_containers = {} 5 | @@container_types ||= EnumList.new('container_type') 6 | @@instance_types ||= EnumList.new('instance_instance_type') # for when we move instances over here 7 | 8 | def self.renew 9 | clear( @@container_types) 10 | clear(@@instance_types) 11 | end 12 | 13 | def self.key_for(top_container, resource) 14 | key = "'#{resource}' #{top_container[:type]}: #{top_container[:indicator]}" 15 | key += " #{top_container[:barcode]}" if top_container[:barcode] 16 | key 17 | end 18 | 19 | def self.build(row,substr) 20 | { 21 | :type => @@container_types.value(row.fetch("type_1#{substr}", 'Box') || 'Box'), 22 | :indicator => row.fetch("indicator_1#{substr}", 'Unknown') || 'Unknown', 23 | :barcode => row.fetch("barcode#{substr}",nil) 24 | } 25 | end 26 | 27 | # returns a top container JSONModel 28 | def self.get_or_create(row, substr, resource, report) 29 | begin 30 | top_container = build(row, substr) 31 | tc_key = key_for(top_container, resource) 32 | # check to see if we already have fetched one from the db, or created one. 33 | existing_tc = @@top_containers.fetch(tc_key, false) || get_db_tc(top_container, resource) 34 | if !existing_tc 35 | tc = JSONModel(:top_container).new._always_valid! 36 | tc.type = top_container[:type] 37 | tc.indicator = top_container[:indicator] 38 | tc.barcode = top_container[:barcode] if top_container[:barcode] 39 | tc.repository = {'ref' => resource.split('/')[0..2].join('/')} 40 | # UpdateUtils.test_exceptions(tc,'top_container') 41 | tc.save 42 | report.add_info(I18n.t('plugins.aspace-import-excel.created', :what =>"#{I18n.t('plugins.aspace-import-excel.tc')} [#{tc.type} #{tc.indicator}]", :id=> tc.uri)) 43 | existing_tc = tc 44 | end 45 | rescue Exception => e 46 | report.add_errors(I18n.t('plugins.aspace-import-excel.error.no_tc', :why => e.message + " in linked_objects")) 47 | existing_tc = nil 48 | end 49 | @@top_containers[tc_key] = existing_tc if existing_tc 50 | existing_tc 51 | end 52 | 53 | def self.get_db_tc(top_container, resource_uri) 54 | repo_id = resource_uri.split('/')[2] 55 | if !(ret_tc = get_db_tc_by_barcode(top_container[:barcode], repo_id)) 56 | tc_str = "#{top_container[:type]} #{top_container[:indicator]}" 57 | tc_str += ": [#{top_container[:barcode]}]" if top_container[:barcode] 58 | tc_params = {} 59 | tc_params["type[]"] = 'top_container' 60 | tc_params["q"] = "display_string:\"#{tc_str}\" AND collection_uri_u_sstr:\"#{resource_uri}\"" 61 | ret_tc = search(repo_id,tc_params, :top_container,'', "display_string:#{tc_str}") 62 | end 63 | ret_tc 64 | end 65 | 66 | def self.get_db_tc_by_barcode(barcode, repo_id) 67 | ret_tc = nil 68 | if barcode 69 | tc_params = {} 70 | tc_params["type[]"] = 'top_container' 71 | tc_params["q"] = "barcode_u_sstr:\"#{barcode}\"" 72 | ret_tc = search(repo_id,tc_params, :top_container) 73 | end 74 | ret_tc 75 | end 76 | 77 | def self.create_container_instance(row, substr, resource_uri,report) 78 | instance = nil 79 | raise ExcelImportException.new(I18n.t('plugins.aspace-import-excel.error.missing_instance_type')) if row["cont_instance_type#{substr}"].blank? 80 | begin 81 | tc = get_or_create(row, substr, resource_uri, report) 82 | sc = {'top_container' => {'ref' => tc.uri}, 83 | 'jsonmodeltype' => 'sub_container'} 84 | %w(2 3).each do |num| 85 | if row["type_#{num}#{substr}"] 86 | sc["type_#{num}"] = @@container_types.value(row["type_#{num}#{substr}"]) 87 | sc["indicator_#{num}"] = row["indicator_#{num}#{substr}"] || 'Unknown' 88 | end 89 | end 90 | instance = JSONModel(:instance).new._always_valid! 91 | instance.instance_type = @@instance_types.value(row["cont_instance_type#{substr}"]) 92 | instance.sub_container = JSONModel(:sub_container).from_hash(sc) 93 | rescue ExcelImportException => ee 94 | raise ee 95 | rescue Exception => e 96 | msg = e.message #+ "\n" + e.backtrace()[0] 97 | raise ExcelImportException.new(msg) 98 | end 99 | instance 100 | end 101 | 102 | end # of container handler 103 | -------------------------------------------------------------------------------- /user_documentation/examples/example.md: -------------------------------------------------------------------------------- 1 | # An Example of Using aspace-import-excel 2 | 3 | Included in this directory are two spreadsheets which you can use to follow the step-by-step description below, where you create an empty Resource, populate it with the first spreadsheet, then add to it with the second. 4 | 5 | ## Create a new Resource 6 | 7 | Create a new Resource of type **Collection**. You make it as minimal as you like, but you must assign an EAD ID of **hl_test_ingest**. Don't create any archival objects for it. 8 | the empty resource 9 | 10 | ## Loading the First Spreadsheet 11 | 12 | the empty resource in edit mode 13 | 14 | 15 | ### Select the Spreadsheet 16 | 17 | With your new Resource in *edit* mode, click on the "Load via Spreadsheet" button. 18 | 19 | the 'Load Spreadsheet Popup' 20 | 21 | 22 | Click on the **Add File** button, and select the **empty_test_collection.xlsx** file, that you've downloade from here . This spreadsheet creates two top level "Series" Archival Objects; the second Archival Object will also have a child "Item" object. There are a few errors in the spreadsheet, so that you can see the error reporting mechanism. 23 | 24 | This spreadsheet has specified an Agent by the Agent ID of 3760. If you don't have an Agent with that ID, an agent will be created, with the header of "PLACEHOLDER FOR person agent ID 3760 NOT FOUND", and reported as such in the results. 25 | It also specifies a subject with the ID of 837, but also specifies the term, type, and source. If you don't have a subject with that ID, a new subject will be created based on that information; again, it will be reported in the results. 26 | 27 | 28 | 29 | Here's what it looks like from an MS Windows view: 30 | 31 | Selecting the first spreadsheet 32 | 33 | ### Click "Import From Spreadsheet" 34 | 35 | The importer will "gray out" that button, and begin processing. When it is completed, you will see a confirmation pop-up: 36 | the confirmation popup 37 | 38 | Click "OK", and you will be presented with the report of the results: 39 | results of the first load 40 | 41 | ### Copying the results 42 | 43 | If you click on the "Copy to Clipboard" button, you will get a "Copied" confirmation popup. You will now have 44 | get a tabbed copy of the results in your clipboard, which you can then paste into a text file, Word document, Excel spreadsheet, etc. We've pasted it into an Excel spreadsheet, which we've also uploaded to GitHub: 45 | image of spreadsheet paste 46 | 47 | ### Addressing errors 48 | 49 | Reading the results, you will see that the processing of each Archival Object did not go smoothly. For example, for the object **"The Early Years,1990 - 1995"**, a Container instance was not created because there was a problem with the container *child type*. 50 | 51 | You can interactively create a Container instance for that object. We suggest you use the Top Container ("Box 1") that was created, because it's referenced in the next spreadsheet. Otherwise, a second free-standing "Box 1" Top Container will be created when the second spreadsheet is run. 52 | 53 | Similarly, you can edit the other two objects, if you like. 54 | 55 | 56 | ## Adding Children and Siblings to the new Resource 57 | 58 | ### Select Your Upload Point 59 | 60 | With your Resource in **edit** mode, select the "**The Early Years, 1990 - 1995**" archival object. 61 | 62 | resource with archival object selected 63 | 64 | ### Load Spreadsheet 65 | 66 | As above, click on "Load via Spreadsheet", add the **add_to_hl_test_ingest_collection.xlsx** file that you've downloaded from here, then click on "Import from SpreadSheet". 67 | 68 | This spreadsheet also specifies Agent ID 3760. If you already had an agent with the ID, that is what will be assigned to it; otherwise, the agent created by the ingest of the previous spreadsheet will be used. Similarly, the subject ID 837 is referenced as well, and treated the same. 69 | 70 | ### Results 71 | 72 | These are the expected results: 73 | results of second import 74 | 75 | If you had not edited the "**The Early Years, 1990 - 1995**" archival object to add the Container instance, as described above, you will also see a "Top Container [box 1] created..." message. 76 | 77 | These results also were copied and pasted into an Excel spreadsheet: 78 | 79 | snapshot of second load results 80 | 81 | We have also uploaded the actual spreadsheet here. 82 | 83 | And here's a view of the Resource's "tree" after the two spreadsheets have been loaded: 84 | snapshot of the Resource's 'tree' 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /frontend/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | plugins: 3 | aspace-import-excel: 4 | import: Import from SpreadSheet 5 | add_file: Select File 6 | drag_drop: Drag and drop file here 7 | clip_btn: Copy to Clipboard 8 | add_archival_objects: Import Archival Objects 9 | add_digital_objects: Add Digital Objects to Archival Objects 10 | row: "Row %{row}" 11 | processing_row: "Processing row %{row}" 12 | dig_assoc: Digital Object added to Archival Object 13 | row_error: "Row %{row} will not be processed due to errors: %{errs}" 14 | no_ao: No Archival Object created 15 | created: "%{what} created: %{id}" 16 | updated: "%{what}: %{id}" 17 | clip_created: "\t%{what} created: \t%{nm}\t%{id}\t%{ref_id}" 18 | clip_what: "\t%{what} : \t%{nm}\t%{id}\t%{ref_id}" 19 | clip_info: "\t\t\t\t\t%{what}\n" 20 | clip_err: "\t\t\t\t\tERROR: %{err}\n" 21 | clip_header: "Row\tStatus\tTitle\tURI\tRef ID\tInfo" 22 | ao: Archival Object 23 | tc: Top Container 24 | subj: Subject 25 | agent: Agent 26 | dig: Digital Object 27 | unfound_id: "PLACEHOLDER FOR %{type} ID %{id} NOT FOUND" 28 | ref_id_notfound: "Ref Id %{refid} not found" 29 | warn: 30 | dup: "Managed Controlled Value List %{which} has multiple instances for the Translation '%{trans}'. '%{used}' will be used as the value." 31 | disam: "Multiple match(es) found. Creating %{name} for disabiguation." 32 | single_date_end: "Single date %{date_str} has end date that will be ignored." 33 | error: 34 | date_type: "Date type [%{what}] invalid for %{date_str}. Defaulting to 'inclusive'" 35 | date_label: "Date label [%{what}] invalid for %{date_str}. The date will not be processed." 36 | certainty: "Invalid 'date certainty' ignored for %{date_str}: (%{what})" 37 | below_bad_ao: Cannot process because it's a child of the bad archival object 38 | enum: "NOT FOUND: '%{label}' not found in list %{which}" 39 | invalid_date: "Invalid date definition (%{what}) for %{date_str}. The date will not be processed." 40 | invalid_date_label: "Invalid date label definition in first date (%{what})" 41 | too_many: More than one match found in the database 42 | type_undef: Unable to determine type 43 | file_name: File name cannot be determined 44 | system: "Some system error has occurred [%{msg}]." 45 | initialize: "Processing is terminated [%{msg}]" 46 | stopped: "Processing stopped at row %{row} [%{msg}]" 47 | duplicates: "This spreadsheet has duplicate Archive Space Field codes: %{codes}" 48 | res_ead: This form's Resource is missing an EAD ID 49 | row_ead: This row is missing an EAD ID 50 | ead_mismatch: "Form's EAD ID [%{res_ead}] does not match row's EAD ID [%{row_ead}]" 51 | title: Missing title 52 | title_and_date: Missing Title AND Valid Date definition 53 | hier_miss: Missing hierachy -- must be a number greater than 0 54 | hier_zero: Hierarchy must be greater than 0 55 | hier_wrong: Hierarchy cannot not be more than one level deeper than the previous row 56 | hier_wrong_resource: did you mean to start processing with an archival object selected? 57 | hier_below_error_level: The parent archival object was not created 58 | level: Missing valid Description level 59 | date: "Date must have at least one of: Date begin; Date end; or Date expression" 60 | number: Missing Extent number 61 | extent_type: Missing Extent type 62 | extent_validation: "Unable to validate extent (%{ext}): %{msg}" 63 | no_header: No header (field codes) row found; are you using the correct template? 64 | no_data: No processible data rows found! 65 | excel: "Error(s) parsing Excel File %{errs}" 66 | no_agent: "Unable to create Agent %{num}: [%{why}]" 67 | no_tc: "Unable to create Top Container %{num}: [%{why}]" 68 | missing_instance_type: Missing container instance type 69 | no_container_instance: "Unable to create Container Instance: [%{why}]" 70 | no_subject: "Unable to create Subject %{num}: [%{why}]" 71 | no_move: "Unable to move the archival objects from the end of the list (response code %{code})" 72 | bad_note: "%{type} note is not wellformed: %{msg}" 73 | bad_relator: "Unable to create agent link: '%{label}' is not a valid relator" 74 | relator_invalid: "Unable to create agent link due to problem with relator '%{label}': %{why}" 75 | bad_role: "Unable to create agent link: '%{label}' is not a valid role" 76 | role_invalid: "Unable to create agent link due to problem with role '%{label}': %{why}" 77 | 78 | has_dig_obj: "Archival object already has an associated digital object" 79 | dig_unassoc: "Unable to save archival object with associated digital object: %{msg}" 80 | ref_id_miss: No Ref Id specified 81 | dig_info_miss: Neither the Digital Object URN or the Thumbnail URN is specified 82 | dig_validation: "Cannot create the Digital Object %{err}" 83 | initial_save_error: "Problem with initial save of %{title} -- %{msg}" 84 | second_save_error: "Error on attempt to re-save archival object with 'instances' %{title} position: %{pos}.This means that the archival object has been created, but possibly not linked to its associated instances (digital object, top container, subject, etc.) [%{what}]" 85 | ao_validation: "Validation error when attempting to save Archival Object: %{err}" 86 | -------------------------------------------------------------------------------- /frontend/models/agent_handler.rb: -------------------------------------------------------------------------------- 1 | class AgentHandler < Handler 2 | @@agents = {} 3 | @@agent_role ||= EnumList.new('linked_agent_role') 4 | @@agent_relators ||= EnumList.new('linked_agent_archival_record_relators') 5 | AGENT_TYPES = { 'families' => 'family', 'corporate_entities' => 'corporate_entity', 'people' => 'person'} 6 | def self.renew 7 | clear(@@agent_relators) 8 | clear(@@agent_role) 9 | @@agents = {} 10 | end 11 | def self.key_for(agent) 12 | key = "#{agent[:type]} #{agent[:name]}" 13 | key 14 | end 15 | 16 | def self.build(row, type, num) 17 | id = row.fetch("#{type}_agent_record_id_#{num}", nil) 18 | input_name = row.fetch("#{type}_agent_header_#{num}",nil) 19 | role = row.fetch("#{type}_agent_role_#{num}", nil) 20 | role ='creator' if role.blank? 21 | { 22 | :type => AGENT_TYPES[type], 23 | :id => id, 24 | :name => input_name || (id ? I18n.t('plugins.aspace-import-excel.unfound_id', :id => id, :type => 'Agent') : nil), 25 | :role => role, 26 | :relator => row.fetch("#{type}_agent_relator_#{num}", nil) , 27 | :id_but_no_name => id && !input_name 28 | } 29 | end 30 | 31 | def self.get_or_create(row, type, num, resource_uri, report) 32 | agent = build(row, type, num) 33 | agent_key = key_for(agent) 34 | if !(agent_obj = stored(@@agents, agent[:id], agent_key)) 35 | unless agent[:id].blank? 36 | begin 37 | agent_obj = JSONModel("agent_#{agent[:type]}".to_sym).find(agent[:id]) 38 | rescue Exception => e 39 | if e.message != 'RecordNotFound' 40 | raise ExcelImportException.new( I18n.t('plugins.aspace-import-excel.error.no_agent', :num => num, :why => e.message)) 41 | end 42 | end 43 | end 44 | begin 45 | if !agent_obj 46 | begin 47 | agent_obj = get_db_agent(agent, resource_uri, num) 48 | rescue Exception => e 49 | if e.message == 'More than one match found in the database' 50 | agent[:name] = agent[:name] + DISAMB_STR 51 | report.add_info(I18n.t('plugins.aspace-import-excel.warn.disam', :name => agent[:name])) 52 | else 53 | raise e 54 | end 55 | end 56 | end 57 | if !agent_obj 58 | agent_obj = create_agent(agent, num) 59 | report.add_info(I18n.t('plugins.aspace-import-excel.created', :what =>"#{I18n.t('plugins.aspace-import-excel.agent')}[#{agent[:name]}]", :id => agent_obj.uri)) 60 | end 61 | rescue Exception => e 62 | raise ExcelImportException.new( I18n.t('plugins.aspace-import-excel.error.no_agent', :num => num, :why => e.message)) 63 | end 64 | end 65 | agent_link = nil 66 | if agent_obj 67 | if agent[:id_but_no_name] 68 | @@agents[agent[:id].to_s] = agent_obj 69 | else 70 | @@agents[agent_obj.id.to_s] = agent_obj 71 | end 72 | @@agents[agent_key] = agent_obj 73 | agent_link = {"ref" => agent_obj.uri} 74 | begin 75 | agent_link["role"] = @@agent_role.value(agent[:role]) 76 | rescue Exception => e 77 | if e.message.start_with?("NOT FOUND") 78 | raise ExcelImportException.new(I18n.t('plugins.aspace-import-excel.error.bad_role', :label => agent[:role])) 79 | else 80 | raise ExcelImportException.new(I18n.t('plugins.aspace-import-excel.error.role_invalid', :label => agent[:role], :why => e.message)) 81 | end 82 | end 83 | begin 84 | agent_link["relator"] = @@agent_relators.value(agent[:relator]) if !agent[:relator].blank? 85 | rescue Exception => e 86 | if e.message.start_with?("NOT FOUND") 87 | raise ExcelImportException.new(I18n.t('plugins.aspace-import-excel.error.bad_relator', :label => agent[:relator])) 88 | else 89 | raise ExcelImportException.new(I18n.t('plugins.aspace-import-excel.error.relator_invalid', :label => agent[:relator], :why => e.message)) 90 | end 91 | end 92 | end 93 | agent_link 94 | end 95 | 96 | def self.create_agent(agent, num) 97 | begin 98 | ret_agent = JSONModel("agent_#{agent[:type]}".to_sym).new._always_valid! 99 | ret_agent.names = [name_obj(agent)] 100 | ret_agent.publish = !(agent[:id_but_no_name] || agent[:name].ends_with?(DISAMB_STR)) 101 | ret_agent.save 102 | rescue Exception => e 103 | raise Exception.new(I18n.t('plugins.aspace-import-excel.error.no_agent', :num => num, :why => e.message)) 104 | end 105 | ret_agent 106 | end 107 | 108 | def self.get_db_agent(agent, resource_uri, num) 109 | ret_ag = nil 110 | if agent[:id] 111 | begin 112 | ret_ag = JSONModel("agent_#{agent[:type]}".to_sym).find(agent[:id]) 113 | rescue Exception => e 114 | if e.message != 'RecordNotFound' 115 | raise ExcelImportException.new( I18n.t('plugins.aspace-import-excel.error.no_agent', :num => num, :why => e.message)) 116 | end 117 | end 118 | end 119 | if !ret_ag 120 | a_params = {"q" => "title:\"#{agent[:name]}\" AND primary_type:agent_#{agent[:type]}"} 121 | repo = resource_uri.split('/')[2] 122 | ret_ag = search(repo, a_params, "agent_#{agent[:type]}".to_sym,'', "title:#{agent[:name]}") 123 | end 124 | ret_ag 125 | end 126 | 127 | def self.name_obj(agent) 128 | obj = JSONModel("name_#{agent[:type]}".to_sym).new._always_valid! 129 | obj.source = 'ingest' 130 | obj.authorized = true 131 | obj.is_display_name = true 132 | if agent[:type] == 'family' 133 | obj.family_name = agent[:name] 134 | else 135 | obj.primary_name = agent[:name] 136 | obj.name_order = 'direct' if agent[:type] == 'person' 137 | end 138 | obj 139 | end 140 | end # agent 141 | 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aspace-import-excel 2 | An [ArchivesSpace ](http://archivesspace.org/) [plugin](https://github.com/archivesspace/tech-docs/blob/master/customization/plugins.md) to support the bulk uploading via Excel SpreadSheet of Archival Objects and (optionally) their associated Creator Agents, Top Containers, Subjects, Digital Objects etc. 3 | 4 | Also supports the import of spreadsheets that will allow for the creation of Digital Objects to be associated with already-created Archival Objects for **Version 2.2.2 and higher** of ArchiveSpace. 5 | 6 | ## Current Version 7 | 8 | For versions of ArchivesSpace **before** v2.2.2: [v1.7.8](https://github.com/harvard-library/aspace-import-excel/releases/tag/v1.7.8) 9 | 10 | **NOTE**: v1.7.8 does *not* support the creation of Digital Objects to be associated with already-created Archival Objects. 11 | 12 | For ArchivesSpace **v2.2.2 and higher**: [v3.0.2](https://github.com/harvard-library/aspace-import-excel/releases/tag/v3.0.3) 13 | 14 | ## Development 15 | 16 | This plugin supports interactive selection of an archival object (or resource) as the starting point of the bulk upload. 17 | 18 | Version 3.0 incorporates new functionality for uploading archival objects (described in the [user documentation](user_documentation/archival_objects_instructions.md)), which supports the use of an [expansion](templates/extended_aspace_import_excel_template.xlsx) to the [original](templates/aspace_import_excel_template.xlsx) Excel template. Version 3.0 is, however, backward compatible, so that users whose workflow is satisfied with the original template can continue to use it. 19 | 20 | ### Bulk upload/creation of Archival Objects 21 | 22 | The Excel templates will be found in the templates/ folder as 23 | * *New in V3.0*: [**extended_aspace_import_excel_template**](templates/extended_aspace_import_excel_template.xlsx) 24 | 25 | * [**aspace_import_excel_template.xlsx**](templates/aspace_import_excel_template.xlsx). 26 | 27 | The intention is not to completely reproduce a Finding Aid as presented in an EAD XML, or to allow for every permutation of Archival Object creation within ArchivesSpace. We are aiming for the "80% rule"; that is, at least 80% of the work that would be done interactively can be replaced by an excel spreadsheet; additional refinements to individual archival objects (such as assignment of locations to top-level containers) would take place interactively. 28 | 29 | See the [user documentation](user_documentation/USER_DOCUMENTATION.md) for more information. 30 | 31 | ### Bulk upload/creation of Digital Objects associated with already-created Archival Objects 32 | 33 | **This functionality is turned on by default** See the Installation instructions for turning it off. 34 | 35 | The Excel template will be found in the templates/ folder as [**aspace_import_excel_DO_template.xlsx**](templates/aspace_import_excel_DO_template.xlsx). 36 | 37 | As with the original development, we are not completely reproducing all the functionality of ArchivesSpace: only one Digital Object, which can have either or both of one: 38 | + File with an *Xlink Actuate Attribute* of **onLoad** and an *Xlink Show Attribute* of **embed** 39 | + File with an Xlink Actuate Attribute of **onRequest** and an *Xlink Show Attribute* of **new** 40 | 41 | See the [user documentation](user_documentation/USER_DOCUMENTATION.md) for more information. 42 | 43 | 44 | 45 | ## Installation 46 | 47 | This is a regular [ArchivesSpace Plug-in](https://github.com/archivesspace/tech-docs/blob/master/customization/plugins.md). 48 | 49 | To install this plug-in: 50 | 1. Either clone this plugin, or download the latest version: 51 | - Clone the plug-in from this [GitHub repository](https://github.com/harvard-library/aspace-import-excel) into the ArchivesSpace **/plugins/** directory. 52 | - Download the zipfile of the appropriate version: see [Current Versions](#current_versions) for links to the appropriate release download. Unzip the download into the **/plugins/** directory. You will probably need to rename the top folder/directory to **aspace-import-excel**. 53 | 54 | 2. (Optional) To turn **off** the functionality for creating Digital Objects associated with already-created Archival objects, you must edit **/plugin/aspace-import-excel/frontend/plugin_init.rb**. Change the line 55 | ```bash 56 | AppConfig[:hide_do_load] = false 57 | ``` 58 | to 59 | ```bash 60 | AppConfig[:hide_do_load] = true 61 | ``` 62 | 3. **IF** you are running, on Windows, a version of ArchivesSpace that is *lower* than version **2.6.0**: 63 | 64 | There was a problem with Bundler versioning. 65 | 66 | Copy 67 | ``` 68 | archivesspace\aspace-import-excel\extras\modified_initialize-plugin.bat 69 | ``` 70 | to 71 | ``` 72 | archivesspace\scripts 73 | ``` 74 | 75 | **UPDATE**: You no longer need to use this modified .bat script **if** you are running ArchivesSpace 2.6.0 or higher. 76 | 77 | 78 | 4. Run the initializer script: 79 | * for Linux, that's 80 | ```bash 81 | scripts/initialize-plugin.sh aspace-import-excel 82 | ``` 83 | * for Windows, running an ArchivesSpace version **lower than 2.6.0** ,that's 84 | ``` 85 | scripts\modified_initialize-plugin.bat aspace-import-excel 86 | ``` 87 | Otherwise, for Windows running ArchivesSpace version **2.6.0** and higher: 88 | ``` 89 | scripts\initialize-plugin.bat aspace-import-excel 90 | ``` 91 | 92 | 93 | 5. In the **common/config/config.rb** file, add 'aspace-import-excel' to the `AppConfig[:plugins]` array. 94 | 6. Stop and restart ArchivesSpace 95 | 96 | ### Why we don't include a Gemfile.lock in this repository 97 | 98 | We have found that when we include a `Gemfile.lock` file in our plugin, some sites have found that, after initializing the plugin and trying to restart ArchivesSpace, they get errors like this: 99 | ```bash 100 | [!] There was an error parsing Gemfile: You cannot specify the same gem twice with different version requirements. 101 | You specified: rubyzip (~> 1.2.2) and rubyzip (= 1.0.0). Bundler cannot continue. 102 | ``` 103 | 104 | This problem does not seem to occur when the `Gemfile.lock` is created through the initialization instead. 105 | 106 | 107 | 108 | ## User Documentation 109 | 110 | User documentation is [available](user_documentation/USER_DOCUMENTATION.md) 111 | 112 | ## Contributors 113 | 114 | * Bobbi Fox: [@bobbi-SMR](https://github.com/bobbi-SMR) (maintainer) 115 | * Robin Wendler: [@rwendler](https://github.com/rwendler) 116 | * Julie Wetherill: [@juliewetherill](https://github.com/juliewetherill) 117 | * Adrienne Pruitt: [@adriennepruitt2](https://github.com/adriennepruitt2) 118 | * Dave Mayo: [@pobocks](https://github.com/pobocks) 119 | * h/t to Chintan Desai: [@cdesai-qi](https://github.com/cdesai-qi) for catching inconsistencies 120 | -------------------------------------------------------------------------------- /frontend/assets/tree_extensions.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Harvard Library 3 | License: MIT license (https://opensource.org/licenses/MIT ) 4 | Author: Bobbi Fox 5 | Version: 1.04 6 | 7 | This script supports the ingest into ArchivesSpace of Excel Spreadsheet data. It currently supports both 8 | ArchivesSpace 1.* and ArchivesSpace 2.* 9 | */ 10 | 11 | 12 | $(function () { 13 | var aspace_version = (typeof(TreeToolbarConfiguration) === 'undefined')? 1 : 2; 14 | var file_modal_html = ''; 15 | var $file_form_modal; 16 | 17 | /* used in aspace v1.* */ 18 | var bulk_btn_str = 'Load via Spreadsheet'; 19 | 20 | 21 | /* returns a hash with information about the selected archival object or resource */ 22 | var get_object_info = function() { 23 | var ret_obj = new Object; 24 | var $tree = $("#archives_tree"); 25 | var $obj_form = $("#archival_object_form"); 26 | if (typeof $obj_form.attr("action") !== 'undefined') { 27 | ret_obj.type = "archival_object"; 28 | ret_obj.aoid = $obj_form.find("#id").val(); 29 | ret_obj.ref_id = $obj_form.find("#archival_object_ref_id_").val(); 30 | ret_obj.resource = $obj_form.find("#archival_object_resource_").val(); 31 | ret_obj.rid = (aspace_version === 1)? $tree.attr("data-root-id") : ret_obj.resource.split('/').pop(); 32 | ret_obj.position = $obj_form.find("#archival_object_position_").val(); 33 | } 34 | else { 35 | $obj_form = $("#resource_form"); 36 | if (typeof $obj_form.attr("action") !== 'undefined') { 37 | ret_obj.type = "resource"; 38 | ret_obj.resource = $obj_form.attr("action"); 39 | ret_obj.aoid = ''; 40 | ret_obj.ref_id = ''; 41 | ret_obj.position = ''; 42 | ret_obj.rid = (aspace_version === 1)? $tree.attr("data-root-id"): $obj_form.find("#id").val(); 43 | } 44 | } 45 | return ret_obj; 46 | } 47 | 48 | /* adds the spreadsheet load button in AS V1.* */ 49 | var add_bulk_button = function() { 50 | var $tmpBtn = $("#bulk-ingest"); 51 | if ($tmpBtn.length == 1) { 52 | // alert("we got it already!"); 53 | } 54 | else { 55 | var $next = $('.btn.add-child'); 56 | if ($next.length == 1) { 57 | $next.parent().append(bulk_btn_str); 58 | // alert("created!"); 59 | } 60 | $("#bulk-ingest").on('click', function() { 61 | file_modal_html = ''; 62 | fileSelection(); 63 | }); 64 | } 65 | } 66 | 67 | var initExcelFileUploadSection = function() { 68 | var handleExcelFileChange = function() { 69 | var $input = $(this); 70 | var filename = $input.val().split("\\").reverse()[0]; 71 | $("#excel_filename").html(filename); 72 | }; 73 | $("#excel_file").on("change", handleExcelFileChange); 74 | 75 | }; 76 | 77 | /* submit the file for processing */ 78 | var handleFileUpload = function($modal) { 79 | /* don't let the modal disappear on submission */ 80 | $modal.on("hide.bs.modal", function (event){ 81 | event.preventDefault(); 82 | event.stopImmediatePropagation(); 83 | }); 84 | /* submit via ajax */ 85 | $form = $("#bulk_ingest_form"); 86 | rid = $form.find("#rid").val(); 87 | /* I do this because ajaxSubmit doesn't like the URL property? */ 88 | $form.attr("action", APP_PATH + "resources/" + rid + "/ssload"); 89 | $form.ajaxSubmit({ 90 | type: "POST", 91 | beforeSubmit: function(arr, $form, options) { 92 | var names = ""; 93 | var hasFile = false; 94 | var missingFile = 'You have not added a file'; 95 | for (var i=0; i < arr.length; i++) { 96 | if (arr[i].type === "file") { 97 | fileObj = arr[i].value; 98 | if (typeof(fileObj) === "object") { 99 | if (typeof(fileObj.type) !== "undefined" && (fileObj.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || fileObj.name.endsWith(".xlsx"))) { 100 | hasFile = true; 101 | } 102 | else { 103 | missingFile = 'The file you have chosen is not an Excel Spreadsheet'; 104 | } 105 | } 106 | } 107 | } 108 | if (!hasFile) { 109 | alert(missingFile); 110 | $(".bulkbtn").removeClass("disabled"); 111 | return false; 112 | } 113 | $(".bulkbtn").addClass('disabled'); 114 | return true; 115 | }, 116 | /* uploadProgress: function(event, position, total, percentComplete) { 117 | var percentVal = percentComplete + '%'; 118 | console.log("Percent: " + percentVal); 119 | }, */ 120 | success: function(data, status, xhr) { 121 | /*display? */ 122 | alert("The file has been processed"); 123 | $("#bulk_messages").html(data); 124 | modalSuccess($file_form_modal); 125 | }, 126 | error: function(xhr, status, err) { 127 | alert("ERROR: " + status + "; Error detected"); 128 | $("#bulk_messages").html(xhr.responseText); 129 | /* console.log(xhr); 130 | console.log(err); */ 131 | /* display error */ 132 | modalError($file_form_modal); 133 | } 134 | }); 135 | $modal.on("hidden.bs.modal", function (event){ 136 | /*console.log("hide hit"); */ 137 | $modal.hide(); 138 | $("body").css("overflow", "auto"); 139 | }); 140 | } 141 | 142 | 143 | /* link switching in the tree in AS v1.* means we have to do some initializing */ 144 | $(document).on('treesingleselected.aspace', function() { 145 | add_bulk_button(); 146 | file_modal_html = ''; 147 | }); 148 | 149 | 150 | 151 | var openFileModal = function() { 152 | $file_form_modal = AS.openCustomModal("bulkIngestFileModal", "Load Spreadsheet", file_modal_html, 'large', null, $("#bulkFileButton").get(0)); 153 | initExcelFileUploadSection(); 154 | $("#bulkFileButton").on("click", function(event) { 155 | event.stopPropagation(); 156 | event.preventDefault(); 157 | handleFileUpload($file_form_modal); 158 | }); 159 | var clipboard = new Clipboard('.clip-btn'); 160 | clipboard.on('success', function(e) { 161 | /* console.log('Action:', e.action); 162 | console.log('Text:', e.text); 163 | console.log('Trigger:', e.trigger); */ 164 | alert('Copied!'); 165 | }); 166 | 167 | clipboard.on('error', function(e) { 168 | console.error('Action:', e.action); 169 | console.error('Trigger:', e.trigger); 170 | alert("Unable to copy"); 171 | }); 172 | 173 | $file_form_modal.show(); 174 | } 175 | var modalError = function($modal) { 176 | $(".bulkbtn").removeClass("disabled"); 177 | $(".bulkbtn.btn-cancel").text("Close").removeClass("disabled").addClass("close") 178 | $(".clip-btn").removeClass("disabled"); 179 | $modal.find(".close").click(function(event) { 180 | $("input").each(function() { 181 | /*console.log($(this).val()); */ 182 | $(this).val(""); 183 | }); 184 | $("#bulk_messages").html(""); 185 | $("#excel_filename").html(""); 186 | $modal.hide(); 187 | $("body").css("overflow", "auto"); 188 | }); 189 | } 190 | 191 | var modalSuccess = function($modal) { 192 | $(".bulkbtn.btn-cancel").text("Close").removeClass("disabled").addClass("close") 193 | $(".clip-btn").removeClass("disabled"); 194 | $modal.find(".close").click(function(event) { 195 | window.location.reload(true); 196 | }); 197 | } 198 | 199 | var toggleTreeSpinner = function(){ 200 | $(".archives-tree-container .spinner").toggle(); 201 | } 202 | 203 | 204 | $(document).on('loadedrecordform.aspace', function () { 205 | /* adding the button to the tree on the resource page */ 206 | add_bulk_button(); 207 | }); 208 | 209 | 210 | var fileSelection = function() { 211 | toggleTreeSpinner(); 212 | obj = get_object_info(); 213 | if ($.isEmptyObject(obj)) { 214 | toggleTreeSpinner(); 215 | return; 216 | } 217 | file_modal_html = ''; 218 | if (typeof($file_form_modal) !== 'undefined') { 219 | /* console.log("Remove"); */ 220 | $file_form_modal.remove(); 221 | } 222 | /*console.log("we got rid: " + obj.rid + " " + obj.aoid + " ref_id: " + obj.ref_id + " resource: " + obj.resource + " position: " + obj.position); */ 223 | $.ajax({ 224 | url: APP_PATH + "resources/" + obj.rid + "/getfile", 225 | type: "POST", 226 | data: {aoid: obj.aoid, type: obj.type, ref_id: obj.ref_id, resource: obj.resource, position: obj.position}, 227 | dataType: "html", 228 | success: function(data) { 229 | file_modal_html = data; 230 | openFileModal(); 231 | }, 232 | error: function(xhr,status,err) { 233 | alert("ERROR: " + status + " " + err); 234 | } 235 | }); 236 | toggleTreeSpinner(); 237 | }; 238 | 239 | var bulkbtnArr = { 240 | label: 'Load via Spreadsheet', 241 | cssClasses: 'btn-default', 242 | onClick: function(event, btn, node, tree, toolbarRenderer) { 243 | fileSelection(); 244 | }, 245 | isEnabled: function(node, tree, toolbarRenderer) { 246 | return true; 247 | }, 248 | isVisible: function(node, tree, toolbarRenderer) { 249 | return !tree.large_tree.read_only; 250 | }, 251 | onFormLoaded: function(btn, form, tree, toolbarRenderer) { 252 | $(btn).removeClass('disabled'); 253 | }, 254 | onToolbarRendered: function(btn, toolbarRenderer) { 255 | $(btn).addClass('disabled'); 256 | }, 257 | } 258 | 259 | if (aspace_version !== 1) { 260 | var res = TreeToolbarConfiguration["resource"]; 261 | TreeToolbarConfiguration["resource"] = [].concat(res).concat([bulkbtnArr]); 262 | var arch = []; 263 | var new_val; 264 | $.each(TreeToolbarConfiguration["archival_object"], function(index,value) { 265 | if ($.type(value) !== "array") { 266 | new_val = value ; 267 | } 268 | else { 269 | new_val = value; 270 | $.each(value,function(i, v){ 271 | if (typeof(v['label']) !== 'undefined' && v['label'] === 'Add Child') { 272 | new_val = [].concat(value).concat([bulkbtnArr]); 273 | } 274 | }); 275 | } 276 | arch.push(new_val); 277 | 278 | }); 279 | TreeToolbarConfiguration["archival_object"] = arch; 280 | } 281 | }); 282 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 The President and Fellows of Harvard College 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "{}" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright {yyyy} {name of copyright owner} 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | 205 | -------------------------------------------------------------------------------- /user_documentation/archival_objects_instructions.md: -------------------------------------------------------------------------------- 1 | # Import Archival Objects 2 | 3 | ## Using the Template to Create a Spreadsheet 4 | 5 | **aspace-import-excel v3.0** introduces an [expanded Excel Spreadsheet template](../templates/extended_aspace_import_excel_template.xlsx) with new functionality for importing Archival Objects. 6 | 7 | The new functionality consists of support for: 8 | 9 | * Individually setting the publish/unpublish flags for Notes. 10 | * Ability to add Agents as Source and Subject, not just Creator. 11 | * Expanded the number of Agents for each type, including directions for adding even more agents. 12 | 13 | * Support for more than one Extent, with the ability to add more extents. 14 | * Support for more than one Container Instance, with the ability to add more container instances. 15 | 16 | The code is backward-compatible with the the original [Excel Spreadsheet template](../templates/aspace_import_excel_template.xlsx) so you may continue using the original if it meets your needs. 17 | 18 | Once you've opened your chosen template, use **Save as** *(your new filename}*.xlsx to begin filling in your spreadsheet. 19 | 20 | 21 | The template is designed to be flexible enough to accommodate different workflows. The *first row* is the place where you can put identifying information, such as "Foo Collection". 22 | 23 | As long as you **don't edit** the **row** marked *"ArchivesSpace field code"*, you may hide, delete, or rearrange **columns** to suit your workflow. Indeed, you will see that there are a few already-hidden columns; these are not currently used, but may be used in future enhancements. **_DO NOT_** hide required columns. 24 | 25 | **Note** that some columns already have in-column drop down data validation defined. You may of course add more of these, or edit the ones that are already defined. See [The Excel help page](https://support.office.com/en-us/article/Apply-data-validation-to-cells-29FECBCC-D1B9-42C1-9D76-EFF3CE5F7249) to learn how to create these. 26 | 27 | Column Definitions \| Dates \| Extent \| Container \| Digital Objects \| Agents \| Subjects \| Notes 28 | 29 | ### Required Columns 30 | 31 | There are very few columns that _must_ be filled in: 32 | 33 | * **EAD ID** - of the resource to which you're adding Archival Objects. This will be used to confirm that you are trying to add your spreadsheet information to the correct resource. 34 | * The **Hierarchical Relationship** of the new Archival Object to the selected resource or selected Archival Object: If you've selected a Resource, **1** indicates that this is the first level of Archival Objects. If you have selected an Archival Object, use **1** if you're adding a sibling to a selected Archival Object, **2** if a child, etc. You can therefore describe several levels of Archival Objects in a single spreadsheet. 35 | * **The Description Level** This is an in-column drop-down. The Description Level in-column drop down 36 | * EITHER the **Title** OR a **valid Date** having at least a begin date or a date expression. 37 | 38 | ## Column Definitions 39 | 40 | Below is a discussion of each used column in the spreadsheet. 41 | 42 | For columns where the value is from a Controlled Value List, you can fill in either the controlled list's **Value** *or* the **Translation**. It must be entered **exactly** as it is written (lower case, title case, etc.). As an example (for English), in the *Extent Extent Type* controlled list, "cubic feet" is represented as the **value** `cubic_feet` or the **translation** `Cubic Feet`. Entering `cubic feet` would result in an error message. 43 | 44 | **Notes:** 45 | 1. The application compares the input first against the **Translation**, then, failing that, against the **Value**. 46 | 2. In the case that your list has more than one entry with the same **Translation**, the **Value** for the first (lowest position) entry is used. A **WARN** message will appear in the frontend log file when this application encounters this situation. 47 | 48 | 49 | Column | Value | Default | Comment 50 | -------|-------|---------|--------- 51 | EAD ID | String | | **REQUIRED** 52 | Title | String| |Title of the Archival Object; required if no Creation Date information 53 | Component Unit Identifier| String | | 54 | Hierarchical Relationship| Number | | **REQUIRED** 55 | Description Level| in column drop-down || **REQUIRED** *from the Archival Record Level controlled value list* 56 | Other Level| String | *unspecified*| This is used if *Other Level* was specified in the **Description Level** 57 | Publish?| in column drop-down | **False** | This is applied to any information (such as subject, note) created with this Archival Object 58 | Restrictions Apply? | in column drop-down | **False** | 59 | Processing Note | String | | No markup allowed 60 | 61 | Column Definitions \| Dates \| Extent \| Container \| Digital Objects \| Agents \| Subjects \| Notes 62 | 63 | ### Dates 64 | 65 | New in version 3.0: Support for more than one Date. The spreadsheet provides for two dates; you can add more by following the instructions for adding additional dates. 66 | 67 | A Date must have **a valid label** and **at least** either a *begin date* or a *date expression.* 68 | 69 | **NOTE:** The cell format for cells containing values for *Date Begin* and *Date End* **MUST** be **Text**, not some date format like `yyyy-mm-dd`, if you don't want the hours, minutes, seconds appended (e.g.: *1969-17-17T00:00:00+00.00*). Some versions of Excel will "helpfully" convert the cell to a date format if you are not watching. 70 | 71 | Column | Value | Default | Comment 72 | -------|-------|---------|--------- 73 | Dates Label | String | *creation* | from the *Date Label* controlled value list. **Note**: If the value given is *not* on the controlled value list, this date will not be processed. 74 | Date Begin | a Date string || in one of the following: **YYYY, YYYY-MM, or YYYY-MM-DD** 75 | Date End | a Date string || in one of the following: **YYYY, YYYY-MM, or YYYY-MM-DD** **Note**: If you choose a Date Type of *'single'*, any value in this column will be ignored. 76 | Date Type | String| *inclusive*| from the *Date Type* controlled value list. **Note**: If the given value is *not* on the controlled value list, it will be overridden with the value 'inclusive'. 77 | Date Expression |String|| 78 | Date Certainty |String | | from the *Date Certainty* controlled value list 79 | 80 | ### Adding more dates to the spreadsheet 81 | 82 | New in version 3.0: 83 | The plugin supports your adding more than the two dates supplied on the spreadsheet. To do this, you may edit, locally, the [extended_aspace_import_excel_template.xlsx](../templates/extended_aspace_import_excel_template.xlsx) by copying the set of columns for the second date, inserting them into the template, and editing the labels in Rows 4 and 5 to reflect the next integer number: 84 | * insert 6 columns to the RIGHT of second date block 85 | * copy the six columns of the second date, then paste them into the blank columns 86 | * edit the labels in Row 4 to increment the number. For example, for the first added date, you'd edit **dates_label_2** to **dates_label_3** . **NOTE**: it is *extremely important* that you ensure that the labels in Row 4 are edited; otherwise, you may not get the results you're expecting. 87 | * While not necessary for proper processing, it's recommended that you also update the numbers in the copied columns in Row 5 to avoid confusion. For example, edit **Date (2) Label** to **Date (3) Label**. 88 | 89 | Column Definitions \| Dates \| Extent \| Container \| Digital Objects \| Agents \| Subjects \| Notes 90 | 91 | ### Extent Information 92 | 93 | New in version 3.0: Support for more than one extent. The spreadsheet provides for two extents; you can add more by following the instructions for adding additional extents. 94 | 95 | Extent information is not required, but if you are defining an extent, please note the required fields. 96 | 97 | Column | Value | Default | Comment 98 | -------|-------|---------|--------- 99 | Extent portion | String| whole| from the *Extent Portion* controlled value list 100 | Extent number | Number||**REQUIRED** 101 | Extent type | String| |**REQUIRED** from the *Extent Extent Type* controlled value list 102 | Container Summary|String|| 103 | Physical details |String|| 104 | Dimensions| String || 105 | 106 | ### Adding more extents to the spreadsheet 107 | 108 | New in version 3.0: 109 | The plugin supports your adding more than the two extents supplied on the spreadsheet. To do this, you may edit, locally, the [extended_aspace_import_excel_template.xlsx](../templates/extended_aspace_import_excel_template.xlsx) by copying the set of columns for the second extent, inserting them into the template, and editing the labels in Rows 4 and 5 to reflect the next integer number: 110 | * insert 6 columns to the RIGHT of second extent block 111 | * copy the six columns of the second extent, then paste them into the blank columns 112 | * edit the labels in Row 4 to increment the number. For example, for the first added extent, you'd edit **portion_2** to **portion_3** . **NOTE**: it is *extremely important* that you ensure that the labels in Row 4 are edited; otherwise, you may not get the results you're expecting. 113 | * While not necessary for proper processing, it's recommended that you also update the numbers in the copied columns in Row 5 to avoid confusion. For example, edit **Extent Portion(2)** to **Extent Portion(3)**. * 114 | 115 | Column Definitions \| Dates \| Extent \| Container \| Digital Objects \| Agents \| Subjects \| Notes 116 | 117 | ### Container Information - Creating a Container Instance 118 | 119 | New in version 3.0: Support for more than one container instance. The spreadsheet provides for two container instances; you can add more by following the instructions for adding additional instances. 120 | 121 | A Container instance associates the Archival Object with a Top Container, with additional information on Child and Grandchild sub-containers if present. 122 | 123 | The ingester will try to find an already-created Top Container in the database. 124 | + If you have defined a barcode: 125 | + If there's a match for the barcode for that resource, that Top Container will be used without further checking. 126 | + Otherwise, a new Top Container will be created. 127 | + If you have not defined a barcode: 128 | + The type and indicator will be used to search the database for a Top Container that is already associated with the resource; 129 | + Otherwise, a new Top Container will be created. 130 | 131 | 132 | **NOTE:** if you want the object in this spreadsheet to be in a Top Container shared with *another Resource*, you must either specify the container by *barcode* or else make sure that at least one archival object in the spreadsheet's Resource with that container has already been created via the usual ArchivesSpace interface. 133 | 134 | If you are specifying container information, note that both **type** and **indicator** are required for each level (top, child, and grandchild) you want to specify. 135 | 136 | 137 | Column | Value | Default | Comment 138 | -------|-------|---------|--------- 139 | Container Instance type| String | | **REQUIRED** if you are defining a Container Instance. Value from the *Instance Instance Type* controlled value list 140 | Top Container indicator|String | Unknown || **REQUIRED** 141 | Barcode|String||| 142 | Child type | String||from the *Container Type* controlled value list 143 | Child indicator|String |Unknown || *only used if a Child type is specified* 144 | Grandchild type | String||from the *Container Type* controlled value list 145 | Grandchild indicator|String | Unknown || *only used if a Grandchild type is specified* 146 | 147 | ### Adding more container instances to the spreadsheet 148 | 149 | New in version 3.0: 150 | The plugin supports your adding more than the two container instances supplied on the spreadsheet. To do this, you may edit, locally, the [extended_aspace_import_excel_template.xlsx](../templates/extended_aspace_import_excel_template.xlsx) by copying the set of columns for the second extent, inserting them into the template, and editing the labels in Rows 4 and 5 to reflect the next integer number: 151 | * insert 8 columns to the LEFT of second container block 152 | * copy the 8 columns of the second container block, then paste them into the blank columns 153 | * edit the labels in Row 4 to increment the number. For example, for the first added container instance, you'd edit **cont_instance_type_2** to **cont_instance_type_3** . 154 | For container instances, there are some Row 4 values with double numbers, such as **type_2_2**, which would be edited to **type_2_3**. Sorry for the confusion! 155 | **NOTE**: it is *extremely important* that you ensure that the labels in Row 4 are edited; otherwise, you may not get the results you're expecting. 156 | * While not necessary for proper processing, it's recommended that you also edit the numbers in the copied columns in Row 5 to avoid confusion. For example, edit **Container Instance Type(2)** to **Container Instance Type(3)**. 157 | 158 | Column Definitions \| Dates \| Extent \| Container \| Digital Objects \| Agents \| Subjects \| Notes 159 | 160 | ### Digital Objects 161 | 162 | Ingest allows you to create a Digital Object, and associate it with the Archival Object. The "publish" state will be whatever the "publish" state of the Archival Object has been defined to be. 163 | 164 | Column | Value | Default | Comment 165 | -------|-------|---------|--------- 166 | Digital Object Title| String || If no Digital Object Title is provided, the display header string of the parent Archival Object will be used. 167 | URL of Linked-out digital object| URL String || this becomes the File Version with the **actuate_attribute** set to "onRequest" and the **show_attribute** set to "new" 168 | URL of thumbnail| URL String || if defined, this becomes the File version with the **actuate_attribute** set to "onLoad", the **show_attribute** set to "embed", and the "is representative" flag is set to TRUE. 169 | 170 | Column Definitions \| Dates \| Extent \| Container \| Digital Objects \| Agents \| Subjects \| Notes 171 | 172 | 173 | ### Agent Objects 174 | 175 | The ingester allows you to link Agents to Archival objects. The [extended_aspace_import_excel_template.xlsx](../templates/extended_aspace_import_excel_template.xlsx), as provided, allows for up to **5** Person Agents, up to **2** Family Agents, and up to **3** Corporate Agents per Archival object. If you need more of any of these types, you can follow the directions for adding more agents. 176 | 177 | If you have previously defined the Agent(s) you are using, you may use the Record ID number (e.g.: for the Agent URI /agents */agent_person/1249*, you would use **1249**) OR the full header string, with all capitalization and punctuation. 178 | 179 | Either the Record ID *or* the header string is **required**. 180 | 181 | If you include both, or only the header, and the record isn't found, a new Agent record will be created. The header string will be used as the **family_name** if it's a Family Agent, and the **primary_name** 182 | otherwise. 183 | 184 | If you enter the header string *without* the ID, the ingester will try to do an **exact match** against the header; if it finds more than one match (for example, if the database contains two agents with identical headers, but different sources): 185 | 186 | * The ingester will create a **new** agent (with publish=false) containing the header with ' DISAMBIGUATE ME!' appended to it. For example, given a person agent with a header of 'George Washington', a new person agent would be created with a primary name of 'George Washington DISAMBIGUATE ME!'. 187 | * After ingest, you can use the *merge* functionality to resolve the ambiguities. 188 | 189 | If you enter a Record ID and **not** the header string, and that ID is not found, a new Agent record will be created with the name "PLACEHOLDER FOR *{agent type}* ID *{ id number}* NOT FOUND", so that you may easily find that record later and edit/merge it. In this case, the new Agent would be marked publish=false. When you correct the record, change publish to true if appropriate. 190 | 191 | 192 | 193 | If you **only** enter the header string, and a record isn't found in the database, a new Agent will be created, with its Linked Agent Role of **Creator**. 194 | 195 | 196 | 197 | 198 | #### Person agents: 199 | 200 | Column | Value | Default | Comment 201 | -------|-------|---------|--------- 202 | Agent (1) Record ID | Number|| 203 | Agent (1) header string |String|| must be the entire header, including punctuation & capitalization 204 | Agent Role(1)|String|Creator|New in v3.0: from the *Linked Agent Role* controlled value list. 205 | Agent (1) Relator|String|| If supplying relator, term must be from the *Linked Agent Archival Record Relators* controlled value list. The default list provided by ArchivesSpace maps to the [MARC Relator Code and Term List](http://www.loc.gov/marc/relators/relaterm.html). 206 | Agent (2) Record ID | Number|| 207 | Agent (2) header string |String|| must be the entire header, including punctuation & capitalization 208 | Agent Role(2)|String|Creator|New in v3.0: from the *Linked Agent Role* controlled value list. 209 | Agent (2) Relator|String|| If supplying relator, term must be from the *Linked Agent Archival Record Relators* controlled value list. 210 | Agent (3) Record ID | Number|| 211 | Agent (3) header string |String|| must be the entire header, including punctuation & capitalization 212 | Agent Role(3)|String|Creator|New in v3.0: from the *Linked Agent Role* controlled value list. 213 | Agent (3) Relator|String|| If supplying relator, term must be from the *Linked Agent Archival Record Relators* controlled value list. 214 | 215 | #### Family Agents: 216 | Column | Value | Default | Comment 217 | -------|-------|---------|--------- 218 | Family Agent Record ID | Number|| 219 | Family Agent header string |String|| must be the entire header, including punctuation & capitalization 220 | Family Agent Role|String|Creator|New in v3.0: from the *Linked Agent Role* controlled value list. 221 | Family Agent Relator|String|| If supplying relator, term must be from the *Linked Agent Archival Record Relators* controlled value list. 222 | 223 | #### Corporate Agents: 224 | Column | Value | Default | Comment 225 | -------|-------|---------|--------- 226 | Corporate Agent Record ID | Number|| 227 | Corporate Agent header string |String|| must be the entire header, including punctuation & capitalization 228 | Corporate Agent Role|String|Creator|New in v3.0: from the *Linked Agent Role* controlled value list. 229 | Corporate Agent Relator|string|| If supplying relator, term must be from the *Linked Agent Archival Record Relators* controlled value list. 230 | Corporate Agent Record ID (2) | Number|| 231 | Corporate Agent header string (2) |String|| must be the entire header, including punctuation & capitalization 232 | Corporate Agent Role(2)|String|Creator|New in v3.0: from the *Linked Agent Role* controlled value list. 233 | Corporate Agent Relator (2)|String|| If supplying relator, term must be from the *Linked Agent Archival Record Relators* controlled value list. 234 | 235 | ### Adding more agents to the spreadsheet 236 | 237 | The plugin supports your associating with an Archival Object even more agents of each type. To do this, you may edit, locally, the [extended_aspace_import_excel_template.xlsx](../templates/extended_aspace_import_excel_template.xlsx) by copying the last set of columns of the particular type, inserting them into the template, and editing the labels in Rows 4 and 5 to reflect the next integer number. 238 | 239 | For example, if you were to want *3* Family Agents, you would: 240 | * insert four blank columns next to the second Family Agent columns 241 | * copy the four columns of the second Family Agent, and paste them into the blank columns 242 | * edit the labels in Row 4, incrementing the number. For example, you would edit the label **families_agent_record_id_2** in the _copied_ column to **families_agent_record_id_3**. **NOTE**: it is *extremely important* that you ensure that the labels in Row 4 are edited; otherwise, you may not get the results you're expecting. 243 | * While not necessary for proper processing, it's recommended that you also update the numbers in Row 5, to avoid confusion. For example, you would edit the label **Family Agent(2) header string** to **Family Agent(3) header string** 244 | 245 | 246 | **Note:** The plugin stops at the first set of columns that are blank. This means that, if you've filled in the columns for Person Agent 1, and Person Agent 3, leaving Person Agent 2 blank, the plugin *will not* 247 | process Person Agent 3. 248 | 249 | Column Definitions \| Dates \| Extent \| Container \| Digital Objects \| Agents \| Subjects \| Notes 250 | 251 | ### Subjects 252 | 253 | As with Agents, you may associate Subjects with the Archival Object. You may associate up to two Subject records. If you know the Record ID, you may use that instead of the **term**, **type**, and **source** in a manner similar to the way that Agent specifications are made, with the same database lookup and handling done there. Again, if you want the ingest to look up the **term** in the database, you must use the entire Subject header, including any punctuation or capitalization. 254 | 255 | If you enter the subject header string *without* the ID, the ingester will try to do an **exact match** against the header; if it finds more than one match (for example, if the database contains two subjects with identical headers, but different sources): 256 | 257 | * The ingester will create a **new** subject (with publish=false) containing the header with ' DISAMBIGUATE ME!' appended to it. For example, given a subject with a header of 'Black Lives Matter', a new subject would be created with the header 'Black Lives Matter DISAMBIGUATE ME!'. 258 | * After ingest, you can use the *merge* functionality to resolve the ambiguities. 259 | 260 | Column | Value | Default | Comment 261 | -------|-------|---------|--------- 262 | Subject (1) Record ID|Number|| 263 | Subject (1) Term |String || 264 | Subject (1) Type | String| topical| from the *Subject Term Type* controlled value list 265 | Subject (1) Source | String| ingest| from the *Subject Source* controlled value list 266 | Subject (2) Record ID|Number|| 267 | Subject (2) Term |String || 268 | Subject (2) Type | String| topical| from the *Subject Term Type* controlled value list 269 | Subject (2) Source | String| ingest| from the *Subject Source* controlled value list 270 | 271 | Column Definitions \| Dates \| Extent \| Container \| Digital Objects \| Agents \| Subjects \| Notes 272 | 273 | 274 | ### Notes fields 275 | 276 | You may specify a variety of notes fields. 277 | 278 | If the note type allows for subfields, what you specify will be put in the first subfield. 279 | 280 | New in version 3.0: 281 | Each Note column is accompanied by a "Publish" column, which has in-column drop down data validation (TRUE/FALSE). The publish flag will be set for that note (and any associated subnote) as follows: 282 | * if the field is left blank, use the value of the Publish field for that Archival Object 283 | * Otherwise, set to True or False as specified. 284 | 285 | 286 | As does ArchivesSpace, you may used Mixed Content (EAD/XML markup). The Ingester will check to make sure that the entry is "well formed" -- that is, that the opening and closing elements match -- but will **not** validate the text to make sure you're using the proper markup. 287 | 288 | The following Notes fields are supported: 289 | 290 | + Abstract 291 | + Access Restrictions 292 | + Acquisition Information 293 | + Arrangement 294 | + Biography/History 295 | + Custodial History 296 | + Dimensions 297 | + General 298 | + Language of Materials 299 | + Physical Description 300 | + Physical Facet 301 | + Physical Location 302 | + Preferred Citation 303 | + Processing Information 304 | + Related Materials 305 | + Scope and Contents 306 | + Separated Materials 307 | + Use Restrictions 308 | 309 | Column Definitions \| Dates \| Extent \| Container \| Digital Objects \| Agents \| Subjects \| Notes 310 | -------------------------------------------------------------------------------- /frontend/assets/clipboard.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * clipboard.js v1.6.1 3 | * https://zenorocha.github.io/clipboard.js 4 | * 5 | * Licensed MIT © Zeno Rocha 6 | */ 7 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 && arguments[0] !== undefined ? arguments[0] : {}; 421 | 422 | this.action = options.action; 423 | this.emitter = options.emitter; 424 | this.target = options.target; 425 | this.text = options.text; 426 | this.trigger = options.trigger; 427 | 428 | this.selectedText = ''; 429 | } 430 | }, { 431 | key: 'initSelection', 432 | value: function initSelection() { 433 | if (this.text) { 434 | this.selectFake(); 435 | } else if (this.target) { 436 | this.selectTarget(); 437 | } 438 | } 439 | }, { 440 | key: 'selectFake', 441 | value: function selectFake() { 442 | var _this = this; 443 | 444 | var isRTL = document.documentElement.getAttribute('dir') == 'rtl'; 445 | 446 | this.removeFake(); 447 | 448 | this.fakeHandlerCallback = function () { 449 | return _this.removeFake(); 450 | }; 451 | this.fakeHandler = document.body.addEventListener('click', this.fakeHandlerCallback) || true; 452 | 453 | this.fakeElem = document.createElement('textarea'); 454 | // Prevent zooming on iOS 455 | this.fakeElem.style.fontSize = '12pt'; 456 | // Reset box model 457 | this.fakeElem.style.border = '0'; 458 | this.fakeElem.style.padding = '0'; 459 | this.fakeElem.style.margin = '0'; 460 | // Move element out of screen horizontally 461 | this.fakeElem.style.position = 'absolute'; 462 | this.fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px'; 463 | // Move element to the same position vertically 464 | var yPosition = window.pageYOffset || document.documentElement.scrollTop; 465 | this.fakeElem.style.top = yPosition + 'px'; 466 | 467 | this.fakeElem.setAttribute('readonly', ''); 468 | this.fakeElem.value = this.text; 469 | 470 | document.body.appendChild(this.fakeElem); 471 | 472 | this.selectedText = (0, _select2.default)(this.fakeElem); 473 | this.copyText(); 474 | } 475 | }, { 476 | key: 'removeFake', 477 | value: function removeFake() { 478 | if (this.fakeHandler) { 479 | document.body.removeEventListener('click', this.fakeHandlerCallback); 480 | this.fakeHandler = null; 481 | this.fakeHandlerCallback = null; 482 | } 483 | 484 | if (this.fakeElem) { 485 | document.body.removeChild(this.fakeElem); 486 | this.fakeElem = null; 487 | } 488 | } 489 | }, { 490 | key: 'selectTarget', 491 | value: function selectTarget() { 492 | this.selectedText = (0, _select2.default)(this.target); 493 | this.copyText(); 494 | } 495 | }, { 496 | key: 'copyText', 497 | value: function copyText() { 498 | var succeeded = void 0; 499 | 500 | try { 501 | succeeded = document.execCommand(this.action); 502 | } catch (err) { 503 | succeeded = false; 504 | } 505 | 506 | this.handleResult(succeeded); 507 | } 508 | }, { 509 | key: 'handleResult', 510 | value: function handleResult(succeeded) { 511 | this.emitter.emit(succeeded ? 'success' : 'error', { 512 | action: this.action, 513 | text: this.selectedText, 514 | trigger: this.trigger, 515 | clearSelection: this.clearSelection.bind(this) 516 | }); 517 | } 518 | }, { 519 | key: 'clearSelection', 520 | value: function clearSelection() { 521 | if (this.target) { 522 | this.target.blur(); 523 | } 524 | 525 | window.getSelection().removeAllRanges(); 526 | } 527 | }, { 528 | key: 'destroy', 529 | value: function destroy() { 530 | this.removeFake(); 531 | } 532 | }, { 533 | key: 'action', 534 | set: function set() { 535 | var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'copy'; 536 | 537 | this._action = action; 538 | 539 | if (this._action !== 'copy' && this._action !== 'cut') { 540 | throw new Error('Invalid "action" value, use either "copy" or "cut"'); 541 | } 542 | }, 543 | get: function get() { 544 | return this._action; 545 | } 546 | }, { 547 | key: 'target', 548 | set: function set(target) { 549 | if (target !== undefined) { 550 | if (target && (typeof target === 'undefined' ? 'undefined' : _typeof(target)) === 'object' && target.nodeType === 1) { 551 | if (this.action === 'copy' && target.hasAttribute('disabled')) { 552 | throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute'); 553 | } 554 | 555 | if (this.action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) { 556 | throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes'); 557 | } 558 | 559 | this._target = target; 560 | } else { 561 | throw new Error('Invalid "target" value, use a valid Element'); 562 | } 563 | } 564 | }, 565 | get: function get() { 566 | return this._target; 567 | } 568 | }]); 569 | 570 | return ClipboardAction; 571 | }(); 572 | 573 | module.exports = ClipboardAction; 574 | }); 575 | 576 | },{"select":5}],8:[function(require,module,exports){ 577 | (function (global, factory) { 578 | if (typeof define === "function" && define.amd) { 579 | define(['module', './clipboard-action', 'tiny-emitter', 'good-listener'], factory); 580 | } else if (typeof exports !== "undefined") { 581 | factory(module, require('./clipboard-action'), require('tiny-emitter'), require('good-listener')); 582 | } else { 583 | var mod = { 584 | exports: {} 585 | }; 586 | factory(mod, global.clipboardAction, global.tinyEmitter, global.goodListener); 587 | global.clipboard = mod.exports; 588 | } 589 | })(this, function (module, _clipboardAction, _tinyEmitter, _goodListener) { 590 | 'use strict'; 591 | 592 | var _clipboardAction2 = _interopRequireDefault(_clipboardAction); 593 | 594 | var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter); 595 | 596 | var _goodListener2 = _interopRequireDefault(_goodListener); 597 | 598 | function _interopRequireDefault(obj) { 599 | return obj && obj.__esModule ? obj : { 600 | default: obj 601 | }; 602 | } 603 | 604 | function _classCallCheck(instance, Constructor) { 605 | if (!(instance instanceof Constructor)) { 606 | throw new TypeError("Cannot call a class as a function"); 607 | } 608 | } 609 | 610 | var _createClass = function () { 611 | function defineProperties(target, props) { 612 | for (var i = 0; i < props.length; i++) { 613 | var descriptor = props[i]; 614 | descriptor.enumerable = descriptor.enumerable || false; 615 | descriptor.configurable = true; 616 | if ("value" in descriptor) descriptor.writable = true; 617 | Object.defineProperty(target, descriptor.key, descriptor); 618 | } 619 | } 620 | 621 | return function (Constructor, protoProps, staticProps) { 622 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 623 | if (staticProps) defineProperties(Constructor, staticProps); 624 | return Constructor; 625 | }; 626 | }(); 627 | 628 | function _possibleConstructorReturn(self, call) { 629 | if (!self) { 630 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 631 | } 632 | 633 | return call && (typeof call === "object" || typeof call === "function") ? call : self; 634 | } 635 | 636 | function _inherits(subClass, superClass) { 637 | if (typeof superClass !== "function" && superClass !== null) { 638 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 639 | } 640 | 641 | subClass.prototype = Object.create(superClass && superClass.prototype, { 642 | constructor: { 643 | value: subClass, 644 | enumerable: false, 645 | writable: true, 646 | configurable: true 647 | } 648 | }); 649 | if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 650 | } 651 | 652 | var Clipboard = function (_Emitter) { 653 | _inherits(Clipboard, _Emitter); 654 | 655 | /** 656 | * @param {String|HTMLElement|HTMLCollection|NodeList} trigger 657 | * @param {Object} options 658 | */ 659 | function Clipboard(trigger, options) { 660 | _classCallCheck(this, Clipboard); 661 | 662 | var _this = _possibleConstructorReturn(this, (Clipboard.__proto__ || Object.getPrototypeOf(Clipboard)).call(this)); 663 | 664 | _this.resolveOptions(options); 665 | _this.listenClick(trigger); 666 | return _this; 667 | } 668 | 669 | /** 670 | * Defines if attributes would be resolved using internal setter functions 671 | * or custom functions that were passed in the constructor. 672 | * @param {Object} options 673 | */ 674 | 675 | 676 | _createClass(Clipboard, [{ 677 | key: 'resolveOptions', 678 | value: function resolveOptions() { 679 | var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 680 | 681 | this.action = typeof options.action === 'function' ? options.action : this.defaultAction; 682 | this.target = typeof options.target === 'function' ? options.target : this.defaultTarget; 683 | this.text = typeof options.text === 'function' ? options.text : this.defaultText; 684 | } 685 | }, { 686 | key: 'listenClick', 687 | value: function listenClick(trigger) { 688 | var _this2 = this; 689 | 690 | this.listener = (0, _goodListener2.default)(trigger, 'click', function (e) { 691 | return _this2.onClick(e); 692 | }); 693 | } 694 | }, { 695 | key: 'onClick', 696 | value: function onClick(e) { 697 | var trigger = e.delegateTarget || e.currentTarget; 698 | 699 | if (this.clipboardAction) { 700 | this.clipboardAction = null; 701 | } 702 | 703 | this.clipboardAction = new _clipboardAction2.default({ 704 | action: this.action(trigger), 705 | target: this.target(trigger), 706 | text: this.text(trigger), 707 | trigger: trigger, 708 | emitter: this 709 | }); 710 | } 711 | }, { 712 | key: 'defaultAction', 713 | value: function defaultAction(trigger) { 714 | return getAttributeValue('action', trigger); 715 | } 716 | }, { 717 | key: 'defaultTarget', 718 | value: function defaultTarget(trigger) { 719 | var selector = getAttributeValue('target', trigger); 720 | 721 | if (selector) { 722 | return document.querySelector(selector); 723 | } 724 | } 725 | }, { 726 | key: 'defaultText', 727 | value: function defaultText(trigger) { 728 | return getAttributeValue('text', trigger); 729 | } 730 | }, { 731 | key: 'destroy', 732 | value: function destroy() { 733 | this.listener.destroy(); 734 | 735 | if (this.clipboardAction) { 736 | this.clipboardAction.destroy(); 737 | this.clipboardAction = null; 738 | } 739 | } 740 | }], [{ 741 | key: 'isSupported', 742 | value: function isSupported() { 743 | var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut']; 744 | 745 | var actions = typeof action === 'string' ? [action] : action; 746 | var support = !!document.queryCommandSupported; 747 | 748 | actions.forEach(function (action) { 749 | support = support && !!document.queryCommandSupported(action); 750 | }); 751 | 752 | return support; 753 | } 754 | }]); 755 | 756 | return Clipboard; 757 | }(_tinyEmitter2.default); 758 | 759 | /** 760 | * Helper function to retrieve attribute value. 761 | * @param {String} suffix 762 | * @param {Element} element 763 | */ 764 | function getAttributeValue(suffix, element) { 765 | var attribute = 'data-clipboard-' + suffix; 766 | 767 | if (!element.hasAttribute(attribute)) { 768 | return; 769 | } 770 | 771 | return element.getAttribute(attribute); 772 | } 773 | 774 | module.exports = Clipboard; 775 | }); 776 | 777 | },{"./clipboard-action":7,"good-listener":4,"tiny-emitter":6}]},{},[8])(8) 778 | }); -------------------------------------------------------------------------------- /frontend/controllers/resources_updates_controller.rb: -------------------------------------------------------------------------------- 1 | class ResourcesUpdatesController < ApplicationController 2 | require 'nokogiri' 3 | require 'pp' 4 | 5 | START_MARKER = /ArchivesSpace field code/ 6 | DO_START_MARKER = /ArchivesSpace digital object import field codes/ 7 | set_access_control "update_resource_record" => [:new, :edit, :create, :update, :rde, :add_children, :publish, :accept_children, :load_ss, :get_file, :get_do_file, :load_dos] 8 | 9 | require 'rubyXL' 10 | require 'asutils' 11 | require 'enum_list' 12 | include NotesHelper 13 | include UpdatesUtils 14 | include LinkedObjects 15 | require 'ingest_report' 16 | 17 | # create the file form for the digital object spreadsheet 18 | def get_do_file 19 | rid = params[:rid] 20 | id = params[:id] 21 | end 22 | 23 | # create the file form for the spreadsheet 24 | def get_file 25 | rid = params[:rid] 26 | type = params[:type] 27 | aoid = params[:aoid] || '' 28 | ref_id = params[:ref_id] || '' 29 | resource = params[:resource] 30 | position = params[:position] || '1' 31 | @resource = Resource.find(params[:rid]) 32 | repo_id = @resource['repository']['ref'].split('/').last 33 | return render_aspace_partial :partial => "resources/bulk_file_form", :locals => {:rid => rid, :aoid => aoid, :type => type, :ref_id => ref_id, :resource => resource, :position => position, :repo_id => repo_id} 34 | end 35 | 36 | # load the digital objects 37 | def load_dos 38 | #first time out of the box: 39 | #Rails.logger.info "\t**** LOAD DOS ***" 40 | ao = fetch_archival_object(params) 41 | Rails.logger.info "ao instances? #{!ao["instances"].blank?}" if ao 42 | if !ao['instances'].blank? 43 | digs = [] 44 | ao['instances'].each {|instance| digs.append(ao) if instance.dig("digital_object") != nil } 45 | unless digs.blank? 46 | # add thrown exception here! 47 | ao = nil 48 | end 49 | end 50 | # Rails.logger.info {ao.pretty_inspect} 51 | end 52 | # load in a spreadsheet 53 | def load_ss 54 | @report_out = [] 55 | @report = IngestReport.new 56 | @headers 57 | @digital_load = params.fetch(:digital_load,'') == 'true' 58 | 59 | if @digital_load 60 | @find_uri = "/repositories/#{params[:repo_id]}/find_by_id/archival_objects" 61 | @resource_ref = "/repositories/#{params[:repo_id]}/resources/#{params[:id]}" 62 | @repo_id = params[:repo_id] 63 | @start_marker = DO_START_MARKER 64 | else 65 | @created_ao_refs = [] 66 | @first_level_aos = [] 67 | @archival_levels = EnumList.new('archival_record_level') 68 | @container_types = EnumList.new('container_type') 69 | @date_types = EnumList.new('date_type') 70 | @date_labels = EnumList.new('date_label') 71 | @date_certainty = EnumList.new('date_certainty') 72 | @extent_types = EnumList.new('extent_extent_type') 73 | @extent_portions = EnumList.new('extent_portion') 74 | @instance_types ||= EnumList.new('instance_instance_type') 75 | @parents = ParentTracker.new 76 | @start_marker = START_MARKER 77 | end 78 | @start_position 79 | @need_to_move = false 80 | begin 81 | rows = initialize_info(params) 82 | while @headers.nil? && (row = rows.next) 83 | @counter += 1 84 | if (row[0] && (row[0].value.to_s =~ @start_marker) || row[2] && row[2].value == 'ead') #FIXME: TEMP FIX 85 | 86 | @headers = row_values(row) 87 | begin 88 | check_for_code_dups 89 | rescue Exception => e 90 | raise StopExcelImportException.new(e.message) 91 | end 92 | # Skip the human readable header too 93 | rows.next 94 | @counter += 1 # for the skipping 95 | end 96 | end 97 | begin 98 | while (row = rows.next) 99 | @counter += 1 100 | values = row_values(row) 101 | next if values.reject(&:blank?).empty? 102 | @row_hash = Hash[@headers.zip(values)] 103 | ao = nil 104 | begin 105 | @report.new_row(@counter) 106 | if @digital_load 107 | ao = process_do_row(params) 108 | else 109 | ao = process_row 110 | end 111 | @rows_processed += 1 112 | @error_level = nil 113 | rescue StopExcelImportException => se 114 | @report.add_errors(I18n.t('plugins.aspace-import-excel.error.stopped', :row => @counter, :msg => se.message)) 115 | raise StopIteration.new 116 | rescue ExcelImportException => e 117 | @error_rows += 1 118 | @report.add_errors( e.message) 119 | @error_level = @hier 120 | end 121 | @report.end_row 122 | end 123 | rescue StopIteration 124 | # we just want to catch this without processing further 125 | end 126 | if @rows_processed == 0 127 | raise ExcelImportException.new( I18n.t('plugins.aspace-import-excel.error.no_data')) 128 | end 129 | rescue Exception => e 130 | if e.is_a?( ExcelImportException) || e.is_a?( StopExcelImportException) 131 | @report.add_terminal_error(I18n.t('plugins.aspace-import-excel.error.excel', :errs => e.message), @counter) 132 | elsif e.is_a?(StopIteration) && @headers.nil? 133 | @report.add_terminal_error(I18n.t('plugins.aspace-import-excel.error.no_header'), @counter) 134 | else # something else went wrong 135 | @report.add_terminal_error(I18n.t('plugins.aspace-import-excel.error.system', :msg => e.message), @counter) 136 | Rails.logger.error("UNEXPECTED EXCEPTION on load_ss!") 137 | Rails.logger.debug(e.pretty_inspect) 138 | Rails.logger.error(e.message) 139 | Rails.logger.error( e.backtrace.pretty_inspect) 140 | end 141 | @report.end_row 142 | return render_aspace_partial :status => 400, :partial => "resources/bulk_response", :locals => {:rid => params[:rid], 143 | :report => @report, :do_load => @digital_load} 144 | end 145 | move_archival_objects if @need_to_move 146 | @report.end_row 147 | return render_aspace_partial :partial => "resources/bulk_response", :locals => {:rid => params[:rid], :report => @report, 148 | :do_load => @digital_load} 149 | end 150 | 151 | private 152 | 153 | # save the archival object, then revive it 154 | def ao_save(ao) 155 | revived = nil 156 | begin 157 | id = ao.save 158 | revived = JSONModel(:archival_object).find(id) 159 | rescue ValidationException => ve 160 | raise ExcelImportException.new(I18n.t('plugins.aspace-import-excel.error.ao_validation', :err=>ve.errors)) 161 | rescue Exception => e 162 | Rails.logger.error("UNEXPECTED ao save error: #{e.message}") 163 | Rails.logger.error(e.pretty_inspect) 164 | Rails.logger.error(ASUtils.jsonmodels_to_hashes(ao).pretty_inspect) if ao 165 | raise e 166 | end 167 | revived 168 | end 169 | 170 | # required fields for a digital object row: ead match, ao_ref_id and at least one of digital_object_link, thumbnail 171 | def check_do_row 172 | err_arr = [] 173 | begin 174 | err_arr.push I18n.t('plugins.aspace-import-excel.error.ref_id_miss') if @row_hash['ao_ref_id'].blank? 175 | obj_link = @row_hash['digital_object_link'] 176 | thumb = @row_hash['thumbnail'] || @row_hash['Thumbnail'] 177 | err_arr.push I18n.t('plugins.aspace-import-excel.error.dig_info_miss') if @row_hash['digital_object_link'].blank? && thumb.blank? 178 | end 179 | v = @row_hash['publish'] 180 | v = v.strip if !v.blank? 181 | @row_hash['publish'] = (v == '1') 182 | err_arr.join('; ') 183 | end 184 | 185 | # look for all the required fields to make sure they are legit 186 | # strip all the strings and turn publish and restrictions_flaginto true/false 187 | def check_row 188 | err_arr = [] 189 | begin 190 | # we'll check hierarchical level first, in case there was a parent that didn't get created 191 | hier = @row_hash['hierarchy'] 192 | if !hier 193 | err_arr.push I18n.t('plugins.aspace-import-excel.error.hier_miss') 194 | else 195 | hier = hier.to_i 196 | # we bail if the parent wasn't created! 197 | return I18n.t('plugins.aspace-import-excel.error.hier_below_error_level') if (@error_level && hier > @error_level) 198 | err_arr.push I18n.t('plugins.aspace-import-excel.error.hier_zero') if hier < 1 199 | # going from a 1 to a 3, for example 200 | if (hier - 1) > @hier 201 | err_arr.push I18n.t('plugins.aspace-import-excel.error.hier_wrong') 202 | if @hier == 0 203 | err_arr.push I18n.t('plugins.aspace-import-excel.error.hier_wrong_resource') 204 | raise StopExcelImportException.new(err_arr.join(';')) 205 | end 206 | end 207 | @hier = hier 208 | end 209 | missing_title = @row_hash['title'].blank? 210 | #date stuff: if already missing the title, we have to make sure the date label is valid 211 | missing_date = [@row_hash['begin'],@row_hash['end'],@row_hash['expression']].reject(&:blank?).empty? 212 | if !missing_date 213 | begin 214 | label = @date_labels.value((@row_hash['dates_label'] || 'creation')) 215 | rescue Exception => e 216 | err_arr.push I18n.t('plugins.aspace-import-excel.error.invalid_date_label', :what => e.message) if missing_title 217 | missing_date = true 218 | end 219 | end 220 | err_arr.push I18n.t('plugins.aspace-import-excel.error.title_and_date') if (missing_title && missing_date) 221 | # tree hierachy 222 | begin 223 | level = @archival_levels.value(@row_hash['level']) 224 | rescue Exception => e 225 | err_arr.push I18n.t('plugins.aspace-import-excel.error.level') 226 | end 227 | rescue StopExcelImportException => se 228 | raise 229 | rescue Exception => e 230 | Rails.logger.error(["UNEXPLAINED EXCEPTION on check row", e.message, e.backtrace, @row_hash].pretty_inspect) 231 | end 232 | if err_arr.blank? 233 | @row_hash.each do |k, v| 234 | @row_hash[k] = v.strip if !v.blank? 235 | if k == 'publish' || k == 'restrictions_flag' 236 | @row_hash[k] = (v == '1') 237 | end 238 | end 239 | end 240 | err_arr.join('; ') 241 | end 242 | 243 | def check_for_code_dups 244 | test = {} 245 | dups = "" 246 | @headers.each do |head| 247 | if test[head] 248 | dups = "#{dups} #{head}," 249 | else 250 | test[head] = true 251 | end 252 | end 253 | if !dups.blank? 254 | raise Exception.new( I18n.t('plugins.aspace-import-excel.error.duplicates', :codes => dups)) 255 | end 256 | return (dups.blank?) 257 | end 258 | 259 | # create an archival_object 260 | def create_archival_object(parent_uri) 261 | ao = JSONModel(:archival_object).new._always_valid! 262 | ao.title = @row_hash['title'] if @row_hash['title'] 263 | ao.dates = create_dates 264 | #because the date may have been invalid, we should check if there's a title, otherwise bail 265 | if ao.title.blank? && ao.dates.blank? 266 | raise ExcelImportException.new(I18n.t('plugins.aspace-import-excel.error.title_and_date')) 267 | end 268 | ao.resource = {'ref' => @resource['uri']} 269 | ao.component_id = @row_hash['unit_id'] if @row_hash['unit_id'] 270 | ao.repository_processing_note = @row_hash['processing_note'] if @row_hash['processing_note'] 271 | ao.level = @archival_levels.value(@row_hash['level']) 272 | ao.other_level = @row_hash['other_level'] || 'unspecified' if ao.level == 'otherlevel' 273 | ao.publish = @row_hash['publish'] 274 | ao.restrictions_apply = @row_hash['restrictions_flag'] 275 | ao.parent = {'ref' => parent_uri} unless parent_uri.blank? 276 | begin 277 | ao.extents = create_extents 278 | rescue Exception => e 279 | @report.add_errors(e.message) 280 | end 281 | errs = handle_notes(ao) 282 | @report.add_errors(errs) if !errs.blank? 283 | # we have to save the ao for the display_string 284 | begin 285 | #Rails.logger.debug(ao.pretty_inspect) 286 | ao = ao_save(ao) 287 | rescue Exception => e 288 | msg = I18n.t('plugins.aspace-import-excel.error.initial_save_error', :title =>ao.title, :msg => e.message) 289 | raise ExcelImportException.new(msg) 290 | end 291 | ao.instances = create_top_container_instances 292 | if (dig_instance = DigitalObjectHandler.create(@row_hash, ao, @report)) 293 | ao.instances ||= [] 294 | ao.instances << dig_instance 295 | end 296 | subjs = process_subjects 297 | subjs.each {|subj| ao.subjects.push({'ref' => subj.uri})} unless subjs.blank? 298 | links = process_agents 299 | ao.linked_agents = links 300 | ao 301 | end 302 | 303 | def create_dates 304 | dates = [] 305 | cntr = 1 306 | substr = '' 307 | until [@row_hash["begin#{substr}"],@row_hash["end#{substr}"],@row_hash["expression#{substr}"]].reject(&:blank?).empty? 308 | date = create_date(substr) 309 | dates << date if date 310 | cntr +=1 311 | substr = "_#{cntr}" 312 | end 313 | return dates 314 | end 315 | 316 | def create_date(substr) 317 | date_str = "(Date: type:#{@row_hash["date_type#{substr}"]}, label: #{@row_hash["dates_label#{substr}"]}, begin: #{@row_hash["begin#{substr}"]}, end: #{@row_hash["end#{substr}"]}, expression: #{@row_hash["expression#{substr}"]})" 318 | date_type = 'inclusive' 319 | begin 320 | date_type = @date_types.value(@row_hash["date_type#{substr}"] || 'inclusive') 321 | rescue Exception => e 322 | @report.add_errors(I18n.t('plugins.aspace-import-excel.error.date_type', :what => @row_hash["date_type#{substr}"],:date_str => date_str )) 323 | end 324 | begin 325 | date = { 'date_type' => date_type, 326 | 'label' => @date_labels.value((@row_hash["dates_label#{substr}"] || 'creation')) } 327 | rescue Exception => e 328 | @report.add_errors(I18n.t('plugins.aspace-import-excel.error.date_label', 329 | :what => @row_hash["dates_label#{substr}"],:date_str => date_str)) 330 | #don't bother processsing if the label mis-matches 331 | return nil 332 | end 333 | 334 | if @row_hash["date_certainty#{substr}"] 335 | begin 336 | date['certainty'] = @date_certainty.value(@row_hash["date_certainty#{substr}"]) 337 | rescue Exception => e 338 | @report.add_errors(I18n.t('plugins.aspace-import-excel.error.certainty', :what => e.message,:date_str => date_str)) 339 | end 340 | end 341 | %w(begin end expression).each do |w| 342 | date[w] = @row_hash["#{w}#{substr}"] if @row_hash["#{w}#{substr}"] 343 | end 344 | invalids = JSONModel::Validations.check_date(date) 345 | unless invalids.blank? 346 | err_msg = "" 347 | invalids.each do |inv| 348 | err_msg << " #{inv[0]}: #{inv[1]}" 349 | end 350 | @report.add_errors(I18n.t('plugins.aspace-import-excel.error.invalid_date', :what => err_msg,:date_str => date_str)) 351 | return nil 352 | end 353 | if date_type == "single" && !date["end"].blank? 354 | @report.add_errors(I18n.t('plugins.aspace-import-excel.warn.single_date_end', :date_str => date_str)) 355 | end 356 | d = JSONModel(:date).new(date) 357 | #[d] 358 | end 359 | 360 | def create_extent(substr) 361 | ext_str = "Extent: #{@row_hash["portion#{substr}"] || 'whole'} #{@row_hash["number#{substr}"]} #{@row_hash["extent_type#{substr}"]} #{@row_hash["container_summary#{substr}"]} #{@row_hash["physical_details#{substr}"]} #{@row_hash["dimensions#{substr}"]}" 362 | begin 363 | extent = {'portion' => @extent_portions.value(@row_hash["portion#{substr}"] || 'whole'), 364 | 'extent_type' => @extent_types.value((@row_hash["extent_type#{substr}"]))} 365 | %w(number container_summary physical_details dimensions).each do |w| 366 | extent[w] = @row_hash["#{w}#{substr}"] || nil 367 | end 368 | ex = JSONModel(:extent).new(extent) 369 | if UpdatesUtils.test_exceptions(ex, "Extent") 370 | return ex 371 | end 372 | rescue Exception => e 373 | @report.add_errors(I18n.t('plugins.aspace-import-excel.error.extent_validation', :msg => e.message, :ext => ext_str)) 374 | return nil 375 | end 376 | end 377 | def create_extents 378 | extents = [] 379 | cntr = 1 380 | substr = '' 381 | until @row_hash["number#{substr}"].blank? && @row_hash["extent_type#{substr}"].blank? 382 | extent = create_extent(substr) 383 | extents << extent if extent 384 | cntr +=1 385 | substr = "_#{cntr}" 386 | end 387 | return extents 388 | end 389 | def create_top_container_instances 390 | instances = [] 391 | cntr = 1 392 | substr = '' 393 | until @row_hash["cont_instance_type#{substr}"].blank? && @row_hash["type_1#{substr}"].blank? && @row_hash["barcode#{substr}"].blank? 394 | begin 395 | instance = ContainerInstanceHandler.create_container_instance(@row_hash, substr, @resource['uri'], @report) 396 | rescue Exception => e 397 | @report.add_errors(I18n.t('plugins.aspace-import-excel.error.no_tc', :num=> cntr,:why=>e.message)) 398 | instance = nil 399 | end 400 | cntr +=1 401 | substr = "_#{cntr}" 402 | instances << instance if instance 403 | end 404 | return instances 405 | end 406 | 407 | def fetch_archival_object(ref_id) 408 | ao = nil 409 | response = JSONModel::HTTP::get_json(URI(@find_uri),{"ref_id[]" => ref_id, "resolve[]" => "archival_objects"}) 410 | # Rails.logger.info("response: #{response} for ref_id: #{ref_id}") 411 | unless response.blank? || response["archival_objects"].blank? 412 | aos = [] 413 | response["archival_objects"].each { |ao| 414 | Rails.logger.info "aodig: #{ao.dig('_resolved','resource','ref')}" 415 | aos.append(ao["ref"]) if ao.dig('_resolved','resource','ref') == @resource_ref 416 | } 417 | #Rails.logger.info "length: #{aos.length}" 418 | #Rails.logger.info {aos.pretty_inspect} 419 | if aos.length == 1 420 | parsed = JSONModel.parse_reference(aos[0]) 421 | begin 422 | ao = JSONModel(:archival_object).find(parsed[:id], :repo_id => @repo_id) 423 | # Rails.logger.info "ao JSONMODEL" 424 | # Rails.logger.info {ao.pretty_inspect} 425 | rescue Exception => e 426 | Rails.logger.info {e.pretty_inspect} 427 | end 428 | end 429 | end 430 | ao 431 | end 432 | 433 | def handle_notes(ao) 434 | publish = ao.publish 435 | errs = [] 436 | notes_keys = @row_hash.keys.grep(/^n_/) 437 | notes_keys.each do |key| 438 | unless @row_hash[key].blank? 439 | content = @row_hash[key] 440 | type = key.match(/n_(.+)$/)[1] 441 | note_type = @note_types[type] 442 | note = JSONModel(note_type[:target]).new 443 | pubnote = @row_hash["p_#{type}"] 444 | if pubnote.blank? 445 | pubnote = publish 446 | else 447 | pubnote = (pubnote == '1') 448 | end 449 | note.publish = pubnote 450 | note.type = note_type[:value] 451 | begin 452 | wellformed(content) 453 | # if the target is multipart, then the data goes in a JSONMODEL(:note_text).content;, which is pushed to the note.subnote array; otherwise it's just pushed to the note.content array 454 | if note_type[:target] == :note_multipart 455 | inner_note = JSONModel(:note_text).new 456 | inner_note.content = content 457 | inner_note.publish = pubnote 458 | note.subnotes.push inner_note 459 | else 460 | note.content.push content 461 | end 462 | ao.notes.push note 463 | rescue Exception => e 464 | errs.push(I18n.t('plugins.aspace-import-excel.error.bad_note', :type => note_type[:value] , :msg => CGI::escapeHTML( e.message))) 465 | end 466 | end 467 | end 468 | errs 469 | end 470 | 471 | # this refreshes the controlled list enumerations, which may have changed since the last import 472 | def initialize_handler_enums 473 | ContainerInstanceHandler.renew 474 | DigitalObjectHandler.renew 475 | SubjectHandler.renew 476 | AgentHandler.renew 477 | end 478 | 479 | # set up all the @ variables (except for @header) 480 | def initialize_info(params) 481 | dispatched_file = params[:file] 482 | @orig_filename = dispatched_file.original_filename 483 | @report.set_file_name(@orig_filename) 484 | initialize_handler_enums 485 | @resource = Resource.find(params[:rid]) 486 | @repository = @resource['repository']['ref'] 487 | @hier = 1 488 | # ingest archival objects needs this 489 | unless @digital_load 490 | @note_types = note_types_for(:archival_object) 491 | tree = JSONModel(:resource_tree).find(nil, :resource_id => params[:rid]).to_hash 492 | @ao = nil 493 | aoid = params[:aoid] 494 | @resource_level = aoid.blank? 495 | @first_one = false # to determine whether we need to worry about positioning 496 | if @resource_level 497 | @parents.set_uri(0, nil) 498 | @hier = 0 499 | else 500 | @ao = JSONModel(:archival_object).find(aoid, find_opts ) 501 | @start_position = @ao.position 502 | parent = @ao.parent # we need this for sibling/child disabiguation later on 503 | @parents.set_uri(0, (parent ? ASUtils.jsonmodels_to_hashes(parent)['ref'] : nil)) 504 | @parents.set_uri(1, @ao.uri) 505 | @first_one = true 506 | end 507 | end 508 | 509 | @input_file = dispatched_file.tempfile 510 | @counter = 0 511 | @rows_processed = 0 512 | @error_rows = 0 513 | workbook = RubyXL::Parser.parse(@input_file) 514 | sheet = workbook[0] 515 | rows = sheet.enum_for(:each) 516 | end 517 | 518 | def move_archival_objects 519 | unless @first_level_aos.empty? 520 | uri = (@ao && @ao.parent) ? @ao.parent['ref'] : @resource.uri 521 | response = JSONModel::HTTP.post_form("#{uri}/accept_children", 522 | "children[]" => @first_level_aos, 523 | "position" => @start_position + 1) 524 | unless response.code == '200' 525 | Rails.logger.error( "UNEXPECTED BAD MOVE on #{uri}/accept_children! #{response.code}") 526 | @report.errors(I18n.t('plugins.aspace-import-excel.error.no_move', :code => response.code)) 527 | end 528 | end 529 | end 530 | 531 | def process_agents 532 | agent_links = [] 533 | %w(people corporate_entities families).each do |type| 534 | num = 1 535 | while true 536 | id_key = "#{type}_agent_record_id_#{num}" 537 | header_key = "#{type}_agent_header_#{num}" 538 | break if @row_hash[id_key].blank? && @row_hash[header_key].blank? 539 | link = nil 540 | begin 541 | link = AgentHandler.get_or_create(@row_hash, type, num.to_s, @resource['uri'], @report) 542 | agent_links.push link if link 543 | rescue ExcelImportException => e 544 | @report.add_errors(e.message) 545 | end 546 | num += 1 547 | end 548 | end 549 | agent_links 550 | end 551 | 552 | 553 | def process_do_row(params) 554 | ret_str = resource_match 555 | # mismatch of resource stops all other processing 556 | if ret_str.blank? 557 | ret_str = check_do_row 558 | end 559 | raise ExcelImportException.new( I18n.t('plugins.aspace-import-excel.row_error', :row => @counter, :errs => ret_str )) if !ret_str.blank? 560 | begin 561 | ao = fetch_archival_object(@row_hash['ao_ref_id']) 562 | raise ExcelImportException.new( I18n.t('plugins.aspace-import-excel.row_error', :row => @counter, :errs => I18n.t('plugins.aspace-import-excel.ref_id_notfound', :refid => @row_hash['ao_ref_id']))) if ao == nil 563 | @report.add_archival_object(ao) 564 | if ao.instances 565 | digs = [] 566 | ao.instances.each {|instance| digs.append(1) if instance["instance_type"] == "digital_object" } 567 | unless digs.blank? 568 | raise ExcelImportException.new( I18n.t('plugins.aspace-import-excel.row_error', :row => @counter, :errs => I18n.t('plugins.aspace-import-excel.error.has_dig_obj'))) 569 | end 570 | end 571 | if (dig_instance = DigitalObjectHandler.create(@row_hash, ao, @report)) 572 | ao.instances ||= [] 573 | ao.instances << dig_instance 574 | begin 575 | ao = ao_save(ao) 576 | @report.add_info(I18n.t('plugins.aspace-import-excel.dig_assoc')) 577 | rescue ExcelImportException => ee 578 | @report.add_errors(I18n.t('plugins.aspace-import-excel.error.dig_unassoc', :msg =>ee.message)) 579 | end 580 | end 581 | end 582 | 583 | end 584 | 585 | def process_row 586 | ret_str = resource_match 587 | # mismatch of resource stops all other processing 588 | if ret_str.blank? 589 | ret_str = check_row 590 | end 591 | raise ExcelImportException.new( I18n.t('plugins.aspace-import-excel.row_error', :row => @counter, :errs => ret_str )) if !ret_str.blank? 592 | parent_uri = @parents.parent_for(@row_hash['hierarchy'].to_i) 593 | begin 594 | ao = create_archival_object(parent_uri) 595 | ao = ao_save(ao) 596 | rescue JSONModel::ValidationException => ve 597 | # ao won't have been created 598 | Rails.logger.error("VALIDATION ERROR ON SECOND SAVE: #{ve.message}") 599 | msg = I18n.t('plugins.aspace-import-excel.error.second_save_error', :what => ve.errors, :title => ao.title, :pos => ao.position) 600 | @report.add_errors(msg) 601 | rescue Exception => e 602 | Rails.logger.error("UNEXPECTED ON SECOND SAVE#{e.message}") 603 | Rails.logger.error(e.backtrace.pretty_inspect) 604 | Rails.logger.error( ASUtils.jsonmodels_to_hashes(ao).pretty_inspect) 605 | raise ExcelImportException.new(e.message) 606 | end 607 | @report.add_archival_object(ao) if !ao.blank? 608 | @parents.set_uri(@hier, ao.uri) 609 | @created_ao_refs.push ao.uri 610 | if @hier == 1 611 | @first_level_aos.push ao.uri 612 | if @first_one && @start_position 613 | @need_to_move = (ao.position - @start_position) > 1 614 | @first_one = false 615 | end 616 | end 617 | end 618 | 619 | def process_subjects 620 | ret_subjs = [] 621 | (1..10).each do |num| 622 | unless @row_hash["subject_#{num}_record_id"].blank? && @row_hash["subject_#{num}_term"].blank? 623 | subj = nil 624 | begin 625 | subj = SubjectHandler.get_or_create(@row_hash, num, @repository.split('/')[2], @report) 626 | ret_subjs.push subj if subj 627 | rescue ExcelImportException => e 628 | @report.add_errors(e.message) 629 | end 630 | end 631 | end 632 | ret_subjs 633 | end 634 | 635 | # make sure that the resource ead id from the form matches that in the spreadsheet 636 | # throws an exception if the designated resource ead doesn't match the spreadsheet row ead 637 | def resource_match 638 | ret_str = '' 639 | ret_str = I18n.t('plugins.aspace-import-excel.error.res_ead') if @resource['ead_id'].blank? 640 | ret_str = ' ' + I18n.t('plugins.aspace-import-excel.error.row_ead') if @row_hash['ead'].blank? 641 | if ret_str.blank? 642 | ret_str = I18n.t('plugins.aspace-import-excel.error.ead_mismatch', :res_ead => @resource['ead_id'], :row_ead => @row_hash['ead']) if @resource['ead_id'] != @row_hash['ead'] 643 | end 644 | ret_str.blank? ? nil : ret_str 645 | end 646 | 647 | def find_subject(subject,source, ext_id) 648 | #title:subject AND primary_type:subject AND source:#{source} AND external_id:#{ext_id} 649 | end 650 | 651 | def find_agent(primary_name, rest_name, type, source, ext_id) 652 | #title: #{primary_name}, #{rest_name} AND primary_type:agent_#{type} AND source:#{source} AND external_id:#{ext_id} 653 | end 654 | 655 | # use nokogiri if there seems to be an XML element (or element closure); allow exceptions to bubble up 656 | def wellformed(note) 657 | if note.match("") 658 | frag = Nokogiri::XML("#{note}") {|config| config.strict} 659 | end 660 | end 661 | 662 | 663 | def row_values(row) 664 | (1...row.size).map {|i| (row[i] && row[i].value) ? row[i].value.to_s.strip : nil} 665 | end 666 | end 667 | -------------------------------------------------------------------------------- /frontend/assets/javascripts/utils.js: -------------------------------------------------------------------------------- 1 | //= require trimpath-template-1.0.38 2 | //= require bootstrap-datepicker 3 | //= require bootstrap-combobox 4 | 5 | var AS = {}; 6 | 7 | // initialise ajax modal 8 | $(function() { 9 | AS.openAjaxModal = function(href) { 10 | $("body").append(''); 11 | 12 | var $modal = $("#tempAjaxModal"); 13 | 14 | $.ajax({ 15 | url: href, 16 | async: false, 17 | success: function(html) { 18 | if ($(html).hasClass("modal")) { 19 | $modal.remove(); 20 | $modal = $(html); 21 | 22 | $("body").append($modal); 23 | } else { 24 | $modal.append(html); 25 | } 26 | 27 | $modal.on("shown.bs.modal",function() { 28 | $modal.find("input[type!=hidden]:first").focus(); 29 | }).on("hidden.bs.modal", function() { 30 | $modal.remove(); 31 | }); 32 | 33 | $modal.modal('show'); 34 | } 35 | }); 36 | 37 | return $modal; 38 | }; 39 | 40 | $("body").on("click", "[data-toggle=modal-ajax]", function(e) { 41 | e.preventDefault(); 42 | AS.openAjaxModal($(this).attr("href")); 43 | }); 44 | }); 45 | 46 | 47 | // add four part indentifier behaviour 48 | $(function() { 49 | var initIdentifierFields = function(scope) { 50 | scope = scope || $(document.body); 51 | $("form:not(.navbar-form) .identifier-fields:not(.initialised)", scope).on("keyup", ":input", function(event) { 52 | $(this).addClass("initialised"); 53 | var currentInputIndex = $(event.target).index(); 54 | $(event.target).parents(".identifier-fields:first").find(":input:eq("+(currentInputIndex+1)+")").each(function() { 55 | if ($(event.target).val().length === 0 && $(this).val().length === 0) { 56 | $(this).attr("disabled", "disabled"); 57 | } else { 58 | $(this).removeAttr("disabled"); 59 | } 60 | }); 61 | }); 62 | } 63 | $(document).bind("loadedrecordform.aspace", function(event, $container) { 64 | initIdentifierFields($container); 65 | }); 66 | initIdentifierFields(); 67 | }); 68 | 69 | 70 | // sidebar action 71 | $(function() { 72 | 73 | var getSubMenuHTML = function(numberOfRecords) { 74 | if ( numberOfRecords < 1 ) { 75 | return ''; 76 | } else { 77 | return $("" + numberOfRecords + "") 78 | } 79 | }; 80 | 81 | var refreshSidebarSubMenus = function() { 82 | if ($(".readonly-context:first").length > 0) { 83 | // this could be a read only page... so don't 84 | // show the sub record bits 85 | return; 86 | } 87 | $(".nav-list-record-count").remove(); 88 | $("#archivesSpaceSidebar .as-nav-list > li").each(function() { 89 | var $nav = $(this); 90 | var $link = $("a", $nav); 91 | var $section = $($link.attr("href")); 92 | var $items = $(".subrecord-form-list:first > li", $section); 93 | 94 | var $submenu = getSubMenuHTML($items.length); 95 | $link.append($submenu); 96 | }); 97 | }; 98 | 99 | var initSidebar = function() { 100 | $("#archivesSpaceSidebar .as-nav-list:not(.initialised)").each(function() { 101 | $(this).affix({ 102 | offset: { 103 | top: function() { 104 | return $("#archivesSpaceSidebar").offset().top; 105 | }, 106 | bottom: 100 107 | } 108 | }); 109 | 110 | $(this).addClass("initialised"); 111 | }); 112 | refreshSidebarSubMenus(); 113 | }; 114 | 115 | initSidebar(); 116 | 117 | $(document).bind("loadedrecordform.aspace", function(event, $container) { 118 | initSidebar(); 119 | }); 120 | 121 | $(document).bind("subrecordcreated.aspace subrecorddeleted.aspace formerrorready.aspace", function() { 122 | if ($("#archivesSpaceSidebar .nav-list.initialised").length > 0) { 123 | refreshSidebarSubMenus(); 124 | // refresh scrollspy offsets.. as they are probably wrong now that things have changed in the form 125 | $('[data-spy="scroll"]').scrollspy('refresh'); 126 | } 127 | }); 128 | $(document).bind("resize.tree", function() { 129 | // refresh scrollspy offsets.. as they are probably wrong now that things have changed in the form 130 | $('[data-spy="scroll"]').scrollspy('refresh'); 131 | }); 132 | 133 | }); 134 | 135 | // date fields and datepicker initialisation 136 | $.fn.combobox.defaults.template = '
'; 137 | $(function() { 138 | var initDateFields = function(scope) { 139 | scope = scope || $(document.body); 140 | $(".date-field:not(.initialised)", scope).each(function() { 141 | var $dateInput = $(this); 142 | $dateInput.wrap("
"); 143 | $dateInput.addClass("initialised"); 144 | 145 | var $addon = "" 146 | $dateInput.after($addon); 147 | 148 | $dateInput.parent(".date").datepicker($dateInput.data()); 149 | }); 150 | }; 151 | initDateFields(); 152 | $(document).bind("loadedrecordform.aspace", function(event, $container) { 153 | initDateFields($container); 154 | }); 155 | $(document).bind("subrecordcreated.aspace", function(event, object_name, subform) { 156 | initDateFields(subform); 157 | }); 158 | $(document).bind("initdatefields.aspace", function(event, container) { 159 | initDateFields(container); 160 | }); 161 | }); 162 | 163 | 164 | // select fields and combobox initialisation 165 | $(function() { 166 | var initComboboxFields = function(scope) { 167 | scope = scope || $(document.body); 168 | $("select[data-combobox]:not(.initialised)", scope).each(function() { 169 | var $selectInput = $(this); 170 | $selectInput.data("combobox", null).addClass("initialised"); 171 | $selectInput.combobox(); 172 | }); 173 | }; 174 | initComboboxFields(); 175 | 176 | $(document).bind("loadedrecordform.aspace", function(event, $container) { 177 | initComboboxFields($container); 178 | }); 179 | $(document).bind("subrecordcreated.aspace", function(event, object_name, subform) { 180 | initComboboxFields(subform); 181 | }); 182 | $(document).bind("initcomboboxfields.aspace", function(event, container) { 183 | initComboboxFields(container); 184 | }); 185 | }); 186 | 187 | 188 | // any element with a popover! 189 | $(function() { 190 | var popoverOptions = { 191 | delay: {show: 0, hide: 200} // if the popover contains a link, allow a few moments for that click to count 192 | }; 193 | 194 | var initPopovers = function(scope) { 195 | scope = scope || $(document.body); 196 | $(".has-popover:not(.initialised)", scope) 197 | .popover(popoverOptions) 198 | .click(function(e) { 199 | e.preventDefault(); 200 | }).addClass("initialised"); 201 | }; 202 | initPopovers(); 203 | $(document).bind("loadedrecordform.aspace init.popovers", function(event, $container) { 204 | initPopovers($container); 205 | }); 206 | $(document).bind("subrecordcreated.aspace", function(event, object_name, subform) { 207 | initPopovers(subform); 208 | }); 209 | }); 210 | 211 | 212 | // any element with a tooltip! 213 | $(function() { 214 | var initTooltips = function(scope) { 215 | scope = scope || $(document.body); 216 | $(".has-tooltip:not(.initialised)", scope).each(function() { 217 | var $this = $(this); 218 | $this.tooltip().addClass("initialised"); 219 | 220 | // for manual ArchiveSpace help tooltips 221 | if ($this.data("trigger") === "manual" && ($this.is("label.control-label") || $this.is(".subrecord-form-heading-label"))) { 222 | var openedViaClick = false; 223 | var showTimeout, hideTimeout; 224 | 225 | var onMouseEnter = function() { 226 | if (openedViaClick) return; 227 | 228 | clearTimeout(hideTimeout); 229 | showTimeout = setTimeout(function() { 230 | showTimeout = null; 231 | $this.tooltip("show"); 232 | }, $this.data("delay") || 500); 233 | $this.off("mouseleave").on("mouseleave", onMouseLeave); 234 | }; 235 | 236 | var onMouseLeave = function() { 237 | if (showTimeout) { 238 | clearTimeout(showTimeout); 239 | } else { 240 | hideTimeout = setTimeout(function() { 241 | $this.tooltip("hide"); 242 | }, 100); 243 | } 244 | }; 245 | 246 | var onClick = function() { 247 | clearTimeout(showTimeout); 248 | 249 | if (openedViaClick) { 250 | $this.tooltip("hide"); 251 | openedViaClick = false; 252 | return; 253 | } 254 | 255 | $this.off("mouseleave"); 256 | 257 | $this.tooltip("show"); 258 | $(".tooltip-inner", $this.data("bs.tooltip").$tip).prepend(''); 259 | $(".tooltip-close", $this.data("bs.tooltip").$tip).click(function() { 260 | $this.trigger("click"); 261 | }); 262 | openedViaClick = true; 263 | }; 264 | 265 | // bind event callbacks 266 | $this.bind("mouseenter", onMouseEnter).click(onClick); 267 | } 268 | }); 269 | }; 270 | initTooltips(); 271 | $(document).bind("loadedrecordform.aspace", function(event, $container) { 272 | initTooltips($container); 273 | }); 274 | $(document).bind("subrecordcreated.aspace", function(event, object_name, subform) { 275 | initTooltips(subform); 276 | }); 277 | 278 | $(document).bind("shown.bs.modal", function(events) { 279 | $(".modal-content").delegate($(".has-tooltip"), "mouseenter", function() { 280 | initTooltips($( this )); 281 | $("a.has-tooltip", $( this )).on("click", function() { 282 | window.open($( this ).attr('href')); 283 | }); 284 | }); 285 | }); 286 | }); 287 | 288 | 289 | // allow click of a submenu link 290 | $(function() { 291 | var initSubmenuLink = function(scope) { 292 | scope = scope || $(document.body); 293 | $(scope).on("click", ".dropdown-submenu > a[href*='javascript:void']:not(.initialised)", function(e) { 294 | e.preventDefault(); 295 | e.stopImmediatePropagation(); 296 | $(this).focus(); 297 | }).addClass("initialised"); 298 | }; 299 | initSubmenuLink(); 300 | $(document).bind("loadedrecordform.aspace", function(event, $container) { 301 | initSubmenuLink($container); 302 | }); 303 | $(document).bind("subrecordcreated.aspace init.popovers", function(event, object_name, subform) { 304 | initSubmenuLink(subform) 305 | }); 306 | }); 307 | 308 | 309 | 310 | 311 | AS.templateCache = []; 312 | AS.renderTemplate = function(templateId, data, cb) { 313 | 314 | if (!AS.templateCache[templateId]) { 315 | var templateNode = $("#"+templateId).get(0); 316 | if (templateNode) { 317 | var firstNode = templateNode.firstChild; 318 | var template = null; 319 | // Check whether the template is wrapped in 320 | if (firstNode && (firstNode.nodeType === 8 || firstNode.nodeType === 4)) { 321 | template = firstNode.data.toString(); 322 | } else { 323 | template = templateNode.innerHTML.toString(); 324 | } 325 | // Parse the template through TrimPath and add the parsed template to the template cache 326 | AS.templateCache[templateId] = TrimPath.parseTemplate(template, templateId); 327 | } 328 | } 329 | return AS.templateCache[templateId].process(data); 330 | }; 331 | 332 | 333 | AS.quickTemplate = function(templateHTML, data) { 334 | return TrimPath.parseTemplate(templateHTML).process(data); 335 | }; 336 | 337 | AS.stripHTML = function(string) { 338 | if (string === null || string === undefined) { 339 | return ""; 340 | } 341 | var rex = /(<([^>]+)>)/ig; 342 | return $.trim(string.replace(rex, "")); 343 | }; 344 | 345 | AS.encodeForAttribute = function(string) { 346 | if (string === null || string === undefined) { 347 | return ""; 348 | } 349 | return $.trim(string.replace(/"/g, """).replace(/(\r\n|\n|\r)/gm,"")); 350 | }; 351 | 352 | AS.openQuickModal = function(title, message) { 353 | AS.openCustomModal("quickModal", title, AS.renderTemplate("modal_quick_template", {message: message})); 354 | }; 355 | 356 | 357 | /* AS.openCustomModal 358 | * id : String - id of the modal element 359 | * title : String - to be applied as the modal header 360 | * contents : String/HTML - the contents of the modal 361 | * size : String/false - 'full'-98% of screen, 'large' - larger modal, 'container' - match the container width, false-standard modal size 362 | * modalOpts : object - any twitter bootstrap options to pass on the modal dialog upon init. 363 | * initiatedBy : Element - the link/button that initiated the modal. This element will be focused again upon close. 364 | */ 365 | AS.openCustomModal = function(id, title, contents, modalSize, modalOpts, initiatedBy) { 366 | var templateData = { 367 | id:id, 368 | title:title, 369 | content: "", 370 | } 371 | 372 | // phase out the class on .modal in favor of .modal-dialog 373 | if (modalSize === 'large') { 374 | templateData.dialogClass = 'modal-lg'; 375 | templateData.fill = false; 376 | } else if (modalSize == 'full') { 377 | templateData.dialogClass = 'modal-jumbo'; 378 | templateData.fill = false; 379 | } else { 380 | templateData.fill = modalSize; 381 | templateData.dialogClass = false; 382 | } 383 | 384 | $("body").append(AS.renderTemplate("modal_custom_template", templateData)); 385 | var $modal = $("#"+id); 386 | $modal.find('.modal-content').append(contents); 387 | $modal.on("hidden.bs.modal", function() { 388 | $modal.remove(); 389 | $(window).unbind("resize", resizeModal); 390 | 391 | if (initiatedBy) { 392 | $(initiatedBy).focus(); 393 | } 394 | }); 395 | 396 | var resizeModal = function() { 397 | var height; 398 | if (modalSize === 'full' || 'large') { 399 | height = $(window).height() - ($(window).height() * 0.03); 400 | } else { 401 | height = $(window).height() - ($(window).height() * 0.2); 402 | } 403 | 404 | $modal.height(height); // -20% for 10% top and bottom margins 405 | var modalBodyHeight = $modal.height() - $(".modal-header", $modal).outerHeight() - $(".modal-footer", $modal).outerHeight() - 95; 406 | $(".modal-body", $modal).height(modalBodyHeight); 407 | // $modal.css("marginLeft", -$modal.width() / 2); 408 | } 409 | 410 | if (modalSize) { 411 | $modal.on("shown resize", resizeModal); 412 | $(window).resize(resizeModal); 413 | } 414 | 415 | if (modalOpts) { 416 | $modal.modal(modalOpts); 417 | } 418 | 419 | // reset the tab index within the modal 420 | $modal.attr("tabindex", 0).focus(); 421 | 422 | $modal.modal('show'); 423 | 424 | $(".linker:not(.initialised)", $modal).linker(); 425 | 426 | return $modal; 427 | }; 428 | 429 | 430 | $.fn.serializeObject = function() { 431 | var o = {}; 432 | 433 | $(this).each(function() { 434 | 435 | if ($(this).is("form")) { 436 | var a = $(this).serializeArray(); 437 | $.each(a, function() { 438 | if (o[this.name] !== undefined) { 439 | if (!o[this.name].push) { 440 | o[this.name] = [o[this.name]]; 441 | } 442 | o[this.name].push(this.value || ''); 443 | } else { 444 | o[this.name] = this.value || ''; 445 | } 446 | }); 447 | } else { 448 | // NOTE: THIS DOESN'T WORK FOR RADIO ELEMENTS (YET) 449 | $(":input", this).each(function() { 450 | if (o[this.name] !== undefined) { 451 | if (!o[this.name].push) { 452 | o[this.name] = [o[this.name]]; 453 | } 454 | o[this.name].push($(this).val() || ''); 455 | } else { 456 | o[this.name] = $(this).val() || ''; 457 | } 458 | }); 459 | } 460 | 461 | }); 462 | 463 | return o; 464 | }; 465 | 466 | $.fn.setValuesFromObject = function(obj) { 467 | // NOTE: THIS DOESN'T WORK FOR RADIO ELEMENTS (YET) 468 | var $this = this; 469 | $.each(obj, function(name, value) { 470 | $("[name='"+name+"']", $this).val(value); 471 | }); 472 | } 473 | 474 | 475 | AS.addControlGroupHighlighting = function(parent) { 476 | $(".form-group :input", parent).on("focus", function() { 477 | $(this).parents(".form-group:first").addClass("active"); 478 | }).on("blur", function() { 479 | $(this).parents(".form-group:first").removeClass("active"); 480 | }); 481 | }; 482 | 483 | 484 | // confirmation behaviour for subform-remove actions 485 | AS.confirmSubFormDelete = function(subformRemoveButtonEl, onConfirmCallback) { 486 | 487 | // Hide any others that were selected first 488 | $(".cancel-removal:visible").trigger('click'); 489 | 490 | var confirmationEl = $(AS.renderTemplate("subform_remove_confirmation_template")); 491 | confirmationEl.hide(); 492 | subformRemoveButtonEl.hide(); 493 | subformRemoveButtonEl.before(confirmationEl); 494 | confirmationEl.fadeIn(function() { 495 | $(".confirm-removal", confirmationEl).focus(); 496 | }); 497 | 498 | $(".cancel-removal", confirmationEl).click(function(event) { 499 | confirmationEl.remove(); 500 | subformRemoveButtonEl.fadeIn(); 501 | return false; 502 | }); 503 | 504 | $(".confirm-removal", confirmationEl).click(function(event) { 505 | event.preventDefault(); 506 | event.stopPropagation(); 507 | onConfirmCallback($(event.target)); 508 | return false; 509 | }); 510 | 511 | return false; 512 | }; 513 | 514 | // extra add button plugin for subrecord forms 515 | AS.initAddAsYouGoActions = function($form, $list) { 516 | if ($form.data("cardinality") === "zero_to_one") { 517 | // nothing to do here 518 | return; 519 | } 520 | 521 | // delete any existing subrecord-add-as-you-go-actions 522 | $(".subrecord-add-as-you-go-actions", $form).remove(); 523 | 524 | var $asYouGo = $("
"); 525 | $form.append($asYouGo); 526 | 527 | var numberOfSubRecords = function() { 528 | return $("> li", $list).length; 529 | }; 530 | 531 | var bindEvents = function() { 532 | $form.off("subrecordcreated.aspace").on("subrecordcreated.aspace", function() { 533 | $asYouGo.fadeIn(); 534 | }); 535 | 536 | $form.off("subrecorddeleted.aspace").on("subrecorddeleted.aspace", function() { 537 | if (numberOfSubRecords() === 0) { 538 | $asYouGo.hide(); 539 | } 540 | }); 541 | } 542 | 543 | var init = function() { 544 | if (numberOfSubRecords() === 0) { 545 | $asYouGo.hide(); 546 | } 547 | 548 | var btnsToReplicate = $(".subrecord-form-heading:first > .btn, .subrecord-form-heading:first > .custom-action > .btn", $form) 549 | btnsToReplicate = btnsToReplicate.map( function() { 550 | var $btn = $(this); 551 | if ( $btn.hasClass('show-all') && numberOfSubRecords() < 5 ) 552 | return; 553 | else 554 | return this; 555 | }); 556 | 557 | var fillToPercentage = 100; // full width 558 | 559 | btnsToReplicate.each(function() { 560 | var $btn = $(this); 561 | 562 | var $a = $("+"); 563 | var btnText = $btn.val().length ? $btn.val() : $btn.text(); 564 | $a.css("width", Math.floor(fillToPercentage / btnsToReplicate.length) + "%"); 565 | 566 | if (btnsToReplicate.length > 1) { 567 | // we need to differentiate the links 568 | $a.text(btnText); 569 | $a.addClass("has-label"); 570 | if ($btn.hasClass('show-all')) { $a.addClass('show-all'); } 571 | } else { 572 | // just add a title and we'll have a '+' 573 | $a.attr("title", btnText); 574 | } 575 | 576 | $a.click(function(e) { 577 | e.preventDefault(); 578 | e.stopPropagation(); 579 | 580 | $btn.trigger("click"); 581 | }); 582 | $asYouGo.append($a); 583 | }); 584 | 585 | bindEvents(); 586 | } 587 | 588 | init(); 589 | }; 590 | 591 | 592 | // Used by all tree layouts -- sets the initial height for the tree pane... but can 593 | // be overridden by a user's cookie value 594 | AS.DEFAULT_TREE_PANE_HEIGHT = 100; 595 | 596 | AS.resetScrollSpy = function() { 597 | // reset the scrollspy plugin 598 | // so the headers update the status of the sidebar 599 | $(document.body).removeData("scrollspy"); 600 | $(document.body).scrollspy({ 601 | target: "#archivesSpaceSidebar", 602 | offset: 20 603 | }); 604 | } 605 | 606 | AS.delayedTypeAhead = function (source, delay) { 607 | if (!delay) { 608 | delay = 200; 609 | } 610 | 611 | return (function () { 612 | var queued_requests = []; 613 | var timer = undefined; 614 | 615 | var startTimer = function () { 616 | if (timer) { 617 | clearTimeout(timer); 618 | } 619 | 620 | timer = setTimeout(function () { 621 | var last_request = queued_requests.pop(); 622 | queued_requests = []; 623 | 624 | source(last_request.query, last_request.process); 625 | }, delay); 626 | }; 627 | 628 | return { 629 | handle: function (query, callback) { 630 | queued_requests.push({query: query, process: callback}); 631 | startTimer(); 632 | } 633 | }; 634 | }()); 635 | }; 636 | 637 | 638 | AS.prefixed_cookie = function(cookie_name, value) { 639 | var args = Array.prototype.slice.call(arguments, 0); 640 | args[0] = COOKIE_PREFIX + '_' + args[0]; 641 | return $.cookie.apply(this, args); 642 | }; 643 | 644 | 645 | // Sub Record Sorting 646 | AS.initSubRecordSorting = function($list) { 647 | var $subform = $list.closest(".subrecord-form"); 648 | if ($subform.data("cardinality") === "zero_to_one" 649 | || $subform.data("sorting") === "disabled") { 650 | // nothing to do here 651 | return; 652 | } 653 | 654 | if ($list.length) { 655 | $list.children("li").each(function() { 656 | var $child = $(this); 657 | if (!$child.hasClass("sort-enabled")) { 658 | var $handle = $("
"); 659 | if ($list.parent().hasClass("controls")) { 660 | $handle.addClass("inline"); 661 | } 662 | $(this).append($handle); 663 | $(this).addClass("sort-enabled"); 664 | } 665 | }); 666 | 667 | if ($list.data("sortable")) { 668 | $list.sortable("destroy"); 669 | } 670 | 671 | $list.sortable({ 672 | items: 'li', 673 | handle: ' > .drag-handle', 674 | forcePlaceholderSize: true, 675 | forceHelperSize: true, 676 | placeholder: "sortable-placeholder", 677 | tolerance: "pointer", 678 | helper: "clone" 679 | }); 680 | 681 | $list.off("sortupdate").on("sortupdate", function() { 682 | $("form.aspace-record-form").triggerHandler("formchanged.aspace"); 683 | }); 684 | } 685 | } 686 | 687 | // Add confirmation btn behaviour 688 | $(function() { 689 | $.fn.initConfirmationAction = function() { 690 | $(this).each(function() { 691 | 692 | var $this = $(this); 693 | 694 | if ($this.hasClass("initialised")) { 695 | return; 696 | } 697 | 698 | $this.addClass("initialised"); 699 | 700 | var template_data = { 701 | message: $this.data("message") || "", 702 | title: $this.data("title") || "Are you sure?", 703 | confirm_label: $this.data("confirm-btn-label") || false, 704 | confirm_class: $this.data("confirm-btn-class") || false 705 | }; 706 | 707 | var onClick = function(event) { 708 | event.preventDefault(); 709 | event.stopImmediatePropagation(); 710 | 711 | AS.openCustomModal("confirmChangesModal", template_data.title , AS.renderTemplate("confirmation_modal_template", template_data), null, {}, $this); 712 | $("#confirmButton", "#confirmChangesModal").click(function() { 713 | $(".btn", "#confirmChangesModal").attr("disabled", "disabled"); 714 | 715 | var $form = $("
") 716 | .attr("action", $this.data("target") || $this.attr("href")) 717 | .attr("accept-charset", "UTF-8") 718 | .attr("method", $this.data("method") || "post"); 719 | 720 | if ($this.data("authenticity_token")) { 721 | var $h = $(""); 722 | $h.attr("name", "authenticity_token").val($this.data("authenticity_token")); 723 | $form.append($h); 724 | } 725 | 726 | if ($this.data("form-data")) { 727 | $.each($this.data("form-data"), function (name, value) { 728 | if (typeof value === "object") { 729 | $.each(value, function(i, val) { 730 | var $h = $(""); 731 | $h.attr("name", name+"[]").val(val); 732 | $form.append($h); 733 | }); 734 | } else { 735 | var $h = $(""); 736 | $h.attr("name", name).val(value); 737 | $form.append($h); 738 | } 739 | }); 740 | } 741 | 742 | $(document.body).append($form); 743 | 744 | $form.submit(); 745 | }); 746 | } 747 | 748 | $this.click(onClick); 749 | }) 750 | }; 751 | 752 | $(document).ready(function() { 753 | $(document).bind("loadedrecordform.aspace", function(event, $container) { 754 | $(".btn[data-confirmation]:not(.initialised)", $container).initConfirmationAction(); 755 | }); 756 | 757 | $(".btn[data-confirmation]:not(.initialised)").initConfirmationAction(); 758 | }); 759 | }); 760 | 761 | // Set up some subrecord specific event bindings 762 | $(document).bind("subrecordcreated.aspace", function(event, object_name, newFormEl) { 763 | newFormEl.parents(".subrecord-form:first").triggerHandler("subrecordcreated.aspace"); 764 | }); 765 | $(document).bind("subrecorddeleted.aspace", function(event, formEl) { 766 | formEl.triggerHandler("subrecorddeleted.aspace"); 767 | }); 768 | 769 | // Global AJAX setup 770 | $(function() { 771 | $.ajaxSetup({ 772 | beforeSend: function(xhr, settings) { 773 | // if it's a POST, lets make sure the CSRF token is passed through 774 | if (settings.type === "POST") { 775 | xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content')); 776 | } 777 | } 778 | }); 779 | }); 780 | 781 | // Add close action to all alerts 782 | $(function() { 783 | 784 | var handleCloseAlert = function(event) { 785 | event.stopPropagation(); 786 | event.preventDefault(); 787 | 788 | var $hideAlert = $(this); 789 | 790 | $hideAlert.hide().closest(".alert").slideUp(function() { 791 | $hideAlert.show(); 792 | }); 793 | }; 794 | 795 | $.fn.initCloseAlertAction = function(event, $container) { 796 | $(this).each(function() { 797 | var $alert = $(this); 798 | 799 | // add a close icon to the alert 800 | var $close = $("").attr("href", "javascript:void(0);").addClass("hide-alert"); 801 | $close.append($("").addClass("glyphicon glyphicon-remove")); 802 | $close.click(handleCloseAlert); 803 | 804 | $alert.prepend($close); 805 | $alert.addClass("with-hide-alert"); 806 | }); 807 | }; 808 | 809 | $(document).ready(function() { 810 | $(document).bind("loadedrecordform.aspace", function(event, $container) { 811 | $(".alert", $container).initCloseAlertAction(); 812 | }); 813 | 814 | $(".alert:not(.with-hide-alert)").initCloseAlertAction(); 815 | }); 816 | }); 817 | 818 | // shortcuts 819 | $(function() { 820 | var initFormShortcuts = function() { 821 | var $form = $(this); 822 | 823 | }; 824 | 825 | $(document).bind('keydown', 'shift+/', function() { 826 | if (!$('#ASModal').length) { 827 | AS.openAjaxModal(APP_PATH + "shortcuts"); 828 | } 829 | 830 | }); 831 | 832 | $(document).bind('keydown', 'esc', function() { 833 | if ($('#ASModal').length) { 834 | $('#ASModal').modal('hide').data('bs.modal', null); 835 | } 836 | }); 837 | 838 | $(document).bind('keydown', 'ctrl+x', function() { 839 | $(document).trigger("formclosed.aspace"); 840 | }); 841 | 842 | $(document).bind('keydown', 'shift+b', function() { 843 | $('li.browse-container a.dropdown-toggle').trigger('click.bs.dropdown'); 844 | }); 845 | 846 | $(document).bind('keydown', 'shift+c', function() { 847 | $('li.create-container a.dropdown-toggle').trigger('click.bs.dropdown'); 848 | }); 849 | 850 | $(window).bind('keydown', function(event) { 851 | if (event.ctrlKey || event.metaKey) { 852 | switch (String.fromCharCode(event.which).toLowerCase()) { 853 | case 's': 854 | event.preventDefault(); 855 | console.log('ctrl-s'); 856 | break; 857 | } 858 | } 859 | }); 860 | 861 | var traverseMenuDown = function() { 862 | var $current = $(this).find('ul li.active'); 863 | var $next = $current.length ? $current.next() : $(this).find('li:first'); 864 | 865 | if ($next.length){ 866 | $next.addClass('active'); 867 | $current.removeClass('active'); 868 | } 869 | }; 870 | 871 | var traverseMenuUp = function() { 872 | var $current = $(this).find('ul li.active'); 873 | var $next = $current.length ? $current.prev() : $(this).find('li:last'); 874 | 875 | if ($next.length){ 876 | $next.addClass('active'); 877 | $current.removeClass('active'); 878 | } 879 | }; 880 | 881 | var clickActive = function(e) { 882 | e.preventDefault(); 883 | e.stopPropagation(); 884 | var $active = $(this).find('ul li.active'); 885 | if ($active.length) { 886 | $active.find('a:first')[0].click(); 887 | } 888 | }; 889 | 890 | 891 | $('li.dropdown').on({ 892 | 'shown.bs.dropdown': function() { 893 | $(this).bind("keydown", 'down', traverseMenuDown); 894 | $(this).bind("keydown", 'up', traverseMenuUp); 895 | $(this).bind("keydown", 'return', clickActive); 896 | }, 897 | 'hide.bs.dropdown': function() { 898 | $(this).unbind("keydown", traverseMenuDown); 899 | $(this).unbind("keydown", traverseMenuUp); 900 | $(this).unbind('keydown', clickActive); 901 | } 902 | }); 903 | 904 | }); 905 | 906 | $(function() { 907 | $realInputField = $('#real_file') 908 | 909 | # drop just the filename in the display field 910 | $realInputField.change -> 911 | $('#file-display').val $(@).val().replace(/^.*[\\\/]/, '') 912 | 913 | # trigger the real input field click to bring up the file selection dialog 914 | $('#upload-btn').click -> 915 | $realInputField.click() 916 | } --------------------------------------------------------------------------------